mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1991: ✨ feat(navigation): implement title management and enhance tab system
✨ feat(navigation): implement title management and enhance tab system This commit introduces a comprehensive title management system and extends the tab functionality with subtitle support, improving navigation clarity and user experience across the application. Key changes: Title Management System: - Add @isa/common/title-management library with dual approach: - IsaTitleStrategy for route-based static titles - usePageTitle() for component-based dynamic titles - Implement TitleRegistryService for nested component hierarchies - Automatic ISA prefix addition and TabService integration - Comprehensive test coverage (1,158 lines of tests) Tab System Enhancement: - Add subtitle field to tab schema for additional context - Update TabService API (addTab, patchTab) to support subtitles - Extend Zod schemas with subtitle validation - Update documentation with usage examples Routing Modernization: - Consolidate route guards using ActivateProcessIdWithConfigKeyGuard - Replace 4+ specific guards with generic config-key-based approach - Add title attributes to 100+ routes across all modules - Remove deprecated ProcessIdGuard in favor of ActivateProcessIdGuard Code Cleanup: - Remove deprecated preview component and related routes - Clean up unused imports and exports - Update TypeScript path aliases Dependencies: - Update package.json and package-lock.json - Add @isa/common/title-management to tsconfig path mappings Refs: #5351, #5418, #5419, #5420
This commit is contained in:
committed by
Nino Righi
parent
0670dbfdb1
commit
68f50b911d
@@ -1,4 +1,4 @@
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
@@ -7,23 +7,17 @@ import {
|
||||
CanActivateCustomerOrdersGuard,
|
||||
CanActivateCustomerOrdersWithProcessIdGuard,
|
||||
CanActivateCustomerWithProcessIdGuard,
|
||||
CanActivateGoodsInGuard,
|
||||
CanActivateProductGuard,
|
||||
CanActivateProductWithProcessIdGuard,
|
||||
CanActivateTaskCalendarGuard,
|
||||
IsAuthenticatedGuard,
|
||||
} from './guards';
|
||||
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
|
||||
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
|
||||
import { MainComponent } from './main.component';
|
||||
import { PreviewComponent } from './preview';
|
||||
import {
|
||||
BranchSectionResolver,
|
||||
CustomerSectionResolver,
|
||||
ProcessIdResolver,
|
||||
} from './resolvers';
|
||||
import { TokenLoginComponent, TokenLoginModule } from './token-login';
|
||||
import { ProcessIdGuard } from './guards/process-id.guard';
|
||||
import {
|
||||
ActivateProcessIdGuard,
|
||||
ActivateProcessIdWithConfigKeyGuard,
|
||||
@@ -74,8 +68,12 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/catalog').then((m) => m.PageCatalogModule),
|
||||
canActivate: [CanActivateProductWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
// TODO: Check if order and :processId/order is still being used
|
||||
// If not, remove these routes and the related guards and resolvers
|
||||
{
|
||||
path: 'order',
|
||||
loadChildren: () =>
|
||||
@@ -87,7 +85,9 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'customer',
|
||||
@@ -100,7 +100,9 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/customer').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
@@ -113,10 +115,13 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/checkout').then((m) => m.PageCheckoutModule),
|
||||
canActivate: [CanActivateCartWithProcessIdGuard],
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ProcessIdGuard],
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
@@ -126,6 +131,9 @@ const routes: Routes = [
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
],
|
||||
@@ -141,7 +149,7 @@ const routes: Routes = [
|
||||
import('@page/task-calendar').then(
|
||||
(m) => m.PageTaskCalendarModule,
|
||||
),
|
||||
canActivate: [CanActivateTaskCalendarGuard],
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('taskCalendar')],
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
@@ -154,27 +162,23 @@ const routes: Routes = [
|
||||
path: 'goods/in',
|
||||
loadChildren: () =>
|
||||
import('@page/goods-in').then((m) => m.GoodsInModule),
|
||||
canActivate: [CanActivateGoodsInGuard],
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('goodsIn')],
|
||||
},
|
||||
// {
|
||||
// path: 'remission',
|
||||
// loadChildren: () =>
|
||||
// import('@page/remission').then((m) => m.PageRemissionModule),
|
||||
// canActivate: [CanActivateRemissionGuard],
|
||||
// },
|
||||
{
|
||||
path: 'package-inspection',
|
||||
loadChildren: () =>
|
||||
import('@page/package-inspection').then(
|
||||
(m) => m.PackageInspectionModule,
|
||||
),
|
||||
canActivate: [CanActivatePackageInspectionGuard],
|
||||
canActivate: [
|
||||
ActivateProcessIdWithConfigKeyGuard('packageInspection'),
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'assortment',
|
||||
loadChildren: () =>
|
||||
import('@page/assortment').then((m) => m.AssortmentModule),
|
||||
canActivate: [CanActivateAssortmentGuard],
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('assortment')],
|
||||
},
|
||||
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
|
||||
],
|
||||
@@ -243,13 +247,6 @@ const routes: Routes = [
|
||||
},
|
||||
];
|
||||
|
||||
if (isDevMode()) {
|
||||
routes.unshift({
|
||||
path: 'preview',
|
||||
component: PreviewComponent,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { version } from '../../../../package.json';
|
||||
import { IsaTitleStrategy } from '@isa/common/title-management';
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
provideHttpClient,
|
||||
@@ -56,7 +57,6 @@ import {
|
||||
ScanditScanAdapterModule,
|
||||
} from '@adapter/scan';
|
||||
import * as Commands from './commands';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from '@external/native-container';
|
||||
import { ShellModule } from '@shared/shell';
|
||||
import { MainComponent } from './main.component';
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
import { Store } from '@ngrx/store';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import z from 'zod';
|
||||
import { TitleStrategy } from '@angular/router';
|
||||
import { TabNavigationService } from '@isa/core/tabs';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
@@ -270,7 +271,6 @@ const USER_SUB_FACTORY = () => {
|
||||
CoreCommandModule.forRoot(Object.values(Commands)),
|
||||
CoreLoggerModule.forRoot(),
|
||||
AppStoreModule,
|
||||
PreviewComponent,
|
||||
AuthModule.forRoot(),
|
||||
CoreApplicationModule.forRoot(),
|
||||
UiModalModule.forRoot(),
|
||||
@@ -330,6 +330,7 @@ const USER_SUB_FACTORY = () => {
|
||||
useValue: 'EUR',
|
||||
},
|
||||
provideUserSubFactory(USER_SUB_FACTORY),
|
||||
{ provide: TitleStrategy, useClass: IsaTitleStrategy },
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,47 +1,82 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
export const ActivateProcessIdGuard: CanActivateFn = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
) => {
|
||||
const application = inject(ApplicationService);
|
||||
|
||||
const processIdStr = route.params.processId;
|
||||
|
||||
if (!processIdStr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const processId = Number(processIdStr);
|
||||
|
||||
// Check if Process already exists
|
||||
const process = await application.getProcessById$(processId).pipe(take(1)).toPromise();
|
||||
|
||||
if (!process) {
|
||||
application.createCustomerProcess(processId);
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ActivateProcessIdWithConfigKeyGuard: (key: string) => CanActivateFn =
|
||||
(key) => async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
const application = inject(ApplicationService);
|
||||
const config = inject(Config);
|
||||
|
||||
const processId = config.get(`process.ids.${key}`);
|
||||
|
||||
if (isNaN(processId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivateFn,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { take } from 'rxjs/operators';
|
||||
import z from 'zod';
|
||||
|
||||
export const ActivateProcessIdGuard: CanActivateFn = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
) => {
|
||||
const application = inject(ApplicationService);
|
||||
|
||||
const processIdStr = route.params.processId;
|
||||
|
||||
if (!processIdStr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const processId = Number(processIdStr);
|
||||
|
||||
// Check if Process already exists
|
||||
const process = await application
|
||||
.getProcessById$(processId)
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
|
||||
if (!process) {
|
||||
application.createProcess({
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${processId}`,
|
||||
});
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ActivateProcessIdWithConfigKeyGuard: (
|
||||
key: string,
|
||||
) => CanActivateFn =
|
||||
(key) =>
|
||||
async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
const application = inject(ApplicationService);
|
||||
const config = inject(Config);
|
||||
|
||||
const processId = config.get(`process.ids.${key}`, z.coerce.number());
|
||||
|
||||
if (typeof processId !== 'number' || isNaN(processId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const processTitle = config.get(
|
||||
`process.titles.${key}`,
|
||||
z.string().default(key),
|
||||
);
|
||||
|
||||
const process = await application
|
||||
.getProcessById$(processId)
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
|
||||
if (!process) {
|
||||
await application.createProcess({
|
||||
id: processId,
|
||||
type: key,
|
||||
section: 'customer', // Not important anymore
|
||||
name: processTitle,
|
||||
});
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { z } from 'zod';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateGoodsInGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _config: Config,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const pid = this._config.get('process.ids.goodsIn', z.number());
|
||||
const process = await this._applicationService
|
||||
.getProcessById$(pid)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (!process) {
|
||||
await this._applicationService.createProcess({
|
||||
id: this._config.get('process.ids.goodsIn'),
|
||||
type: 'goods-in',
|
||||
section: 'branch',
|
||||
name: '',
|
||||
});
|
||||
}
|
||||
this._applicationService.activateProcess(
|
||||
this._config.get('process.ids.goodsIn'),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { z } from 'zod';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateGoodsInGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _config: Config,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const pid = this._config.get('process.ids.goodsIn', z.number());
|
||||
const process = await this._applicationService
|
||||
.getProcessById$(pid)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (!process) {
|
||||
await this._applicationService.createProcess({
|
||||
id: this._config.get('process.ids.goodsIn'),
|
||||
type: 'goods-in',
|
||||
section: 'branch',
|
||||
name: 'Abholfach',
|
||||
});
|
||||
}
|
||||
this._applicationService.activateProcess(
|
||||
this._config.get('process.ids.goodsIn'),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './preview.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
@apply grid min-h-screen content-center justify-center;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<h1>Platform: {{ platform | json }}</h1>
|
||||
<br />
|
||||
<h1>{{ appVersion }}</h1>
|
||||
<br />
|
||||
<h1>{{ userAgent }}</h1>
|
||||
<br />
|
||||
<h1>Navigator: {{ navigator | json }}</h1>
|
||||
<br />
|
||||
<br />
|
||||
<h1>Device: {{ device }}</h1>
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Platform, PlatformModule } from '@angular/cdk/platform';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-preview',
|
||||
templateUrl: 'preview.component.html',
|
||||
styleUrls: ['preview.component.css'],
|
||||
imports: [CommonModule, PlatformModule],
|
||||
})
|
||||
export class PreviewComponent {
|
||||
selectedBranch$ = new BehaviorSubject<BranchDTO>({});
|
||||
|
||||
get appVersion() {
|
||||
return 'App Version: ' + (window.navigator as any).appVersion;
|
||||
}
|
||||
|
||||
get userAgent() {
|
||||
return 'User Agent: ' + (window.navigator as any).userAgent;
|
||||
}
|
||||
|
||||
get navigator() {
|
||||
const nav = {};
|
||||
for (const i in window.navigator) nav[i] = navigator[i];
|
||||
return nav;
|
||||
}
|
||||
|
||||
get platform() {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
get device() {
|
||||
const isIpadNative = this._platform.IOS && !this._platform.SAFARI;
|
||||
const isIpadMini6Native = window?.navigator?.userAgent?.includes('Macintosh') && !this._platform.SAFARI;
|
||||
const isNative = isIpadNative || isIpadMini6Native;
|
||||
const isPWA = this._platform.IOS && this._platform.SAFARI;
|
||||
const isDesktop = !isNative && !isPWA;
|
||||
if (isNative) {
|
||||
if (isIpadMini6Native) {
|
||||
return 'IPAD mini 6 Native App';
|
||||
} else if (isIpadNative) {
|
||||
return 'IPAD mini 2 Native App or IPAD mini 5 Native App';
|
||||
}
|
||||
} else if (isPWA) {
|
||||
return 'IPAD Safari PWA';
|
||||
} else if (isDesktop) return 'Desktop or Macintosh';
|
||||
}
|
||||
|
||||
constructor(private readonly _platform: Platform) {}
|
||||
|
||||
setNewBranch(branch: BranchDTO) {
|
||||
this.selectedBranch$.next(branch);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
// start:ng42.barrel
|
||||
export * from './process-id.resolver';
|
||||
export * from './section.resolver';
|
||||
// end:ng42.barrel
|
||||
export * from './process-id.resolver';
|
||||
export * from './section.resolver';
|
||||
|
||||
@@ -1,87 +1,86 @@
|
||||
{
|
||||
"title": "ISA - Local",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"debug": true,
|
||||
"dev-scanner": true,
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
"@core/auth": {
|
||||
"issuer": "https://sso-test.paragon-data.de",
|
||||
"clientId": "isa-client",
|
||||
"responseType": "code",
|
||||
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
|
||||
"showDebugInformation": true
|
||||
},
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
"@swagger/cat": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
|
||||
},
|
||||
"@swagger/av": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/ava/v6"
|
||||
},
|
||||
"@swagger/checkout": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
|
||||
},
|
||||
"@swagger/crm": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/crm/v6"
|
||||
},
|
||||
"@swagger/oms": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/oms/v6"
|
||||
},
|
||||
"@swagger/print": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/print/v1"
|
||||
},
|
||||
"@swagger/eis": {
|
||||
"rootUrl": "https://filialinformationsystem-test.paragon-systems.de/eiswebapi/v1"
|
||||
},
|
||||
"@swagger/remi": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/inv/v6"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
"url": "https://isa-test.paragon-data.net/isa/v1/rt",
|
||||
"enableAutomaticReconnect": false,
|
||||
"httpOptions": {
|
||||
"transport": 1,
|
||||
"logMessageContent": true,
|
||||
"skipNegotiation": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"ids": {
|
||||
"goodsOut": 1000,
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000,
|
||||
"pickupShelf": 7000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "Ae8F2Wx2RMq5Lvn7UUAlWzVFZTt2+ubMAF8XtDpmPlNkBeG/LWs1M7AbgDW0LQqYLnszClEENaEHS56/6Ts2vrJ1Ux03CXUjK3jUvZpF5OchXR1CpnmpepJ6WxPCd7LMVHUGG1BbwPLDTFjP3y8uT0caTSmmGrYQWAs4CZcEF+ZBabP0z7vfm+hCZF/ebj9qqCJZcW8nH/n19hohshllzYBjFXjh87P2lIh1s6yZS3OaQWWXo/o0AKdxx7T6CVyR0/G5zq6uYJWf6rs3euUBEhpzOZHbHZK86Lvy2AVBEyVkkcttlDW1J2fA4l1W1JV/Xibz8AQV6kG482EpGF42KEoK48paZgX3e1AQsqUtmqzw294dcP4zMVstnw5/WrwKKi/5E/nOOJT2txYP1ZufIjPrwNFsqTlv7xCQlHjMzFGYwT816yD5qLRLbwOtjrkUPXNZLZ06T4upvWwJDmm8XgdeoDqMjHdcO4lwji1bl9EiIYJ/2qnsk9yZ2FqSaHzn4cbiL0f5u2HFlNAP0GUujGRlthGhHi6o4dFU+WAxKsFMKVt+SfoQUazNKHFVQgiAklTIZxIc/HUVzRvOLMxf+wFDerraBtcqGJg+g/5mrWYqeDBGhCBHtKiYf6244IJ4afzNTiH1/30SJcRzXwbEa3A7q1fJTx9/nLTOfVPrJKBQs7f/OQs2dA7LDCel8mzXdbjvsNQaeU5+iCIAq6zbTNKy1xT8wwj+VZrQmtNJs+qeznD+u29nCM24h8xCmRpvNPo4/Mww/lrTNrrNwLBSn1pMIwsH7yS9hH0v0oNAM3A6bVtk1D9qEkbyw+xZa+MZGpMP0D0CdcsqHalPcm5r/Ik="
|
||||
},
|
||||
"gender": {
|
||||
"0": "Keine Anrede",
|
||||
"1": "Enby",
|
||||
"2": "Herr",
|
||||
"4": "Frau"
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
{
|
||||
"title": "ISA - Local",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"debug": true,
|
||||
"dev-scanner": true,
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
"@core/auth": {
|
||||
"issuer": "https://sso-test.paragon-data.de",
|
||||
"clientId": "isa-client",
|
||||
"responseType": "code",
|
||||
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
|
||||
"showDebugInformation": true
|
||||
},
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
"@swagger/cat": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
|
||||
},
|
||||
"@swagger/av": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/ava/v6"
|
||||
},
|
||||
"@swagger/checkout": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
|
||||
},
|
||||
"@swagger/crm": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/crm/v6"
|
||||
},
|
||||
"@swagger/oms": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/oms/v6"
|
||||
},
|
||||
"@swagger/print": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/print/v1"
|
||||
},
|
||||
"@swagger/eis": {
|
||||
"rootUrl": "https://filialinformationsystem-test.paragon-systems.de/eiswebapi/v1"
|
||||
},
|
||||
"@swagger/remi": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/inv/v6"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
"url": "https://isa-test.paragon-data.net/isa/v1/rt",
|
||||
"enableAutomaticReconnect": false,
|
||||
"httpOptions": {
|
||||
"transport": 1,
|
||||
"logMessageContent": true,
|
||||
"skipNegotiation": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"ids": {
|
||||
"goodsOut": 1000,
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000,
|
||||
"pickupShelf": 2000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "Ae8F2Wx2RMq5Lvn7UUAlWzVFZTt2+ubMAF8XtDpmPlNkBeG/LWs1M7AbgDW0LQqYLnszClEENaEHS56/6Ts2vrJ1Ux03CXUjK3jUvZpF5OchXR1CpnmpepJ6WxPCd7LMVHUGG1BbwPLDTFjP3y8uT0caTSmmGrYQWAs4CZcEF+ZBabP0z7vfm+hCZF/ebj9qqCJZcW8nH/n19hohshllzYBjFXjh87P2lIh1s6yZS3OaQWWXo/o0AKdxx7T6CVyR0/G5zq6uYJWf6rs3euUBEhpzOZHbHZK86Lvy2AVBEyVkkcttlDW1J2fA4l1W1JV/Xibz8AQV6kG482EpGF42KEoK48paZgX3e1AQsqUtmqzw294dcP4zMVstnw5/WrwKKi/5E/nOOJT2txYP1ZufIjPrwNFsqTlv7xCQlHjMzFGYwT816yD5qLRLbwOtjrkUPXNZLZ06T4upvWwJDmm8XgdeoDqMjHdcO4lwji1bl9EiIYJ/2qnsk9yZ2FqSaHzn4cbiL0f5u2HFlNAP0GUujGRlthGhHi6o4dFU+WAxKsFMKVt+SfoQUazNKHFVQgiAklTIZxIc/HUVzRvOLMxf+wFDerraBtcqGJg+g/5mrWYqeDBGhCBHtKiYf6244IJ4afzNTiH1/30SJcRzXwbEa3A7q1fJTx9/nLTOfVPrJKBQs7f/OQs2dA7LDCel8mzXdbjvsNQaeU5+iCIAq6zbTNKy1xT8wwj+VZrQmtNJs+qeznD+u29nCM24h8xCmRpvNPo4/Mww/lrTNrrNwLBSn1pMIwsH7yS9hH0v0oNAM3A6bVtk1D9qEkbyw+xZa+MZGpMP0D0CdcsqHalPcm5r/Ik="
|
||||
},
|
||||
"gender": {
|
||||
"0": "Keine Anrede",
|
||||
"1": "Enby",
|
||||
"2": "Herr",
|
||||
"4": "Frau"
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
|
||||
@@ -1,174 +1,196 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ApplicationProcess } from './defs';
|
||||
import {
|
||||
removeProcess,
|
||||
selectSection,
|
||||
selectProcesses,
|
||||
setSection,
|
||||
addProcess,
|
||||
setActivatedProcess,
|
||||
selectActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
selectTitle,
|
||||
setTitle,
|
||||
} from './store';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.activatedProcessIdSubject.value;
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
}
|
||||
|
||||
constructor(private store: Store) {}
|
||||
|
||||
getProcesses$(section?: 'customer' | 'branch') {
|
||||
const processes$ = this.store.select(selectProcesses);
|
||||
return processes$.pipe(
|
||||
map((processes) => processes.filter((process) => (section ? process.section === section : true))),
|
||||
);
|
||||
}
|
||||
|
||||
getProcessById$(processId: number): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$().pipe(map((processes) => processes.find((process) => process.id === processId)));
|
||||
}
|
||||
|
||||
getSection$() {
|
||||
return this.store.select(selectSection);
|
||||
}
|
||||
|
||||
getTitle$() {
|
||||
return this.getSection$().pipe(
|
||||
map((section) => {
|
||||
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$() {
|
||||
return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number) {
|
||||
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
|
||||
this.activatedProcessIdSubject.next(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number) {
|
||||
this.store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
|
||||
this.store.dispatch(patchProcess({ processId, changes }));
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch))),
|
||||
);
|
||||
}
|
||||
|
||||
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
|
||||
}
|
||||
|
||||
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
|
||||
const processes = await this.getProcesses$('customer').pipe(first()).toPromise();
|
||||
|
||||
const processIds = processes.filter((x) => this.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
|
||||
|
||||
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: processId ?? Date.now(),
|
||||
type: 'cart',
|
||||
name: `Vorgang ${maxId + 1}`,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
await this.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async createProcess(process: ApplicationProcess) {
|
||||
const existingProcess = await this.getProcessById$(process?.id).pipe(first()).toPromise();
|
||||
if (existingProcess?.id === process?.id) {
|
||||
throw new Error('Process Id existiert bereits');
|
||||
}
|
||||
|
||||
if (!isNumber(process.id)) {
|
||||
throw new Error('Process Id nicht gesetzt');
|
||||
}
|
||||
|
||||
if (!isBoolean(process.closeable)) {
|
||||
process.closeable = true;
|
||||
}
|
||||
|
||||
if (!isBoolean(process.confirmClosing)) {
|
||||
process.confirmClosing = true;
|
||||
}
|
||||
|
||||
process.created = this._createTimestamp();
|
||||
process.activated = 0;
|
||||
this.store.dispatch(addProcess({ process }));
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch') {
|
||||
this.store.dispatch(setSection({ section }));
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSectionAndType$(
|
||||
section: 'customer' | 'branch',
|
||||
type: string,
|
||||
): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes
|
||||
?.filter((process) => process.type === type)
|
||||
?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSection$(section: 'customer' | 'branch'): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _createTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ApplicationProcess } from './defs';
|
||||
import {
|
||||
removeProcess,
|
||||
selectSection,
|
||||
selectProcesses,
|
||||
setSection,
|
||||
addProcess,
|
||||
setActivatedProcess,
|
||||
selectActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
selectTitle,
|
||||
setTitle,
|
||||
} from './store';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.activatedProcessIdSubject.value;
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
}
|
||||
|
||||
constructor(private store: Store) {}
|
||||
|
||||
getProcesses$(section?: 'customer' | 'branch') {
|
||||
const processes$ = this.store.select(selectProcesses);
|
||||
return processes$.pipe(
|
||||
map((processes) =>
|
||||
processes.filter((process) =>
|
||||
section ? process.section === section : true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getProcessById$(processId: number): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$().pipe(
|
||||
map((processes) => processes.find((process) => process.id === processId)),
|
||||
);
|
||||
}
|
||||
|
||||
getSection$() {
|
||||
return this.store.select(selectSection);
|
||||
}
|
||||
|
||||
getTitle$() {
|
||||
return this.getSection$().pipe(
|
||||
map((section) => {
|
||||
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$() {
|
||||
return this.store
|
||||
.select(selectActivatedProcess)
|
||||
.pipe(map((process) => process?.id));
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number) {
|
||||
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
|
||||
this.activatedProcessIdSubject.next(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number) {
|
||||
this.store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
|
||||
this.store.dispatch(patchProcess({ processId, changes }));
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) =>
|
||||
this.getProcessById$(processId).pipe(
|
||||
map((process) => process?.data?.selectedBranch),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return this.getProcessById$(processId).pipe(
|
||||
map((process) => process?.data?.selectedBranch),
|
||||
);
|
||||
}
|
||||
|
||||
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
|
||||
const processes = await this.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
const processIds = processes
|
||||
.filter((x) => this.REGEX_PROCESS_NAME.test(x.name))
|
||||
.map((x) => +x.name.split(' ')[1]);
|
||||
|
||||
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: processId ?? Date.now(),
|
||||
type: 'cart',
|
||||
name: `Vorgang ${maxId + 1}`,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
await this.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async createProcess(process: ApplicationProcess) {
|
||||
const existingProcess = await this.getProcessById$(process?.id)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (existingProcess?.id === process?.id) {
|
||||
throw new Error('Process Id existiert bereits');
|
||||
}
|
||||
|
||||
if (!isNumber(process.id)) {
|
||||
throw new Error('Process Id nicht gesetzt');
|
||||
}
|
||||
|
||||
if (!isBoolean(process.closeable)) {
|
||||
process.closeable = true;
|
||||
}
|
||||
|
||||
if (!isBoolean(process.confirmClosing)) {
|
||||
process.confirmClosing = true;
|
||||
}
|
||||
|
||||
process.created = this._createTimestamp();
|
||||
process.activated = 0;
|
||||
this.store.dispatch(addProcess({ process }));
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch') {
|
||||
this.store.dispatch(setSection({ section }));
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSectionAndType$(
|
||||
section: 'customer' | 'branch',
|
||||
type: string,
|
||||
): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes
|
||||
?.filter((process) => process.type === type)
|
||||
?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSection$(
|
||||
section: 'customer' | 'branch',
|
||||
): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _createTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const _domainCheckoutReducer = createReducer(
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
for (const shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[
|
||||
`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`
|
||||
@@ -275,7 +275,7 @@ function getOrCreateCheckoutEntity({
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
processId: number;
|
||||
}): CheckoutEntity {
|
||||
let entity = entities[processId];
|
||||
const entity = entities[processId];
|
||||
|
||||
if (isNullOrUndefined(entity)) {
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { AssortmentComponent } from './assortment.component';
|
||||
import { PriceUpdateComponent } from './price-update/price-update.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AssortmentComponent,
|
||||
children: [
|
||||
{ path: 'price-update', component: PriceUpdateComponent },
|
||||
{ path: '**', redirectTo: 'price-update' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { AssortmentComponent } from './assortment.component';
|
||||
import { PriceUpdateComponent } from './price-update/price-update.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AssortmentComponent,
|
||||
title: 'Sortiment',
|
||||
children: [
|
||||
{
|
||||
path: 'price-update',
|
||||
component: PriceUpdateComponent,
|
||||
title: 'Sortiment - Preisänderung',
|
||||
},
|
||||
{ path: '**', redirectTo: 'price-update' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,125 +1,135 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ArticleDetailsComponent } from './article-details/article-details.component';
|
||||
import { ArticleSearchComponent } from './article-search/article-search.component';
|
||||
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
|
||||
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
|
||||
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageCatalogComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'filter',
|
||||
component: ArticleSearchFilterComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'filter/:id',
|
||||
component: ArticleSearchFilterComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: ArticleSearchComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ArticleSearchMainComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:id',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:ean/ean',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:id',
|
||||
component: ArticleDetailsComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:ean/ean',
|
||||
component: ArticleDetailsComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'filter',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageCatalogRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ArticleDetailsComponent } from './article-details/article-details.component';
|
||||
import { ArticleSearchComponent } from './article-search/article-search.component';
|
||||
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
|
||||
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
|
||||
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageCatalogComponent,
|
||||
title: 'Artikelsuche',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'filter',
|
||||
component: ArticleSearchFilterComponent,
|
||||
title: 'Artikelsuche - Filter',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'filter/:id',
|
||||
component: ArticleSearchFilterComponent,
|
||||
title: 'Artikelsuche - Filter',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: ArticleSearchComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ArticleSearchMainComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:id',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:ean/ean',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:id',
|
||||
component: ArticleDetailsComponent,
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:ean/ean',
|
||||
component: ArticleDetailsComponent,
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'filter',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageCatalogRoutingModule {}
|
||||
|
||||
@@ -14,20 +14,24 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'details',
|
||||
component: CheckoutReviewDetailsComponent,
|
||||
title: 'Bestelldetails',
|
||||
outlet: 'side',
|
||||
},
|
||||
{
|
||||
path: 'review',
|
||||
component: CheckoutReviewComponent,
|
||||
title: 'Bestellung überprüfen',
|
||||
},
|
||||
{
|
||||
path: 'summary',
|
||||
component: CheckoutSummaryComponent,
|
||||
title: 'Bestellübersicht',
|
||||
canDeactivate: [canDeactivateTabCleanup],
|
||||
},
|
||||
{
|
||||
path: 'summary/:orderIds',
|
||||
component: CheckoutSummaryComponent,
|
||||
title: 'Bestellübersicht',
|
||||
canDeactivate: [canDeactivateTabCleanup],
|
||||
},
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'review' },
|
||||
|
||||
@@ -1,170 +1,187 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { CustomerComponent } from './customer-page.component';
|
||||
import { CustomerSearchComponent } from './customer-search/customer-search.component';
|
||||
import { CustomerResultsMainViewComponent } from './customer-search/results-main-view/results-main-view.component';
|
||||
import { CustomerDetailsViewMainComponent } from './customer-search/details-main-view/details-main-view.component';
|
||||
import { CustomerHistoryMainViewComponent } from './customer-search/history-main-view/history-main-view.component';
|
||||
import { CustomerFilterMainViewComponent } from './customer-search/filter-main-view/filter-main-view.component';
|
||||
import { CustomerCreateGuard } from './guards/customer-create.guard';
|
||||
import {
|
||||
CreateB2BCustomerComponent,
|
||||
CreateGuestCustomerComponent,
|
||||
// CreateP4MCustomerComponent,
|
||||
CreateStoreCustomerComponent,
|
||||
CreateWebshopCustomerComponent,
|
||||
} from './create-customer';
|
||||
// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
||||
import { CreateCustomerComponent } from './create-customer/create-customer.component';
|
||||
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
|
||||
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
|
||||
import { AddBillingAddressMainViewComponent } from './customer-search/add-billing-address-main-view/add-billing-address-main-view.component';
|
||||
import { AddShippingAddressMainViewComponent } from './customer-search/add-shipping-address-main-view/add-shipping-address-main-view.component';
|
||||
import { EditBillingAddressMainViewComponent } from './customer-search/edit-billing-address-main-view/edit-billing-address-main-view.component';
|
||||
import { EditShippingAddressMainViewComponent } from './customer-search/edit-shipping-address-main-view/edit-shipping-address-main-view.component';
|
||||
import { CustomerOrdersMainViewComponent } from './customer-search/orders-main-view/orders-main-view.component';
|
||||
import { OrderDetailsMainViewComponent } from './customer-search/order-details-main-view/order-details-main-view.component';
|
||||
import { KundenkarteMainViewComponent } from './customer-search/kundenkarte-main-view/kundenkarte-main-view.component';
|
||||
import { CustomerOrderDetailsHistoryMainViewComponent } from './customer-search/order-details-history-main-view/order-details-history-main-view.component';
|
||||
import { CustomerMainViewComponent } from './customer-search/main-view/main-view.component';
|
||||
import { MainSideViewComponent } from './customer-search/main-side-view/main-side-view.component';
|
||||
import { CustomerResultsSideViewComponent } from './customer-search/results-side-view/results-side-view.component';
|
||||
import { OrderDetailsSideViewComponent } from './customer-search/order-details-side-view/order-details-side-view.component';
|
||||
import { CustomerCreateSideViewComponent } from './create-customer/customer-create-side-view';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerSearchComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'search',
|
||||
component: CustomerMainViewComponent,
|
||||
data: { side: 'main', breadcrumb: 'main' },
|
||||
},
|
||||
{
|
||||
path: 'search/list',
|
||||
component: CustomerResultsMainViewComponent,
|
||||
data: { breadcrumb: 'search' },
|
||||
},
|
||||
{
|
||||
path: 'search/filter',
|
||||
component: CustomerFilterMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'filter' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId',
|
||||
component: CustomerDetailsViewMainComponent,
|
||||
data: { side: 'results', breadcrumb: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/history',
|
||||
component: CustomerHistoryMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/kundenkarte',
|
||||
component: KundenkarteMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'kundenkarte' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders',
|
||||
component: CustomerOrdersMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'orders' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId/history',
|
||||
component: CustomerOrderDetailsHistoryMainViewComponent,
|
||||
data: {
|
||||
side: 'order-details',
|
||||
breadcrumb: 'order-details-history',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit/b2b',
|
||||
component: CustomerDataEditB2BComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit',
|
||||
component: CustomerDataEditB2CComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/add',
|
||||
component: AddBillingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'add-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/:payerId/edit',
|
||||
component: EditBillingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/add',
|
||||
component: AddShippingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'add-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/:shippingAddressId/edit',
|
||||
component: EditShippingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search-customer-main',
|
||||
outlet: 'side',
|
||||
component: MainSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
outlet: 'side',
|
||||
component: CustomerResultsSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'order-details',
|
||||
outlet: 'side',
|
||||
component: OrderDetailsSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CreateCustomerComponent,
|
||||
canActivate: [CustomerCreateGuard],
|
||||
canActivateChild: [CustomerCreateGuard],
|
||||
children: [
|
||||
{ path: 'create', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/store', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/webshop', component: CreateWebshopCustomerComponent },
|
||||
{ path: 'create/b2b', component: CreateB2BCustomerComponent },
|
||||
{ path: 'create/guest', component: CreateGuestCustomerComponent },
|
||||
// { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
||||
// { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
||||
// {
|
||||
// path: 'create/webshop-p4m/update',
|
||||
// component: UpdateP4MWebshopCustomerComponent,
|
||||
// data: { customerType: 'webshop' },
|
||||
// },
|
||||
{
|
||||
path: 'create-customer-main',
|
||||
outlet: 'side',
|
||||
component: CustomerCreateSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { CustomerComponent } from './customer-page.component';
|
||||
import { CustomerSearchComponent } from './customer-search/customer-search.component';
|
||||
import { CustomerResultsMainViewComponent } from './customer-search/results-main-view/results-main-view.component';
|
||||
import { CustomerDetailsViewMainComponent } from './customer-search/details-main-view/details-main-view.component';
|
||||
import { CustomerHistoryMainViewComponent } from './customer-search/history-main-view/history-main-view.component';
|
||||
import { CustomerFilterMainViewComponent } from './customer-search/filter-main-view/filter-main-view.component';
|
||||
import { CustomerCreateGuard } from './guards/customer-create.guard';
|
||||
import {
|
||||
CreateB2BCustomerComponent,
|
||||
CreateGuestCustomerComponent,
|
||||
// CreateP4MCustomerComponent,
|
||||
CreateStoreCustomerComponent,
|
||||
CreateWebshopCustomerComponent,
|
||||
} from './create-customer';
|
||||
// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
||||
import { CreateCustomerComponent } from './create-customer/create-customer.component';
|
||||
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
|
||||
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
|
||||
import { AddBillingAddressMainViewComponent } from './customer-search/add-billing-address-main-view/add-billing-address-main-view.component';
|
||||
import { AddShippingAddressMainViewComponent } from './customer-search/add-shipping-address-main-view/add-shipping-address-main-view.component';
|
||||
import { EditBillingAddressMainViewComponent } from './customer-search/edit-billing-address-main-view/edit-billing-address-main-view.component';
|
||||
import { EditShippingAddressMainViewComponent } from './customer-search/edit-shipping-address-main-view/edit-shipping-address-main-view.component';
|
||||
import { CustomerOrdersMainViewComponent } from './customer-search/orders-main-view/orders-main-view.component';
|
||||
import { OrderDetailsMainViewComponent } from './customer-search/order-details-main-view/order-details-main-view.component';
|
||||
import { KundenkarteMainViewComponent } from './customer-search/kundenkarte-main-view/kundenkarte-main-view.component';
|
||||
import { CustomerOrderDetailsHistoryMainViewComponent } from './customer-search/order-details-history-main-view/order-details-history-main-view.component';
|
||||
import { CustomerMainViewComponent } from './customer-search/main-view/main-view.component';
|
||||
import { MainSideViewComponent } from './customer-search/main-side-view/main-side-view.component';
|
||||
import { CustomerResultsSideViewComponent } from './customer-search/results-side-view/results-side-view.component';
|
||||
import { OrderDetailsSideViewComponent } from './customer-search/order-details-side-view/order-details-side-view.component';
|
||||
import { CustomerCreateSideViewComponent } from './create-customer/customer-create-side-view';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerSearchComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'search',
|
||||
component: CustomerMainViewComponent,
|
||||
title: 'Kundensuche',
|
||||
data: { side: 'main', breadcrumb: 'main' },
|
||||
},
|
||||
{
|
||||
path: 'search/list',
|
||||
component: CustomerResultsMainViewComponent,
|
||||
title: 'Kundensuche - Trefferliste',
|
||||
data: { breadcrumb: 'search' },
|
||||
},
|
||||
{
|
||||
path: 'search/filter',
|
||||
component: CustomerFilterMainViewComponent,
|
||||
title: 'Kundensuche - Filter',
|
||||
data: { side: 'results', breadcrumb: 'filter' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId',
|
||||
component: CustomerDetailsViewMainComponent,
|
||||
title: 'Kundendetails',
|
||||
data: { side: 'results', breadcrumb: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/history',
|
||||
component: CustomerHistoryMainViewComponent,
|
||||
title: 'Kundendetails - Verlauf',
|
||||
data: { side: 'results', breadcrumb: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/kundenkarte',
|
||||
component: KundenkarteMainViewComponent,
|
||||
title: 'Kundendetails - Kundenkarte',
|
||||
data: { side: 'results', breadcrumb: 'kundenkarte' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders',
|
||||
component: CustomerOrdersMainViewComponent,
|
||||
title: 'Kundendetails - Bestellungen',
|
||||
data: { side: 'results', breadcrumb: 'orders' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
title: 'Kundendetails - Bestelldetails',
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
title: 'Kundendetails - Bestelldetails',
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId/history',
|
||||
component: CustomerOrderDetailsHistoryMainViewComponent,
|
||||
title: 'Kundendetails - Bestelldetails Verlauf',
|
||||
data: {
|
||||
side: 'order-details',
|
||||
breadcrumb: 'order-details-history',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit/b2b',
|
||||
component: CustomerDataEditB2BComponent,
|
||||
title: 'Kundendetails - Bearbeiten (B2B)',
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit',
|
||||
component: CustomerDataEditB2CComponent,
|
||||
title: 'Kundendetails - Bearbeiten',
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/add',
|
||||
component: AddBillingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Neue Rechnungsadresse',
|
||||
data: { side: 'results', breadcrumb: 'add-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/:payerId/edit',
|
||||
component: EditBillingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Rechnungsadresse bearbeiten',
|
||||
data: { side: 'results', breadcrumb: 'edit-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/add',
|
||||
component: AddShippingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Neue Lieferadresse',
|
||||
data: { side: 'results', breadcrumb: 'add-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/:shippingAddressId/edit',
|
||||
component: EditShippingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Lieferadresse bearbeiten',
|
||||
data: { side: 'results', breadcrumb: 'edit-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search-customer-main',
|
||||
outlet: 'side',
|
||||
component: MainSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
outlet: 'side',
|
||||
component: CustomerResultsSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'order-details',
|
||||
outlet: 'side',
|
||||
component: OrderDetailsSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CreateCustomerComponent,
|
||||
canActivate: [CustomerCreateGuard],
|
||||
canActivateChild: [CustomerCreateGuard],
|
||||
title: 'Kundendaten erfassen',
|
||||
children: [
|
||||
{ path: 'create', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/store', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/webshop', component: CreateWebshopCustomerComponent },
|
||||
{ path: 'create/b2b', component: CreateB2BCustomerComponent },
|
||||
{ path: 'create/guest', component: CreateGuestCustomerComponent },
|
||||
// { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
||||
// { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
||||
// {
|
||||
// path: 'create/webshop-p4m/update',
|
||||
// component: UpdateP4MWebshopCustomerComponent,
|
||||
// data: { customerType: 'webshop' },
|
||||
// },
|
||||
{
|
||||
path: 'create-customer-main',
|
||||
outlet: 'side',
|
||||
component: CustomerCreateSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DashboardComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class DashboardRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DashboardComponent,
|
||||
title: 'Dashboard',
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class DashboardRoutingModule {}
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { GoodsInCleanupListComponent } from './goods-in-cleanup-list/goods-in-cleanup-list.component';
|
||||
import { GoodsInCleanupListModule } from './goods-in-cleanup-list/goods-in-cleanup-list.module';
|
||||
import { GoodsInListComponent } from './goods-in-list/goods-in-list.component';
|
||||
import { GoodsInListModule } from './goods-in-list/goods-in-list.module';
|
||||
import { GoodsInRemissionPreviewComponent } from './goods-in-remission-preview/goods-in-remission-preview.component';
|
||||
import { GoodsInRemissionPreviewModule } from './goods-in-remission-preview/goods-in-remission-preview.module';
|
||||
import { GoodsInReservationComponent } from './goods-in-reservation/goods-in-reservation.component';
|
||||
import { GoodsInReservationModule } from './goods-in-reservation/goods-in-reservation.module';
|
||||
import { GoodsInComponent } from './goods-in.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: GoodsInComponent,
|
||||
children: [
|
||||
{ path: 'list', component: GoodsInListComponent },
|
||||
{ path: 'reservation', component: GoodsInReservationComponent },
|
||||
{ path: 'cleanup', component: GoodsInCleanupListComponent },
|
||||
{ path: 'preview', component: GoodsInRemissionPreviewComponent },
|
||||
],
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
GoodsInListModule,
|
||||
GoodsInCleanupListModule,
|
||||
GoodsInReservationModule,
|
||||
GoodsInRemissionPreviewModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class GoodsInRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { GoodsInCleanupListComponent } from './goods-in-cleanup-list/goods-in-cleanup-list.component';
|
||||
import { GoodsInCleanupListModule } from './goods-in-cleanup-list/goods-in-cleanup-list.module';
|
||||
import { GoodsInListComponent } from './goods-in-list/goods-in-list.component';
|
||||
import { GoodsInListModule } from './goods-in-list/goods-in-list.module';
|
||||
import { GoodsInRemissionPreviewComponent } from './goods-in-remission-preview/goods-in-remission-preview.component';
|
||||
import { GoodsInRemissionPreviewModule } from './goods-in-remission-preview/goods-in-remission-preview.module';
|
||||
import { GoodsInReservationComponent } from './goods-in-reservation/goods-in-reservation.component';
|
||||
import { GoodsInReservationModule } from './goods-in-reservation/goods-in-reservation.module';
|
||||
import { GoodsInComponent } from './goods-in.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: GoodsInComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
component: GoodsInListComponent,
|
||||
title: 'Abholfach - Fehlende',
|
||||
},
|
||||
{
|
||||
path: 'reservation',
|
||||
component: GoodsInReservationComponent,
|
||||
title: 'Abholfach - Reservierung',
|
||||
},
|
||||
{
|
||||
path: 'cleanup',
|
||||
component: GoodsInCleanupListComponent,
|
||||
title: 'Abholfach - Ausräumen',
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
component: GoodsInRemissionPreviewComponent,
|
||||
title: 'Abholfach - Vorschau',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
GoodsInListModule,
|
||||
GoodsInCleanupListModule,
|
||||
GoodsInReservationModule,
|
||||
GoodsInRemissionPreviewModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class GoodsInRoutingModule {}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { PackageDetailsComponent } from './package-details';
|
||||
import { PackageInspectionComponent } from './package-inspection.component';
|
||||
import { PackageResultComponent } from './package-result';
|
||||
|
||||
export const packageInspectionRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PackageInspectionComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'packages',
|
||||
component: PackageResultComponent,
|
||||
},
|
||||
{
|
||||
path: 'packages/:id',
|
||||
component: PackageDetailsComponent,
|
||||
},
|
||||
{ path: '**', redirectTo: 'packages' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { PackageDetailsComponent } from './package-details';
|
||||
import { PackageInspectionComponent } from './package-inspection.component';
|
||||
import { PackageResultComponent } from './package-result';
|
||||
|
||||
export const packageInspectionRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PackageInspectionComponent,
|
||||
title: 'Packstück-Prüfung',
|
||||
children: [
|
||||
{
|
||||
path: 'packages',
|
||||
component: PackageResultComponent,
|
||||
},
|
||||
{
|
||||
path: 'packages/:id',
|
||||
component: PackageDetailsComponent,
|
||||
title: 'Packstück-Prüfung - Details',
|
||||
},
|
||||
{ path: '**', redirectTo: 'packages' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,146 +1,168 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfInComponent } from './pickup-shelf-in.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfInDetailsComponent } from './pickup-shelf-in-details/pickup-shelf-in-details.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickUpShelfInMainSideViewComponent } from './pickup-shelf-in-main-side-view/pickup-shelf-in-main-side-view.component';
|
||||
import { PickUpShelfInMainComponent } from './pickup-shelf-in-main/pickup-shelf-in-main.component';
|
||||
import { PickUpShelfInListComponent } from './pickup-shelf-in-list/pickup-shelf-in-list.component';
|
||||
import { PickupShelfInEditComponent } from './pickup-shelf-in-edit/pickup-shelf-in-edit.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfInComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{
|
||||
path: 'main',
|
||||
component: PickUpShelfInMainComponent,
|
||||
data: {
|
||||
view: 'main',
|
||||
matomo: {
|
||||
title: 'Abholfach',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickUpShelfInListComponent,
|
||||
data: {
|
||||
view: 'list',
|
||||
matomo: {
|
||||
title: 'Abholfach - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list/filter',
|
||||
component: PickupShelfFilterComponent,
|
||||
data: {
|
||||
view: 'filter',
|
||||
matomo: {
|
||||
title: 'Abholfach - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{ path: 'search', component: PickUpShelfInMainSideViewComponent, outlet: 'side' },
|
||||
{ path: 'list', component: PickUpShelfInListComponent, data: { view: 'list' }, outlet: 'side' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfInComponent } from './pickup-shelf-in.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfInDetailsComponent } from './pickup-shelf-in-details/pickup-shelf-in-details.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickUpShelfInMainSideViewComponent } from './pickup-shelf-in-main-side-view/pickup-shelf-in-main-side-view.component';
|
||||
import { PickUpShelfInMainComponent } from './pickup-shelf-in-main/pickup-shelf-in-main.component';
|
||||
import { PickUpShelfInListComponent } from './pickup-shelf-in-list/pickup-shelf-in-list.component';
|
||||
import { PickupShelfInEditComponent } from './pickup-shelf-in-edit/pickup-shelf-in-edit.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfInComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
title: 'Abholfach - Einbuchen',
|
||||
children: [
|
||||
{
|
||||
path: 'main',
|
||||
component: PickUpShelfInMainComponent,
|
||||
data: {
|
||||
view: 'main',
|
||||
matomo: {
|
||||
title: 'Abholfach',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickUpShelfInListComponent,
|
||||
title: 'Abholfach - Trefferliste',
|
||||
data: {
|
||||
view: 'list',
|
||||
matomo: {
|
||||
title: 'Abholfach - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list/filter',
|
||||
component: PickupShelfFilterComponent,
|
||||
title: 'Abholfach - Filter',
|
||||
data: {
|
||||
view: 'filter',
|
||||
matomo: {
|
||||
title: 'Abholfach - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Abholfach - Historie',
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Abholfach - Historie',
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Abholfach - Historie',
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
title: 'Abholfach - Details',
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
title: 'Abholfach - Details',
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
title: 'Abholfach - Details',
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: PickUpShelfInMainSideViewComponent,
|
||||
|
||||
outlet: 'side',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickUpShelfInListComponent,
|
||||
data: { view: 'list' },
|
||||
outlet: 'side',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,75 +1,107 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfOutComponent } from './pickup-shelf-out.component';
|
||||
import { PickupShelfOutMainComponent } from './pickup-shelf-out-main/pickup-shelf-out-main.component';
|
||||
|
||||
import { PickupShelfOutListComponent } from './pickup-shelf-out-list/pickup-shelf-out-list.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfOutDetailsComponent } from './pickup-shelf-out-details/pickup-shelf-out-details.component';
|
||||
import { PickupShelfOutMainSideViewComponent } from './pickup-shelf-out-main-side-view/pickup-shelf-out-main-side-view.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickupShelfOutEditComponent } from './pickup-shelf-out-edit/pickup-shelf-out-edit.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfOutComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{ path: 'main', component: PickupShelfOutMainComponent, data: { view: 'main' } },
|
||||
{ path: 'list', component: PickupShelfOutListComponent, data: { view: 'list' } },
|
||||
{ path: 'list/filter', component: PickupShelfFilterComponent, data: { view: 'filter' } },
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{ path: 'search', component: PickupShelfOutMainSideViewComponent, outlet: 'side' },
|
||||
{ path: 'list', component: PickupShelfOutListComponent, data: { view: 'list' }, outlet: 'side' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfOutComponent } from './pickup-shelf-out.component';
|
||||
import { PickupShelfOutMainComponent } from './pickup-shelf-out-main/pickup-shelf-out-main.component';
|
||||
|
||||
import { PickupShelfOutListComponent } from './pickup-shelf-out-list/pickup-shelf-out-list.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfOutDetailsComponent } from './pickup-shelf-out-details/pickup-shelf-out-details.component';
|
||||
import { PickupShelfOutMainSideViewComponent } from './pickup-shelf-out-main-side-view/pickup-shelf-out-main-side-view.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickupShelfOutEditComponent } from './pickup-shelf-out-edit/pickup-shelf-out-edit.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfOutComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
title: 'Warenausgabe',
|
||||
children: [
|
||||
{
|
||||
path: 'main',
|
||||
component: PickupShelfOutMainComponent,
|
||||
data: { view: 'main' },
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickupShelfOutListComponent,
|
||||
data: { view: 'list' },
|
||||
},
|
||||
{
|
||||
path: 'list/filter',
|
||||
component: PickupShelfFilterComponent,
|
||||
data: { view: 'filter' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Warenausgabe - Verlauf',
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Warenausgabe - Verlauf',
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Warenausgabe - Verlauf',
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: PickupShelfOutMainSideViewComponent,
|
||||
outlet: 'side',
|
||||
data: { view: 'search' },
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickupShelfOutListComponent,
|
||||
data: { view: 'list' },
|
||||
outlet: 'side',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { PageTaskCalendarComponent } from './page-task-calendar.component';
|
||||
import { CalendarComponent } from './pages/calendar/calendar.component';
|
||||
import { CalendarModule } from './pages/calendar/calendar.module';
|
||||
import { TaskSearchComponent } from './pages/task-search';
|
||||
import { TasksComponent } from './pages/tasks/tasks.component';
|
||||
import { TasksModule } from './pages/tasks/tasks.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageTaskCalendarComponent,
|
||||
children: [
|
||||
{ path: 'calendar', component: CalendarComponent },
|
||||
{ path: 'tasks', component: TasksComponent },
|
||||
{ path: 'search', component: TaskSearchComponent },
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'tasks' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), CalendarModule, TasksModule],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageTaskCalendarRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { PageTaskCalendarComponent } from './page-task-calendar.component';
|
||||
import { CalendarComponent } from './pages/calendar/calendar.component';
|
||||
import { CalendarModule } from './pages/calendar/calendar.module';
|
||||
import { TaskSearchComponent } from './pages/task-search';
|
||||
import { TasksComponent } from './pages/tasks/tasks.component';
|
||||
import { TasksModule } from './pages/tasks/tasks.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageTaskCalendarComponent,
|
||||
title: 'Tätigkeitskalender',
|
||||
children: [
|
||||
{ path: 'calendar', component: CalendarComponent },
|
||||
{
|
||||
path: 'tasks',
|
||||
title: 'Tätigkeitskalender - Aufgaben',
|
||||
component: TasksComponent,
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
title: 'Tätigkeitskalender - Suche',
|
||||
component: TaskSearchComponent,
|
||||
},
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'tasks' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), CalendarModule, TasksModule],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageTaskCalendarRoutingModule {}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RewardCatalogComponent,
|
||||
title: 'Prämienshop',
|
||||
resolve: { querySettings: querySettingsResolverFn },
|
||||
data: {
|
||||
scrollPositionRestoration: true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { canDeactivateTabCleanup } from '@isa/core/tabs';
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: ':displayOrderIds',
|
||||
title: 'Prämienshop - Bestellbestätigung',
|
||||
providers: [
|
||||
CoreCommandModule.forChild(OMS_ACTION_HANDLERS).providers ?? [],
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RewardShoppingCartComponent } from './reward-shopping-cart.component';
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
title: 'Prämienshop - Warenkorb',
|
||||
component: RewardShoppingCartComponent,
|
||||
},
|
||||
];
|
||||
|
||||
600
libs/common/title-management/README.md
Normal file
600
libs/common/title-management/README.md
Normal file
@@ -0,0 +1,600 @@
|
||||
# @isa/common/title-management
|
||||
|
||||
> Reusable title management patterns for Angular applications with reactive updates and tab integration.
|
||||
|
||||
## Overview
|
||||
|
||||
This library provides two complementary approaches for managing page titles in the ISA application:
|
||||
|
||||
1. **`IsaTitleStrategy`** - A custom TitleStrategy for route-based static titles
|
||||
2. **`usePageTitle()`** - A reactive helper function for component-based dynamic titles
|
||||
|
||||
Both approaches automatically:
|
||||
- Add the ISA prefix from config to all titles
|
||||
- Update the TabService for multi-tab navigation
|
||||
- Set the browser document title
|
||||
|
||||
## When to Use What
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Static page title (never changes) | Route configuration with `IsaTitleStrategy` |
|
||||
| Dynamic title based on user input (search, filters) | `usePageTitle()` in component |
|
||||
| Title depends on loaded data (item name, ID) | `usePageTitle()` in component |
|
||||
| Wizard/multi-step flows with changing steps | `usePageTitle()` in component |
|
||||
| Combination of static base + dynamic suffix | Both (route + `usePageTitle()`) |
|
||||
|
||||
## Installation
|
||||
|
||||
This library is already installed and configured in your workspace. Import from:
|
||||
|
||||
```typescript
|
||||
import { IsaTitleStrategy, usePageTitle } from '@isa/common/title-management';
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Configure IsaTitleStrategy in AppModule
|
||||
|
||||
To enable automatic title management for all routes, add the `IsaTitleStrategy` to your app providers:
|
||||
|
||||
```typescript
|
||||
// apps/isa-app/src/app/app.module.ts
|
||||
import { TitleStrategy } from '@angular/router';
|
||||
import { IsaTitleStrategy } from '@isa/common/title-management';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{ provide: TitleStrategy, useClass: IsaTitleStrategy }
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
**Note:** This replaces Angular's default `TitleStrategy` with our custom implementation that adds the ISA prefix and updates tabs.
|
||||
|
||||
## Usage
|
||||
|
||||
### Static Titles (Route Configuration)
|
||||
|
||||
For pages with fixed titles, simply add a `title` property to your route:
|
||||
|
||||
```typescript
|
||||
// In your routing module
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
title: 'Dashboard' // Will become "ISA - Dashboard"
|
||||
},
|
||||
{
|
||||
path: 'artikelsuche',
|
||||
component: ArticleSearchComponent,
|
||||
title: 'Artikelsuche' // Will become "ISA - Artikelsuche"
|
||||
},
|
||||
{
|
||||
path: 'returns',
|
||||
component: ReturnsComponent,
|
||||
title: 'Rückgaben' // Will become "ISA - Rückgaben"
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
The `IsaTitleStrategy` will automatically:
|
||||
- Add the configured prefix (default: "ISA")
|
||||
- Update the active tab name
|
||||
- Set the document title
|
||||
|
||||
### Dynamic Titles (Component with Signals)
|
||||
|
||||
For pages where the title depends on component state, use the `usePageTitle()` helper:
|
||||
|
||||
#### Example 1: Search Page with Query Term
|
||||
|
||||
```typescript
|
||||
import { Component, signal, computed } from '@angular/core';
|
||||
import { usePageTitle } from '@isa/common/title-management';
|
||||
|
||||
@Component({
|
||||
selector: 'app-article-search',
|
||||
standalone: true,
|
||||
template: `
|
||||
<input [(ngModel)]="searchTerm" placeholder="Search..." />
|
||||
<h1>{{ pageTitle().title }}</h1>
|
||||
`
|
||||
})
|
||||
export class ArticleSearchComponent {
|
||||
searchTerm = signal('');
|
||||
|
||||
// Computed signal that updates when searchTerm changes
|
||||
pageTitle = computed(() => {
|
||||
const term = this.searchTerm();
|
||||
return {
|
||||
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Title updates automatically when searchTerm changes!
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- Initial load: `ISA - Artikelsuche`
|
||||
- After searching "Laptop": `ISA - Artikelsuche - "Laptop"`
|
||||
- Tab name also updates automatically
|
||||
|
||||
#### Example 2: Detail Page with Item Name
|
||||
|
||||
```typescript
|
||||
import { Component, signal, computed, inject, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { usePageTitle } from '@isa/common/title-management';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-details',
|
||||
standalone: true,
|
||||
template: `<h1>{{ productName() || 'Loading...' }}</h1>`
|
||||
})
|
||||
export class ProductDetailsComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
productName = signal<string | null>(null);
|
||||
|
||||
pageTitle = computed(() => {
|
||||
const name = this.productName();
|
||||
return {
|
||||
title: name ? `Produkt - ${name}` : 'Produkt Details'
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Load product data...
|
||||
this.productName.set('Samsung Galaxy S24');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example 3: Combining Route Title with Dynamic Content
|
||||
|
||||
```typescript
|
||||
import { Component, signal, computed, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { usePageTitle } from '@isa/common/title-management';
|
||||
|
||||
@Component({
|
||||
selector: 'app-order-wizard',
|
||||
standalone: true,
|
||||
template: `<div>Step {{ currentStep() }} of 3</div>`
|
||||
})
|
||||
export class OrderWizardComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
currentStep = signal(1);
|
||||
|
||||
pageTitle = computed(() => {
|
||||
// Get base title from route config
|
||||
const baseTitle = this.route.snapshot.title || 'Bestellung';
|
||||
const step = this.currentStep();
|
||||
return {
|
||||
title: `${baseTitle} - Schritt ${step}/3`
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Components (Parent/Child Routes)
|
||||
|
||||
When using `usePageTitle()` in nested component hierarchies (parent/child routes), the **deepest component automatically wins**. When the child component is destroyed (e.g., navigating away), the parent's title is automatically restored.
|
||||
|
||||
**This happens automatically** - no configuration or depth tracking needed!
|
||||
|
||||
#### Example: Dashboard → Settings Flow
|
||||
|
||||
```typescript
|
||||
// Parent route: /dashboard
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
template: `<router-outlet />`
|
||||
})
|
||||
export class DashboardComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
// Sets: "ISA - Dashboard"
|
||||
}
|
||||
}
|
||||
|
||||
// Child route: /dashboard/settings
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
standalone: true,
|
||||
template: `<h1>Settings</h1>`
|
||||
})
|
||||
export class SettingsComponent {
|
||||
pageTitle = signal({ title: 'Settings' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
// Sets: "ISA - Settings" (child wins!)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation flow:
|
||||
// 1. Navigate to /dashboard → Title: "ISA - Dashboard"
|
||||
// 2. Navigate to /dashboard/settings → Title: "ISA - Settings" (child takes over)
|
||||
// 3. Navigate back to /dashboard → Title: "ISA - Dashboard" (parent restored automatically)
|
||||
```
|
||||
|
||||
#### How It Works
|
||||
|
||||
The library uses an internal registry that tracks component creation order:
|
||||
- **Last-registered (deepest) component controls the title**
|
||||
- **Parent components' title updates are ignored** while child is active
|
||||
- **Automatic cleanup** via Angular's `DestroyRef` - when child is destroyed, parent becomes active again
|
||||
|
||||
#### Real-World Scenario
|
||||
|
||||
```typescript
|
||||
// Main page with search
|
||||
@Component({...})
|
||||
export class ArticleSearchComponent {
|
||||
searchTerm = signal('');
|
||||
|
||||
pageTitle = computed(() => {
|
||||
const term = this.searchTerm();
|
||||
return {
|
||||
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle); // "ISA - Artikelsuche"
|
||||
}
|
||||
}
|
||||
|
||||
// Detail view (child route)
|
||||
@Component({...})
|
||||
export class ArticleDetailsComponent {
|
||||
articleName = signal('Samsung Galaxy S24');
|
||||
|
||||
pageTitle = computed(() => ({
|
||||
title: `Artikel - ${this.articleName()}`
|
||||
}));
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle); // "ISA - Artikel - Samsung Galaxy S24" (wins!)
|
||||
}
|
||||
}
|
||||
|
||||
// When user closes detail view → "ISA - Artikelsuche" is restored automatically
|
||||
```
|
||||
|
||||
#### Important Notes
|
||||
|
||||
✅ **Works with any nesting depth** - Grandparent → Parent → Child → Grandchild
|
||||
|
||||
✅ **No manual depth tracking** - Registration order determines precedence
|
||||
|
||||
✅ **Automatic restoration** - Parent title restored when child is destroyed
|
||||
|
||||
⚠️ **Parent signal updates are ignored** while child is active (by design!)
|
||||
|
||||
### Tab Subtitles
|
||||
|
||||
You can include a subtitle in the signal to display additional context in the tab:
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
this.pageTitle = signal({
|
||||
title: 'Dashboard',
|
||||
subtitle: 'Active Orders'
|
||||
});
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases for Subtitles:**
|
||||
- **Status indicators**: `"Pending"`, `"Active"`, `"Completed"`
|
||||
- **Context information**: `"3 items"`, `"Last updated: 2min ago"`
|
||||
- **Category labels**: `"Customer"`, `"Order"`, `"Product"`
|
||||
- **Step indicators**: `"Step 2 of 5"`, `"Review"`
|
||||
|
||||
#### Example: Order Processing with Status
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-order-details',
|
||||
standalone: true,
|
||||
template: `
|
||||
<h1>Order {{ orderId() }}</h1>
|
||||
<p>Status: {{ orderStatus() }}</p>
|
||||
`
|
||||
})
|
||||
export class OrderDetailsComponent {
|
||||
orderId = signal('12345');
|
||||
orderStatus = signal<'pending' | 'processing' | 'complete'>('pending');
|
||||
|
||||
// Status labels for subtitle
|
||||
statusLabels = {
|
||||
pending: 'Awaiting Payment',
|
||||
processing: 'In Progress',
|
||||
complete: 'Completed'
|
||||
};
|
||||
|
||||
pageTitle = computed(() => ({
|
||||
title: `Order ${this.orderId()}`,
|
||||
subtitle: this.statusLabels[this.orderStatus()]
|
||||
}));
|
||||
|
||||
constructor() {
|
||||
// Title and subtitle both update dynamically
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example: Search Results with Count
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-article-search',
|
||||
standalone: true,
|
||||
template: `...`
|
||||
})
|
||||
export class ArticleSearchComponent {
|
||||
searchTerm = signal('');
|
||||
resultCount = signal(0);
|
||||
|
||||
pageTitle = computed(() => {
|
||||
const term = this.searchTerm();
|
||||
const count = this.resultCount();
|
||||
return {
|
||||
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche',
|
||||
subtitle: `${count} Ergebnis${count === 1 ? '' : 'se'}`
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Title and Subtitle
|
||||
|
||||
Both `title` and `subtitle` are optional. When a property is `undefined`, it will not be updated:
|
||||
|
||||
#### Skip Title Update (Subtitle Only)
|
||||
|
||||
```typescript
|
||||
// Only update the subtitle, keep existing document title
|
||||
pageTitle = signal({ subtitle: '3 items' });
|
||||
usePageTitle(this.pageTitle);
|
||||
```
|
||||
|
||||
#### Skip Subtitle Update (Title Only)
|
||||
|
||||
```typescript
|
||||
// Only update the title, no subtitle
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
usePageTitle(this.pageTitle);
|
||||
```
|
||||
|
||||
#### Skip All Updates
|
||||
|
||||
```typescript
|
||||
// Empty object - skips all updates
|
||||
pageTitle = signal({});
|
||||
usePageTitle(this.pageTitle);
|
||||
```
|
||||
|
||||
#### Conditional Updates
|
||||
|
||||
```typescript
|
||||
// Skip title update when data is not loaded
|
||||
pageTitle = computed(() => {
|
||||
const name = this.productName();
|
||||
return {
|
||||
title: name ? `Product - ${name}` : undefined,
|
||||
subtitle: 'Loading...'
|
||||
};
|
||||
});
|
||||
usePageTitle(this.pageTitle);
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Resolver-Based Titles
|
||||
|
||||
If you're currently using a resolver for titles (e.g., `resolveTitle`), here's how to migrate:
|
||||
|
||||
**Before (with resolver):**
|
||||
```typescript
|
||||
// In resolver file
|
||||
export const resolveTitle: (keyOrTitle: string) => ResolveFn<string> =
|
||||
(keyOrTitle) => (route, state) => {
|
||||
const config = inject(Config);
|
||||
const title = inject(Title);
|
||||
const tabService = inject(TabService);
|
||||
|
||||
const titleFromConfig = config.get(`process.titles.${keyOrTitle}`, z.string().default(keyOrTitle));
|
||||
// ... manual title setting logic
|
||||
return titleFromConfig;
|
||||
};
|
||||
|
||||
// In routing module
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
resolve: { title: resolveTitle('Dashboard') }
|
||||
}
|
||||
```
|
||||
|
||||
**After (with IsaTitleStrategy):**
|
||||
```typescript
|
||||
// No resolver needed - just use route config
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
title: 'Dashboard' // Much simpler!
|
||||
}
|
||||
```
|
||||
|
||||
**For dynamic titles, use usePageTitle() instead:**
|
||||
```typescript
|
||||
// In component
|
||||
pageTitle = computed(() => ({ title: this.dynamicTitle() }));
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `IsaTitleStrategy`
|
||||
|
||||
Custom TitleStrategy implementation that extends Angular's TitleStrategy.
|
||||
|
||||
**Methods:**
|
||||
- `updateTitle(snapshot: RouterStateSnapshot): void` - Called automatically by Angular Router
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/core/config` - For title prefix configuration
|
||||
- `@isa/core/tabs` - For tab name updates
|
||||
- `@angular/platform-browser` - For document title updates
|
||||
|
||||
**Configuration:**
|
||||
```typescript
|
||||
// In app providers
|
||||
{ provide: TitleStrategy, useClass: IsaTitleStrategy }
|
||||
```
|
||||
|
||||
### `usePageTitle(titleSubtitleSignal)`
|
||||
|
||||
Reactive helper function for managing dynamic component titles and subtitles.
|
||||
|
||||
**Parameters:**
|
||||
- `titleSubtitleSignal: Signal<PageTitleInput>` - A signal containing optional title and subtitle
|
||||
|
||||
**Returns:** `void`
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/core/config` - For title prefix configuration
|
||||
- `@isa/core/tabs` - For tab name updates
|
||||
- `@angular/platform-browser` - For document title updates
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const pageTitle = computed(() => ({
|
||||
title: `Search - ${query()}`,
|
||||
subtitle: `${count()} results`
|
||||
}));
|
||||
usePageTitle(pageTitle);
|
||||
```
|
||||
|
||||
### `PageTitleInput`
|
||||
|
||||
Input interface for `usePageTitle()`.
|
||||
|
||||
**Properties:**
|
||||
- `title?: string` - Optional page title (without ISA prefix). When undefined, document title is not updated.
|
||||
- `subtitle?: string` - Optional subtitle to display in the tab. When undefined, tab subtitle is not updated.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Do
|
||||
|
||||
- Use route-based titles for static pages
|
||||
- Use `usePageTitle()` for dynamic content-dependent titles
|
||||
- Keep title signals computed from other signals for reactivity
|
||||
- Use descriptive, user-friendly titles
|
||||
- Combine route titles with component-level refinements for clarity
|
||||
- Use subtitles for status indicators, context info, or step numbers
|
||||
- Return `undefined` for title/subtitle when you want to skip updates
|
||||
|
||||
### ❌ Don't
|
||||
|
||||
- Add the "ISA" prefix manually (it's added automatically)
|
||||
- Call `Title.setTitle()` directly (use these utilities instead)
|
||||
- Create multiple effects updating the same title (use one computed signal)
|
||||
- Put long, complex logic in title computations (keep them simple)
|
||||
|
||||
## Examples from ISA Codebase
|
||||
|
||||
### Artikelsuche (Search with Term)
|
||||
```typescript
|
||||
@Component({...})
|
||||
export class ArticleSearchComponent {
|
||||
searchTerm = signal('');
|
||||
|
||||
pageTitle = computed(() => {
|
||||
const term = this.searchTerm();
|
||||
return {
|
||||
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rückgabe Details (Return with ID)
|
||||
```typescript
|
||||
@Component({...})
|
||||
export class ReturnDetailsComponent {
|
||||
returnId = signal<string | null>(null);
|
||||
|
||||
pageTitle = computed(() => {
|
||||
const id = this.returnId();
|
||||
return {
|
||||
title: id ? `Rückgabe - ${id}` : 'Rückgabe Details'
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests for this library:
|
||||
|
||||
```bash
|
||||
npx nx test common-title-management --skip-nx-cache
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
This library is placed in the **common** domain (not core) because:
|
||||
- It's a reusable utility pattern that features opt into
|
||||
- Components can function without it (unlike core infrastructure)
|
||||
- Provides patterns for solving a recurring problem (page titles)
|
||||
- Similar to other common libraries like decorators and data-access utilities
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- `@isa/core/config` - Configuration management
|
||||
- `@isa/core/tabs` - Multi-tab navigation
|
||||
- `@isa/core/navigation` - Navigation context preservation
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions, refer to the main ISA documentation or contact the development team.
|
||||
34
libs/common/title-management/eslint.config.cjs
Normal file
34
libs/common/title-management/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'common',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'common',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/common/title-management/project.json
Normal file
20
libs/common/title-management/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "common-title-management",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/common/title-management/src",
|
||||
"prefix": "common",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/common/title-management"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
libs/common/title-management/src/index.ts
Normal file
3
libs/common/title-management/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './lib/isa-title.strategy';
|
||||
export * from './lib/use-page-title.function';
|
||||
export * from './lib/title-management.types';
|
||||
157
libs/common/title-management/src/lib/isa-title.strategy.spec.ts
Normal file
157
libs/common/title-management/src/lib/isa-title.strategy.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { RouterStateSnapshot } from '@angular/router';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { signal } from '@angular/core';
|
||||
import { IsaTitleStrategy } from './isa-title.strategy';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { TITLE_PREFIX } from './title-prefix';
|
||||
|
||||
describe('IsaTitleStrategy', () => {
|
||||
let strategy: IsaTitleStrategy;
|
||||
let titleServiceMock: { setTitle: ReturnType<typeof vi.fn>; getTitle: ReturnType<typeof vi.fn> };
|
||||
let tabServiceMock: {
|
||||
activatedTabId: ReturnType<typeof signal<number | null>>;
|
||||
patchTab: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Arrange - Create mocks
|
||||
titleServiceMock = {
|
||||
setTitle: vi.fn(),
|
||||
getTitle: vi.fn().mockReturnValue(''),
|
||||
};
|
||||
|
||||
tabServiceMock = {
|
||||
activatedTabId: signal<number | null>(123),
|
||||
patchTab: vi.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
IsaTitleStrategy,
|
||||
{ provide: Title, useValue: titleServiceMock },
|
||||
{ provide: TITLE_PREFIX, useValue: 'ISA' },
|
||||
{ provide: TabService, useValue: tabServiceMock },
|
||||
],
|
||||
});
|
||||
|
||||
strategy = TestBed.inject(IsaTitleStrategy);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(strategy).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('updateTitle', () => {
|
||||
it('should set document title with ISA prefix', () => {
|
||||
// Arrange
|
||||
const mockSnapshot = {
|
||||
url: '/dashboard',
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
// Mock buildTitle to return a specific title
|
||||
vi.spyOn(strategy, 'buildTitle').mockReturnValue('Dashboard');
|
||||
|
||||
// Act
|
||||
strategy.updateTitle(mockSnapshot);
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
});
|
||||
|
||||
it('should update tab name via TabService', () => {
|
||||
// Arrange
|
||||
const mockSnapshot = {
|
||||
url: '/search',
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
vi.spyOn(strategy, 'buildTitle').mockReturnValue('Artikelsuche');
|
||||
|
||||
// Act
|
||||
strategy.updateTitle(mockSnapshot);
|
||||
|
||||
// Assert
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(123, {
|
||||
name: 'Artikelsuche',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom prefix from TITLE_PREFIX', () => {
|
||||
// Arrange - Reset TestBed and configure with custom TITLE_PREFIX
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
IsaTitleStrategy,
|
||||
{ provide: Title, useValue: titleServiceMock },
|
||||
{ provide: TITLE_PREFIX, useValue: 'MyApp' }, // Custom prefix
|
||||
{ provide: TabService, useValue: tabServiceMock },
|
||||
],
|
||||
});
|
||||
|
||||
const customStrategy = TestBed.inject(IsaTitleStrategy);
|
||||
|
||||
const mockSnapshot = {
|
||||
url: '/settings',
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
vi.spyOn(customStrategy, 'buildTitle').mockReturnValue('Settings');
|
||||
|
||||
// Act
|
||||
customStrategy.updateTitle(mockSnapshot);
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('MyApp - Settings');
|
||||
});
|
||||
|
||||
it('should not update tab when activatedTabId is null', () => {
|
||||
// Arrange
|
||||
tabServiceMock.activatedTabId.set(null);
|
||||
|
||||
const mockSnapshot = {
|
||||
url: '/dashboard',
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
vi.spyOn(strategy, 'buildTitle').mockReturnValue('Dashboard');
|
||||
|
||||
// Act
|
||||
strategy.updateTitle(mockSnapshot);
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update anything when buildTitle returns empty string', () => {
|
||||
// Arrange
|
||||
const mockSnapshot = {
|
||||
url: '/no-title',
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
vi.spyOn(strategy, 'buildTitle').mockReturnValue('');
|
||||
|
||||
// Act
|
||||
strategy.updateTitle(mockSnapshot);
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update anything when buildTitle returns undefined', () => {
|
||||
// Arrange
|
||||
const mockSnapshot = {
|
||||
url: '/no-title',
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
vi.spyOn(strategy, 'buildTitle').mockReturnValue(undefined as any);
|
||||
|
||||
// Act
|
||||
strategy.updateTitle(mockSnapshot);
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
89
libs/common/title-management/src/lib/isa-title.strategy.ts
Normal file
89
libs/common/title-management/src/lib/isa-title.strategy.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Injectable, inject, Injector } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { RouterStateSnapshot, TitleStrategy } from '@angular/router';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { TITLE_PREFIX } from './title-prefix';
|
||||
|
||||
/**
|
||||
* Custom TitleStrategy for the ISA application that:
|
||||
* 1. Automatically adds the ISA prefix from config to all page titles
|
||||
* 2. Updates the TabService with the page title for tab management
|
||||
* 3. Sets the browser document title
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In app.module.ts or app.config.ts
|
||||
* import { TitleStrategy } from '@angular/router';
|
||||
* import { IsaTitleStrategy } from '@isa/common/title-management';
|
||||
*
|
||||
* @NgModule({
|
||||
* providers: [
|
||||
* { provide: TitleStrategy, useClass: IsaTitleStrategy }
|
||||
* ]
|
||||
* })
|
||||
* export class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Usage in routes
|
||||
* const routes: Routes = [
|
||||
* {
|
||||
* path: 'dashboard',
|
||||
* component: DashboardComponent,
|
||||
* title: 'Dashboard' // Will become "ISA - Dashboard"
|
||||
* },
|
||||
* {
|
||||
* path: 'search',
|
||||
* component: SearchComponent,
|
||||
* title: 'Artikelsuche' // Will become "ISA - Artikelsuche"
|
||||
* }
|
||||
* ];
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class IsaTitleStrategy extends TitleStrategy {
|
||||
readonly #title = inject(Title);
|
||||
readonly #injector = inject(Injector);
|
||||
readonly #titlePrefix = inject(TITLE_PREFIX);
|
||||
|
||||
// Lazy injection to avoid circular dependency:
|
||||
// TitleStrategy → TabService → UserStorageProvider → AuthService → Router → TitleStrategy
|
||||
#getTabService() {
|
||||
return this.#injector.get(TabService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the page title when navigation occurs.
|
||||
* This method is called automatically by Angular's Router.
|
||||
*
|
||||
* @param snapshot - The router state snapshot containing route data
|
||||
*/
|
||||
override updateTitle(snapshot: RouterStateSnapshot): void {
|
||||
const pageTitle = this.buildTitle(snapshot);
|
||||
|
||||
if (pageTitle) {
|
||||
this.#updateTitleAndTab(pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates both the document title and the tab name with the given page title.
|
||||
*
|
||||
* @param pageTitle - The page title to set (without prefix)
|
||||
* @private
|
||||
*/
|
||||
#updateTitleAndTab(pageTitle: string): void {
|
||||
const fullTitle = `${this.#titlePrefix} - ${pageTitle}`;
|
||||
|
||||
this.#title.setTitle(fullTitle);
|
||||
|
||||
const tabService = this.#getTabService();
|
||||
const activeTabId = tabService.activatedTabId();
|
||||
if (activeTabId !== null) {
|
||||
tabService.patchTab(activeTabId, {
|
||||
name: pageTitle,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Input type for the `usePageTitle` helper function.
|
||||
* Contains optional title and subtitle properties that can be set independently.
|
||||
*/
|
||||
export interface PageTitleInput {
|
||||
/**
|
||||
* Optional page title (without ISA prefix).
|
||||
* When undefined, the document title will not be updated.
|
||||
* @example
|
||||
* ```typescript
|
||||
* { title: 'Dashboard' }
|
||||
* { title: searchTerm() ? `Search - "${searchTerm()}"` : undefined }
|
||||
* ```
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Optional subtitle to display in the tab.
|
||||
* When undefined, the tab subtitle will not be updated.
|
||||
* Useful for status indicators, context information, or step numbers.
|
||||
* @example
|
||||
* ```typescript
|
||||
* { subtitle: 'Active Orders' }
|
||||
* { subtitle: 'Step 2 of 5' }
|
||||
* { title: 'Order Details', subtitle: 'Pending' }
|
||||
* ```
|
||||
*/
|
||||
subtitle?: string;
|
||||
}
|
||||
14
libs/common/title-management/src/lib/title-prefix.ts
Normal file
14
libs/common/title-management/src/lib/title-prefix.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { inject, InjectionToken } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const TITLE_PREFIX = new InjectionToken(
|
||||
'isa.common.title-management.title-prefix',
|
||||
{
|
||||
providedIn: 'root',
|
||||
factory: () => {
|
||||
const config = inject(Config);
|
||||
return config.get('title', z.string().default('ISA'));
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,255 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { TitleRegistryService } from './title-registry.service';
|
||||
|
||||
describe('TitleRegistryService', () => {
|
||||
let service: TitleRegistryService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [TitleRegistryService],
|
||||
});
|
||||
|
||||
service = TestBed.inject(TitleRegistryService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register and immediately execute the updater', () => {
|
||||
// Arrange
|
||||
const updater = vi.fn();
|
||||
|
||||
// Act
|
||||
service.register(updater);
|
||||
|
||||
// Assert
|
||||
expect(updater).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should return a unique symbol for each registration', () => {
|
||||
// Arrange
|
||||
const updater1 = vi.fn();
|
||||
const updater2 = vi.fn();
|
||||
|
||||
// Act
|
||||
const id1 = service.register(updater1);
|
||||
const id2 = service.register(updater2);
|
||||
|
||||
// Assert
|
||||
expect(id1).not.toBe(id2);
|
||||
expect(typeof id1).toBe('symbol');
|
||||
expect(typeof id2).toBe('symbol');
|
||||
});
|
||||
|
||||
it('should make the last registered updater the active one', () => {
|
||||
// Arrange
|
||||
const updater1 = vi.fn();
|
||||
const updater2 = vi.fn();
|
||||
const updater3 = vi.fn();
|
||||
|
||||
const id1 = service.register(updater1);
|
||||
const id2 = service.register(updater2);
|
||||
const id3 = service.register(updater3);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Only the last registered should execute
|
||||
service.updateIfActive(id1, updater1);
|
||||
service.updateIfActive(id2, updater2);
|
||||
service.updateIfActive(id3, updater3);
|
||||
|
||||
// Assert
|
||||
expect(updater1).not.toHaveBeenCalled();
|
||||
expect(updater2).not.toHaveBeenCalled();
|
||||
expect(updater3).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregister', () => {
|
||||
it('should remove the registration', () => {
|
||||
// Arrange
|
||||
const updater = vi.fn();
|
||||
const id = service.register(updater);
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act
|
||||
service.unregister(id);
|
||||
service.updateIfActive(id, updater);
|
||||
|
||||
// Assert - Updater should not be called after unregistration
|
||||
expect(updater).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should restore the previous registration when active one is unregistered', () => {
|
||||
// Arrange
|
||||
const updater1 = vi.fn();
|
||||
const updater2 = vi.fn();
|
||||
|
||||
const id1 = service.register(updater1);
|
||||
const id2 = service.register(updater2);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Unregister the active one (id2)
|
||||
service.unregister(id2);
|
||||
|
||||
// Assert - updater1 should have been called to restore title
|
||||
expect(updater1).toHaveBeenCalledOnce();
|
||||
expect(updater2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should make the previous registration active after unregistering current active', () => {
|
||||
// Arrange
|
||||
const updater1 = vi.fn();
|
||||
const updater2 = vi.fn();
|
||||
|
||||
const id1 = service.register(updater1);
|
||||
const id2 = service.register(updater2);
|
||||
|
||||
service.unregister(id2);
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - id1 should now be active
|
||||
service.updateIfActive(id1, updater1);
|
||||
service.updateIfActive(id2, updater2);
|
||||
|
||||
// Assert
|
||||
expect(updater1).toHaveBeenCalledOnce();
|
||||
expect(updater2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle unregistering a non-active registration', () => {
|
||||
// Arrange
|
||||
const updater1 = vi.fn();
|
||||
const updater2 = vi.fn();
|
||||
const updater3 = vi.fn();
|
||||
|
||||
const id1 = service.register(updater1);
|
||||
const id2 = service.register(updater2);
|
||||
const id3 = service.register(updater3);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Unregister the middle one (not active)
|
||||
service.unregister(id2);
|
||||
|
||||
// Assert - id3 should still be active, updater2 should not be called
|
||||
service.updateIfActive(id3, updater3);
|
||||
expect(updater2).not.toHaveBeenCalled();
|
||||
expect(updater3).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should handle unregistering when no registrations remain', () => {
|
||||
// Arrange
|
||||
const updater = vi.fn();
|
||||
const id = service.register(updater);
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act
|
||||
service.unregister(id);
|
||||
|
||||
// Assert - No errors should occur
|
||||
service.updateIfActive(id, updater);
|
||||
expect(updater).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIfActive', () => {
|
||||
it('should execute updater only if it is the active registration', () => {
|
||||
// Arrange
|
||||
const updater1 = vi.fn();
|
||||
const updater2 = vi.fn();
|
||||
|
||||
const id1 = service.register(updater1);
|
||||
const id2 = service.register(updater2); // This becomes active
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act
|
||||
service.updateIfActive(id1, updater1);
|
||||
service.updateIfActive(id2, updater2);
|
||||
|
||||
// Assert
|
||||
expect(updater1).not.toHaveBeenCalled(); // Not active
|
||||
expect(updater2).toHaveBeenCalledOnce(); // Active
|
||||
});
|
||||
|
||||
it('should not execute updater for unregistered id', () => {
|
||||
// Arrange
|
||||
const updater = vi.fn();
|
||||
const id = service.register(updater);
|
||||
|
||||
service.unregister(id);
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act
|
||||
service.updateIfActive(id, updater);
|
||||
|
||||
// Assert
|
||||
expect(updater).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested component scenario', () => {
|
||||
it('should handle parent → child → back to parent flow', () => {
|
||||
// Arrange - Simulate parent component
|
||||
const parentUpdater = vi.fn(() => 'Parent Title');
|
||||
const parentId = service.register(parentUpdater);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act 1 - Child component registers
|
||||
const childUpdater = vi.fn(() => 'Child Title');
|
||||
const childId = service.register(childUpdater);
|
||||
|
||||
// Assert 1 - Child is active
|
||||
expect(childUpdater).toHaveBeenCalledOnce();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act 2 - Child component is destroyed
|
||||
service.unregister(childId);
|
||||
|
||||
// Assert 2 - Parent title is restored
|
||||
expect(parentUpdater).toHaveBeenCalledOnce();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act 3 - Parent updates should work
|
||||
service.updateIfActive(parentId, parentUpdater);
|
||||
|
||||
// Assert 3 - Parent is active again
|
||||
expect(parentUpdater).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should handle three-level nesting (grandparent → parent → child)', () => {
|
||||
// Arrange
|
||||
const grandparentUpdater = vi.fn();
|
||||
const parentUpdater = vi.fn();
|
||||
const childUpdater = vi.fn();
|
||||
|
||||
const grandparentId = service.register(grandparentUpdater);
|
||||
const parentId = service.register(parentUpdater);
|
||||
const childId = service.register(childUpdater);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act 1 - Verify child is active
|
||||
service.updateIfActive(childId, childUpdater);
|
||||
expect(childUpdater).toHaveBeenCalledOnce();
|
||||
|
||||
// Act 2 - Remove child, parent should become active
|
||||
service.unregister(childId);
|
||||
expect(parentUpdater).toHaveBeenCalledOnce();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act 3 - Remove parent, grandparent should become active
|
||||
service.unregister(parentId);
|
||||
expect(grandparentUpdater).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Internal service that tracks which component is currently managing the page title.
|
||||
* This ensures that in nested component hierarchies (parent/child routes),
|
||||
* the deepest (most recently registered) component's title takes precedence.
|
||||
*
|
||||
* When a child component is destroyed, the parent component's title is automatically restored.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TitleRegistryService {
|
||||
#registrations = new Map<symbol, () => void>();
|
||||
#activeRegistration: symbol | null = null;
|
||||
|
||||
/**
|
||||
* Register a new title updater. The most recently registered updater
|
||||
* becomes the active one and will control the title.
|
||||
*
|
||||
* This implements a stack-like behavior where the last component to register
|
||||
* (deepest in the component hierarchy) takes precedence.
|
||||
*
|
||||
* @param updater - Function that updates the title
|
||||
* @returns A symbol that uniquely identifies this registration
|
||||
*/
|
||||
register(updater: () => void): symbol {
|
||||
const id = Symbol('title-registration');
|
||||
this.#registrations.set(id, updater);
|
||||
this.#activeRegistration = id;
|
||||
|
||||
// Execute the updater immediately since it's now active
|
||||
updater();
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a title updater. If this was the active updater,
|
||||
* the previous one (if any) becomes active and its title is restored.
|
||||
*
|
||||
* This ensures that when a child component is destroyed, the parent
|
||||
* component's title is automatically restored.
|
||||
*
|
||||
* @param id - The symbol identifying the registration to remove
|
||||
*/
|
||||
unregister(id: symbol): void {
|
||||
this.#registrations.delete(id);
|
||||
|
||||
// If we just unregistered the active one, activate the most recent remaining one
|
||||
if (this.#activeRegistration === id) {
|
||||
// Get the last registration (most recent)
|
||||
const entries = Array.from(this.#registrations.entries());
|
||||
if (entries.length > 0) {
|
||||
const [lastId, lastUpdater] = entries[entries.length - 1];
|
||||
this.#activeRegistration = lastId;
|
||||
// Restore the previous component's title
|
||||
lastUpdater();
|
||||
} else {
|
||||
this.#activeRegistration = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the updater only if it's the currently active registration.
|
||||
*
|
||||
* This prevents inactive (parent) components from overwriting the title
|
||||
* set by active (child) components.
|
||||
*
|
||||
* @param id - The symbol identifying the registration
|
||||
* @param updater - Function to execute if this registration is active
|
||||
*/
|
||||
updateIfActive(id: symbol, updater: () => void): void {
|
||||
if (this.#activeRegistration === id) {
|
||||
updater();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Component, signal, computed } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { usePageTitle } from './use-page-title.function';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { TITLE_PREFIX } from './title-prefix';
|
||||
|
||||
describe('usePageTitle', () => {
|
||||
let titleServiceMock: { setTitle: ReturnType<typeof vi.fn>; getTitle: ReturnType<typeof vi.fn> };
|
||||
let tabServiceMock: {
|
||||
activatedTabId: ReturnType<typeof signal<number | null>>;
|
||||
patchTab: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Arrange - Create mocks
|
||||
titleServiceMock = {
|
||||
setTitle: vi.fn(),
|
||||
getTitle: vi.fn().mockReturnValue(''),
|
||||
};
|
||||
|
||||
tabServiceMock = {
|
||||
activatedTabId: signal<number | null>(456),
|
||||
patchTab: vi.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Title, useValue: titleServiceMock },
|
||||
{ provide: TITLE_PREFIX, useValue: 'ISA' },
|
||||
{ provide: TabService, useValue: tabServiceMock },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should set initial title on component creation', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
name: 'Dashboard',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update title when signal changes', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
searchTerm = signal('');
|
||||
pageTitle = computed(() => {
|
||||
const term = this.searchTerm();
|
||||
return {
|
||||
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche',
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const component = fixture.componentInstance;
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Clear previous calls
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act
|
||||
component.searchTerm.set('Laptop');
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith(
|
||||
'ISA - Artikelsuche - "Laptop"'
|
||||
);
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
name: 'Artikelsuche - "Laptop"',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update tab when activatedTabId is null', () => {
|
||||
// Arrange
|
||||
tabServiceMock.activatedTabId.set(null);
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ title: 'Profile' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Profile');
|
||||
expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use custom prefix from TITLE_PREFIX', () => {
|
||||
// Arrange
|
||||
TestBed.overrideProvider(TITLE_PREFIX, { useValue: 'MyApp' });
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ title: 'About' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('MyApp - About');
|
||||
});
|
||||
|
||||
it('should handle multiple signal updates correctly', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
counter = signal(0);
|
||||
pageTitle = computed(() => ({ title: `Page ${this.counter()}` }));
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const component = fixture.componentInstance;
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Clear initial call
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act
|
||||
component.counter.set(1);
|
||||
TestBed.flushEffects();
|
||||
component.counter.set(2);
|
||||
TestBed.flushEffects();
|
||||
component.counter.set(3);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledTimes(3);
|
||||
expect(titleServiceMock.setTitle).toHaveBeenNthCalledWith(1, 'ISA - Page 1');
|
||||
expect(titleServiceMock.setTitle).toHaveBeenNthCalledWith(2, 'ISA - Page 2');
|
||||
expect(titleServiceMock.setTitle).toHaveBeenNthCalledWith(3, 'ISA - Page 3');
|
||||
});
|
||||
|
||||
describe('nested components', () => {
|
||||
it('should prioritize child component title over parent', () => {
|
||||
// Arrange - Parent component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Parent</div>',
|
||||
})
|
||||
class ParentComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Child component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Child</div>',
|
||||
})
|
||||
class ChildComponent {
|
||||
pageTitle = signal({ title: 'Settings' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Create parent first
|
||||
TestBed.createComponent(ParentComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Create child (should win)
|
||||
TestBed.createComponent(ChildComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert - Child title should be set
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Settings');
|
||||
});
|
||||
|
||||
it('should restore parent title when child component is destroyed', () => {
|
||||
// Arrange - Parent component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Parent</div>',
|
||||
})
|
||||
class ParentComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Child component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Child</div>',
|
||||
})
|
||||
class ChildComponent {
|
||||
pageTitle = signal({ title: 'Settings' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Create both components
|
||||
TestBed.createComponent(ParentComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
const childFixture = TestBed.createComponent(ChildComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Destroy child
|
||||
childFixture.destroy();
|
||||
|
||||
// Assert - Parent title should be restored
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
});
|
||||
|
||||
it('should handle parent title updates when child is active', () => {
|
||||
// Arrange - Parent component with mutable signal
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Parent</div>',
|
||||
})
|
||||
class ParentComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Child component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Child</div>',
|
||||
})
|
||||
class ChildComponent {
|
||||
pageTitle = signal({ title: 'Settings' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Create both
|
||||
const parentFixture = TestBed.createComponent(ParentComponent);
|
||||
const parent = parentFixture.componentInstance;
|
||||
TestBed.flushEffects();
|
||||
|
||||
TestBed.createComponent(ChildComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Parent tries to update (should be ignored while child is active)
|
||||
parent.pageTitle.set({ title: 'Dashboard Updated' });
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert - Title should still be child's (parent update ignored)
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow parent title updates after child is destroyed', () => {
|
||||
// Arrange - Parent component with mutable signal
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Parent</div>',
|
||||
})
|
||||
class ParentComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Child component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Child</div>',
|
||||
})
|
||||
class ChildComponent {
|
||||
pageTitle = signal({ title: 'Settings' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Create both
|
||||
const parentFixture = TestBed.createComponent(ParentComponent);
|
||||
const parent = parentFixture.componentInstance;
|
||||
TestBed.flushEffects();
|
||||
|
||||
const childFixture = TestBed.createComponent(ChildComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Destroy child
|
||||
childFixture.destroy();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Parent updates now (should work)
|
||||
parent.pageTitle.set({ title: 'Dashboard Updated' });
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert - Parent update should be reflected
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard Updated');
|
||||
});
|
||||
|
||||
it('should handle three-level nesting (grandparent → parent → child)', () => {
|
||||
// Arrange - Three levels of components
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Grandparent</div>',
|
||||
})
|
||||
class GrandparentComponent {
|
||||
pageTitle = signal({ title: 'Main' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Parent</div>',
|
||||
})
|
||||
class ParentComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Child</div>',
|
||||
})
|
||||
class ChildComponent {
|
||||
pageTitle = signal({ title: 'Settings' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Create all three
|
||||
TestBed.createComponent(GrandparentComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
TestBed.createComponent(ParentComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
const childFixture = TestBed.createComponent(ChildComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Verify child is active
|
||||
expect(titleServiceMock.setTitle).toHaveBeenLastCalledWith('ISA - Settings');
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Destroy child
|
||||
childFixture.destroy();
|
||||
|
||||
// Assert - Parent title should be restored
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('subtitle', () => {
|
||||
it('should set tab subtitle when provided', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ title: 'Dashboard', subtitle: 'Overview' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
name: 'Dashboard',
|
||||
subtitle: 'Overview',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include subtitle in patchTab when not provided', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
const callArgs = tabServiceMock.patchTab.mock.calls[0];
|
||||
expect(callArgs[0]).toBe(456);
|
||||
expect(callArgs[1]).toEqual({ name: 'Dashboard' });
|
||||
expect(callArgs[1]).not.toHaveProperty('subtitle');
|
||||
});
|
||||
|
||||
it('should maintain subtitle across title signal changes', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
searchTerm = signal('');
|
||||
pageTitle = computed(() => {
|
||||
const term = this.searchTerm();
|
||||
return {
|
||||
title: term ? `Search - "${term}"` : 'Search',
|
||||
subtitle: 'Active',
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const component = fixture.componentInstance;
|
||||
TestBed.flushEffects();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act
|
||||
component.searchTerm.set('Laptop');
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
name: 'Search - "Laptop"',
|
||||
subtitle: 'Active',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested components each with different subtitles', () => {
|
||||
// Arrange - Parent component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Parent</div>',
|
||||
})
|
||||
class ParentComponent {
|
||||
pageTitle = signal({ title: 'Dashboard', subtitle: 'Main' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Child component
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Child</div>',
|
||||
})
|
||||
class ChildComponent {
|
||||
pageTitle = signal({ title: 'Settings', subtitle: 'Preferences' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Create parent first
|
||||
TestBed.createComponent(ParentComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
name: 'Dashboard',
|
||||
subtitle: 'Main',
|
||||
});
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Create child (should win)
|
||||
TestBed.createComponent(ChildComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert - Child subtitle should be set
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
name: 'Settings',
|
||||
subtitle: 'Preferences',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('optional title/subtitle', () => {
|
||||
it('should skip document title update when title is undefined', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ subtitle: 'Loading' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
subtitle: 'Loading',
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip tab subtitle update when subtitle is undefined', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ title: 'Dashboard' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
const callArgs = tabServiceMock.patchTab.mock.calls[0];
|
||||
expect(callArgs[1]).toEqual({ name: 'Dashboard' });
|
||||
expect(callArgs[1]).not.toHaveProperty('subtitle');
|
||||
});
|
||||
|
||||
it('should handle empty object (skip all updates)', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({});
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle dynamic changes from defined to undefined', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
showTitle = signal(true);
|
||||
pageTitle = computed(() => ({
|
||||
title: this.showTitle() ? 'Dashboard' : undefined,
|
||||
subtitle: 'Active',
|
||||
}));
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const component = fixture.componentInstance;
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Verify initial state
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Hide title
|
||||
component.showTitle.set(false);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert - Title should not be updated, but subtitle should
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
subtitle: 'Active',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle both title and subtitle', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
pageTitle = signal({ title: 'Orders', subtitle: '3 items' });
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
TestBed.createComponent(TestComponent);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert
|
||||
expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Orders');
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
name: 'Orders',
|
||||
subtitle: '3 items',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle subtitle only (no title)', () => {
|
||||
// Arrange
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
count = signal(5);
|
||||
pageTitle = computed(() => ({
|
||||
subtitle: `${this.count()} items`,
|
||||
}));
|
||||
|
||||
constructor() {
|
||||
usePageTitle(this.pageTitle);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const component = fixture.componentInstance;
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert initial
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
subtitle: '5 items',
|
||||
});
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Act - Update count
|
||||
component.count.set(10);
|
||||
TestBed.flushEffects();
|
||||
|
||||
// Assert - Only subtitle updates
|
||||
expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
|
||||
expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
|
||||
subtitle: '10 items',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
203
libs/common/title-management/src/lib/use-page-title.function.ts
Normal file
203
libs/common/title-management/src/lib/use-page-title.function.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { inject, effect, Signal, DestroyRef } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { PageTitleInput } from './title-management.types';
|
||||
import { TitleRegistryService } from './title-registry.service';
|
||||
import { TITLE_PREFIX } from './title-prefix';
|
||||
|
||||
/**
|
||||
* Reactive helper function for managing dynamic page titles and subtitles in components.
|
||||
* Uses Angular signals and effects to automatically update the document title
|
||||
* and tab name/subtitle whenever the provided signal changes.
|
||||
*
|
||||
* This is ideal for pages where the title depends on component state, such as:
|
||||
* - Search pages with query terms
|
||||
* - Detail pages with item names
|
||||
* - Wizard flows with step names
|
||||
* - Filter pages with applied filters
|
||||
* - Status indicators with changing subtitles
|
||||
*
|
||||
* @param titleSubtitleSignal - A signal containing optional title and subtitle
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage with static title
|
||||
* import { Component, signal } from '@angular/core';
|
||||
* import { usePageTitle } from '@isa/common/title-management';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app-dashboard',
|
||||
* template: `<h1>Dashboard</h1>`
|
||||
* })
|
||||
* export class DashboardComponent {
|
||||
* pageTitle = signal({ title: 'Dashboard' });
|
||||
*
|
||||
* constructor() {
|
||||
* usePageTitle(this.pageTitle);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Dynamic title with search term
|
||||
* import { Component, signal, computed } from '@angular/core';
|
||||
* import { usePageTitle } from '@isa/common/title-management';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app-article-search',
|
||||
* template: `<input [(ngModel)]="searchTerm" />`
|
||||
* })
|
||||
* export class ArticleSearchComponent {
|
||||
* searchTerm = signal('');
|
||||
*
|
||||
* pageTitle = computed(() => {
|
||||
* const term = this.searchTerm();
|
||||
* return {
|
||||
* title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
|
||||
* };
|
||||
* });
|
||||
*
|
||||
* constructor() {
|
||||
* usePageTitle(this.pageTitle);
|
||||
* // Title updates automatically when searchTerm changes!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Title with subtitle
|
||||
* import { Component, signal, computed } from '@angular/core';
|
||||
* import { usePageTitle } from '@isa/common/title-management';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app-order-details',
|
||||
* template: `<h1>Order Details</h1>`
|
||||
* })
|
||||
* export class OrderDetailsComponent {
|
||||
* orderId = signal('12345');
|
||||
* orderStatus = signal<'pending' | 'processing' | 'complete'>('pending');
|
||||
*
|
||||
* statusLabels = {
|
||||
* pending: 'Awaiting Payment',
|
||||
* processing: 'In Progress',
|
||||
* complete: 'Completed'
|
||||
* };
|
||||
*
|
||||
* pageTitle = computed(() => ({
|
||||
* title: `Order ${this.orderId()}`,
|
||||
* subtitle: this.statusLabels[this.orderStatus()]
|
||||
* }));
|
||||
*
|
||||
* constructor() {
|
||||
* usePageTitle(this.pageTitle);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Skip title when undefined (e.g., data not loaded yet)
|
||||
* import { Component, signal, computed } from '@angular/core';
|
||||
* import { usePageTitle } from '@isa/common/title-management';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app-product-details',
|
||||
* template: `<h1>{{ productName() || 'Loading...' }}</h1>`
|
||||
* })
|
||||
* export class ProductDetailsComponent {
|
||||
* productName = signal<string | null>(null);
|
||||
*
|
||||
* pageTitle = computed(() => {
|
||||
* const name = this.productName();
|
||||
* return {
|
||||
* title: name ? `Product - ${name}` : undefined
|
||||
* };
|
||||
* });
|
||||
*
|
||||
* constructor() {
|
||||
* usePageTitle(this.pageTitle);
|
||||
* // Title only updates when productName is not null
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Nested components - child component's title automatically takes precedence
|
||||
* // Parent component (route: /dashboard)
|
||||
* export class DashboardComponent {
|
||||
* pageTitle = signal({ title: 'Dashboard' });
|
||||
* constructor() {
|
||||
* usePageTitle(this.pageTitle); // "ISA - Dashboard"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Child component (route: /dashboard/settings)
|
||||
* export class SettingsComponent {
|
||||
* pageTitle = signal({ title: 'Settings' });
|
||||
* constructor() {
|
||||
* usePageTitle(this.pageTitle); // "ISA - Settings" (wins!)
|
||||
* }
|
||||
* }
|
||||
* // When SettingsComponent is destroyed → "ISA - Dashboard" (automatically restored)
|
||||
* ```
|
||||
*/
|
||||
export function usePageTitle(
|
||||
titleSubtitleSignal: Signal<PageTitleInput>
|
||||
): void {
|
||||
const title = inject(Title);
|
||||
const titlePrefix = inject(TITLE_PREFIX);
|
||||
const tabService = inject(TabService);
|
||||
const registry = inject(TitleRegistryService);
|
||||
const destroyRef = inject(DestroyRef);
|
||||
|
||||
// Create the updater function that will be called by the registry
|
||||
const updateTitle = () => {
|
||||
const { title: pageTitle, subtitle } = titleSubtitleSignal();
|
||||
|
||||
// Update document title if title is defined
|
||||
if (pageTitle !== undefined) {
|
||||
const fullTitle = `${titlePrefix} - ${pageTitle}`;
|
||||
title.setTitle(fullTitle);
|
||||
}
|
||||
|
||||
// Update tab if activeTabId exists
|
||||
const activeTabId = tabService.activatedTabId();
|
||||
if (activeTabId !== null) {
|
||||
// Build patch object conditionally based on what's defined
|
||||
const patch: { name?: string; subtitle?: string } = {};
|
||||
|
||||
if (pageTitle !== undefined) {
|
||||
patch.name = pageTitle;
|
||||
}
|
||||
|
||||
if (subtitle !== undefined) {
|
||||
patch.subtitle = subtitle;
|
||||
}
|
||||
|
||||
// Only patch if we have something to update
|
||||
if (Object.keys(patch).length > 0) {
|
||||
tabService.patchTab(activeTabId, patch);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Register with the registry (this will execute updateTitle immediately)
|
||||
const registrationId = registry.register(updateTitle);
|
||||
|
||||
// Automatically unregister when component is destroyed
|
||||
destroyRef.onDestroy(() => {
|
||||
registry.unregister(registrationId);
|
||||
});
|
||||
|
||||
// React to signal changes, but only update if this is the active registration
|
||||
effect(() => {
|
||||
// Access the signal to track it as a dependency
|
||||
titleSubtitleSignal();
|
||||
|
||||
// Only update if this component is the active one
|
||||
registry.updateIfActive(registrationId, updateTitle);
|
||||
});
|
||||
}
|
||||
13
libs/common/title-management/src/test-setup.ts
Normal file
13
libs/common/title-management/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/common/title-management/tsconfig.json
Normal file
30
libs/common/title-management/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/common/title-management/tsconfig.lib.json
Normal file
27
libs/common/title-management/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/common/title-management/tsconfig.spec.json
Normal file
29
libs/common/title-management/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
33
libs/common/title-management/vite.config.mts
Normal file
33
libs/common/title-management/vite.config.mts
Normal file
@@ -0,0 +1,33 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default
|
||||
// @ts-expect-error - Vitest reporter tuple types have complex inference issues, but config works correctly at runtime
|
||||
defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/common/title-management',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: [
|
||||
'default',
|
||||
['junit', { outputFile: '../../../testresults/junit-common-title-management.xml' }],
|
||||
],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/common/title-management',
|
||||
provider: 'v8' as const,
|
||||
reporter: ['text', 'cobertura'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { inject, Injectable, Injector } from '@angular/core';
|
||||
import { StorageProvider } from './storage-provider';
|
||||
import {
|
||||
ResponseArgsOfUserState,
|
||||
@@ -32,9 +32,13 @@ const DEFAULT_USER_STATE_RESPONSE: ResponseArgsOfUserState = {
|
||||
export class UserStorageProvider implements StorageProvider {
|
||||
#logger = logger(() => ({ service: 'UserStorageProvider' }));
|
||||
#userStateService = inject(UserStateService);
|
||||
#authService = inject(AuthService);
|
||||
#injector = inject(Injector);
|
||||
|
||||
#loadUserState = this.#authService.authenticated$.pipe(
|
||||
#getAuthService() {
|
||||
return this.#injector.get(AuthService);
|
||||
}
|
||||
|
||||
#loadUserState = this.#getAuthService().authenticated$.pipe(
|
||||
filter((authenticated) => authenticated),
|
||||
switchMap(() =>
|
||||
this.#userStateService.UserStateGetUserState().pipe(
|
||||
|
||||
@@ -142,6 +142,7 @@ Each tab is stored as an NgRx entity with the following structure:
|
||||
interface Tab {
|
||||
id: number; // Unique identifier
|
||||
name: string; // Display name
|
||||
subtitle: string; // Subtitle (defaults to empty string)
|
||||
createdAt: number; // Creation timestamp (ms)
|
||||
activatedAt?: number; // Last activation timestamp
|
||||
metadata: Record<string, unknown>; // Flexible metadata storage
|
||||
@@ -295,6 +296,7 @@ Creates and adds a new tab to the store.
|
||||
**Parameters:**
|
||||
- `params: AddTabInput` - Tab creation parameters (validated with Zod)
|
||||
- `name?: string` - Display name (default: 'Neuer Vorgang')
|
||||
- `subtitle?: string` - Subtitle (default: '')
|
||||
- `tags?: string[]` - Initial tags (default: [])
|
||||
- `metadata?: Record<string, unknown>` - Initial metadata (default: {})
|
||||
- `id?: number` - Optional ID (auto-generated if omitted)
|
||||
@@ -306,6 +308,7 @@ Creates and adds a new tab to the store.
|
||||
```typescript
|
||||
const tab = this.#tabService.addTab({
|
||||
name: 'Customer Order #1234',
|
||||
subtitle: 'Pending approval',
|
||||
tags: ['order', 'customer'],
|
||||
metadata: { orderId: 1234, status: 'pending' }
|
||||
});
|
||||
@@ -337,6 +340,7 @@ Partially updates a tab's properties.
|
||||
- `id: number` - Tab ID to update
|
||||
- `changes: PatchTabInput` - Partial tab updates
|
||||
- `name?: string` - Updated display name
|
||||
- `subtitle?: string` - Updated subtitle
|
||||
- `tags?: string[]` - Updated tags array
|
||||
- `metadata?: Record<string, unknown>` - Metadata to merge
|
||||
- `location?: TabLocationHistory` - Updated location history
|
||||
@@ -345,6 +349,7 @@ Partially updates a tab's properties.
|
||||
```typescript
|
||||
this.#tabService.patchTab(42, {
|
||||
name: 'Updated Name',
|
||||
subtitle: 'New subtitle',
|
||||
tags: ['new-tag'],
|
||||
metadata: { additionalField: 'value' }
|
||||
});
|
||||
|
||||
@@ -100,6 +100,8 @@ export const TabSchema = z.object({
|
||||
id: z.number(),
|
||||
/** Display name for the tab (minimum 1 character) */
|
||||
name: z.string().default('Neuer Vorgang'),
|
||||
/** Subtitle for the tab */
|
||||
subtitle: z.string().default(''),
|
||||
/** Creation timestamp (milliseconds since epoch) */
|
||||
createdAt: z.number(),
|
||||
/** Last activation timestamp (optional) */
|
||||
@@ -123,6 +125,8 @@ export interface Tab {
|
||||
id: number;
|
||||
/** Display name for the tab */
|
||||
name: string;
|
||||
/** Subtitle for the tab */
|
||||
subtitle: string;
|
||||
/** Creation timestamp */
|
||||
createdAt: number;
|
||||
/** Last activation timestamp */
|
||||
@@ -146,6 +150,8 @@ export interface TabCreate {
|
||||
id?: number;
|
||||
/** Display name for the tab */
|
||||
name: string;
|
||||
/** Subtitle for the tab */
|
||||
subtitle?: string;
|
||||
/** Creation timestamp */
|
||||
createdAt: number;
|
||||
/** Last activation timestamp */
|
||||
@@ -170,6 +176,8 @@ export const PersistedTabSchema = z
|
||||
id: z.number(),
|
||||
/** Tab display name */
|
||||
name: z.string().default('Neuer Vorgang'),
|
||||
/** Tab subtitle */
|
||||
subtitle: z.string(),
|
||||
/** Creation timestamp */
|
||||
createdAt: z.number(),
|
||||
/** Last activation timestamp */
|
||||
@@ -195,6 +203,8 @@ export type TabInput = z.input<typeof TabSchema>;
|
||||
export const AddTabSchema = z.object({
|
||||
/** Display name for the new tab */
|
||||
name: z.string().default('Neuer Vorgang'),
|
||||
/** Subtitle for the new tab */
|
||||
subtitle: z.string().default(''),
|
||||
/** Initial tags for the tab */
|
||||
tags: TabTagsSchema,
|
||||
/** Initial metadata for the tab */
|
||||
@@ -221,6 +231,8 @@ export const TabUpdateSchema = z
|
||||
.object({
|
||||
/** Updated display name */
|
||||
name: z.string().min(1).optional(),
|
||||
/** Updated subtitle */
|
||||
subtitle: z.string().optional(),
|
||||
/** Updated activation timestamp */
|
||||
activatedAt: z.number().optional(),
|
||||
/** Updated metadata object */
|
||||
|
||||
@@ -63,6 +63,7 @@ export const TabService = signalStore(
|
||||
const tab: Tab = {
|
||||
id: parsed.id ?? store._generateId(),
|
||||
name: parsed.name,
|
||||
subtitle: parsed.subtitle,
|
||||
createdAt: Date.now(),
|
||||
activatedAt: parsed.activatedAt,
|
||||
tags: parsed.tags,
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { ReturnSearchMainComponent } from './return-search-main/return-search-main.component';
|
||||
import { ReturnSearchComponent } from './return-search.component';
|
||||
import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
|
||||
import { ReturnSearchResultComponent } from './return-search-result/return-search-result.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ReturnSearchComponent,
|
||||
resolve: { querySettings: querySettingsResolverFn },
|
||||
children: [
|
||||
{ path: '', component: ReturnSearchMainComponent },
|
||||
{
|
||||
path: 'receipts',
|
||||
component: ReturnSearchResultComponent,
|
||||
data: { scrollPositionRestoration: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'receipt',
|
||||
loadChildren: () =>
|
||||
import('@isa/oms/feature/return-details').then((feat) => feat.routes),
|
||||
},
|
||||
{
|
||||
path: 'process',
|
||||
loadChildren: () =>
|
||||
import('@isa/oms/feature/return-process').then((feat) => feat.routes),
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { ReturnSearchMainComponent } from './return-search-main/return-search-main.component';
|
||||
import { ReturnSearchComponent } from './return-search.component';
|
||||
import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
|
||||
import { ReturnSearchResultComponent } from './return-search-result/return-search-result.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ReturnSearchComponent,
|
||||
resolve: { querySettings: querySettingsResolverFn },
|
||||
title: 'Retoure',
|
||||
children: [
|
||||
{ path: '', component: ReturnSearchMainComponent },
|
||||
{
|
||||
path: 'receipts',
|
||||
component: ReturnSearchResultComponent,
|
||||
data: { scrollPositionRestoration: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'receipt',
|
||||
title: 'Retoure - Beleg',
|
||||
loadChildren: () =>
|
||||
import('@isa/oms/feature/return-details').then((feat) => feat.routes),
|
||||
},
|
||||
{
|
||||
path: 'process',
|
||||
title: 'Retoure - Prozess',
|
||||
loadChildren: () =>
|
||||
import('@isa/oms/feature/return-process').then((feat) => feat.routes),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { RemissionListComponent } from './remission-list.component';
|
||||
import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
|
||||
import { querySettingsDepartmentResolverFn } from './resolvers/query-settings-department.resolver-fn';
|
||||
import { RemissionListType } from '@isa/remission/data-access';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'mandatory',
|
||||
component: RemissionListComponent,
|
||||
resolve: { querySettings: querySettingsResolverFn },
|
||||
data: {
|
||||
scrollPositionRestoration: true,
|
||||
remiType: RemissionListType.Pflicht,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'department',
|
||||
component: RemissionListComponent,
|
||||
resolve: { querySettings: querySettingsDepartmentResolverFn },
|
||||
data: {
|
||||
scrollPositionRestoration: true,
|
||||
remiType: RemissionListType.Abteilung,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'mandatory',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { RemissionListComponent } from './remission-list.component';
|
||||
import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
|
||||
import { querySettingsDepartmentResolverFn } from './resolvers/query-settings-department.resolver-fn';
|
||||
import { RemissionListType } from '@isa/remission/data-access';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'mandatory',
|
||||
component: RemissionListComponent,
|
||||
resolve: { querySettings: querySettingsResolverFn },
|
||||
title: 'Remission - Pflicht',
|
||||
data: {
|
||||
scrollPositionRestoration: true,
|
||||
remiType: RemissionListType.Pflicht,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'department',
|
||||
component: RemissionListComponent,
|
||||
resolve: { querySettings: querySettingsDepartmentResolverFn },
|
||||
title: 'Remission - Abteilung',
|
||||
data: {
|
||||
scrollPositionRestoration: true,
|
||||
remiType: RemissionListType.Abteilung,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'mandatory',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,12 +4,14 @@ export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RemissionReturnReceiptListComponent,
|
||||
title: 'Remission',
|
||||
data: {
|
||||
scrollPositionRestoration: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':returnId/:receiptId',
|
||||
title: 'Remission - Beleg',
|
||||
loadComponent: () =>
|
||||
import('@isa/remission/feature/remission-return-receipt-details').then(
|
||||
(m) => m.RemissionReturnReceiptDetailsComponent,
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -13674,9 +13674,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
||||
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
"@isa/common/data-access": ["libs/common/data-access/src/index.ts"],
|
||||
"@isa/common/decorators": ["libs/common/decorators/src/index.ts"],
|
||||
"@isa/common/print": ["libs/common/print/src/index.ts"],
|
||||
"@isa/common/title-management": [
|
||||
"libs/common/title-management/src/index.ts"
|
||||
],
|
||||
"@isa/core/auth": ["libs/core/auth/src/index.ts"],
|
||||
"@isa/core/config": ["libs/core/config/src/index.ts"],
|
||||
"@isa/core/logging": ["libs/core/logging/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user