Stu Mason
Stu Mason

Activity

StuMason/cleanconnect
TidyLinker.com
TypeScript
Pull Request Opened

PR #159 opened: fix: surface form validation errors generically across the app

What Adele saw

Edit a job, click Save, page stays on /edit, no visible change. From her POV: "it hasn't saved." Screen recording confirmed the POST returns 422 but no inline error appears.

Root cause (Edit job)

Edit.tsx rendered InputError per-field. Of 16 form fields, 8 had no error display wired up (parking, access_instructions, budget_max, num_bathrooms, has_pets, schedule_type, budget_type, property_category). A 422 on any of those was silent — and a future field added without the InputError wiring would silently break the same way.

But this isn't unique to Edit job — every form in the app relies on the dev remembering to add per-field error JSX. So the fix needs to be generic.

The fix

Core mechanism

  • resources/js/hooks/use-form-error-focus.ts — given the errors map from Inertia's useForm or <Form> render prop:

    1. Marks each erroring field with aria-invalid="true". The design-system Input/Textarea/Select already paint that red via the aria-invalid:border-destructive Tailwind variant — red border for free.
    2. Injects an inline <p> error message after each erroring field, matching the styling of hand-written InputError elements.
    3. Skips injection if an existing InputError is already rendering for that field (detected via a data-slot="input-error" marker added to the shared InputError component) — no double-rendering.
    4. Scrolls a summary ref into view so feedback isn't missed on long forms.
    5. Cleans up markers + injected messages on the next submit.
  • resources/js/components/form-error-surface.tsx — a drop-in <FormErrorSurface errors={errors} /> component that wires the hook + renders an AlertError summary banner listing every error. Renders nothing when errors are empty.

Rollout

Applied to every form in the codebase (23 files total):

  • Edit job (the original repro)
  • Post-job wizard (all 6 steps)
  • Cleaner onboarding (steps 1-4) + verification upload
  • Auth: register (already covered separately), login + 2FA + confirm-password + forgot-password + verify-email
  • Settings: addresses, two-factor
  • Admin: support reply, verifications rejection
  • Other: contact form, message thread reply, support widget, client job review

Result

Any 422 anywhere in the app surfaces:

  • Summary banner at top of the form (always)
  • Red border on each erroring field (where name/id matches the validation key — true for the vast majority of fields)
  • Inline message after each field (auto-injected unless the form already has an InputError for that field)

Adding new fields requires no error-handling JSX — the surface catches everything.

Test plan

  • Pest browser test (EditJob422SurfaceTest) — triggers parking > 200 chars, asserts the summary banner + inline error text both appear, asserts DB unchanged
  • Visual local repro on Edit job: parking > 200 + access_instructions > 500 → both fields red-bordered, both inline errors injected, summary banner with both at top (screenshot)
  • Visual local repro on settings/addresses: submit empty → 3 errors in summary banner
  • React re-render survival: typed in another field after error rendered — injected message persists
  • Cleanup: fixed the field + re-saved → injected message removed, redirect to show page
  • Pint/Prettier/ESLint/TypeScript clean
+209
additions
-18
deletions
26
files changed