Editor's "Back to Workspace" Link navigates silently with no unsaved-changes warning — beforeunload (#161) only covers tab close, not in-app Link navigation #184

Closed
opened 2026-10-03 23:20:47 +00:00 by Mark · 1 comment
Owner

Finding

src/components/editor/EditorHeader.tsx:188-204 renders the "Back to Workspace" button as a React Router <Link to="/workspace">. The Link has no onClick confirmation. If the user has unsaved edits (state.isDirty is true per the #161 wiring), clicking the Link navigates away without prompting.

The beforeunload handler registered in Editor.tsx (per #161) only fires for window.beforeunload — i.e., the user closing the tab, refreshing, or closing the window. React Router-driven navigation within the SPA does NOT fire beforeunload. The comment at the handler explicitly notes: "React Router-driven navigations within the SPA don't fire beforeunload, so this only covers refresh / tab-close / window-close, which matches the finding's scope (sessionStorage round-trips still carry the edit on in-app navigation)."

The current behavior is: the user edits, clicks "Back to Workspace", the editor unmounts, the sessionStorage snapshot remains. The user can re-open the editor and find their work, but the explicit "you have unsaved changes" prompt never appears.

Where

src/components/editor/EditorHeader.tsx:188-204 — the <Link to="/workspace"> element

Suggested fix shape

Add an onClick handler to the Link that confirms navigation when isDirty:

<Link
  to="/workspace"
  onClick={(e) => {
    if (isDirty) {
      const ok = window.confirm(
        "You have unsaved changes. Leave the editor anyway?

" +
        "Your edits are still in session storage and will be restored if you return, " +
        "but the design won't be in your Gallery until you click Save."
      );
      if (!ok) e.preventDefault();
    }
  }}
  ...
>

This requires plumbing isDirty from Editor.tsx into EditorHeader as a new prop (alongside canSave, onSave, etc).

For the keyboard shortcut equivalent, the same confirmation would need to fire on the navbar's nav clicks and the back button. A shared useUnsavedChangesGuard(isDirty) hook would centralize this.

Severity

Medium. The user can lose context (not work) by clicking away from the editor. The sessionStorage round-trip is a safety net but the user doesn't know about it.

Discovered by

Gap analysis (2026-10-02) of editor unsaved-changes handling.

  • #161 (closed) — Editor beforeunload handler. Covers tab close; doesn't cover in-app Link navigation. The two are complements, not duplicates.
## Finding `src/components/editor/EditorHeader.tsx:188-204` renders the "Back to Workspace" button as a React Router `<Link to="/workspace">`. The Link has no `onClick` confirmation. If the user has unsaved edits (state.isDirty is true per the #161 wiring), clicking the Link navigates away without prompting. The `beforeunload` handler registered in `Editor.tsx` (per #161) only fires for `window.beforeunload` — i.e., the user closing the tab, refreshing, or closing the window. React Router-driven navigation within the SPA does NOT fire `beforeunload`. The comment at the handler explicitly notes: "React Router-driven navigations within the SPA don't fire `beforeunload`, so this only covers refresh / tab-close / window-close, which matches the finding's scope (sessionStorage round-trips still carry the edit on in-app navigation)." The current behavior is: the user edits, clicks "Back to Workspace", the editor unmounts, the sessionStorage snapshot remains. The user can re-open the editor and find their work, but the explicit "you have unsaved changes" prompt never appears. ## Where `src/components/editor/EditorHeader.tsx:188-204` — the `<Link to="/workspace">` element ## Suggested fix shape Add an `onClick` handler to the Link that confirms navigation when isDirty: ```tsx <Link to="/workspace" onClick={(e) => { if (isDirty) { const ok = window.confirm( "You have unsaved changes. Leave the editor anyway? " + "Your edits are still in session storage and will be restored if you return, " + "but the design won't be in your Gallery until you click Save." ); if (!ok) e.preventDefault(); } }} ... > ``` This requires plumbing `isDirty` from `Editor.tsx` into `EditorHeader` as a new prop (alongside `canSave`, `onSave`, etc). For the keyboard shortcut equivalent, the same confirmation would need to fire on the navbar's nav clicks and the back button. A shared `useUnsavedChangesGuard(isDirty)` hook would centralize this. ## Severity Medium. The user can lose context (not work) by clicking away from the editor. The sessionStorage round-trip is a safety net but the user doesn't know about it. ## Discovered by Gap analysis (2026-10-02) of editor unsaved-changes handling. ## Related - #161 (closed) — Editor beforeunload handler. Covers tab close; doesn't cover in-app Link navigation. The two are complements, not duplicates.
Author
Owner

Closed via commit 56033cf on main.

Fix:

  • src/components/editor/EditorHeader.tsx — added optional isDirty prop; the Back-to-Workspace <Link> now calls window.confirm(...) when isDirty is true, and e.preventDefault() if the user cancels.
  • src/pages/Editor.tsx — plumbed isDirty (already in scope from #161) into EditorHeader.
  • src/components/editor/EditorHeader.test.tsx — 5 new tests covering: link present, no prompt when not dirty, prompt message content when dirty, cancel branch, accept branch.

19/19 EditorHeader tests pass; 9/9 Editor tests pass; tsc -b clean; eslint clean on the touched files.

Scope kept tight to the suggested fix shape; the broader useUnsavedChangesGuard hook is intentionally deferred — there is no second consumer yet (keyboard shortcut, navbar nav, back button), so the abstraction would be premature.

Closed via commit 56033cf on main. Fix: - src/components/editor/EditorHeader.tsx — added optional `isDirty` prop; the Back-to-Workspace `<Link>` now calls `window.confirm(...)` when isDirty is true, and `e.preventDefault()` if the user cancels. - src/pages/Editor.tsx — plumbed `isDirty` (already in scope from #161) into EditorHeader. - src/components/editor/EditorHeader.test.tsx — 5 new tests covering: link present, no prompt when not dirty, prompt message content when dirty, cancel branch, accept branch. 19/19 EditorHeader tests pass; 9/9 Editor tests pass; tsc -b clean; eslint clean on the touched files. Scope kept tight to the suggested fix shape; the broader `useUnsavedChangesGuard` hook is intentionally deferred — there is no second consumer yet (keyboard shortcut, navbar nav, back button), so the abstraction would be premature.
Mark closed this issue 2026-10-03 23:24:34 +00:00
Sign in to join this conversation.
No description provided.