Severity: High.From a frontend UX audit. These compound into real data-loss / duplicate-record risk in long daily sessions.
Problems
Submit button never disabled during save.web/src/objects/object-form.tsx:180-186 — no disabled, no pending label. Both create (object-new-page.tsx:21-42) and edit (object-edit-form.tsx:51-66) do two sequential awaited mutations (create/update, then setFields) with no isPending feedback → a second click can fire a duplicate create or race the two-step write. (publish-control.tsx is the model: it disables on isPending.)
No unsaved-changes guard. No useBlocker/beforeunload/isDirty anywhere; onCancel navigates away immediately. 20 minutes of work is lost silently on an accidental Cancel/nav.
Two-phase write recovers inconsistently. On create, if setFields fails the user is teleported to the edit page; on edit, it stays put with a banner. Same failure class, two models. The partial write (core saved, fields rejected) is invisible as "your object was created."
Thin validation messages. Core errors always show t("form.required") regardless of cause; no range/date validation; the server code (type_mismatch/unresolved/unknown) carried on FieldRejection (queries.ts:13-18) is discarded. (The 422→field-highlight path itself, object-form.tsx:81-88, is good.)
Flexible fields aren't grouped/ordered (see the detail issue); no "Save & create another" or keyboard submit for batch entry.
Suggested fixes
Disable submit while create/update.isPending || setFields.isPending, show "Saving…".
Track formState.isDirty; guard route changes with useBlocker and confirm on Cancel when dirty.
Unify partial-failure recovery (stay on form, banner + field highlight in both create & edit); message the create case as "Object created, but a field was rejected."
Echo the server code in messages; validate number_of_objects >= 1; add "Save & create another" + Cmd/Ctrl+Enter submit.
Source: frontend UX/design audit, 2026-06-06.
**Severity: High.** _From a frontend UX audit. These compound into real data-loss / duplicate-record risk in long daily sessions._
## Problems
- **Submit button never disabled during save.** `web/src/objects/object-form.tsx:180-186` — no `disabled`, no pending label. Both create (`object-new-page.tsx:21-42`) and edit (`object-edit-form.tsx:51-66`) do **two sequential awaited mutations** (create/update, then `setFields`) with no `isPending` feedback → a second click can fire a duplicate create or race the two-step write. (`publish-control.tsx` is the model: it disables on `isPending`.)
- **No unsaved-changes guard.** No `useBlocker`/`beforeunload`/`isDirty` anywhere; `onCancel` navigates away immediately. 20 minutes of work is lost silently on an accidental Cancel/nav.
- **Two-phase write recovers inconsistently.** On create, if `setFields` fails the user is *teleported to the edit page*; on edit, it stays put with a banner. Same failure class, two models. The partial write (core saved, fields rejected) is invisible as "your object was created."
- **Thin validation messages.** Core errors always show `t("form.required")` regardless of cause; no range/date validation; the server `code` (`type_mismatch`/`unresolved`/`unknown`) carried on `FieldRejection` (`queries.ts:13-18`) is discarded. (The 422→field-highlight path itself, `object-form.tsx:81-88`, is good.)
- Flexible fields aren't grouped/ordered (see the detail issue); no "Save & create another" or keyboard submit for batch entry.
## Suggested fixes
- Disable submit while `create/update.isPending || setFields.isPending`, show "Saving…".
- Track `formState.isDirty`; guard route changes with `useBlocker` and confirm on Cancel when dirty.
- Unify partial-failure recovery (stay on form, banner + field highlight in both create & edit); message the create case as "Object created, but a field was rejected."
- Echo the server `code` in messages; validate `number_of_objects >= 1`; add "Save & create another" + Cmd/Ctrl+Enter submit.
_Source: frontend UX/design audit, 2026-06-06._
Done — merged to main (28e444c). All the compounding data-loss/duplicate-record risks are closed.
Foundation: migrated the app to a React Router data router (createBrowserRouter, route tree verbatim) so useBlocker is available. This required upgrading Vitest 3→4 (the old jsdom/Node-26 combo clobbered AbortSignal, which the data router's per-navigation Request needs) — done as an isolated first step, @storybook/addon-vitest@10 already supported Vitest 4, swapped @vitest/browser→@vitest/browser-playwright. Dev-only; no runtime dependency added.
Form robustness:
No double-submit: the form's onSubmit is now awaited by react-hook-form, so isSubmitting is real — submit/Cancel disable and the button reads "Saving…" across both the create/update + setFields writes. (Double-click fires the create exactly once — tested.)
Unsaved-changes guard:useBlocker(isDirty && !isSubmitting) blocks in-app navigation when dirty (Cancel and sidebar links flow through one Discard/Keep-editing dialog) + a beforeunload for reload/close. Save-driven navigation is never falsely prompted (it happens while isSubmitting).
Unified partial-failure recovery: when the core object saves but a field is rejected, both create and edit land on the edit form; the create case shows a distinct banner "Object created, but a field was rejected — fix it below" + highlights the field.
Better validation: field-rejection messages now reflect the server code (type_mismatch/unresolved/unknown); number_of_objects ≥ 1 is validated client-side; core errors show type-specific messages.
Batch entry:"Save & create another" (resets the form, refocuses) + Cmd/Ctrl+Enter to submit.
Gate green: typecheck, lint, 203 tests, build, check:size (214.4 KB gz), check:colors; en/sv parity; no codename; no eslint-disable/any.
Follow-ups (out of scope): flexible-field grouping/ordering (#45/detail); per-field server validation rules (#11); turning core-field 422s into per-field rejections server-side; autosave/draft persistence.
Done — merged to `main` (`28e444c`). All the compounding data-loss/duplicate-record risks are closed.
**Foundation:** migrated the app to a **React Router data router** (`createBrowserRouter`, route tree verbatim) so `useBlocker` is available. This required upgrading **Vitest 3→4** (the old jsdom/Node-26 combo clobbered `AbortSignal`, which the data router's per-navigation `Request` needs) — done as an isolated first step, `@storybook/addon-vitest@10` already supported Vitest 4, swapped `@vitest/browser`→`@vitest/browser-playwright`. Dev-only; no runtime dependency added.
**Form robustness:**
- **No double-submit:** the form's `onSubmit` is now awaited by react-hook-form, so `isSubmitting` is real — submit/Cancel disable and the button reads "Saving…" across both the create/update + setFields writes. (Double-click fires the create exactly once — tested.)
- **Unsaved-changes guard:** `useBlocker(isDirty && !isSubmitting)` blocks in-app navigation when dirty (Cancel and sidebar links flow through one Discard/Keep-editing dialog) + a `beforeunload` for reload/close. Save-driven navigation is never falsely prompted (it happens while `isSubmitting`).
- **Unified partial-failure recovery:** when the core object saves but a field is rejected, both create and edit land on the edit form; the create case shows a distinct banner **"Object created, but a field was rejected — fix it below"** + highlights the field.
- **Better validation:** field-rejection messages now reflect the server `code` (`type_mismatch`/`unresolved`/`unknown`); `number_of_objects ≥ 1` is validated client-side; core errors show type-specific messages.
- **Batch entry:** **"Save & create another"** (resets the form, refocuses) + **Cmd/Ctrl+Enter** to submit.
Gate green: typecheck, lint, **203 tests**, build, check:size (214.4 KB gz), check:colors; en/sv parity; no codename; no `eslint-disable`/`any`.
Follow-ups (out of scope): flexible-field grouping/ordering (#45/detail); per-field server validation rules (#11); turning core-field 422s into per-field rejections server-side; autosave/draft persistence.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Severity: High. From a frontend UX audit. These compound into real data-loss / duplicate-record risk in long daily sessions.
Problems
web/src/objects/object-form.tsx:180-186— nodisabled, no pending label. Both create (object-new-page.tsx:21-42) and edit (object-edit-form.tsx:51-66) do two sequential awaited mutations (create/update, thensetFields) with noisPendingfeedback → a second click can fire a duplicate create or race the two-step write. (publish-control.tsxis the model: it disables onisPending.)useBlocker/beforeunload/isDirtyanywhere;onCancelnavigates away immediately. 20 minutes of work is lost silently on an accidental Cancel/nav.setFieldsfails the user is teleported to the edit page; on edit, it stays put with a banner. Same failure class, two models. The partial write (core saved, fields rejected) is invisible as "your object was created."t("form.required")regardless of cause; no range/date validation; the servercode(type_mismatch/unresolved/unknown) carried onFieldRejection(queries.ts:13-18) is discarded. (The 422→field-highlight path itself,object-form.tsx:81-88, is good.)Suggested fixes
create/update.isPending || setFields.isPending, show "Saving…".formState.isDirty; guard route changes withuseBlockerand confirm on Cancel when dirty.codein messages; validatenumber_of_objects >= 1; add "Save & create another" + Cmd/Ctrl+Enter submit.Source: frontend UX/design audit, 2026-06-06.
Done — merged to
main(28e444c). All the compounding data-loss/duplicate-record risks are closed.Foundation: migrated the app to a React Router data router (
createBrowserRouter, route tree verbatim) souseBlockeris available. This required upgrading Vitest 3→4 (the old jsdom/Node-26 combo clobberedAbortSignal, which the data router's per-navigationRequestneeds) — done as an isolated first step,@storybook/addon-vitest@10already supported Vitest 4, swapped@vitest/browser→@vitest/browser-playwright. Dev-only; no runtime dependency added.Form robustness:
onSubmitis now awaited by react-hook-form, soisSubmittingis real — submit/Cancel disable and the button reads "Saving…" across both the create/update + setFields writes. (Double-click fires the create exactly once — tested.)useBlocker(isDirty && !isSubmitting)blocks in-app navigation when dirty (Cancel and sidebar links flow through one Discard/Keep-editing dialog) + abeforeunloadfor reload/close. Save-driven navigation is never falsely prompted (it happens whileisSubmitting).code(type_mismatch/unresolved/unknown);number_of_objects ≥ 1is validated client-side; core errors show type-specific messages.Gate green: typecheck, lint, 203 tests, build, check:size (214.4 KB gz), check:colors; en/sv parity; no codename; no
eslint-disable/any.Follow-ups (out of scope): flexible-field grouping/ordering (#45/detail); per-field server validation rules (#11); turning core-field 422s into per-field rejections server-side; autosave/draft persistence.