# @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: `<button (click)="editCustomer()">Edit Customer</button>`
})
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: `<button (click)="complete()">Complete</button>`
})
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<T>(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<T>(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<T>(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<T>(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<CheckoutContext>({
  returnUrl: '/reward/cart',
  selectedItems: [1, 2, 3],
  customerId: 456,
  metadata: {
    source: 'reward',
    timestamp: Date.now()
  }
});

// Restore with type safety
const context = navState.restoreAndClearContext<CheckoutContext>();
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<MyComponent>;
  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 (`<T>`)
- **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<T>, 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.
