# 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 `