[Frontend] Warn before an in-app navigation discards an unsaved edit (needs a data router) #464
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: LOW. Split from #410, which is now closed — this issue owns the remaining behaviour outright rather than being a blocker on it.
What is missing
The session summary and transcript editors warn before a tab close or reload, and nothing else. Navigating within the app — clicking through to another session, say — still discards an unsaved edit silently.
#410 delivered the rest of the dirty-state work in PR #463: the shared
useUnsavedChangesWarninghook, the Beat Planner's confirm-before-discard on its own session dropdown, and the Shelf's 1.5s debounced auto-save for tonight-notes. This is the one behaviour that could not be built at the time.Why it could not be built
App.jsxmounts<BrowserRouter>— the declarative router. React Router'suseBlockercallsuseDataRouterContext("useBlocker"), whichinvariant-throws when there is no data router:So no amount of work inside a component can block in-app navigation while the app routes this way.
beforeunloadcovers tab close and reload; an internal route change never fires it. The fix is a routing-layer migration, which is why it was split out.Scope
Migrate
App.jsxfrom<BrowserRouter>+<Routes>tocreateBrowserRouter+<RouterProvider>, then useuseBlockerinuseUnsavedChangesWarning(or a sibling hook) for the summary and transcript editors, keepingbeforeunloadfor the tab-close case a data router does not help with.Worth knowing on the way:
errorElementbecomes available, which may subsume some current per-page error handling.useNavigate,useParams,useLocationand<Link>are all unaffected.MemoryRouter(verified 2026-09-01 — an earlier version of this issue said "~40", which was wrong). They keep working, sinceMemoryRouterstays supported; only a test that wants to exercise blocking needscreateMemoryRouter.beforeunloadhandler returns, so the tab-close prompt is generic and always will be.useBlockercan show custom UI, but only for in-app navigation. That is a browser constraint, not scope.Timing — why v4.4.0
Two corrections to the rationale this issue previously carried, because it would have misled whoever picked this up:
BrowserRouter→createBrowserRouteris a mechanism change; if v4.4.0 rewrites the route list, it rewrites it inside whatever mechanism is current. Migrating early would be inherited, not redone. v4.4.0's own description does not actually mention routing.Pull it forward if in-app navigation loss actually bites someone. The case people reported —
Ctrl+Wwith focus in the Shelf notes during live play — is already covered, and that field auto-saves regardless.Acceptance criteria
App.jsxusescreateBrowserRouter/RouterProvider.beforeunloadretained for tab close and reload.createMemoryRouter, and is confirmed to fail without the blocker.[Frontend] Migrate to a data router so in-app navigation can be blockedto [Frontend] Warn before an in-app navigation discards an unsaved edit (needs a data router)Picking this up as the first engineering lane of v4.4.0. It is a mechanism change with no dependence on the design direction, and the rebuild inherits whichever router mechanism is current, so it goes first. Scope exactly as the body:
createBrowserRouter+RouterProvider,useBlockerin the unsaved-changes hook for the summary and transcript editors,beforeunloadkept, acreateMemoryRoutertest that fails without the blocker.Done in PR #537 (auto-merge armed).
App.jsxnow mountscreateBrowserRouter+RouterProvider(fromreact-router/dom), with the JSX route tree kept throughcreateRoutesFromElementsand exported for tests. Every route, redirect, the singleAuthGuardand the catch-all are unchanged;AuthProviderandCampaignProvidermoved into a pathless root layout route because the latter calls router hooks.useUnsavedChangesPrompt(a sibling of the unload-only hook, which is unchanged) confirms before a pathname change while an edit is dirty, registers through the router's public blocker API, and no-ops without a data router so the ~30MemoryRouterpage tests keep working.?tab=and#recordingnever prompt. Applied to the summary and transcript editors.createMemoryRoutertest was seen to fail with the blocker removed (2 failed / 9 passed) and passes with it.ErrorBoundary. The root route now carries anerrorElementrendering the same fallback, with a test.Found on the way and filed as #536: the Shelf's tonight-notes debounce is cancelled on unmount without flushing, which loses up to 1.5 s of typing today; a prompt would surface that loss, not prevent it.