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>
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
Submittedonly (line 34);Failedfalls into the@elseat line 36, which renders the editable form. editing = whenTag(state(), 'Editing')(line 112) isundefinedinFailed, sobesluit()andtoelichting()(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 inFailedit is enabled. - Clicking dispatches
Submit, andbesluit.machine.ts:75isif (s.tag !== 'Editing') return s— a no-op.
change-request-form.component.ts — the fields are frozen.
- Same
@if Submitted / @else formshape (lines 55, 68), same always-enabled submit button (line 118), same no-opSubmit(change-request.machine.ts:62). - Different in one way:
telefoon()(lines 158-163) does readFailed.data, with the comment "so the user sees what they sent". So the value stays on screen — butSetFieldisEditing-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 itsSeedruleapps/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.tsapps/ssp/src/app/registratie/domain/change-request.machine.tslibs/shared/src/layout/wizard-shell/wizard-shell.component.ts:134— the retry button and the$localizeid to reuse- Both
*.stories.tsfor the two components
Decisions (pre-made, don't relitigate)
-
Give
Failedits 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 } -
Show what was sent, read from
Failed.data.change-request-form.telefoon()already does exactly this and says why in its comment.besluit-formcopies that pattern rather than inventing one. The user must be able to see what they are retrying. -
The Retry button dispatches
{ tag: 'Retry' }. No machine change is needed — both reducers already handle it, and RD-05's effect map fires onFailed → Submittingbecause that is a tag transition.onRetryneeds no effect call of its own. -
Reuse the
$localizeid@@wizard.opnieuwProberenwith byte-identical source textOpnieuw proberen. Verified present with an English target in bothapps/ssp/src/locale/messages.en.xlf:2429andapps/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. -
Migrate both components to RD-05's effect map. Delete
runIfSubmittingfrom both; register the body as{ Submitting: (s, store) => … }oncreateStore. The narrowed state arrives as argument one, so theconst s = this.state(); if (s.tag !== 'Submitting') return;preamble goes away.onSubmitbecomes a singledispatch. -
Do not add a "go back and edit after a failure" path. Neither machine has a
Failed → Editingmessage, 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. -
Do not change the
Resetmessage or theSeedcontract. The stories mount states viaSeed, and RD-05 exemptsSeedfrom firing effects precisely so they do not perform real HTTP.
Files
apps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.component.tsapps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.stories.tsapps/ssp/src/app/registratie/ui/change-request-form/change-request-form.component.tsapps/ssp/src/app/registratie/ui/change-request-form/change-request-form.stories.ts
No machine file changes. No xlf changes.
Steps
- In
besluit-form, add theFailedbranch per decisions 1-3, reading the besluit and toelichting fromFailed.data. - Replace
runIfSubmittingwith an effect-map entry per decision 5; reduceonSubmitto one dispatch. - Repeat both steps for
change-request-form. - Add a
Failedstory to each component's*.stories.tsso the new branch has a rendered, axe-checked state. Seed it directly, per decision 7. - Update this ticket's
Status:todoneand the README's RD-06 row todone. - 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:
- The submitted values are visible — not blank.
- No enabled control dispatches
Submit. - The Retry button is present, and clicking it leaves
Failed(it entersSubmitting).
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
Retrythe same data orResetto empty. AFailed → Editingtransition that mapsdataback to adraftwould 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
SubmittingviaSeedin stories that use a realprovideHttpClient()with no request mocking (besluit-form.stories.ts:33,change-request-form.stories.ts:34). RD-05'sSeedexemption is what stops those firing real network calls. If a story flips toFailedon load, orstorybook-a11ygoes 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.mdxdrift if you add or rename any spec. Runnpm run gen:behaviour-specin the same commit if you do.besluit-formhas 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.