diff --git a/docs/superpowers/specs/2026-06-07-object-form-robustness-design.md b/docs/superpowers/specs/2026-06-07-object-form-robustness-design.md new file mode 100644 index 0000000..7041c2b --- /dev/null +++ b/docs/superpowers/specs/2026-06-07-object-form-robustness-design.md @@ -0,0 +1,164 @@ +# Object Form Robustness — Design + +**Date:** 2026-06-07 +**Status:** Approved (brainstorming) — ready for implementation planning. +**Issue:** #46. + +## Context + +The object create/edit form has data-integrity gaps that compound in long daily cataloguing sessions +(High severity): +- **Submit never disabled during save.** `object-form.tsx` submit button has no `disabled`/pending. + Both create (`object-new-page.tsx`) and edit (`object-edit-form.tsx`) do **two sequential awaited + mutations** (create/update, then `setFields`) with no feedback → a second click can duplicate-create + or race the two-step write. `publish-control.tsx` is the model (disables on `isPending`). +- **No unsaved-changes guard.** No `isDirty`/`useBlocker`/`beforeunload`; Cancel/nav loses work + silently. +- **Two-phase write recovers inconsistently.** Create teleports to the edit page on `setFields` + failure; edit stays put with a banner. The partial write (core saved, field rejected) reads as + nothing happened. +- **Thin validation.** Core errors always show `form.required`; the server `code` + (`type_mismatch`/`unresolved`/`unknown`) on `FieldRejection` is discarded; no `number_of_objects >= 1`. +- No "Save & create another" / keyboard submit for batch entry. + +**Facts established:** the form uses **react-hook-form** (so `formState.isDirty` + `isSubmitting` are +available; `isSubmitting` stays true across both awaited mutations — the unified pending signal). +`ObjectForm` props: `mode`, `defaults`, `onSubmit`, `onCancel`, `formError`, `fieldErrorKey`; the +422→highlight path uses `form.setError`. `FieldRejection` (`queries.ts`) carries `field` + `code`. +The router is `` + `` (component router) in `app.tsx`; **`useBlocker` requires +a data router** (`createBrowserRouter` + `RouterProvider`). `renderApp` wraps `ui` in `` +with no `` (tests supply their own when they need params). + +### Decisions (from brainstorming) +1. **Migrate to a data router** (the long-term-correct foundation; unblocks `useBlocker` and React + Router v7 data APIs). Done via `createRoutesFromElements` to keep the exact route tree (minimal + diff), isolated as the first task, with the test harness moved to `createMemoryRouter`. +2. **Unify create partial-failure** to the edit route with a distinct "Object created, but a field was + rejected" banner (single recovery surface). +3. **Include batch entry** ("Save & create another" + Cmd/Ctrl+Enter). + +## Components + +### 0. Data-router migration (foundation — isolated) +- **`app.tsx`:** replace `` with a module-level + `const router = createBrowserRouter(createRoutesFromElements(<>…the existing tree…))` + and `export function App() { return ; }`. The route JSX (login, + `RequireAuth`→`AppShell` nest with all nested routes, lazy `Suspense` wrappers, splat) moves + **verbatim** into `createRoutesFromElements`. No behavior/path change. +- **`main.tsx`:** unchanged provider stack (QueryClient → Config → Toast) now wraps `` which is + ``. +- **`test/render.tsx`:** `renderApp(ui, { route })` switches to + `createMemoryRouter([{ path: "*", element: ui }], { initialEntries: [route] })` rendered via + `` (inside `QueryClientProvider`). Behavior-preserving: `ui` renders at `route`, + and tests that supply their own `` still nest correctly; now a data-router context exists so + `useBlocker` works. **Acceptance: the entire existing suite stays green** before any feature is built + on top. + +### 1. Submit pending + keyboard + batch (`object-form.tsx`) +- Submit `