refactor: strip WP-/RB- ticket refs from apps and libs (RD-18)
204 WP-NN/RB-NN comments named a closed ticket instead of the code they sit next to. git blame already records history and stays correct when code moves; the comment does not. This sweep removes the reference and keeps the sentence, across 95 files in apps/ and libs/ plus the behaviour-spec generator's header text. Eleven references stay: five story files justify an a11y disable per the README's rule, and one line in a11y.mdx documents that convention. Two sentences needed a rewrite, not a deletion, so the reference's meaning survives its removal. behaviour-spec.mdx is regenerated, not hand-edited. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'aanvraag/:id',
|
path: 'aanvraag/:id',
|
||||||
// Same capability the werkvoorraad list itself is gated by (WP-64/65) — the
|
// Same capability the werkvoorraad list itself is gated by — the
|
||||||
// detail page is reachable only from a row already filtered to that capability.
|
// detail page is reachable only from a row already filtered to that capability.
|
||||||
canActivate: [capabilityGuard('aanvraag:beoordelen')],
|
canActivate: [capabilityGuard('aanvraag:beoordelen')],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
@@ -36,14 +36,14 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'beheer/audit',
|
path: 'beheer/audit',
|
||||||
// Admin-only authz/PII-reveal audit trail (WP-41/42). capabilityGuard denies-by-default
|
// Admin-only authz/PII-reveal audit trail. capabilityGuard denies-by-default
|
||||||
// unless GET /me resolved `cases:manage` (reused for audit read). Backend re-enforces.
|
// unless GET /me resolved `cases:manage` (reused for audit read). Backend re-enforces.
|
||||||
canActivate: [capabilityGuard('cases:manage')],
|
canActivate: [capabilityGuard('cases:manage')],
|
||||||
loadComponent: () => import('@beheer/ui/audit.page').then((m) => m.AuditPage),
|
loadComponent: () => import('@beheer/ui/audit.page').then((m) => m.AuditPage),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'beheer/functies',
|
path: 'beheer/functies',
|
||||||
// Admin-only feature-flag toggles (WP-47), gated by `flags:manage`.
|
// Admin-only feature-flag toggles, gated by `flags:manage`.
|
||||||
canActivate: [capabilityGuard('flags:manage')],
|
canActivate: [capabilityGuard('flags:manage')],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
|
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MEDEWERKER_ID, currentRollen } from './medewerker';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Infrastructure: resolves the current medewerker identity into a `Principal`
|
* Infrastructure: resolves the current medewerker identity into a `Principal`
|
||||||
* (ADR-C-004/RB-13). Stands in for a real employee-SSO redirect flow (ADR-0002 §3,
|
* (ADR-C-004). Stands in for a real employee-SSO redirect flow (ADR-0002 §3,
|
||||||
* "out of scope here") — there is no credential to enter and, unlike `DigidAdapter`'s
|
* "out of scope here") — there is no credential to enter and, unlike `DigidAdapter`'s
|
||||||
* BSN check, no format to reject, so `authenticate()` takes no input and returns the
|
* BSN check, no format to reject, so `authenticate()` takes no input and returns the
|
||||||
* `Principal` directly rather than a `Result` with an error variant that can never
|
* `Principal` directly rather than a `Result` with an error variant that can never
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Component, output } from '@angular/core';
|
|||||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Organism: employee-SSO-style mock login (ADR-C-004/RB-13). No real auth — and,
|
* Organism: employee-SSO-style mock login (ADR-C-004). No real auth — and,
|
||||||
* unlike the SSP's DigiD form, no credential to enter at all: a Behandelaar has no
|
* unlike the SSP's DigiD form, no credential to enter at all: a Behandelaar has no
|
||||||
* BSN, and this app has no password of its own to check either way. There is
|
* BSN, and this app has no password of its own to check either way. There is
|
||||||
* nothing to compose beyond one button, which is itself evidence for the ADR — the
|
* nothing to compose beyond one button, which is itself evidence for the ADR — the
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
|
|
||||||
type Err = Error | undefined;
|
type Err = Error | undefined;
|
||||||
|
|
||||||
/** One aanvraag's beoordeling detail (WP-65) — a root singleton like `WerkvoorraadStore`.
|
/** One aanvraag's beoordeling detail — a root singleton like `WerkvoorraadStore`.
|
||||||
Keyed by id: navigating to a different case resets to Loading. */
|
Keyed by id: navigating to a different case resets to Loading. */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class BeoordelingStore {
|
export class BeoordelingStore {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
|
|
||||||
type Err = Error | undefined;
|
type Err = Error | undefined;
|
||||||
|
|
||||||
/** The behandelaar's queue (WP-64) — a root singleton like `AdminCasesStore`'s ssp
|
/** The behandelaar's queue — a root singleton like `AdminCasesStore`'s ssp
|
||||||
counterpart. Fetch + parse at the trust boundary, publish as RemoteData. */
|
counterpart. Fetch + parse at the trust boundary, publish as RemoteData. */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class WerkvoorraadStore {
|
export class WerkvoorraadStore {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { formatDatumNl } from '@shared/kernel/datum';
|
|||||||
import { AanvraagType } from './werkvoorraad-item';
|
import { AanvraagType } from './werkvoorraad-item';
|
||||||
import { BeoordelingStatus, BeoordelingView } from './beoordeling';
|
import { BeoordelingStatus, BeoordelingView } from './beoordeling';
|
||||||
|
|
||||||
/** View-model mapping shared by the werkvoorraad list (WP-64) and the beoordeling
|
/** View-model mapping shared by the werkvoorraad list and the beoordeling
|
||||||
detail screen (WP-65): type/status → labels. Pure, no Angular. Lives here (not in
|
detail screen: type/status → labels. Pure, no Angular. Lives here (not in
|
||||||
`werkvoorraad-item-view.ts`) because `BeoordelingStatus` is the wider of the two
|
`werkvoorraad-item-view.ts`) because `BeoordelingStatus` is the wider of the two
|
||||||
status unions — `werkvoorraad-item-view.ts` re-exports these for its own use. */
|
status unions — `werkvoorraad-item-view.ts` re-exports these for its own use. */
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AanvraagType } from './werkvoorraad-item';
|
import { AanvraagType } from './werkvoorraad-item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A case's full status lifecycle as the beoordeling detail screen sees it (WP-65) —
|
* A case's full status lifecycle as the beoordeling detail screen sees it —
|
||||||
* wider than `WerkvoorraadStatus` (WP-64), which only ever sees the two "still open"
|
* wider than `WerkvoorraadStatus`, which only ever sees the two "still open"
|
||||||
* tags. This is the same five-tag union ssp's `AanvraagStatus` models (minus `Concept`
|
* tags. This is the same five-tag union ssp's `AanvraagStatus` models (minus `Concept`
|
||||||
* — the detail endpoint 404s a Concept, it isn't a case a behandelaar can treat yet).
|
* — the detail endpoint 404s a Concept, it isn't a case a behandelaar can treat yet).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Result, assertNever } from '@shared/kernel/fp';
|
import { Result, assertNever } from '@shared/kernel/fp';
|
||||||
|
|
||||||
/** The three actions the beoordeling screen offers a behandelaar (WP-65b) — mirrors the
|
/** The three actions the beoordeling screen offers a behandelaar — mirrors the
|
||||||
backend's `Besluit` enum member names 1:1 (the wire convention: a string, not a raw
|
backend's `Besluit` enum member names 1:1 (the wire convention: a string, not a raw
|
||||||
enum — see `RecordBesluitRequest`). */
|
enum — see `RecordBesluitRequest`). */
|
||||||
const BESLUIT_TAGS = ['Goedkeuren', 'Afwijzen', 'MeerInfoOpvragen'] as const;
|
const BESLUIT_TAGS = ['Goedkeuren', 'Afwijzen', 'MeerInfoOpvragen'] as const;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* A queue entry as the behandelportal sees it (WP-64) — the parsed, domain-side view
|
* A queue entry as the behandelportal sees it — the parsed, domain-side view
|
||||||
* of the backend's cross-owner `GET /werkvoorraad`. Pure types, no Angular.
|
* of the backend's cross-owner `GET /werkvoorraad`. Pure types, no Angular.
|
||||||
*
|
*
|
||||||
* The status union is narrower than the SSP's full `AanvraagStatus` (ssp's
|
* The status union is narrower than the SSP's full `AanvraagStatus` (ssp's
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { AanvraagType } from '@behandeling/domain/werkvoorraad-item';
|
import { AanvraagType } from '@behandeling/domain/werkvoorraad-item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infrastructure adapter for the beoordeling detail read (WP-65) — the only place its
|
* Infrastructure adapter for the beoordeling detail read — the only place its
|
||||||
* HTTP lives (ADR-0001 anti-corruption boundary). The untrusted response is validated +
|
* HTTP lives (ADR-0001 anti-corruption boundary). The untrusted response is validated +
|
||||||
* mapped to domain by the parse* boundary below.
|
* mapped to domain by the parse* boundary below.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ApiClient } from '@shared/infrastructure/api-client';
|
|||||||
import { Valid } from '@behandeling/domain/besluit.machine';
|
import { Valid } from '@behandeling/domain/besluit.machine';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infrastructure adapter for recording a behandelaar's decision (WP-65b) — the single
|
* Infrastructure adapter for recording a behandelaar's decision — the single
|
||||||
* place its HTTP lives. No return value: a successful call means the server accepted
|
* place its HTTP lives. No return value: a successful call means the server accepted
|
||||||
* the transition; the caller reloads `BeoordelingStore` to see the new status (the
|
* the transition; the caller reloads `BeoordelingStore` to see the new status (the
|
||||||
* server, not this adapter, re-validates and is the authority).
|
* server, not this adapter, re-validates and is the authority).
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@behandeling/domain/werkvoorraad-item';
|
} from '@behandeling/domain/werkvoorraad-item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infrastructure adapter for the behandelportal's queue read (WP-64) — the only
|
* Infrastructure adapter for the behandelportal's queue read — the only
|
||||||
* place its HTTP lives (ADR-0001 anti-corruption boundary). The untrusted response
|
* place its HTTP lives (ADR-0001 anti-corruption boundary). The untrusted response
|
||||||
* is validated + mapped to the (narrower) queue domain shape by the parse* boundary
|
* is validated + mapped to the (narrower) queue domain shape by the parse* boundary
|
||||||
* below; a case whose status isn't `Ingediend`/`InBehandeling` is a parse error, not
|
* below; a case whose status isn't `Ingediend`/`InBehandeling` is a parse error, not
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { Component, input } from '@angular/core';
|
import { Component, input } from '@angular/core';
|
||||||
import { BeoordelingDocument } from '@behandeling/domain/beoordeling';
|
import { BeoordelingDocument } from '@behandeling/domain/beoordeling';
|
||||||
|
|
||||||
/** Organism: the documents linked to an aanvraag (WP-65) — plain links to the existing
|
/** Organism: the documents linked to an aanvraag — plain links to the existing
|
||||||
(pre-existing, unauthenticated — same as ssp's own document previews) content
|
(pre-existing, unauthenticated — same as ssp's own document previews) content
|
||||||
endpoint. No new shared atom: a context-local list, not a reusable building block. */
|
endpoint. No new shared atom: a context-local list, not a reusable building block. */
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { BeoordelingDocumentenComponent } from '@behandeling/ui/beoordeling-docu
|
|||||||
import { BesluitFormComponent } from '@behandeling/ui/besluit-form/besluit-form.component';
|
import { BesluitFormComponent } from '@behandeling/ui/besluit-form/besluit-form.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page: one aanvraag's beoordeling detail (WP-65). The werkvoorraad list (WP-64) links
|
* Page: one aanvraag's beoordeling detail. The werkvoorraad list links
|
||||||
* here. `canBesluiten` (server-computed, ADR-0001) gates the decision form (WP-65b) —
|
* here. `canBesluiten` (server-computed, ADR-0001) gates the decision form —
|
||||||
* the page never recomputes the lifecycle itself. On a recorded decision the form emits
|
* the page never recomputes the lifecycle itself. On a recorded decision the form emits
|
||||||
* `decided`, and the page just reloads (the server is the authority on the new status).
|
* `decided`, and the page just reloads (the server is the authority on the new status).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { BesluitState, BesluitMsg, initial, reduce } from '@behandeling/domain/b
|
|||||||
import { createSubmitBesluit } from '@behandeling/application/submit-besluit';
|
import { createSubmitBesluit } from '@behandeling/application/submit-besluit';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Organism: the decision form (WP-65b) — goedkeuren/afwijzen/meer-info-opvragen. Same
|
* Organism: the decision form — goedkeuren/afwijzen/meer-info-opvragen. Same
|
||||||
* idiom as every other form in this house (`change-request-form`): all state in one
|
* idiom as every other form in this house (`change-request-form`): all state in one
|
||||||
* signal driven by the pure `reduce` (besluit.machine.ts), submitted via a `submit-*`
|
* signal driven by the pure `reduce` (besluit.machine.ts), submitted via a `submit-*`
|
||||||
* command returning `Result`. The server re-validates the transition and is the
|
* command returning `Result`. The server re-validates the transition and is the
|
||||||
|
|||||||
+2
-2
@@ -4,9 +4,9 @@ import { ApplicationLinkComponent } from '@shared/ui/application-link/applicatio
|
|||||||
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
|
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
|
||||||
import { werkvoorraadRow } from '@behandeling/domain/werkvoorraad-item-view';
|
import { werkvoorraadRow } from '@behandeling/domain/werkvoorraad-item-view';
|
||||||
|
|
||||||
/** Organism: the behandelaar's queue as CIBG "aanvragen" rows (WP-64) — composition
|
/** Organism: the behandelaar's queue as CIBG "aanvragen" rows — composition
|
||||||
of the two existing shared/ui molecules, no new atom. Each row links to the
|
of the two existing shared/ui molecules, no new atom. Each row links to the
|
||||||
beoordeling detail page (WP-65). */
|
beoordeling detail page. */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-werkvoorraad-list',
|
selector: 'app-werkvoorraad-list',
|
||||||
imports: [ApplicationListComponent, ApplicationLinkComponent],
|
imports: [ApplicationListComponent, ApplicationLinkComponent],
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import { WerkvoorraadStore } from '@behandeling/application/werkvoorraad.store';
|
|||||||
import { WerkvoorraadListComponent } from '@behandeling/ui/werkvoorraad-list/werkvoorraad-list.component';
|
import { WerkvoorraadListComponent } from '@behandeling/ui/werkvoorraad-list/werkvoorraad-list.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page: the behandelaar's werkvoorraad (WP-64) — the behandelportal's landing page.
|
* Page: the behandelaar's werkvoorraad — the behandelportal's landing page.
|
||||||
* Deny-by-default capability gate (`aanvraag:beoordelen`), same idiom as ssp's
|
* Deny-by-default capability gate (`aanvraag:beoordelen`), same idiom as ssp's
|
||||||
* AdminCasesPage: a denial alert for a non-behandelaar, the queue for one. Opening
|
* AdminCasesPage: a denial alert for a non-behandelaar, the queue for one. Opening
|
||||||
* a case's detail is out of scope here (WP-65).
|
* a case's detail is out of scope here.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-werkvoorraad-page',
|
selector: 'app-werkvoorraad-page',
|
||||||
@@ -69,7 +69,7 @@ export class WerkvoorraadPage {
|
|||||||
private loadRequested = false;
|
private loadRequested = false;
|
||||||
constructor() {
|
constructor() {
|
||||||
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise) —
|
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise) —
|
||||||
// same guard-against-the-loop idiom as AdminCasesPage (WP-26 lesson).
|
// same guard-against-the-loop idiom as AdminCasesPage.
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (this.canBeoordelen() && !this.loadRequested) {
|
if (this.canBeoordelen() && !this.loadRequested) {
|
||||||
this.loadRequested = true;
|
this.loadRequested = true;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const NAV_ITEMS: readonly HeaderNavItem[] = [
|
|||||||
|
|
||||||
/** This app's admin pages — provided to the shared site header via HEADER_ADMIN_LINKS.
|
/** This app's admin pages — provided to the shared site header via HEADER_ADMIN_LINKS.
|
||||||
No huisstijl (that's the SSP's brief context) or zaken entry — inherited as-is from
|
No huisstijl (that's the SSP's brief context) or zaken entry — inherited as-is from
|
||||||
WP-61's bootstrap trim, not revisited by this migration. */
|
the bootstrap trim, not revisited by this migration. */
|
||||||
export const ADMIN_LINKS: readonly AdminLink[] = [
|
export const ADMIN_LINKS: readonly AdminLink[] = [
|
||||||
{
|
{
|
||||||
label: $localize`:@@header.nav.stamdata:Stamdata`,
|
label: $localize`:@@header.nav.stamdata:Stamdata`,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'brief/huisstijl',
|
path: 'brief/huisstijl',
|
||||||
// Admin-only org-template editor (WP-26): capabilityGuard denies-by-default
|
// Admin-only org-template editor: capabilityGuard denies-by-default
|
||||||
// unless GET /me resolved `orgtemplate:edit` (Admin role). Backend re-enforces
|
// unless GET /me resolved `orgtemplate:edit` (Admin role). Backend re-enforces
|
||||||
// via the OrgAdmin gate — the guard just avoids loading a page that would 403.
|
// via the OrgAdmin gate — the guard just avoids loading a page that would 403.
|
||||||
canActivate: [capabilityGuard('orgtemplate:edit')],
|
canActivate: [capabilityGuard('orgtemplate:edit')],
|
||||||
@@ -78,7 +78,7 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'beheer/zaken',
|
path: 'beheer/zaken',
|
||||||
// Admin-only cases overview + delete (WP-36): capabilityGuard denies-by-default
|
// Admin-only cases overview + delete: capabilityGuard denies-by-default
|
||||||
// unless GET /me resolved `cases:manage` (Admin role). Backend re-enforces via the
|
// unless GET /me resolved `cases:manage` (Admin role). Backend re-enforces via the
|
||||||
// CasesAdmin gate — the guard just avoids loading a page that would 403. The page
|
// CasesAdmin gate — the guard just avoids loading a page that would 403. The page
|
||||||
// lives in registratie/ui (which owns the Aanvraag aggregate); routed under /beheer.
|
// lives in registratie/ui (which owns the Aanvraag aggregate); routed under /beheer.
|
||||||
@@ -88,14 +88,14 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'beheer/audit',
|
path: 'beheer/audit',
|
||||||
// Admin-only authz/PII-reveal audit trail (WP-41/42). capabilityGuard denies-by-default
|
// Admin-only authz/PII-reveal audit trail. capabilityGuard denies-by-default
|
||||||
// unless GET /me resolved `cases:manage` (reused for audit read). Backend re-enforces.
|
// unless GET /me resolved `cases:manage` (reused for audit read). Backend re-enforces.
|
||||||
canActivate: [capabilityGuard('cases:manage')],
|
canActivate: [capabilityGuard('cases:manage')],
|
||||||
loadComponent: () => import('@beheer/ui/audit.page').then((m) => m.AuditPage),
|
loadComponent: () => import('@beheer/ui/audit.page').then((m) => m.AuditPage),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'beheer/functies',
|
path: 'beheer/functies',
|
||||||
// Admin-only feature-flag toggles (WP-47), gated by `flags:manage`.
|
// Admin-only feature-flag toggles, gated by `flags:manage`.
|
||||||
canActivate: [capabilityGuard('flags:manage')],
|
canActivate: [capabilityGuard('flags:manage')],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
|
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Principal } from '../domain/principal';
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class DigidAdapter {
|
export class DigidAdapter {
|
||||||
// ponytail: fake DigiD — any elfproef-valid BSN authenticates to a fixed identity.
|
// ponytail: fake DigiD — any elfproef-valid BSN authenticates to a fixed identity.
|
||||||
// Real BSN validation (parseBsn, WP-40) is the trust boundary; swap the fixed identity
|
// Real BSN validation (parseBsn) is the trust boundary; swap the fixed identity
|
||||||
// for a real OIDC redirect flow when there's an IdP.
|
// for a real OIDC redirect flow when there's an IdP.
|
||||||
async authenticate(bsn: string): Promise<Result<string, Principal>> {
|
async authenticate(bsn: string): Promise<Result<string, Principal>> {
|
||||||
const r = parseBsn(bsn);
|
const r = parseBsn(bsn);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const caseContext: CaseContext = {
|
|||||||
|
|
||||||
const view: BriefView = { brief, availablePassages: [], decisions, orgTemplate, caseContext };
|
const view: BriefView = { brief, availablePassages: [], decisions, orgTemplate, caseContext };
|
||||||
|
|
||||||
/** A recording fake of BLOB_PRESENTER (RB-28/TE-006) — records every call instead of
|
/** A recording fake of BLOB_PRESENTER (TE-006) — records every call instead of
|
||||||
touching the DOM, so a spec can assert a command's success path directly. */
|
touching the DOM, so a spec can assert a command's success path directly. */
|
||||||
function fakeBlobPresenter() {
|
function fakeBlobPresenter() {
|
||||||
const opened: Blob[] = [];
|
const opened: Blob[] = [];
|
||||||
@@ -158,7 +158,7 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- WP-27: undo/redo history + rejection diff ---
|
// --- Undo/redo history + rejection diff ---
|
||||||
|
|
||||||
function block(id: string, text: string): LetterBlock {
|
function block(id: string, text: string): LetterBlock {
|
||||||
return {
|
return {
|
||||||
@@ -308,7 +308,7 @@ describe('BriefStore rejection diff', () => {
|
|||||||
describe('BriefStore.previewLetter', () => {
|
describe('BriefStore.previewLetter', () => {
|
||||||
afterEach(() => vi.restoreAllMocks());
|
afterEach(() => vi.restoreAllMocks());
|
||||||
|
|
||||||
it('opens the composed letter via BLOB_PRESENTER on success (RB-28)', async () => {
|
it('opens the composed letter via BLOB_PRESENTER on success', async () => {
|
||||||
const { presenter, opened } = fakeBlobPresenter();
|
const { presenter, opened } = fakeBlobPresenter();
|
||||||
const store = setup(
|
const store = setup(
|
||||||
{
|
{
|
||||||
@@ -412,11 +412,11 @@ describe('BriefStore.flushPending (CanDeactivate guard / beforeunload)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- RB-22 (CQ-007 expand half): a 404 from GET /brief tolerates by calling the
|
// --- CQ-007's expand half: a 404 from GET /brief tolerates by calling the
|
||||||
// existing reset() command, exactly once. Today's backend never 404s (RB-23 adds
|
// existing reset() command, exactly once. Today's backend never 404s yet;
|
||||||
// that); this fake adapter is what exercises the branch until then. ---
|
// this fake adapter is what exercises the branch until then. ---
|
||||||
|
|
||||||
describe('BriefStore.load — 404 tolerance (RB-22)', () => {
|
describe('BriefStore.load — 404 tolerance', () => {
|
||||||
const notFound: Result<BriefLoadFailure, BriefView> = { ok: false, error: { tag: 'notFound' } };
|
const notFound: Result<BriefLoadFailure, BriefView> = { ok: false, error: { tag: 'notFound' } };
|
||||||
const resetOk: Result<string, BriefView> = { ok: true, value: view };
|
const resetOk: Result<string, BriefView> = { ok: true, value: view };
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export class BriefStore implements PendingSave {
|
|||||||
/** Surfaced autosave state for the indicator + aria-live region. */
|
/** Surfaced autosave state for the indicator + aria-live region. */
|
||||||
readonly saveState = signal<SaveState>({ tag: 'Idle' });
|
readonly saveState = signal<SaveState>({ tag: 'Idle' });
|
||||||
|
|
||||||
/** Undo/redo is SHELL state, not machine state (WP-27): a `createHistory` stack of
|
/** Undo/redo is SHELL state, not machine state: a `createHistory` stack of
|
||||||
`Brief` snapshots (WP-31 extracted the mechanics). Only CONTENT edits are recorded
|
`Brief` snapshots (the mechanics live in a shared helper). Only CONTENT edits are recorded
|
||||||
(they flow through `edit()`); status transitions never enter history, or undo would
|
(they flow through `edit()`); status transitions never enter history, or undo would
|
||||||
replay workflow state. Restore re-dispatches the existing `Seed` Msg — zero machine
|
replay workflow state. Restore re-dispatches the existing `Seed` Msg — zero machine
|
||||||
changes. */
|
changes. */
|
||||||
@@ -64,7 +64,7 @@ export class BriefStore implements PendingSave {
|
|||||||
readonly canUndo = this.history.canUndo;
|
readonly canUndo = this.history.canUndo;
|
||||||
readonly canRedo = this.history.canRedo;
|
readonly canRedo = this.history.canRedo;
|
||||||
|
|
||||||
/** The letter as it stood when it was REJECTED, captured shell-side (WP-27). The
|
/** The letter as it stood when it was REJECTED, captured shell-side. The
|
||||||
approver diffs it against the resubmitted letter. POC limit: in-memory only, so a
|
approver diffs it against the resubmitted letter. POC limit: in-memory only, so a
|
||||||
full page reload loses it — a real system would persist the rejected revision. */
|
full page reload loses it — a real system would persist the rejected revision. */
|
||||||
private rejectionSnapshot = signal<Brief | null>(null);
|
private rejectionSnapshot = signal<Brief | null>(null);
|
||||||
@@ -81,7 +81,7 @@ export class BriefStore implements PendingSave {
|
|||||||
);
|
);
|
||||||
readonly hasRejectionDiff = computed(() => this.blockDiffs().size > 0);
|
readonly hasRejectionDiff = computed(() => this.blockDiffs().size > 0);
|
||||||
|
|
||||||
/** The org template the letter renders with (WP-24). Server-owned appearance data,
|
/** The org template the letter renders with. Server-owned appearance data,
|
||||||
not letter state — held beside the machine, never inside it (`brief.machine.ts`
|
not letter state — held beside the machine, never inside it (`brief.machine.ts`
|
||||||
stays untouched by design). Set from every server view that carries it. */
|
stays untouched by design). Set from every server view that carries it. */
|
||||||
readonly orgTemplate = signal<OrgTemplate | null>(null);
|
readonly orgTemplate = signal<OrgTemplate | null>(null);
|
||||||
@@ -125,7 +125,7 @@ export class BriefStore implements PendingSave {
|
|||||||
return !!b && canSubmit(b) && !hasBlockingErrors(this.diagnostics());
|
return !!b && canSubmit(b) && !hasBlockingErrors(this.diagnostics());
|
||||||
});
|
});
|
||||||
|
|
||||||
/** True once a 404-triggered recovery has been attempted (RB-22, CQ-007's expand
|
/** True once a 404-triggered recovery has been attempted (CQ-007's expand
|
||||||
half — see `recoverFromMissingBrief`). This is the structural once-only bound:
|
half — see `recoverFromMissingBrief`). This is the structural once-only bound:
|
||||||
a repeated 404 falls straight to the `error` branch below and can never reach
|
a repeated 404 falls straight to the `error` branch below and can never reach
|
||||||
`adapter.reset()` a second time, regardless of how many times `load()` runs. */
|
`adapter.reset()` a second time, regardless of how many times `load()` runs. */
|
||||||
@@ -200,7 +200,7 @@ export class BriefStore implements PendingSave {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 600ms debounced autosave (the server is the store of record). Timer mechanics live in
|
// 600ms debounced autosave (the server is the store of record). Timer mechanics live in
|
||||||
// the shared helper; `flushSave` below is the store-specific write + save-state (WP-31).
|
// the shared helper; `flushSave` below is the store-specific write + save-state.
|
||||||
private debouncedSave = createDebouncedSave({
|
private debouncedSave = createDebouncedSave({
|
||||||
canSave: () => this.canEdit(),
|
canSave: () => this.canEdit(),
|
||||||
flush: () => this.flushSave(),
|
flush: () => this.flushSave(),
|
||||||
@@ -223,7 +223,7 @@ export class BriefStore implements PendingSave {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retry a failed autosave — reuses the existing flush path, no new state (WP-27). */
|
/** Retry a failed autosave — reuses the existing flush path, no new state. */
|
||||||
retrySave() {
|
retrySave() {
|
||||||
void this.flushSave();
|
void this.flushSave();
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@ export class BriefStore implements PendingSave {
|
|||||||
this.store.dispatch({ tag: 'Approved', by: s.approvedBy, at: s.approvedAt, decisions });
|
this.store.dispatch({ tag: 'Approved', by: s.approvedBy, at: s.approvedAt, decisions });
|
||||||
break;
|
break;
|
||||||
case 'rejected':
|
case 'rejected':
|
||||||
// Capture the letter as-rejected for the resubmission diff (WP-27). This is the
|
// Capture the letter as-rejected for the resubmission diff. This is the
|
||||||
// "before" snapshot the approver later compares against.
|
// "before" snapshot the approver later compares against.
|
||||||
this.rejectionSnapshot.set(brief);
|
this.rejectionSnapshot.set(brief);
|
||||||
this.store.dispatch({
|
this.store.dispatch({
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const subOrgs: SubOrgSummary[] = [
|
|||||||
{ subOrgId: 'cibg-registers', orgName: 'CIBG', publishedVersion: 1 },
|
{ subOrgId: 'cibg-registers', orgName: 'CIBG', publishedVersion: 1 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** A recording fake of BLOB_PRESENTER (RB-28/TE-006) — records every call instead of
|
/** A recording fake of BLOB_PRESENTER (TE-006) — records every call instead of
|
||||||
touching the DOM, so a spec can assert a command's success path directly. */
|
touching the DOM, so a spec can assert a command's success path directly. */
|
||||||
function fakeBlobPresenter() {
|
function fakeBlobPresenter() {
|
||||||
const opened: Blob[] = [];
|
const opened: Blob[] = [];
|
||||||
@@ -70,10 +70,10 @@ function setup(
|
|||||||
return TestBed.inject(OrgTemplateStore);
|
return TestBed.inject(OrgTemplateStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RB-28 (TE-006): proefbrief() ends in BLOB_PRESENTER.open, not a raw
|
// --- TE-006: proefbrief() ends in BLOB_PRESENTER.open, not a raw
|
||||||
// window.open(URL.createObjectURL(...)) call, so both outcomes are assertable. ---
|
// window.open(URL.createObjectURL(...)) call, so both outcomes are assertable. ---
|
||||||
|
|
||||||
describe('OrgTemplateStore.proefbrief (RB-28)', () => {
|
describe('OrgTemplateStore.proefbrief', () => {
|
||||||
it('opens the rendered proefbrief via BLOB_PRESENTER on success', async () => {
|
it('opens the rendered proefbrief via BLOB_PRESENTER on success', async () => {
|
||||||
// Given a loaded sub-org template.
|
// Given a loaded sub-org template.
|
||||||
const { presenter, opened } = fakeBlobPresenter();
|
const { presenter, opened } = fakeBlobPresenter();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const LOGO_CATEGORY = 'org-logo';
|
|||||||
const NO_SUBORGS = $localize`:@@orgTemplate.noSubOrgs:Er zijn geen organisatiesjablonen om te beheren.`;
|
const NO_SUBORGS = $localize`:@@orgTemplate.noSubOrgs:Er zijn geen organisatiesjablonen om te beheren.`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Root singleton for the admin org-template editor (WP-26). The Elm machine owns the
|
* Root singleton for the admin org-template editor. The Elm machine owns the
|
||||||
* editable draft; commands here do the debounced save, publish (impact-confirm),
|
* editable draft; commands here do the debounced save, publish (impact-confirm),
|
||||||
* rollback and proefbrief, then dispatch the outcome — the reducer stays pure. The
|
* rollback and proefbrief, then dispatch the outcome — the reducer stays pure. The
|
||||||
* logo upload reuses the shared upload transport; its completion mutates the draft
|
* logo upload reuses the shared upload transport; its completion mutates the draft
|
||||||
@@ -151,7 +151,7 @@ export class OrgTemplateStore implements PendingSave {
|
|||||||
this.debouncedSave.schedule();
|
this.debouncedSave.schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 600ms debounced autosave (same idiom as BriefStore, WP-31). Timer mechanics live in the
|
// 600ms debounced autosave (same idiom as BriefStore). Timer mechanics live in the
|
||||||
// shared helper; `flushSave` below is the store-specific write + save-state.
|
// shared helper; `flushSave` below is the store-specific write + save-state.
|
||||||
private debouncedSave = createDebouncedSave({
|
private debouncedSave = createDebouncedSave({
|
||||||
canSave: () => this.loaded() !== null,
|
canSave: () => this.loaded() !== null,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Brief, LetterBlock, allBlocks } from './brief';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The rejection diff as a PURE function over two immutable `Brief` values — the whole
|
* The rejection diff as a PURE function over two immutable `Brief` values — the whole
|
||||||
* teaching payload of WP-27: because state is one value, "what changed since the letter
|
* teaching payload here: because state is one value, "what changed since the letter
|
||||||
* was rejected" is just a fold over two snapshots, no change-tracking bookkeeping.
|
* was rejected" is just a fold over two snapshots, no change-tracking bookkeeping.
|
||||||
*
|
*
|
||||||
* Blocks are matched by `blockId` (stable `local-N`/seed ids):
|
* Blocks are matched by `blockId` (stable `local-N`/seed ids):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Margins, OrgTemplate, OrgTemplateAdminView, OrgTemplateVersion } from '
|
|||||||
import { UploadMsg, UploadState, initialUpload, reduceUpload } from '@shared/domain/upload.machine';
|
import { UploadMsg, UploadState, initialUpload, reduceUpload } from '@shared/domain/upload.machine';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The admin org-template editor as one Elm-style machine (WP-26, PRD Brief v2 §5) —
|
* The admin org-template editor as one Elm-style machine (PRD Brief v2 §5) —
|
||||||
* the same idiom as the wizards. The DRAFT org template is form state (edited in
|
* the same idiom as the wizards. The DRAFT org template is form state (edited in
|
||||||
* place on the canvas); publish/rollback are effects that come back as `DraftLoaded`.
|
* place on the canvas); publish/rollback are effects that come back as `DraftLoaded`.
|
||||||
* `dirty` tracks unsaved edits (the store debounce-saves them). The logo upload is
|
* `dirty` tracks unsaved edits (the store debounce-saves them). The logo upload is
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* The organization template (Brief v2 PRD §3, WP-23/24): the SECOND template axis —
|
* The organization template (Brief v2 PRD §3): the SECOND template axis —
|
||||||
* appearance/identity per sub-organization (letterhead, footer, signature, margins).
|
* appearance/identity per sub-organization (letterhead, footer, signature, margins).
|
||||||
* Orthogonal to the case-type template (sections + placeholders); the two only meet
|
* Orthogonal to the case-type template (sections + placeholders); the two only meet
|
||||||
* at render time, on the letter canvas. Server-owned: the FE renders it verbatim,
|
* at render time, on the letter canvas. Server-owned: the FE renders it verbatim,
|
||||||
* never edits it here (the admin editor is WP-26).
|
* never edits it here (the admin editor does).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Margins {
|
export interface Margins {
|
||||||
@@ -30,7 +30,7 @@ export interface OrgTemplate {
|
|||||||
readonly version: number;
|
readonly version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- admin editor (WP-26) ---
|
// --- admin editor ---
|
||||||
|
|
||||||
/** A published snapshot in the version history: who is faked, `publishedAt` is real. */
|
/** A published snapshot in the version history: who is faked, `publishedAt` is real. */
|
||||||
export interface OrgTemplateVersion {
|
export interface OrgTemplateVersion {
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ import { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/ric
|
|||||||
* (ProblemDetails → error string, plus the Idempotency-Key mint), then parses the
|
* (ProblemDetails → error string, plus the Idempotency-Key mint), then parses the
|
||||||
* returned brief. `load` (the only read) does its own try/catch instead of the
|
* returned brief. `load` (the only read) does its own try/catch instead of the
|
||||||
* shared `runResult` fold, because it needs one extra bit `runResult` throws away:
|
* shared `runResult` fold, because it needs one extra bit `runResult` throws away:
|
||||||
* whether the failure was an HTTP 404 (see `BriefLoadFailure` — RB-22, CQ-007's
|
* whether the failure was an HTTP 404 (see `BriefLoadFailure` — CQ-007's
|
||||||
* expand half). Today's backend never 404s `GET /brief` (RB-23 adds that), so the
|
* expand half). Today's backend never 404s `GET /brief`, so the
|
||||||
* `notFound` branch is unreached until RB-23 ships; this adapter is ready in advance.
|
* `notFound` branch is unreached until it does; this adapter is ready in advance.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface BriefView {
|
export interface BriefView {
|
||||||
@@ -66,7 +66,7 @@ export const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is
|
|||||||
/** True when the thrown value carries an HTTP 404 status — matches both the
|
/** True when the thrown value carries an HTTP 404 status — matches both the
|
||||||
generic `SwaggerException` (today's shape, since `GET /brief` declares no 404
|
generic `SwaggerException` (today's shape, since `GET /brief` declares no 404
|
||||||
response yet) and a parsed `ProblemDetails` (RFC 7807 `status`, the shape once
|
response yet) and a parsed `ProblemDetails` (RFC 7807 `status`, the shape once
|
||||||
RB-23 gives the endpoint a documented 404 response). */
|
the endpoint gets a documented 404 response). */
|
||||||
function isHttpNotFound(e: unknown): boolean {
|
function isHttpNotFound(e: unknown): boolean {
|
||||||
return !!e && typeof e === 'object' && (e as { status?: unknown }).status === 404;
|
return !!e && typeof e === 'object' && (e as { status?: unknown }).status === 404;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,19 @@ export const PREVIEW_FAILED = $localize`:@@brief.preview.failed:De voorvertoning
|
|||||||
* to keep the NSwag-generated client JSON-only (same seam as uploads) — so this is a
|
* to keep the NSwag-generated client JSON-only (same seam as uploads) — so this is a
|
||||||
* hand-written fetch, not the `ApiClient`. That also means it bypasses `HttpClient`'s
|
* hand-written fetch, not the `ApiClient`. That also means it bypasses `HttpClient`'s
|
||||||
* `roleInterceptor` AND `subjectInterceptor`, so both `X-Role` and `X-Subject` are set
|
* `roleInterceptor` AND `subjectInterceptor`, so both `X-Role` and `X-Subject` are set
|
||||||
* here explicitly (WP-74 — without `X-Subject` this always previewed
|
* here explicitly (without `X-Subject` this always previewed
|
||||||
* `DocumentStore.DemoOwner`'s letter regardless of who was actually logged in). Both are
|
* `DocumentStore.DemoOwner`'s letter regardless of who was actually logged in). Both are
|
||||||
* dev-only identity stand-ins (`role.ts`/`subject.ts`) and are sent only under
|
* dev-only identity stand-ins (`role.ts`/`subject.ts`) and are sent only under
|
||||||
* `isDevMode()`, mirroring how the interceptors themselves are only registered in dev
|
* `isDevMode()`, mirroring how the interceptors themselves are only registered in dev
|
||||||
* (`app.config.ts`) — a production build sends neither header from this call (BIO-012).
|
* (`app.config.ts`) — a production build sends neither header from this call (BIO-012).
|
||||||
*
|
*
|
||||||
* `cache: 'no-store'` (WP-74): the endpoint has no `Cache-Control`, only a CORS-driven
|
* `cache: 'no-store'`: the endpoint has no `Cache-Control`, only a CORS-driven
|
||||||
* `Vary: Origin`, and its content changes at the SAME URL as the letter moves
|
* `Vary: Origin`, and its content changes at the SAME URL as the letter moves
|
||||||
* draft → sent. Explicitly bypassing the HTTP cache is the correct default for any
|
* draft → sent. Explicitly bypassing the HTTP cache is the correct default for any
|
||||||
* mutable resource served under one unversioned URL — independent of WP-74's
|
* mutable resource served under one unversioned URL — independent of the
|
||||||
* identity work, and not a complete fix by itself: see the KNOWN GAP note below.
|
* identity work above, and not a complete fix by itself: see the KNOWN GAP note below.
|
||||||
*
|
*
|
||||||
* KNOWN GAP (WP-74, not fixed here): under a non-`DocumentStore.DemoOwner` `X-Subject`,
|
* KNOWN GAP (not fixed here): under a non-`DocumentStore.DemoOwner` `X-Subject`,
|
||||||
* this repo's own e2e run against a real backend observed this endpoint's SENT
|
* this repo's own e2e run against a real backend observed this endpoint's SENT
|
||||||
* response still carrying the draft watermark, even though (a) the outgoing request
|
* response still carrying the draft watermark, even though (a) the outgoing request
|
||||||
* carried the correct `X-Subject`, and (b) `curl` against the same backend at the
|
* carried the correct `X-Subject`, and (b) `curl` against the same backend at the
|
||||||
@@ -34,7 +34,7 @@ export const PREVIEW_FAILED = $localize`:@@brief.preview.failed:De voorvertoning
|
|||||||
* did not change the outcome, so it is very unlikely a client-side caching artifact —
|
* did not change the outcome, so it is very unlikely a client-side caching artifact —
|
||||||
* it looks like a genuine backend-side staleness/race in `BriefStore`'s SQLite-backed
|
* it looks like a genuine backend-side staleness/race in `BriefStore`'s SQLite-backed
|
||||||
* read path, reproducible for MULTIPLE distinct owners and NOT reproducible for
|
* read path, reproducible for MULTIPLE distinct owners and NOT reproducible for
|
||||||
* `DemoOwner`, which needs backend-side investigation (out of WP-74's file scope —
|
* `DemoOwner`, which needs backend-side investigation (out of this file's scope —
|
||||||
* see `e2e/brief-v2.spec.ts`'s header comment, which keeps that spec on the shared
|
* see `e2e/brief-v2.spec.ts`'s header comment, which keeps that spec on the shared
|
||||||
* `zorgverlener` identity until this is root-caused).
|
* `zorgverlener` identity until this is root-caused).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export class BriefPage {
|
|||||||
void this.store.resetDemo();
|
void this.store.resetDemo();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Typed narrowing for the `<app-async>` loaded slot — see WP-06: a structural
|
/** Typed narrowing for the `<app-async>` loaded slot: a structural
|
||||||
directive's context can't inherit a generic from a sibling host input, so the
|
directive's context can't inherit a generic from a sibling host input, so the
|
||||||
Success value is unwrapped here instead of through `let-`. */
|
Success value is unwrapped here instead of through `let-`. */
|
||||||
protected readonly loaded = computed(() => {
|
protected readonly loaded = computed(() => {
|
||||||
@@ -179,7 +179,7 @@ export class BriefPage {
|
|||||||
void this.store.load();
|
void this.store.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ctrl/Cmd+Z = undo, Ctrl/Cmd+Shift+Z = redo (WP-27). Ignored while focus is in the
|
/** Ctrl/Cmd+Z = undo, Ctrl/Cmd+Shift+Z = redo. Ignored while focus is in the
|
||||||
rich-text editor or a form control, so the browser's own text undo keeps working
|
rich-text editor or a form control, so the browser's own text undo keeps working
|
||||||
there — our shell-level undo is for structural edits (add/remove/reorder blocks). */
|
there — our shell-level undo is for structural edits (add/remove/reorder blocks). */
|
||||||
protected onKey(e: KeyboardEvent) {
|
protected onKey(e: KeyboardEvent) {
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
|
|||||||
footer around the case-type template's sections. `editableRegions` picks who edits
|
footer around the case-type template's sections. `editableRegions` picks who edits
|
||||||
what: `'content'` hosts the editable letter-sections in place (drafter), `'none'`
|
what: `'content'` hosts the editable letter-sections in place (drafter), `'none'`
|
||||||
renders everything read-only (approver/locked, absorbs the old letter-preview),
|
renders everything read-only (approver/locked, absorbs the old letter-preview),
|
||||||
`'template'` reserves the org-identity regions for the admin editor (WP-26).
|
`'template'` reserves the org-identity regions for the admin editor.
|
||||||
Letter typography/geometry come from the shared `public/letter.css` contract —
|
Letter typography/geometry come from the shared `public/letter.css` contract —
|
||||||
the same file the backend preview renderer inlines (WP-25). */
|
the same file the backend preview renderer inlines. */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-letter-canvas',
|
selector: 'app-letter-canvas',
|
||||||
imports: [NgTemplateOutlet, ButtonComponent, PlaceholderChipComponent],
|
imports: [NgTemplateOutlet, ButtonComponent, PlaceholderChipComponent],
|
||||||
@@ -82,7 +82,7 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
|
|||||||
color: var(--rhc-color-foreground-subtle);
|
color: var(--rhc-color-foreground-subtle);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
/* Rejection-diff badge (WP-27): a small pill above a changed/added block. */
|
/* Rejection-diff badge: a small pill above a changed/added block. */
|
||||||
.diff-block.diff-changed {
|
.diff-block.diff-changed {
|
||||||
border-inline-start: 3px solid var(--rhc-color-oranje-500);
|
border-inline-start: 3px solid var(--rhc-color-oranje-500);
|
||||||
padding-inline-start: var(--rhc-space-max-sm);
|
padding-inline-start: var(--rhc-space-max-sm);
|
||||||
@@ -98,7 +98,7 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
|
|||||||
background: var(--rhc-color-oranje-500);
|
background: var(--rhc-color-oranje-500);
|
||||||
}
|
}
|
||||||
.diff-badge.added {
|
.diff-badge.added {
|
||||||
/* added = white on groen-700 (6.4:1); dark text on any green fails 4.5:1 (WP-29 axe). */
|
/* added = white on groen-700 (6.4:1); dark text on any green fails 4.5:1 (axe). */
|
||||||
color: var(--rhc-color-wit);
|
color: var(--rhc-color-wit);
|
||||||
background: var(--rhc-color-groen-700);
|
background: var(--rhc-color-groen-700);
|
||||||
}
|
}
|
||||||
@@ -345,12 +345,12 @@ export class LetterCanvasComponent {
|
|||||||
brief = input.required<Brief>();
|
brief = input.required<Brief>();
|
||||||
orgTemplate = input.required<OrgTemplate>();
|
orgTemplate = input.required<OrgTemplate>();
|
||||||
/** Who edits what on the surface: read-only ('none', the drafter preview + approver
|
/** Who edits what on the surface: read-only ('none', the drafter preview + approver
|
||||||
view) or admin editor ('template', WP-26). Authoring moved to letter-editor. */
|
view) or admin editor ('template'). Authoring moved to letter-editor. */
|
||||||
editableRegions = input<'template' | 'none'>('none');
|
editableRegions = input<'template' | 'none'>('none');
|
||||||
diagnostics = input<readonly Diagnostic[]>([]);
|
diagnostics = input<readonly Diagnostic[]>([]);
|
||||||
/** Initial zoom; the in-canvas controls take over from here (WP-27). */
|
/** Initial zoom; the in-canvas controls take over from here. */
|
||||||
zoom = input(1);
|
zoom = input(1);
|
||||||
/** Blocks changed/added/removed since the letter was rejected (WP-27); badged when
|
/** Blocks changed/added/removed since the letter was rejected; badged when
|
||||||
`showDiff` is on. Removed blocks aren't in the map's rendered set — they no longer
|
`showDiff` is on. Removed blocks aren't in the map's rendered set — they no longer
|
||||||
exist in the letter — the composer surfaces them as a count. */
|
exist in the letter — the composer surfaces them as a count. */
|
||||||
blockDiffs = input<ReadonlyMap<string, BlockDiffKind>>(new Map());
|
blockDiffs = input<ReadonlyMap<string, BlockDiffKind>>(new Map());
|
||||||
@@ -445,7 +445,7 @@ export class LetterCanvasComponent {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// ponytail: whole-surface height / A4-interval — ignores that a break never truly
|
// ponytail: whole-surface height / A4-interval — ignores that a break never truly
|
||||||
// falls mid-line; the caption says "±" and WP-25's server preview is authoritative.
|
// falls mid-line; the caption says "±" and the server preview is authoritative.
|
||||||
const observer = new ResizeObserver(([entry]) => {
|
const observer = new ResizeObserver(([entry]) => {
|
||||||
// ~1cm tolerance so a letter ending on a page boundary gets no edge-hugging mark.
|
// ~1cm tolerance so a letter ending on a page boundary gets no edge-hugging mark.
|
||||||
const pages = Math.ceil((entry.target.scrollHeight - 40) / A4_HEIGHT_PX);
|
const pages = Math.ceil((entry.target.scrollHeight - 40) / A4_HEIGHT_PX);
|
||||||
|
|||||||
@@ -127,12 +127,12 @@ export const ReadOnlyZonderBevindingen: Story = {
|
|||||||
args: { editableRegions: 'none', diagnostics: [] },
|
args: { editableRegions: 'none', diagnostics: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Admin editor focus (consumer arrives in WP-26): body read-only, no "not yours" tint. */
|
/** Admin editor focus: body read-only, no "not yours" tint. */
|
||||||
export const TemplateMode: Story = { args: { editableRegions: 'template' } };
|
export const TemplateMode: Story = { args: { editableRegions: 'template' } };
|
||||||
|
|
||||||
export const Zoomed: Story = { args: { editableRegions: 'none', zoom: 0.6 } };
|
export const Zoomed: Story = { args: { editableRegions: 'none', zoom: 0.6 } };
|
||||||
|
|
||||||
/** Approver's "Toon wijzigingen": blocks changed/added since rejection are badged (WP-27). */
|
/** Approver's "Toon wijzigingen": blocks changed/added since rejection are badged. */
|
||||||
export const WithDiff: Story = {
|
export const WithDiff: Story = {
|
||||||
args: {
|
args: {
|
||||||
editableRegions: 'none',
|
editableRegions: 'none',
|
||||||
@@ -150,14 +150,14 @@ export const PageBreak: Story = {
|
|||||||
args: { editableRegions: 'none', brief: longBrief, diagnostics: [] },
|
args: { editableRegions: 'none', brief: longBrief, diagnostics: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inline SVG so the story needs no backend/upload round-trip (WP-26 logo upload).
|
// Inline SVG so the story needs no backend/upload round-trip (the logo upload).
|
||||||
const sampleLogo =
|
const sampleLogo =
|
||||||
'data:image/svg+xml;utf8,' +
|
'data:image/svg+xml;utf8,' +
|
||||||
encodeURIComponent(
|
encodeURIComponent(
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#003366"/><text x="60" y="25" font-size="14" fill="white" text-anchor="middle">CIBG</text></svg>',
|
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#003366"/><text x="60" y="25" font-size="14" fill="white" text-anchor="middle">CIBG</text></svg>',
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Published org logo (WP-26 AC2): the letterhead shows it above the org name. */
|
/** Published org logo: the letterhead shows it above the org name. */
|
||||||
export const MetLogo: Story = {
|
export const MetLogo: Story = {
|
||||||
args: { editableRegions: 'none', diagnostics: [], logoUrl: sampleLogo },
|
args: { editableRegions: 'none', diagnostics: [], logoUrl: sampleLogo },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export class LetterComposerComponent {
|
|||||||
canReject = input(false);
|
canReject = input(false);
|
||||||
canSend = input(false);
|
canSend = input(false);
|
||||||
busy = input(false);
|
busy = input(false);
|
||||||
/** Rejection diff (WP-27): the changed/added/removed blocks and their count. The
|
/** Rejection diff: the changed/added/removed blocks and their count. The
|
||||||
"Toon wijzigingen" toggle only appears when there's something to show. */
|
"Toon wijzigingen" toggle only appears when there's something to show. */
|
||||||
blockDiffs = input<ReadonlyMap<string, BlockDiffKind>>(new Map());
|
blockDiffs = input<ReadonlyMap<string, BlockDiffKind>>(new Map());
|
||||||
removedCount = input(0);
|
removedCount = input(0);
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export const Sent: Story = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Approver's "Toon wijzigingen" (WP-27): a resubmitted letter with blocks changed,
|
/** Approver's "Toon wijzigingen": a resubmitted letter with blocks changed,
|
||||||
added and removed since the last rejection. */
|
added and removed since the last rejection. */
|
||||||
export const RejectionDiff: Story = {
|
export const RejectionDiff: Story = {
|
||||||
render: () =>
|
render: () =>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const SAMPLE_LETTER_BRIEF: Brief = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Organism (WP-26): the admin org-template editor. The mirror of the drafter's
|
* Organism: the admin org-template editor. The mirror of the drafter's
|
||||||
* composer — the letter canvas runs in `editableRegions='template'` so the
|
* composer — the letter canvas runs in `editableRegions='template'` so the
|
||||||
* letterhead/signature/footer are edited in place, while the content is a read-only
|
* letterhead/signature/footer are edited in place, while the content is a read-only
|
||||||
* sample. Margins, logo upload, version history and the publish bar sit around it.
|
* sample. Margins, logo upload, version history and the publish bar sit around it.
|
||||||
|
|||||||
@@ -87,12 +87,12 @@ const sampleLogo =
|
|||||||
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#003366"/><text x="60" y="25" font-size="14" fill="white" text-anchor="middle">CIBG</text></svg>',
|
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#003366"/><text x="60" y="25" font-size="14" fill="white" text-anchor="middle">CIBG</text></svg>',
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Published logo (WP-26 AC2): the letterhead canvas shows it above the org name. */
|
/** Published logo: the letterhead canvas shows it above the org name. */
|
||||||
export const MetLogo: Story = {
|
export const MetLogo: Story = {
|
||||||
args: { logoUrl: sampleLogo },
|
args: { logoUrl: sampleLogo },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Client-side upload rejection (existing `rejectReason`, WP-26 AC5) — type/size caught
|
/** Client-side upload rejection (existing `rejectReason`) — type/size caught
|
||||||
before the file ever reaches the backend. */
|
before the file ever reaches the backend. */
|
||||||
export const LogoUploadFout: Story = {
|
export const LogoUploadFout: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AccessStore } from '@shared/application/access.store';
|
|||||||
import { OrgTemplateStore } from '@brief/application/org-template.store';
|
import { OrgTemplateStore } from '@brief/application/org-template.store';
|
||||||
import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-template-editor.component';
|
import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-template-editor.component';
|
||||||
|
|
||||||
/** Page: thin container for the admin org-template editor (WP-26). Deny-by-default
|
/** Page: thin container for the admin org-template editor. Deny-by-default
|
||||||
capability gate (`orgtemplate:edit`) — a denial alert for non-admins, the editor
|
capability gate (`orgtemplate:edit`) — a denial alert for non-admins, the editor
|
||||||
for admins. Loads once the capability resolves; wires store commands to the organism. */
|
for admins. Loads once the capability resolves; wires store commands to the organism. */
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { LibraryPassage } from '@brief/domain/brief';
|
|||||||
inserts ALL checked passages at once (a single message upstream) — there is no
|
inserts ALL checked passages at once (a single message upstream) — there is no
|
||||||
single-insert path. Presentational: emits the chosen passages in list order.
|
single-insert path. Presentational: emits the chosen passages in list order.
|
||||||
|
|
||||||
Superseded by `besluit-panel` (WP-27's guided drafting): no consumer left in
|
Superseded by `besluit-panel`'s guided drafting: no consumer left in
|
||||||
`src/app` outside its own story (WP-28 audit). Kept for now rather than deleted
|
`src/app` outside its own story. Kept for now rather than deleted
|
||||||
in-flight of an unrelated WP; a future cleanup can remove it. */
|
in-flight of an unrelated WP; a future cleanup can remove it. */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-passage-picker',
|
selector: 'app-passage-picker',
|
||||||
@@ -87,7 +87,7 @@ export class PassagePickerComponent {
|
|||||||
protected checked = signal<Record<string, boolean>>({});
|
protected checked = signal<Record<string, boolean>>({});
|
||||||
protected query = signal('');
|
protected query = signal('');
|
||||||
/** Client-side filter on label + rendered content text — the library is small, so no
|
/** Client-side filter on label + rendered content text — the library is small, so no
|
||||||
server search (WP-27). Placeholder keys are searchable too (see `textOf`). */
|
server search. Placeholder keys are searchable too (see `textOf`). */
|
||||||
protected filtered = computed(() => {
|
protected filtered = computed(() => {
|
||||||
const q = this.query().trim().toLowerCase();
|
const q = this.query().trim().toLowerCase();
|
||||||
if (!q) return this.passages();
|
if (!q) return this.passages();
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ describe('intake acceptance journeys', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('raising uren above the threshold after answering scholing drops both fields (WP-69 §6)', () => {
|
it('raising uren above the threshold after answering scholing drops both fields', () => {
|
||||||
// Given a journey that answered the scholing question while uren was low.
|
// Given a journey that answered the scholing question while uren was low.
|
||||||
const atReview = givenIntake(
|
const atReview = givenIntake(
|
||||||
{ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' },
|
{ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' },
|
||||||
@@ -170,7 +170,7 @@ describe('intake acceptance journeys', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Then the submission succeeds, and BOTH the stale answer and its punten are gone —
|
// Then the submission succeeds, and BOTH the stale answer and its punten are gone —
|
||||||
// exactly the crafted-POST-shaped payload WP-69's server rule rejects.
|
// exactly the crafted-POST-shaped payload the server rule rejects.
|
||||||
expect(done.tag).toBe('Submitted');
|
expect(done.tag).toBe('Submitted');
|
||||||
expect(done.tag === 'Submitted' && done.data.aanvullendeScholing).toBeUndefined();
|
expect(done.tag === 'Submitted' && done.data.aanvullendeScholing).toBeUndefined();
|
||||||
expect(done.tag === 'Submitted' && done.data.punten).toBeUndefined();
|
expect(done.tag === 'Submitted' && done.data.punten).toBeUndefined();
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ describe('submit', () => {
|
|||||||
expect(withScholing.data.punten).toBe(200);
|
expect(withScholing.data.punten).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not require punten for a hidden question (WP-69 §6)', () => {
|
it('does not require punten for a hidden question', () => {
|
||||||
// scholingGevolgd is a stale 'ja' from when uren was low, but uren is now above
|
// scholingGevolgd is a stale 'ja' from when uren was low, but uren is now above
|
||||||
// threshold — the template hides the question, so punten must not be required either.
|
// threshold — the template hides the question, so punten must not be required either.
|
||||||
const staleScholingNoPunten = givenIntake(
|
const staleScholingNoPunten = givenIntake(
|
||||||
@@ -183,7 +183,7 @@ describe('submit', () => {
|
|||||||
expect(good.data.aanvullendeScholing).toBeUndefined();
|
expect(good.data.aanvullendeScholing).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('drops punten when raising uren hides the question (WP-69 §6)', () => {
|
it('drops punten when raising uren hides the question', () => {
|
||||||
// Same stale answer, but this time punten was also filled in while uren was low.
|
// Same stale answer, but this time punten was also filled in while uren was low.
|
||||||
const staleScholingWithPunten = givenIntake(
|
const staleScholingWithPunten = givenIntake(
|
||||||
{ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' },
|
{ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' },
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
|
|||||||
// visible (lageUren) AND scholing was followed — matching the template's
|
// visible (lageUren) AND scholing was followed — matching the template's
|
||||||
// `@if (scholingZichtbaar())`. Without the `lageUren` guard, answering 'ja' and then
|
// `@if (scholingZichtbaar())`. Without the `lageUren` guard, answering 'ja' and then
|
||||||
// raising uren above the threshold left an error on a field the template no longer
|
// raising uren above the threshold left an error on a field the template no longer
|
||||||
// renders (WP-69 §6).
|
// renders.
|
||||||
if (lageUren(a, scholingThreshold) && a.scholingGevolgd === 'ja') {
|
if (lageUren(a, scholingThreshold) && a.scholingGevolgd === 'ja') {
|
||||||
const p = parseUren(a.punten ?? '');
|
const p = parseUren(a.punten ?? '');
|
||||||
if (!p.ok) errors.punten = p.error;
|
if (!p.ok) errors.punten = p.error;
|
||||||
@@ -149,8 +149,8 @@ function validateAll(a: Answers, scholingThreshold: number): Result<Errors, Vali
|
|||||||
const aanvullendeScholing = lageUren(a, scholingThreshold)
|
const aanvullendeScholing = lageUren(a, scholingThreshold)
|
||||||
? a.scholingGevolgd === 'ja'
|
? a.scholingGevolgd === 'ja'
|
||||||
: undefined;
|
: undefined;
|
||||||
// Punten are derived from aanvullendeScholing, NOT the raw scholingGevolgd answer (WP-69
|
// Punten are derived from aanvullendeScholing, NOT the raw scholingGevolgd answer —
|
||||||
// §6) — a stale 'ja' left over from when uren was low, after uren was raised above the
|
// a stale 'ja' left over from when uren was low, after uren was raised above the
|
||||||
// threshold, must not leak a punten value into the parsed, submitted ValidIntake.
|
// threshold, must not leak a punten value into the parsed, submitted ValidIntake.
|
||||||
const punten = aanvullendeScholing === true ? parseUren(a.punten ?? '') : undefined;
|
const punten = aanvullendeScholing === true ? parseUren(a.punten ?? '') : undefined;
|
||||||
return ok({
|
return ok({
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export class IntakeWizardComponent {
|
|||||||
private store = createStore<IntakeState, IntakeMsg>(initial, reduce, {
|
private store = createStore<IntakeState, IntakeMsg>(initial, reduce, {
|
||||||
Submitting: async (s, store) => {
|
Submitting: async (s, store) => {
|
||||||
this.profile.beginHerregistratie();
|
this.profile.beginHerregistratie();
|
||||||
// WP-69: the scholing answer rides along so the server can re-validate it as the
|
// The scholing answer rides along so the server can re-validate it as the
|
||||||
// authority (IntakePolicy.RejectIncompleteScholing) — undefined members are dropped by
|
// authority (IntakePolicy.RejectIncompleteScholing) — undefined members are dropped by
|
||||||
// JSON.stringify, so a wizard above the threshold sends neither field.
|
// JSON.stringify, so a wizard above the threshold sends neither field.
|
||||||
const r = await this.draftSync.submit({
|
const r = await this.draftSync.submit({
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ describe('AanvragenStore', () => {
|
|||||||
expect(store.lastError()).toBeNull();
|
expect(store.lastError()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
// RB-20: a failed cancel must not be silent — the row rolls back AND the store
|
// A failed cancel must not be silent — the row rolls back AND the store
|
||||||
// surfaces the error the page renders. Before RB-20 this only rolled back
|
// surfaces the error the page renders. Before this fix it only rolled back
|
||||||
// (bare `catch { this.state.set(before) }`), so `lastError()` stayed null forever.
|
// (bare `catch { this.state.set(before) }`), so `lastError()` stayed null forever.
|
||||||
it('rolls back the removal and surfaces the error when the cancel fails', async () => {
|
it('rolls back the removal and surfaces the error when the cancel fails', async () => {
|
||||||
const cancel = vi.fn().mockRejectedValue(new Error('boom'));
|
const cancel = vi.fn().mockRejectedValue(new Error('boom'));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Err = Error | undefined;
|
|||||||
* change-detection timing, HTTP caching, or a resource `reload()`. `reload()` re-fetches
|
* change-detection timing, HTTP caching, or a resource `reload()`. `reload()` re-fetches
|
||||||
* so a page revisit reflects auto-approval (Concept → In behandeling → Goedgekeurd is
|
* so a page revisit reflects auto-approval (Concept → In behandeling → Goedgekeurd is
|
||||||
* computed server-side on read). Cancel goes through `runSubmit` and rolls back plus
|
* computed server-side on read). Cancel goes through `runSubmit` and rolls back plus
|
||||||
* surfaces `lastError` on failure (RB-20).
|
* surfaces `lastError` on failure.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AanvragenStore {
|
export class AanvragenStore {
|
||||||
@@ -23,7 +23,7 @@ export class AanvragenStore {
|
|||||||
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
|
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
|
||||||
readonly aanvragen = this.state.asReadonly();
|
readonly aanvragen = this.state.asReadonly();
|
||||||
|
|
||||||
/** Set on a failed cancel (RB-20): the optimistic removal already rolled back by
|
/** Set on a failed cancel: the optimistic removal already rolled back by
|
||||||
then, this is only the message for the alert the page renders above the list. */
|
then, this is only the message for the alert the page renders above the list. */
|
||||||
private error = signal<string | null>(null);
|
private error = signal<string | null>(null);
|
||||||
readonly lastError = this.error.asReadonly();
|
readonly lastError = this.error.asReadonly();
|
||||||
@@ -55,7 +55,7 @@ export class AanvragenStore {
|
|||||||
|
|
||||||
/** Cancel a Concept: drop it now (synchronous, guaranteed), then confirm the DELETE.
|
/** Cancel a Concept: drop it now (synchronous, guaranteed), then confirm the DELETE.
|
||||||
No resync — the delete succeeded, so the optimistic removal is authoritative. On
|
No resync — the delete succeeded, so the optimistic removal is authoritative. On
|
||||||
failure, roll back AND surface the error (RB-20) — a silent reappearance leaves the
|
failure, roll back AND surface the error — a silent reappearance leaves the
|
||||||
user guessing why the block came back. */
|
user guessing why the block came back. */
|
||||||
async cancel(id: string) {
|
async cancel(id: string) {
|
||||||
const before = this.state();
|
const before = this.state();
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ describe('AdminCasesStore', () => {
|
|||||||
expect(s.tag === 'Success' && s.value.map((c) => c.id)).toEqual(['b']);
|
expect(s.tag === 'Success' && s.value.map((c) => c.id)).toEqual(['b']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// RB-20: a failed delete must not be silent — the row rolls back AND the store
|
// A failed delete must not be silent — the row rolls back AND the store
|
||||||
// surfaces the error the page renders. Before RB-20 this only rolled back
|
// surfaces the error the page renders. Before this fix it only rolled back
|
||||||
// (bare `catch { this.state.set(before) }`), so `lastError()` stayed null forever.
|
// (bare `catch { this.state.set(before) }`), so `lastError()` stayed null forever.
|
||||||
it('rolls back the removal and surfaces the error when the delete fails', async () => {
|
it('rolls back the removal and surfaces the error when the delete fails', async () => {
|
||||||
const deleteAny = vi.fn().mockRejectedValue(new Error('boom'));
|
const deleteAny = vi.fn().mockRejectedValue(new Error('boom'));
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import { AanvragenAdapter, parseAanvragen } from '@registratie/infrastructure/aa
|
|||||||
type Err = Error | undefined;
|
type Err = Error | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin view of ALL cases across owners (WP-36; `cases:manage`) — the back-office
|
* Admin view of ALL cases across owners (`cases:manage`) — the back-office
|
||||||
* counterpart of the user-facing `AanvragenStore`. Same shape: one root singleton
|
* counterpart of the user-facing `AanvragenStore`. Same shape: one root singleton
|
||||||
* owns the list as a writable RemoteData signal, delete removes the row synchronously
|
* owns the list as a writable RemoteData signal, delete removes the row synchronously
|
||||||
* (optimistic), goes through `runSubmit`, and rolls back plus surfaces `lastError` on
|
* (optimistic), goes through `runSubmit`, and rolls back plus surfaces `lastError` on
|
||||||
* failure (RB-20). Admin delete removes any case (any owner, submitted or not — the
|
* failure. Admin delete removes any case (any owner, submitted or not — the
|
||||||
* server enforces the capability).
|
* server enforces the capability).
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -21,7 +21,7 @@ export class AdminCasesStore {
|
|||||||
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
|
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
|
||||||
readonly cases = this.state.asReadonly();
|
readonly cases = this.state.asReadonly();
|
||||||
|
|
||||||
/** Set on a failed delete (RB-20): the optimistic removal already rolled back by
|
/** Set on a failed delete: the optimistic removal already rolled back by
|
||||||
then, this is only the message for the alert the page renders above the list. */
|
then, this is only the message for the alert the page renders above the list. */
|
||||||
private error = signal<string | null>(null);
|
private error = signal<string | null>(null);
|
||||||
readonly lastError = this.error.asReadonly();
|
readonly lastError = this.error.asReadonly();
|
||||||
@@ -47,7 +47,7 @@ export class AdminCasesStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Delete a case: drop it now (synchronous), then confirm the DELETE; roll back on error
|
/** Delete a case: drop it now (synchronous), then confirm the DELETE; roll back on error
|
||||||
AND surface it (RB-20) — a silent reappearance leaves the admin guessing why. */
|
AND surface it — a silent reappearance leaves the admin guessing why. */
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
const before = this.state();
|
const before = this.state();
|
||||||
if (before.tag === 'Success') {
|
if (before.tag === 'Success') {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ describe('createDraftSync', () => {
|
|||||||
expect(r.ok).toBe(false);
|
expect(r.ok).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recovers from a create conflict by adopting the existing Concept (WP-35)', async () => {
|
it('recovers from a create conflict by adopting the existing Concept', async () => {
|
||||||
// Server enforces one Concept per type: a stale/cross-tab create is rejected (409),
|
// Server enforces one Concept per type: a stale/cross-tab create is rejected (409),
|
||||||
// and ensureId adopts the existing Concept from the list instead of erroring.
|
// and ensureId adopts the existing Concept from the list instead of erroring.
|
||||||
const create = vi.fn().mockRejectedValue({ status: 409 });
|
const create = vi.fn().mockRejectedValue({ status: 409 });
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function createDraftSync(deps: DraftSyncDeps) {
|
|||||||
if (id) return id;
|
if (id) return id;
|
||||||
ensuring ??= adapter
|
ensuring ??= adapter
|
||||||
.create(deps.type)
|
.create(deps.type)
|
||||||
// WP-35: one Concept per type is server-enforced. Within a tab the resumeGate
|
// One Concept per type is server-enforced. Within a tab the resumeGate
|
||||||
// already prevents a second create, but a cross-tab/stale race can still hit the
|
// already prevents a second create, but a cross-tab/stale race can still hit the
|
||||||
// server's guard (409) — recover by adopting the existing Concept instead of
|
// server's guard (409) — recover by adopting the existing Concept instead of
|
||||||
// erroring. Only recover when one actually exists; otherwise surface the failure.
|
// erroring. Only recover when one actually exists; otherwise surface the failure.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AanvragenAdapter, parseAanvragen } from '@registratie/infrastructure/aa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read half of the Concept lookup that `createDraftSync` (`draft-sync.ts`) needs
|
* Read half of the Concept lookup that `createDraftSync` (`draft-sync.ts`) needs
|
||||||
* before it can start writing (RB-21 / CQ-001). Free functions that take the adapter
|
* before it can start writing (CQ-001). Free functions that take the adapter
|
||||||
* as a parameter, not `inject()`, so they get a direct spec without Angular TestBed.
|
* as a parameter, not `inject()`, so they get a direct spec without Angular TestBed.
|
||||||
* `createDraftSync` keeps the closure state (`id`, `resumeGate`) and the write path;
|
* `createDraftSync` keeps the closure state (`id`, `resumeGate`) and the write path;
|
||||||
* these two functions only read.
|
* these two functions only read.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
*/
|
*/
|
||||||
export type AanvraagType = 'registratie' | 'herregistratie' | 'intake';
|
export type AanvraagType = 'registratie' | 'herregistratie' | 'intake';
|
||||||
|
|
||||||
// Ingediend/MeerInfoGevraagd (ADR-0002/WP-63) are widened into the union so the parse
|
// Ingediend/MeerInfoGevraagd (ADR-0002) are widened into the union so the parse
|
||||||
// boundary + renderers are ready, but no backend path emits them yet — that's WP-65's
|
// boundary + renderers are ready, but no backend path emits them yet — that's the
|
||||||
// behandelaar-facing transition endpoint.
|
// behandelaar-facing transition endpoint.
|
||||||
export type AanvraagStatus =
|
export type AanvraagStatus =
|
||||||
| { tag: 'Concept'; stepIndex: number; stepCount: number }
|
| { tag: 'Concept'; stepIndex: number; stepCount: number }
|
||||||
@@ -29,7 +29,7 @@ export interface Aanvraag {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
submittedAt?: string;
|
submittedAt?: string;
|
||||||
/** The case owner (a BSN). Only populated by the admin cross-owner list (WP-36);
|
/** The case owner (a BSN). Only populated by the admin cross-owner list;
|
||||||
the user's own list leaves it undefined. */
|
the user's own list leaves it undefined. */
|
||||||
owner?: string;
|
owner?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from '@registratie/domain/value-objects/telefoonnummer';
|
} from '@registratie/domain/value-objects/telefoonnummer';
|
||||||
|
|
||||||
/** What the user is typing (raw, possibly invalid). The BRP address is NOT part of
|
/** What the user is typing (raw, possibly invalid). The BRP address is NOT part of
|
||||||
the form — it is authoritative and shown read-only (WP-34); only the phone number
|
the form — it is authoritative and shown read-only; only the phone number
|
||||||
is editable here. */
|
is editable here. */
|
||||||
export interface Draft {
|
export interface Draft {
|
||||||
telefoon: string;
|
telefoon: string;
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ export class AanvragenAdapter {
|
|||||||
return this.client.aanvragenAll();
|
return this.client.aanvragenAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Admin: every case across all owners (WP-36; `cases:manage`). Parsed at the boundary. */
|
/** Admin: every case across all owners (`cases:manage`). Parsed at the boundary. */
|
||||||
listAll(): Promise<AanvraagSummaryDto[]> {
|
listAll(): Promise<AanvraagSummaryDto[]> {
|
||||||
return this.client.casesAll();
|
return this.client.casesAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Admin: delete ANY case (any owner, submitted or not — WP-36). */
|
/** Admin: delete ANY case (any owner, submitted or not). */
|
||||||
deleteAny(id: string): Promise<void> {
|
deleteAny(id: string): Promise<void> {
|
||||||
return this.client.cases(id);
|
return this.client.cases(id);
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ function parseCommon(dto: AanvraagSummaryDto): Result<string, Aanvraag> {
|
|||||||
createdAt: dto.createdAt,
|
createdAt: dto.createdAt,
|
||||||
updatedAt: dto.updatedAt,
|
updatedAt: dto.updatedAt,
|
||||||
submittedAt: dto.submittedAt,
|
submittedAt: dto.submittedAt,
|
||||||
owner: dto.owner, // only present on the admin cross-owner list (WP-36)
|
owner: dto.owner, // only present on the admin cross-owner list
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Valid } from '@registratie/domain/change-request.machine';
|
|||||||
* Infrastructure adapter for the telefoonwijziging POST (`/api/v1/change-requests`) —
|
* Infrastructure adapter for the telefoonwijziging POST (`/api/v1/change-requests`) —
|
||||||
* the single place the network client lives for contact changes, so the command
|
* the single place the network client lives for contact changes, so the command
|
||||||
* and the UI never touch `ApiClient`. The BRP address is authoritative and not
|
* and the UI never touch `ApiClient`. The BRP address is authoritative and not
|
||||||
* submitted (WP-34); only the phone number is. Returns the server reference; the
|
* submitted; only the phone number is. Returns the server reference; the
|
||||||
* server re-validates and is the authority.
|
* server re-validates and is the authority.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { TYPE_LABELS, statusLabel, referentie } from '@registratie/domain/aanvra
|
|||||||
import { AdminCasesStore } from '@registratie/application/admin-cases.store';
|
import { AdminCasesStore } from '@registratie/application/admin-cases.store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin page: every case across all owners, with an admin delete (WP-36). Lives in
|
* Admin page: every case across all owners, with an admin delete. Lives in
|
||||||
* `registratie` (which owns the Aanvraag aggregate) — the back-office counterpart of the
|
* `registratie` (which owns the Aanvraag aggregate) — the back-office counterpart of the
|
||||||
* user's dashboard, reusing the same view labels + trust-boundary parse. Deny-by-default
|
* user's dashboard, reusing the same view labels + trust-boundary parse. Deny-by-default
|
||||||
* capability gate (`cases:manage`): a denial alert for non-admins, the list for admins.
|
* capability gate (`cases:manage`): a denial alert for non-admins, the list for admins.
|
||||||
@@ -109,7 +109,7 @@ export class AdminCasesPage {
|
|||||||
private loadRequested = false;
|
private loadRequested = false;
|
||||||
constructor() {
|
constructor() {
|
||||||
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise).
|
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise).
|
||||||
// Depends only on canManage() + a plain flag — never the store model (WP-26 loop lesson).
|
// Depends only on canManage() + a plain flag — never the store model (the loop lesson).
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (this.canManage() && !this.loadRequested) {
|
if (this.canManage() && !this.loadRequested) {
|
||||||
this.loadRequested = true;
|
this.loadRequested = true;
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import { createSubmitChangeRequest } from '@registratie/application/submit-chang
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Organism: contact-change (telefoonwijziging) form. The BRP address is authoritative
|
* Organism: contact-change (telefoonwijziging) form. The BRP address is authoritative
|
||||||
* and shown READ-ONLY (WP-34) — you change your address at the gemeente, not here — so
|
* and shown READ-ONLY — you change your address at the gemeente, not here — so
|
||||||
* only the phone number is editable. Uses the SAME idiom as the wizards: all state in
|
* only the phone number is editable. Uses the SAME idiom as the wizards: all state in
|
||||||
* one signal driven by the pure `reduce` (change-request.machine.ts), submitted via a
|
* one signal driven by the pure `reduce` (change-request.machine.ts), submitted via a
|
||||||
* `submit-*` command returning `Result`. The server re-validates.
|
* `submit-*` command returning `Result`. The server re-validates.
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export class DebugStateComponent {
|
|||||||
pendingHerregistratie: this.profileStore?.pendingHerregistratie(),
|
pendingHerregistratie: this.profileStore?.pendingHerregistratie(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Dev switchers (WP-33): flip role/scenario without hand-editing the URL. Both are
|
// Dev switchers flip role/scenario without hand-editing the URL. Both are
|
||||||
// read per-request in interceptors, so a reload re-runs them and re-fetches decisions.
|
// read per-request in interceptors, so a reload re-runs them and re-fetches decisions.
|
||||||
protected readonly roles = ROLES;
|
protected readonly roles = ROLES;
|
||||||
protected readonly scenarios = SCENARIOS;
|
protected readonly scenarios = SCENARIOS;
|
||||||
@@ -158,7 +158,7 @@ export class DebugStateComponent {
|
|||||||
this.applyAndReload();
|
this.applyAndReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip the dev params from the URL before reloading (WP-37) so a stale ?scenario=/?role=
|
// Strip the dev params from the URL before reloading so a stale ?scenario=/?role=
|
||||||
// in the address bar can't override the value the switcher just stored (currentScenario/
|
// in the address bar can't override the value the switcher just stored (currentScenario/
|
||||||
// currentRole read the URL first) — otherwise a switch to "default"/"drafter" gets stuck.
|
// currentRole read the URL first) — otherwise a switch to "default"/"drafter" gets stuck.
|
||||||
private applyAndReload(): void {
|
private applyAndReload(): void {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Tiny, dependency-free TS highlighter for the teaching showcase (WP-39). Escapes HTML,
|
* Tiny, dependency-free TS highlighter for the teaching showcase. Escapes HTML,
|
||||||
* then wraps line-comments, strings, and a fixed keyword set in `.c`/`.s`/`.k` spans (the
|
* then wraps line-comments, strings, and a fixed keyword set in `.c`/`.s`/`.k` spans (the
|
||||||
* classes `concepts.page` styles). Deliberately naive — good enough for the short, curated
|
* classes `concepts.page` styles). Deliberately naive — good enough for the short, curated
|
||||||
* snippets shown here; not a real tokenizer. Input is always our OWN source (extracted by
|
* snippets shown here; not a real tokenizer. Input is always our OWN source (extracted by
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
url(../fonts|icons|images) refs resolve against the vendored folder at runtime.
|
url(../fonts|icons|images) refs resolve against the vendored folder at runtime.
|
||||||
Licensed Rijksoverheid fonts are not used — styles.scss overrides the stack to system-ui. -->
|
Licensed Rijksoverheid fonts are not used — styles.scss overrides the stack to system-ui. -->
|
||||||
<link rel="stylesheet" href="cibg-huisstijl/css/huisstijl.min.css" />
|
<link rel="stylesheet" href="cibg-huisstijl/css/huisstijl.min.css" />
|
||||||
<!-- The letter-rendering contract (WP-24): shared verbatim with the backend's
|
<!-- The letter-rendering contract: shared verbatim with the backend's
|
||||||
HTML preview renderer (WP-25 inlines this same file) — keep it self-contained. -->
|
HTML preview renderer (the backend inlines this same file) — keep it self-contained. -->
|
||||||
<link rel="stylesheet" href="letter.css" />
|
<link rel="stylesheet" href="letter.css" />
|
||||||
</head>
|
</head>
|
||||||
<!-- brand--cibg activates CIBG's official palette: robijn layout chrome + lintblauw accents
|
<!-- brand--cibg activates CIBG's official palette: robijn layout chrome + lintblauw accents
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# RD-18 — Strip the `WP-`/`RB-` ticket references from `apps/` and `libs/`
|
||||||
|
|
||||||
|
Status: done
|
||||||
|
Source: PLAN.md Phase 2, item 4
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
204 `WP-NN`/`RB-NN` references sit in comments across 104 files in `apps/` and `libs/`. They
|
||||||
|
record which ticket introduced a line. `git blame` already records that, and it stays correct
|
||||||
|
when the code moves. The comment does not: it names a closed ticket, and the reader who chases
|
||||||
|
it learns nothing the surrounding sentence did not already say.
|
||||||
|
|
||||||
|
The sweep removes the reference and keeps the sentence. It changes no behaviour.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
|
||||||
|
- `libs/shared/docs/a11y.mdx` lines 14, 20, 28 and 68 — three references to strip and one to
|
||||||
|
keep, in one file. It is the clearest example of the rule in decision 2.
|
||||||
|
- `libs/shared/src/ui/task-list/task-list.stories.ts:17` — an exempt reference, and why.
|
||||||
|
- `scripts/gen-behaviour-spec.mjs:183` — the generated document's header text.
|
||||||
|
- The README's rule: "No ticket leaves a check disabled without an inline reason **and** a
|
||||||
|
reference to the ticket that removes it."
|
||||||
|
|
||||||
|
## Decisions (pre-made, don't relitigate)
|
||||||
|
|
||||||
|
1. **Strip `WP-NN` and `RB-NN`. Keep every `ADR-000x`.** ADR references point at documents
|
||||||
|
that exist and that the reader must read. There are 50 of them in scope; the count must not
|
||||||
|
move. `CD-` does not appear anywhere in this repo — do not go looking for it.
|
||||||
|
|
||||||
|
2. **Eleven references are exempt, in six files.** A reference survives when it names an
|
||||||
|
obligation that is not yet discharged, rather than recording history:
|
||||||
|
|
||||||
|
| File | Refs | Why exempt |
|
||||||
|
| ----------------------------------------------------------------- | ---- | ----------------------------------- |
|
||||||
|
| `registratie/ui/aanvraag-block/aanvraag-block.stories.ts` | 2 | justifies `a11y: { disable: true }` |
|
||||||
|
| `registratie/ui/dashboard/wat-moet-ik-regelen.section.stories.ts` | 2 | justifies `a11y: { disable: true }` |
|
||||||
|
| `libs/shared/src/ui/choice-link/choice-link.stories.ts` | 2 | justifies `a11y: { disable: true }` |
|
||||||
|
| `libs/shared/src/ui/choice-list/choice-list.stories.ts` | 2 | justifies `a11y: { disable: true }` |
|
||||||
|
| `libs/shared/src/ui/task-list/task-list.stories.ts` | 2 | justifies `a11y: { disable: true }` |
|
||||||
|
| `libs/shared/docs/a11y.mdx` line 68 only | 1 | points at WP-13's marker convention |
|
||||||
|
|
||||||
|
The five story files each disable an accessibility check. The README forbids leaving a check
|
||||||
|
disabled without naming the ticket that removes it, so stripping WP-11 there would break a
|
||||||
|
rule this arc enforces. **Do not edit those five files at all.**
|
||||||
|
|
||||||
|
`a11y.mdx` is edited: lines 14, 20 and 28 are provenance and go; line 68 stays.
|
||||||
|
|
||||||
|
3. **Thirteen references live in `describe`/`it` titles.** Strip them there too. A spec title
|
||||||
|
is documentation the generator publishes, so the reference reaches
|
||||||
|
`libs/shared/docs/behaviour-spec.mdx` and shows up in the rendered behaviour spec.
|
||||||
|
|
||||||
|
4. **Never hand-edit `behaviour-spec.mdx`.** It is generated. Fix the spec titles, then run
|
||||||
|
`npm run gen:behaviour-spec`. Its 14 references disappear on their own — except one.
|
||||||
|
|
||||||
|
5. **The exception in decision 4 is the generated file's header.**
|
||||||
|
`behaviour-spec.mdx:4` says "the compile-time guarantees WP-70 bought". That sentence comes
|
||||||
|
from `scripts/gen-behaviour-spec.mjs:183`, not from a spec. Edit the generator. Fix the
|
||||||
|
duplicate of the same sentence in that file's own header comment at line 4 while you are
|
||||||
|
there. This is the one file outside `apps/`+`libs/` that this ticket touches.
|
||||||
|
|
||||||
|
6. **Two sentences need a rewrite, not a deletion.** Deleting the reference alone leaves them
|
||||||
|
meaningless:
|
||||||
|
- `subject.interceptor.spec.ts:45` — the title ends "(WP-33-style stickiness)". The
|
||||||
|
reference _is_ the adjective. Rewrite the parenthetical as "(sticky per tab)".
|
||||||
|
- `apps/ssp/src/index.html:14` — "(WP-25 inlines this same file)". The subject is the
|
||||||
|
backend renderer. Rewrite as "(the backend inlines this same file)".
|
||||||
|
|
||||||
|
7. **Keep the sentence readable, not merely shorter.** `passage-picker.component.ts:13-14`
|
||||||
|
reads "Superseded by `besluit-panel` (WP-27's guided drafting)" and "no consumer left …
|
||||||
|
(WP-28 audit)". Strip to "Superseded by `besluit-panel`'s guided drafting" and "no consumer
|
||||||
|
left … outside its own story". A stripped line must not leave an empty `()`, a stranded
|
||||||
|
"see", or a doubled space.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
`apps/` and `libs/`, minus the five exempt story files, plus
|
||||||
|
`scripts/gen-behaviour-spec.mjs`. About 99 files change.
|
||||||
|
|
||||||
|
This is the one ticket type where an acceptance command **should** address a whole directory
|
||||||
|
rather than a file list: the sweep's contract is "no reference survives in `apps` or `libs`
|
||||||
|
except the listed eleven". Enumerating 99 files would restate the sweep, not check it.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Strip the references outside the six files in decision 2. Work directory by directory so
|
||||||
|
the diff stays reviewable.
|
||||||
|
2. Apply the rewrites in decisions 6 and 7.
|
||||||
|
3. Edit `scripts/gen-behaviour-spec.mjs` per decision 5.
|
||||||
|
4. Run `npm run gen:behaviour-spec`.
|
||||||
|
5. `git add -A`, then run the acceptance commands.
|
||||||
|
6. Update this ticket's `Status:` to `done` and the README's RD-18 row to `done`.
|
||||||
|
7. Commit all of it together.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
Measured against the tree before handover. `git grep -o … | wc -l` counts **occurrences**;
|
||||||
|
`git grep -c` would count lines and give a different, wrong number.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -oE "\b(WP|RB)-[0-9]+" -- apps libs | wc -l # is 204 -> MUST be 11
|
||||||
|
```
|
||||||
|
|
||||||
|
Those eleven are exactly the exempt ones, all inside the six files of decision 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -hoE "\b(WP|RB)-[0-9]+" -- \
|
||||||
|
apps/ssp/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts \
|
||||||
|
apps/ssp/src/app/registratie/ui/dashboard/wat-moet-ik-regelen.section.stories.ts \
|
||||||
|
libs/shared/src/ui/choice-link/choice-link.stories.ts \
|
||||||
|
libs/shared/src/ui/choice-list/choice-list.stories.ts \
|
||||||
|
libs/shared/src/ui/task-list/task-list.stories.ts \
|
||||||
|
libs/shared/docs/a11y.mdx | wc -l # is 14 -> MUST be 11
|
||||||
|
```
|
||||||
|
|
||||||
|
The five story files are untouched, so their disables keep their justification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --name-only HEAD -- '*.stories.ts' | grep -cE "aanvraag-block|wat-moet-ik-regelen|choice-link|choice-list|task-list" # MUST be 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The ADR references all survive, and the sweep leaves no damaged prose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -oE "ADR-[0-9]+" -- apps libs | wc -l # unchanged: 50
|
||||||
|
git grep -nE "^\s*(//|\*|1\.|\|).*\s\(\)" -- apps libs | wc -l # unchanged: 0
|
||||||
|
git grep -nE "^\s*(//|\*).*[a-z] [a-z]" -- apps libs | wc -l # unchanged: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run ci --full # exits 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
**`--full` is required, although the README's Order table leaves that column blank for this
|
||||||
|
ticket.** The table is wrong here: the sweep edits four `.mdx` files (`a11y.mdx`,
|
||||||
|
`cibg-gaps.mdx`, `layers.mdx`, and the generated `behaviour-spec.mdx`), and the README's own
|
||||||
|
rule says an `.mdx` edit needs `--full`. Fix the column to `yes` in the same commit.
|
||||||
|
|
||||||
|
The behaviour-spec drift check inside `npm run ci` is what proves step 4 was run. If it fails,
|
||||||
|
you edited `behaviour-spec.mdx` by hand or skipped the regeneration.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- `backend/` — 370 references across 86 files. RD-19 owns them.
|
||||||
|
- `docs/`, `.claude/`, `e2e/`, `scripts/` — except the one generator file in decision 5.
|
||||||
|
- The five exempt story files (decision 2). Fixing the markup that makes their a11y disables
|
||||||
|
necessary is WP-11's job, not this ticket's.
|
||||||
|
- Rewording a comment beyond what removing the reference requires. This ticket is a sweep, not
|
||||||
|
a documentation pass.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **`git grep`, never `grep -r`.** `grep -r` reaches gitignored build output and inflates every
|
||||||
|
count.
|
||||||
|
- **A reference inside a spec title changes a generated document.** Strip the title, then
|
||||||
|
regenerate. Editing the `.mdx` directly leaves the spec title unchanged, and the next
|
||||||
|
regeneration silently puts the reference back.
|
||||||
|
- **The generator's header is not a spec title** (decision 5). It is the one reference that
|
||||||
|
regeneration cannot remove, because it comes from `scripts/`.
|
||||||
|
- **Do not open the five exempt story files.** An edit there is the one way this ticket can
|
||||||
|
break a rule the README enforces.
|
||||||
|
- **204, not the 181 that PLAN records.** The arc's own commits added references since PLAN was
|
||||||
|
measured. Trust the command, not the prose.
|
||||||
@@ -112,7 +112,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
|||||||
| RD-15 | Remove 22 abandoned agent worktrees (4.7 GB) | 01 | | done |
|
| RD-15 | Remove 22 abandoned agent worktrees (4.7 GB) | 01 | | done |
|
||||||
| RD-16 | ~~`parseDashboardView` returns `BigProfile`~~ — DROPPED, see PLAN.md 2.2 | 01 | | n/a |
|
| RD-16 | ~~`parseDashboardView` returns `BigProfile`~~ — DROPPED, see PLAN.md 2.2 | 01 | | n/a |
|
||||||
| RD-17 | `successOf`/`successOr` sweep — 10 sites, 8 files | 01 | | done |
|
| RD-17 | `successOf`/`successOr` sweep — 10 sites, 8 files | 01 | | done |
|
||||||
| RD-18 | Ticket-reference sweep, frontend — 181 refs, 100 files | 01 | | todo |
|
| RD-18 | Ticket-reference sweep, frontend — 181 refs, 100 files | 01 | yes | done |
|
||||||
| RD-19 | Ticket-reference sweep, backend — 370 refs, 86 files | 01 | | todo |
|
| RD-19 | Ticket-reference sweep, backend — 370 refs, 86 files | 01 | | todo |
|
||||||
| RD-20 | `wizard-errors.ts` + spec, adopted by all 3 wizards | 02 | | todo |
|
| RD-20 | `wizard-errors.ts` + spec, adopted by all 3 wizards | 02 | | todo |
|
||||||
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | todo |
|
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | todo |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { AuditAdapter, parseAuditEntries } from '@beheer/infrastructure/audit.ad
|
|||||||
type Err = Error | undefined;
|
type Err = Error | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin view of the persisted authz/PII-reveal audit trail (WP-41/42). One root singleton
|
* Admin view of the persisted authz/PII-reveal audit trail. One root singleton
|
||||||
* owning the list as a RemoteData signal, parsed at the trust boundary. Read-only.
|
* owning the list as a RemoteData signal, parsed at the trust boundary. Read-only.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function setup(blobPresenter?: BlobPresenter): StamdataStore {
|
|||||||
return TestBed.inject(StamdataStore);
|
return TestBed.inject(StamdataStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('StamdataStore undo/redo (WP-32)', () => {
|
describe('StamdataStore undo/redo', () => {
|
||||||
it('records a cell edit, undoes and redoes it', async () => {
|
it('records a cell edit, undoes and redoes it', async () => {
|
||||||
const store = setup();
|
const store = setup();
|
||||||
await store.load();
|
await store.load();
|
||||||
@@ -82,10 +82,10 @@ describe('StamdataStore undo/redo (WP-32)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- RB-28 (TE-006): download() ends in BLOB_PRESENTER.download, not raw DOM calls,
|
// --- TE-006: download() ends in BLOB_PRESENTER.download, not raw DOM calls,
|
||||||
// so the seam makes both the guard's branches and the success path assertable. ---
|
// so the seam makes both the guard's branches and the success path assertable. ---
|
||||||
|
|
||||||
describe('StamdataStore.download (RB-28)', () => {
|
describe('StamdataStore.download', () => {
|
||||||
it('does not call the presenter while the two-clause guard blocks (nothing dirty yet)', async () => {
|
it('does not call the presenter while the two-clause guard blocks (nothing dirty yet)', async () => {
|
||||||
// Given a freshly loaded table with no edits — canDownload() is false.
|
// Given a freshly loaded table with no edits — canDownload() is false.
|
||||||
const { presenter, downloaded } = fakeBlobPresenter();
|
const { presenter, downloaded } = fakeBlobPresenter();
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export class StamdataStore {
|
|||||||
this.previewDate.set(date);
|
this.previewDate.set(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Undo/redo over the edited rows (WP-32): the document snapshot is `rows`; restore via
|
/** Undo/redo over the edited rows: the document snapshot is `rows`; restore via
|
||||||
the existing `Seed` msg. Only real edits are recorded (a no-op reduce leaves no step). */
|
the existing `Seed` msg. Only real edits are recorded (a no-op reduce leaves no step). */
|
||||||
private history = createHistory<readonly StamRow[]>(50);
|
private history = createHistory<readonly StamRow[]>(50);
|
||||||
readonly canUndo = this.history.canUndo;
|
readonly canUndo = this.history.canUndo;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** One authz/PII-reveal audit row as the FE sees it (WP-41 backend → WP-42 view). Pure
|
/** One authz/PII-reveal audit row as the FE sees it. Pure
|
||||||
type; data-minimised (no PII) by construction on the server. */
|
type; data-minimised (no PII) by construction on the server. */
|
||||||
export interface AuditEntry {
|
export interface AuditEntry {
|
||||||
at: string; // ISO timestamp
|
at: string; // ISO timestamp
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type { AuthzAuditDto } from '@shared/infrastructure/api-client';
|
|||||||
import { AuditEntry } from '@beheer/domain/audit-entry';
|
import { AuditEntry } from '@beheer/domain/audit-entry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infrastructure adapter for the admin authz/PII-reveal audit trail (`GET /admin/audit`,
|
* Infrastructure adapter for the admin authz/PII-reveal audit trail (`GET /admin/audit`).
|
||||||
* WP-41). The single place the ApiClient lives for audit; the store parses at the boundary.
|
* The single place the ApiClient lives for audit; the store parses at the boundary.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuditAdapter {
|
export class AuditAdapter {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { successOr } from '@shared/application/remote-data';
|
|||||||
import { AuditStore } from '@beheer/application/audit.store';
|
import { AuditStore } from '@beheer/application/audit.store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin page: the persisted authz/PII-reveal audit trail (WP-41/42) — data-minimised, no PII.
|
* Admin page: the persisted authz/PII-reveal audit trail — data-minimised, no PII.
|
||||||
* Deny-by-default capability gate (`cases:manage`, reused for admin audit read). Read-only table.
|
* Deny-by-default capability gate (`cases:manage`, reused for admin audit read). Read-only table.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AccessStore } from '@shared/application/access.store';
|
|||||||
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin page: toggle runtime feature flags (WP-47). Deny-by-default capability gate
|
* Admin page: toggle runtime feature flags. Deny-by-default capability gate
|
||||||
* (`flags:manage`). The catalog is server-owned (code); this only flips the on/off state, which
|
* (`flags:manage`). The catalog is server-owned (code); this only flips the on/off state, which
|
||||||
* the whole app reads via the same `FeatureFlagStore`.
|
* the whole app reads via the same `FeatureFlagStore`.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -242,13 +242,13 @@ export class StamdataTableEditorComponent {
|
|||||||
protected expireLabel = $localize`:@@beheer.expire:Sluiten per vandaag`;
|
protected expireLabel = $localize`:@@beheer.expire:Sluiten per vandaag`;
|
||||||
private removeConfirm = $localize`:@@beheer.removeConfirm:Rij verwijderen? Als andere gegevens ernaar verwijzen, faalt de build-controle (CI). Bij een tabel met een geldigheidsperiode kunt u de rij beter sluiten (geldig tot) in plaats van verwijderen.`;
|
private removeConfirm = $localize`:@@beheer.removeConfirm:Rij verwijderen? Als andere gegevens ernaar verwijzen, faalt de build-controle (CI). Bij een tabel met een geldigheidsperiode kunt u de rij beter sluiten (geldig tot) in plaats van verwijderen.`;
|
||||||
|
|
||||||
/** Deletions can orphan a reference (the CI gate catches it); confirm first (WP-48). */
|
/** Deletions can orphan a reference (the CI gate catches it); confirm first. */
|
||||||
protected onRemove(index: number) {
|
protected onRemove(index: number) {
|
||||||
if (confirm(this.removeConfirm)) this.rowRemoved.emit(index);
|
if (confirm(this.removeConfirm)) this.rowRemoved.emit(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Steer temporal tables toward expiring (close the validity per today) over hard delete —
|
/** Steer temporal tables toward expiring (close the validity per today) over hard delete —
|
||||||
preserves history and can't orphan a reference that was valid earlier (WP-48). */
|
preserves history and can't orphan a reference that was valid earlier. */
|
||||||
protected onExpire(index: number) {
|
protected onExpire(index: number) {
|
||||||
const col = this.table().columns.find((c) => /geldigtot/i.test(c.name));
|
const col = this.table().columns.find((c) => /geldigtot/i.test(c.name));
|
||||||
if (col) this.cellEdited.emit({ row: index, column: col.name, value: this.today });
|
if (col) this.cellEdited.emit({ row: index, column: col.name, value: this.today });
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export class StamdataPage {
|
|||||||
constructor() {
|
constructor() {
|
||||||
// Load once the capability resolves to `allowed` (a 403 GET would be wasted otherwise).
|
// Load once the capability resolves to `allowed` (a 403 GET would be wasted otherwise).
|
||||||
// Depends only on canEdit() + a plain flag — never on the store model, so dispatching
|
// Depends only on canEdit() + a plain flag — never on the store model, so dispatching
|
||||||
// `Loading` inside load() can't retrigger this effect (the WP-26 runaway-loop lesson).
|
// `Loading` inside load() can't retrigger this effect (the runaway-loop lesson).
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (this.canEdit() && !this.loadRequested) {
|
if (this.canEdit() && !this.loadRequested) {
|
||||||
this.loadRequested = true;
|
this.loadRequested = true;
|
||||||
@@ -92,7 +92,7 @@ export class StamdataPage {
|
|||||||
void this.store.load();
|
void this.store.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ctrl/Cmd+Z undo, Ctrl/Cmd+Shift+Z redo (WP-32). Ignored while focus is in a grid
|
/** Ctrl/Cmd+Z undo, Ctrl/Cmd+Shift+Z redo. Ignored while focus is in a grid
|
||||||
cell input so the browser's native text-undo still works there (mirrors brief.page). */
|
cell input so the browser's native text-undo still works there (mirrors brief.page). */
|
||||||
protected onKeydown(e: KeyboardEvent) {
|
protected onKeydown(e: KeyboardEvent) {
|
||||||
if (!this.canEdit() || !(e.ctrlKey || e.metaKey) || (e.key !== 'z' && e.key !== 'Z')) return;
|
if (!this.canEdit() || !(e.ctrlKey || e.metaKey) || (e.key !== 'z' && e.key !== 'Z')) return;
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ what the ones below/above it can't.
|
|||||||
|
|
||||||
## The layers
|
## The layers
|
||||||
|
|
||||||
1. **Axe on every story** (WP-01) — `@storybook/addon-a11y` in the panel, plus
|
1. **Axe on every story** — `@storybook/addon-a11y` in the panel, plus
|
||||||
`@storybook/test-runner` + `axe-playwright` gating CI (`npm run test-storybook:ci`).
|
`@storybook/test-runner` + `axe-playwright` gating CI (`npm run test-storybook:ci`).
|
||||||
Catches structural/contrast/ARIA-shape violations on every component, automatically,
|
Catches structural/contrast/ARIA-shape violations on every component, automatically,
|
||||||
as soon as a story exists. Escape hatch: `parameters: { a11y: { disable: true } }`,
|
as soon as a story exists. Escape hatch: `parameters: { a11y: { disable: true } }`,
|
||||||
only with an inline justification comment + a cross-reference to the WP that will fix
|
only with an inline justification comment + a cross-reference to the WP that will fix
|
||||||
it (see e.g. `task-list.stories.ts`).
|
it (see e.g. `task-list.stories.ts`).
|
||||||
2. **Template a11y lint** (WP-17) — `angular-eslint`'s `templateAccessibility` config
|
2. **Template a11y lint** — `angular-eslint`'s `templateAccessibility` config
|
||||||
(`alt-text`, `label-has-associated-control`, `click`/`mouse-events-have-key-events`,
|
(`alt-text`, `label-has-associated-control`, `click`/`mouse-events-have-key-events`,
|
||||||
`interactive-supports-focus`, `valid-aria`, `no-autofocus`, …) running on every inline
|
`interactive-supports-focus`, `valid-aria`, `no-autofocus`, …) running on every inline
|
||||||
template via `angular.processInlineTemplates` (this repo has no `.html` files — every
|
template via `angular.processInlineTemplates` (this repo has no `.html` files — every
|
||||||
@@ -25,7 +25,7 @@ what the ones below/above it can't.
|
|||||||
into a virtual file the template rules can lint). Catches missing alt text, unlabelled
|
into a virtual file the template rules can lint). Catches missing alt text, unlabelled
|
||||||
controls, and interactive elements that can't be reached by keyboard — at lint time,
|
controls, and interactive elements that can't be reached by keyboard — at lint time,
|
||||||
before a story even exists.
|
before a story even exists.
|
||||||
3. **Play tests** (WP-16) — Storybook stories assert the wiring axe/lint can't see:
|
3. **Play tests** — Storybook stories assert the wiring axe/lint can't see:
|
||||||
`form-field.stories.ts`'s canonical composition asserts `aria-describedby` joins
|
`form-field.stories.ts`'s canonical composition asserts `aria-describedby` joins
|
||||||
`-desc`/`-error` in the right order; `alert.stories.ts` asserts `role="alert"` for
|
`-desc`/`-error` in the right order; `alert.stories.ts` asserts `role="alert"` for
|
||||||
errors vs `role="status"` for info/ok/warning. These run as part of the same
|
errors vs `role="status"` for info/ok/warning. These run as part of the same
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{/* GENERATED by `npm run gen:behaviour-spec` (scripts/gen-behaviour-spec.mjs) — do not
|
{/* GENERATED by `npm run gen:behaviour-spec` (scripts/gen-behaviour-spec.mjs) — do not
|
||||||
edit. Every bullet below is a real `it()` title or backend test method name, extracted
|
edit. Every bullet below is a real `it()` title or backend test method name, extracted
|
||||||
verbatim from the suite. The team rejected Cucumber/Gherkin for BDD scenarios (runtime string
|
verbatim from the suite. The team rejected Cucumber/Gherkin for BDD scenarios (runtime string
|
||||||
matching undoes the compile-time guarantees WP-70 bought, and needs two frameworks for
|
matching undoes the compile-time guarantees the TypeScript compiler bought, and needs two frameworks for
|
||||||
.NET+TS) — this page is the replacement: business-readable documentation generated FROM test
|
.NET+TS) — this page is the replacement: business-readable documentation generated FROM test
|
||||||
names, so it can never drift from what the suite actually asserts. A test name changing (or a
|
names, so it can never drift from what the suite actually asserts. A test name changing (or a
|
||||||
test being added/removed) is the only way this page changes; hand-editing it is pointless,
|
test being added/removed) is the only way this page changes; hand-editing it is pointless,
|
||||||
@@ -108,13 +108,13 @@ classes.
|
|||||||
|
|
||||||
### beheer
|
### beheer
|
||||||
|
|
||||||
#### StamdataStore undo/redo (WP-32)
|
#### StamdataStore undo/redo
|
||||||
|
|
||||||
- records a cell edit, undoes and redoes it
|
- records a cell edit, undoes and redoes it
|
||||||
- records addRow and undoes it
|
- records addRow and undoes it
|
||||||
- clears history when switching table
|
- clears history when switching table
|
||||||
|
|
||||||
#### StamdataStore.download (RB-28)
|
#### StamdataStore.download
|
||||||
|
|
||||||
- does not call the presenter while the two-clause guard blocks (nothing dirty yet)
|
- does not call the presenter while the two-clause guard blocks (nothing dirty yet)
|
||||||
- does not call the presenter while previewing a date, even with edits
|
- does not call the presenter while previewing a date, even with edits
|
||||||
@@ -186,14 +186,14 @@ classes.
|
|||||||
- flushes a pending debounced edit immediately and clears the pending flag
|
- flushes a pending debounced edit immediately and clears the pending flag
|
||||||
- is a no-op when no edit is pending
|
- is a no-op when no edit is pending
|
||||||
|
|
||||||
#### BriefStore.load — 404 tolerance (RB-22)
|
#### BriefStore.load — 404 tolerance
|
||||||
|
|
||||||
- a 404 drives exactly one reset(), which populates the store
|
- a 404 drives exactly one reset(), which populates the store
|
||||||
- a second 404 does not drive a second reset()
|
- a second 404 does not drive a second reset()
|
||||||
|
|
||||||
#### BriefStore.previewLetter
|
#### BriefStore.previewLetter
|
||||||
|
|
||||||
- opens the composed letter via BLOB_PRESENTER on success (RB-28)
|
- opens the composed letter via BLOB_PRESENTER on success
|
||||||
- surfaces the error without opening a tab on failure
|
- surfaces the error without opening a tab on failure
|
||||||
|
|
||||||
#### BriefStore.revealBigNummer (PRD-0002 §5c)
|
#### BriefStore.revealBigNummer (PRD-0002 §5c)
|
||||||
@@ -206,7 +206,7 @@ classes.
|
|||||||
- sends no X-Role/X-Subject headers outside isDevMode()
|
- sends no X-Role/X-Subject headers outside isDevMode()
|
||||||
- sends X-Role (and X-Subject when known) under isDevMode()
|
- sends X-Role (and X-Subject when known) under isDevMode()
|
||||||
|
|
||||||
#### OrgTemplateStore.proefbrief (RB-28)
|
#### OrgTemplateStore.proefbrief
|
||||||
|
|
||||||
- opens the rendered proefbrief via BLOB_PRESENTER on success
|
- opens the rendered proefbrief via BLOB_PRESENTER on success
|
||||||
- surfaces the error without opening a tab on failure
|
- surfaces the error without opening a tab on failure
|
||||||
@@ -372,7 +372,7 @@ classes.
|
|||||||
- low uren requires the scholing question, and punten only once scholing is followed
|
- low uren requires the scholing question, and punten only once scholing is followed
|
||||||
- buitenland gewerkt requires land and hours abroad before advancing
|
- buitenland gewerkt requires land and hours abroad before advancing
|
||||||
- gaNaarStap corrects an earlier answer without losing later ones
|
- gaNaarStap corrects an earlier answer without losing later ones
|
||||||
- raising uren above the threshold after answering scholing drops both fields (WP-69 §6)
|
- raising uren above the threshold after answering scholing drops both fields
|
||||||
- SetPolicy (server-owned threshold) can turn an already-answered uren into one that now requires scholing
|
- SetPolicy (server-owned threshold) can turn an already-answered uren into one that now requires scholing
|
||||||
|
|
||||||
#### intake hasProgress
|
#### intake hasProgress
|
||||||
@@ -419,8 +419,8 @@ classes.
|
|||||||
- reaches Submitting ONLY with valid answers
|
- reaches Submitting ONLY with valid answers
|
||||||
- punten is required only when aanvullende scholing was gevolgd
|
- punten is required only when aanvullende scholing was gevolgd
|
||||||
- low hours requires the scholing answer before submit
|
- low hours requires the scholing answer before submit
|
||||||
- does not require punten for a hidden question (WP-69 §6)
|
- does not require punten for a hidden question
|
||||||
- drops punten when raising uren hides the question (WP-69 §6)
|
- drops punten when raising uren hides the question
|
||||||
- resolve maps Submitting to Submitted on a successful submit
|
- resolve maps Submitting to Submitted on a successful submit
|
||||||
- resolve maps Submitting to Failed on a failed submit
|
- resolve maps Submitting to Failed on a failed submit
|
||||||
|
|
||||||
@@ -501,7 +501,7 @@ classes.
|
|||||||
|
|
||||||
- resolves ok with the server response on success
|
- resolves ok with the server response on success
|
||||||
- folds a rejected submit into a Result error, never throwing
|
- folds a rejected submit into a Result error, never throwing
|
||||||
- recovers from a create conflict by adopting the existing Concept (WP-35)
|
- recovers from a create conflict by adopting the existing Concept
|
||||||
|
|
||||||
#### createSubmitChangeRequest
|
#### createSubmitChangeRequest
|
||||||
|
|
||||||
@@ -863,8 +863,8 @@ classes.
|
|||||||
|
|
||||||
- parses a known capability list
|
- parses a known capability list
|
||||||
- parses an empty list (drafter — no capabilities)
|
- parses an empty list (drafter — no capabilities)
|
||||||
- recognizes the admin org-template capability (WP-23)
|
- recognizes the admin org-template capability
|
||||||
- recognizes the behandelportal besluit capability (WP-66)
|
- recognizes the behandelportal besluit capability
|
||||||
- drops unrecognized capability strings instead of rejecting the response
|
- drops unrecognized capability strings instead of rejecting the response
|
||||||
- rejects malformed responses instead of trusting them
|
- rejects malformed responses instead of trusting them
|
||||||
|
|
||||||
@@ -950,7 +950,7 @@ classes.
|
|||||||
- falls back to default when nothing is set or the value is invalid
|
- falls back to default when nothing is set or the value is invalid
|
||||||
- setScenario persists the chosen scenario
|
- setScenario persists the chosen scenario
|
||||||
|
|
||||||
#### stripDevParams (WP-37)
|
#### stripDevParams
|
||||||
|
|
||||||
- removes ?scenario and ?role so the stored dev value wins on reload
|
- removes ?scenario and ?role so the stored dev value wins on reload
|
||||||
- keeps unrelated query params and the path/hash
|
- keeps unrelated query params and the path/hash
|
||||||
@@ -959,7 +959,7 @@ classes.
|
|||||||
#### subjectInterceptor
|
#### subjectInterceptor
|
||||||
|
|
||||||
- stamps X-Subject on an /api/v1/ request once ?subject= has been seen
|
- stamps X-Subject on an /api/v1/ request once ?subject= has been seen
|
||||||
- keeps stamping later requests on the same tab after the query param is gone (WP-33-style stickiness)
|
- keeps stamping later requests on the same tab after the query param is gone (sticky per tab)
|
||||||
- leaves a non-API request untouched even when a subject is known
|
- leaves a non-API request untouched even when a subject is known
|
||||||
- sends no header at all when no subject has ever been seen
|
- sends no header at all when no subject has ever been seen
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ placed above the `@Component` decorator, plus `parameters: { cibgGap: true }` an
|
|||||||
| --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `skeleton` | Laadindicatie | No loading-skeleton class in the vendored build. |
|
| `skeleton` | Laadindicatie | No loading-skeleton class in the vendored build. |
|
||||||
| `spinner` | Laadindicatie | No loading-spinner class in the vendored build. |
|
| `spinner` | Laadindicatie | No loading-spinner class in the vendored build. |
|
||||||
| `rich-text-editor` | Tekstgebied | No rich-text/WYSIWYG pattern; toolbar buttons still use vendored `.btn-ghost` (WP-10). |
|
| `rich-text-editor` | Tekstgebied | No rich-text/WYSIWYG pattern; toolbar buttons still use vendored `.btn-ghost`. |
|
||||||
| `wizard-shell` (error summary only) | Foutmelding | No error-summary/Veldvalidatie list class; renders inside a vendored `.feedback-error` alert. |
|
| `wizard-shell` (error summary only) | Foutmelding | No error-summary/Veldvalidatie list class; renders inside a vendored `.feedback-error` alert. |
|
||||||
| `application-link` (non-navigating row) | Aanvragen | The vendored `.dashboard-block.applications li a` chain only styles `<a>`; `.static-row` mirrors it from tokens for the informational (non-link) case. |
|
| `application-link` (non-navigating row) | Aanvragen | The vendored `.dashboard-block.applications li a` chain only styles `<a>`; `.static-row` mirrors it from tokens for the informational (non-link) case. |
|
||||||
| `debug-state` | n/a | Dev-only tool, deliberately off-theme — see the component's own `ponytail:` note. |
|
| `debug-state` | n/a | Dev-only tool, deliberately off-theme — see the component's own `ponytail:` note. |
|
||||||
@@ -36,7 +36,7 @@ placed above the `@Component` decorator, plus `parameters: { cibgGap: true }` an
|
|||||||
Not a gap: `confirmation` renders entirely with vendored `.confirmation*` classes (no `styles:
|
Not a gap: `confirmation` renders entirely with vendored `.confirmation*` classes (no `styles:
|
||||||
[...]` block) — its header comment names the pattern, no marker needed. The `upload/` suite
|
[...]` block) — its header comment names the pattern, no marker needed. The `upload/` suite
|
||||||
renders entirely with vendored classes (`.file-picker-drop-area`, `.btn-upload`, …) — reworked
|
renders entirely with vendored classes (`.file-picker-drop-area`, `.btn-upload`, …) — reworked
|
||||||
onto them rather than marked (see WP-11's correction note). `task-list`, `application-list`, and
|
onto them rather than marked. `task-list`, `application-list`, and
|
||||||
`choice-list` each wrap a distinct vendored pattern (Keuzelijst / Aanvragen / Keuzelijst) and name
|
`choice-list` each wrap a distinct vendored pattern (Keuzelijst / Aanvragen / Keuzelijst) and name
|
||||||
it in their own header comment — no marker needed, they don't hand-roll surface CSS.
|
it in their own header comment — no marker needed, they don't hand-roll surface CSS.
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ context, so the per-context scoping rule never applies to it in the first place.
|
|||||||
|
|
||||||
`npm run lint` (`eslint.config.mjs`) is a separate gate — mainly the `any`-free rule —
|
`npm run lint` (`eslint.config.mjs`) is a separate gate — mainly the `any`-free rule —
|
||||||
and no longer carries the import-boundary rules above (moved to dependency-cruiser,
|
and no longer carries the import-boundary rules above (moved to dependency-cruiser,
|
||||||
WP-38/WP-67, so they don't have to be hand-copied per context).
|
so they don't have to be hand-copied per context).
|
||||||
|
|
||||||
## The English/Dutch seam
|
## The English/Dutch seam
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface DebouncedSave {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The debounced-autosave timer shared by the editor stores (WP-31). It owns ONLY the timer
|
* The debounced-autosave timer shared by the editor stores. It owns ONLY the timer
|
||||||
* bookkeeping; the actual write + save-state transitions live in the caller's `flush`
|
* bookkeeping; the actual write + save-state transitions live in the caller's `flush`
|
||||||
* (store-specific — it touches that store's SaveState + adapter). The handle is
|
* (store-specific — it touches that store's SaveState + adapter). The handle is
|
||||||
* nulled the moment it fires, so `hasPendingSave()` means "a write is still owed". Integrates
|
* nulled the moment it fires, so `hasPendingSave()` means "a write is still owed". Integrates
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type Err = Error | undefined;
|
|||||||
const SET_FAILED = $localize`:@@flags.set.failed:De functievlag kon niet worden opgeslagen.`;
|
const SET_FAILED = $localize`:@@flags.set.failed:De functievlag kon niet worden opgeslagen.`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime feature-flag state (WP-47) — one root singleton, mirroring `AccessStore`. Loads the
|
* Runtime feature-flag state — one root singleton, mirroring `AccessStore`. Loads the
|
||||||
* resolved flag set once from `GET /flags`; `enabled(key)` gates a feature (deny-by-default:
|
* resolved flag set once from `GET /flags`; `enabled(key)` gates a feature (deny-by-default:
|
||||||
* false until loaded / unknown key). `set()` is the admin toggle (PUT + reload). The catalog is
|
* false until loaded / unknown key). `set()` is the admin toggle (PUT + reload). The catalog is
|
||||||
* server-owned; the FE only mirrors + renders it.
|
* server-owned; the FE only mirrors + renders it.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export interface History<T> {
|
|||||||
* restore a returned snapshot by re-dispatching a `Seed`-style Msg — this helper only
|
* restore a returned snapshot by re-dispatching a `Seed`-style Msg — this helper only
|
||||||
* shuffles references, it never mutates them, so the caller must hold copy-on-write state
|
* shuffles references, it never mutates them, so the caller must hold copy-on-write state
|
||||||
* (every edit produces a fresh value). Both stacks are capped so a long session can't grow
|
* (every edit produces a fresh value). Both stacks are capped so a long session can't grow
|
||||||
* unbounded. Extracted from BriefStore's WP-27 undo/redo (WP-31); reused by the stamdata
|
* unbounded. Extracted from BriefStore's undo/redo; reused by the stamdata
|
||||||
* editor (WP-32).
|
* editor.
|
||||||
*/
|
*/
|
||||||
export function createHistory<T>(cap = 50): History<T> {
|
export function createHistory<T>(cap = 50): History<T> {
|
||||||
const past = signal<readonly T[]>([]);
|
const past = signal<readonly T[]>([]);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function fromResource<T>(
|
|||||||
* Project an Elm-machine's load lifecycle onto `RemoteData`, for the `<app-async>` seam. The
|
* Project an Elm-machine's load lifecycle onto `RemoteData`, for the `<app-async>` seam. The
|
||||||
* machine keeps owning its own domain lifecycle (draft/submitted/…); this is purely the
|
* machine keeps owning its own domain lifecycle (draft/submitted/…); this is purely the
|
||||||
* Loading/Failed/Loaded → async mapping, which was byte-identical across BriefStore,
|
* Loading/Failed/Loaded → async mapping, which was byte-identical across BriefStore,
|
||||||
* OrgTemplateStore and StamdataStore (WP-31). A `RemoteData` constructor, not a sixth
|
* OrgTemplateStore and StamdataStore. A `RemoteData` constructor, not a sixth
|
||||||
* encoding — wrap the call in a `computed`.
|
* encoding — wrap the call in a `computed`.
|
||||||
*/
|
*/
|
||||||
export function fromLoadLifecycle<
|
export function fromLoadLifecycle<
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { currentIdempotencyKey } from '@shared/infrastructure/api-client.provide
|
|||||||
// So calling it twice inside the same `fn` tells us, behaviourally, whether a key was
|
// So calling it twice inside the same `fn` tells us, behaviourally, whether a key was
|
||||||
// minted for this call: two reads agreeing means one pending key was reused; two reads
|
// minted for this call: two reads agreeing means one pending key was reused; two reads
|
||||||
// disagreeing means there was no pending key at all — each call fell back to its own
|
// disagreeing means there was no pending key at all — each call fell back to its own
|
||||||
// random one. This is the seam RB-17 exists to keep separated, so it is asserted
|
// random one. This is a deliberate seam, kept separate so it can be asserted
|
||||||
// directly rather than via a mock (relative-import mocking is off-limits under this
|
// directly rather than via a mock (relative-import mocking is off-limits under this
|
||||||
// repo's Angular/vitest setup — see role.interceptor.spec.ts).
|
// repo's Angular/vitest setup — see role.interceptor.spec.ts).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* The letter workflow's acting role: drafter or approver for the two-person
|
* The letter workflow's acting role: drafter or approver for the two-person
|
||||||
* compose/review flow, admin for org-template management (WP-23, Brief v2).
|
* compose/review flow, admin for org-template management (Brief v2).
|
||||||
* A pure domain type (no framework, no reading mechanism) — the `?role=` reader and
|
* A pure domain type (no framework, no reading mechanism) — the `?role=` reader and
|
||||||
* the X-Role header live in shared/infrastructure/role.ts. Consumers (brief.store,
|
* the X-Role header live in shared/infrastructure/role.ts. Consumers (brief.store,
|
||||||
* letter-composer) depend on this type, not on how the role is obtained.
|
* letter-composer) depend on this type, not on how the role is obtained.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { stripDevParams } from './dev-params';
|
import { stripDevParams } from './dev-params';
|
||||||
|
|
||||||
describe('stripDevParams (WP-37)', () => {
|
describe('stripDevParams', () => {
|
||||||
it('removes ?scenario and ?role so the stored dev value wins on reload', () => {
|
it('removes ?scenario and ?role so the stored dev value wins on reload', () => {
|
||||||
expect(stripDevParams('http://localhost:4200/dashboard?scenario=slow&role=admin')).toBe(
|
expect(stripDevParams('http://localhost:4200/dashboard?scenario=slow&role=admin')).toBe(
|
||||||
'http://localhost:4200/dashboard',
|
'http://localhost:4200/dashboard',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Remove the dev-only `?scenario=` and `?role=` params from a URL (WP-37). Once the
|
* Remove the dev-only `?scenario=` and `?role=` params from a URL. Once the
|
||||||
* dev switcher (debug-state) has been used, sessionStorage is the authoritative source
|
* dev switcher (debug-state) has been used, sessionStorage is the authoritative source
|
||||||
* for both — `currentScenario()`/`currentRole()` read the URL FIRST, so a stale param
|
* for both — `currentScenario()`/`currentRole()` read the URL FIRST, so a stale param
|
||||||
* left in the address bar would override the switcher on reload (the "stuck on slow"
|
* left in the address bar would override the switcher on reload (the "stuck on slow"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ApiClient } from '@shared/infrastructure/api-client';
|
|||||||
import { FeatureFlag } from '@shared/domain/feature-flag';
|
import { FeatureFlag } from '@shared/domain/feature-flag';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infrastructure adapter for feature flags (WP-47): `GET /flags` (resolved set, drives FE gating)
|
* Infrastructure adapter for feature flags: `GET /flags` (resolved set, drives FE gating)
|
||||||
* and the admin `PUT /admin/flags/{key}`. The single place the ApiClient lives for flags; the
|
* and the admin `PUT /admin/flags/{key}`. The single place the ApiClient lives for flags; the
|
||||||
* store parses at the boundary.
|
* store parses at the boundary.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ describe('parseMe (trust boundary)', () => {
|
|||||||
expect(parseMe({ capabilities: [] })).toEqual({ ok: true, value: [] });
|
expect(parseMe({ capabilities: [] })).toEqual({ ok: true, value: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recognizes the admin org-template capability (WP-23)', () => {
|
it('recognizes the admin org-template capability', () => {
|
||||||
expect(parseMe({ capabilities: ['orgtemplate:edit'] })).toEqual({
|
expect(parseMe({ capabilities: ['orgtemplate:edit'] })).toEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
value: ['orgtemplate:edit'],
|
value: ['orgtemplate:edit'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regression: WP-66's `aanvraag:beoordelen` (behandelportal) shipped on the `Capability`
|
// Regression: `aanvraag:beoordelen` (behandelportal) shipped on the `Capability`
|
||||||
// type but was never added to this trust-boundary's runtime KNOWN list, so a real
|
// type but was never added to this trust-boundary's runtime KNOWN list, so a real
|
||||||
// behandelaar's `/me` response had the capability silently dropped and the werkvoorraad
|
// behandelaar's `/me` response had the capability silently dropped and the werkvoorraad
|
||||||
// page always denied — every `Capability` union member belongs in KNOWN too.
|
// page always denied — every `Capability` union member belongs in KNOWN too.
|
||||||
it('recognizes the behandelportal besluit capability (WP-66)', () => {
|
it('recognizes the behandelportal besluit capability', () => {
|
||||||
expect(parseMe({ capabilities: ['aanvraag:beoordelen'] })).toEqual({
|
expect(parseMe({ capabilities: ['aanvraag:beoordelen'] })).toEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
value: ['aanvraag:beoordelen'],
|
value: ['aanvraag:beoordelen'],
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe('roleInterceptor', () => {
|
|||||||
it.each([
|
it.each([
|
||||||
'/api/v1/brief',
|
'/api/v1/brief',
|
||||||
'/api/v1/admin/org-template',
|
'/api/v1/admin/org-template',
|
||||||
'/api/v1/stamdata', // WP-29: the admin stamdata reads 403 without X-Role
|
'/api/v1/stamdata', // the admin stamdata reads 403 without X-Role
|
||||||
'/api/v1/stamdata/professions?peildatum=1999-01-01',
|
'/api/v1/stamdata/professions?peildatum=1999-01-01',
|
||||||
'/api/v1/me',
|
'/api/v1/me',
|
||||||
])('stamps X-Role on the role-aware endpoint %s', (url) => {
|
])('stamps X-Role on the role-aware endpoint %s', (url) => {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { currentRole } from './role';
|
|||||||
/**
|
/**
|
||||||
* Dev-only: stamps role-aware requests with the current `?role=` as an `X-Role`
|
* Dev-only: stamps role-aware requests with the current `?role=` as an `X-Role`
|
||||||
* header so the backend can enforce the drafter/approver/admin rules. Only the
|
* header so the backend can enforce the drafter/approver/admin rules. Only the
|
||||||
* brief, org-template, stamdata and /me endpoints carry it (WP-23 widened the set —
|
* brief, org-template, stamdata and /me endpoints carry it (the set was widened —
|
||||||
* /me must see the role or `AccessStore` could never learn a capability; WP-29 added
|
* /me must see the role or `AccessStore` could never learn a capability; /stamdata was
|
||||||
* /stamdata, whose admin-only reads 403 without it); everything else is untouched.
|
* added later, since its admin-only reads 403 without it); everything else is untouched.
|
||||||
* A new admin-gated endpoint MUST be added here or its page silently 403s.
|
* A new admin-gated endpoint MUST be added here or its page silently 403s.
|
||||||
*/
|
*/
|
||||||
const ROLE_AWARE = [
|
const ROLE_AWARE = [
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function currentRole(): Role {
|
|||||||
return isRole(stored) ? stored : 'drafter';
|
return isRole(stored) ? stored : 'drafter';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dev switcher entry point: persist the chosen role for the tab (WP-33). */
|
/** Dev switcher entry point: persist the chosen role for the tab. */
|
||||||
export function setRole(r: Role): void {
|
export function setRole(r: Role): void {
|
||||||
sessionStorage.setItem(STORAGE_KEY, r);
|
sessionStorage.setItem(STORAGE_KEY, r);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function currentScenario(): Scenario {
|
|||||||
return isScenario(stored) ? stored : 'default';
|
return isScenario(stored) ? stored : 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dev switcher entry point: persist the chosen scenario for the tab (WP-33). */
|
/** Dev switcher entry point: persist the chosen scenario for the tab. */
|
||||||
export function setScenario(s: Scenario): void {
|
export function setScenario(s: Scenario): void {
|
||||||
sessionStorage.setItem(STORAGE_KEY, s);
|
sessionStorage.setItem(STORAGE_KEY, s);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe('subjectInterceptor', () => {
|
|||||||
expect(forward('/api/v1/registratie/concept').headers.get('X-Subject')).toBe('111222333');
|
expect(forward('/api/v1/registratie/concept').headers.get('X-Subject')).toBe('111222333');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps stamping later requests on the same tab after the query param is gone (WP-33-style stickiness)', () => {
|
it('keeps stamping later requests on the same tab after the query param is gone (sticky per tab)', () => {
|
||||||
window.history.replaceState({}, '', '/?subject=111222333');
|
window.history.replaceState({}, '', '/?subject=111222333');
|
||||||
forward('/api/v1/me');
|
forward('/api/v1/me');
|
||||||
window.history.replaceState({}, '', '/dashboard'); // navigation drops the query param
|
window.history.replaceState({}, '', '/dashboard'); // navigation drops the query param
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { HttpInterceptorFn } from '@angular/common/http';
|
|||||||
import { currentSubject } from './subject';
|
import { currentSubject } from './subject';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dev-only (WP-74): stamps every API request with `X-Subject`, the BSN
|
* Dev-only: stamps every API request with `X-Subject`, the BSN
|
||||||
* `StubIdentityProvider` (backend) resolves the caller's `ZorgverlenerCaller` from —
|
* `StubIdentityProvider` (backend) resolves the caller's `ZorgverlenerCaller` from —
|
||||||
* every owner-keyed store (`ApplicationStore`, `DocumentStore`, `BriefStore`) reads
|
* every owner-keyed store (`ApplicationStore`, `DocumentStore`, `BriefStore`) reads
|
||||||
* off that resolved identity, so this is the seam that lets e2e specs log in as
|
* off that resolved identity, so this is the seam that lets e2e specs log in as
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { isDevMode } from '@angular/core';
|
|||||||
* comment) — so `subject.interceptor.ts` can't reach it without a layering
|
* comment) — so `subject.interceptor.ts` can't reach it without a layering
|
||||||
* violation (`libs/shared` may not depend on an app-local `auth` context). Instead a
|
* violation (`libs/shared` may not depend on an app-local `auth` context). Instead a
|
||||||
* `?subject=<bsn>` query param, seen once on any navigation, is remembered for the
|
* `?subject=<bsn>` query param, seen once on any navigation, is remembered for the
|
||||||
* tab in sessionStorage — the exact `?role=` trick `role.ts` already uses (WP-33).
|
* tab in sessionStorage — the exact `?role=` trick `role.ts` already uses.
|
||||||
*
|
*
|
||||||
* Two consumers read this, both dev/e2e-only: `subject.interceptor.ts` (every
|
* Two consumers read this, both dev/e2e-only: `subject.interceptor.ts` (every
|
||||||
* `HttpClient` request) and `letter-preview.adapter.ts` (`/brief/preview`'s
|
* `HttpClient` request) and `letter-preview.adapter.ts` (`/brief/preview`'s
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class UploadAdapter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
xhr.open('POST', `${environment.apiBaseUrl}/api/v1/uploads`);
|
xhr.open('POST', `${environment.apiBaseUrl}/api/v1/uploads`);
|
||||||
// WP-74: this XHR bypasses `HttpClient`'s `subjectInterceptor` (the same reason
|
// This XHR bypasses `HttpClient`'s `subjectInterceptor` (the same reason
|
||||||
// `letter-preview.adapter.ts` sets `X-Role` explicitly) — without `X-Subject` a
|
// `letter-preview.adapter.ts` sets `X-Role` explicitly) — without `X-Subject` a
|
||||||
// document always uploaded under `DocumentStore.DemoOwner` regardless of who was
|
// document always uploaded under `DocumentStore.DemoOwner` regardless of who was
|
||||||
// actually logged in, so a submission attempted under any other BSN would find
|
// actually logged in, so a submission attempted under any other BSN would find
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* PII masking — pure functional core (WP-40). Data-minimisation helpers shared by the app
|
* PII masking — pure functional core. Data-minimisation helpers shared by the app
|
||||||
* (dev state panel, the masked-value atom, anywhere sensitive data is shown). No framework,
|
* (dev state panel, the masked-value atom, anywhere sensitive data is shown). No framework,
|
||||||
* no domain imports. The backend keeps a `MaskTail` twin in sync (see Program.cs).
|
* no domain imports. The backend keeps a `MaskTail` twin in sync (see Program.cs).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const meta: Meta<ShellComponent> = {
|
|||||||
title: 'Design System/Templates/Shell',
|
title: 'Design System/Templates/Shell',
|
||||||
component: ShellComponent,
|
component: ShellComponent,
|
||||||
// The persistent header injects AccessStore (for its capability-gated admin links) and
|
// The persistent header injects AccessStore (for its capability-gated admin links) and
|
||||||
// FeatureFlagStore (WP-47, for the Inschrijven nav gate); stub both so the story needs no
|
// FeatureFlagStore (for the Inschrijven nav gate); stub both so the story needs no
|
||||||
// HTTP/ApiClient. `can` false → no admin links; `enabled` true → Inschrijven stays visible.
|
// HTTP/ApiClient. `can` false → no admin links; `enabled` true → Inschrijven stays visible.
|
||||||
decorators: [
|
decorators: [
|
||||||
applicationConfig({
|
applicationConfig({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Capability } from '@shared/domain/capability';
|
|||||||
export interface HeaderNavItem {
|
export interface HeaderNavItem {
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
readonly to: string;
|
readonly to: string;
|
||||||
/** Hidden when this feature flag is off (e.g. WP-47's Inschrijven gate). Omit for an
|
/** Hidden when this feature flag is off (e.g. the Inschrijven gate). Omit for an
|
||||||
always-visible item. */
|
always-visible item. */
|
||||||
readonly flag?: string;
|
readonly flag?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export class SiteHeaderComponent {
|
|||||||
private rawNavItems = inject(HEADER_NAV_ITEMS);
|
private rawNavItems = inject(HEADER_NAV_ITEMS);
|
||||||
private rawAdminLinks = inject(HEADER_ADMIN_LINKS);
|
private rawAdminLinks = inject(HEADER_ADMIN_LINKS);
|
||||||
|
|
||||||
/** Hides an item whose `flag` is off (e.g. the SSP's Inschrijven gate, WP-47) — which
|
/** Hides an item whose `flag` is off (e.g. the SSP's Inschrijven gate) — which
|
||||||
items exist, and which carry a flag, is entirely up to the app that provided them. */
|
items exist, and which carry a flag, is entirely up to the app that provided them. */
|
||||||
protected readonly navItems = computed(() =>
|
protected readonly navItems = computed(() =>
|
||||||
this.rawNavItems.filter((i) => !i.flag || this.flags.enabled(i.flag)),
|
this.rawNavItems.filter((i) => !i.flag || this.flags.enabled(i.flag)),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { HEADER_ADMIN_LINKS, HEADER_NAV_ITEMS } from './nav-config';
|
|||||||
import { SiteHeaderComponent } from './site-header.component';
|
import { SiteHeaderComponent } from './site-header.component';
|
||||||
|
|
||||||
// The header injects AccessStore for the capability-gated admin links and FeatureFlagStore
|
// The header injects AccessStore for the capability-gated admin links and FeatureFlagStore
|
||||||
// (WP-47, for the Inschrijven nav gate); stub both so the story needs no HTTP/ApiClient.
|
// (for the Inschrijven nav gate); stub both so the story needs no HTTP/ApiClient.
|
||||||
// `can` decides which admin links appear; `enabled` true keeps Inschrijven visible. Nav/admin
|
// `can` decides which admin links appear; `enabled` true keeps Inschrijven visible. Nav/admin
|
||||||
// links are app-provided (HEADER_NAV_ITEMS/HEADER_ADMIN_LINKS) — this story supplies a
|
// links are app-provided (HEADER_NAV_ITEMS/HEADER_ADMIN_LINKS) — this story supplies a
|
||||||
// representative sample rather than importing a real app's config, keeping the story
|
// representative sample rather than importing a real app's config, keeping the story
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const meta: Meta<AlertComponent> = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<AlertComponent>;
|
type Story = StoryObj<AlertComponent>;
|
||||||
|
|
||||||
// role assertions guard the polite/assertive split (WP-16): errors interrupt, others don't.
|
// role assertions guard the polite/assertive split: errors interrupt, others don't.
|
||||||
export const Info: Story = {
|
export const Info: Story = {
|
||||||
args: { type: 'info' },
|
args: { type: 'info' },
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
|||||||
`.block-wrapper` panel with a `<dl>` of projected `<app-data-row>`s. Use `stacked`
|
`.block-wrapper` panel with a `<dl>` of projected `<app-data-row>`s. Use `stacked`
|
||||||
(`.data-block--stacked`) when labels/values are long and should stack. This is the
|
(`.data-block--stacked`) when labels/values are long and should stack. This is the
|
||||||
single data surface (a generic white `app-card` used to exist but was unused and
|
single data surface (a generic white `app-card` used to exist but was unused and
|
||||||
removed — see WP-12); the datablock carries its own surface, so it is not nested in
|
removed); the datablock carries its own surface, so it is not nested in
|
||||||
another one. When there is no visible `heading`, pass an `ariaLabel` so the definition
|
another one. When there is no visible `heading`, pass an `ariaLabel` so the definition
|
||||||
list is announced. */
|
list is announced. */
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ type Story = StoryObj<FormFieldComponent>;
|
|||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: { label: 'BSN', fieldId: 'bsn', description: '9 cijfers', required: true },
|
args: { label: 'BSN', fieldId: 'bsn', description: '9 cijfers', required: true },
|
||||||
// Composition contract: fieldId must equal the input's id — enforced here, not by DI
|
// Composition contract: fieldId must equal the input's id — enforced here, not by DI.
|
||||||
// (see WP-16). Catches drift in the description→aria-describedby wiring.
|
// Catches drift in the description→aria-describedby wiring.
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
const input = canvas.getByRole('textbox');
|
const input = canvas.getByRole('textbox');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ButtonComponent } from '@shared/ui/button/button.component';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Atom: a possibly-masked sensitive value (BSN, BIG-nummer, …) with an optional, audited
|
* Atom: a possibly-masked sensitive value (BSN, BIG-nummer, …) with an optional, audited
|
||||||
* reveal affordance (WP-40). The value arrives masked from the server (data-minimisation)
|
* reveal affordance. The value arrives masked from the server (data-minimisation)
|
||||||
* and is swapped for the full value on reveal; the reveal button shows only when the value
|
* and is swapped for the full value on reveal; the reveal button shows only when the value
|
||||||
* is still masked AND the caller says the principal may reveal it. Centralises the
|
* is still masked AND the caller says the principal may reveal it. Centralises the
|
||||||
* masked-detection that consumers used to sniff inline. The atom only emits `reveal`; the
|
* masked-detection that consumers used to sniff inline. The atom only emits `reveal`; the
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user