Files
atomic-design-poc/docs/project/readable-codebase/RD-06-fix-unrecoverable-submit-failure.md
T
ehoandClaude Sonnet 5 0c6fd37ed8 fix: give Failed its own branch in the two single-step forms (RD-06)
Before this fix, a failed submit on the besluit-form or the
change-request-form left the user stuck. Both templates rendered `Failed`
through the same `@else` branch as the editable form. In besluit-form the
fields read from `Editing` only, so they went blank. In change-request-form
the fields still showed the sent value, but `SetField` only applies to
`Editing`, so typing did nothing. In both forms the submit button stayed
enabled, but `Submit` is a no-op outside `Editing`. The only escape was a
page reload.

After this fix, `Failed` gets its own template branch: an error message, a
read-only summary of what was sent (an `app-data-block`, reused from the
existing BRP-address pattern), and a "Opnieuw proberen" button that
dispatches `Retry`. Both machines already handle `Retry` (`Failed ->
Submitting` with the preserved data), so no machine change was needed.

Both components also move to `createStore`'s effect map (RD-05): the
`Submitting` handler replaces the hand-called `runIfSubmitting`, so
`onSubmit` is now a single `dispatch`. `runIfSubmitting`/`runIfIndienen`
now remain only in the three wizards, migrated later by RD-08.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 16:55:20 +02:00

8.0 KiB

RD-06 — Fix the unrecoverable submit failure in the two single-step forms

Status: done Source: PLAN.md 1a (the two bugs)

Why

This is a bug fix, not a refactor. Two forms have an unrecoverable dead end reachable from any failed submit. Both machines already support recovery; only the UI affordance is missing.

Verified mechanics, per component:

besluit-form.component.ts — the fields are wiped.

  • The template branches on Submitted only (line 34); Failed falls into the @else at line 36, which renders the editable form.
  • editing = whenTag(state(), 'Editing') (line 112) is undefined in Failed, so besluit() and toelichting() (lines 116-117) both return ''. The user's decision disappears from the screen.
  • The submit button's only disable condition is Submitting (line 80), so in Failed it is enabled.
  • Clicking dispatches Submit, and besluit.machine.ts:75 is if (s.tag !== 'Editing') return sa no-op.

change-request-form.component.ts — the fields are frozen.

  • Same @if Submitted / @else form shape (lines 55, 68), same always-enabled submit button (line 118), same no-op Submit (change-request.machine.ts:62).
  • Different in one way: telefoon() (lines 158-163) does read Failed.data, with the comment "so the user sees what they sent". So the value stays on screen — but SetField is Editing-only (change-request.machine.ts:60), so typing does nothing.

Either way the only escape is a page reload.

The machines are already correct. Failed carries data: Valid (besluit.machine.ts:34, change-request.machine.ts:32), and Retry maps Failed → Submitting with that preserved data (besluit.machine.ts:80, change-request.machine.ts:67). Neither UI ever dispatches it.

Read first

  • libs/shared/src/application/store.ts — the effect map RD-05 added, and its Seed rule
  • apps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.component.ts (140 lines)
  • apps/behandelportal/src/app/behandeling/domain/besluit.machine.ts (92 lines)
  • apps/ssp/src/app/registratie/ui/change-request-form/change-request-form.component.ts
  • apps/ssp/src/app/registratie/domain/change-request.machine.ts
  • libs/shared/src/layout/wizard-shell/wizard-shell.component.ts:134 — the retry button and the $localize id to reuse
  • Both *.stories.ts for the two components

