libs/pfe-connector/src/lib/frame/pfe-frame-navigation.service.ts
No results matching.
Properties |
|
Methods |
constructor()
|
| cancel |
cancel()
|
|
Inherited from
TalyFrameNavigationService
|
|
Returns :
void
|
| getOfferCode$ | ||||||
getOfferCode$(stateKey: string)
|
||||||
|
Inherited from
TalyFrameNavigationService
|
||||||
|
Parameters :
Returns :
Observable<string | undefined>
|
| gotoErrorPage | ||||||
gotoErrorPage(errorResponse?: HttpErrorResponse)
|
||||||
|
Inherited from
TalyFrameNavigationService
|
||||||
|
Method to navigate to the error page
Parameters :
Returns :
void
|
| gotoPage |
gotoPage(pageId: string, rewindHistory?: boolean)
|
|
Inherited from
TalyFrameNavigationService
|
|
Returns :
void
|
| gotoSection |
gotoSection(sectionId: string, rewindHistory?: boolean)
|
|
Inherited from
TalyFrameNavigationService
|
|
Method to navigate to a particular section with given section id Implement this function. It might not be always the first page of the section. It might change based on the conditions in pfe navigation configuration as well.
Returns :
void
|
| init |
init()
|
|
Returns :
void
|
| initApplicationBusy$ |
initApplicationBusy$()
|
|
Returns :
void
|
| initApplicationBusyMessage$ | ||||||
initApplicationBusyMessage$(busy$: Observable
|
||||||
|
Parameters :
Returns :
void
|
| navigateBack |
navigateBack()
|
|
Inherited from
TalyFrameNavigationService
|
|
Returns :
void
|
| Async navigateHome |
navigateHome()
|
|
Inherited from
TalyFrameNavigationService
|
|
Returns :
any
|
| Async navigateNext |
navigateNext()
|
|
Inherited from
TalyFrameNavigationService
|
|
Returns :
any
|
| saveOffer |
saveOffer()
|
|
Inherited from
TalyFrameNavigationService
|
|
Returns :
void
|
| Public disableTitleUpdates |
disableTitleUpdates()
|
|
Inherited from
TalyFrameNavigationService
|
|
Returns :
void
|
| Public setSections | ||||||||
setSections(sections: SectionConfig)
|
||||||||
|
Inherited from
TalyFrameNavigationService
|
||||||||
|
Parameters :
Returns :
void
|
| Public setTranslatedTitle | ||||||
setTranslatedTitle(translatedTitle: string)
|
||||||
|
Inherited from
TalyFrameNavigationService
|
||||||
|
Parameters :
Returns :
void
|
| Readonly APPLICATION_BUSY_DELAY |
Type : number
|
Default value : 1000
|
| applicationBusy$ |
Type : Observable<boolean>
|
|
Inherited from
TalyFrameNavigationService
|
| applicationBusyMessage$ |
Type : Observable<ExtendedDisplayMessage | undefined>
|
|
Inherited from
TalyFrameNavigationService
|
| Readonly backClicked$ |
Type : unknown
|
Default value : this.backClickedSubject.asObservable()
|
|
Inherited from
TalyFrameNavigationService
|
| Readonly MINIMUM_MESSAGE_TIME |
Type : number
|
Default value : 1000
|
| navigationConfig |
Type : unknown
|
Default value : inject<WritableSignal<NavigationConfig>>(TALY_FRAME_NAVIGATION_CONFIG)
|
| Readonly nextClicked$ |
Type : unknown
|
Default value : this.nextClickedSubject.asObservable()
|
|
Inherited from
TalyFrameNavigationService
|
| pageActionStatus$ |
Type : Observable<PageActionStatus>
|
|
Inherited from
TalyFrameNavigationService
|
| currentSectionStates$ |
Type : unknown
|
Default value : this._currentSectionStates$.asObservable()
|
|
Inherited from
TalyFrameNavigationService
|
| history$ |
Type : unknown
|
Default value : this._history$.asObservable()
|
|
Inherited from
TalyFrameNavigationService
|
| visibleSections$ |
Type : unknown
|
Default value : this._visibleSections$.asObservable()
|
|
Inherited from
TalyFrameNavigationService
|
import {
ExpressionCondition,
ExtendedDisplayMessage,
JSON_PATH_REGEX,
PfeActionsService,
PfeBusinessService,
PfeConditionsService,
PfeConfigurationService,
PfeNavigationService,
PfeNavigationUtilService,
PfeServiceActivatorService,
PfeStateService,
ServiceActivatorProgressDetails
} from '@allianz/ngx-pfe';
import {
NavigationConfig,
PageActionStatus,
PageHistory,
SectionConfig,
TALY_FRAME_NAVIGATION_CONFIG,
TalyFrameNavigationService
} from '@allianz/taly-core/frame';
import { BackLinkUtilsService } from '@allianz/taly-core/web-components';
import { HttpErrorResponse } from '@angular/common/http';
import {
ApplicationRef,
effect,
inject,
Injectable,
Renderer2,
RendererFactory2,
WritableSignal
} from '@angular/core';
import { BehaviorSubject, combineLatest, EMPTY, merge, Observable, of, Subject, timer } from 'rxjs';
import {
debounce,
debounceTime,
distinctUntilChanged,
filter,
map,
switchMap,
take,
tap,
timeInterval
} from 'rxjs/operators';
import { createPageActionStatusObservable } from './page-action-status';
import { TalyStickyService, TalyPageDataService, TalyPageService } from '@allianz/taly-core';
export const PFE_HISTORY_KEY = 'pfeHistory';
@Injectable()
export class PfeFrameNavigationService extends TalyFrameNavigationService {
private pfeBusinessService = inject(PfeBusinessService);
private pfeConditionsService = inject(PfeConditionsService);
private pfeConfigurationService = inject(PfeConfigurationService);
private pfeStateService = inject(PfeStateService);
private pfeServiceActivatorService = inject(PfeServiceActivatorService);
private pfeNavigationService = inject(PfeNavigationService);
private talyPageService = inject(TalyPageService);
private talyPageDataService = inject(TalyPageDataService);
navigationConfig = inject<WritableSignal<NavigationConfig>>(TALY_FRAME_NAVIGATION_CONFIG);
private backLinkUtilsService = inject(BackLinkUtilsService);
private actionsService = inject(PfeActionsService);
private pfeNavigationUtilService = inject(PfeNavigationUtilService);
pageActionStatus$!: Observable<PageActionStatus>;
applicationBusy$!: Observable<boolean>;
applicationBusyMessage$!: Observable<ExtendedDisplayMessage | undefined>;
readonly APPLICATION_BUSY_DELAY = 1000;
readonly MINIMUM_MESSAGE_TIME = 1000;
private readonly nextClickedSubject = new Subject<void>();
readonly nextClicked$ = this.nextClickedSubject.asObservable();
private readonly backClickedSubject = new Subject<void>();
readonly backClicked$ = this.backClickedSubject.asObservable();
private nextButtonDisabledSubject = new BehaviorSubject<boolean>(false);
private applicationRef = inject(ApplicationRef);
private talyStickyService = inject(TalyStickyService);
private renderer!: Renderer2;
private rendererFactory = inject(RendererFactory2);
constructor() {
super();
this.init();
this.renderer = this.rendererFactory.createRenderer(null, null);
effect(() => {
this.nextButtonDisabledSubject.next(
this.navigationConfig().nextButtonDisabledWhenPageInvalid ?? false
);
});
}
init() {
createPfeHistoryObservable(this.pfeStateService)
.pipe(
tap((value) => {
this.updateHistory(value.history);
})
)
.subscribe();
this.pageActionStatus$ = this.nextButtonDisabledSubject.pipe(
switchMap((nextButtonDisabledWhenPageInvalid) =>
createPageActionStatusObservable(
this.pfeServiceActivatorService,
this.pfeNavigationService,
this.pfeConfigurationService,
this.pfeBusinessService,
this.backLinkUtilsService,
this.talyPageDataService,
nextButtonDisabledWhenPageInvalid
)
)
);
this.initApplicationBusy$();
}
async navigateNext() {
this.nextClickedSubject.next();
function hasErrorNotification() {
return document.querySelector('.error-notification-wrapper');
}
const scrollToTopOfFrame = () => {
const frameContent = document.querySelector('#frameContent');
if (frameContent) {
const scrollMargin = this.talyStickyService.getStickyElementHeights();
this.renderer.setStyle(frameContent, 'scroll-margin-top', `${scrollMargin}px`);
frameContent.scrollIntoView({ behavior: 'smooth' });
}
};
// WHEN PAGE IS NOT COMPLETE
if (!this.pfeBusinessService.pageStatus$.value) {
if (this.navigationConfig().nextButtonDisabledWhenPageInvalid) {
return;
}
if (hasErrorNotification()) {
scrollToTopOfFrame();
return;
}
this.talyPageService.onNextPageRequested();
return;
}
// WHEN PAGE IS COMPLETE
// forward the option doNotActuallyNavigate = true to prevent PFE from navigating
// the actual navigation will be done once TALY makes sure that there is no error notification on a page
// the pfeNavigationService will take care of executing the "onPageLeave" service activator and action.
const pageConfig = await this.pfeNavigationService.navigateNext(true);
// trigger the notification component to re-render after the actions are executed
this.applicationRef.tick();
if (hasErrorNotification()) {
scrollToTopOfFrame();
// Normally, PFE takes care of sending the `navigationInProgress$.next(false)` when a navigation is success or cancelled
// But now that we handle the navigation logic within TALY, this has to be taken care of manually
// Otherwise, the spinner will keep running and block the entire page.
this.pfeNavigationService.navigationInProgress$.next(false);
return;
}
// PFE returns no page config when the navigation was intentionally stopped
// (e.g. an "onPageLeave" action performed an external redirect like OAUTH_AUTHORIZE).
// Genuine errors are already handled by PFE before it returns, so we just stop here
// instead of calling navigateRelative with an undefined page id (which would throw NG04008 error).
if (!pageConfig?.pageId) {
return;
}
this.pfeNavigationUtilService.navigateRelative(pageConfig.pageId);
}
navigateBack() {
this.backClickedSubject.next();
this.pfeBusinessService.navigateBack();
}
async navigateHome() {
const firstPageId = await this.pfeBusinessService.getFirstPage();
if (!firstPageId) {
throw new Error('Trying to navigate to the Homepage: PFE returned an undefined first page.');
}
this.gotoPage(firstPageId);
}
gotoPage(pageId: string, rewindHistory?: boolean): void {
if (this.pfeBusinessService.currentPageId$.value === pageId) {
return;
}
if (!rewindHistory) {
this.pfeBusinessService.navigateToPage(pageId);
} else {
const rewindHistoryAction = {
type: 'PFE_REWIND_HISTORY',
pageId
};
this.actionsService.executeAction(rewindHistoryAction).then(() => {
this.pfeBusinessService.navigateToPage(pageId);
});
}
}
/**
* Method to navigate to a particular section with given section id
*
* @todo Implement this function. It might not be always the first
* page of the section. It might change based on the conditions in
* pfe navigation configuration as well.
*/
gotoSection(sectionId: string, rewindHistory?: boolean): void {
const sections = this._visibleSections$.value;
const section = sections.get(sectionId);
const pages = section?.pages || [];
if (pages?.length === 0) {
throw new Error(
`Could not find proper configuration to navigate to the section ${sectionId}`
);
}
// Navigating to first page of the section
this.gotoPage(pages[0], rewindHistory);
}
/**
* Method to navigate to the error page
*
*/
gotoErrorPage(errorResponse?: HttpErrorResponse) {
this.pfeBusinessService.navigateToErrorPage(errorResponse);
}
saveOffer(): void {
this.actionsService.executeAction({
type: 'TALY_SAVE_OFFER'
});
}
cancel(): void {
this.actionsService.executeAction({
type: 'TALY_CANCEL_OPERATION'
});
}
getOfferCode$(stateKey: string): Observable<string | undefined> {
return this.pfeBusinessService.getObservableForExpressionKey(stateKey, true);
}
initApplicationBusy$(): void {
const busy$: Observable<boolean> = combineLatest([
this.pfeServiceActivatorService.serviceActivatorCallInProgress$,
this.pfeNavigationService.navigationInProgress$
]).pipe(
map(
([serviceActivatorCallInProgress, navigationInProgress]) =>
serviceActivatorCallInProgress || navigationInProgress
),
distinctUntilChanged()
);
this.applicationBusy$ = busy$.pipe(
debounce((busy) => (busy ? timer(this.APPLICATION_BUSY_DELAY) : timer(0)))
);
this.initApplicationBusyMessage$(busy$);
}
initApplicationBusyMessage$(busy$: Observable<boolean>): void {
const message$ = this.pfeServiceActivatorService.serviceActivatorCallInProgressDetails$.pipe(
filter((configs: ServiceActivatorProgressDetails[]) => Boolean(configs?.length)),
map(
(configs: ServiceActivatorProgressDetails[]) =>
// If multiple service activators are running at the same time
// Use the first one that has displayMessage config
configs.find((config) => config.displayMessage)?.displayMessage
)
);
// Emit a new message every 1 second
const minimumDisplay$ = timer(0, this.MINIMUM_MESSAGE_TIME).pipe(
switchMap(() => message$.pipe(take(1)))
);
// Switch a message if it's been displayed longer than 1 second
const messageSwitcher$ = message$.pipe(
timeInterval(),
filter(({ interval }) => interval > this.MINIMUM_MESSAGE_TIME),
map(({ value }) => value)
);
const applicationBusyMessage$ = merge(minimumDisplay$, messageSwitcher$).pipe(
distinctUntilChanged()
);
this.applicationBusyMessage$ = busy$.pipe(
switchMap((isBusy) => {
if (isBusy) {
return applicationBusyMessage$;
}
return EMPTY;
})
);
}
protected getVisibleSections$(sections: SectionConfig): Observable<SectionConfig> {
const conditionalSections = Array.from(sections).filter(
([, section]) => section.visibleIf !== undefined
);
if (conditionalSections.length === 0) {
return of(sections);
}
const conditions$: Observable<[sectionId: string, stateValue: boolean]>[] =
conditionalSections.map(([sectionId, section]) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (JSON_PATH_REGEX.test(section.visibleIf!)) {
return this.createSectionObservableWithPfeExpressionCondition(
sectionId,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
section.visibleIf!
);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.createSectionObservableWithJsonPathExpression(sectionId, section.visibleIf!);
}
});
return combineLatest(conditions$).pipe(
map((conditions) => {
const conditionMap = new Map(conditions);
return filterSections(sections, conditionMap);
})
);
}
private createSectionObservableWithPfeExpressionCondition(
sectionId: string,
visibleIf: string
): Observable<[sectionId: string, stateValue: boolean]> {
const condition: ExpressionCondition[] = [{ expression: visibleIf }];
// get all property keys which are in curly brackets (e.g. "{key1} == {key2}" => ["key1", "key2"])
const propertyKeys = visibleIf.match(JSON_PATH_REGEX)?.map((match) => match.slice(1, -1)) || [];
return merge(
...propertyKeys.map((key) => this.pfeBusinessService.getObservableForExpressionKey(key, true))
).pipe(
map(() => [
sectionId,
this.pfeConditionsService.evaluateConditions(condition, this.pfeStateService.getFullState())
])
);
}
private createSectionObservableWithJsonPathExpression(
sectionId: string,
visibleIf: string
): Observable<[sectionId: string, stateValue: boolean]> {
return this.pfeBusinessService
.getObservableForExpressionKey(visibleIf, true)
.pipe(map((stateValue) => [sectionId, Boolean(stateValue)]));
}
}
function createPfeHistoryObservable(pfeStateService: PfeStateService): Observable<PageHistory> {
return pfeStateService.getObservableForKey(PFE_HISTORY_KEY).pipe(
debounceTime(50),
map((history) => {
let currentPage = null;
if (history.length > 0) {
currentPage = history[history.length - 1];
}
return { history, currentPage };
})
);
}
function filterSections(
allSections: SectionConfig,
conditions: Map<string, boolean>
): SectionConfig {
return new Map(
Array.from(allSections).filter(([sectionId]) => {
const hasCondition = conditions.has(sectionId);
const conditionIsTrue = conditions.get(sectionId) === true;
return !hasCondition || conditionIsTrue;
})
);
}