import { NgxLoggerService } from '@allianz/ngx-logger';
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateChildFn,
CanActivateFn,
CanDeactivateFn,
CanLoadFn,
CanMatchFn,
Route,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { PfeEmptyRoutingComponent } from '../../components/pfe-empty-routing/pfe-empty-routing.component';
import { AppConfiguration } from '../../models/app-configuration.model';
import { NavOptionConfig, isNavigationNode } from '../../models/navigation-config.model';
import { NgxPfeConfig, PageConfig } from '../../models/ngx-pfe-page-config.model';
import { PfeBusinessService } from '../pfe-business-service/business.service';
import { PfeConfigurationService } from '../pfe-config-service/config-service.service';
import { LAST_VISITED_PAGE_STATE_KEY, PfeNavigationUtilService } from '../pfe-navigation-service/navigation-util.service';
import { PfeNavigationService } from '../pfe-navigation-service/navigation.service';
import { PageDefinition } from '../pfe-page-mapping-service/page-definition';
import { PfePageMappingService } from '../pfe-page-mapping-service/page-mapping-service.service';
import { pfePageConfigResolver } from './page-config-resolver';
import { PFEPageRoutingEventsHandlerService } from './page-routing-events-handler.service';
import { PfeRouteGuardsService } from './route-generator-guards.service';
import { extractQueryParams } from './utils/extract-query-params';
@Injectable()
export class PFERouteGeneratorService {
constructor(
public router: Router,
private pfeRouteGuardsService: PfeRouteGuardsService,
private pfePageMappingService: PfePageMappingService,
private pfeConfigService: PfeConfigurationService,
private pfeNavigationService: PfeNavigationService,
private pfeNavigationUtilService: PfeNavigationUtilService,
private pfeBusinessService: PfeBusinessService,
private pfePageRoutingEventsHandlerService: PFEPageRoutingEventsHandlerService,
private logger: NgxLoggerService
) {}
public generatePFERoutingConfig(pfeConfig: NgxPfeConfig): Route[] {
const routerConfig: Route[] = [];
if (pfeConfig.pagesConfiguration) {
pfeConfig.pagesConfiguration.forEach((page) => {
const pageDefinition: PageDefinition | undefined =
this.pfePageMappingService.getPageDefinition(page) ?? this.getNavigationNodePageDefinition(pfeConfig, page);
if (pageDefinition) {
/**
* The actual page route is added as a child to an empty parent route.
* The background for this is, that Angular runs route guards of every "layer" in parallel.
* That means, a configured route guard and the pfe route guard (PFEPageRoutingEventsHandlerService)
* would run in parallel.
*
* This is not desired, as the pfe route guard could trigger service activators, etc...
*
* The solution is, to setup the configured route guards in a sequence that they run first.
* This ensures, that they finish before the pfe guard. The pfe guard
* will also not be executed when the parent returns a false result.
*
* See also: https://angular.io/guide/router-tutorial-toh#milestone-5-route-guards
*
* Execution order:
* - From the deepest child towards the top: CanDeactivate, CanActivateChild
* - From the top towards the children: CanActivate
* - On module layer: CanMatch
*/
const guards = this.pfeRouteGuardsService.generateGuards(page.guards);
const route: Route = {
path: '',
canActivate: [...((guards?.canActivate as CanActivateFn[]) || [])],
canDeactivate: [this.pfePageRoutingEventsHandlerService.canDeactivate],
children: [],
};
const childRoute: Route = {
path: page.pageId,
canActivate: [this.pfePageRoutingEventsHandlerService.canActivate],
canActivateChild: [...((guards?.canActivateChild as CanActivateChildFn[]) || [])],
canDeactivate: [...((guards?.canDeactivate as CanDeactivateFn<unknown>[]) || [])],
canMatch: [...((guards?.canMatch as CanMatchFn[]) || [])],
canLoad: [...((guards?.canLoad as CanLoadFn[]) || [])],
data: {
pfePage: true,
},
resolve: {
pageConfig: pfePageConfigResolver,
},
};
if (pageDefinition.pageComponent) {
childRoute.component = pageDefinition.pageComponent;
} else {
childRoute.loadChildren = pageDefinition?.lazyPageRoute?.loadChildren;
childRoute.loadComponent = pageDefinition?.lazyPageRoute?.loadComponent;
}
route.children?.push(childRoute);
routerConfig.push(route);
} else {
this.logger.errorToServer(`generatePFERoutingConfig: PageType used in configuration not found: `, page);
}
});
}
return routerConfig;
}
public canActivate: CanActivateFn = async (activatedRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> => {
this.pfeNavigationService.navigationInProgress$.next(true);
if (activatedRoute?.routeConfig?.children) {
// Navigate to first child:
// We can be sure that the structure below the activatedRoute looks like this, as we created it ourselves in the generatePFERoutingConfig()
if (activatedRoute?.children?.[0]?.children?.length === 0) {
const firstPageOption: NavOptionConfig | undefined = await this.pfeNavigationService.getFirstPageOrErrorPageOption();
if (this.pfeNavigationUtilService.navigateExternal(firstPageOption)) {
return false;
} else if (firstPageOption?.nextPageId) {
this.router.navigate([state.url, firstPageOption.nextPageId], { replaceUrl: true });
}
return false;
}
return true;
}
let config: NgxPfeConfig;
try {
config = await this.pfeConfigService.getConfig();
} catch (error) {
// Loading the config failed. Child routes cannot be generated. Master Page will trigger an error state to show an error message
return true;
}
const pfeChildRoutes = this.generatePFERoutingConfig(config);
if (activatedRoute?.parent?.routeConfig) {
activatedRoute.parent.routeConfig.children = pfeChildRoutes;
activatedRoute.parent.routeConfig.canActivate = [this.canActivate];
}
// Try to restore the state from server:
let lastVisitedPage;
try {
await this.pfeBusinessService.restoreState();
lastVisitedPage = this.pfeBusinessService.getFullState()[LAST_VISITED_PAGE_STATE_KEY];
} catch (error) {
const errorPageID = await this.pfeNavigationUtilService.getErrorPage(this.isHttpErrorResponse(error) ? error : undefined);
if (errorPageID) {
this.navigateToPageID(state, activatedRoute, errorPageID);
} else {
throw error;
}
return false;
}
// default is to navigate to the last page.
let navigateToLastPage = true;
if (activatedRoute.url[0] && activatedRoute.url[0].path) {
const navConfig = await this.pfeConfigService.getPageNavigationConfiguration(activatedRoute.url[0].path);
navigateToLastPage =
navConfig !== undefined && navConfig.navigateToLastVisitedPage !== undefined ? navConfig.navigateToLastVisitedPage : true;
}
if (lastVisitedPage) {
// We have a restored state:
if (navigateToLastPage) {
this.navigateToPageID(state, activatedRoute, lastVisitedPage);
} else {
// Retrigger navigation with current child:
this.navigateWithURL(state.url);
}
} else if (
(activatedRoute.url.length > 0 && (await this.getAllowGlobalDirectNavigation())) ||
(await this.getAllowDirectNavigationForPage(activatedRoute))
) {
// Directly navigated to child
const pageID = activatedRoute.url[0].path;
const pageConfig = await this.pfeConfigService.getPageConfiguration(pageID);
if (!pageConfig) {
// We don't have a config for that pageID, redirect to first page:
const firstPageOption = await this.pfeNavigationService.getFirstPageOrErrorPageOption();
if (this.pfeNavigationUtilService.navigateExternal(firstPageOption)) {
return false;
} else if (firstPageOption?.nextPageId) {
this.navigateToChildOnSameLevel(state, activatedRoute, firstPageOption.nextPageId);
}
} else {
// Retrigger navigation with current child:
this.navigateWithURL(state.url);
}
} else {
// Navigate to first child:
const firstPageOption = await this.pfeNavigationService.getFirstPageOrErrorPageOption();
if (this.pfeNavigationUtilService.navigateExternal(firstPageOption)) {
return false;
} else if (firstPageOption?.nextPageId) {
this.navigateToPageID(state, activatedRoute, firstPageOption.nextPageId);
}
}
return false;
};
private navigateToPageID(state: RouterStateSnapshot, activatedRoute: ActivatedRouteSnapshot, pageID: string) {
if (activatedRoute.url.length > 0) {
this.navigateToChildOnSameLevel(state, activatedRoute, pageID);
} else {
const { path, queryParams } = extractQueryParams(state.url);
this.router.navigate([path, pageID], { replaceUrl: true, queryParams });
}
}
private navigateToChildOnSameLevel(state: RouterStateSnapshot, activatedRoute: ActivatedRouteSnapshot, pageID: string) {
const parentUrl = state.url.slice(0, state.url.indexOf(activatedRoute.url[activatedRoute.url.length - 1].path));
this.router.navigate([parentUrl, pageID], { replaceUrl: true });
}
/**
* Triggers a navigation, with a URL. Can handle query parameters correctly.
*
* We need to pay special attention to query parameters so we can deal with deep-linked
* entry URLs. We can't simply use these URLs to navigate further because they might lead
* to unidentifiable routes.
* E.g. the router won't know how to deal with `my-page?key=value`. It's not a known route
*/
private navigateWithURL(url: string) {
const { path, queryParams } = extractQueryParams(url);
this.router.navigate([path], { replaceUrl: true, queryParams });
}
private async getAllowGlobalDirectNavigation() {
const appConfig: AppConfiguration | undefined = await this.pfeConfigService.getAppConfiguration();
return appConfig?.pfeConfig?.allowDirectNavigationWithoutState;
}
/**
* Determines if it is allowed to directly navigate to this specific navigation config.
*/
private async getAllowDirectNavigationForPage(activatedRoute: ActivatedRouteSnapshot) {
if (activatedRoute.url.length > 0) {
// Directly navigated to child
const pageID = activatedRoute.url[0].path;
if (pageID) {
const pageNavConfig = await this.pfeConfigService.getPageNavigationConfiguration(pageID);
if (pageNavConfig && pageNavConfig.allowDirectNavigationWithoutState) {
return true;
}
}
}
return false;
}
private getNavigationNodePageDefinition(pfeConfig: NgxPfeConfig, page: PageConfig<string>): PageDefinition | undefined {
for (const pageNavConfig of pfeConfig.navConfiguration.pages) {
if (page.pageId === pageNavConfig.pageId && isNavigationNode(pageNavConfig)) {
return new PageDefinition(PfeEmptyRoutingComponent);
}
}
}
private isHttpErrorResponse(error: HttpErrorResponse | unknown): error is HttpErrorResponse {
return error instanceof HttpErrorResponse;
}
}