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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { ShellComponent } from '@shared/layout/shell/shell.component';
|
||||
import { authGuard, capabilityGuard } from '@auth/auth.guard';
|
||||
import { flushPendingGuard } from '@shared/application/pending-saves';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -32,23 +33,28 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'registreren',
|
||||
canActivate: [authGuard],
|
||||
// Autosave wizard: flush the pending debounced draft before leaving (pending-saves.ts).
|
||||
canDeactivate: [flushPendingGuard],
|
||||
loadComponent: () =>
|
||||
import('@registratie/ui/registratie.page').then((m) => m.RegistratiePage),
|
||||
},
|
||||
{
|
||||
path: 'herregistratie',
|
||||
canActivate: [authGuard],
|
||||
canDeactivate: [flushPendingGuard],
|
||||
loadComponent: () =>
|
||||
import('@herregistratie/ui/herregistratie.page').then((m) => m.HerregistratiePage),
|
||||
},
|
||||
{
|
||||
path: 'intake',
|
||||
canActivate: [authGuard],
|
||||
canDeactivate: [flushPendingGuard],
|
||||
loadComponent: () => import('@herregistratie/ui/intake.page').then((m) => m.IntakePage),
|
||||
},
|
||||
{
|
||||
path: 'brief',
|
||||
canActivate: [authGuard],
|
||||
canDeactivate: [flushPendingGuard],
|
||||
loadComponent: () => import('@brief/ui/brief.page').then((m) => m.BriefPage),
|
||||
},
|
||||
{
|
||||
@@ -57,6 +63,7 @@ export const routes: Routes = [
|
||||
// unless GET /me resolved `orgtemplate:edit` (Admin role). Backend re-enforces
|
||||
// via the OrgAdmin gate — the guard just avoids loading a page that would 403.
|
||||
canActivate: [capabilityGuard('orgtemplate:edit')],
|
||||
canDeactivate: [flushPendingGuard],
|
||||
loadComponent: () =>
|
||||
import('@brief/ui/org-template.page').then((m) => m.OrgTemplatePage),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user