Files
atomic-design-poc/src/app/app.config.ts
T
ehoandClaude Opus 4.8 645fad088e feat(fp): flush pending autosave before navigation/unload
Close the last-mile autosave gap: a debounced edit made in the final <600ms
before leaving a page was lost — the wizard draft-sync timer is cleared on
destroy without flushing, and root stores keep an armed timer the teardown
ignores.

New `shared/application/pending-saves.ts`: a root `PendingSaves` registry every
autosave owner joins (BriefStore, OrgTemplateStore, each createDraftSync). Two
seams flush through it — `flushPendingGuard` (CanDeactivate, on the five
autosave routes) awaits the pending write before an in-app route change; a
`beforeunload` handler (provideUnloadFlush) fires it best-effort and raises the
browser's native unsaved-changes prompt. ponytail: the HTTP seam is Angular
HttpClient (no keepalive/sendBeacon), so a hard-close flush can't be guaranteed
— hence the prompt; upgrade path noted in a comment. Each owner now nulls its
timer handle on fire so `hasPendingSave()` is accurate, and exposes
`flushPending()`.

Verified live against the running stack: navigating away 91ms after a keystroke
(well inside the debounce) fires one PUT /brief before the route changes; a
dirty reload raises the prompt, a clean reload does not. FE lint / check:tokens
/ 299 tests (+11) / build / build-storybook green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:29:09 +02:00

58 lines
2.6 KiB
TypeScript

import {
ApplicationConfig,
LOCALE_ID,
isDevMode,
provideBrowserGlobalErrorListeners,
} from '@angular/core';
import { provideRouter, withInMemoryScrolling, withViewTransitions } from '@angular/router';
import type { ActivatedRouteSnapshot } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { registerLocaleData } from '@angular/common';
import localeNl from '@angular/common/locales/nl';
import { routes } from './app.routes';
import { scenarioInterceptor } from '@shared/infrastructure/scenario.interceptor';
import { roleInterceptor } from '@shared/infrastructure/role.interceptor';
import { provideApiClient } from '@shared/infrastructure/api-client.provider';
import { SESSION_PORT } from '@shared/application/session.port';
import { SessionStore } from '@auth/application/session.store';
import { provideRouteFocus } from '@shared/layout/route-focus';
import { provideUnloadFlush } from '@shared/application/pending-saves';
registerLocaleData(localeNl);
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(
routes,
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
// Cross-fade page-to-page navigations only. A silent same-route nav — e.g.
// draft-sync stamping `?aanvraag=<id>` into the URL mid-wizard — must NOT
// animate: for the transition's duration Firefox's `::view-transition`
// overlay swallows pointer events (Chrome sets pointer-events:none, so it
// doesn't), which loses a click landing on it and makes the wizard's "next"
// button need a second click. Skip the transition when the route is unchanged.
withViewTransitions({
onViewTransitionCreated: ({ transition, from, to }) => {
// `from`/`to` are the ROOT snapshots (the shared shell), so descend to the
// leaf before comparing — otherwise every navigation looks "same route".
const leaf = (r: ActivatedRouteSnapshot) => {
while (r.firstChild) r = r.firstChild;
return r;
};
if (leaf(from).routeConfig === leaf(to).routeConfig) transition.skipTransition();
},
}),
),
// Dev-only: the ?scenario= toggle must never reach a production build, where
// a query param could otherwise force errors on the live app.
provideHttpClient(withInterceptors(isDevMode() ? [scenarioInterceptor, roleInterceptor] : [])),
provideApiClient(),
{ provide: SESSION_PORT, useExisting: SessionStore },
{ provide: LOCALE_ID, useValue: 'nl' },
provideRouteFocus(),
provideUnloadFlush(),
],
};