feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { OrgTemplate, OrgTemplateAdminView } from './org-template';
|
||||
import { OrgTemplateState, reduce } from './org-template.machine';
|
||||
import { DocumentCategory } from '@shared/upload/upload.machine';
|
||||
|
||||
const template: OrgTemplate = {
|
||||
subOrgId: 'cibg-registers',
|
||||
orgName: 'CIBG',
|
||||
returnAddress: 'Postbus 1\n2500 AA Den Haag',
|
||||
footerContact: 'info@cibg.nl',
|
||||
footerLegal: 'CIBG is onderdeel van VWS',
|
||||
signatureName: 'A. de Vries',
|
||||
signatureRole: 'Hoofd Registratie',
|
||||
signatureClosing: 'Met vriendelijke groet,',
|
||||
margins: { topMm: 25, rightMm: 20, bottomMm: 25, leftMm: 20 },
|
||||
version: 3,
|
||||
};
|
||||
|
||||
const view = (over: Partial<OrgTemplateAdminView> = {}): OrgTemplateAdminView => ({
|
||||
draft: template,
|
||||
publishedVersion: 3,
|
||||
history: [],
|
||||
unsentBriefs: 2,
|
||||
...over,
|
||||
});
|
||||
|
||||
const loaded = (): OrgTemplateState =>
|
||||
reduce({ tag: 'loading' }, { tag: 'DraftLoaded', view: view() });
|
||||
|
||||
const logoCategory: DocumentCategory = {
|
||||
categoryId: 'org-logo',
|
||||
label: 'Logo',
|
||||
description: '',
|
||||
required: false,
|
||||
acceptedTypes: ['image/png'],
|
||||
maxSizeMb: 2,
|
||||
multiple: false,
|
||||
allowPostDelivery: false,
|
||||
};
|
||||
|
||||
describe('org-template.machine', () => {
|
||||
it('DraftLoaded moves to loaded with the draft, clean', () => {
|
||||
const s = loaded();
|
||||
expect(s.tag).toBe('loaded');
|
||||
if (s.tag !== 'loaded') return;
|
||||
expect(s.draft.orgName).toBe('CIBG');
|
||||
expect(s.subOrgId).toBe('cibg-registers');
|
||||
expect(s.unsentBriefs).toBe(2);
|
||||
expect(s.dirty).toBe(false);
|
||||
});
|
||||
|
||||
it('LoadFailed carries the reason', () => {
|
||||
const s = reduce({ tag: 'loading' }, { tag: 'LoadFailed', reason: 'boom' });
|
||||
expect(s).toEqual({ tag: 'failed', reason: 'boom' });
|
||||
});
|
||||
|
||||
it('FieldEdited edits the draft and marks dirty', () => {
|
||||
const s = reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'CIBG Nieuw' });
|
||||
expect(s.tag === 'loaded' && s.draft.orgName).toBe('CIBG Nieuw');
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('MarginEdited edits one edge and marks dirty', () => {
|
||||
const s = reduce(loaded(), { tag: 'MarginEdited', edge: 'topMm', value: 40 });
|
||||
expect(s.tag === 'loaded' && s.draft.margins.topMm).toBe(40);
|
||||
expect(s.tag === 'loaded' && s.draft.margins.leftMm).toBe(20);
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('DraftSaved clears dirty when the saved draft is the current one', () => {
|
||||
const edited = reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'X' });
|
||||
const savedDraft = edited.tag === 'loaded' ? edited.draft : template;
|
||||
const s = reduce(edited, { tag: 'DraftSaved', savedDraft });
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(false);
|
||||
expect(s.tag === 'loaded' && s.draft.orgName).toBe('X');
|
||||
});
|
||||
|
||||
it('DraftSaved keeps dirty when an edit landed during the save round-trip', () => {
|
||||
const editing = reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'X' });
|
||||
const savedDraft = editing.tag === 'loaded' ? editing.draft : template;
|
||||
// a further edit changes the draft reference before the save resolves
|
||||
const raced = reduce(editing, { tag: 'FieldEdited', field: 'orgName', value: 'Y' });
|
||||
const s = reduce(raced, { tag: 'DraftSaved', savedDraft });
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('edits are no-ops in non-loaded states', () => {
|
||||
expect(
|
||||
reduce({ tag: 'loading' }, { tag: 'FieldEdited', field: 'orgName', value: 'x' }),
|
||||
).toEqual({
|
||||
tag: 'loading',
|
||||
});
|
||||
});
|
||||
|
||||
it('a completed logo upload sets logoDocumentId + dirty', () => {
|
||||
const withCat = reduce(loaded(), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [logoCategory] },
|
||||
});
|
||||
const selected = reduce(withCat, {
|
||||
tag: 'Upload',
|
||||
msg: {
|
||||
type: 'FileSelected',
|
||||
categoryId: 'org-logo',
|
||||
localId: 'a',
|
||||
fileName: 'l.png',
|
||||
fileSizeMb: 0.1,
|
||||
},
|
||||
});
|
||||
const done = reduce(selected, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadComplete', localId: 'a', documentId: 'doc-1' },
|
||||
});
|
||||
expect(done.tag === 'loaded' && done.draft.logoDocumentId).toBe('doc-1');
|
||||
expect(done.tag === 'loaded' && done.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('removing the logo clears logoDocumentId + dirty', () => {
|
||||
const withLogo = reduce(loaded(), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadComplete', localId: 'a', documentId: 'doc-1' },
|
||||
});
|
||||
const removed = reduce(withLogo, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadRemoved', localId: 'a' },
|
||||
});
|
||||
expect(removed.tag === 'loaded' && removed.draft.logoDocumentId).toBeUndefined();
|
||||
expect(removed.tag === 'loaded' && removed.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('DraftLoaded (sub-org switch) keeps the loaded logo category, drops uploads', () => {
|
||||
const withCat = reduce(loaded(), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [logoCategory] },
|
||||
});
|
||||
const switched = reduce(withCat, {
|
||||
tag: 'DraftLoaded',
|
||||
view: view({ draft: { ...template, subOrgId: 'cibg-vakbekwaamheid' } }),
|
||||
});
|
||||
expect(switched.tag === 'loaded' && switched.upload.categories).toHaveLength(1);
|
||||
expect(switched.tag === 'loaded' && switched.upload.uploads).toHaveLength(0);
|
||||
expect(switched.tag === 'loaded' && switched.subOrgId).toBe('cibg-vakbekwaamheid');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user