File

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

Extends

TalyFrameNavigationService

Index

Properties
Methods

Constructor

constructor()

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
navigationConfig
Default value : inject<WritableSignal<NavigationConfig>>(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,
  effect,
  inject,
  Injectable,
  Renderer2,
  RendererFactory2,
  WritableSignal
} from '@angular/core';
import { BehaviorSubject, 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 {
  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 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() {
    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 ""