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>
This commit is contained in:
@@ -6,6 +6,8 @@ import { AlertComponent } from '@shared/ui/alert/alert.component';
|
|||||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||||
import { RadioGroupComponent, RadioOption } from '@shared/ui/radio-group/radio-group.component';
|
import { RadioGroupComponent, RadioOption } from '@shared/ui/radio-group/radio-group.component';
|
||||||
|
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||||
|
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||||
import { createStore } from '@shared/application/store';
|
import { createStore } from '@shared/application/store';
|
||||||
import { whenTag } from '@shared/kernel/fp';
|
import { whenTag } from '@shared/kernel/fp';
|
||||||
import { BesluitState, BesluitMsg, initial, reduce } from '@behandeling/domain/besluit.machine';
|
import { BesluitState, BesluitMsg, initial, reduce } from '@behandeling/domain/besluit.machine';
|
||||||
@@ -29,10 +31,35 @@ import { createSubmitBesluit } from '@behandeling/application/submit-besluit';
|
|||||||
FormFieldComponent,
|
FormFieldComponent,
|
||||||
TextInputComponent,
|
TextInputComponent,
|
||||||
RadioGroupComponent,
|
RadioGroupComponent,
|
||||||
|
DataBlockComponent,
|
||||||
|
DataRowComponent,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
@if (state().tag === 'Submitted') {
|
@if (state().tag === 'Submitted') {
|
||||||
<app-alert type="ok" i18n="@@besluit.success">Het besluit is vastgelegd.</app-alert>
|
<app-alert type="ok" i18n="@@besluit.success">Het besluit is vastgelegd.</app-alert>
|
||||||
|
} @else if (state().tag === 'Failed') {
|
||||||
|
<app-heading [level]="2" i18n="@@besluit.heading">Besluit vastleggen</app-heading>
|
||||||
|
|
||||||
|
<app-alert type="error"
|
||||||
|
><ng-container i18n="@@besluit.failed">Het vastleggen is niet gelukt:</ng-container>
|
||||||
|
{{ failedError() }}</app-alert
|
||||||
|
>
|
||||||
|
|
||||||
|
<app-data-block class="app-section" [ariaLabel]="besluitLabelText">
|
||||||
|
<div app-data-row [key]="besluitLabelText" [value]="besluitOptieLabel()"></div>
|
||||||
|
@if (toelichting()) {
|
||||||
|
<div app-data-row [key]="toelichtingLabelText" [value]="toelichting()"></div>
|
||||||
|
}
|
||||||
|
</app-data-block>
|
||||||
|
|
||||||
|
<div class="app-section">
|
||||||
|
<app-button
|
||||||
|
variant="secondary"
|
||||||
|
(click)="dispatch({ tag: 'Retry' })"
|
||||||
|
i18n="@@wizard.opnieuwProberen"
|
||||||
|
>Opnieuw proberen</app-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<app-heading [level]="2" i18n="@@besluit.heading">Besluit vastleggen</app-heading>
|
<app-heading [level]="2" i18n="@@besluit.heading">Besluit vastleggen</app-heading>
|
||||||
|
|
||||||
@@ -70,13 +97,6 @@ import { createSubmitBesluit } from '@behandeling/application/submit-besluit';
|
|||||||
/>
|
/>
|
||||||
</app-form-field>
|
</app-form-field>
|
||||||
|
|
||||||
@if (failedError()) {
|
|
||||||
<app-alert type="error"
|
|
||||||
><ng-container i18n="@@besluit.failed">Het vastleggen is niet gelukt:</ng-container>
|
|
||||||
{{ failedError() }}</app-alert
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">
|
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">
|
||||||
{{ state().tag === 'Submitting' ? submitBezigLabel : submitLabel }}
|
{{ state().tag === 'Submitting' ? submitBezigLabel : submitLabel }}
|
||||||
</app-button>
|
</app-button>
|
||||||
@@ -86,7 +106,19 @@ import { createSubmitBesluit } from '@behandeling/application/submit-besluit';
|
|||||||
})
|
})
|
||||||
export class BesluitFormComponent {
|
export class BesluitFormComponent {
|
||||||
private submit = createSubmitBesluit();
|
private submit = createSubmitBesluit();
|
||||||
private store = createStore<BesluitState, BesluitMsg>(initial, reduce);
|
// Effect fires once, on Editing -> Submitting (RD-05's tag-transition rule; `Seed` is
|
||||||
|
// exempt, so a story mounting straight into `Submitting` does not call the network).
|
||||||
|
private store = createStore<BesluitState, BesluitMsg>(initial, reduce, {
|
||||||
|
Submitting: async (s, store) => {
|
||||||
|
const r = await this.submit(this.id(), s.data);
|
||||||
|
if (r.ok) {
|
||||||
|
store.dispatch({ tag: 'SubmitConfirmed' });
|
||||||
|
this.decided.emit();
|
||||||
|
} else {
|
||||||
|
store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
id = input.required<string>();
|
id = input.required<string>();
|
||||||
decided = output<void>();
|
decided = output<void>();
|
||||||
@@ -109,12 +141,33 @@ export class BesluitFormComponent {
|
|||||||
protected readonly submitLabel = $localize`:@@besluit.submit:Besluit vastleggen`;
|
protected readonly submitLabel = $localize`:@@besluit.submit:Besluit vastleggen`;
|
||||||
protected readonly submitBezigLabel = $localize`:@@besluit.submitBezig:Bezig met vastleggen…`;
|
protected readonly submitBezigLabel = $localize`:@@besluit.submitBezig:Bezig met vastleggen…`;
|
||||||
|
|
||||||
|
// Same ids as the form-field labels above, reused for the Failed data-block's row
|
||||||
|
// keys (the pattern change-request-form already uses for its read-only BRP rows).
|
||||||
|
protected readonly besluitLabelText = $localize`:@@besluit.besluitLabel:Besluit`;
|
||||||
|
protected readonly toelichtingLabelText = $localize`:@@besluit.toelichtingLabel:Toelichting`;
|
||||||
|
|
||||||
private editing = computed(() => whenTag(this.state(), 'Editing'));
|
private editing = computed(() => whenTag(this.state(), 'Editing'));
|
||||||
protected errors = computed(() => this.editing()?.errors ?? {});
|
protected errors = computed(() => this.editing()?.errors ?? {});
|
||||||
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
|
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
|
||||||
|
|
||||||
protected besluit = computed(() => this.editing()?.draft.besluit ?? '');
|
/** The value shown in the field — the live draft while editing, the parsed value
|
||||||
protected toelichting = computed(() => this.editing()?.draft.toelichting ?? '');
|
while submitting/failed (so the user sees what they sent, same idiom as
|
||||||
|
change-request-form.telefoon()). */
|
||||||
|
protected besluit = computed(() => {
|
||||||
|
const s = this.state();
|
||||||
|
if (s.tag === 'Editing') return s.draft.besluit;
|
||||||
|
if (s.tag === 'Submitting' || s.tag === 'Failed') return s.data.besluit;
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
protected toelichting = computed(() => {
|
||||||
|
const s = this.state();
|
||||||
|
if (s.tag === 'Editing') return s.draft.toelichting;
|
||||||
|
if (s.tag === 'Submitting' || s.tag === 'Failed') return s.data.toelichting ?? '';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
protected besluitOptieLabel = computed(
|
||||||
|
() => this.BESLUIT_OPTIONS.find((o) => o.value === this.besluit())?.label ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() }));
|
queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() }));
|
||||||
@@ -122,19 +175,5 @@ export class BesluitFormComponent {
|
|||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.dispatch({ tag: 'Submit' });
|
this.dispatch({ tag: 'Submit' });
|
||||||
this.runIfSubmitting();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Effect: when we entered Submitting, call the command, then dispatch the outcome. */
|
|
||||||
private async runIfSubmitting() {
|
|
||||||
const s = this.state();
|
|
||||||
if (s.tag !== 'Submitting') return;
|
|
||||||
const r = await this.submit(this.id(), s.data);
|
|
||||||
if (r.ok) {
|
|
||||||
this.dispatch({ tag: 'SubmitConfirmed' });
|
|
||||||
this.decided.emit();
|
|
||||||
} else {
|
|
||||||
this.dispatch({ tag: 'SubmitFailed', error: r.error });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-18
@@ -65,6 +65,26 @@ import { createSubmitChangeRequest } from '@registratie/application/submit-chang
|
|||||||
>Nieuwe wijziging doorgeven</app-button
|
>Nieuwe wijziging doorgeven</app-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
} @else if (state().tag === 'Failed') {
|
||||||
|
<app-heading [level]="2" i18n="@@changeRequest.heading">Contactgegevens wijzigen</app-heading>
|
||||||
|
|
||||||
|
<app-alert type="error"
|
||||||
|
><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container>
|
||||||
|
{{ failedError() }}</app-alert
|
||||||
|
>
|
||||||
|
|
||||||
|
<app-data-block class="app-section" [ariaLabel]="telefoonLabelText">
|
||||||
|
<div app-data-row [key]="telefoonLabelText" [value]="telefoon()"></div>
|
||||||
|
</app-data-block>
|
||||||
|
|
||||||
|
<div class="app-section">
|
||||||
|
<app-button
|
||||||
|
variant="secondary"
|
||||||
|
(click)="dispatch({ tag: 'Retry' })"
|
||||||
|
i18n="@@wizard.opnieuwProberen"
|
||||||
|
>Opnieuw proberen</app-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<app-heading [level]="2" i18n="@@changeRequest.heading">Contactgegevens wijzigen</app-heading>
|
<app-heading [level]="2" i18n="@@changeRequest.heading">Contactgegevens wijzigen</app-heading>
|
||||||
|
|
||||||
@@ -108,13 +128,6 @@ import { createSubmitChangeRequest } from '@registratie/application/submit-chang
|
|||||||
</app-form-field>
|
</app-form-field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@if (failedError()) {
|
|
||||||
<app-alert type="error"
|
|
||||||
><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container>
|
|
||||||
{{ failedError() }}</app-alert
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">
|
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">
|
||||||
{{ state().tag === 'Submitting' ? submitBezigLabel : submitLabel }}
|
{{ state().tag === 'Submitting' ? submitBezigLabel : submitLabel }}
|
||||||
</app-button>
|
</app-button>
|
||||||
@@ -127,7 +140,15 @@ export class ChangeRequestFormComponent {
|
|||||||
// adapter); the UI holds only this bound command. Field initializer = injection
|
// adapter); the UI holds only this bound command. Field initializer = injection
|
||||||
// context, like createStore below.
|
// context, like createStore below.
|
||||||
private submit = createSubmitChangeRequest();
|
private submit = createSubmitChangeRequest();
|
||||||
private store = createStore<ChangeRequestState, ChangeRequestMsg>(initial, reduce);
|
// Effect fires once, on Editing -> Submitting (RD-05's tag-transition rule; `Seed` is
|
||||||
|
// exempt, so a story mounting straight into `Submitting` does not call the network).
|
||||||
|
private store = createStore<ChangeRequestState, ChangeRequestMsg>(initial, reduce, {
|
||||||
|
Submitting: async (s, store) => {
|
||||||
|
const r = await this.submit(s.data);
|
||||||
|
if (r.ok) store.dispatch({ tag: 'SubmitConfirmed', referentie: r.value });
|
||||||
|
else store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/** BRP address, shown read-only. Undefined until the profile loads. */
|
/** BRP address, shown read-only. Undefined until the profile loads. */
|
||||||
brpAdres = input<Adres | undefined>(undefined);
|
brpAdres = input<Adres | undefined>(undefined);
|
||||||
@@ -148,6 +169,10 @@ export class ChangeRequestFormComponent {
|
|||||||
protected readonly postcodeLabel = $localize`:@@address.postcode:Postcode`;
|
protected readonly postcodeLabel = $localize`:@@address.postcode:Postcode`;
|
||||||
protected readonly woonplaatsLabel = $localize`:@@address.woonplaats:Woonplaats`;
|
protected readonly woonplaatsLabel = $localize`:@@address.woonplaats:Woonplaats`;
|
||||||
|
|
||||||
|
// Same id as the telefoon form-field's label above, reused for the Failed
|
||||||
|
// data-block's row key.
|
||||||
|
protected readonly telefoonLabelText = $localize`:@@changeRequest.telefoonLabel:Telefoonnummer`;
|
||||||
|
|
||||||
private editing = computed(() => whenTag(this.state(), 'Editing'));
|
private editing = computed(() => whenTag(this.state(), 'Editing'));
|
||||||
protected errors = computed(() => this.editing()?.errors ?? {});
|
protected errors = computed(() => this.editing()?.errors ?? {});
|
||||||
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
|
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
|
||||||
@@ -168,15 +193,5 @@ export class ChangeRequestFormComponent {
|
|||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.dispatch({ tag: 'Submit' });
|
this.dispatch({ tag: 'Submit' });
|
||||||
this.runIfSubmitting();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Effect: when we entered Submitting, call the command, then dispatch the outcome. */
|
|
||||||
private async runIfSubmitting() {
|
|
||||||
const s = this.state();
|
|
||||||
if (s.tag !== 'Submitting') return;
|
|
||||||
const r = await this.submit(s.data);
|
|
||||||
if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value });
|
|
||||||
else this.dispatch({ tag: 'SubmitFailed', error: r.error });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# 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 s` — **a 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
@@ -100,7 +100,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
|||||||
| RD-03 | `overzicht` context: page + 2 nav sections, boundary edge, admin-links token | 02 | yes | done |
|
| RD-03 | `overzicht` context: page + 2 nav sections, boundary edge, admin-links token | 02 | yes | done |
|
||||||
| RD-04 | Story titles to `Domein/<Context>/<Name>`; add the missing stories | 03 | yes | todo |
|
| RD-04 | Story titles to `Domein/<Context>/<Name>`; add the missing stories | 03 | yes | todo |
|
||||||
| RD-05 | `createStore` gains the effect map + specs | 02 | | done |
|
| RD-05 | `createStore` gains the effect map + specs | 02 | | done |
|
||||||
| RD-06 | **Bug fix:** 2 single-step forms to the effect map + retry affordance | 05 | yes | todo |
|
| RD-06 | **Bug fix:** 2 single-step forms to the effect map + retry affordance | 05 | yes | done |
|
||||||
| RD-07 | Add `Primary` to the 3 wizard machines + specs | 05 | | todo |
|
| RD-07 | Add `Primary` to the 3 wizard machines + specs | 05 | | todo |
|
||||||
| RD-08 | Migrate the 3 wizards to the effect map + `Primary` | 07 | yes | todo |
|
| RD-08 | Migrate the 3 wizards to the effect map + `Primary` | 07 | yes | todo |
|
||||||
| RD-09 | **Docs + generator:** `form-machine.hbs`, ARCHITECTURE, fp-tea, skill | 08 | | todo |
|
| RD-09 | **Docs + generator:** `form-machine.hbs`, ARCHITECTURE, fp-tea, skill | 08 | | todo |
|
||||||
|
|||||||
Reference in New Issue
Block a user