mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1969: Reward Shopping Cart Implementation with Navigation State Management and Shipping Address Integration
1. Reward Shopping Cart Implementation - New shopping cart with quantity control and availability checking - Responsive shopping cart item component with improved CSS styling - Shipping address integration in cart - Customer reward card and billing/shipping address components 2. Navigation State Management Library (@isa/core/navigation) - New library with type-safe navigation context service (373 lines) - Navigation state service (287 lines) for temporary state between routes - Comprehensive test coverage (668 + 227 lines of tests) - Documentation (792 lines in README.md) - Replaces query parameters for passing temporary navigation context 3. CRM Shipping Address Services - New ShippingAddressService with fetching and validation - CustomerShippingAddressResource and CustomerShippingAddressesResource - Zod schemas for data validation 4. Additional Improvements - Enhanced searchbox accessibility with ARIA support - Availability data access rework for better fetching/mapping - Storybook tooltip variant support - Vitest JUnit and Cobertura reporting configuration Related work items: #5382, #5383, #5384
This commit is contained in:
committed by
Nino Righi
parent
f15848d5c0
commit
596ae1da1b
@@ -11,3 +11,346 @@
|
||||
- Use for complex application state
|
||||
- Follow feature-based store organization
|
||||
- Implement proper error handling
|
||||
|
||||
## Navigation State
|
||||
|
||||
Navigation state refers to temporary data preserved between routes during navigation flows. Unlike global state or local component state, navigation state is transient and tied to a specific navigation flow within a tab.
|
||||
|
||||
### Storage Architecture
|
||||
|
||||
Navigation contexts are stored in **tab metadata** using `@isa/core/navigation`:
|
||||
|
||||
```typescript
|
||||
// Context stored in tab metadata:
|
||||
tab.metadata['navigation-contexts'] = {
|
||||
'default': {
|
||||
data: { returnUrl: '/cart', customerId: 123 },
|
||||
createdAt: 1234567890
|
||||
},
|
||||
'customer-flow': {
|
||||
data: { step: 2, selectedOptions: ['A', 'B'] },
|
||||
createdAt: 1234567895
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ **Automatic cleanup** when tabs close (no manual cleanup needed)
|
||||
- ✅ **Tab isolation** (contexts don't leak between tabs)
|
||||
- ✅ **Persistence** across page refresh (via TabService UserStorage)
|
||||
- ✅ **Integration** with tab lifecycle management
|
||||
|
||||
### When to Use Navigation State
|
||||
|
||||
Use `@isa/core/navigation` for:
|
||||
|
||||
- **Return URLs**: Storing the previous route to navigate back to
|
||||
- **Wizard/Multi-step Forms**: Passing context between wizard steps
|
||||
- **Context Preservation**: Maintaining search queries, filters, or selections when drilling into details
|
||||
- **Temporary Data**: Any data needed only for the current navigation flow within a tab
|
||||
|
||||
### When NOT to Use Navigation State
|
||||
|
||||
Avoid using navigation state for:
|
||||
|
||||
- **Persistent Data**: Use NgRx stores or services instead
|
||||
- **Shareable URLs**: Use route parameters or query parameters if the URL should be bookmarkable
|
||||
- **Long-lived State**: Use session storage or NgRx with persistence
|
||||
- **Cross-tab Communication**: Use services with proper state management
|
||||
|
||||
### Best Practices
|
||||
|
||||
#### ✅ Do Use Navigation Context (Tab Metadata)
|
||||
|
||||
```typescript
|
||||
// Good: Clean URLs, automatic cleanup, tab-scoped
|
||||
navState.preserveContext({
|
||||
returnUrl: '/customer-list',
|
||||
searchQuery: 'John Doe',
|
||||
context: 'reward-selection'
|
||||
});
|
||||
|
||||
await router.navigate(['/customer', customerId]);
|
||||
|
||||
// Later (after intermediate navigations):
|
||||
const context = navState.restoreAndClearContext<{ returnUrl: string }>();
|
||||
await router.navigateByUrl(context.returnUrl);
|
||||
```
|
||||
|
||||
#### ❌ Don't Use Query Parameters for Temporary State
|
||||
|
||||
```typescript
|
||||
// Bad: URL pollution, can be overwritten, visible in browser bar
|
||||
await router.navigate(['/customer', customerId], {
|
||||
queryParams: {
|
||||
returnUrl: '/customer-list',
|
||||
searchQuery: 'John Doe'
|
||||
}
|
||||
});
|
||||
// URL becomes: /customer/123?returnUrl=%2Fcustomer-list&searchQuery=John%20Doe
|
||||
```
|
||||
|
||||
#### ❌ Don't Use Router State for Multi-Step Flows
|
||||
|
||||
```typescript
|
||||
// Bad: Lost after intermediate navigations
|
||||
await router.navigate(['/customer/search'], {
|
||||
state: { returnUrl: '/reward/cart' } // Lost after next navigation!
|
||||
});
|
||||
```
|
||||
|
||||
### Integration with TabService
|
||||
|
||||
Navigation context relies on `TabService` for automatic tab scoping:
|
||||
|
||||
```typescript
|
||||
// Context automatically scoped to active tab
|
||||
const tabId = tabService.activatedTabId(); // e.g., 123
|
||||
|
||||
// When you preserve context:
|
||||
navState.preserveContext({ returnUrl: '/cart' });
|
||||
// Stored in: tab[123].metadata['navigation-contexts']['default']
|
||||
|
||||
// When you preserve with custom scope:
|
||||
navState.preserveContext({ step: 2 }, 'wizard-flow');
|
||||
// Stored in: tab[123].metadata['navigation-contexts']['wizard-flow']
|
||||
```
|
||||
|
||||
**Automatic Cleanup:**
|
||||
When the tab closes, all contexts stored in that tab's metadata are automatically removed. No manual cleanup required!
|
||||
|
||||
### Usage Example: Multi-Step Flow
|
||||
|
||||
**Start of Flow (preserving context):**
|
||||
```typescript
|
||||
import { inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
|
||||
export class CustomerListComponent {
|
||||
private router = inject(Router);
|
||||
private navState = inject(NavigationStateService);
|
||||
|
||||
async viewCustomerDetails(customerId: number) {
|
||||
// Preserve context before navigating
|
||||
this.navState.preserveContext({
|
||||
returnUrl: this.router.url,
|
||||
searchQuery: this.searchForm.value.query
|
||||
});
|
||||
|
||||
await this.router.navigate(['/customer', customerId]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Intermediate Navigation (context persists):**
|
||||
```typescript
|
||||
export class CustomerDetailsComponent {
|
||||
private router = inject(Router);
|
||||
|
||||
async editAddress(addressId: number) {
|
||||
// Context still preserved through intermediate navigations
|
||||
await this.router.navigate(['/address/edit', addressId]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**End of Flow (restoring context):**
|
||||
```typescript
|
||||
import { inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
|
||||
interface CustomerNavigationContext {
|
||||
returnUrl: string;
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
export class AddressEditComponent {
|
||||
private router = inject(Router);
|
||||
private navState = inject(NavigationStateService);
|
||||
|
||||
async complete() {
|
||||
// Restore and clear context
|
||||
const context = this.navState.restoreAndClearContext<CustomerNavigationContext>();
|
||||
|
||||
if (context?.returnUrl) {
|
||||
await this.router.navigateByUrl(context.returnUrl);
|
||||
} else {
|
||||
// Fallback navigation
|
||||
await this.router.navigate(['/customers']);
|
||||
}
|
||||
}
|
||||
|
||||
// For template usage
|
||||
hasReturnUrl(): boolean {
|
||||
return this.navState.hasPreservedContext();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Template:**
|
||||
```html
|
||||
@if (hasReturnUrl()) {
|
||||
<button (click)="complete()">Zurück</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced: Multiple Concurrent Flows
|
||||
|
||||
Use custom scopes to manage multiple flows in the same tab:
|
||||
|
||||
```typescript
|
||||
export class DashboardComponent {
|
||||
private navState = inject(NavigationStateService);
|
||||
private router = inject(Router);
|
||||
|
||||
async startCustomerFlow() {
|
||||
// Save context for customer flow
|
||||
this.navState.preserveContext(
|
||||
{ returnUrl: '/dashboard', flowType: 'customer' },
|
||||
'customer-flow'
|
||||
);
|
||||
|
||||
await this.router.navigate(['/customer/search']);
|
||||
}
|
||||
|
||||
async startProductFlow() {
|
||||
// Save context for product flow (different scope)
|
||||
this.navState.preserveContext(
|
||||
{ returnUrl: '/dashboard', flowType: 'product' },
|
||||
'product-flow'
|
||||
);
|
||||
|
||||
await this.router.navigate(['/product/search']);
|
||||
}
|
||||
|
||||
async completeCustomerFlow() {
|
||||
// Restore customer flow context
|
||||
const context = this.navState.restoreAndClearContext('customer-flow');
|
||||
if (context?.returnUrl) {
|
||||
await this.router.navigateByUrl(context.returnUrl);
|
||||
}
|
||||
}
|
||||
|
||||
async completeProductFlow() {
|
||||
// Restore product flow context
|
||||
const context = this.navState.restoreAndClearContext('product-flow');
|
||||
if (context?.returnUrl) {
|
||||
await this.router.navigateByUrl(context.returnUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Architecture Decision
|
||||
|
||||
**Why Tab Metadata instead of SessionStorage?**
|
||||
|
||||
Tab metadata provides several advantages over SessionStorage:
|
||||
|
||||
- **Automatic Cleanup**: Contexts are automatically removed when tabs close (no manual cleanup or TTL management needed)
|
||||
- **Better Integration**: Seamlessly integrated with tab lifecycle and TabService
|
||||
- **Tab Isolation**: Impossible to leak contexts between tabs (scoped by tab ID)
|
||||
- **Simpler Mental Model**: Contexts are "owned" by tabs, not stored globally
|
||||
- **Persistence**: Tab metadata persists across page refresh via UserStorage
|
||||
|
||||
**Why not Query Parameters?**
|
||||
|
||||
Query parameters were traditionally used for passing state, but they have significant drawbacks:
|
||||
|
||||
- **URL Pollution**: Makes URLs long, ugly, and non-bookmarkable
|
||||
- **Overwritable**: Intermediate navigations can overwrite query parameters
|
||||
- **Security**: Sensitive data visible in browser URL bar
|
||||
- **User Experience**: Affects URL sharing and bookmarking
|
||||
|
||||
**Why not Router State?**
|
||||
|
||||
Angular's Router state mechanism has limitations:
|
||||
|
||||
- **Lost After Navigation**: State is lost after the immediate navigation (doesn't survive intermediate navigations)
|
||||
- **Not Persistent**: Doesn't survive page refresh
|
||||
- **No Tab Scoping**: Can't isolate state by tab
|
||||
|
||||
**Why Tab Metadata is Better:**
|
||||
|
||||
Navigation context using tab metadata provides:
|
||||
|
||||
- **Survives Intermediate Navigations**: State persists across multiple navigation steps
|
||||
- **Clean URLs**: No visible state in the URL bar
|
||||
- **Reliable**: State survives page refresh (via TabService UserStorage)
|
||||
- **Type-safe**: Full TypeScript support with generics
|
||||
- **Platform-agnostic**: Works with SSR/Angular Universal
|
||||
- **Automatic Cleanup**: No manual cleanup needed when tabs close
|
||||
- **Tab Isolation**: Contexts automatically scoped to tabs
|
||||
|
||||
### Comparison with Other State Solutions
|
||||
|
||||
| Feature | Navigation Context | NgRx Store | Service State | Query Params | Router State |
|
||||
|---------|-------------------|------------|---------------|--------------|--------------|
|
||||
| **Scope** | Tab-scoped flow | Application-wide | Feature-specific | URL-based | Single navigation |
|
||||
| **Persistence** | Until tab closes | Configurable | Component lifetime | URL lifetime | Lost after nav |
|
||||
| **Survives Refresh** | ✅ Yes | ⚠️ Optional | ❌ No | ✅ Yes | ❌ No |
|
||||
| **Survives Intermediate Navs** | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Sometimes | ❌ No |
|
||||
| **Automatic Cleanup** | ✅ Yes (tab close) | ❌ Manual | ❌ Manual | N/A | ✅ Yes |
|
||||
| **Tab Isolation** | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No |
|
||||
| **Clean URLs** | ✅ Yes | N/A | N/A | ❌ No | ✅ Yes |
|
||||
| **Shareability** | ❌ No | ❌ No | ❌ No | ✅ Yes | ❌ No |
|
||||
| **Type Safety** | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Limited | ✅ Yes |
|
||||
| **Use Case** | Multi-step flow | Global state | Feature state | Bookmarkable state | Simple navigation |
|
||||
|
||||
### Best Practices for Navigation Context
|
||||
|
||||
#### ✅ Do
|
||||
|
||||
- **Use for temporary flow context** (return URLs, wizard state, search filters)
|
||||
- **Use custom scopes** for multiple concurrent flows in the same tab
|
||||
- **Always use type safety** with TypeScript generics
|
||||
- **Trust automatic cleanup** - no need to manually clear contexts in `ngOnDestroy`
|
||||
- **Check for null** when restoring contexts (they may not exist)
|
||||
|
||||
#### ❌ Don't
|
||||
|
||||
- **Don't store large objects** - keep contexts lean (URLs, IDs, simple flags)
|
||||
- **Don't use for persistent data** - use NgRx or services for long-lived state
|
||||
- **Don't store sensitive data** - contexts may be visible in browser dev tools
|
||||
- **Don't manually clear in ngOnDestroy** - tab lifecycle handles cleanup automatically
|
||||
- **Don't use for cross-tab communication** - use services or BroadcastChannel
|
||||
|
||||
### Cleanup Behavior
|
||||
|
||||
**Automatic Cleanup (Recommended):**
|
||||
```typescript
|
||||
// ✅ No manual cleanup needed - tab lifecycle handles it!
|
||||
export class CustomerFlowComponent {
|
||||
navState = inject(NavigationStateService);
|
||||
|
||||
async startFlow() {
|
||||
this.navState.preserveContext({ returnUrl: '/home' });
|
||||
// Context automatically cleaned up when tab closes
|
||||
}
|
||||
|
||||
// No ngOnDestroy needed!
|
||||
}
|
||||
```
|
||||
|
||||
**Manual Cleanup (Rarely Needed):**
|
||||
```typescript
|
||||
// Use only if you need to explicitly clear contexts during tab lifecycle
|
||||
export class ComplexFlowComponent {
|
||||
navState = inject(NavigationStateService);
|
||||
|
||||
async cancelFlow() {
|
||||
// Explicitly clear all contexts for this tab
|
||||
const cleared = this.navState.clearScopeContexts();
|
||||
console.log(`Cleared ${cleared} contexts`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- **Full API Reference**: See [libs/core/navigation/README.md](../../libs/core/navigation/README.md) for complete documentation
|
||||
- **Usage Patterns**: Detailed examples and patterns in the library README
|
||||
- **Testing Guide**: Full testing guide included in the library documentation
|
||||
- **Migration Guide**: Instructions for migrating from SessionStorage approach in the library README
|
||||
|
||||
@@ -103,6 +103,41 @@
|
||||
- **[Storybook](https://storybook.js.org/)**
|
||||
- Isolated component development and living documentation environment.
|
||||
|
||||
## Core Libraries
|
||||
|
||||
### Navigation State Management
|
||||
|
||||
- **`@isa/core/navigation`**
|
||||
- Type-safe navigation state management through Angular Router state
|
||||
- Provides clean abstraction for passing temporary state between routes
|
||||
- Eliminates URL pollution from query parameters
|
||||
- Platform-agnostic using Angular's Location service
|
||||
- Full documentation: [libs/core/navigation/README.md](../libs/core/navigation/README.md)
|
||||
|
||||
### Logging
|
||||
|
||||
- **`@isa/core/logging`**
|
||||
- Centralized logging service for application-wide logging
|
||||
- Provides contextual information for debugging
|
||||
|
||||
### Storage
|
||||
|
||||
- **`@isa/core/storage`**
|
||||
- Storage providers for state persistence
|
||||
- Session and local storage abstractions
|
||||
|
||||
### Tabs
|
||||
|
||||
- **`@isa/core/tabs`**
|
||||
- Tab management and navigation history tracking
|
||||
- Persistent tab state across sessions
|
||||
|
||||
### Configuration
|
||||
|
||||
- **`@isa/core/config`**
|
||||
- Application configuration management
|
||||
- Environment-specific settings
|
||||
|
||||
## Domain Libraries
|
||||
|
||||
### Customer Relationship Management (CRM)
|
||||
|
||||
Reference in New Issue
Block a user