UNPKG

@o3r/components

Version:

This module contains component-related features (Component replacement, CMS compatibility, helpers, pipes, debugging developer tools...) It comes with an integrated ng builder to help you generate components compatible with Otter features (CMS integration

300 lines (293 loc) 18 kB
import * as i0 from '@angular/core'; import { inject, Injector, Injectable, NgModule } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Store, select } from '@ngrx/store'; import { DynamicContentService } from '@o3r/dynamic-content'; import { LocalizationService } from '@o3r/localization'; import { LoggerService } from '@o3r/logger'; import { Subject, map, startWith, distinctUntilChanged, of, combineLatest, withLatestFrom, firstValueFrom, EMPTY } from 'rxjs'; import { selectPlaceholderTemplateEntities, selectPlaceholderRequestEntities, deletePlaceholderTemplateEntity, setPlaceholderTemplateEntity, updatePlaceholderRequestEntity, setPlaceholderRequestEntityFromUrl, selectPlaceholderRequestEntityUsage, failPlaceholderRequestEntity, cancelPlaceholderRequest, PlaceholderRequestStoreModule, PlaceholderTemplateStoreModule } from '@o3r/components'; import * as i1 from '@ngrx/effects'; import { Actions, createEffect, ofType, EffectsModule } from '@ngrx/effects'; import { fromApiEffectSwitchMapById } from '@o3r/core'; import { RulesEngineRunnerService } from '@o3r/rules-engine'; import { JSONPath } from 'jsonpath-plus'; import { map as map$1, distinctUntilChanged as distinctUntilChanged$1, switchMap, take } from 'rxjs/operators'; /** ActionUpdatePlaceholderBlock */ const RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE = 'UPDATE_PLACEHOLDER'; /** * Service to handle async PlaceholderTemplate actions */ class PlaceholderRulesEngineActionHandler { constructor() { this.logger = inject(LoggerService); this.injector = inject(Injector); this.placeholdersActions$ = new Subject(); /** @inheritdoc */ this.supportingActions = [RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE]; const store = inject(Store); const translateService = inject(LocalizationService, { optional: true }); const lang$ = translateService ? translateService.getTranslateService().onLangChange.pipe(map(({ lang }) => lang), startWith(translateService.getCurrentLanguage()), distinctUntilChanged()) : of(null); const filteredActions$ = combineLatest([ lang$, this.placeholdersActions$.pipe(distinctUntilChanged((prev, next) => JSON.stringify(prev) === JSON.stringify(next))) ]).pipe(withLatestFrom(combineLatest([store.pipe(select(selectPlaceholderTemplateEntities)), store.pipe(select(selectPlaceholderRequestEntities))])), map(([langAndTemplatesUrls, storedPlaceholdersAndRequests]) => { const [lang, placeholderActions] = langAndTemplatesUrls; const storedPlaceholders = storedPlaceholdersAndRequests[0] || {}; const storedPlaceholderRequests = storedPlaceholdersAndRequests[1] || {}; const placeholderNewRequests = []; // Stores all raw Urls used from the current engine execution const usedUrls = {}; // Get all Urls that needs to be resolved from current rules engine output const placeholdersTemplates = placeholderActions.reduce((acc, placeholderAction) => { const placeholdersTemplateUrl = { rawUrl: placeholderAction.templateUrl, priority: placeholderAction.priority }; if (acc[placeholderAction.placeholderId]) { acc[placeholderAction.placeholderId].push(placeholdersTemplateUrl); } else { acc[placeholderAction.placeholderId] = [placeholdersTemplateUrl]; } const resolvedUrl = this.resolveUrlWithLang(placeholderAction.templateUrl, lang); // Filters duplicates and resolved urls that are already in the store if (!usedUrls[placeholderAction.templateUrl] && (!storedPlaceholderRequests[placeholderAction.templateUrl] || storedPlaceholderRequests[placeholderAction.templateUrl].resolvedUrl !== resolvedUrl)) { placeholderNewRequests.push({ rawUrl: placeholderAction.templateUrl, resolvedUrl: this.resolveUrlWithLang(placeholderAction.templateUrl, lang) }); } usedUrls[placeholderAction.templateUrl] = true; return acc; }, {}); // Urls not used anymore and not already disabled const placeholderRequestsToDisable = []; // Urls used that were disabled const placeholderRequestsToEnable = []; Object.keys(storedPlaceholderRequests).forEach((storedPlaceholderRequestRawUrl) => { const usedFromEngineIteration = usedUrls[storedPlaceholderRequestRawUrl]; const usedFromStore = (storedPlaceholderRequests && storedPlaceholderRequests[storedPlaceholderRequestRawUrl]) ? storedPlaceholderRequests[storedPlaceholderRequestRawUrl].used : false; if (!usedFromEngineIteration && usedFromStore) { placeholderRequestsToDisable.push(storedPlaceholderRequestRawUrl); } else if (usedFromEngineIteration && !usedFromStore) { placeholderRequestsToEnable.push(storedPlaceholderRequestRawUrl); } }); // Placeholder that are no longer filled by the current engine execution output will be cleared const placeholdersTemplatesToBeCleanedUp = Object.keys(storedPlaceholders) .filter((placeholderId) => !placeholdersTemplates[placeholderId]); const placeholdersTemplatesToBeSet = Object.keys(placeholdersTemplates).reduce((changedPlaceholderTemplates, placeholderTemplateId) => { // Caching if the placeholder template already exists with the same urls if (!storedPlaceholders[placeholderTemplateId] || !(JSON.stringify(storedPlaceholders[placeholderTemplateId].urlsWithPriority) === JSON.stringify(placeholdersTemplates[placeholderTemplateId]))) { changedPlaceholderTemplates.push({ id: placeholderTemplateId, urlsWithPriority: placeholdersTemplates[placeholderTemplateId] }); } return changedPlaceholderTemplates; }, []); return { placeholdersTemplatesToBeCleanedUp, placeholderRequestsToDisable, placeholderRequestsToEnable, placeholdersTemplatesToBeSet, placeholderNewRequests }; })); filteredActions$.pipe(takeUntilDestroyed()).subscribe((placeholdersUpdates) => { placeholdersUpdates.placeholdersTemplatesToBeCleanedUp.forEach((placeholderId) => store.dispatch(deletePlaceholderTemplateEntity({ id: placeholderId }))); placeholdersUpdates.placeholdersTemplatesToBeSet.forEach((placeholdersTemplateToBeSet) => { store.dispatch(setPlaceholderTemplateEntity({ entity: placeholdersTemplateToBeSet })); }); placeholdersUpdates.placeholderRequestsToDisable.forEach((placeholderRequestToDisable) => { store.dispatch(updatePlaceholderRequestEntity({ entity: { id: placeholderRequestToDisable, used: false } })); }); placeholdersUpdates.placeholderRequestsToEnable.forEach((placeholderRequestToEnable) => { store.dispatch(updatePlaceholderRequestEntity({ entity: { id: placeholderRequestToEnable, used: true } })); }); placeholdersUpdates.placeholderNewRequests.forEach((placeholderNewRequest) => { store.dispatch(setPlaceholderRequestEntityFromUrl({ resolvedUrl: placeholderNewRequest.resolvedUrl, id: placeholderNewRequest.rawUrl, call: this.retrieveTemplate(placeholderNewRequest.resolvedUrl) })); }); }); } /** * Localize the url, replacing the language marker * @param url * @param language */ resolveUrlWithLang(url, language) { if (!language && url.includes('[LANGUAGE]')) { this.logger.warn(`Missing language when trying to resolve ${url}`); } return language ? url.replace(/\[LANGUAGE]/g, language) : url; } /** * Retrieve template as json from a given url * @param url */ async retrieveTemplate(url) { const resolvedUrl$ = this.injector.get(DynamicContentService, null, { optional: true })?.getContentPathStream(url) || of(url); const fullUrl = await firstValueFrom(resolvedUrl$); return fetch(fullUrl).then((response) => response.json()); } /** @inheritdoc */ executeActions(actions) { const templates = actions.map((action) => ({ placeholderId: action.placeholderId, templateUrl: action.value, priority: action.priority || 0 })); this.placeholdersActions$.next(templates); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderRulesEngineActionHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderRulesEngineActionHandler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderRulesEngineActionHandler, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * Service to handle async PlaceholderTemplate actions */ class PlaceholderTemplateResponseEffect { constructor() { this.actions$ = inject(Actions); this.store = inject(Store); this.rulesEngineService = inject(RulesEngineRunnerService, { optional: true }); this.dynamicContentService = inject(DynamicContentService, { optional: true }); this.translationService = inject(LocalizationService, { optional: true }); /** * Set the PlaceholderRequest entity with the reply content, dispatch failPlaceholderRequestEntity if it catches a failure * Handles the rendering of the HTML content from the template and creates the combine latest from facts list if needed * Disables unused templates refresh if used is false in the store */ this.setPlaceholderRequestEntityFromUrl$ = createEffect(() => this.actions$.pipe(ofType(setPlaceholderRequestEntityFromUrl), fromApiEffectSwitchMapById((templateResponse, action) => { const facts = templateResponse.vars ? Object.entries(templateResponse.vars).filter(([, variable]) => variable.type === 'fact') : []; const factsStreamsList = this.rulesEngineService ? facts.map(([varName, fact]) => this.rulesEngineService.engine.retrieveOrCreateFactStream(fact.value).pipe(map$1((factValue) => ({ varName, factName: fact.value, // eslint-disable-next-line new-cap -- naming convention imposed by jsonpath-plus factValue: (fact.path && factValue) ? JSONPath({ wrap: false, json: factValue, path: fact.path }) : factValue })), distinctUntilChanged$1((previous, current) => previous.factValue === current.factValue))) : []; const factsStreamsList$ = factsStreamsList.length > 0 ? combineLatest(factsStreamsList) : of([]); return combineLatest([factsStreamsList$, this.store.select(selectPlaceholderRequestEntityUsage(action.id)).pipe(distinctUntilChanged$1())]).pipe(switchMap(([factsUsedInTemplate, placeholderRequestUsage]) => { if (!placeholderRequestUsage) { return EMPTY; } return this.getRenderedHTML$(templateResponse.template, templateResponse.vars, factsUsedInTemplate).pipe(map$1(({ renderedTemplate, unknownTypeFound }) => // Update instead of set because used already set by the update from url action updatePlaceholderRequestEntity({ entity: { ...templateResponse, resolvedUrl: action.resolvedUrl, id: action.id, renderedTemplate, unknownTypeFound }, requestId: action.requestId }))); })); }, (error, action) => of(failPlaceholderRequestEntity({ ids: [action.id], error, requestId: action.requestId })), (requestIdPayload, action) => cancelPlaceholderRequest({ ...requestIdPayload, id: action.id })))); } /** * Renders the html template, replacing facts and urls and localizationKeys * @param template * @param vars * @param facts */ getRenderedHTML$(template, vars, facts) { let unknownTypeFound = false; const factMapFromVars = (facts || []).reduce((mapping, fact) => { mapping[fact.varName] = fact.factValue; return mapping; }, {}); const replacements$ = []; if (vars && template) { for (const varName in vars) { if (Object.prototype.hasOwnProperty.call(vars, varName)) { const ejsVar = new RegExp(`<%=\\s*${varName}\\s*%>`, 'g'); switch (vars[varName].type) { case 'relativeUrl': { replacements$.push(this.dynamicContentService?.getMediaPathStream(vars[varName].value).pipe(take(1), map$1((value) => ({ ejsVar, value }))) || of({ ejsVar, value: vars[varName].value })); break; } case 'fullUrl': { template = template.replace(ejsVar, vars[varName].value); break; } case 'fact': { template = template.replace(ejsVar, factMapFromVars[varName] ?? ''); break; } case 'localisation': { const linkedParams = (Object.entries(vars[varName].parameters || {})).reduce((acc, [paramKey, paramValue]) => { acc[paramKey] = factMapFromVars[paramValue]; return acc; }, {}); replacements$.push(this.translationService ? this.translationService.translate(vars[varName].value, linkedParams).pipe(map$1((value) => (value ? { ejsVar, value } : null))) : of(null)); break; } default: { unknownTypeFound = true; break; } } } } } return replacements$.length > 0 && !!template ? combineLatest(replacements$).pipe(map$1((replacements) => ({ renderedTemplate: replacements.reduce((acc, replacement) => replacement ? acc.replace(replacement.ejsVar, replacement.value) : acc, template), unknownTypeFound }))) : of({ renderedTemplate: template, unknownTypeFound }); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderTemplateResponseEffect, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderTemplateResponseEffect }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderTemplateResponseEffect, decorators: [{ type: Injectable }] }); class PlaceholderRulesEngineActionModule { /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderRulesEngineActionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } /** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderRulesEngineActionModule, imports: [i1.EffectsFeatureModule, PlaceholderRequestStoreModule, PlaceholderTemplateStoreModule] }); } /** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderRulesEngineActionModule, providers: [ PlaceholderRulesEngineActionHandler ], imports: [EffectsModule.forFeature([PlaceholderTemplateResponseEffect]), PlaceholderRequestStoreModule, PlaceholderTemplateStoreModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.7", ngImport: i0, type: PlaceholderRulesEngineActionModule, decorators: [{ type: NgModule, args: [{ imports: [ EffectsModule.forFeature([PlaceholderTemplateResponseEffect]), PlaceholderRequestStoreModule, PlaceholderTemplateStoreModule ], providers: [ PlaceholderRulesEngineActionHandler ] }] }] }); /** * Generated bundle index. Do not edit. */ export { PlaceholderRulesEngineActionHandler, PlaceholderRulesEngineActionModule, RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE }; //# sourceMappingURL=o3r-components-rules-engine.mjs.map