Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc2a3c348b | ||
|
|
551cabce5e | ||
|
|
b6bd7eea5a | ||
|
|
d4c5a9450a | ||
|
|
57fc72f254 | ||
|
|
43dc3210cd |
@@ -109,6 +109,29 @@ module.exports = function buildConfig(contextAllowed, appName, tsConfigFileName)
|
||||
},
|
||||
},
|
||||
|
||||
// --- Atomic ladder within libs/shared/src/ui (folder = layer, CLAUDE.md decision 2) ---
|
||||
{
|
||||
name: 'atoms-compose-nothing-above',
|
||||
comment: 'An atom composes nothing above it — no molecule or organism. See CLAUDE.md §2.',
|
||||
severity: 'error',
|
||||
from: { path: '^libs/shared/src/ui/atoms/' },
|
||||
to: { path: '^libs/shared/src/ui/(molecules|organisms)/' },
|
||||
},
|
||||
{
|
||||
name: 'molecules-below-organisms',
|
||||
comment: 'A molecule composes nothing above it — no organism. See CLAUDE.md §2.',
|
||||
severity: 'error',
|
||||
from: { path: '^libs/shared/src/ui/molecules/' },
|
||||
to: { path: '^libs/shared/src/ui/organisms/' },
|
||||
},
|
||||
{
|
||||
name: 'design-system-not-layout',
|
||||
comment: 'The design system (ui/) does not depend on layout/ templates. See CLAUDE.md §2.',
|
||||
severity: 'error',
|
||||
from: { path: '^libs/shared/src/ui/' },
|
||||
to: { path: '^libs/shared/src/layout/' },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'no-testing-in-production',
|
||||
comment:
|
||||
|
||||
@@ -210,10 +210,10 @@ each app has its **own Storybook instance** (`.storybook-ssp/`, `.storybook-beha
|
||||
WP-67 — a single merged tsconfig can't resolve both apps' `@auth/*` at once), each globbing
|
||||
its own app's stories plus both shared libraries'. **Story titles mirror the sidebar's
|
||||
Design System/Domein split** (see `libs/shared/docs/layers.mdx`): a `libs/shared/ui|layout`
|
||||
or `libs/beheer/ui` component is titled `Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`;
|
||||
a component in an app context's `ui/` is titled `Domein/<Context>/<Name>` — full stop,
|
||||
regardless of which atomic layer it is (a context organism doesn't get its own
|
||||
`Organisms/` bucket).
|
||||
component is titled `Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`;
|
||||
a component in an app context's `ui/`, or in `libs/beheer/ui`, is titled
|
||||
`Domein/<Context>/<Name>` — full stop, regardless of which atomic layer it is (a context
|
||||
organism doesn't get its own `Organisms/` bucket).
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
/**
|
||||
* Organism: employee-SSO-style mock login (ADR-C-004). No real auth — and,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Component, computed, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BeoordelingStore } from '@behandeling/application/beoordeling.store';
|
||||
import { detailRows } from '@behandeling/domain/beoordeling-view';
|
||||
import { BeoordelingDocumentenComponent } from '@behandeling/ui/beoordeling-documenten/beoordeling-documenten.component';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.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 { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import {
|
||||
RadioGroupComponent,
|
||||
RadioOption,
|
||||
} from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
import { BesluitState, BesluitMsg, initial, reduce } from '@behandeling/domain/besluit.machine';
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
|
||||
import { werkvoorraadRow } from '@behandeling/domain/werkvoorraad-item-view';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { WerkvoorraadStore } from '@behandeling/application/werkvoorraad.store';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
/** Organism: DigiD-style mock login. No real auth — just composes atoms/molecules. */
|
||||
@Component({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { LoginFormComponent } from '@auth/ui/login-form/login-form.component';
|
||||
import { SessionStore } from '@auth/application/session.store';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, ElementRef, computed, input, output, viewChild } from '@angular/core';
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
|
||||
import { PlaceholderOption } from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/atoms/masked-value/masked-value.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { StepperComponent } from '@shared/ui/molecules/stepper/stepper.component';
|
||||
import { Besluit, Brief, CaseContext, LibraryPassage } from '@brief/domain/brief';
|
||||
import { besluitGuidance, inferSelection } from '@brief/domain/besluit';
|
||||
import { Diagnostic } from '@brief/domain/placeholders';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component, computed, input, linkedSignal, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CheckboxComponent } from '@shared/ui/checkbox/checkbox.component';
|
||||
import { RadioGroupComponent, RadioOption } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { CheckboxComponent } from '@shared/ui/atoms/checkbox/checkbox.component';
|
||||
import {
|
||||
RadioGroupComponent,
|
||||
RadioOption,
|
||||
} from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { Besluit, LibraryPassage } from '@brief/domain/brief';
|
||||
import { redenenFor } from '@brief/domain/besluit';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BriefStore } from '@brief/application/brief.store';
|
||||
import { LetterComposerComponent } from '@brief/ui/letter-composer/letter-composer.component';
|
||||
import { BehandelSchermComponent } from '@brief/ui/behandel-scherm/behandel-scherm.component';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { Diagnostic } from '@brief/domain/placeholders';
|
||||
|
||||
/** Molecule: lists all letter diagnostics grouped by severity. Errors block
|
||||
|
||||
@@ -3,8 +3,8 @@ import { RichTextBlock } from '@shared/kernel/rich-text';
|
||||
import {
|
||||
RichTextEditorComponent,
|
||||
PlaceholderOption,
|
||||
} from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
} from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { LetterBlock } from '@brief/domain/brief';
|
||||
|
||||
/** Molecule: one block in a section — its editor plus provenance + block controls.
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
import { Paragraph } from '@shared/kernel/rich-text';
|
||||
import { Brief, LetterBlock } from '@brief/domain/brief';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';
|
||||
import { PlaceholderChipComponent } from '@shared/ui/atoms/placeholder-chip/placeholder-chip.component';
|
||||
import { RichTextNode } from '@shared/kernel/rich-text';
|
||||
import { Diagnostic, PlaceholderDef } from '@brief/domain/placeholders';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output, signal } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/atoms/status-badge/status-badge.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { OrgTemplate } from '@brief/domain/org-template';
|
||||
import { Diagnostic } from '@brief/domain/placeholders';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { PlaceholderOption } from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { BriefMsg } from '@brief/domain/brief.machine';
|
||||
import { LetterSectionComponent } from '@brief/ui/letter-section/letter-section.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { RichTextBlock } from '@shared/kernel/rich-text';
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { PlaceholderOption } from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { LetterSection } from '@brief/domain/brief';
|
||||
import { BriefMsg } from '@brief/domain/brief.machine';
|
||||
import { LetterBlockComponent } from '@brief/ui/letter-block/letter-block.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FileInputComponent } from '@shared/ui/upload/file-input/file-input.component';
|
||||
import { SingleUploadComponent } from '@shared/ui/upload/single-upload/single-upload.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { FileInputComponent } from '@shared/ui/atoms/upload/file-input/file-input.component';
|
||||
import { SingleUploadComponent } from '@shared/ui/molecules/upload/single-upload/single-upload.component';
|
||||
import { UploadState } from '@shared/domain/upload.machine';
|
||||
|
||||
const LOGO_CATEGORY = 'org-logo';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { UploadState } from '@shared/domain/upload.machine';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { SAMPLE_LETTER_BRIEF } from '@brief/domain/sample-letter';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { OrgTemplateVersion } from '@brief/domain/org-template';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { OrgTemplateStore } from '@brief/application/org-template.store';
|
||||
import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-template-editor.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CheckboxComponent } from '@shared/ui/checkbox/checkbox.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { CheckboxComponent } from '@shared/ui/atoms/checkbox/checkbox.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { textOf } from '@shared/kernel/rich-text';
|
||||
import { LibraryPassage } from '@brief/domain/brief';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
/** Molecule: shows the rejection comments (drafter view) or collects them from the
|
||||
approver. The approver rejects WITH comments; they never edit the letter. */
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
reduceUpload,
|
||||
requiredCategoriesSatisfied,
|
||||
deliveryRefs,
|
||||
digitalDocumentIds,
|
||||
} from '@shared/domain/upload.machine';
|
||||
|
||||
/** What the user is typing (raw, possibly invalid). */
|
||||
@@ -53,7 +54,7 @@ export function hasProgress(s: Extract<WizardState, { tag: 'Editing' }>): boolea
|
||||
!!s.draft.uren ||
|
||||
!!s.draft.jaren ||
|
||||
!!s.draft.punten ||
|
||||
deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)
|
||||
digitalDocumentIds(s.upload).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+46
-40
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, inject, input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import {
|
||||
WizardShellComponent,
|
||||
WizardError,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
naarStapLabel,
|
||||
} from '@shared/layout/wizard-shell/wizard-shell.component';
|
||||
import { toWizardErrors } from '@shared/layout/wizard-shell/wizard-errors';
|
||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/molecules/confirmation/confirmation.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
hasProgress,
|
||||
} from '@herregistratie/domain/herregistratie.machine';
|
||||
import { createDraftSync } from '@registratie/application/draft-sync';
|
||||
import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';
|
||||
import { DocumentUploadComponent } from '@shared/ui/organisms/upload/document-upload/document-upload.component';
|
||||
import { createUploadController } from '@shared/application/upload-controller';
|
||||
import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine';
|
||||
import { UploadState, initialUpload, digitalDocumentIds } from '@shared/domain/upload.machine';
|
||||
|
||||
/** Organism: multi-step herregistratie wizard. ALL state lives in one signal
|
||||
driven by the pure `reduce` function (see herregistratie.machine.ts) via an
|
||||
@@ -148,8 +148,14 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.
|
||||
})
|
||||
export class HerregistratieWizardComponent {
|
||||
private profile = inject(BigProfileStore);
|
||||
// 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).
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<WizardState>(initial);
|
||||
|
||||
// --- The store: all state in one signal, changed only by a pure reduce -----
|
||||
// The effect fires once, on the `Editing -> Submitting` transition. `Seed` is exempt,
|
||||
// so a story that mounts straight into `Submitting` does not call the network.
|
||||
// `draftSync` is declared below (both callbacks are deferred, so the cycle is safe).
|
||||
private store = createStore<WizardState, WizardMsg>(initial, reduce, {
|
||||
Submitting: async (s, store) => {
|
||||
this.profile.beginHerregistratie();
|
||||
@@ -163,36 +169,10 @@ export class HerregistratieWizardComponent {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** Preview/download link for a completed upload; delegates to the upload
|
||||
controller (application layer), which knows the dev-simulation `demo-*` ids
|
||||
have no stored bytes and returns no link for them. */
|
||||
protected previewUrlFor = (documentId: string): string | undefined =>
|
||||
this.uploadCtl.previewUrlFor(documentId);
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<WizardState>(initial);
|
||||
|
||||
readonly state = this.store.model; // public so the showcase can highlight the live state
|
||||
protected dispatch = this.store.dispatch;
|
||||
|
||||
// Backend draft-sync (new persistence for this wizard): create a Concept on first
|
||||
// progress, debounced-sync the snapshot, resume by `?aanvraag=<id>`.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'herregistratie',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Editing' || !hasProgress(s)) return null;
|
||||
const documentIds = deliveryRefs(s.upload)
|
||||
.filter((r) => r.channel === 'digital' && r.documentId)
|
||||
.map((r) => r.documentId!);
|
||||
return { draft: s, stepIndex: s.step - 1, stepCount: this.stepLabels.length, documentIds };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as WizardState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
|
||||
// Stepper labels + per-step heading titles (presentational only).
|
||||
// --- Static copy: stepper labels and per-step headings ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@herregWizard.step.werkervaring:Werkervaring`,
|
||||
$localize`:@@herregWizard.step.nascholing:Nascholing`,
|
||||
@@ -204,6 +184,7 @@ export class HerregistratieWizardComponent {
|
||||
$localize`:@@herregWizard.title.documenten:Documenten aanleveren`,
|
||||
];
|
||||
|
||||
// --- State projections: one narrow, then read-only views of it -------------
|
||||
private editing = computed(() => whenTag(this.state(), 'Editing'));
|
||||
protected step = computed(() => this.editing()?.step ?? 1);
|
||||
protected draft = computed<Draft>(
|
||||
@@ -214,11 +195,35 @@ export class HerregistratieWizardComponent {
|
||||
protected errJaren = computed(() => this.editing()?.errors.jaren ?? '');
|
||||
protected errPunten = computed(() => this.editing()?.errors.punten ?? '');
|
||||
protected errDocumenten = computed(() => this.editing()?.errors.documenten ?? '');
|
||||
|
||||
// --- Controllers: persistence and uploads ----------------------------------
|
||||
// Create a Concept on first progress, then debounced-sync the snapshot.
|
||||
// `?aanvraag=<id>` resumes it.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'herregistratie',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Editing' || !hasProgress(s)) return null;
|
||||
return {
|
||||
draft: s,
|
||||
stepIndex: s.step - 1,
|
||||
stepCount: this.stepLabels.length,
|
||||
documentIds: digitalDocumentIds(s.upload),
|
||||
};
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as WizardState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
protected uploadCtl = createUploadController({
|
||||
wizardId: 'herregistratie',
|
||||
getUpload: () => this.upload(),
|
||||
dispatch: (msg) => this.dispatch({ tag: 'Upload', msg }),
|
||||
});
|
||||
/** Preview/download link for a completed upload; delegates to the upload
|
||||
controller (application layer), which knows the dev-simulation `demo-*` ids
|
||||
have no stored bytes and returns no link for them. */
|
||||
protected previewUrlFor = (documentId: string): string | undefined =>
|
||||
this.uploadCtl.previewUrlFor(documentId);
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
protected stepTitle = computed(() => this.stepTitles[this.step() - 1]);
|
||||
@@ -228,11 +233,6 @@ export class HerregistratieWizardComponent {
|
||||
? naarStapLabel(step + 1, this.stepLabels[step])
|
||||
: $localize`:@@herregWizard.indienen:Herregistratie aanvragen`;
|
||||
});
|
||||
|
||||
/** Stepper emits a 0-based index for an earlier (visited) step. */
|
||||
protected goToStep(index: number) {
|
||||
this.dispatch({ tag: 'GaNaarStap', step: (index + 1) as 1 | 2 | 3 });
|
||||
}
|
||||
/** Maps this machine's own tags onto the shell's `WizardPhase` vocabulary,
|
||||
composing the localized failure prefix so the `Failed` message arrives intact. */
|
||||
protected phase = computed<WizardPhase>(() => {
|
||||
@@ -254,6 +254,12 @@ export class HerregistratieWizardComponent {
|
||||
/** Current step's field errors, flattened for the shell's error summary. */
|
||||
protected errorList = computed<WizardError[]>(() => toWizardErrors(this.editing()?.errors ?? {}));
|
||||
|
||||
// --- Event handlers: narrow a child event into a message -------------------
|
||||
/** Stepper emits a 0-based index for an earlier (visited) step. */
|
||||
protected goToStep(index: number) {
|
||||
this.dispatch({ tag: 'GaNaarStap', step: (index + 1) as 1 | 2 | 3 });
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume the backend draft
|
||||
// (`?aanvraag=<id>`) or start fresh. Persistence is the draftSync controller's job.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { map } from '@shared/application/remote-data';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { Answers, Errors } from '@herregistratie/domain/intake.machine';
|
||||
|
||||
/** Step: the intake wizard's first screen (foreign work in the last 5 years).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, computed, effect, inject, input, untracked } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/molecules/confirmation/confirmation.component';
|
||||
import {
|
||||
WizardShellComponent,
|
||||
WizardError,
|
||||
@@ -32,8 +32,9 @@ import { ReviewStep } from './review.step';
|
||||
/** Organism: a BRANCHING intake questionnaire. All state lives in one signal
|
||||
driven by the pure `reduce` (intake.machine.ts). Which step renders is derived
|
||||
from the answers via `visibleSteps`, never stored — so editing an earlier
|
||||
answer immediately changes the remaining steps. Answers are persisted to
|
||||
sessionStorage so a page reload keeps the user's progress (cleared on tab close). */
|
||||
answer immediately changes the remaining steps. The draft persists to the
|
||||
backend as a Concept aanvraag (createDraftSync), so a reload — or a "Verder
|
||||
gaan" from the dashboard via `?aanvraag=<id>` — resumes progress. */
|
||||
@Component({
|
||||
selector: 'app-intake-wizard',
|
||||
imports: [
|
||||
@@ -106,8 +107,14 @@ 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);
|
||||
// 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).
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<IntakeState>(initial);
|
||||
|
||||
// --- The store: all state in one signal, changed only by a pure reduce -----
|
||||
// The effect fires once, on the `Answering -> Submitting` transition. `Seed` is exempt,
|
||||
// so a story that mounts straight into `Submitting` does not call the network.
|
||||
// `draftSync` is declared below (both callbacks are deferred, so the cycle is safe).
|
||||
private store = createStore<IntakeState, IntakeMsg>(initial, reduce, {
|
||||
Submitting: async (s, store) => {
|
||||
this.profile.beginHerregistratie();
|
||||
@@ -129,24 +136,22 @@ export class IntakeWizardComponent {
|
||||
},
|
||||
});
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<IntakeState>(initial);
|
||||
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
// Backend draft-sync (replaces sessionStorage); the intake has no uploads.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'intake',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Answering' || !hasProgress(s)) return null;
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds: [] };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as IntakeState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
// --- Static copy: stepper labels and per-step headings ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@intake.step.buitenland:Buitenland`,
|
||||
$localize`:@@intake.step.werk:Werk`,
|
||||
$localize`:@@intake.step.controle:Controle`,
|
||||
];
|
||||
private stepTitles: Record<StepId, string> = {
|
||||
buitenland: $localize`:@@intake.title.buitenland:Werken in het buitenland`,
|
||||
werk: $localize`:@@intake.title.werk:Werkervaring in Nederland`,
|
||||
review: $localize`:@@intake.title.review:Controleren en indienen`,
|
||||
};
|
||||
|
||||
// --- State projections: one narrow, then read-only views of it -------------
|
||||
private answering = computed(() => whenTag(this.state(), 'Answering'));
|
||||
/** Public so the showcase can render the (fixed) step list next to the wizard. */
|
||||
readonly steps = STEPS;
|
||||
@@ -159,17 +164,21 @@ export class IntakeWizardComponent {
|
||||
);
|
||||
protected errors = computed<Errors>(() => this.answering()?.errors ?? {});
|
||||
|
||||
// --- Controllers: persistence and uploads ----------------------------------
|
||||
// Create a Concept on first progress, then debounced-sync the snapshot.
|
||||
// `?aanvraag=<id>` resumes it. The intake has no uploads.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'intake',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Answering' || !hasProgress(s)) return null;
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds: [] };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as IntakeState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@intake.step.buitenland:Buitenland`,
|
||||
$localize`:@@intake.step.werk:Werk`,
|
||||
$localize`:@@intake.step.controle:Controle`,
|
||||
];
|
||||
private stepTitles: Record<StepId, string> = {
|
||||
buitenland: $localize`:@@intake.title.buitenland:Werken in het buitenland`,
|
||||
werk: $localize`:@@intake.title.werk:Werkervaring in Nederland`,
|
||||
review: $localize`:@@intake.title.review:Controleren en indienen`,
|
||||
};
|
||||
protected stepTitle = computed(() => this.stepTitles[this.step()]);
|
||||
protected primaryLabel = computed(() => {
|
||||
if (this.step() === 'review') return $localize`:@@intake.indienen:Aanvraag indienen`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/molecules/review-section/review-section.component';
|
||||
import { Answers, lageUren } from '@herregistratie/domain/intake.machine';
|
||||
|
||||
/** Step: the intake wizard's review screen. Pure & presentational — values in via
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { Answers, Errors, lageUren } from '@herregistratie/domain/intake.machine';
|
||||
|
||||
/** Step: the intake wizard's second screen (work experience in the Netherlands,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';
|
||||
|
||||
/** Page: the branching intake questionnaire. Built entirely from existing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { HEADER_ADMIN_LINKS } from '@shared/layout/site-header/nav-config';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||
import { FLAG_INSCHRIJVING_OPEN } from '@shared/domain/feature-flag';
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
reduceUpload,
|
||||
requiredCategoriesSatisfied,
|
||||
deliveryRefs,
|
||||
digitalDocumentIds,
|
||||
} from '@shared/domain/upload.machine';
|
||||
|
||||
/**
|
||||
@@ -109,7 +110,7 @@ export function hasProgress(s: Extract<RegistratieState, { tag: 'Invullen' }>):
|
||||
!!d.email ||
|
||||
!!d.diplomaId ||
|
||||
!!d.beroep ||
|
||||
deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)
|
||||
digitalDocumentIds(s.upload).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { TYPE_LABELS } from '@registratie/domain/aanvraag-view';
|
||||
import { blockActions } from '@registratie/domain/block-actions';
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
|
||||
export interface AdresValue {
|
||||
straat: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { Adres } from '@registratie/domain/person';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { TaskListComponent } from '@shared/ui/task-list/task-list.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { TaskListComponent } from '@shared/ui/molecules/task-list/task-list.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, inject, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';
|
||||
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||
import { Draft, DraftField, Errors } from '@registratie/domain/registratie-wizard.machine';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Component, computed, inject, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { DocumentUploadComponent } from '@shared/ui/organisms/upload/document-upload/document-upload.component';
|
||||
import { createUploadController } from '@shared/application/upload-controller';
|
||||
import { UploadMsg, UploadState } from '@shared/domain/upload.machine';
|
||||
import { RemoteData, successOr } from '@shared/application/remote-data';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject, input, output } from '@angular/core';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/molecules/review-section/review-section.component';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
import { diplomaMsg } from './diploma-msg';
|
||||
import { HANDMATIG } from './beroep.step';
|
||||
|
||||
const data: DuoLookupDto = {
|
||||
diplomas: [
|
||||
{
|
||||
id: 'd1',
|
||||
naam: 'Verpleegkunde',
|
||||
instelling: 'Hogeschool Utrecht',
|
||||
jaar: 2019,
|
||||
beroep: 'Verpleegkundige',
|
||||
policyQuestions: [
|
||||
{ id: 'q1', vraag: 'Vraag 1', type: 'ja-nee' },
|
||||
{ id: 'q2', vraag: 'Vraag 2', type: 'tekst' },
|
||||
],
|
||||
},
|
||||
],
|
||||
handmatig: {
|
||||
beroepen: ['Verpleegkundige', 'Arts'],
|
||||
policyQuestions: [{ id: 'm1', vraag: 'Handmatige vraag', type: 'ja-nee' }],
|
||||
},
|
||||
};
|
||||
|
||||
describe('diplomaMsg', () => {
|
||||
it('resolves a known diploma into KiesDiploma with the server-derived beroep', () => {
|
||||
expect(diplomaMsg(data, 'd1')).toEqual({
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: 'd1',
|
||||
beroep: 'Verpleegkundige',
|
||||
vraagIds: ['q1', 'q2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the manual sentinel into KiesHandmatig with the maximal question set', () => {
|
||||
expect(diplomaMsg(data, HANDMATIG)).toEqual({ tag: 'KiesHandmatig', vraagIds: ['m1'] });
|
||||
});
|
||||
|
||||
it('returns null for an unknown diploma id', () => {
|
||||
expect(diplomaMsg(data, 'onbekend')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
import { RegistratieMsg } from '@registratie/domain/registratie-wizard.machine';
|
||||
import { HANDMATIG } from './beroep.step';
|
||||
|
||||
/**
|
||||
* Resolve the diploma that the user picked into the machine message it implies.
|
||||
*
|
||||
* The DUO payload maps a diploma id onto a server-derived beroep and the policy
|
||||
* questions that apply to it. Reading that map is message construction, so it
|
||||
* belongs beside the container, not in the beroep step. The backend stays the
|
||||
* authority on both values (ADR-0001) — this function only selects them.
|
||||
*
|
||||
* Returns null when the id matches no known diploma. The caller then dispatches
|
||||
* nothing and the wizard keeps its current state.
|
||||
*/
|
||||
export function diplomaMsg(data: DuoLookupDto, id: string): RegistratieMsg | null {
|
||||
if (id === HANDMATIG) {
|
||||
return { tag: 'KiesHandmatig', vraagIds: data.handmatig.policyQuestions.map((q) => q.id) };
|
||||
}
|
||||
const diploma = data.diplomas.find((d) => d.id === id);
|
||||
if (!diploma) return null;
|
||||
return {
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: diploma.id,
|
||||
beroep: diploma.beroep,
|
||||
vraagIds: diploma.policyQuestions.map((q) => q.id),
|
||||
};
|
||||
}
|
||||
+41
-48
@@ -1,6 +1,6 @@
|
||||
import { Component, computed, effect, inject, input, untracked } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/molecules/confirmation/confirmation.component';
|
||||
import {
|
||||
WizardShellComponent,
|
||||
WizardError,
|
||||
@@ -26,17 +26,18 @@ import {
|
||||
STEPS,
|
||||
} from '@registratie/domain/registratie-wizard.machine';
|
||||
import { createDraftSync } from '@registratie/application/draft-sync';
|
||||
import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine';
|
||||
import { UploadState, initialUpload, digitalDocumentIds } from '@shared/domain/upload.machine';
|
||||
import { AdresStep } from './adres.step';
|
||||
import { BeroepStep, HANDMATIG } from './beroep.step';
|
||||
import { BeroepStep } from './beroep.step';
|
||||
import { ControleStep } from './controle.step';
|
||||
import { diplomaMsg } from './diploma-msg';
|
||||
|
||||
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
|
||||
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
|
||||
draft via an effect; the DUO diploma list renders through <app-async>; choosing
|
||||
a diploma reveals its server-derived beroep. The draft is persisted to the
|
||||
backend as a Concept aanvraag (createDraftSync) so a reload — or a"Verder gaan"
|
||||
from the dashboard via `?aanvraag=<id>` — resumes progress. Built from existing
|
||||
a diploma reveals its server-derived beroep. The draft persists to the backend
|
||||
as a Concept aanvraag (createDraftSync), so a reload — or a "Verder gaan" from
|
||||
the dashboard via `?aanvraag=<id>` — resumes progress. Built from existing
|
||||
atoms/molecules. */
|
||||
@Component({
|
||||
selector: 'app-registratie-wizard',
|
||||
@@ -117,8 +118,14 @@ import { ControleStep } from './controle.step';
|
||||
})
|
||||
export class RegistratieWizardComponent {
|
||||
private lookup = inject(RegistratieLookupStore);
|
||||
// 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).
|
||||
|
||||
/** Optional seed so Storybook / tests can mount any state directly. */
|
||||
seed = input<RegistratieState>(initial);
|
||||
|
||||
// --- The store: all state in one signal, changed only by a pure reduce -----
|
||||
// The effect fires once, on the `Invullen -> Indienen` transition. `Seed` is exempt,
|
||||
// so a story that mounts straight into `Indienen` does not call the network.
|
||||
// `draftSync` is declared below (both callbacks are deferred, so the cycle is safe).
|
||||
private store = createStore<RegistratieState, RegistratieMsg>(initial, reduce, {
|
||||
Indienen: async (s, store) => {
|
||||
const r = await this.draftSync.submit({
|
||||
@@ -129,50 +136,56 @@ export class RegistratieWizardComponent {
|
||||
else store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
},
|
||||
});
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
/** Optional seed so Storybook / tests can mount any state directly. */
|
||||
seed = input<RegistratieState>(initial);
|
||||
|
||||
// --- Static copy: stepper labels and per-step headings ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@regWizard.step.adres:Adres`,
|
||||
$localize`:@@regWizard.step.beroep:Beroep`,
|
||||
$localize`:@@regWizard.step.controle:Controle`,
|
||||
]; // short labels for the stepper
|
||||
];
|
||||
private stepTitles = [
|
||||
$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`,
|
||||
$localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`,
|
||||
$localize`:@@regWizard.title.controle:Controleren en indienen`,
|
||||
];
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
// --- State projections: one narrow, then read-only views of it -------------
|
||||
private invullen = computed(() => whenTag(this.state(), 'Invullen'));
|
||||
protected cursor = computed(() => this.invullen()?.cursor ?? 0);
|
||||
protected draft = computed<Draft>(() => this.invullen()?.draft ?? { antwoorden: {} });
|
||||
protected errors = computed<Errors>(() => this.invullen()?.errors ?? {});
|
||||
protected upload = computed<UploadState>(() => this.invullen()?.upload ?? initialUpload);
|
||||
// Backend draft-sync (replaces sessionStorage): create a Concept once the user has
|
||||
// made progress, then debounced-sync the whole machine snapshot; resume by `?aanvraag`.
|
||||
protected step = computed<StepId>(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);
|
||||
protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');
|
||||
/** From the lookup store, not the machine: the beroep step renders it, and
|
||||
`onDiplomaKeuze` reads it to resolve the picked id into a message. */
|
||||
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookup.duoLookup(), null));
|
||||
|
||||
// --- Controllers: persistence and uploads ----------------------------------
|
||||
// Create a Concept once the user has made progress, then debounced-sync the whole
|
||||
// machine snapshot. `?aanvraag=<id>` resumes it.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'registratie',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Invullen' || !hasProgress(s)) return null;
|
||||
const documentIds = deliveryRefs(s.upload)
|
||||
.filter((r) => r.channel === 'digital' && r.documentId)
|
||||
.map((r) => r.documentId!);
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds };
|
||||
return {
|
||||
draft: s,
|
||||
stepIndex: s.cursor,
|
||||
stepCount: STEPS.length,
|
||||
documentIds: digitalDocumentIds(s.upload),
|
||||
};
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as RegistratieState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
protected step = computed<StepId>(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
protected stepTitle = computed(
|
||||
() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)],
|
||||
);
|
||||
protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
protected primaryLabel = computed(() => {
|
||||
if (this.step() === 'controle') return $localize`:@@regWizard.indienen:Registratie indienen`;
|
||||
const next = this.cursor() + 1;
|
||||
@@ -203,30 +216,12 @@ export class RegistratieWizardComponent {
|
||||
return [...toWizardErrors(e), ...toWizardErrors(e.antwoorden ?? {}, 'vraag-')];
|
||||
});
|
||||
|
||||
/** Parsed lookup as a plain value (or null) — needed here only to resolve
|
||||
`onDiplomaKeuze`'s message from an id (the DUO payload maps an id to a
|
||||
beroep and its question ids; that is machine-message construction, and it
|
||||
belongs in the container, not the beroep step). */
|
||||
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookup.duoLookup(), null));
|
||||
|
||||
// --- Event handlers: narrow a child event into a message -------------------
|
||||
protected onDiplomaKeuze(id: string) {
|
||||
const data = this.duoData();
|
||||
if (!data) return;
|
||||
if (id === HANDMATIG) {
|
||||
this.dispatch({
|
||||
tag: 'KiesHandmatig',
|
||||
vraagIds: data.handmatig.policyQuestions.map((q) => q.id),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const d = data.diplomas.find((x) => x.id === id);
|
||||
if (d)
|
||||
this.dispatch({
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: d.id,
|
||||
beroep: d.beroep,
|
||||
vraagIds: d.policyQuestions.map((q) => q.id),
|
||||
});
|
||||
const msg = diplomaMsg(data, id);
|
||||
if (msg) this.dispatch(msg);
|
||||
}
|
||||
|
||||
/** Narrows the beroep step's plain-string `kanaalChange` into the machine's
|
||||
@@ -260,8 +255,6 @@ export class RegistratieWizardComponent {
|
||||
});
|
||||
});
|
||||
});
|
||||
// A11y: focus management (step heading on step change, error summary on a
|
||||
// failed submit) now lives in the shared WizardShellComponent.
|
||||
}
|
||||
|
||||
/** Reset the wizard to a fresh start. Reload the BRP lookup so the address
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { RegistratieWizardComponent } from '@registratie/ui/registratie-wizard/registratie-wizard.component';
|
||||
|
||||
/** Page: register in the BIG-register. Built entirely from existing building
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
import { ChangeRequestFormComponent } from '@registratie/ui/change-request-form/change-request-form.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ import { Component, input } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { statusColor, statusLabel } from '@registratie/domain/registration.policy';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/atoms/status-badge/status-badge.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
|
||||
/** Organism: registration summary in a CIBG Datablock. Composes data-row rows +
|
||||
status-badge atom (no card wrapper — the datablock carries its own surface). */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { parsePostcode } from '@registratie/domain/value-objects/postcode';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/atoms/masked-value/masked-value.component';
|
||||
import { parseBsn } from '@shared/kernel/bsn';
|
||||
import { maskBsn } from '@shared/kernel/pii';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import type { Resource } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
import { highlightTs } from './highlight-ts';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
|
||||
@@ -787,6 +787,34 @@ than ESLint: it is where every other boundary rule lives and it emits the archit
|
||||
|
||||
## Phase 5 — Fix the docs that describe this flow
|
||||
|
||||
0. **First, RD-37 — five accessibility suppressions point at a ticket that closed.** Found
|
||||
while measuring RD-30, because archiving `backlog/` would bury the reference.
|
||||
|
||||
Five stories carry `a11y: { disable: true }` whose reason reads "WP-11 (CIBG markup
|
||||
fidelity) reworks this markup" — future tense. **WP-11 is `Status: done`**, and so is
|
||||
WP-13, the gap register it hands the remainder to. No open ticket owns the defect, so the
|
||||
README's rule ("no check disabled without a reference to the ticket that removes it") holds
|
||||
only in letter.
|
||||
|
||||
The defect is real and shipped, not story-only: `app-choice-link` and `app-aanvraag-block`
|
||||
render a component host between the keuzelijst `<ul>` and its `<li>`, which breaks axe's
|
||||
`list`/`listitem` rule for assistive technology. `display: contents` does not fix it.
|
||||
|
||||
WP-11 solved exactly this for `application-link` by making the host **be** the `<li>`
|
||||
(`selector: 'li[app-application-link]'`), which is axe-clean today. The same move is
|
||||
available here — but it is **not** obviously correct, and that is why this is a ticket
|
||||
rather than a one-line change: `atomic-design.mdx:113` documents the current split as
|
||||
deliberate, "different list/host semantics … Merging would fight the vendored CSS".
|
||||
|
||||
So RD-37 must decide one question before it writes any code: **does giving `choice-link` and
|
||||
`aanvraag-block` an `li[…]` attribute host still match the vendored CIBG keuzelijst CSS?**
|
||||
If yes, convert both, delete the five suppressions, and correct `atomic-design.mdx`'s claim.
|
||||
If no, the honest outcome is a new open ticket named in five rewritten reasons — not a
|
||||
pointer into an archive.
|
||||
|
||||
**Sequence RD-37 before RD-30**, so the archive move does not have to rewrite five paths
|
||||
that are about to disappear.
|
||||
|
||||
1. **Archive the finished backlog.** `git mv docs/project/backlog` and
|
||||
`docs/project/refactor-backlog-setup` under `docs/project/archive/`. Verified: **all 74
|
||||
WP files are `Status: done`**; the two trees are 6,982 + 9,318 = **16,300 of the docs
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# RD-27 — Make the folder equal the layer in `libs/shared/src/ui/`
|
||||
|
||||
Status: done
|
||||
Source: PLAN.md 4a, Phase 4
|
||||
|
||||
## Why
|
||||
|
||||
CLAUDE.md decision 2 says "folder = layer": `libs/shared/ui` atoms → molecules → organisms.
|
||||
Today `libs/shared/src/ui/` is 26 flat directories, and the only record of a component's layer
|
||||
is its story title and a header comment. Nothing stops an atom importing an organism.
|
||||
|
||||
This ticket makes the structure say what the rule says. It is a **pure move**: no component
|
||||
changes, no story title changes, no behaviour. RD-29 then adds the dependency-cruiser rules
|
||||
that the folders make expressible.
|
||||
|
||||
**This is the highest-risk ticket in the arc**, because a broken `.mdx` story import compiles
|
||||
fine and only fails when Storybook builds. The README says it must not be pushed without
|
||||
`npm run ci --full`.
|
||||
|
||||
## Read first
|
||||
|
||||
- `libs/shared/src/ui/` — 25 flat component directories plus `upload/` with 8 of its own.
|
||||
- `libs/shared/docs/atomic-design.mdx:2-5` — three of the seven `.mdx` story imports that break.
|
||||
- PLAN.md 4a — the design record, including why `layout/` does not move.
|
||||
- `.storybook-ssp/main.ts:11-14` — the globs, which are recursive and need no edit.
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
1. **Three layer folders, and the full 33-directory mapping. This table is the ticket.**
|
||||
|
||||
`libs/shared/src/ui/atoms/` — 12 flat:
|
||||
|
||||
```
|
||||
alert button checkbox heading link masked-value
|
||||
placeholder-chip radio-group skeleton spinner status-badge text-input
|
||||
```
|
||||
|
||||
`libs/shared/src/ui/molecules/` — 13 flat:
|
||||
|
||||
```
|
||||
application-link application-list async choice-link choice-list confirmation
|
||||
data-block data-row form-field review-section rich-text-editor stepper task-list
|
||||
```
|
||||
|
||||
`upload/` **keeps its feature subfolder inside each layer** — 8 directories:
|
||||
|
||||
| New location | Directories |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `ui/atoms/upload/` | `delivery-channel-toggle`, `document-chip`, `file-input`, `upload-progress-bar`, `upload-status-icon` |
|
||||
| `ui/molecules/upload/` | `single-upload` |
|
||||
| `ui/organisms/upload/` | `document-category`, `document-upload` |
|
||||
|
||||
`ui/organisms/` holds nothing but `upload/`. That is correct and worth seeing.
|
||||
|
||||
The mapping is derived from each component's own story title, not invented — every one of
|
||||
the 33 already declares its layer as `Design System/<Layer>/…`.
|
||||
|
||||
2. **Use `git mv` per directory, so the diff reads as renames.** `git diff --stat -M` must show
|
||||
renames plus one-line import edits, nothing else.
|
||||
|
||||
3. **Rewrite the 28 distinct specifiers, longest key first.** All 28 are listed by the mapping
|
||||
in decision 1; 25 are flat and 3 are the upload components imported from outside
|
||||
(`upload/document-upload` → `organisms/upload/document-upload`, `upload/file-input` →
|
||||
`atoms/upload/file-input`, `upload/single-upload` → `molecules/upload/single-upload`).
|
||||
|
||||
Verified: **no specifier is a prefix of another**, so ordering cannot corrupt a rewrite here.
|
||||
Do it longest-first anyway — it costs nothing and the property is not guaranteed to hold if
|
||||
this is ever repeated.
|
||||
|
||||
There are 200 occurrences across 73 files, and this rewrite alone changes none of them.
|
||||
|
||||
**Corrected after the ticket ran: the total lands at 205, not 200.** Decision 4 converts five
|
||||
relative imports into `@shared/ui/…` aliases, and each of those is itself a new occurrence.
|
||||
This decision's "must not change" governs _this_ rewrite; decision 4 adds five on top. The
|
||||
two decisions were written as if they counted different things, and they do not.
|
||||
|
||||
The honest check is per-source: 200 from the specifier rewrite, plus exactly the 5 that
|
||||
decision 4 names.
|
||||
|
||||
4. **Five of the seven relative imports inside `upload/` become aliases; two stay relative.**
|
||||
A `../sibling/` import only breaks when the sibling lands in a different layer:
|
||||
|
||||
| File (new location) | Import | Becomes |
|
||||
| ------------------------------------ | ---------------------------- | ------------------------------------------------- |
|
||||
| `organisms/upload/document-category` | `../delivery-channel-toggle` | `@shared/ui/atoms/upload/delivery-channel-toggle` |
|
||||
| `organisms/upload/document-category` | `../file-input` | `@shared/ui/atoms/upload/file-input` |
|
||||
| `organisms/upload/document-category` | `../single-upload` | `@shared/ui/molecules/upload/single-upload` |
|
||||
| `molecules/upload/single-upload` | `../document-chip` | `@shared/ui/atoms/upload/document-chip` |
|
||||
| `molecules/upload/single-upload` | `../upload-progress-bar` | `@shared/ui/atoms/upload/upload-progress-bar` |
|
||||
|
||||
Unchanged, because both ends stay in the same layer: `atoms/upload/document-chip` →
|
||||
`../upload-status-icon`, and `organisms/upload/document-upload` → `../document-category`.
|
||||
|
||||
**Use the alias, not `../../../`.** Cross-directory imports inside `libs/shared` already use
|
||||
`@shared/…` (see `wizard-shell.component.ts`), and a three-level relative path is exactly the
|
||||
thing a later move breaks silently.
|
||||
|
||||
5. **Seven `.mdx` story imports break. All seven are real imports, not prose.** The Order table
|
||||
says eight; measured, it is seven:
|
||||
|
||||
```
|
||||
a11y.mdx:2 ../src/ui/alert/alert.stories -> ui/atoms/alert/…
|
||||
a11y.mdx:3 ../src/ui/form-field/form-field.stories -> ui/molecules/form-field/…
|
||||
atomic-design.mdx:2 ../src/ui/button/button.stories -> ui/atoms/button/…
|
||||
atomic-design.mdx:3 ../src/ui/form-field/form-field.stories -> ui/molecules/form-field/…
|
||||
atomic-design.mdx:5 ../src/ui/upload/document-upload/… -> ui/organisms/upload/document-upload/…
|
||||
fp-in-ui.mdx:2 ../src/ui/async/async.stories -> ui/molecules/async/…
|
||||
remote-data.mdx:2 ../src/ui/async/async.stories -> ui/molecules/async/…
|
||||
```
|
||||
|
||||
`atomic-design.mdx:4` imports `../src/layout/page-shell/…`. **Leave it alone** — `layout/`
|
||||
does not move.
|
||||
|
||||
6. **`layout/` does not move, and neither does `libs/beheer/src/ui/`.** CLAUDE.md §5 explicitly
|
||||
sanctions `libs/shared/layout` holding several layers, and `libs/beheer` is a bounded context
|
||||
that lives in `libs/` only because two apps share it. Both are settled; do not revisit them.
|
||||
|
||||
7. **Change no story title, no layer-tag comment, and no component code.** The titles already
|
||||
say the right thing, which is what made decision 1's mapping derivable. The two mislabelled
|
||||
tags (`async` has `/** Convenience: */`, `breadcrumb` says `/** Chrome: */`) and the two
|
||||
missing ones (`masked-value`, `rich-text-editor`) are **RD-28's** job, not this ticket's.
|
||||
|
||||
8. **No barrel file.** The repository has none and does not need one. A barrel would also hide
|
||||
exactly the layer boundary this ticket exists to expose.
|
||||
|
||||
## Files
|
||||
|
||||
- 33 directories moved under `libs/shared/src/ui/`
|
||||
- ~73 files with a one-line specifier edit
|
||||
- 4 `.mdx` files (7 import lines)
|
||||
|
||||
No changes to `angular.json`, `eslint.config.mjs`, `plopfile.mjs`, any `tsconfig.json`,
|
||||
`.dependency-cruiser.*`, `scripts/check-tokens.sh`, or `e2e/`. Verified: `@shared/*` maps to
|
||||
`libs/shared/src/*`, so a deeper path resolves unchanged; both Storybook globs are recursive;
|
||||
dependency-cruiser's `ui-not-infrastructure` pattern is `(/ui/|/layout/)`, which still matches a
|
||||
nested path; and `check-tokens.sh`'s CIBG-GAP check keys on the **directory basename**, which a
|
||||
parent-folder move preserves.
|
||||
|
||||
## Steps
|
||||
|
||||
1. `mkdir` the three layer folders, then `git mv` all 33 directories per decision 1.
|
||||
2. Rewrite the 28 specifiers across `apps/` and `libs/` (decision 3).
|
||||
3. Fix the five relative imports inside `upload/` (decision 4).
|
||||
4. Fix the seven `.mdx` imports (decision 5).
|
||||
5. `npm run typecheck` — four tsconfigs, and the fastest way to catch a missed specifier.
|
||||
6. `git add -A`, then run the acceptance commands.
|
||||
7. Update this ticket's `Status:` to `done` and the README's RD-27 row to `done`.
|
||||
8. Commit all of it together.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
Measured against the tree before handover. Run after `git add -A`.
|
||||
|
||||
The structure is three folders, and every directory landed:
|
||||
|
||||
```bash
|
||||
ls -d libs/shared/src/ui/*/ | wc -l # is 26 -> MUST be 3
|
||||
ls -d libs/shared/src/ui/atoms/*/ | wc -l # MUST be 13 (12 + upload)
|
||||
ls -d libs/shared/src/ui/atoms/upload/*/ | wc -l # MUST be 5
|
||||
ls -d libs/shared/src/ui/molecules/*/ | wc -l # MUST be 14 (13 + upload)
|
||||
ls -d libs/shared/src/ui/molecules/upload/*/ | wc -l # MUST be 1
|
||||
ls -d libs/shared/src/ui/organisms/*/ | wc -l # MUST be 1 (upload)
|
||||
ls -d libs/shared/src/ui/organisms/upload/*/ | wc -l # MUST be 2
|
||||
```
|
||||
|
||||
**The single strongest check in this ticket** — every specifier now names a layer, so only three
|
||||
distinct values may remain:
|
||||
|
||||
```bash
|
||||
git grep -ho "@shared/ui/[a-z0-9-]*" -- apps libs | sort -u # is 26 values -> MUST be exactly these 3:
|
||||
# @shared/ui/atoms
|
||||
# @shared/ui/molecules
|
||||
# @shared/ui/organisms
|
||||
```
|
||||
|
||||
Nothing was lost or duplicated in the rewrite:
|
||||
|
||||
```bash
|
||||
git grep -ho "@shared/ui/" -- apps libs | wc -l # is 200 -> MUST be 205 (200 + decision 4's 5)
|
||||
git grep -c "src/ui/" -- '*.mdx' | awk -F: '{s+=$NF} END {print s+0}' # is 7 -> MUST still be 7
|
||||
git grep -c "from '\.\./" -- libs/shared/src/ui/ | awk -F: '{s+=$NF} END {print s+0}' # is 7 -> MUST be 2
|
||||
```
|
||||
|
||||
The move reads as a move (decision 2):
|
||||
|
||||
```bash
|
||||
git diff --cached --stat -M | tail -1 # inspect: renames + one-line edits, no rewritten files
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run ci --full # exits 0 — REQUIRED, see Verification
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
**`npm run ci --full` is mandatory and non-negotiable for this ticket.** Plain `npm run ci` does
|
||||
not build Storybook, and a `.mdx` importing a moved story path is invisible to the type-checker,
|
||||
the linter and every unit test. It fails only when `build-storybook` runs. The README calls out
|
||||
RD-27 by name for this reason.
|
||||
|
||||
Run `npm run typecheck` first anyway (step 5): it covers four tsconfigs and catches a missed
|
||||
`@shared/ui/*` specifier in seconds rather than at the end of a full gate.
|
||||
|
||||
`libs/shared/docs/layers.mdx` deep-links two story ids
|
||||
(`design-system-molecules-application-link--navigatie`,
|
||||
`domein-registratie-aanvraag-block--concept`). Story ids derive from **titles**, and decision 7
|
||||
changes no title, so both links survive. Do not "fix" them.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The dependency-cruiser ladder rules. RD-29 adds them, and it depends on this ticket.
|
||||
- Layer-tag comments and the `libs/beheer` title rule — RD-28.
|
||||
- `layout/` (decision 6).
|
||||
- Any component's code, template, styles or story title.
|
||||
|
||||
## Risks
|
||||
|
||||
- **A broken `.mdx` import is invisible until Storybook builds.** This is the whole reason the
|
||||
ticket carries `--full`. Decision 5 lists all seven; check each one after the move.
|
||||
- **`git mv`, not `mv` + `git add`.** Both produce the same tree, but only the first keeps the
|
||||
diff readable as renames — and this diff is 33 directories wide.
|
||||
- **Do not flatten `upload/`** (decision 1). Its subfolder survives inside each layer.
|
||||
- **Do not reach for `../../../`** (decision 4). Use the alias.
|
||||
- **The occurrence count is the tripwire for a bad `sed`.** 200 before, 200 after. A rewrite
|
||||
that accidentally matches twice, or drops a line, moves that number.
|
||||
- **`atomic-design.mdx` has four story imports and only three of them move.** The fourth is
|
||||
`page-shell`, in `layout/`.
|
||||
@@ -0,0 +1,154 @@
|
||||
# RD-28 — Two layer-tag fixes, and the `libs/beheer` title rule
|
||||
|
||||
Status: done
|
||||
Source: PLAN.md 4a (the two mislabels) and 4b (keep the tags)
|
||||
|
||||
## Why
|
||||
|
||||
RD-27 made the folder equal the layer. Three statements about a component's layer now exist —
|
||||
the folder, the header comment's tag, and the story title — and a three-way agreement that has
|
||||
never once disagreed is cheap documentation.
|
||||
|
||||
It disagrees in exactly two places. And one repository-wide rule in CLAUDE.md is simply wrong
|
||||
about `libs/beheer`.
|
||||
|
||||
## Read first
|
||||
|
||||
- `libs/shared/src/ui/molecules/async/async.component.ts:45` — the class header, with no layer
|
||||
tag; and line 154, the `Convenience:` comment on the `ASYNC` array, which is **correct and
|
||||
stays**.
|
||||
- `libs/shared/src/layout/breadcrumb/breadcrumb.component.ts:9` — `/** Chrome: …`, against a
|
||||
story titled `Design System/Molecules/Breadcrumb`.
|
||||
- `CLAUDE.md:213` and `libs/shared/docs/layers.mdx:13` — the two lines that claim
|
||||
`libs/beheer/ui` is Design System.
|
||||
- PLAN.md 4a's third paragraph and 4b.
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
1. **Exactly two components are mislabelled. Verified by scanning all 36**, comparing each
|
||||
component's first layer tag against its own story title:
|
||||
|
||||
| File | Story title | Tag today | Becomes |
|
||||
| ------------------------------------------- | ----------- | --------- | ----------- |
|
||||
| `ui/molecules/async/async.component.ts` | Molecules | _(none)_ | `Molecule:` |
|
||||
| `layout/breadcrumb/breadcrumb.component.ts` | Molecules | `Chrome:` | `Molecule:` |
|
||||
|
||||
Every other component already agrees. Do not "tidy" any other tag.
|
||||
|
||||
2. **`async.component.ts` gets the tag added to its class header at line 45, not to line 154.**
|
||||
The `Convenience:` comment at 154 describes the `ASYNC` array export — "import this array to
|
||||
get the wrapper + all slot directives" — which is an accurate description of a convenience
|
||||
export, not a layer claim. Leave it exactly as it is.
|
||||
|
||||
3. **Keep all 68 layer-tag comments. Delete none.** PLAN 4b reversed an earlier proposal to
|
||||
strip them: the tag prefixes a real one-line description, so removing the word leaves the
|
||||
sentence and buys nothing. Only the tags in `libs/shared/src/ui/` are made redundant by the
|
||||
new folders; the ones in `apps/**/ui/` are the sole carrier of the layer, because a context
|
||||
component's title deliberately omits it.
|
||||
|
||||
4. **`libs/beheer` is Domein, not Design System. The docs are wrong; the code is right.**
|
||||
`stamdata-table-editor.stories.ts` is titled `Domein/Beheer/Stamdata Table Editor`, and that
|
||||
is correct: `libs/beheer` **is** a bounded context, which lives in `libs/` only because two
|
||||
apps share it. It is not part of the design system, and it correctly has no layer folders.
|
||||
|
||||
Amend the two doc lines that say otherwise:
|
||||
|
||||
- `CLAUDE.md:213` — drop the words "or `libs/beheer/ui`" from the Design System title rule,
|
||||
and say that a `libs/beheer/ui` component is titled `Domein/Beheer/<Name>`.
|
||||
- `libs/shared/docs/layers.mdx:13` — drop the parenthetical "(or `libs/beheer/ui`)", and put
|
||||
`libs/beheer` on the Domein side of the same sentence.
|
||||
|
||||
**Do not change the story title.** This ticket fixes the documentation to match the code.
|
||||
|
||||
5. **No code behaviour changes anywhere.** Two comment words and two doc sentences.
|
||||
|
||||
## Files
|
||||
|
||||
- `libs/shared/src/ui/molecules/async/async.component.ts`
|
||||
- `libs/shared/src/layout/breadcrumb/breadcrumb.component.ts`
|
||||
- `CLAUDE.md`
|
||||
- `libs/shared/docs/layers.mdx`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add `Molecule: ` to `async.component.ts`'s class header (decision 2).
|
||||
2. Change `Chrome:` to `Molecule:` in `breadcrumb.component.ts`.
|
||||
3. Amend `CLAUDE.md:213` and `layers.mdx:13` per decision 4.
|
||||
4. `git add -A`, then run the acceptance commands.
|
||||
5. Update this ticket's `Status:` to `done`, the README's RD-28 row to `done`, and that row's
|
||||
`--full`? column to `yes` (see Verification).
|
||||
6. Commit all of it together.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
Measured against the tree before handover.
|
||||
|
||||
**The real check is the scan itself.** Run it; it must print nothing but `--- done`:
|
||||
|
||||
```bash
|
||||
for f in $(git ls-files 'libs/shared/src/ui/**/*.component.ts' 'libs/shared/src/layout/**/*.component.ts'); do
|
||||
d=$(dirname $f)
|
||||
title=$(grep -ho "title: 'Design System/[A-Za-z]*" $d/*.stories.ts 2>/dev/null | head -1 | sed "s/.*Design System\///")
|
||||
tag=$(grep -oE "\b(Atom|Molecule|Organism|Template|Chrome|Convenience|Devtool)s?:" $f | head -1 | tr -d ':')
|
||||
exp=$(echo "$title" | sed 's/s$//')
|
||||
[ "$tag" = "$exp" ] || echo "MISMATCH $f | title=${title:-NONE} | tag=${tag:-NONE}"
|
||||
done
|
||||
echo "--- done"
|
||||
```
|
||||
|
||||
The two edits, and the comment that must survive (decisions 1 and 2):
|
||||
|
||||
```bash
|
||||
git grep -c "Chrome:" -- libs/shared/src/layout/breadcrumb/breadcrumb.component.ts # is 1 -> MUST be 0
|
||||
git grep -c "Molecule:" -- libs/shared/src/ui/molecules/async/async.component.ts # is 0 -> MUST be 1
|
||||
git grep -c "Convenience:" -- libs/shared/src/ui/molecules/async/async.component.ts # is 1 -> MUST still be 1
|
||||
```
|
||||
|
||||
Nothing was stripped (decision 3):
|
||||
|
||||
```bash
|
||||
git grep -ohE "\b(Atom|Molecule|Organism|Template|Chrome|Convenience|Devtool)s?:" -- libs apps | wc -l # is 68 before the edits -> MUST be 69 after
|
||||
```
|
||||
|
||||
Note: the pre-edit tree measures 68. Decision 2 adds a brand-new `Molecule:` tag to
|
||||
`async.component.ts` (which had no tag at all), a net +1; the `Chrome:` → `Molecule:`
|
||||
rename in `breadcrumb.component.ts` is a wash. 68 unchanged would mean decision 2 was
|
||||
not applied. This is the same class of miss the README's ticket-writing rules log for
|
||||
RD-11/RD-14/RD-20/RD-27: add up every decision that touches the counted thing before
|
||||
writing the number.
|
||||
|
||||
The docs changed and the code did not (decision 4):
|
||||
|
||||
```bash
|
||||
grep -c 'or `libs/beheer/ui` component is titled' CLAUDE.md # is 1 -> MUST be 0
|
||||
grep -c '(or `libs/beheer/ui`)' libs/shared/docs/layers.mdx # is 1 -> MUST be 0
|
||||
git grep -c "Domein/Beheer/Stamdata Table Editor" -- libs/beheer # is 1 -> MUST still be 1
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run ci --full # exits 0
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
**`--full` is required, although the README's Order table leaves that column blank.** This
|
||||
ticket edits `layers.mdx`, and the README's own rule says an `.mdx` edit needs `--full`. Fix the
|
||||
column to `yes` in the same commit — the same correction RD-18 needed.
|
||||
|
||||
The tag count of 68 is the guard against a well-meant tidy-up. Decision 3 says keep every one.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Every other layer tag (decision 1). The scan found no other disagreement.
|
||||
- The `libs/beheer` story title (decision 4). The code is right.
|
||||
- Layer folders for `libs/beheer`. It is a context; it does not want them.
|
||||
- The dependency-cruiser ladder rules — RD-29, which depends on this ticket only by ordering.
|
||||
|
||||
## Risks
|
||||
|
||||
- **`async.component.ts` has two comments that look like tags.** Line 154's `Convenience:` is
|
||||
about the `ASYNC` array export and is accurate. Edit line 45's class header instead, and leave
|
||||
154 alone — the acceptance commands check both.
|
||||
- **Do not "fix" the beheer story title.** The instinct is to make the code match CLAUDE.md; here
|
||||
CLAUDE.md is the thing that is wrong.
|
||||
- **Do not delete a tag** (decision 3). The count must hold at 68.
|
||||
@@ -0,0 +1,143 @@
|
||||
# RD-29 — Enforce the atomic ladder in dependency-cruiser
|
||||
|
||||
Status: done
|
||||
Source: PLAN.md 4c
|
||||
|
||||
## Why
|
||||
|
||||
RD-27 made the ladder **expressible**: `libs/shared/src/ui/` is now `atoms/`, `molecules/`,
|
||||
`organisms/`. Nothing yet makes it **enforced** — an atom importing an organism compiles, lints,
|
||||
tests and ships.
|
||||
|
||||
This ticket adds the three rules. It is the payoff for the move, and PLAN calls it "the real
|
||||
prize".
|
||||
|
||||
## Read first
|
||||
|
||||
- `.dependency-cruiser.base.js:38` — `anyRoot`, and the atomic-layer rules that follow it. The
|
||||
new rules go beside them.
|
||||
- `.dependency-cruiser.base.js:93-99` — `ui-not-infrastructure`, the closest existing rule in
|
||||
shape.
|
||||
- PLAN.md 4c.
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
1. **Three rules, in `.dependency-cruiser.base.js`, forbidding upward edges only:**
|
||||
|
||||
| Rule name | from | to |
|
||||
| ----------------------------- | -------------------------------- | --------------------------------------------- |
|
||||
| `atoms-compose-nothing-above` | `^libs/shared/src/ui/atoms/` | `^libs/shared/src/ui/(molecules\|organisms)/` |
|
||||
| `molecules-below-organisms` | `^libs/shared/src/ui/molecules/` | `^libs/shared/src/ui/organisms/` |
|
||||
| `design-system-not-layout` | `^libs/shared/src/ui/` | `^libs/shared/src/layout/` |
|
||||
|
||||
`severity: 'error'`, each with a `comment` naming CLAUDE.md decision 2, matching the house
|
||||
style of every other rule in that file.
|
||||
|
||||
Literal `libs/shared/src/...` paths, not `anyRoot`: only `libs/shared` has layer folders. The
|
||||
rules are evaluated once per app cruise, which is harmless — the same tree, the same answer.
|
||||
|
||||
2. **Never "atoms are leaves". Same-layer edges are legitimate and four exist today:**
|
||||
|
||||
```
|
||||
atoms/masked-value -> atoms/button
|
||||
molecules/review-section -> molecules/data-block
|
||||
molecules/task-list -> molecules/choice-list
|
||||
molecules/task-list -> molecules/choice-link
|
||||
```
|
||||
|
||||
A rule forbidding an atom from importing any atom would fail on the first of these. Forbid
|
||||
the layers **above**, nothing else.
|
||||
|
||||
3. **No `pathNot` exemption for specs and stories. This corrects PLAN.**
|
||||
|
||||
PLAN says the exemption is needed "because `async.stories.ts` composes `skeleton`".
|
||||
Measured: `async` is a molecule and `skeleton` is an atom, so that edge points **downward**
|
||||
and is legal under decision 1. The example does not justify an exemption.
|
||||
|
||||
Measured further: **zero upward edges exist anywhere in `libs/shared/src/ui/`, in production
|
||||
code, specs and stories alike.** Nothing needs the exemption today.
|
||||
|
||||
So leave it out. An exemption that nothing needs is dead flexibility, and it silently widens
|
||||
the rule the moment someone does write an upward import in a story. If a story ever earns one
|
||||
— an atom's story demonstrating it inside a molecule is the plausible case — add the
|
||||
exemption then, with that real example in the comment.
|
||||
|
||||
4. **All three land green immediately.** This ticket adds enforcement and changes no application
|
||||
code. If `dep:check` fails after adding them, the rule is written wrongly — do not "fix" the
|
||||
application to satisfy it without saying so.
|
||||
|
||||
5. **Dependency-cruiser, not ESLint.** It is where every other boundary rule in this repository
|
||||
lives, and it emits the architecture graph. Do not add an ESLint variant.
|
||||
|
||||
## Files
|
||||
|
||||
- `.dependency-cruiser.base.js`
|
||||
|
||||
Nothing else. No application code changes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add the three rules per decision 1, beside the existing atomic-layer rules.
|
||||
2. Run `npm run dep:check` — it must pass.
|
||||
3. **Prove each rule bites** (see Verification). This is the point of the ticket.
|
||||
4. `git add -A`, then run the acceptance commands.
|
||||
5. Update this ticket's `Status:` to `done` and the README's RD-29 row to `done`.
|
||||
6. Commit all of it together.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
Measured against the tree before handover.
|
||||
|
||||
```bash
|
||||
git grep -c "atoms-compose-nothing-above" -- .dependency-cruiser.base.js # is 0 -> MUST be 1
|
||||
git grep -c "molecules-below-organisms" -- .dependency-cruiser.base.js # is 0 -> MUST be 1
|
||||
git grep -c "design-system-not-layout" -- .dependency-cruiser.base.js # is 0 -> MUST be 1
|
||||
```
|
||||
|
||||
The rules pass on the current tree, and no application file changed (decisions 4 and 5):
|
||||
|
||||
```bash
|
||||
npm run dep:check # exits 0
|
||||
git diff --cached --name-only | grep -v '^docs/' # MUST list only .dependency-cruiser.base.js
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run ci # exits 0
|
||||
```
|
||||
|
||||
`--full` is not required: no story, no `.mdx`, and nothing under `libs/shared/src/ui/**` is
|
||||
edited. The Order table's blank column is correct here.
|
||||
|
||||
## Verification
|
||||
|
||||
**A rule that matches nothing is worse than no rule, because it reads as protection.** Prove
|
||||
each of the three actually fires, one at a time:
|
||||
|
||||
1. Add a temporary import that violates it — for example, in
|
||||
`libs/shared/src/ui/atoms/button/button.component.ts`, import
|
||||
`@shared/ui/molecules/data-row/data-row.component`.
|
||||
2. Run `npm run dep:check` and confirm it fails, naming that rule.
|
||||
3. Revert the temporary import.
|
||||
|
||||
Do this for all three. Report which rule name each violation produced. **Do not commit any
|
||||
temporary import** — `git status` must be clean of them before you commit, and the acceptance
|
||||
command above checks that only the config file changed.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Layer folders or ladder rules for `libs/beheer`. It is a bounded context, not a design system
|
||||
(RD-28 settled this).
|
||||
- `layout/`'s internal structure. It is sanctioned to hold several layers.
|
||||
- Any rule about app contexts' own `ui/` folders. They have no layer folders by design — a
|
||||
context organism does not get its own bucket.
|
||||
- Changing an existing dependency-cruiser rule.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Do not add the spec/stories exemption out of habit** (decision 3). Two other rules in the
|
||||
file have one; these three do not need it, and the ticket explains why.
|
||||
- **Forbid upward, not sideways** (decision 2). Four same-layer edges exist and are correct.
|
||||
- **`from` must not match the layer it forbids.** `design-system-not-layout` starts at
|
||||
`^libs/shared/src/ui/`, which covers all three layer folders; that is intended.
|
||||
- **If `dep:check` goes red, suspect the rule, not the code** (decision 4). Nothing in the tree
|
||||
violates the ladder today.
|
||||
@@ -0,0 +1,100 @@
|
||||
# RD-38 — One member order for the three wizard containers
|
||||
|
||||
Status: done
|
||||
Source: user report — "the registratie-wizard component still doesn't look readable"
|
||||
|
||||
## Why
|
||||
|
||||
RD-22 and RD-23 split the intake and registratie wizards into step components. That brought
|
||||
every container under the 250-line budget, so `max-lines` reports nothing. The user read the
|
||||
result and still called it unreadable. Line count was never the problem.
|
||||
|
||||
Three problems remained, and the user named all three:
|
||||
|
||||
1. **The member order is scrambled, and it differs per file.** You bounce up and down to
|
||||
follow one thread, and the three containers cannot be compared side by side.
|
||||
2. **Pure logic sits in the container**, where it has no test.
|
||||
3. **Comments carry archaeology** — ticket numbers, history, and one comment that describes
|
||||
code which is no longer in the file.
|
||||
|
||||
RD-02 measures a file. This ticket is about what a file reads like at a fixed size.
|
||||
|
||||
## Evidence, before the change
|
||||
|
||||
- `registratie` declared `stepLabels`/`stepTitles` between the `seed` input and
|
||||
`state`/`dispatch`; `intake` declared them near the bottom; `herregistratie` declared them
|
||||
after `draftSync`. Three files, three orders for the same three roles.
|
||||
- `registratie` declared `draftSync` in the middle of a run of `computed`s.
|
||||
- `herregistratie`'s `snapshot` read `this.stepLabels.length` seven lines before
|
||||
`stepLabels` was declared.
|
||||
- `intake`'s class comment claimed answers persist to `sessionStorage`. Thirty lines below,
|
||||
another comment said draft-sync replaced it. The class comment was false.
|
||||
- All three cited "RD-05's tag-transition rule" in an identical sentence.
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
1. **One nine-section member order, identical in all three files.** Injected stores → inputs
|
||||
→ the store (`createStore`, `state`, `dispatch`) → static copy → state projections →
|
||||
controllers → shell wiring → event handlers → constructor and `restart`. Each section
|
||||
carries a `// --- <name> ---` header. Section 7's header already existed in all three
|
||||
files, so it is reused byte for byte.
|
||||
|
||||
2. **The store ⇄ `draftSync` cycle stays.** The store's effect map calls `this.draftSync`,
|
||||
and `draftSync`'s `snapshot` calls `this.state()`. Both are arrow functions that run after
|
||||
construction, so there is no temporal-dead-zone hazard; the cycle cannot be removed by
|
||||
reordering. One comment at the effect map names it.
|
||||
|
||||
3. **Extract `digitalDocumentIds` into `libs/shared/src/domain/upload.machine.ts`.** The
|
||||
"digital and finished uploading" filter was written out four times, in two shapes: mapped
|
||||
to ids in the two container snapshots, and as `.some(...)` inside two machines'
|
||||
`hasProgress`. One function serves all four, and it removes the `r.documentId!` non-null
|
||||
assertion from both containers. It joins an existing file beside `deliveryRefs`, and its
|
||||
spec joins the existing `deliveryRefs` block.
|
||||
|
||||
4. **Extract `diplomaMsg` into a new sibling of the step files.** `onDiplomaKeuze` was the
|
||||
fattest member in the three containers and had no test. It is pure: a `DuoLookupDto` and a
|
||||
selected id in, a machine message or `null` out. `HANDMATIG` does not move — the new file
|
||||
is its sibling and imports it exactly as the container did.
|
||||
|
||||
5. **`phase` stays in all three containers.** It maps this machine's tags onto the shell's
|
||||
`WizardPhase` vocabulary and composes a `$localize` failure message. That is a container's
|
||||
job. It cannot move to `domain/`: `WizardPhase` comes from an Angular component, and
|
||||
`domain/` points inward only. Moving it to a per-wizard sibling would add three files and
|
||||
three specs to relocate 17 readable lines each. Decision 1 already fixes what was wrong
|
||||
with it — it belongs in the shell-wiring section, and only two of three files had it there.
|
||||
|
||||
6. **The four `err*` computeds in `herregistratie` stay.** They are one-line projections
|
||||
feeding four template bindings. Folding them into one `errors()` would edit the template,
|
||||
which is behaviour-shaped work this ticket does not do.
|
||||
|
||||
7. **Comment policy, four rules.** Delete ticket references and keep the sentence (RD-18
|
||||
decision 1 stripped `WP-`/`RB-` for the same reason; `RD-` is the same debt). Delete
|
||||
history — a comment says what the code does now. Delete a comment that describes code
|
||||
which is not in the file. Keep a comment that states a current why: the `untracked`
|
||||
loop-avoidance notes, the `IntakePolicy.RejectIncompleteScholing` seam pointer, and the
|
||||
`demo-*` preview note all stay.
|
||||
|
||||
## Traps
|
||||
|
||||
- **`messages.en.xlf`.** Every `$localize` id in these files is translated. The reorder moves
|
||||
the copy arrays; it must not touch an id or its source text.
|
||||
- **`enabled: () => this.seed() === initial`** is a reference-identity check against the
|
||||
exported `initial` singleton. Never clone or rebuild it — breaking the identity turns
|
||||
draft-sync on inside Storybook and the tests.
|
||||
- **Public members are load-bearing.** `showcase/vragenlijst.section.ts` reads
|
||||
`IntakeWizardComponent.steps`; `showcase/form-machine.section.ts` reads
|
||||
`HerregistratieWizardComponent.state`. No `private`/`protected`/`readonly` modifier changes.
|
||||
- **`intake.machine.ts` carries `#region showcase:steps` markers** that feed `gen:snippets`.
|
||||
This ticket does not touch them.
|
||||
- **New specs change a generated document.** `scripts/ci-local.sh` regenerates
|
||||
`libs/shared/docs/behaviour-spec.mdx` and diffs it. Four new `it()` titles land there, so
|
||||
the regenerated file belongs in the same commit.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `npm run ci` passes.
|
||||
- Each container's member list and every modifier are unchanged:
|
||||
`diff <(git show HEAD:$f | grep -oE '^ (private|protected|readonly)? ?[a-zA-Z]+ *[=(]' | sort) <(...)`
|
||||
reports no difference for all three files.
|
||||
- `git grep -nE "\bRD-[0-9]+" -- 'apps/ssp/src/app/*/ui/*wizard*'` returns nothing.
|
||||
- `git grep -n "sessionStorage" -- 'apps/ssp/src/app/herregistratie/ui'` returns nothing.
|
||||
@@ -121,9 +121,9 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
||||
| RD-24 | `concepts.page` to 6 sections + `concept-card` + globals + code tokens | 02 | yes | done |
|
||||
| RD-25 | `org-template-editor` to `sample-letter.ts` + labels + 2 children | 02 | yes | done |
|
||||
| RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | done |
|
||||
| RD-27 | **The layer move:** 33 `git mv` + 28 specifiers + 8 MDX imports | 21 | yes | todo |
|
||||
| RD-28 | Layer-tag fixes + the `libs/beheer` title rule | 27 | | todo |
|
||||
| RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | todo |
|
||||
| RD-27 | **The layer move:** 33 `git mv` + 28 specifiers + 8 MDX imports | 21 | yes | done |
|
||||
| RD-28 | Layer-tag fixes + the `libs/beheer` title rule | 27 | yes | done |
|
||||
| RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | done |
|
||||
| RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | | todo |
|
||||
| RD-31 | `ARCHITECTURE.md` section 6a: symbols not lines, 2 dead paths, new names | 03, 08, 16 | | todo |
|
||||
| RD-32 | `fp-tea-atomic-design.md`: 11 broken paths + the broken anchor | 27 | | todo |
|
||||
@@ -131,6 +131,8 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
||||
| RD-34 | _(optional)_ `NO_SUBORGS`/`NO_TABLES` become `RemoteData.Empty` | 11 | | todo |
|
||||
| RD-35 | _(optional, last, alone)_ upload `type:` discriminant to `tag:` | 27 | | todo |
|
||||
| RD-36 | `ui/dashboard/` → `ui/overzicht-secties/` + 2 stale `dashboard.page` paths | 04 | yes | todo |
|
||||
| RD-37 | **a11y:** 5 suppressions name a closed ticket — decide the `li[…]` host | 01 | yes | todo |
|
||||
| RD-38 | One member order for the 3 wizard containers + 2 pure extractions | 22, 23 | | done |
|
||||
|
||||
The ID order already respects every dependency, so it is the recommended running order.
|
||||
|
||||
@@ -142,6 +144,12 @@ four import lines, and it collides with nothing else in the table — RD-27's mo
|
||||
`libs/shared/src/ui/`. Pull it forward into any short session. It is numbered last only because
|
||||
it was added after RD-04 shipped.
|
||||
|
||||
**RD-37 must run before RD-30**, despite its number. RD-30 archives `docs/project/backlog/`,
|
||||
and five of the paths it would have to rewrite point at `WP-11-markup-fidelity.md` from
|
||||
accessibility suppressions that RD-37 either deletes or re-aims. Doing RD-30 first means
|
||||
rewriting five paths that are about to change again — and enshrining a promise nobody owns.
|
||||
See PLAN.md Phase 5, item 0.
|
||||
|
||||
**Two ordering traps the table encodes.** RD-01 must precede RD-30, because RD-01 copies its
|
||||
ticket template out of the directory that RD-30 archives. And four tickets edit the same two
|
||||
documents in different sections — RD-09 rewrites the submit-idiom teaching, while RD-31 and
|
||||
@@ -209,6 +217,13 @@ Three rules when you write a ticket file, because the agent reads its ticket and
|
||||
number, say out loud what the command counts — declarations, call sites, or matching lines
|
||||
— and check that the decisions use that same meaning.
|
||||
|
||||
RD-27 then did it a fourth time, in the very next ticket written after that rule was
|
||||
recorded. Its decision 3 said the `@shared/ui/` occurrence count "must not change" at 200,
|
||||
while decision 4, nine lines below, mandated converting five relative imports into
|
||||
`@shared/ui/` aliases — five new occurrences by construction. The answer is 205.
|
||||
**When two decisions both touch the thing you are counting, add them up before you write
|
||||
the number.** Naming a habit does not retire it.
|
||||
|
||||
- RD-09 grepped `docs/ apps/ libs/ .claude/`, which also matched this backlog's own ticket
|
||||
files (they name the deleted method as the history of `done` work) and 22 gitignored
|
||||
abandoned worktrees. Satisfying it literally would have corrupted completed-ticket
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { AuditStore } from '@beheer/application/audit.store';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ChangeCounts, StamColumn, StamRow, StamTable, activeOn } from '@beheer/domain/stamdata';
|
||||
|
||||
interface DisplayRow {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { StamdataStore } from '@beheer/application/stamdata.store';
|
||||
import { StamdataTableEditorComponent } from '@beheer/ui/stamdata-table-editor/stamdata-table-editor.component';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
||||
import * as AlertStories from '../src/ui/alert/alert.stories';
|
||||
import * as FormFieldStories from '../src/ui/form-field/form-field.stories';
|
||||
import * as AlertStories from '../src/ui/atoms/alert/alert.stories';
|
||||
import * as FormFieldStories from '../src/ui/molecules/form-field/form-field.stories';
|
||||
|
||||
<Meta title="Foundations/Accessibility" />
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
||||
import * as ButtonStories from '../src/ui/button/button.stories';
|
||||
import * as FormFieldStories from '../src/ui/form-field/form-field.stories';
|
||||
import * as ButtonStories from '../src/ui/atoms/button/button.stories';
|
||||
import * as FormFieldStories from '../src/ui/molecules/form-field/form-field.stories';
|
||||
import * as PageShellStories from '../src/layout/page-shell/page-shell.stories';
|
||||
import * as DocumentUploadStories from '../src/ui/upload/document-upload/document-upload.stories';
|
||||
import * as DocumentUploadStories from '../src/ui/organisms/upload/document-upload/document-upload.stories';
|
||||
|
||||
<Meta title="Foundations/Atomic Design" />
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ tested where._
|
||||
|
||||
Every bullet below is a real test name from the suite — an `it()` title (frontend) or a test
|
||||
method name (backend), read as a sentence. Nothing here is hand-written prose: this page
|
||||
**is** the suite, reshaped for a business reader. 556 frontend behaviours across
|
||||
**is** the suite, reshaped for a business reader. 562 frontend behaviours across
|
||||
9 contexts; 261 backend behaviours across 42 test
|
||||
classes.
|
||||
|
||||
@@ -545,6 +545,12 @@ classes.
|
||||
- lists soort/waarvoor/status/referentie/ingediend, plus reason when rejected
|
||||
- reference falls back to em dash for a Concept
|
||||
|
||||
#### diplomaMsg
|
||||
|
||||
- resolves a known diploma into KiesDiploma with the server-derived beroep
|
||||
- resolves the manual sentinel into KiesHandmatig with the maximal question set
|
||||
- returns null for an unknown diploma id
|
||||
|
||||
#### findConcept
|
||||
|
||||
- returns the id of the existing Concept of the given type
|
||||
@@ -857,6 +863,12 @@ classes.
|
||||
- emits documentId for completed digital uploads and channel for post
|
||||
- omits digital categories with no completed upload
|
||||
|
||||
#### digitalDocumentIds
|
||||
|
||||
- returns the id of a completed digital upload
|
||||
- omits an upload that is still in flight
|
||||
- omits a category that the user delivers by post
|
||||
|
||||
#### flushPendingGuard
|
||||
|
||||
- flushes then allows navigation when a write is pending
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
||||
import * as AsyncStories from '../src/ui/async/async.stories';
|
||||
import * as AsyncStories from '../src/ui/molecules/async/async.stories';
|
||||
|
||||
<Meta title="Foundations/FP in the UI" />
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ This project is **domain-driven**: the code is organised first by **bounded cont
|
||||
with dependencies pointing inward. The Storybook sidebar is laid out to **be** that
|
||||
architecture, not just document it: **Foundations** (this curriculum) → **Design System**
|
||||
(reusable, domain-free) → **Domein** (the app-local DDD contexts). If a component lives
|
||||
under a context's `ui/`, it's in Domein; everything else in `libs/shared/ui`/`layout`
|
||||
(or `libs/beheer/ui`) is Design System. See [Atomic design](?path=/docs/foundations-atomic-design--docs)
|
||||
under a context's `ui/`, or under `libs/beheer/ui`, it's in Domein; everything else in
|
||||
`libs/shared/ui`/`layout` is Design System. See [Atomic design](?path=/docs/foundations-atomic-design--docs)
|
||||
for the Atoms → Molecules → Organisms → Templates ladder inside Design System.
|
||||
|
||||
## Two apps, two shared libraries
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
||||
import * as AsyncStories from '../src/ui/async/async.stories';
|
||||
import * as AsyncStories from '../src/ui/molecules/async/async.stories';
|
||||
|
||||
<Meta title="Foundations/RemoteData & Async" />
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
categorySatisfied,
|
||||
requiredCategoriesSatisfied,
|
||||
deliveryRefs,
|
||||
digitalDocumentIds,
|
||||
inFlight,
|
||||
rejectReason,
|
||||
planFileSelection,
|
||||
@@ -284,6 +285,28 @@ describe('deliveryRefs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('digitalDocumentIds', () => {
|
||||
it('returns the id of a completed digital upload', () => {
|
||||
let s = select(stateWith([cat({ categoryId: 'a' })]), 'a', 'u1');
|
||||
s = reduceUpload(s, { type: 'UploadComplete', localId: 'u1', documentId: 'doc1' });
|
||||
expect(digitalDocumentIds(s)).toEqual(['doc1']);
|
||||
});
|
||||
|
||||
it('omits an upload that is still in flight', () => {
|
||||
const s = select(stateWith([cat({ categoryId: 'a' })]), 'a', 'u1'); // still queued
|
||||
expect(digitalDocumentIds(s)).toEqual([]);
|
||||
});
|
||||
|
||||
it('omits a category that the user delivers by post', () => {
|
||||
let s = stateWith([cat({ categoryId: 'a' }), cat({ categoryId: 'b' })], {
|
||||
deliveryChannel: { b: 'post' },
|
||||
});
|
||||
s = select(s, 'a', 'u1');
|
||||
s = reduceUpload(s, { type: 'UploadComplete', localId: 'u1', documentId: 'doc1' });
|
||||
expect(digitalDocumentIds(s)).toEqual(['doc1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejectReason', () => {
|
||||
it('rejects a disallowed type', () => {
|
||||
expect(
|
||||
|
||||
@@ -312,6 +312,14 @@ export function deliveryRefs(
|
||||
return refs;
|
||||
}
|
||||
|
||||
/** Ids of the documents that the user delivers digitally and that finished uploading.
|
||||
A category set to post, or one whose upload is still in flight, contributes nothing. */
|
||||
export function digitalDocumentIds(s: UploadState): string[] {
|
||||
return deliveryRefs(s)
|
||||
.filter((r) => r.channel === 'digital' && r.documentId)
|
||||
.map((r) => r.documentId as string);
|
||||
}
|
||||
|
||||
/** Used by the shell to find what to poll on return: still-in-flight uploads. */
|
||||
export const inFlight = (s: UploadState): Upload[] =>
|
||||
s.uploads.filter((u) => u.status.type === 'queued' || u.status.type === 'uploading');
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface BreadcrumbItem {
|
||||
link?: string; // omit on the current (last) page
|
||||
}
|
||||
|
||||
/** Chrome: breadcrumb navigation, styled for the CIBG titlebar (`.titlebar .title`) —
|
||||
/** Molecule: breadcrumb navigation, styled for the CIBG titlebar (`.titlebar .title`) —
|
||||
plain links with a chevron `::after` from the CIBG Icons font, current page as an
|
||||
unlinked, bold span. Domain-free — the caller supplies the trail. */
|
||||
@Component({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { LinkComponent } from '@shared/ui/link/link.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { LinkComponent } from '@shared/ui/atoms/link/link.component';
|
||||
|
||||
/** Template: standard page body — optional back-link, a heading, optional intro,
|
||||
and projected content. The breadcrumb lives in the site header (blue bar), so
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig, moduleMetadata } from '@storybook/angular';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { PageShellComponent } from './page-shell.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
const meta: Meta<PageShellComponent> = {
|
||||
title: 'Design System/Templates/PageShell',
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SpinnerComponent } from '@shared/ui/spinner/spinner.component';
|
||||
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SpinnerComponent } from '@shared/ui/atoms/spinner/spinner.component';
|
||||
import { StepperComponent } from '@shared/ui/molecules/stepper/stepper.component';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
|
||||
/** CIBG procesnavigatie primary-button copy for a non-final step: "Naar stap 2 - Werk".
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
/**
|
||||
* Atom: a possibly-masked sensitive value (BSN, BIG-nummer, …) with an optional, audited
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user