feat(brief): WP-28 demo script, e2e spec, and story gap-fill

Closes phase 6 (Brief v2): a demo script mapping shipped scenarios to
URL+click paths (no Brief v2 PRD ever existed to translate one from —
written directly against the code instead), one e2e spec covering
compose→approve→send and admin republish→drafter-sees-it, and
Storybook state gaps (rejection diff, read-only viewer, org logo,
upload rejection) that prior WPs left uncovered. Flags passage-picker
as dead code, superseded by besluit-panel.

npm run e2e is not verified green in this sandbox — see WP-28's
Deviations section; the pre-existing, unmodified smoke.spec.ts fails
identically here, pointing at a sandbox rendering issue rather than a
regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-27 20:01:51 +02:00
co-authored by Claude Sonnet 5
parent 94cd0b82c2
commit e7e2f070f9
11 changed files with 513 additions and 160 deletions
@@ -149,3 +149,15 @@ export const WithDiff: Story = {
export const PageBreak: Story = {
args: { editableRegions: 'none', brief: longBrief, diagnostics: [] },
};
// Inline SVG so the story needs no backend/upload round-trip (WP-26 logo upload).
const sampleLogo =
'data:image/svg+xml;utf8,' +
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>',
);
/** Published org logo (WP-26 AC2): the letterhead shows it above the org name. */
export const MetLogo: Story = {
args: { editableRegions: 'none', diagnostics: [], logoUrl: sampleLogo },
};
@@ -3,6 +3,7 @@ import { LetterComposerComponent } from './letter-composer.component';
import { Brief, BriefDecisions, BriefStatus, LibraryPassage } from '@brief/domain/brief';
import { allDiagnostics } from '@brief/domain/brief';
import { OrgTemplate } from '@brief/domain/org-template';
import { BlockDiffKind } from '@brief/domain/brief-diff';
const orgTemplate: OrgTemplate = {
subOrgId: 'cibg-registers',
@@ -117,17 +118,29 @@ function brief(status: BriefStatus): Brief {
};
}
const render = (b: Brief, decisions: BriefDecisions) => ({
const render = (
b: Brief,
decisions: BriefDecisions,
extra: {
blockDiffs?: ReadonlyMap<string, BlockDiffKind>;
removedCount?: number;
logoUrl?: string | null;
} = {},
) => ({
props: {
brief: b,
orgTemplate,
diagnostics: allDiagnostics(b),
...decisions,
busy: false,
blockDiffs: extra.blockDiffs ?? new Map<string, BlockDiffKind>(),
removedCount: extra.removedCount ?? 0,
logoUrl: extra.logoUrl ?? null,
},
template: `<app-letter-composer [brief]="brief" [orgTemplate]="orgTemplate"
template: `<app-letter-composer [brief]="brief" [orgTemplate]="orgTemplate" [logoUrl]="logoUrl"
[diagnostics]="diagnostics" [canApprove]="canApprove" [canReject]="canReject"
[canSend]="canSend" [busy]="busy"></app-letter-composer>`,
[canSend]="canSend" [busy]="busy" [blockDiffs]="blockDiffs"
[removedCount]="removedCount"></app-letter-composer>`,
});
const meta: Meta<LetterComposerComponent> = {
@@ -167,3 +180,43 @@ export const Sent: Story = {
canRevealBigNummer: false,
}),
};
/** Approver's "Toon wijzigingen" (WP-27): a resubmitted letter with blocks changed,
added and removed since the last rejection. */
export const RejectionDiff: Story = {
render: () =>
render(
brief({
tag: 'submitted',
submittedBy: 'demo-drafter',
submittedAt: '2026-07-02',
}),
{
canEdit: false,
canApprove: true,
canReject: true,
canSend: false,
canRevealBigNummer: false,
},
{
blockDiffs: new Map<string, BlockDiffKind>([
['local-2', 'changed'],
['local-3', 'added'],
]),
removedCount: 1,
},
),
};
/** A pure viewer (e.g. admin) has no approve/reject/send capability on this letter —
the read-only notice, not a broken-looking editor. */
export const AlleenLezen: Story = {
render: () =>
render(brief({ tag: 'submitted', submittedBy: 'demo-drafter', submittedAt: '2026-07-01' }), {
canEdit: false,
canApprove: false,
canReject: false,
canSend: false,
canRevealBigNummer: false,
}),
};
@@ -79,3 +79,26 @@ export const Invalid: Story = {
export const NoHistory: Story = {
args: { history: [], publishedVersion: 0 },
};
// Inline SVG so the story needs no backend/upload round-trip.
const sampleLogo =
'data:image/svg+xml;utf8,' +
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>',
);
/** Published logo (WP-26 AC2): the letterhead canvas shows it above the org name. */
export const MetLogo: Story = {
args: { logoUrl: sampleLogo },
};
/** Client-side upload rejection (existing `rejectReason`, WP-26 AC5) — type/size caught
before the file ever reaches the backend. */
export const LogoUploadFout: Story = {
args: {
uploadState: {
...uploadWithCategory,
rejections: { 'org-logo': 'Alleen PNG of JPEG, maximaal 2 MB.' },
},
},
};
@@ -8,7 +8,11 @@ import { LibraryPassage } from '@brief/domain/brief';
/** Molecule: multi-select list of the section's library passages. One "Voeg toe"
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
`src/app` outside its own story (WP-28 audit). Kept for now rather than deleted
in-flight of an unrelated WP; a future cleanup can remove it. */
@Component({
selector: 'app-passage-picker',
imports: [FormsModule, CheckboxComponent, ButtonComponent, TextInputComponent],