Routing and navigation
Configuration
All routing is configured once, in src/app/app-routes.module.ts:
RouterModule.forRoot(generalAppRoutes, {
enableTracing: false,
scrollPositionRestoration: 'enabled',
useHash: true,
relativeLinkResolution: 'legacy',
})
| Option | Consequence |
|---|---|
|
URLs are |
|
The router restores scroll on back/forward. |
|
Angular 12 compatibility flag; relative |
|
Router tracing off; use |
Route aggregation
Each feature module exports a plain Routes array from a *-routing.routes.ts
file. The root module imports them all and nests them under the layout shell:
export const generalAppRoutes: Routes = [
{
path: '',
component: MainLayoutComponent,
children: [
...welcomeRoutes, ...adminRoutes, ...tasksRoutes, ...emailRoutes,
...manageRoutes, ...marketplacesRoutes, ...processesRoutes,
...reportsRoutes, ...supplierRoutes, ...UwRoutes,
redirectToWelcomeRoute, // '' → /welcome
],
},
redirectToWelcomeRoute,
{ path: '**', component: PageNotFoundComponent },
];
Two modules opt out of the aggregate and declare their own child routers:
-
auth/auth-routing.module.ts—/login,/logout. These render outsideMainLayoutComponent, which is why the login page has no sidebar. -
shared/shared-routing.module.ts—/reset-password.
|
Adding a top-level area means: create |
Route admission
Guard order
The order guards are listed in canActivate is the order they run, and it is
consistent across every feature route table:
canActivate: [RouteAccessResolver, FeatureFlagsGeneralGuard, NgxPermissionsGuard]
| Guard | What it does |
|---|---|
|
Despite the name it is a |
|
Waits for the |
|
Evaluates |
Two additional guards appear on specific routes:
| Guard | Used by |
|---|---|
|
Routes whose |
|
Routes whose |
|
Only on |
|
|
Permission clause conventions
Most feature areas use the same deny clause:
data: { permissions: { except: [USER_PERMISSION.REST_UW_ACCESS,
USER_PERMISSION.ADVANCE_AUDITORS] } }
The suppliers module formalises the variants as reusable spreads in
suppliers/suppliers-routing.routes.ts:
| Constant | Denies |
|---|---|
|
|
|
|
|
|
|
As |
These are spread after the route’s own properties, so a route that spreads one of
them and also declares its own data will have the spread win. Watch for that when
adding route data to a restricted route.
Only two routes use an allow clause: /admin (only: DEV) and
suppliers/:key/underwriting/background-reports (only: FINANCE).
The full table is in the route catalogue; the mechanics are in authorisation.
Route data as configuration
Route data is used as a lightweight configuration channel, not only for
permissions:
| Key | Meaning |
|---|---|
|
|
|
Flag name checked by |
|
Sub-permission list checked by |
|
Which external report an |
|
Which internal report/view a report component renders. |
|
Suppresses the supplier-key query-parameter requirement for report tables. |
|
Distinguishes |
This "one component, many routes, differentiated by data`" pattern is used
deliberately for report tabs and add/edit pairs. When you see two routes pointing
at the same component, check `data before assuming duplication.
Resolvers
Only one resolver is in live use:
SupplierKeyResolver(src/app/shared/resolvers/supplier-key.resolver.ts)-
Reads
supplierKeyfromroute.parent.params, falling back toroute.parent.parent.params, and dispatchesSetSupplierKeyrather than returning fetched data. It is attached to thesuppliers/:supplierKeyroute so that the store points at the right supplier before any child component renders.SupplierKeyMockResolveris its test double.
Domain data loading is not done in resolvers — it is done in
SupplierEffects.navigateToSupplier, which reacts to ROUTER_NAVIGATION. See
router-driven state.
Deep links, redirects and the hash
Because useHash is on, URL handling in code has to strip the fragment.
RouteAccessResolver.handleLogout() does this:
const backUrl = window.location.href
.replace(window.location.origin, '')
.replace(/^\/#\/(.*)$/, '/$1');
if (this.loginPageRoute !== backUrl) {
sessionStorage.setItem(LOGIN_REDIRECT_URL, backUrl);
}
After a successful login, AuthEffects.login reads sessionStorage['loginRedirectUrl']
(defaulting to /) and navigates inside ngZone.run(…) — required because the
Google Identity callback resolves outside Angular’s zone.
Note that loginRedirectUrl is not cleared after use; a later logout overwrites
it. See authentication.
Redirect conventions
Every area follows the same shape:
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{ path: 'list', component: XListComponent },
{ path: 'add' | 'new', component: XAddComponent },
{ path: ':key/edit', component: XEditComponent },
Deviations worth knowing:
-
suppliersredirects''to'../'— i.e. out of the suppliers area entirely. -
adminuses a**child redirect to/admin/feature-flagsinstead of a''redirect. -
tasksandemailsredirect to a parameterised default:list/0/filterandlistrespectively. -
processeshas an ordering hazard::processKey/:paymentKeyand:supKeyare both declared beforeapproval, so/processes/approvalmatches:supKeyfirst. This is why the sidebar links to/proceses/approval(the second, misspelled route table) for Balance Approval. Do not reorder without checking both.
Navigation UI
The sidebar (src/app/shared/sidenav/) is a hand-maintained mirror of the
route table, not generated from it. Menu items are wrapped in
*ngxPermissionsExcept / *ngxPermissionsOnly and, where relevant, in
*appEnabledByFeatureFlag.
|
Adding a route does not add a menu entry, and the permission clause on the route
and on the menu item are maintained separately. When you add or restrict a route,
update |
Layout reacts to the URL through the layout slice, which matches
urlAfterRedirects against regexes for supplierList, supplierGeneral and
supplierDefault to decide whether the CRM panel and supplier-info panel are
hidden, closed, open or fixed. See UI composition.