feat(po): prompt to save as draft when leaving with unsaved changes #116

Merged
shad0w merged 1 commit from feat/po-draft-prompt into master 2026-06-24 01:14:20 +00:00
Owner

Closes #18.

Problem

Navigating away from a PO create or edit screen with unsaved changes silently lost the in-progress data.

Solution

A reusable <UnsavedChangesGuard> (components/po/unsaved-changes-guard.tsx) wired into new-po-form and edit-po-form. It arms once the form is dirty (any input/change on the form, plus the React-state editors — line items, terms, files, accounting code) and guards both navigation paths:

  • Hard navigations (refresh, tab close, external link) → the browser's native "Leave site?" prompt (beforeunload). Browsers can't render custom buttons here, so save-as-draft isn't offered on this path.
  • In-app navigations (sidebar / header / any internal <a>) → a capture-phase click interceptor opens a modal offering:
    • Save as draft — runs the form's existing draft save (which redirects to the PO).
    • Discard changes — continues to the link's destination.
    • Stay on page — dismisses.

dirty is reset before a successful submit's redirect, so saving never trips the prompt.

Scope / limitations

  • The SPA back button (popstate) is left to the beforeunload prompt only — robust popstate interception in the App Router is fragile, and the dominant "leave" paths (sidebar/header clicks, refresh, close, typing a URL) are covered.
  • The manager inline-edit panel on /approvals/[id] is out of scope — it saves in place via router.refresh() and has no draft concept.

Verification

  • 7 new unit tests for the guard (intercept-when-dirty, no-op-when-clean, external links pass through, Stay / Discard / Save actions, beforeunload arming).
  • pnpm test296 passed
  • pnpm exec tsc --noEmit → clean

🤖 Generated with Claude Code

Closes #18. ## Problem Navigating away from a PO **create** or **edit** screen with unsaved changes silently lost the in-progress data. ## Solution A reusable `<UnsavedChangesGuard>` (`components/po/unsaved-changes-guard.tsx`) wired into `new-po-form` and `edit-po-form`. It arms once the form is **dirty** (any input/change on the form, plus the React-state editors — line items, terms, files, accounting code) and guards both navigation paths: - **Hard navigations** (refresh, tab close, external link) → the browser's native "Leave site?" prompt (`beforeunload`). Browsers can't render custom buttons here, so save-as-draft isn't offered on this path. - **In-app navigations** (sidebar / header / any internal `<a>`) → a capture-phase click interceptor opens a modal offering: - **Save as draft** — runs the form's existing draft save (which redirects to the PO). - **Discard changes** — continues to the link's destination. - **Stay on page** — dismisses. `dirty` is reset before a successful submit's redirect, so saving never trips the prompt. ## Scope / limitations - The SPA **back button** (popstate) is left to the `beforeunload` prompt only — robust popstate interception in the App Router is fragile, and the dominant "leave" paths (sidebar/header clicks, refresh, close, typing a URL) are covered. - The **manager inline-edit panel** on `/approvals/[id]` is out of scope — it saves in place via `router.refresh()` and has no draft concept. ## Verification - 7 new unit tests for the guard (intercept-when-dirty, no-op-when-clean, external links pass through, Stay / Discard / Save actions, `beforeunload` arming). - `pnpm test` → **296 passed** - `pnpm exec tsc --noEmit` → clean 🤖 Generated with [Claude Code](https://claude.com/claude-code)
shad0w added 1 commit 2026-06-24 01:07:57 +00:00
feat(po): prompt to save as draft when leaving with unsaved changes
All checks were successful
PR checks / checks (pull_request) Successful in 44s
PR checks / integration (pull_request) Successful in 32s
7d4ad6a9b8
Closes #18. Navigating away from a PO create/edit screen with unsaved
changes could silently lose in-progress work. The forms now track a dirty
flag and guard both navigation paths:

- Hard navigations (refresh / tab close / external link) → the browser's
  native "Leave site?" prompt via beforeunload.
- In-app navigations (sidebar / header / any internal link) → a capture-phase
  click interceptor opens a modal offering Save as draft / Discard changes /
  Stay on page. Save as draft runs the form's existing draft save (which
  redirects to the PO); Discard continues to the intended destination.

The guard (components/po/unsaved-changes-guard.tsx) is reusable and wired into
both new-po-form and edit-po-form. dirty is cleared before a successful submit
so saving never trips the prompt. SPA back-button (popstate) is left to
beforeunload only; the manager inline-edit panel is out of scope (saves in
place, no draft concept).

Tests: 7 new unit cases for the guard (intercept-when-dirty, no-op-when-clean,
external links pass through, Stay/Discard/Save actions, beforeunload arming).
Unit suite 296 green; tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shad0w merged commit 56497a0d20 into master 2026-06-24 01:14:20 +00:00
Sign in to join this conversation.
No description provided.