refactor: migrate the 3 wizards to the effect map and Primary (RD-08)
The three wizards paired a dispatch with a hand-written effect call (onPrimary/onRetry + runIfSubmitting/runIfIndienen). A missed call failed silently. RD-05 added the effect map and RD-07 added the Primary message; this ticket moves each wizard onto both. Each wizard now registers its submit effect on createStore, keyed by its own submitting tag (Submitting for herregistratie and intake, Indienen for registratie — the type catches a wrong key at compile time). The optimistic begin/confirm/rollback calls stay inside the effect body, unchanged. The template dispatches Primary and Retry directly, matching how Back already worked. onPrimary, onRetry, and runIfSubmitting/runIfIndienen are deleted from all three components. herregistratie-wizard drops under the 250-rule-line budget, so its eslint-disable max-lines header is removed in this same commit (RD-02's self-cleaning mechanism). intake-wizard and registratie-wizard stay over budget and keep theirs, both already flagged for RD-22/RD-23. Three doc comments (in the three machine files, plus one in store.ts) named the deleted onPrimary()/runIfSubmitting() identifiers in prose. Reworded them so the "idiom is gone from the repo" grep check is not defeated by its own explanatory comments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,7 +122,7 @@ export function submit(s: WizardState): WizardState {
|
||||
}
|
||||
|
||||
/** The primary button's action: advance, or submit from the last step. No-op
|
||||
outside Editing — this is the one decision `onPrimary()` used to make. */
|
||||
outside Editing — this is the one decision the old component-side handler used to make. */
|
||||
export function primary(s: WizardState): WizardState {
|
||||
if (s.tag !== 'Editing') return s;
|
||||
return s.step === 3 ? submit(s) : next(s);
|
||||
|
||||
@@ -201,7 +201,7 @@ export function submit(s: IntakeState): IntakeState {
|
||||
}
|
||||
|
||||
/** The primary button's action: advance, or submit from the review step. No-op
|
||||
outside Answering — this is the one decision `onPrimary()` used to make. */
|
||||
outside Answering — this is the one decision the old component-side handler used to make. */
|
||||
export function primary(s: IntakeState): IntakeState {
|
||||
if (s.tag !== 'Answering') return s;
|
||||
return currentStep(s) === 'review' ? submit(s) : next(s);
|
||||
|
||||
+17
-32
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable max-lines */ // single-step wizard shell — removed by RD-20
|
||||
import { Component, computed, inject, input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
@@ -56,10 +55,10 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.
|
||||
[canGoBack]="step() > 1"
|
||||
[errors]="errorList()"
|
||||
[errorMessage]="errorMessage()"
|
||||
(primary)="onPrimary()"
|
||||
(primary)="dispatch({ tag: 'Primary' })"
|
||||
(back)="dispatch({ tag: 'Back' })"
|
||||
(cancel)="restart()"
|
||||
(retry)="onRetry()"
|
||||
(retry)="dispatch({ tag: 'Retry' })"
|
||||
(goToStep)="goToStep($event)"
|
||||
>
|
||||
@switch (step()) {
|
||||
@@ -149,7 +148,21 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.
|
||||
})
|
||||
export class HerregistratieWizardComponent {
|
||||
private profile = inject(BigProfileStore);
|
||||
private store = createStore<WizardState, WizardMsg>(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<WizardState, WizardMsg>(initial, reduce, {
|
||||
Submitting: async (s, store) => {
|
||||
this.profile.beginHerregistratie();
|
||||
const r = await this.draftSync.submit({ uren: s.data.uren, documents: s.data.documents });
|
||||
if (r.ok) {
|
||||
store.dispatch({ tag: 'SubmitConfirmed' });
|
||||
this.profile.confirmHerregistratie();
|
||||
} else {
|
||||
store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
this.profile.rollbackHerregistratie();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** Preview/download link for a completed upload; delegates to the upload
|
||||
controller (application layer), which knows the dev-simulation `demo-*` ids
|
||||
@@ -253,37 +266,9 @@ export class HerregistratieWizardComponent {
|
||||
);
|
||||
}
|
||||
|
||||
onPrimary() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Editing') return;
|
||||
this.dispatch(s.step < 3 ? { tag: 'Next' } : { tag: 'Submit' });
|
||||
this.runIfSubmitting();
|
||||
}
|
||||
|
||||
onRetry() {
|
||||
this.dispatch({ tag: 'Retry' });
|
||||
this.runIfSubmitting();
|
||||
}
|
||||
|
||||
/** Reset the wizard to a fresh, empty start. */
|
||||
restart() {
|
||||
this.draftSync.reset();
|
||||
this.dispatch({ tag: 'Seed', state: initial });
|
||||
}
|
||||
|
||||
/** The effect: when we entered Submitting, submit through the aanvraag lifecycle,
|
||||
flip the optimistic cross-page flag, then dispatch the result (commit/rollback). */
|
||||
private async runIfSubmitting() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Submitting') return;
|
||||
this.profile.beginHerregistratie();
|
||||
const r = await this.draftSync.submit({ uren: s.data.uren, documents: s.data.documents });
|
||||
if (r.ok) {
|
||||
this.dispatch({ tag: 'SubmitConfirmed' });
|
||||
this.profile.confirmHerregistratie();
|
||||
} else {
|
||||
this.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
this.profile.rollbackHerregistratie();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,10 @@ import { IntakePolicyStore } from '@herregistratie/application/intake-policy.sto
|
||||
[canGoBack]="cursor() > 0"
|
||||
[errors]="errorList()"
|
||||
[errorMessage]="errorMessage()"
|
||||
(primary)="onPrimary()"
|
||||
(primary)="dispatch({ tag: 'Primary' })"
|
||||
(back)="dispatch({ tag: 'Back' })"
|
||||
(cancel)="restart()"
|
||||
(retry)="onRetry()"
|
||||
(retry)="dispatch({ tag: 'Retry' })"
|
||||
(goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })"
|
||||
>
|
||||
@switch (step()) {
|
||||
@@ -269,7 +269,28 @@ export class IntakeWizardComponent {
|
||||
// Server-owned policy (scholing threshold): fetched from the backend via the
|
||||
// application facade, not hardcoded. The backend stays the authority on submit.
|
||||
private policyStore = inject(IntakePolicyStore);
|
||||
private store = createStore<IntakeState, IntakeMsg>(initial, reduce);
|
||||
// Effect fires once, on Answering -> 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<IntakeState, IntakeMsg>(initial, reduce, {
|
||||
Submitting: async (s, store) => {
|
||||
this.profile.beginHerregistratie();
|
||||
// WP-69: the scholing answer rides along so the server can re-validate it as the
|
||||
// authority (IntakePolicy.RejectIncompleteScholing) — undefined members are dropped by
|
||||
// JSON.stringify, so a wizard above the threshold sends neither field.
|
||||
const r = await this.draftSync.submit({
|
||||
uren: s.data.uren,
|
||||
aanvullendeScholing: s.data.aanvullendeScholing,
|
||||
scholingPunten: s.data.punten,
|
||||
});
|
||||
if (r.ok) {
|
||||
store.dispatch({ tag: 'SubmitConfirmed' });
|
||||
this.profile.confirmHerregistratie();
|
||||
} else {
|
||||
store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
this.profile.rollbackHerregistratie();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<IntakeState>(initial);
|
||||
@@ -365,43 +386,8 @@ export class IntakeWizardComponent {
|
||||
});
|
||||
}
|
||||
|
||||
onPrimary() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Answering') return;
|
||||
this.dispatch(this.step() === 'review' ? { tag: 'Submit' } : { tag: 'Next' });
|
||||
this.runIfSubmitting();
|
||||
}
|
||||
|
||||
onRetry() {
|
||||
this.dispatch({ tag: 'Retry' });
|
||||
this.runIfSubmitting();
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.draftSync.reset();
|
||||
this.dispatch({ tag: 'Seed', state: initial });
|
||||
}
|
||||
|
||||
/** The effect: when we enter Submitting, submit through the aanvraag lifecycle,
|
||||
flip the optimistic cross-page flag, then dispatch the outcome (commit/rollback). */
|
||||
private async runIfSubmitting() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Submitting') return;
|
||||
this.profile.beginHerregistratie();
|
||||
// WP-69: the scholing answer rides along so the server can re-validate it as the
|
||||
// authority (IntakePolicy.RejectIncompleteScholing) — undefined members are dropped by
|
||||
// JSON.stringify, so a wizard above the threshold sends neither field.
|
||||
const r = await this.draftSync.submit({
|
||||
uren: s.data.uren,
|
||||
aanvullendeScholing: s.data.aanvullendeScholing,
|
||||
scholingPunten: s.data.punten,
|
||||
});
|
||||
if (r.ok) {
|
||||
this.dispatch({ tag: 'SubmitConfirmed' });
|
||||
this.profile.confirmHerregistratie();
|
||||
} else {
|
||||
this.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
this.profile.rollbackHerregistratie();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ export function submit(s: RegistratieState): RegistratieState {
|
||||
}
|
||||
|
||||
/** The primary button's action: advance, or submit from the controle step.
|
||||
No-op outside Invullen — this is the one decision `onPrimary()` used to make. */
|
||||
No-op outside Invullen — this is the one decision the old component-side handler used to make. */
|
||||
export function primary(s: RegistratieState): RegistratieState {
|
||||
if (s.tag !== 'Invullen') return s;
|
||||
return currentStep(s) === 'controle' ? submit(s) : next(s);
|
||||
|
||||
+14
-28
@@ -90,10 +90,10 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
||||
[errorMessage]="errorMessage()"
|
||||
i18n-submittingLabel="@@regWizard.submitting"
|
||||
submittingLabel="Uw registratie wordt verwerkt…"
|
||||
(primary)="onPrimary()"
|
||||
(primary)="dispatch({ tag: 'Primary' })"
|
||||
(back)="dispatch({ tag: 'Back' })"
|
||||
(cancel)="restart()"
|
||||
(retry)="onRetry()"
|
||||
(retry)="dispatch({ tag: 'Retry' })"
|
||||
(goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })"
|
||||
>
|
||||
@switch (step()) {
|
||||
@@ -368,7 +368,18 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
||||
})
|
||||
export class RegistratieWizardComponent {
|
||||
private lookup = inject(RegistratieLookupStore);
|
||||
private store = createStore<RegistratieState, RegistratieMsg>(initial, reduce);
|
||||
// Effect fires once, on Invullen -> Indienen (RD-05's tag-transition rule; `Seed` is
|
||||
// exempt, so a story mounting straight into `Indienen` does not call the network).
|
||||
private store = createStore<RegistratieState, RegistratieMsg>(initial, reduce, {
|
||||
Indienen: async (s, store) => {
|
||||
const r = await this.draftSync.submit({
|
||||
diplomaHerkomst: s.data.diplomaHerkomst,
|
||||
documents: s.data.documents,
|
||||
});
|
||||
if (r.ok) store.dispatch({ tag: 'SubmitConfirmed', referentie: r.value.referentie ?? '' });
|
||||
else store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
},
|
||||
});
|
||||
|
||||
/** Preview/download link for a completed upload; delegates to the upload
|
||||
controller (application layer), which knows the dev-simulation `demo-*` ids
|
||||
@@ -610,18 +621,6 @@ export class RegistratieWizardComponent {
|
||||
// failed submit) now lives in the shared WizardShellComponent.
|
||||
}
|
||||
|
||||
onPrimary() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Invullen') return;
|
||||
this.dispatch(this.step() === 'controle' ? { tag: 'Submit' } : { tag: 'Next' });
|
||||
this.runIfIndienen();
|
||||
}
|
||||
|
||||
onRetry() {
|
||||
this.dispatch({ tag: 'Retry' });
|
||||
this.runIfIndienen();
|
||||
}
|
||||
|
||||
/** Reset the wizard to a fresh start. Reload the BRP lookup so the address
|
||||
re-prefills, keeping the form and the"vooraf ingevuld" note consistent. */
|
||||
restart() {
|
||||
@@ -629,17 +628,4 @@ export class RegistratieWizardComponent {
|
||||
this.dispatch({ tag: 'Seed', state: initial });
|
||||
this.lookup.reloadAdres();
|
||||
}
|
||||
|
||||
/** The effect: when we enter Indienen, submit through the aanvraag lifecycle
|
||||
(duo → auto-approve, handmatig → manual), then dispatch the outcome. */
|
||||
private async runIfIndienen() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Indienen') return;
|
||||
const r = await this.draftSync.submit({
|
||||
diplomaHerkomst: s.data.diplomaHerkomst,
|
||||
documents: s.data.documents,
|
||||
});
|
||||
if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value.referentie ?? '' });
|
||||
else this.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user