Decisions (pre-made, don't relitigate)

  1. Give Failed its own template branch. Do not let it fall through to the editable form. One change fixes both symptoms: nothing renders wiped fields, and no dead submit button exists. Shape:

    @if (state().tag === 'Submitted') { … }
    @else if (state().tag === 'Failed') { error + what was sent + Retry }
    @else { the form }
    
  2. Show what was sent, read from Failed.data. change-request-form.telefoon() already does exactly this and says why in its comment. besluit-form copies that pattern rather than inventing one. The user must be able to see what they are retrying.

  3. The Retry button dispatches { tag: 'Retry' }. No machine change is needed — both reducers already handle it, and RD-05's effect map fires on Failed → Submitting because that is a tag transition. onRetry needs no effect call of its own.

  4. Reuse the $localize id @@wizard.opnieuwProberen with byte-identical source text Opnieuw proberen. Verified present with an English target in both apps/ssp/src/locale/messages.en.xlf:2429 and apps/behandelportal/src/locale/messages.en.xlf:2373 (<target datatype="html">Try again</target>). No new xlf target is needed. A different source text under the same id fails extraction, so do not reword it.

  5. Migrate both components to RD-05's effect map. Delete runIfSubmitting from both; register the body as { Submitting: (s, store) => … } on createStore. The narrowed state arrives as argument one, so the const s = this.state(); if (s.tag !== 'Submitting') return; preamble goes away. onSubmit becomes a single dispatch.

  6. Do not add a "go back and edit after a failure" path. Neither machine has a Failed → Editing message, and adding one is scope creep for a bug fix. Retry recovers the dead end, which is what this ticket is for. Record the edit-after-failure gap as a follow-up in the Out of scope section.

  7. Do not change the Reset message or the Seed contract. The stories mount states via Seed, and RD-05 exempts Seed from firing effects precisely so they do not perform real HTTP.

Files

  • apps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.component.ts
  • apps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.stories.ts
  • apps/ssp/src/app/registratie/ui/change-request-form/change-request-form.component.ts
  • apps/ssp/src/app/registratie/ui/change-request-form/change-request-form.stories.ts

No machine file changes. No xlf changes.

Steps

  1. In besluit-form, add the Failed branch per decisions 1-3, reading the besluit and toelichting from Failed.data.
  2. Replace runIfSubmitting with an effect-map entry per decision 5; reduce onSubmit to one dispatch.
  3. Repeat both steps for change-request-form.
  4. Add a Failed story to each component's *.stories.ts so the new branch has a rendered, axe-checked state. Seed it directly, per decision 7.
  5. Update this ticket's Status: to done and the README's RD-06 row to done.
  6. Commit all of it together.

Acceptance criteria

npm run ci        # exits 0
npm run ci --full # exits 0 — required: this ticket adds stories

Then prove the dead end is gone. There is no automated coverage for this, so verify by seeding the Failed state in Storybook (npm run storybook and npm run storybook:behandelportal) and checking all three, for both components:

  1. The submitted values are visible — not blank.
  2. No enabled control dispatches Submit.
  3. The Retry button is present, and clicking it leaves Failed (it enters Submitting).

Prove the method is gone from both forms rather than renamed. Baseline today is 5 files (3 wizards + these 2 forms); note the registratie wizard spells it runIfIndienen, so both names must be matched:

grep -rl "runIfSubmitting\|runIfIndienen" apps/ssp apps/behandelportal | sort
# MUST be exactly these 3 (the wizards, migrated later by RD-08):
#   apps/ssp/.../herregistratie-wizard/herregistratie-wizard.component.ts
#   apps/ssp/.../intake-wizard/intake-wizard.component.ts
#   apps/ssp/.../registratie-wizard/registratie-wizard.component.ts

Verification

npm run ci --full. --full is mandatory here: this ticket adds stories, and only build-storybook plus the axe run exercise them.

Out of scope

  • The 3 wizards. That is RD-08, after RD-07 adds Primary.
  • Editing after a failure. Both machines can only Retry the same data or Reset to empty. A Failed → Editing transition that maps data back to a draft would be a genuine UX improvement and needs a new message plus a reducer spec. Recorded here as a follow-up; not part of this fix.
  • WizardStatus/WizardPhase. That is RD-10.

Risks

  • The Storybook trap. Both components mount Submitting via Seed in stories that use a real provideHttpClient() with no request mocking (besluit-form.stories.ts:33, change-request-form.stories.ts:34). RD-05's Seed exemption is what stops those firing real network calls. If a story flips to Failed on load, or storybook-a11y goes red, the exemption is not working — fix that, do not delete the story.
  • Do not reword the retry label. Same id, same source text, or extraction fails (decision 4).
  • behaviour-spec.mdx drift if you add or rename any spec. Run npm run gen:behaviour-spec in the same commit if you do.
  • besluit-form has no *.spec.ts. Do not add a component TestBed spec for this — the house tests UI through Storybook (CLAUDE.md decision 5). The machines already have specs, and this ticket changes no machine.