mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
✨ feat(core-tabs): add pattern-based URL exclusion and tab deactivation guard
- 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.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
35
libs/core/tabs/src/lib/deactivate-tab.guard.ts
Normal file
35
libs/core/tabs/src/lib/deactivate-tab.guard.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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'];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<typeof PatchTabSchema>) {
|
||||
const currentTab = store.entityMap()[id];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user