File

libs/pfe-connector/src/lib/frame/pfe-frame-navigation.service.ts

Extends

TalyFrameNavigationService

Index

Properties
Methods

Constructor

constructor(pfeBusinessService: PfeBusinessService, pfeConditionsService: PfeConditionsService, pfeConfigurationService: PfeConfigurationService, pfeStateService: PfeStateService, pfeServiceActivatorService: PfeServiceActivatorService, pfeNavigationService: PfeNavigationService, talyPageService: TalyPageService, talyPageDataService: TalyPageDataService, navigationConfig: NavigationConfig, backLinkUtilsService: BackLinkUtilsService, actionsService: PfeActionsService, pfeNavigationUtilService: PfeNavigationUtilService)
Parameters :
Name Type Optional
pfeBusinessService PfeBusinessService No
pfeConditionsService PfeConditionsService No
pfeConfigurationService PfeConfigurationService No
pfeStateService PfeStateService No
pfeServiceActivatorService PfeServiceActivatorService No
pfeNavigationService PfeNavigationService No
talyPageService TalyPageService No
talyPageDataService TalyPageDataService No
navigationConfig NavigationConfig No
backLinkUtilsService BackLinkUtilsService No
actionsService PfeActionsService No
pfeNavigationUtilService PfeNavigationUtilService No

Methods

cancel
cancel()
Returns : void
getOfferCode$
getOfferCode$(stateKey: string)
Parameters :
Name Type Optional
stateKey string No
gotoErrorPage
gotoErrorPage(errorResponse?: HttpErrorResponse)

Method to navigate to the error page

Parameters :
Name Type Optional
errorResponse HttpErrorResponse Yes
Returns : void
gotoPage
gotoPage(pageId: string)
Parameters :
Name Type Optional
pageId string No
Returns : void
gotoSection
gotoSection(sectionId: string)

Method to navigate to a particular section with given section id

page of the section. It might change based on the conditions in pfe navigation configuration as well.

Parameters :
Name Type Optional
sectionId string No
Returns : void
init
init()
Returns : void
initApplicationBusy$
initApplicationBusy$()
Returns : void
initApplicationBusyMessage$
initApplicationBusyMessage$(busy$: Observable)
Parameters :
Name Type Optional
busy$ Observable<boolean> No
Returns : void
navigateBack
navigateBack()
Returns : void
Async navigateHome
navigateHome()
Returns : any
Async navigateNext
navigateNext()
Returns : any
saveOffer
saveOffer()
Returns : void
Public disableTitleUpdates
disableTitleUpdates()
Returns : void
Public setSections
setSections(sections: SectionConfig)
Parameters :
Name Type Optional Default value
sections SectionConfig No new Map()
Returns : void
Public setTranslatedTitle
setTranslatedTitle(translatedTitle: string)
Parameters :
Name Type Optional
translatedTitle string No
Returns : void

Properties

Readonly APPLICATION_BUSY_DELAY
Type : number
Default value : 1000
applicationBusy$
Type : Observable<boolean>
applicationBusyMessage$
Type : Observable<ExtendedDisplayMessage | undefined>
Readonly MINIMUM_MESSAGE_TIME
Type : number
Default value : 1000
Public navigationConfig
Type : NavigationConfig
Decorators :
@Inject(TALY_FRAME_NAVIGATION_CONFIG)
pageActionStatus$
Type : Observable<PageActionStatus>
currentSectionStates$
Default value : this._currentSectionStates$.asObservable()
history$
Default value : this._history$.asObservable()
visibleSections$
Default value : this._visibleSections$.asObservable()
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-common/frame';
import { BackLinkUtilsService } from '@allianz/taly-common/web-components';
import { TalyPageDataService, TalyPageService, TalyStickyService } from '@allianz/taly-core';
import { HttpErrorResponse } from '@angular/common/http';
import {
  ApplicationRef,
  Inject,
  inject,
  Injectable,
  Renderer2,
  RendererFactory2
} from '@angular/core';
import { combineLatest, EMPTY, merge, Observable, of, timer } from 'rxjs';
import {
  debounce,
  debounceTime,
  distinctUntilChanged,
  filter,
  map,
  switchMap,
  take,
  tap,
  timeInterval
} from 'rxjs/operators';
import { createPageActionStatusObservable } from './page-action-status';

export const PFE_HISTORY_KEY = 'pfeHistory';

@Injectable()
export class PfeFrameNavigationService extends TalyFrameNavigationService {
  pageActionStatus$!: Observable<PageActionStatus>;
  applicationBusy$!: Observable<boolean>;
  applicationBusyMessage$!: Observable<ExtendedDisplayMessage | undefined>;

  readonly APPLICATION_BUSY_DELAY = 1000;
  readonly MINIMUM_MESSAGE_TIME = 1000;

  private applicationRef = inject(ApplicationRef);
  private talyStickyService = inject(TalyStickyService);
  private renderer!: Renderer2;
  private rendererFactory = inject(RendererFactory2);

  constructor(
    private pfeBusinessService: PfeBusinessService,
    private pfeConditionsService: PfeConditionsService,
    private pfeConfigurationService: PfeConfigurationService,
    private pfeStateService: PfeStateService,
    private pfeServiceActivatorService: PfeServiceActivatorService,
    private pfeNavigationService: PfeNavigationService,
    private talyPageService: TalyPageService,
    private talyPageDataService: TalyPageDataService,
    @Inject(TALY_FRAME_NAVIGATION_CONFIG) public navigationConfig: NavigationConfig,
    private backLinkUtilsService: BackLinkUtilsService,
    private actionsService: PfeActionsService,
    private pfeNavigationUtilService: PfeNavigationUtilService
  ) {
    super();
    this.init();
    this.renderer = this.rendererFactory.createRenderer(null, null);
  }

  init() {
    createPfeHistoryObservable(this.pfeStateService)
      .pipe(
        tap((value) => {
          this.updateHistory(value.history);
        })
      )
      .subscribe();

    this.pageActionStatus$ = createPageActionStatusObservable(
      this.pfeServiceActivatorService,
      this.pfeNavigationService,
      this.pfeConfigurationService,
      this.pfeBusinessService,
      this.backLinkUtilsService,
      this.talyPageDataService,
      this.navigationConfig.nextButtonDisabledWhenPageInvalid ?? false
    );

    this.initApplicationBusy$();
  }

  async navigateNext() {
    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;
    }
    this.pfeNavigationUtilService.navigateRelative(pageConfig?.pageId as string);
  }

  navigateBack() {
    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): void {
    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): 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]);
  }

  /**
   * 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;
    })
  );
}

results matching ""

    No results matching ""