Frontend UX: object form robustness — double-submit, no dirty guard, inconsistent partial-failure recovery #46
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.