From 33026c064fc7fa0159c592b5633fd71666d326fd Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Thu, 11 Dec 2025 13:54:39 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(core-tabs):=20add=20pattern-ba?= =?UTF-8?q?sed=20URL=20exclusion=20and=20tab=20deactivation=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change URL blacklist from exact match to prefix matching - Rename HISTORY_BLACKLIST_URLS to HISTORY_BLACKLIST_PATTERNS - Add deactivateTab() method to TabService - Add deactivateTabGuard for routes outside tab context - Apply guard to dashboard route in app.routes.ts - Update README with new features and API documentation Routes like /dashboard and /kunde/dashboard now properly deactivate the active tab when navigated to. --- apps/isa-app/src/app/app.routes.ts | 2 + libs/core/tabs/README.md | 73 +++++++++++++++---- libs/core/tabs/src/index.ts | 1 + .../core/tabs/src/lib/deactivate-tab.guard.ts | 35 +++++++++ .../tabs/src/lib/tab-navigation.constants.ts | 17 +++-- .../tabs/src/lib/tab-navigation.service.ts | 11 ++- libs/core/tabs/src/lib/tab.ts | 7 ++ 7 files changed, 122 insertions(+), 24 deletions(-) create mode 100644 libs/core/tabs/src/lib/deactivate-tab.guard.ts diff --git a/apps/isa-app/src/app/app.routes.ts b/apps/isa-app/src/app/app.routes.ts index 8edf7c84a..7cbbda7cc 100644 --- a/apps/isa-app/src/app/app.routes.ts +++ b/apps/isa-app/src/app/app.routes.ts @@ -25,6 +25,7 @@ import { tabResolverFn, processResolverFn, hasTabIdGuard, + deactivateTabGuard, } from '@isa/core/tabs'; export const routes: Routes = [ @@ -47,6 +48,7 @@ export const routes: Routes = [ path: 'dashboard', loadChildren: () => import('@page/dashboard').then((m) => m.DashboardModule), + canActivate: [deactivateTabGuard], data: { matomo: { title: 'Dashboard', diff --git a/libs/core/tabs/README.md b/libs/core/tabs/README.md index 3cdcb0afd..762feadc6 100644 --- a/libs/core/tabs/README.md +++ b/libs/core/tabs/README.md @@ -32,6 +32,7 @@ The Core Tabs library provides a comprehensive solution for managing multiple ta - **Metadata system** - Flexible per-tab metadata storage - **Angular DevTools integration** - Debug tab state with Redux DevTools - **Router resolver** - Automatic tab creation from route parameters +- **Tab deactivation guard** - Deactivate tabs when entering global routes - **Injection helpers** - Convenient signal-based injection functions - **Navigate back component** - Ready-to-use back button component @@ -332,6 +333,20 @@ Activates a tab by ID and updates its activation timestamp. this.#tabService.activateTab(42); ``` +##### `deactivateTab(): void` + +Deactivates the currently active tab by setting `activatedTabId` to null. + +**Use Cases:** +- Navigating to global routes (dashboard, branch operations) +- Exiting tab context without closing the tab + +**Example:** +```typescript +this.#tabService.deactivateTab(); +// activatedTabId is now null +``` + ##### `patchTab(id: number, changes: PatchTabInput): void` Partially updates a tab's properties. @@ -666,6 +681,42 @@ export class CustomerComponent { } ``` +### Deactivate Tab Guard + +#### `deactivateTabGuard: CanActivateFn` + +Angular Router guard that deactivates the active tab when entering a route. + +**Use Case:** Routes that exist outside of a process/tab context, such as dashboard pages or global branch operations. + +**Behavior:** +- Sets `activatedTabId` to null +- Logs the deactivation with previous tab ID +- Always returns true (allows navigation) + +**Route Configuration:** +```typescript +import { Routes } from '@angular/router'; +import { deactivateTabGuard } from '@isa/core/tabs'; + +export const routes: Routes = [ + { + path: 'dashboard', + loadChildren: () => import('./dashboard').then(m => m.DashboardModule), + canActivate: [deactivateTabGuard] + }, + { + path: 'filiale/task-calendar', + loadChildren: () => import('./task-calendar').then(m => m.TaskCalendarModule), + canActivate: [deactivateTabGuard] + } +]; +``` + +**Why use a guard instead of TabNavigationService?** + +Tab activation happens in route resolvers (`tabResolverFn`), so tab deactivation should also happen in route guards for consistency. This keeps all tab activation/deactivation logic within the Angular Router lifecycle. + ### NavigateBackButtonComponent Ready-to-use back button component with automatic state management. @@ -1209,12 +1260,18 @@ The service recognizes two URL patterns: ### URL Blacklist -Certain URLs are excluded from history tracking: +Certain URL patterns are excluded from history tracking using prefix matching: ```typescript -export const HISTORY_BLACKLIST_URLS = ['/kunde/dashboard']; +export const HISTORY_BLACKLIST_PATTERNS = ['/kunde/dashboard', '/dashboard']; ``` +**Behavior:** +- Uses prefix matching (not exact match) +- `/kunde/dashboard` excludes `/kunde/dashboard`, `/kunde/dashboard/`, `/kunde/dashboard/stats`, etc. +- `/dashboard` excludes `/dashboard`, `/dashboard/overview`, etc. +- Does NOT exclude `/kunde/123/dashboard` (different prefix) + ### Router Resolver Integration ```typescript @@ -1495,18 +1552,6 @@ const { index, wasInvalid } = TabHistoryPruner.validateLocationIndex( **Impact:** Medium risk for long-running sessions -#### 3. URL Blacklist Not Configurable (Low Priority) - -**Current State:** -- Hardcoded blacklist in constants -- Cannot customize at runtime - -**Proposed Solution:** -- Injection token for blacklist -- Merge with default blacklist - -**Impact:** Low, blacklist rarely needs customization - ### Future Enhancements Potential improvements identified: diff --git a/libs/core/tabs/src/index.ts b/libs/core/tabs/src/index.ts index 904896e40..dc11b6852 100644 --- a/libs/core/tabs/src/index.ts +++ b/libs/core/tabs/src/index.ts @@ -9,3 +9,4 @@ export * from './lib/tab-config'; export * from './lib/helpers'; export * from './lib/has-tab-id.guard'; export * from './lib/tab-cleanup.guard'; +export * from './lib/deactivate-tab.guard'; diff --git a/libs/core/tabs/src/lib/deactivate-tab.guard.ts b/libs/core/tabs/src/lib/deactivate-tab.guard.ts new file mode 100644 index 000000000..149dde770 --- /dev/null +++ b/libs/core/tabs/src/lib/deactivate-tab.guard.ts @@ -0,0 +1,35 @@ +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; +import { TabService } from './tab'; +import { logger } from '@isa/core/logging'; + +/** + * Guard that deactivates the currently active tab when entering a route. + * + * Use this guard on routes that exist outside of a process/tab context, + * such as dashboard pages or global branch operations. + * + * @example + * ```typescript + * const routes: Routes = [ + * { + * path: 'dashboard', + * loadChildren: () => import('./dashboard').then(m => m.DashboardModule), + * canActivate: [deactivateTabGuard], + * }, + * ]; + * ``` + */ +export const deactivateTabGuard: CanActivateFn = () => { + const tabService = inject(TabService); + const log = logger({ guard: 'deactivateTabGuard' }); + + const previousTabId = tabService.activatedTabId(); + + if (previousTabId !== null) { + tabService.deactivateTab(); + log.debug('Tab deactivated on route activation', () => ({ previousTabId })); + } + + return true; +}; diff --git a/libs/core/tabs/src/lib/tab-navigation.constants.ts b/libs/core/tabs/src/lib/tab-navigation.constants.ts index d62b65f40..c27e927fb 100644 --- a/libs/core/tabs/src/lib/tab-navigation.constants.ts +++ b/libs/core/tabs/src/lib/tab-navigation.constants.ts @@ -6,16 +6,21 @@ */ /** - * URLs that should not be added to tab navigation history. + * URL patterns that should not be added to tab navigation history. * - * These routes are excluded to prevent cluttering the history stack with - * frequently visited pages that don't need to be tracked in tab history. + * These patterns are matched using prefix matching - any URL that starts with + * a pattern in this list will be excluded from history tracking. * * @example * ```typescript - * // Dashboard routes are excluded because they serve as entry points - * // and don't represent meaningful navigation steps in a workflow + * // Dashboard routes are excluded because they don't run in a process context + * // Pattern '/kunde/dashboard' excludes: + * // - '/kunde/dashboard' + * // - '/kunde/dashboard/' + * // - '/kunde/dashboard/stats' + * // But NOT: + * // - '/kunde/123/dashboard' * '/kunde/dashboard' * ``` */ -export const HISTORY_BLACKLIST_URLS = ['/kunde/dashboard']; +export const HISTORY_BLACKLIST_PATTERNS = ['/kunde/dashboard', '/dashboard']; diff --git a/libs/core/tabs/src/lib/tab-navigation.service.ts b/libs/core/tabs/src/lib/tab-navigation.service.ts index 7af9a5ec6..459500fed 100644 --- a/libs/core/tabs/src/lib/tab-navigation.service.ts +++ b/libs/core/tabs/src/lib/tab-navigation.service.ts @@ -4,7 +4,7 @@ import { filter } from 'rxjs/operators'; import { TabService } from './tab'; import { TabLocation } from './schemas'; import { Title } from '@angular/platform-browser'; -import { HISTORY_BLACKLIST_URLS } from './tab-navigation.constants'; +import { HISTORY_BLACKLIST_PATTERNS } from './tab-navigation.constants'; /** * Service that automatically syncs browser navigation events to tab location history. @@ -39,7 +39,7 @@ export class TabNavigationService { } #syncNavigationToTab(event: NavigationEnd) { - // Skip blacklisted URLs + // Skip blacklisted URLs (tab deactivation handled by route guards) if (this.#shouldSkipHistory(event.url)) { return; } @@ -65,11 +65,14 @@ export class TabNavigationService { /** * Checks if a URL should be excluded from tab navigation history. * - * @param url - The URL to check against the blacklist + * Uses prefix matching - a URL is excluded if it starts with any pattern + * in the blacklist. This allows excluding entire route trees. + * + * @param url - The URL to check against the blacklist patterns * @returns true if the URL should be skipped, false otherwise */ #shouldSkipHistory(url: string): boolean { - return HISTORY_BLACKLIST_URLS.includes(url); + return HISTORY_BLACKLIST_PATTERNS.some(pattern => url.startsWith(pattern)); } #getActiveTabId(url: string): number | null { diff --git a/libs/core/tabs/src/lib/tab.ts b/libs/core/tabs/src/lib/tab.ts index d0a0e0477..d393e30c9 100644 --- a/libs/core/tabs/src/lib/tab.ts +++ b/libs/core/tabs/src/lib/tab.ts @@ -104,6 +104,13 @@ export const TabService = signalStore( store._logger.debug('Tab activated', () => ({ tabId: id })); }, + deactivateTab() { + const previousTabId = store.activatedTabId(); + patchState(store, { activatedTabId: null }); + store._logger.debug('Tab deactivated', () => ({ + previousTabId, + })); + }, patchTab(id: number, changes: z.infer) { const currentTab = store.entityMap()[id];