@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
JavaScript
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