From fdfb54a3a0f4622be0ca36440a60f2378e38f90e Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Tue, 2 Dec 2025 15:41:18 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=202065:=20=E2=99=BB=EF=B8=8F=20refa?= =?UTF-8?q?ctor(core-navigation):=20remove=20library=20and=20use=20TabServ?= =?UTF-8?q?ice=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ refactor(core-navigation): remove library and use TabService directly Remove @isa/core/navigation library entirely as it was just a thin wrapper around TabService.patchTabMetadata(). Consumers now use TabService directly for scoped metadata operations. Changes: - Delete libs/core/navigation/ (~12 files, ~2900 LOC removed) - Update 6 consumer components to use TabService directly - Remove @isa/core/navigation path alias from tsconfig.base.json - All operations now synchronous (removed async/await) Migration pattern: - preserveContext() → patchTabMetadata(tabId, { [scope]: data }) - restoreContext() → activatedTab()?.metadata?.[scope] - restoreAndClearContext() → get + patchTabMetadata(tabId, { [scope]: null }) Refs #5502 --- .../kundenkarte/kundenkarte.component.ts | 12 +- .../details-main-view.component.ts | 73 +- .../kundenkarte-main-view.component.ts | 11 +- .../reward-action/reward-action.component.ts | 14 +- .../reward-start-card.component.ts | 21 +- ...ing-and-shipping-address-card.component.ts | 26 +- libs/core/navigation/README.md | 855 ------------------ libs/core/navigation/eslint.config.cjs | 34 - libs/core/navigation/project.json | 20 - libs/core/navigation/src/index.ts | 5 - .../src/lib/navigation-context.constants.ts | 22 - .../lib/navigation-context.service.spec.ts | 668 -------------- .../src/lib/navigation-context.service.ts | 442 --------- .../src/lib/navigation-context.types.ts | 102 --- .../src/lib/navigation-state.service.spec.ts | 227 ----- .../src/lib/navigation-state.service.ts | 331 ------- .../src/lib/navigation-state.types.ts | 23 - libs/core/navigation/src/test-setup.ts | 13 - libs/core/navigation/tsconfig.json | 30 - libs/core/navigation/tsconfig.lib.json | 27 - libs/core/navigation/tsconfig.spec.json | 29 - libs/core/navigation/vite.config.mts | 33 - tsconfig.base.json | 1 - 23 files changed, 82 insertions(+), 2937 deletions(-) delete mode 100644 libs/core/navigation/README.md delete mode 100644 libs/core/navigation/eslint.config.cjs delete mode 100644 libs/core/navigation/project.json delete mode 100644 libs/core/navigation/src/index.ts delete mode 100644 libs/core/navigation/src/lib/navigation-context.constants.ts delete mode 100644 libs/core/navigation/src/lib/navigation-context.service.spec.ts delete mode 100644 libs/core/navigation/src/lib/navigation-context.service.ts delete mode 100644 libs/core/navigation/src/lib/navigation-context.types.ts delete mode 100644 libs/core/navigation/src/lib/navigation-state.service.spec.ts delete mode 100644 libs/core/navigation/src/lib/navigation-state.service.ts delete mode 100644 libs/core/navigation/src/lib/navigation-state.types.ts delete mode 100644 libs/core/navigation/src/test-setup.ts delete mode 100644 libs/core/navigation/tsconfig.json delete mode 100644 libs/core/navigation/tsconfig.lib.json delete mode 100644 libs/core/navigation/tsconfig.spec.json delete mode 100644 libs/core/navigation/vite.config.mts diff --git a/apps/isa-app/src/page/customer/components/kundenkarte/kundenkarte.component.ts b/apps/isa-app/src/page/customer/components/kundenkarte/kundenkarte.component.ts index 637078d0d..f0f28f7ac 100644 --- a/apps/isa-app/src/page/customer/components/kundenkarte/kundenkarte.component.ts +++ b/apps/isa-app/src/page/customer/components/kundenkarte/kundenkarte.component.ts @@ -9,8 +9,7 @@ import { DecimalPipe } from '@angular/common'; import { Component, Input, OnInit, inject } from '@angular/core'; import { IconComponent } from '@shared/components/icon'; import { BonusCardInfoDTO } from '@generated/swagger/crm-api'; -import { injectTabId } from '@isa/core/tabs'; -import { NavigationStateService } from '@isa/core/navigation'; +import { injectTabId, TabService } from '@isa/core/tabs'; import { Router } from '@angular/router'; import { CustomerSearchNavigation } from '@shared/services/navigation'; @@ -47,7 +46,7 @@ import { CustomerSearchNavigation } from '@shared/services/navigation'; export class KundenkarteComponent implements OnInit { #tabId = injectTabId(); #router = inject(Router); - #navigationState = inject(NavigationStateService); + #tabService = inject(TabService); #customerNavigationService = inject(CustomerSearchNavigation); @Input() cardDetails: BonusCardInfoDTO; @@ -69,13 +68,12 @@ export class KundenkarteComponent implements OnInit { return; } - this.#navigationState.preserveContext( - { + this.#tabService.patchTabMetadata(tabId, { + 'select-customer': { returnUrl: `/${tabId}/reward`, autoTriggerContinueFn: true, }, - 'select-customer', - ); + }); await this.#router.navigate( this.#customerNavigationService.detailsRoute({ diff --git a/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view.component.ts b/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view.component.ts index 26c462147..a5b9dc8cc 100644 --- a/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view.component.ts +++ b/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view.component.ts @@ -49,9 +49,14 @@ import { NavigateAfterRewardSelection, RewardSelectionPopUpService, } from '@isa/checkout/shared/reward-selection-dialog'; -import { NavigationStateService } from '@isa/core/navigation'; +import { TabService } from '@isa/core/tabs'; import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api'; +interface SelectCustomerContext { + returnUrl?: string; + autoTriggerContinueFn?: boolean; +} + export interface CustomerDetailsViewMainState { isBusy: boolean; shoppingCart: ShoppingCartDTO; @@ -80,7 +85,7 @@ export class CustomerDetailsViewMainComponent private _router = inject(Router); private _activatedRoute = inject(ActivatedRoute); private _genderSettings = inject(GenderSettingsService); - private _navigationState = inject(NavigationStateService); + private _tabService = inject(TabService); private _onDestroy$ = new Subject(); customerService = inject(CrmCustomerService); @@ -97,18 +102,19 @@ export class CustomerDetailsViewMainComponent map(([fetchingCustomer, fetchingList]) => fetchingCustomer || fetchingList), ); - async getReturnUrlFromContext(): Promise { - // Get from preserved context (survives intermediate navigations, auto-scoped to tab) - const context = await this._navigationState.restoreContext<{ - returnUrl?: string; - }>('select-customer'); + getReturnUrlFromContext(): string | null { + // Get from preserved context (survives intermediate navigations, scoped to tab) + const context = this._tabService.activatedTab()?.metadata?.[ + 'select-customer' + ] as SelectCustomerContext | undefined; return context?.returnUrl ?? null; } - async checkHasReturnUrl(): Promise { - const hasContext = - await this._navigationState.hasPreservedContext('select-customer'); + checkHasReturnUrl(): void { + const hasContext = !!this._tabService.activatedTab()?.metadata?.[ + 'select-customer' + ]; this.hasReturnUrl.set(hasContext); } @@ -321,24 +327,23 @@ export class CustomerDetailsViewMainComponent ngOnInit() { // Check if we have a return URL context - this.checkHasReturnUrl().then(async () => { - // Check if we should auto-trigger continue() (only from Kundenkarte) - const context = await this._navigationState.restoreContext<{ - returnUrl?: string; - autoTriggerContinueFn?: boolean; - }>('select-customer'); + this.checkHasReturnUrl(); - if (context?.autoTriggerContinueFn) { - // Clear the autoTriggerContinueFn flag immediately (preserves returnUrl automatically) - await this._navigationState.patchContext( - { autoTriggerContinueFn: undefined }, - 'select-customer', - ); + // Check if we should auto-trigger continue() (only from Kundenkarte) + const tab = this._tabService.activatedTab(); + const context = tab?.metadata?.['select-customer'] as + | SelectCustomerContext + | undefined; - // Auto-trigger continue() ONLY when coming from Kundenkarte - this.continue(); - } - }); + if (context?.autoTriggerContinueFn && tab) { + // Clear the autoTriggerContinueFn flag immediately (preserves returnUrl) + this._tabService.patchTabMetadata(tab.id, { + 'select-customer': { ...context, autoTriggerContinueFn: undefined }, + }); + + // Auto-trigger continue() ONLY when coming from Kundenkarte + this.continue(); + } this.processId$ .pipe( @@ -436,10 +441,18 @@ export class CustomerDetailsViewMainComponent // #5262 Check for reward selection flow before navigation if (this.hasReturnUrl()) { - // Restore from preserved context (auto-scoped to current tab) and clean up - const context = await this._navigationState.restoreAndClearContext<{ - returnUrl?: string; - }>('select-customer'); + // Restore from preserved context (scoped to current tab) and clean up + const tab = this._tabService.activatedTab(); + const context = tab?.metadata?.['select-customer'] as + | SelectCustomerContext + | undefined; + + // Clear the context + if (tab) { + this._tabService.patchTabMetadata(tab.id, { + 'select-customer': null, + }); + } if (context?.returnUrl) { await this._router.navigateByUrl(context.returnUrl); diff --git a/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.ts b/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.ts index e30ae9ce5..d41472a05 100644 --- a/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.ts +++ b/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.ts @@ -10,7 +10,7 @@ import { import { CustomerSearchStore } from '../store'; import { ActivatedRoute, Router } from '@angular/router'; import { map } from 'rxjs/operators'; -import { NavigationStateService } from '@isa/core/navigation'; +import { TabService } from '@isa/core/tabs'; import { CustomerSearchNavigation } from '@shared/services/navigation'; import { AsyncPipe } from '@angular/common'; import { CustomerMenuComponent } from '../../components/customer-menu'; @@ -51,7 +51,7 @@ export class KundenkarteMainViewComponent implements OnDestroy { #cardTransactionsResource = inject(CustomerCardTransactionsResource); elementRef = inject(ElementRef); #router = inject(Router); - #navigationState = inject(NavigationStateService); + #tabService = inject(TabService); #customerNavigationService = inject(CustomerSearchNavigation); /** @@ -120,13 +120,12 @@ export class KundenkarteMainViewComponent implements OnDestroy { } // Preserve context for auto-triggering continue() in details view - this.#navigationState.preserveContext( - { + this.#tabService.patchTabMetadata(tabId, { + 'select-customer': { returnUrl: `/${tabId}/reward`, autoTriggerContinueFn: true, }, - 'select-customer', - ); + }); // Navigate to customer details - will auto-trigger continue() await this.#router.navigate( diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts index c52c32731..86eaf8472 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts @@ -11,14 +11,13 @@ import { ShoppingCartFacade, SelectedRewardShoppingCartResource, } from '@isa/checkout/data-access'; -import { injectTabId } from '@isa/core/tabs'; +import { injectTabId, TabService } from '@isa/core/tabs'; import { StatefulButtonComponent, StatefulButtonState } from '@isa/ui/buttons'; import { PurchaseOptionsModalService } from '@modal/purchase-options'; import { firstValueFrom } from 'rxjs'; import { Router } from '@angular/router'; import { getRouteToCustomer } from '../helpers'; import { PrimaryCustomerCardResource } from '@isa/crm/data-access'; -import { NavigationStateService } from '@isa/core/navigation'; @Component({ selector: 'reward-action', @@ -32,7 +31,7 @@ export class RewardActionComponent { #store = inject(RewardCatalogStore); #tabId = injectTabId(); - #navigationState = inject(NavigationStateService); + #tabService = inject(TabService); #purchasingOptionsModal = inject(PurchaseOptionsModalService); #shoppingCartFacade = inject(ShoppingCartFacade); #checkoutMetadataService = inject(CheckoutMetadataService); @@ -122,12 +121,9 @@ export class RewardActionComponent { const route = getRouteToCustomer(tabId); // Preserve context: Store current reward page URL to return to after customer selection - await this.#navigationState.preserveContext( - { - returnUrl: this.#router.url, - }, - 'select-customer', - ); + this.#tabService.patchTabMetadata(tabId, { + 'select-customer': { returnUrl: this.#router.url }, + }); await this.#router.navigate(route.path, { queryParams: route.queryParams, diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts index 1f030e11d..7ded89105 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts @@ -1,9 +1,8 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ButtonComponent } from '@isa/ui/buttons'; -import { injectTabId } from '@isa/core/tabs'; +import { injectTabId, TabService } from '@isa/core/tabs'; import { Router } from '@angular/router'; import { getRouteToCustomer } from '../../helpers'; -import { NavigationStateService } from '@isa/core/navigation'; @Component({ selector: 'reward-start-card', @@ -13,7 +12,7 @@ import { NavigationStateService } from '@isa/core/navigation'; imports: [ButtonComponent], }) export class RewardStartCardComponent { - readonly #navigationState = inject(NavigationStateService); + readonly #tabService = inject(TabService); readonly #router = inject(Router); tabId = injectTabId(); @@ -22,19 +21,19 @@ export class RewardStartCardComponent { * Called when "Kund*in auswählen" button is clicked. * Preserves the current URL as returnUrl before navigating to customer search. */ - async onSelectCustomer() { + onSelectCustomer() { const customerRoute = getRouteToCustomer(this.tabId()); + const tabId = this.#tabService.activatedTabId(); // Preserve context: Store current reward page URL to return to after customer selection - await this.#navigationState.preserveContext( - { - returnUrl: this.#router.url, - }, - 'select-customer', - ); + if (tabId) { + this.#tabService.patchTabMetadata(tabId, { + 'select-customer': { returnUrl: this.#router.url }, + }); + } // Navigate to customer search - await this.#router.navigate(customerRoute.path, { + this.#router.navigate(customerRoute.path, { queryParams: customerRoute.queryParams, }); } diff --git a/libs/checkout/feature/reward-shopping-cart/src/lib/billing-and-shipping-address-card/billing-and-shipping-address-card.component.ts b/libs/checkout/feature/reward-shopping-cart/src/lib/billing-and-shipping-address-card/billing-and-shipping-address-card.component.ts index ddac3ac70..43126a2d4 100644 --- a/libs/checkout/feature/reward-shopping-cart/src/lib/billing-and-shipping-address-card/billing-and-shipping-address-card.component.ts +++ b/libs/checkout/feature/reward-shopping-cart/src/lib/billing-and-shipping-address-card/billing-and-shipping-address-card.component.ts @@ -14,8 +14,8 @@ import { isaActionEdit } from '@isa/icons'; import { IconButtonComponent } from '@isa/ui/buttons'; import { provideIcons } from '@ng-icons/core'; import { AddressComponent } from '@isa/shared/address'; -import { injectTabId } from '@isa/core/tabs'; -import { NavigationStateService } from '@isa/core/navigation'; +import { injectTabId, TabService } from '@isa/core/tabs'; +import { Router } from '@angular/router'; @Component({ selector: 'checkout-billing-and-shipping-address-card', @@ -26,7 +26,8 @@ import { NavigationStateService } from '@isa/core/navigation'; providers: [provideIcons({ isaActionEdit })], }) export class BillingAndShippingAddressCardComponent { - #navigationState = inject(NavigationStateService); + #tabService = inject(TabService); + #router = inject(Router); #shippingAddressResource = inject(SelectedCustomerShippingAddressResource); #payerAddressResource = inject(SelectedCustomerPayerAddressResource); @@ -45,18 +46,19 @@ export class BillingAndShippingAddressCardComponent { return this.#customerResource.value(); }); - async navigateToCustomer() { + navigateToCustomer() { const customerId = this.customer()?.id; - if (!customerId) return; + const tabId = this.tabId(); + if (!customerId || !tabId) return; - const returnUrl = `/${this.tabId()}/reward/cart`; + const returnUrl = `/${tabId}/reward/cart`; - // Preserve context across intermediate navigations (auto-scoped to active tab) - await this.#navigationState.navigateWithPreservedContext( - ['/', 'kunde', this.tabId(), 'customer', 'search', customerId], - { returnUrl }, - 'select-customer', - ); + // Preserve context across intermediate navigations (scoped to active tab) + this.#tabService.patchTabMetadata(tabId, { + 'select-customer': { returnUrl }, + }); + + this.#router.navigate(['/', 'kunde', tabId, 'customer', 'search', customerId]); } payer = computed(() => { diff --git a/libs/core/navigation/README.md b/libs/core/navigation/README.md deleted file mode 100644 index c85cbedac..000000000 --- a/libs/core/navigation/README.md +++ /dev/null @@ -1,855 +0,0 @@ -# @isa/core/navigation - -A reusable Angular library providing **context preservation** for multi-step navigation flows with automatic tab-scoped storage. - -## Overview - -`@isa/core/navigation` solves the problem of **lost navigation state** during intermediate navigations. Unlike Angular's router state which is lost after intermediate navigations, this library persists navigation context in **tab metadata** with automatic cleanup when tabs close. - -### The Problem It Solves - -```typescript -// ❌ Problem: Router state is lost during intermediate navigations -await router.navigate(['/customer/search'], { - state: { returnUrl: '/reward/cart' } // Works for immediate navigation -}); - -// After intermediate navigations: -// /customer/search → /customer/details → /add-shipping-address -// ⚠️ The returnUrl is LOST! -``` - -### The Solution - -```typescript -// ✅ Solution: Context preservation survives intermediate navigations -navState.preserveContext({ returnUrl: '/reward/cart' }); -// Context persists in tab metadata, automatically cleaned up when tab closes - -// After multiple intermediate navigations: -const context = navState.restoreAndClearContext(); -// ✅ returnUrl is PRESERVED! -``` - -## Features - -- ✅ **Survives Intermediate Navigations** - State persists across multiple navigation steps -- ✅ **Automatic Tab Scoping** - Contexts automatically isolated per tab using `TabService` -- ✅ **Automatic Cleanup** - Contexts cleared automatically when tabs close (no manual cleanup needed) -- ✅ **Hierarchical Scoping** - Combine tab ID with custom scopes (e.g., `"customer-details"`) -- ✅ **Type-Safe** - Full TypeScript generics support -- ✅ **Simple API** - No context IDs to track, scope is the identifier -- ✅ **Auto-Restore on Refresh** - Contexts survive page refresh (via TabService UserStorage persistence) -- ✅ **Map-Based Storage** - One context per scope for clarity -- ✅ **Platform-Agnostic** - Works with Angular Universal (SSR) -- ✅ **Zero URL Pollution** - No query parameters needed - -## Installation - -This library is part of the ISA Frontend monorepo. Import from the path alias: - -```typescript -import { NavigationStateService } from '@isa/core/navigation'; -``` - -## Quick Start - -### Basic Flow - -```typescript -import { Component, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { NavigationStateService } from '@isa/core/navigation'; - -@Component({ - selector: 'app-cart', - template: `` -}) -export class CartComponent { - private router = inject(Router); - private navState = inject(NavigationStateService); - - async editCustomer() { - // Start flow - preserve context (auto-scoped to active tab) - this.navState.preserveContext({ - returnUrl: '/reward/cart', - customerId: 123 - }); - - await this.router.navigate(['/customer/search']); - } -} - -@Component({ - selector: 'app-customer-details', - template: `` -}) -export class CustomerDetailsComponent { - private router = inject(Router); - private navState = inject(NavigationStateService); - - async complete() { - // End flow - restore and auto-cleanup (auto-scoped to active tab) - const context = this.navState.restoreAndClearContext<{ returnUrl: string }>(); - - if (context?.returnUrl) { - await this.router.navigateByUrl(context.returnUrl); - } - } -} -``` - -### Simplified Navigation - -Use `navigateWithPreservedContext()` to combine navigation + context preservation: - -```typescript -async editCustomer() { - // Navigate and preserve in one call - const { success } = await this.navState.navigateWithPreservedContext( - ['/customer/search'], - { returnUrl: '/reward/cart', customerId: 123 } - ); -} -``` - -## Core API - -### Context Management - -#### `preserveContext(state, customScope?)` - -Save navigation context that survives intermediate navigations. - -```typescript -// Default tab scope -navState.preserveContext({ - returnUrl: '/reward/cart', - selectedItems: [1, 2, 3] -}); - -// Custom scope within tab -navState.preserveContext( - { customerId: 42 }, - 'customer-details' // Stored as 'customer-details' in active tab's metadata -); -``` - -**Parameters:** -- `state`: The data to preserve (any object) -- `customScope` (optional): Custom scope within the tab (e.g., `'customer-details'`) - -**Storage Location:** -- Stored in active tab's metadata at: `tab.metadata['navigation-contexts'][scopeKey]` -- Default scope: `'default'` -- Custom scope: `customScope` value - ---- - -#### `patchContext(partialState, customScope?)` - -Partially update preserved context without replacing the entire context. - -This method merges partial state with the existing context, preserving properties you don't specify. Properties explicitly set to `undefined` will be removed from the context. - -```typescript -// Existing context: { returnUrl: '/cart', autoTriggerContinueFn: true, customerId: 123 } - -// Clear one property while preserving others -await navState.patchContext( - { autoTriggerContinueFn: undefined }, - 'select-customer' -); -// Result: { returnUrl: '/cart', customerId: 123 } - -// Update one property while preserving others -await navState.patchContext( - { customerId: 456 }, - 'select-customer' -); -// Result: { returnUrl: '/cart', customerId: 456 } - -// Add new property to existing context -await navState.patchContext( - { selectedTab: 'details' }, - 'select-customer' -); -// Result: { returnUrl: '/cart', customerId: 456, selectedTab: 'details' } -``` - -**Parameters:** -- `partialState`: Partial state object to merge (set properties to `undefined` to remove them) -- `customScope` (optional): Custom scope within the tab - -**Use Cases:** -- Clear trigger flags while preserving flow data -- Update specific properties without affecting others -- Remove properties from context -- Add properties to existing context - -**Comparison with `preserveContext`:** -- `preserveContext`: Replaces entire context (overwrites all properties) -- `patchContext`: Merges with existing context (updates only specified properties) - -```typescript -// ❌ preserveContext - must manually preserve existing properties -const context = await navState.restoreContext(); -await navState.preserveContext({ - returnUrl: context?.returnUrl, // Must specify - customerId: context?.customerId, // Must specify - autoTriggerContinueFn: undefined, // Clear this -}); - -// ✅ patchContext - automatically preserves unspecified properties -await navState.patchContext({ - autoTriggerContinueFn: undefined, // Only specify what changes -}); -``` - ---- - -#### `restoreContext(customScope?)` - -Retrieve preserved context **without** removing it. - -```typescript -// Default tab scope -const context = navState.restoreContext<{ returnUrl: string }>(); -if (context?.returnUrl) { - console.log('Return URL:', context.returnUrl); -} - -// Custom scope -const context = navState.restoreContext<{ customerId: number }>('customer-details'); -``` - -**Parameters:** -- `customScope` (optional): Custom scope to retrieve from (defaults to 'default') - -**Returns:** The preserved data, or `null` if not found - ---- - -#### `restoreAndClearContext(customScope?)` - -Retrieve preserved context **and automatically remove** it (recommended for cleanup). - -```typescript -// Default tab scope -const context = navState.restoreAndClearContext<{ returnUrl: string }>(); -if (context?.returnUrl) { - await router.navigateByUrl(context.returnUrl); -} - -// Custom scope -const context = navState.restoreAndClearContext<{ customerId: number }>('customer-details'); -``` - -**Parameters:** -- `customScope` (optional): Custom scope to retrieve from (defaults to 'default') - -**Returns:** The preserved data, or `null` if not found - ---- - -#### `clearPreservedContext(customScope?)` - -Manually remove a context without retrieving its data. - -```typescript -// Clear default tab scope -navState.clearPreservedContext(); - -// Clear custom scope -navState.clearPreservedContext('customer-details'); -``` - ---- - -#### `hasPreservedContext(customScope?)` - -Check if a context exists. - -```typescript -// Check default tab scope -if (navState.hasPreservedContext()) { - const context = navState.restoreContext(); -} - -// Check custom scope -if (navState.hasPreservedContext('customer-details')) { - const context = navState.restoreContext('customer-details'); -} -``` - ---- - -### Navigation Helpers - -#### `navigateWithPreservedContext(commands, state, customScope?, extras?)` - -Navigate and preserve context in one call. - -```typescript -const { success } = await navState.navigateWithPreservedContext( - ['/customer/search'], - { returnUrl: '/reward/cart' }, - 'customer-flow', // optional customScope - { queryParams: { foo: 'bar' } } // optional NavigationExtras -); - -// Later... -const context = navState.restoreAndClearContext('customer-flow'); -``` - ---- - -### Cleanup Methods - -#### `clearScopeContexts()` - -Clear all contexts for the active tab (both default and custom scopes). - -```typescript -// Clear all contexts for active tab -const cleared = this.navState.clearScopeContexts(); -console.log(`Cleaned up ${cleared} contexts`); -``` - -**Returns:** Number of contexts cleared - -**Note:** This is typically not needed because contexts are **automatically cleaned up when the tab closes**. Use this only for explicit cleanup during the tab's lifecycle. - ---- - -## Usage Patterns - -### Pattern 1: Multi-Step Flow with Intermediate Navigations - -**Problem:** You need to return to a page after multiple intermediate navigations. - -```typescript -// Component A: Start of flow -export class RewardCartComponent { - navState = inject(NavigationStateService); - router = inject(Router); - - async selectCustomer() { - // Preserve returnUrl (auto-scoped to tab) - this.navState.preserveContext({ - returnUrl: '/reward/cart' - }); - - await this.router.navigate(['/customer/search']); - } -} - -// Component B: Intermediate navigation -export class CustomerSearchComponent { - router = inject(Router); - - async viewDetails(customerId: number) { - await this.router.navigate(['/customer/details', customerId]); - // Context still persists! - } -} - -// Component C: Another intermediate navigation -export class CustomerDetailsComponent { - router = inject(Router); - - async addShippingAddress() { - await this.router.navigate(['/add-shipping-address']); - // Context still persists! - } -} - -// Component D: End of flow -export class FinalStepComponent { - navState = inject(NavigationStateService); - router = inject(Router); - - async complete() { - // Restore context (auto-scoped to tab) and navigate back - const context = this.navState.restoreAndClearContext<{ returnUrl: string }>(); - - if (context?.returnUrl) { - await this.router.navigateByUrl(context.returnUrl); - } - } -} -``` - ---- - -### Pattern 2: Multiple Flows in Same Tab - -Use custom scopes to manage different flows within the same tab. - -```typescript -export class ComplexPageComponent { - navState = inject(NavigationStateService); - - async startCustomerFlow() { - // Store context for customer flow - this.navState.preserveContext( - { returnUrl: '/dashboard', step: 1 }, - 'customer-flow' - ); - // Stored in active tab metadata under scope 'customer-flow' - } - - async startProductFlow() { - // Store context for product flow - this.navState.preserveContext( - { returnUrl: '/dashboard', selectedProducts: [1, 2] }, - 'product-flow' - ); - // Stored in active tab metadata under scope 'product-flow' - } - - async completeCustomerFlow() { - // Restore from customer flow - const context = this.navState.restoreAndClearContext('customer-flow'); - } - - async completeProductFlow() { - // Restore from product flow - const context = this.navState.restoreAndClearContext('product-flow'); - } -} -``` - ---- - -### Pattern 3: Complex Context Data - -```typescript -interface CheckoutContext { - returnUrl: string; - selectedItems: number[]; - customerId: number; - shippingAddressId?: number; - metadata: { - source: 'reward' | 'checkout'; - timestamp: number; - }; -} - -// Save -navState.preserveContext({ - returnUrl: '/reward/cart', - selectedItems: [1, 2, 3], - customerId: 456, - metadata: { - source: 'reward', - timestamp: Date.now() - } -}); - -// Restore with type safety -const context = navState.restoreAndClearContext(); -if (context) { - console.log('Items:', context.selectedItems); - console.log('Customer:', context.customerId); -} -``` - ---- - -### Pattern 4: No Manual Cleanup Needed - -```typescript -export class TabAwareComponent { - navState = inject(NavigationStateService); - - async startFlow() { - // Set context - this.navState.preserveContext({ returnUrl: '/home' }); - - // No need to clear in ngOnDestroy! - // Context is automatically cleaned up when tab closes - } - - // ❌ NOT NEEDED: - // ngOnDestroy() { - // this.navState.clearScopeContexts(); - // } -} -``` - ---- - -## Architecture - -### How It Works - -```mermaid -graph LR - A[NavigationStateService] --> B[NavigationContextService] - B --> C[TabService] - C --> D[Tab Metadata Storage] - D --> E[UserStorage Persistence] - - style A fill:#e1f5ff - style B fill:#e1f5ff - style C fill:#fff4e1 - style D fill:#e8f5e9 - style E fill:#f3e5f5 -``` - -1. **Context Storage**: Contexts are stored in **tab metadata** using `TabService` -2. **Automatic Scoping**: Active tab ID determines storage location automatically -3. **Hierarchical Keys**: Scopes are organized as `tab.metadata['navigation-contexts'][customScope]` -4. **Automatic Cleanup**: Contexts removed automatically when tabs close (via tab lifecycle) -5. **Persistent Across Refresh**: Tab metadata persists via UserStorage, so contexts survive page refresh -6. **Map-Based**: One context per scope for clarity - -### Tab Metadata Structure - -```typescript -// Example: Tab with ID 123 -tab.metadata = { - 'navigation-contexts': { - 'default': { - data: { returnUrl: '/cart', selectedItems: [1, 2, 3] }, - createdAt: 1234567890000 - }, - 'customer-details': { - data: { customerId: 42, step: 2 }, - createdAt: 1234567891000 - }, - 'product-flow': { - data: { productIds: [100, 200], source: 'recommendation' }, - createdAt: 1234567892000 - } - }, - // ... other tab metadata -} -``` - -### Storage Layers - -``` -┌─────────────────────────────────────┐ -│ NavigationStateService (Public API)│ -└──────────────┬──────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ NavigationContextService (Storage) │ -└──────────────┬──────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ TabService.patchTabMetadata() │ -└──────────────┬──────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ Tab Metadata Storage (In-Memory) │ -└──────────────┬──────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ UserStorage (SessionStorage) │ -│ (Automatic Persistence) │ -└─────────────────────────────────────┘ -``` - -### Integration with TabService - -This library **requires** `@isa/core/tabs` for automatic tab scoping: - -```typescript -import { TabService } from '@isa/core/tabs'; - -// NavigationContextService uses: -const tabId = this.tabService.activatedTabId(); // Returns: number | null - -if (tabId !== null) { - // Store in: tab.metadata['navigation-contexts'][customScope] - this.tabService.patchTabMetadata(tabId, { - 'navigation-contexts': { - [customScope]: { data, createdAt } - } - }); -} -``` - -**When no tab is active** (tabId = null): -- Operations throw an error to prevent data loss -- This ensures contexts are always properly scoped to a tab - ---- - -## Migration Guide - -### From SessionStorage to Tab Metadata - -This library previously used SessionStorage for context persistence. It has been refactored to use tab metadata for better integration with the tab lifecycle and automatic cleanup. - -### What Changed - -**Storage Location:** -- **Before**: SessionStorage with key `'isa:navigation:context-map'` -- **After**: Tab metadata at `tab.metadata['navigation-contexts']` - -**Cleanup:** -- **Before**: Manual cleanup required + automatic expiration after 24 hours -- **After**: Automatic cleanup when tab closes (no manual cleanup needed) - -**Scope Keys:** -- **Before**: `"123"` (tab ID), `"123-customer-details"` (tab ID + custom scope) -- **After**: `"default"`, `"customer-details"` (custom scope only, tab ID implicit from storage location) - -**TTL Parameter:** -- **Before**: `preserveContext(data, customScope, ttl)` - TTL respected -- **After**: `preserveContext(data, customScope, ttl)` - TTL parameter ignored (kept for compatibility) - -### What Stayed the Same - -✅ **Public API**: All public methods remain unchanged -✅ **Type Safety**: Full TypeScript support with generics -✅ **Hierarchical Scoping**: Custom scopes still work the same way -✅ **Usage Patterns**: All existing code continues to work -✅ **Persistence**: Contexts still survive page refresh (via TabService UserStorage) - -### Benefits of Tab Metadata Approach - -1. **Automatic Cleanup**: No need to manually clear contexts or worry about stale data -2. **Better Integration**: Seamless integration with tab lifecycle management -3. **Simpler Mental Model**: Contexts are "owned" by tabs, not global storage -4. **No TTL Management**: Tab lifecycle handles cleanup automatically -5. **Safer**: Impossible to leak contexts across unrelated tabs - -### Migration Steps - -**No action required!** The public API is unchanged. Your existing code will continue to work: - -```typescript -// ✅ This code works exactly the same before and after migration -navState.preserveContext({ returnUrl: '/cart' }); -const context = navState.restoreAndClearContext<{ returnUrl: string }>(); -``` - -**Optional: Remove manual cleanup code** - -If you have manual cleanup in `ngOnDestroy`, you can safely remove it: - -```typescript -// Before (still works, but unnecessary): -ngOnDestroy() { - this.navState.clearScopeContexts(); -} - -// After (automatic cleanup): -ngOnDestroy() { - // No cleanup needed - tab lifecycle handles it! -} -``` - -**Note on TTL parameter** - -If you were using the TTL parameter, be aware it's now ignored: - -```typescript -// Before: TTL respected -navState.preserveContext({ data: 'foo' }, undefined, 60000); // Expires in 1 minute - -// After: TTL ignored (context lives until tab closes) -navState.preserveContext({ data: 'foo' }, undefined, 60000); // Ignored parameter -``` - ---- - -## Testing - -### Mocking NavigationStateService - -```typescript -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NavigationStateService } from '@isa/core/navigation'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -describe('MyComponent', () => { - let component: MyComponent; - let fixture: ComponentFixture; - let navStateMock: any; - - beforeEach(async () => { - navStateMock = { - preserveContext: vi.fn(), - restoreContext: vi.fn().mockReturnValue({ returnUrl: '/test' }), - restoreAndClearContext: vi.fn().mockReturnValue({ returnUrl: '/test' }), - clearPreservedContext: vi.fn().mockReturnValue(true), - hasPreservedContext: vi.fn().mockReturnValue(true), - }; - - await TestBed.configureTestingModule({ - imports: [MyComponent], - providers: [ - { provide: NavigationStateService, useValue: navStateMock } - ] - }).compileComponents(); - - fixture = TestBed.createComponent(MyComponent); - component = fixture.componentInstance; - }); - - it('should preserve context when navigating', async () => { - await component.startFlow(); - - expect(navStateMock.preserveContext).toHaveBeenCalledWith({ - returnUrl: '/reward/cart' - }); - }); - - it('should restore context and navigate back', async () => { - navStateMock.restoreAndClearContext.mockReturnValue({ returnUrl: '/cart' }); - - await component.complete(); - - expect(navStateMock.restoreAndClearContext).toHaveBeenCalled(); - // Assert navigation occurred - }); -}); -``` - ---- - -## Best Practices - -### ✅ Do - -- **Use `restoreAndClearContext()`** for automatic cleanup when completing flows -- **Use custom scopes** for multiple concurrent flows in the same tab -- **Leverage type safety** with TypeScript generics (``) -- **Trust automatic cleanup** - no need to manually clear contexts when tabs close -- **Check for null** when restoring contexts (they may not exist) - -### ❌ Don't - -- **Don't store large objects** - keep contexts lean (return URLs, IDs, simple flags) -- **Don't use for persistent data** - use NgRx or services for long-lived state -- **Don't rely on TTL** - the TTL parameter is ignored in the current implementation -- **Don't manually clear in ngOnDestroy** - tab lifecycle handles it automatically -- **Don't store sensitive data** - contexts may be visible in browser dev tools - -### When to Use Navigation Context - -✅ **Good Use Cases:** -- Return URLs for multi-step flows -- Wizard/multi-step form state -- Temporary search filters or selections -- Flow-specific context (customer ID during checkout) - -❌ **Bad Use Cases:** -- User preferences (use NgRx or services) -- Authentication tokens (use dedicated auth service) -- Large datasets (use data services with caching) -- Cross-tab communication (use BroadcastChannel or shared services) - ---- - -## Configuration - -### Constants - -All configuration is in `navigation-context.constants.ts`: - -```typescript -// Metadata key for storing contexts in tab metadata -export const NAVIGATION_CONTEXT_METADATA_KEY = 'navigation-contexts'; -``` - -**Note:** Previous SessionStorage constants (`DEFAULT_CONTEXT_TTL`, `CLEANUP_INTERVAL`, `NAVIGATION_CONTEXT_STORAGE_KEY`) have been removed as they are no longer needed with tab metadata storage. - ---- - -## API Reference Summary - -| Method | Parameters | Returns | Purpose | -|--------|-----------|---------|---------| -| `preserveContext(state, customScope?)` | state: T, customScope?: string | void | Save context | -| `patchContext(partialState, customScope?)` | partialState: Partial, customScope?: string | void | Merge partial updates | -| `restoreContext(customScope?)` | customScope?: string | T \| null | Get context (keep) | -| `restoreAndClearContext(customScope?)` | customScope?: string | T \| null | Get + remove | -| `clearPreservedContext(customScope?)` | customScope?: string | boolean | Remove context | -| `hasPreservedContext(customScope?)` | customScope?: string | boolean | Check exists | -| `navigateWithPreservedContext(...)` | commands, state, customScope?, extras? | Promise<{success}> | Navigate + preserve | -| `clearScopeContexts()` | none | number | Bulk cleanup (rarely needed) | - ---- - -## Troubleshooting - -### Context Not Found After Refresh - -**Problem**: Context is `null` after page refresh. - -**Solution**: Ensure `TabService` is properly initialized and the tab ID is restored from UserStorage. Contexts rely on tab metadata which persists via UserStorage. - -### Context Cleared Unexpectedly - -**Problem**: Context disappears before you retrieve it. - -**Solution**: Check if you're using `restoreAndClearContext()` multiple times. This method removes the context after retrieval. Use `restoreContext()` if you need to access it multiple times. - -### "No active tab" Error - -**Problem**: Getting error "No active tab - cannot set navigation context". - -**Solution**: Ensure `TabService` has an active tab before using navigation context. This typically happens during app initialization before tabs are ready. - -### Context Not Isolated Between Tabs - -**Problem**: Contexts from one tab appearing in another. - -**Solution**: This should not happen with tab metadata storage. If you see this, it may indicate a TabService issue. Check that `TabService.activatedTabId()` returns the correct tab ID. - ---- - -## Running Tests - -```bash -# Run tests -npx nx test core-navigation - -# Run tests with coverage -npx nx test core-navigation --coverage.enabled=true - -# Run tests without cache (CI) -npx nx test core-navigation --skip-cache -``` - -**Test Results:** -- 79 tests passing -- 2 test files (navigation-state.service.spec.ts, navigation-context.service.spec.ts) - ---- - -## CI/CD Integration - -This library generates JUnit and Cobertura reports for Azure Pipelines: - -- **JUnit Report**: `testresults/junit-core-navigation.xml` -- **Cobertura Report**: `coverage/libs/core/navigation/cobertura-coverage.xml` - ---- - -## Contributing - -This library follows the ISA Frontend monorepo conventions: - -- **Path Alias**: `@isa/core/navigation` -- **Testing Framework**: Vitest with Angular Testing Utilities -- **Code Style**: ESLint + Prettier -- **Test Coverage**: Required for all public APIs -- **Dependencies**: Requires `@isa/core/tabs` for tab scoping - ---- - -## License - -Internal ISA Frontend monorepo library. diff --git a/libs/core/navigation/eslint.config.cjs b/libs/core/navigation/eslint.config.cjs deleted file mode 100644 index bdab98018..000000000 --- a/libs/core/navigation/eslint.config.cjs +++ /dev/null @@ -1,34 +0,0 @@ -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: 'core', - style: 'camelCase', - }, - ], - '@angular-eslint/component-selector': [ - 'error', - { - type: 'element', - prefix: 'core', - style: 'kebab-case', - }, - ], - }, - }, - { - files: ['**/*.html'], - // Override or add rules here - rules: {}, - }, -]; diff --git a/libs/core/navigation/project.json b/libs/core/navigation/project.json deleted file mode 100644 index 61c69246f..000000000 --- a/libs/core/navigation/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "core-navigation", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/core/navigation/src", - "prefix": "core", - "projectType": "library", - "tags": ["skip:ci"], - "targets": { - "test": { - "executor": "@nx/vite:test", - "outputs": ["{options.reportsDirectory}"], - "options": { - "reportsDirectory": "../../../coverage/libs/core/navigation" - } - }, - "lint": { - "executor": "@nx/eslint:lint" - } - } -} diff --git a/libs/core/navigation/src/index.ts b/libs/core/navigation/src/index.ts deleted file mode 100644 index 6a10b041d..000000000 --- a/libs/core/navigation/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './lib/navigation-state.types'; -export * from './lib/navigation-state.service'; -export * from './lib/navigation-context.types'; -export * from './lib/navigation-context.service'; -export * from './lib/navigation-context.constants'; diff --git a/libs/core/navigation/src/lib/navigation-context.constants.ts b/libs/core/navigation/src/lib/navigation-context.constants.ts deleted file mode 100644 index b23d06293..000000000 --- a/libs/core/navigation/src/lib/navigation-context.constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Constants for navigation context storage in tab metadata. - * Navigation contexts are stored directly in tab metadata instead of sessionStorage, - * providing automatic cleanup when tabs are closed and better integration with the tab system. - */ - -/** - * Key used to store navigation contexts in tab metadata. - * Contexts are stored as: tab.metadata[NAVIGATION_CONTEXT_METADATA_KEY][customScope] - * - * @example - * ```typescript - * // Structure in tab metadata: - * tab.metadata = { - * 'navigation-contexts': { - * 'default': { data: { returnUrl: '/cart' }, createdAt: 123 }, - * 'customer-details': { data: { customerId: 42 }, createdAt: 456 } - * } - * } - * ``` - */ -export const NAVIGATION_CONTEXT_METADATA_KEY = 'navigation-contexts'; diff --git a/libs/core/navigation/src/lib/navigation-context.service.spec.ts b/libs/core/navigation/src/lib/navigation-context.service.spec.ts deleted file mode 100644 index c24dff77c..000000000 --- a/libs/core/navigation/src/lib/navigation-context.service.spec.ts +++ /dev/null @@ -1,668 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { signal } from '@angular/core'; -import { NavigationContextService } from './navigation-context.service'; -import { TabService } from '@isa/core/tabs'; -import { ReturnUrlContext } from './navigation-context.types'; -import { NAVIGATION_CONTEXT_METADATA_KEY } from './navigation-context.constants'; - -describe('NavigationContextService', () => { - let service: NavigationContextService; - let tabServiceMock: { - activatedTabId: ReturnType>; - entityMap: ReturnType; - patchTabMetadata: ReturnType; - }; - - beforeEach(() => { - // Create mock TabService with signals and methods - tabServiceMock = { - activatedTabId: signal(null), - entityMap: vi.fn(), - patchTabMetadata: vi.fn(), - }; - - TestBed.configureTestingModule({ - providers: [ - NavigationContextService, - { provide: TabService, useValue: tabServiceMock }, - ], - }); - - service = TestBed.inject(NavigationContextService); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - describe('setContext', () => { - it('should set context in tab metadata', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - const data: ReturnUrlContext = { returnUrl: '/test-page' }; - - // Act - await service.setContext(data); - - // Assert - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith( - tabId, - expect.objectContaining({ - [NAVIGATION_CONTEXT_METADATA_KEY]: expect.objectContaining({ - default: expect.objectContaining({ - data, - createdAt: expect.any(Number), - }), - }), - }), - ); - }); - - it('should set context with custom scope', async () => { - // Arrange - const tabId = 123; - const customScope = 'customer-details'; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - const data = { customerId: 42 }; - - // Act - await service.setContext(data, customScope); - - // Assert - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith( - tabId, - expect.objectContaining({ - [NAVIGATION_CONTEXT_METADATA_KEY]: expect.objectContaining({ - [customScope]: expect.objectContaining({ - data, - createdAt: expect.any(Number), - }), - }), - }), - ); - }); - - it('should throw error when no active tab', async () => { - // Arrange - tabServiceMock.activatedTabId.set(null); - - // Act & Assert - await expect(service.setContext({ returnUrl: '/test' })).rejects.toThrow( - 'No active tab - cannot set navigation context', - ); - }); - - it('should merge with existing contexts', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - - const existingContexts = { - 'existing-scope': { - data: { existingData: 'value' }, - createdAt: Date.now() - 1000, - }, - }; - - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: existingContexts, - }, - }, - }); - - const newData = { returnUrl: '/new-page' }; - - // Act - await service.setContext(newData); - - // Assert - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith( - tabId, - expect.objectContaining({ - [NAVIGATION_CONTEXT_METADATA_KEY]: expect.objectContaining({ - 'existing-scope': existingContexts['existing-scope'], - default: expect.objectContaining({ - data: newData, - }), - }), - }), - ); - }); - - it('should accept TTL parameter for backward compatibility', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - // Act - await service.setContext({ returnUrl: '/test' }, undefined, 60000); - - // Assert - TTL is ignored but method should still work - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalled(); - }); - }); - - describe('getContext', () => { - it('should return null when no active tab', async () => { - // Arrange - tabServiceMock.activatedTabId.set(null); - - // Act - const result = await service.getContext(); - - // Assert - expect(result).toBeNull(); - }); - - it('should return null when context does not exist', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - // Act - const result = await service.getContext(); - - // Assert - expect(result).toBeNull(); - }); - - it('should retrieve context from default scope', async () => { - // Arrange - const tabId = 123; - const data: ReturnUrlContext = { returnUrl: '/test-page' }; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - default: { - data, - createdAt: Date.now(), - }, - }, - }, - }, - }); - - // Act - const result = await service.getContext(); - - // Assert - expect(result).toEqual(data); - }); - - it('should retrieve context from custom scope', async () => { - // Arrange - const tabId = 123; - const customScope = 'checkout-flow'; - const data = { step: 2, productId: 456 }; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - [customScope]: { - data, - createdAt: Date.now(), - }, - }, - }, - }, - }); - - // Act - const result = await service.getContext(customScope); - - // Assert - expect(result).toEqual(data); - }); - - it('should return null when tab not found', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({}); - - // Act - const result = await service.getContext(); - - // Assert - expect(result).toBeNull(); - }); - - it('should handle invalid metadata gracefully', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: 'invalid', // Invalid type - }, - }, - }); - - // Act - const result = await service.getContext(); - - // Assert - expect(result).toBeNull(); - }); - }); - - describe('getAndClearContext', () => { - it('should return null when no active tab', async () => { - // Arrange - tabServiceMock.activatedTabId.set(null); - - // Act - const result = await service.getAndClearContext(); - - // Assert - expect(result).toBeNull(); - }); - - it('should retrieve and remove context from default scope', async () => { - // Arrange - const tabId = 123; - const data: ReturnUrlContext = { returnUrl: '/test-page' }; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - default: { - data, - createdAt: Date.now(), - }, - }, - }, - }, - }); - - // Act - const result = await service.getAndClearContext(); - - // Assert - expect(result).toEqual(data); - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, { - [NAVIGATION_CONTEXT_METADATA_KEY]: {}, - }); - }); - - it('should retrieve and remove context from custom scope', async () => { - // Arrange - const tabId = 123; - const customScope = 'wizard-flow'; - const data = { currentStep: 3 }; - const otherScopeData = { otherData: 'value' }; - - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - [customScope]: { - data, - createdAt: Date.now(), - }, - 'other-scope': { - data: otherScopeData, - createdAt: Date.now(), - }, - }, - }, - }, - }); - - // Act - const result = await service.getAndClearContext(customScope); - - // Assert - expect(result).toEqual(data); - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - 'other-scope': expect.objectContaining({ - data: otherScopeData, - }), - }, - }); - }); - - it('should return null when context not found', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - // Act - const result = await service.getAndClearContext(); - - // Assert - expect(result).toBeNull(); - expect(tabServiceMock.patchTabMetadata).not.toHaveBeenCalled(); - }); - }); - - describe('clearContext', () => { - it('should return true when context exists and is cleared', async () => { - // Arrange - const tabId = 123; - const data = { returnUrl: '/test' }; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - default: { data, createdAt: Date.now() }, - }, - }, - }, - }); - - // Act - const result = await service.clearContext(); - - // Assert - expect(result).toBe(true); - }); - - it('should return false when context not found', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - // Act - const result = await service.clearContext(); - - // Assert - expect(result).toBe(false); - }); - }); - - describe('clearScope', () => { - it('should clear all contexts for active tab', async () => { - // Arrange - const tabId = 123; - const contexts = { - default: { data: { url: '/test' }, createdAt: Date.now() }, - 'scope-1': { data: { value: 1 }, createdAt: Date.now() }, - 'scope-2': { data: { value: 2 }, createdAt: Date.now() }, - }; - - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: contexts, - }, - }, - }); - - // Act - const clearedCount = await service.clearScope(); - - // Assert - expect(clearedCount).toBe(3); - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, { - [NAVIGATION_CONTEXT_METADATA_KEY]: {}, - }); - }); - - it('should return 0 when no contexts exist', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - // Act - const clearedCount = await service.clearScope(); - - // Assert - expect(clearedCount).toBe(0); - expect(tabServiceMock.patchTabMetadata).not.toHaveBeenCalled(); - }); - - it('should return 0 when no active tab', async () => { - // Arrange - tabServiceMock.activatedTabId.set(null); - - // Act - const clearedCount = await service.clearScope(); - - // Assert - expect(clearedCount).toBe(0); - }); - }); - - describe('clearAll', () => { - it('should clear all contexts for active tab', async () => { - // Arrange - const tabId = 123; - const contexts = { - default: { data: { url: '/test' }, createdAt: Date.now() }, - }; - - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: contexts, - }, - }, - }); - - // Act - await service.clearAll(); - - // Assert - expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, { - [NAVIGATION_CONTEXT_METADATA_KEY]: {}, - }); - }); - }); - - describe('hasContext', () => { - it('should return true when context exists', async () => { - // Arrange - const tabId = 123; - const data = { returnUrl: '/test' }; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - default: { data, createdAt: Date.now() }, - }, - }, - }, - }); - - // Act - const result = await service.hasContext(); - - // Assert - expect(result).toBe(true); - }); - - it('should return false when context does not exist', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - // Act - const result = await service.hasContext(); - - // Assert - expect(result).toBe(false); - }); - - it('should check custom scope', async () => { - // Arrange - const tabId = 123; - const customScope = 'wizard'; - const data = { step: 1 }; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: { - [customScope]: { data, createdAt: Date.now() }, - }, - }, - }, - }); - - // Act - const result = await service.hasContext(customScope); - - // Assert - expect(result).toBe(true); - }); - }); - - describe('getContextCount', () => { - it('should return total number of contexts for active tab', async () => { - // Arrange - const tabId = 123; - const contexts = { - default: { data: { url: '/test' }, createdAt: Date.now() }, - 'scope-1': { data: { value: 1 }, createdAt: Date.now() }, - 'scope-2': { data: { value: 2 }, createdAt: Date.now() }, - }; - - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: { - [NAVIGATION_CONTEXT_METADATA_KEY]: contexts, - }, - }, - }); - - // Act - const count = await service.getContextCount(); - - // Assert - expect(count).toBe(3); - }); - - it('should return 0 when no contexts exist', async () => { - // Arrange - const tabId = 123; - tabServiceMock.activatedTabId.set(tabId); - tabServiceMock.entityMap.mockReturnValue({ - [tabId]: { - id: tabId, - name: 'Test Tab', - metadata: {}, - }, - }); - - // Act - const count = await service.getContextCount(); - - // Assert - expect(count).toBe(0); - }); - - it('should return 0 when no active tab', async () => { - // Arrange - tabServiceMock.activatedTabId.set(null); - - // Act - const count = await service.getContextCount(); - - // Assert - expect(count).toBe(0); - }); - }); -}); diff --git a/libs/core/navigation/src/lib/navigation-context.service.ts b/libs/core/navigation/src/lib/navigation-context.service.ts deleted file mode 100644 index 170b58bf0..000000000 --- a/libs/core/navigation/src/lib/navigation-context.service.ts +++ /dev/null @@ -1,442 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { TabService } from '@isa/core/tabs'; -import { logger } from '@isa/core/logging'; -import { - NavigationContext, - NavigationContextData, - NavigationContextsMetadataSchema, -} from './navigation-context.types'; -import { NAVIGATION_CONTEXT_METADATA_KEY } from './navigation-context.constants'; - -/** - * Service for managing navigation context using tab metadata storage. - * - * This service provides a type-safe approach to preserving navigation state - * across intermediate navigations, solving the problem of lost router state - * in multi-step flows. - * - * Key Features: - * - Stores contexts in tab metadata (automatic cleanup when tab closes) - * - Type-safe with Zod validation - * - Scoped to individual tabs (no cross-tab pollution) - * - Simple API with hierarchical scoping support - * - * Storage Architecture: - * - Contexts stored at: `tab.metadata[NAVIGATION_CONTEXT_METADATA_KEY]` - * - Structure: `{ [customScope]: { data, createdAt } }` - * - No manual cleanup needed (handled by tab lifecycle) - * - * @example - * ```typescript - * // Start of flow - preserve context (auto-scoped to active tab) - * contextService.setContext({ - * returnUrl: '/original-page', - * customerId: 123 - * }); - * - * // ... intermediate navigations happen ... - * - * // End of flow - restore and cleanup - * const context = contextService.getAndClearContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * await router.navigateByUrl(context.returnUrl); - * } - * ``` - */ -@Injectable({ providedIn: 'root' }) -export class NavigationContextService { - readonly #tabService = inject(TabService); - readonly #log = logger(() => ({ module: 'navigation-context' })); - - /** - * Get the navigation contexts map from tab metadata. - * - * @param tabId The tab ID to get contexts for - * @returns Record of scope keys to contexts, or empty object if not found - */ - #getContextsMap(tabId: number): Record { - const tab = this.#tabService.entityMap()[tabId]; - if (!tab) { - this.#log.debug('Tab not found', () => ({ tabId })); - return {}; - } - - const contextsMap = tab.metadata[NAVIGATION_CONTEXT_METADATA_KEY]; - if (!contextsMap) { - return {}; - } - - // Validate with Zod schema - const result = NavigationContextsMetadataSchema.safeParse(contextsMap); - if (!result.success) { - this.#log.warn('Invalid contexts map in tab metadata', () => ({ - tabId, - validationErrors: result.error.errors, - })); - return {}; - } - - return result.data as Record; - } - - /** - * Save the navigation contexts map to tab metadata. - * - * @param tabId The tab ID to save contexts to - * @param contextsMap The contexts map to save - */ - #saveContextsMap( - tabId: number, - contextsMap: Record, - ): void { - this.#tabService.patchTabMetadata(tabId, { - [NAVIGATION_CONTEXT_METADATA_KEY]: contextsMap, - }); - } - - /** - * Set a context in the active tab's metadata. - * - * Creates or overwrites a navigation context and persists it to tab metadata. - * The context will automatically be cleaned up when the tab is closed. - * - * @template T The type of data being stored in the context - * @param data The navigation data to preserve - * @param customScope Optional custom scope (defaults to 'default') - * @param _ttl Optional TTL parameter (kept for API compatibility but ignored) - * - * @example - * ```typescript - * // Set context for default scope - * contextService.setContext({ returnUrl: '/products', selectedIds: [1, 2, 3] }); - * - * // Set context for custom scope - * contextService.setContext({ customerId: 42 }, 'customer-details'); - * ``` - */ - async setContext( - data: T, - customScope?: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _ttl?: number, // Kept for API compatibility but ignored - ): Promise { - const tabId = this.#tabService.activatedTabId(); - if (tabId === null) { - throw new Error('No active tab - cannot set navigation context'); - } - - const scopeKey = customScope || 'default'; - const context: NavigationContext = { - data, - createdAt: Date.now(), - }; - - const contextsMap = this.#getContextsMap(tabId); - contextsMap[scopeKey] = context; - this.#saveContextsMap(tabId, contextsMap); - - this.#log.debug('Context set in tab metadata', () => ({ - tabId, - scopeKey, - dataKeys: Object.keys(data), - totalContexts: Object.keys(contextsMap).length, - })); - } - - /** - * Patch a context in the active tab's metadata. - * - * Merges partial data with the existing context, preserving unspecified properties. - * Properties explicitly set to `undefined` will be removed from the context. - * If the context doesn't exist, creates a new one (behaves like setContext). - * - * @template T The type of data being patched - * @param partialData The partial navigation data to merge - * @param customScope Optional custom scope (defaults to 'default') - * - * @example - * ```typescript - * // Clear one property while preserving others - * contextService.patchContext({ autoTriggerContinueFn: undefined }, 'select-customer'); - * - * // Update one property while preserving others - * contextService.patchContext({ selectedTab: 'details' }, 'customer-flow'); - * ``` - */ - async patchContext( - partialData: Partial, - customScope?: string, - ): Promise { - const tabId = this.#tabService.activatedTabId(); - if (tabId === null) { - throw new Error('No active tab - cannot patch navigation context'); - } - - const scopeKey = customScope || 'default'; - const existingContext = await this.getContext(customScope); - - const mergedData = { - ...(existingContext ?? {}), - ...partialData, - }; - - // Remove properties explicitly set to undefined - const removedKeys: string[] = []; - Object.keys(mergedData).forEach((key) => { - if (mergedData[key] === undefined) { - removedKeys.push(key); - delete mergedData[key]; - } - }); - - const contextsMap = this.#getContextsMap(tabId); - const context: NavigationContext = { - data: mergedData, - createdAt: - existingContext && contextsMap[scopeKey] - ? contextsMap[scopeKey].createdAt - : Date.now(), - }; - - contextsMap[scopeKey] = context; - this.#saveContextsMap(tabId, contextsMap); - - this.#log.debug('Context patched in tab metadata', () => ({ - tabId, - scopeKey, - patchedKeys: Object.keys(partialData), - removedKeys, - totalDataKeys: Object.keys(mergedData), - wasUpdate: existingContext !== null, - totalContexts: Object.keys(contextsMap).length, - })); - } - - /** - * Get a context from the active tab's metadata without removing it. - * - * Retrieves a preserved navigation context by scope. - * - * @template T The expected type of the context data - * @param customScope Optional custom scope (defaults to 'default') - * @returns The context data, or null if not found - * - * @example - * ```typescript - * // Get context for default scope - * const context = contextService.getContext<{ returnUrl: string }>(); - * - * // Get context for custom scope - * const context = contextService.getContext<{ customerId: number }>('customer-details'); - * ``` - */ - async getContext( - customScope?: string, - ): Promise { - const tabId = this.#tabService.activatedTabId(); - if (tabId === null) { - this.#log.debug('No active tab - cannot get context'); - return null; - } - - const scopeKey = customScope || 'default'; - const contextsMap = this.#getContextsMap(tabId); - const context = contextsMap[scopeKey]; - - if (!context) { - this.#log.debug('Context not found', () => ({ tabId, scopeKey })); - return null; - } - - this.#log.debug('Context retrieved', () => ({ - tabId, - scopeKey, - dataKeys: Object.keys(context.data), - })); - - return context.data as T; - } - - /** - * Get a context from the active tab's metadata and remove it. - * - * Retrieves a preserved navigation context and removes it from the metadata. - * Use this when completing a flow to clean up automatically. - * - * @template T The expected type of the context data - * @param customScope Optional custom scope (defaults to 'default') - * @returns The context data, or null if not found - * - * @example - * ```typescript - * // Get and clear context for default scope - * const context = contextService.getAndClearContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * await router.navigateByUrl(context.returnUrl); - * } - * - * // Get and clear context for custom scope - * const context = contextService.getAndClearContext<{ customerId: number }>('customer-details'); - * ``` - */ - async getAndClearContext< - T extends NavigationContextData = NavigationContextData, - >(customScope?: string): Promise { - const tabId = this.#tabService.activatedTabId(); - if (tabId === null) { - this.#log.debug('No active tab - cannot get and clear context'); - return null; - } - - const scopeKey = customScope || 'default'; - const contextsMap = this.#getContextsMap(tabId); - const context = contextsMap[scopeKey]; - - if (!context) { - this.#log.debug('Context not found for clearing', () => ({ - tabId, - scopeKey, - })); - return null; - } - - // Remove from map - delete contextsMap[scopeKey]; - this.#saveContextsMap(tabId, contextsMap); - - this.#log.debug('Context retrieved and cleared', () => ({ - tabId, - scopeKey, - dataKeys: Object.keys(context.data), - remainingContexts: Object.keys(contextsMap).length, - })); - - return context.data as T; - } - - /** - * Clear a specific context from the active tab's metadata. - * - * Removes a context without returning its data. - * Useful for explicit cleanup without needing the data. - * - * @param customScope Optional custom scope (defaults to 'default') - * @returns true if context was found and cleared, false otherwise - * - * @example - * ```typescript - * // Clear context for default scope - * contextService.clearContext(); - * - * // Clear context for custom scope - * contextService.clearContext('customer-details'); - * ``` - */ - async clearContext(customScope?: string): Promise { - const result = await this.getAndClearContext(customScope); - return result !== null; - } - - /** - * Clear all contexts for the active tab. - * - * Removes all contexts from the active tab's metadata. - * Useful for cleanup when a workflow is cancelled or completed. - * - * @returns The number of contexts cleared - * - * @example - * ```typescript - * // Clear all contexts for active tab - * const cleared = contextService.clearScope(); - * console.log(`Cleared ${cleared} contexts`); - * ``` - */ - async clearScope(): Promise { - const tabId = this.#tabService.activatedTabId(); - if (tabId === null) { - this.#log.warn('Cannot clear scope: no active tab'); - return 0; - } - - const contextsMap = this.#getContextsMap(tabId); - const contextCount = Object.keys(contextsMap).length; - - if (contextCount === 0) { - return 0; - } - - // Clear entire metadata key - this.#tabService.patchTabMetadata(tabId, { - [NAVIGATION_CONTEXT_METADATA_KEY]: {}, - }); - - this.#log.debug('Tab scope cleared', () => ({ - tabId, - clearedCount: contextCount, - })); - - return contextCount; - } - - /** - * Clear all contexts from the active tab (alias for clearScope). - * - * This method is kept for backward compatibility with the previous API. - * It clears all contexts for the active tab only, not globally. - * - * @example - * ```typescript - * contextService.clearAll(); - * ``` - */ - async clearAll(): Promise { - await this.clearScope(); - this.#log.debug('All contexts cleared for active tab'); - } - - /** - * Check if a context exists for the active tab. - * - * @param customScope Optional custom scope (defaults to 'default') - * @returns true if context exists, false otherwise - * - * @example - * ```typescript - * // Check default scope - * if (contextService.hasContext()) { - * const context = contextService.getContext(); - * } - * - * // Check custom scope - * if (contextService.hasContext('customer-details')) { - * const context = contextService.getContext('customer-details'); - * } - * ``` - */ - async hasContext(customScope?: string): Promise { - const context = await this.getContext(customScope); - return context !== null; - } - - /** - * Get the current context count for the active tab (for debugging/monitoring). - * - * @returns The total number of contexts in the active tab's metadata - * - * @example - * ```typescript - * const count = await contextService.getContextCount(); - * console.log(`Active tab has ${count} contexts`); - * ``` - */ - async getContextCount(): Promise { - const tabId = this.#tabService.activatedTabId(); - if (tabId === null) { - return 0; - } - - const contextsMap = this.#getContextsMap(tabId); - return Object.keys(contextsMap).length; - } -} diff --git a/libs/core/navigation/src/lib/navigation-context.types.ts b/libs/core/navigation/src/lib/navigation-context.types.ts deleted file mode 100644 index bada5b424..000000000 --- a/libs/core/navigation/src/lib/navigation-context.types.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { z } from 'zod'; - -/** - * Base interface for navigation context data. - * Extend this interface for type-safe context preservation. - * - * @example - * ```typescript - * interface MyFlowContext extends NavigationContextData { - * returnUrl: string; - * selectedItems: number[]; - * } - * ``` - */ -export interface NavigationContextData { - [key: string]: unknown; -} - -/** - * Navigation context stored in tab metadata. - * Represents a single preserved navigation state with metadata. - * Stored at: tab.metadata[NAVIGATION_CONTEXT_METADATA_KEY][customScope] - */ -export interface NavigationContext { - /** The preserved navigation state/data */ - data: NavigationContextData; - /** Timestamp when context was created (for debugging and monitoring) */ - createdAt: number; - /** - * Optional expiration timestamp (reserved for future TTL implementation) - * @deprecated Currently unused - contexts are cleaned up automatically when tabs close - */ - expiresAt?: number; -} - -/** - * Zod schema for navigation context data validation. - */ -export const NavigationContextDataSchema = z.record(z.string(), z.unknown()); - -/** - * Zod schema for navigation context validation. - */ -export const NavigationContextSchema = z.object({ - data: NavigationContextDataSchema, - createdAt: z.number().positive(), - expiresAt: z.number().positive().optional(), -}); - -/** - * Zod schema for navigation contexts stored in tab metadata. - * Structure: { [customScope: string]: NavigationContext } - * - * @example - * ```typescript - * { - * "default": { data: { returnUrl: '/cart' }, createdAt: 123, expiresAt: 456 }, - * "customer-details": { data: { customerId: 42 }, createdAt: 123 } - * } - * ``` - */ -export const NavigationContextsMetadataSchema = z.record( - z.string(), - NavigationContextSchema -); - -/** - * Common navigation context for "return URL" pattern. - * Used when navigating through a flow and needing to return to the original location. - * - * @example - * ```typescript - * navContextService.preserveContext({ - * returnUrl: '/original-page' - * }); - * ``` - */ -export interface ReturnUrlContext extends NavigationContextData { - returnUrl: string; -} - -/** - * Extended context with additional flow metadata. - * Useful for complex multi-step flows that need to preserve additional state. - * - * @example - * ```typescript - * interface CheckoutFlowContext extends FlowContext { - * returnUrl: string; - * selectedProductIds: number[]; - * shippingAddressId?: number; - * } - * ``` - */ -export interface FlowContext extends NavigationContextData { - /** Step identifier for multi-step flows */ - currentStep?: string; - /** Total number of steps (if known) */ - totalSteps?: number; - /** Flow-specific metadata */ - metadata?: Record; -} diff --git a/libs/core/navigation/src/lib/navigation-state.service.spec.ts b/libs/core/navigation/src/lib/navigation-state.service.spec.ts deleted file mode 100644 index db689b3e0..000000000 --- a/libs/core/navigation/src/lib/navigation-state.service.spec.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { Location } from '@angular/common'; -import { Router } from '@angular/router'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { NavigationStateService } from './navigation-state.service'; -import { NavigationContextService } from './navigation-context.service'; -import { ReturnUrlContext } from './navigation-context.types'; - -describe('NavigationStateService', () => { - let service: NavigationStateService; - let locationMock: { getState: ReturnType }; - let routerMock: { navigate: ReturnType }; - let contextServiceMock: { - setContext: ReturnType; - getContext: ReturnType; - getAndClearContext: ReturnType; - clearContext: ReturnType; - hasContext: ReturnType; - clearScope: ReturnType; - }; - - beforeEach(() => { - locationMock = { - getState: vi.fn(), - }; - - routerMock = { - navigate: vi.fn(), - }; - - contextServiceMock = { - setContext: vi.fn().mockResolvedValue(undefined), - getContext: vi.fn().mockResolvedValue(null), - getAndClearContext: vi.fn().mockResolvedValue(null), - clearContext: vi.fn().mockResolvedValue(false), - hasContext: vi.fn().mockResolvedValue(false), - clearScope: vi.fn().mockResolvedValue(0), - }; - - TestBed.configureTestingModule({ - providers: [ - NavigationStateService, - { provide: Location, useValue: locationMock }, - { provide: Router, useValue: routerMock }, - { provide: NavigationContextService, useValue: contextServiceMock }, - ], - }); - - service = TestBed.inject(NavigationStateService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - // Context Preservation Methods Tests - - describe('preserveContext', () => { - it('should call contextService.setContext with correct parameters', async () => { - const data: ReturnUrlContext = { returnUrl: '/test-page' }; - const scopeKey = 'process-123'; - - await service.preserveContext(data, scopeKey); - - expect(contextServiceMock.setContext).toHaveBeenCalledWith(data, scopeKey); - }); - - it('should work without scope key', async () => { - const data: ReturnUrlContext = { returnUrl: '/test-page' }; - - await service.preserveContext(data); - - expect(contextServiceMock.setContext).toHaveBeenCalledWith(data, undefined); - }); - }); - - describe('restoreContext', () => { - it('should call contextService.getContext with correct parameters', async () => { - const expectedData: ReturnUrlContext = { returnUrl: '/test-page' }; - contextServiceMock.getContext.mockResolvedValue(expectedData); - - const result = await service.restoreContext('scope-123'); - - expect(contextServiceMock.getContext).toHaveBeenCalledWith('scope-123'); - expect(result).toEqual(expectedData); - }); - - it('should return null when context not found', async () => { - contextServiceMock.getContext.mockResolvedValue(null); - - const result = await service.restoreContext(); - - expect(result).toBeNull(); - }); - - it('should work without parameters', async () => { - const expectedData: ReturnUrlContext = { returnUrl: '/test-page' }; - contextServiceMock.getContext.mockResolvedValue(expectedData); - - const result = await service.restoreContext(); - - expect(contextServiceMock.getContext).toHaveBeenCalledWith(undefined); - expect(result).toEqual(expectedData); - }); - }); - - describe('restoreAndClearContext', () => { - it('should call contextService.getAndClearContext with correct parameters', async () => { - const expectedData: ReturnUrlContext = { returnUrl: '/test-page' }; - contextServiceMock.getAndClearContext.mockResolvedValue(expectedData); - - const result = await service.restoreAndClearContext('scope-123'); - - expect(contextServiceMock.getAndClearContext).toHaveBeenCalledWith('scope-123'); - expect(result).toEqual(expectedData); - }); - - it('should return null when context not found', async () => { - contextServiceMock.getAndClearContext.mockResolvedValue(null); - - const result = await service.restoreAndClearContext(); - - expect(result).toBeNull(); - }); - }); - - describe('clearPreservedContext', () => { - it('should call contextService.clearContext and return result', async () => { - contextServiceMock.clearContext.mockResolvedValue(true); - - const result = await service.clearPreservedContext('scope-123'); - - expect(contextServiceMock.clearContext).toHaveBeenCalledWith('scope-123'); - expect(result).toBe(true); - }); - - it('should return false when context not found', async () => { - contextServiceMock.clearContext.mockResolvedValue(false); - - const result = await service.clearPreservedContext(); - - expect(result).toBe(false); - }); - }); - - describe('hasPreservedContext', () => { - it('should call contextService.hasContext and return result', async () => { - contextServiceMock.hasContext.mockResolvedValue(true); - - const result = await service.hasPreservedContext('scope-123'); - - expect(contextServiceMock.hasContext).toHaveBeenCalledWith('scope-123'); - expect(result).toBe(true); - }); - - it('should return false when context not found', async () => { - contextServiceMock.hasContext.mockResolvedValue(false); - - const result = await service.hasPreservedContext(); - - expect(result).toBe(false); - }); - }); - - describe('navigateWithPreservedContext', () => { - it('should preserve context and navigate', async () => { - const data: ReturnUrlContext = { returnUrl: '/reward/cart', customerId: 123 }; - const commands = ['/customer/search']; - const scopeKey = 'process-123'; - - routerMock.navigate.mockResolvedValue(true); - - const result = await service.navigateWithPreservedContext(commands, data, scopeKey); - - expect(contextServiceMock.setContext).toHaveBeenCalledWith(data, scopeKey); - expect(routerMock.navigate).toHaveBeenCalledWith(commands, { - state: data, - }); - expect(result).toEqual({ success: true }); - }); - - it('should merge navigation extras', async () => { - const data: ReturnUrlContext = { returnUrl: '/test' }; - const commands = ['/page']; - const extras = { queryParams: { foo: 'bar' } }; - - routerMock.navigate.mockResolvedValue(true); - - await service.navigateWithPreservedContext(commands, data, undefined, extras); - - expect(routerMock.navigate).toHaveBeenCalledWith(commands, { - queryParams: { foo: 'bar' }, - state: data, - }); - }); - - it('should return false when navigation fails', async () => { - const data: ReturnUrlContext = { returnUrl: '/test' }; - const commands = ['/page']; - - routerMock.navigate.mockResolvedValue(false); - - const result = await service.navigateWithPreservedContext(commands, data); - - expect(result).toEqual({ success: false }); - }); - }); - - describe('clearScopeContexts', () => { - it('should call contextService.clearScope and return count', async () => { - contextServiceMock.clearScope.mockResolvedValue(3); - - const result = await service.clearScopeContexts(); - - expect(contextServiceMock.clearScope).toHaveBeenCalled(); - expect(result).toBe(3); - }); - - it('should return 0 when no contexts cleared', async () => { - contextServiceMock.clearScope.mockResolvedValue(0); - - const result = await service.clearScopeContexts(); - - expect(result).toBe(0); - }); - }); -}); diff --git a/libs/core/navigation/src/lib/navigation-state.service.ts b/libs/core/navigation/src/lib/navigation-state.service.ts deleted file mode 100644 index 4983e0127..000000000 --- a/libs/core/navigation/src/lib/navigation-state.service.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { Router, NavigationExtras } from '@angular/router'; -import { NavigationContextService } from './navigation-context.service'; -import { NavigationContextData } from './navigation-context.types'; - -/** - * Service for managing navigation context preservation across multi-step flows. - * - * This service provides automatic context preservation using tab metadata, - * allowing navigation state to survive intermediate navigations. Contexts are - * automatically scoped to the active tab and cleaned up when the tab closes. - * - * ## Context Preservation for Multi-Step Flows - * - * @example - * ```typescript - * // Start of flow - preserve context (automatically scoped to active tab) - * await navigationStateService.preserveContext({ - * returnUrl: '/reward/cart', - * customerId: 123 - * }); - * - * // ... multiple intermediate navigations happen ... - * await router.navigate(['/customer/details']); - * await router.navigate(['/add-shipping-address']); - * - * // End of flow - restore and cleanup (auto-scoped to active tab) - * const context = await navigationStateService.restoreAndClearContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * await router.navigateByUrl(context.returnUrl); - * } - * ``` - * - * ## Simplified Navigation with Context - * - * @example - * ```typescript - * // Navigate and preserve context in one call - * const { success } = await navigationStateService.navigateWithPreservedContext( - * ['/customer/search'], - * { returnUrl: '/reward/cart' } - * ); - * - * // Later, restore and navigate back - * const context = await navigationStateService.restoreAndClearContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * await router.navigateByUrl(context.returnUrl); - * } - * ``` - * - * ## Automatic Tab Cleanup - * - * @example - * ```typescript - * ngOnDestroy() { - * // Clean up all contexts when tab closes (auto-uses active tab ID) - * this.navigationStateService.clearScopeContexts(); - * } - * ``` - * - * Key Features: - * - ✅ Automatic tab scoping using TabService - * - ✅ Stored in tab metadata (automatic cleanup when tab closes) - * - ✅ Type-safe with TypeScript generics and Zod validation - * - ✅ Automatic cleanup with restoreAndClearContext() - * - ✅ Support for multiple custom scopes per tab - * - ✅ No manual expiration management needed - * - ✅ Platform-agnostic (works with SSR) - */ -@Injectable({ providedIn: 'root' }) -export class NavigationStateService { - readonly #router = inject(Router); - readonly #contextService = inject(NavigationContextService); - - // Context Preservation Methods - - /** - * Preserve navigation state for multi-step flows. - * - * This method stores navigation context in tab metadata, allowing it to - * persist across intermediate navigations within a flow. Contexts are automatically - * scoped to the active tab, with optional custom scope for different flows. - * - * Use this when starting a flow that will have intermediate navigations - * before returning to the original location. - * - * @template T The type of state data being preserved - * @param state The navigation state to preserve - * @param customScope Optional custom scope within the tab (e.g., 'customer-details') - * - * @example - * ```typescript - * // Preserve context for default tab scope - * await navigationStateService.preserveContext({ returnUrl: '/products' }); - * - * // Preserve context for custom scope within tab - * await navigationStateService.preserveContext({ customerId: 42 }, 'customer-details'); - * - * // ... multiple intermediate navigations ... - * - * // Restore at end of flow - * const context = await navigationStateService.restoreContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * await router.navigateByUrl(context.returnUrl); - * } - * ``` - */ - async preserveContext( - state: T, - customScope?: string, - ): Promise { - await this.#contextService.setContext(state, customScope); - } - - /** - * Patch preserved navigation state. - * - * Merges partial state with existing preserved context, keeping unspecified properties intact. - * This is useful when you need to update or clear specific properties without replacing - * the entire context. Properties set to `undefined` will be removed. - * - * Use cases: - * - Clear a trigger flag while preserving return URL - * - Update one property in a multi-property context - * - Remove specific properties from context - * - * @template T The type of state data being patched - * @param partialState The partial state to merge (properties set to undefined will be removed) - * @param customScope Optional custom scope within the tab - * - * @example - * ```typescript - * // Clear the autoTriggerContinueFn flag while preserving returnUrl - * await navigationStateService.patchContext( - * { autoTriggerContinueFn: undefined }, - * 'select-customer' - * ); - * - * // Update selectedTab while keeping other properties - * await navigationStateService.patchContext( - * { selectedTab: 'rewards' }, - * 'customer-flow' - * ); - * - * // Add a new property to existing context - * await navigationStateService.patchContext( - * { shippingAddressId: 123 }, - * 'checkout-flow' - * ); - * ``` - */ - async patchContext( - partialState: Partial, - customScope?: string, - ): Promise { - await this.#contextService.patchContext(partialState, customScope); - } - - /** - * Restore preserved navigation state. - * - * Retrieves a previously preserved navigation context for the active tab scope, - * or a custom scope if specified. - * - * This method does NOT remove the context - use clearPreservedContext() or - * restoreAndClearContext() for automatic cleanup. - * - * @template T The expected type of the preserved state - * @param customScope Optional custom scope (defaults to active tab scope) - * @returns The preserved state, or null if not found - * - * @example - * ```typescript - * // Restore from default tab scope - * const context = navigationStateService.restoreContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * console.log('Returning to:', context.returnUrl); - * } - * - * // Restore from custom scope - * const context = navigationStateService.restoreContext<{ customerId: number }>('customer-details'); - * ``` - */ - async restoreContext( - customScope?: string, - ): Promise { - return await this.#contextService.getContext(customScope); - } - - /** - * Restore and automatically clear preserved navigation state. - * - * Retrieves a preserved navigation context and removes it from tab metadata in one operation. - * Use this when completing a flow to clean up automatically. - * - * @template T The expected type of the preserved state - * @param customScope Optional custom scope (defaults to active tab scope) - * @returns The preserved state, or null if not found - * - * @example - * ```typescript - * // Restore and clear from default tab scope - * const context = await navigationStateService.restoreAndClearContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * await router.navigateByUrl(context.returnUrl); - * } - * - * // Restore and clear from custom scope - * const context = await navigationStateService.restoreAndClearContext<{ customerId: number }>('customer-details'); - * ``` - */ - async restoreAndClearContext< - T extends NavigationContextData = NavigationContextData, - >(customScope?: string): Promise { - return await this.#contextService.getAndClearContext(customScope); - } - - /** - * Clear a preserved navigation context. - * - * Removes a context from tab metadata without returning its data. - * Use this for explicit cleanup when you no longer need the preserved state. - * - * @param customScope Optional custom scope (defaults to active tab scope) - * @returns true if context was found and cleared, false otherwise - * - * @example - * ```typescript - * // Clear default tab scope context - * await navigationStateService.clearPreservedContext(); - * - * // Clear custom scope context - * await navigationStateService.clearPreservedContext('customer-details'); - * ``` - */ - async clearPreservedContext(customScope?: string): Promise { - return await this.#contextService.clearContext(customScope); - } - - /** - * Check if a preserved context exists. - * - * @param customScope Optional custom scope (defaults to active tab scope) - * @returns true if context exists, false otherwise - * - * @example - * ```typescript - * // Check default tab scope - * if (navigationStateService.hasPreservedContext()) { - * const context = navigationStateService.restoreContext(); - * } - * - * // Check custom scope - * if (navigationStateService.hasPreservedContext('customer-details')) { - * const context = navigationStateService.restoreContext('customer-details'); - * } - * ``` - */ - async hasPreservedContext(customScope?: string): Promise { - return await this.#contextService.hasContext(customScope); - } - - /** - * Navigate while preserving context state. - * - * Convenience method that combines navigation with context preservation. - * The context will be stored in tab metadata and available throughout the - * navigation flow and any intermediate navigations. Context is automatically - * scoped to the active tab. - * - * @param commands Navigation commands (same as Router.navigate) - * @param state The state to preserve - * @param customScope Optional custom scope within the tab - * @param extras Optional navigation extras - * @returns Promise resolving to navigation success status - * - * @example - * ```typescript - * // Navigate and preserve context - * const { success } = await navigationStateService.navigateWithPreservedContext( - * ['/customer/search'], - * { returnUrl: '/reward/cart', customerId: 123 } - * ); - * - * // Later, retrieve and navigate back - * const context = await navigationStateService.restoreAndClearContext<{ returnUrl: string }>(); - * if (context?.returnUrl) { - * await router.navigateByUrl(context.returnUrl); - * } - * ``` - */ - async navigateWithPreservedContext( - commands: unknown[], - state: T, - customScope?: string, - extras?: NavigationExtras, - ): Promise<{ success: boolean }> { - await this.preserveContext(state, customScope); - - // Also pass state via router for immediate access - const navigationExtras: NavigationExtras = { - ...extras, - state, - }; - - const success = await this.#router.navigate(commands, navigationExtras); - - return { success }; - } - - /** - * Clear all preserved contexts for the active tab. - * - * Removes all contexts for the active tab (both default and custom scopes). - * Useful for cleanup when a tab is closed. - * - * @returns The number of contexts cleared - * - * @example - * ```typescript - * // Clear all contexts for active tab - * ngOnDestroy() { - * const cleared = this.navigationStateService.clearScopeContexts(); - * console.log(`Cleared ${cleared} contexts`); - * } - * ``` - */ - async clearScopeContexts(): Promise { - return await this.#contextService.clearScope(); - } -} diff --git a/libs/core/navigation/src/lib/navigation-state.types.ts b/libs/core/navigation/src/lib/navigation-state.types.ts deleted file mode 100644 index 13dc096fd..000000000 --- a/libs/core/navigation/src/lib/navigation-state.types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Type definition for navigation state that can be passed through Angular Router. - * Use generic type parameter to ensure type safety for your specific state shape. - * - * @example - * ```typescript - * interface MyNavigationState extends NavigationState { - * returnUrl: string; - * customerId: number; - * } - * ``` - */ -export interface NavigationState { - [key: string]: unknown; -} - -/** - * Common navigation state for "return URL" pattern. - * Used when navigating to a page and needing to return to the previous location. - */ -export interface ReturnUrlNavigationState extends NavigationState { - returnUrl: string; -} diff --git a/libs/core/navigation/src/test-setup.ts b/libs/core/navigation/src/test-setup.ts deleted file mode 100644 index cebf5ae72..000000000 --- a/libs/core/navigation/src/test-setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -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(), -); diff --git a/libs/core/navigation/tsconfig.json b/libs/core/navigation/tsconfig.json deleted file mode 100644 index 3268ed4dc..000000000 --- a/libs/core/navigation/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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" - } - ] -} diff --git a/libs/core/navigation/tsconfig.lib.json b/libs/core/navigation/tsconfig.lib.json deleted file mode 100644 index 312ee86bb..000000000 --- a/libs/core/navigation/tsconfig.lib.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "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"] -} diff --git a/libs/core/navigation/tsconfig.spec.json b/libs/core/navigation/tsconfig.spec.json deleted file mode 100644 index 5785a8a5f..000000000 --- a/libs/core/navigation/tsconfig.spec.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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"] -} diff --git a/libs/core/navigation/vite.config.mts b/libs/core/navigation/vite.config.mts deleted file mode 100644 index 18ed5d6b9..000000000 --- a/libs/core/navigation/vite.config.mts +++ /dev/null @@ -1,33 +0,0 @@ -/// -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/core/navigation', - 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-core-navigation.xml' }], - ], - coverage: { - reportsDirectory: '../../../coverage/libs/core/navigation', - provider: 'v8' as const, - reporter: ['text', 'cobertura'], - }, - }, -})); diff --git a/tsconfig.base.json b/tsconfig.base.json index 0f5b3d3db..6f2d370c3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -71,7 +71,6 @@ "@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"], - "@isa/core/navigation": ["libs/core/navigation/src/index.ts"], "@isa/core/storage": ["libs/core/storage/src/index.ts"], "@isa/core/tabs": ["libs/core/tabs/src/index.ts"], "@isa/crm/data-access": ["libs/crm/data-access/src/index.ts"],