Merged PR 2042: fix(navigation): prevent autoTriggerContinueFn from persisting across navigat...

fix(navigation): prevent autoTriggerContinueFn from persisting across navigations

The autoTriggerContinueFn flag was remaining in navigation context after
being read, causing incorrect auto-triggering on subsequent page visits.

Changes:
- Add patchContext() method to NavigationContextService and NavigationStateService
  for partial context updates without full replacement
- Update details-main-view.component to use patchContext() to clear the flag
  immediately after reading, while preserving returnUrl
- Add comprehensive JSDoc and README documentation for patchContext()
- Include structured logging for patch operations

The new patchContext() method provides a cleaner API for updating specific
context properties without manually preserving all other properties.

Closes #5500

Related work items: #5500
This commit is contained in:
Lorenz Hilpert
2025-11-21 13:45:40 +00:00
committed by Nino Righi
parent 7a6a2dc49d
commit bf87df6273
4 changed files with 182 additions and 0 deletions

View File

@@ -146,6 +146,68 @@ navState.preserveContext(
---
#### `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.
@@ -710,6 +772,7 @@ export const NAVIGATION_CONTEXT_METADATA_KEY = 'navigation-contexts';
| 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 |

View File

@@ -143,6 +143,75 @@ export class NavigationContextService {
}));
}
/**
* 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<T extends NavigationContextData>(
partialData: Partial<T>,
customScope?: string,
): Promise<void> {
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<T>(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.
*

View File

@@ -112,6 +112,50 @@ export class NavigationStateService {
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<T extends NavigationContextData>(
partialState: Partial<T>,
customScope?: string,
): Promise<void> {
await this.#contextService.patchContext(partialState, customScope);
}
/**
* Restore preserved navigation state.
*