Some checks failed
CI / frontend (push) Failing after 57s
CI / storybook-a11y (push) Successful in 4m27s
CI / backend (push) Successful in 1m15s
CI / codeql (csharp) (push) Failing after 1m42s
CI / codeql (javascript-typescript) (push) Failing after 1m19s
CI / api-client-drift (push) Successful in 1m40s
CI / e2e (push) Failing after 3h11m33s
Edit the letter's org identity in place on the same canvas the drafter composes on (editableRegions='template'): letterhead/signature/footer become inline controls, content a read-only sample. Margins (bounded), logo upload (reuses the shared upload transport + single-upload), version history + rollback, proefbrief, and publish-with-impact-confirmation. House form-machine idiom (org-template.machine.ts) + root store with debounced save. Capability-gated (orgtemplate:edit) with a deny-by-default alert; route /brief/huisstijl. Backend + generated client were already in place (WP-23). Also fixes a pre-existing red check:tokens (WP-24 canvas hex fallbacks) and threads the published logo through to the drafter's canvas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
6.2 KiB
Markdown
119 lines
6.2 KiB
Markdown
# WP-26 — Admin org-template editor
|
|
|
|
Status: done
|
|
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
|
|
|
## Why
|
|
|
|
The org template (WP-23) needs its editor: an admin edits, per sub-organization, the
|
|
letter's appearance **in place on the same canvas** the drafter composes on — the
|
|
mirror image (`editableRegions='template'`: letterhead/footer/signature editable,
|
|
content a read-only sample). PRD Brief v2 §5, §7h.
|
|
|
|
## Read first
|
|
|
|
- PRD Brief v2 §5, §7h; `docs/backlog/WP-23/24/25` (endpoints, canvas, proefbrief)
|
|
- `src/app/shared/application/access.store.ts` (`can('orgtemplate:edit')`)
|
|
- `.claude/skills/form-machine` — the house form idiom this editor follows
|
|
- `src/app/shared/ui/upload/single-upload` (logo upload reuse)
|
|
|
|
## Decisions (pre-made, don't relitigate)
|
|
|
|
- **Lives in the `brief` context** (route `/brief/huisstijl`, lazy) — same bounded
|
|
capability, no new context. Gated by `AccessStore.can('orgtemplate:edit')` with a
|
|
denial alert (deny-by-default); no new route guard.
|
|
- **House form-machine idiom**: `org-template.machine.ts`
|
|
(`OrgTemplateState`/`OrgTemplateMsg`, pure `reduce` + spec) — draft fields are form
|
|
state, not brief state. Store (`org-template.store.ts`, root singleton) does
|
|
debounced draft save (mirror `BriefStore.scheduleSave`), publish, rollback.
|
|
- **Publish shows impact first**: confirmation displays the WP-23 impact count
|
|
("Dit raakt N nog niet verzonden brieven") before the POST.
|
|
- **Version history is a list, rollback copies into draft** (WP-23 semantics) —
|
|
no side-by-side rendered diff (deferred; field-level history list is enough here).
|
|
- **Logo upload reuses `single-upload`** against the `org-logo` category; the canvas
|
|
shows `<img src="/api/v1/uploads/{id}/content">`.
|
|
- **Proefbrief** = the WP-25 admin preview endpoint; just a button.
|
|
- Margins are bounded number inputs (server re-validates, WP-23).
|
|
|
|
## Files
|
|
|
|
- `src/app/brief/domain/org-template.machine.ts` (+spec)
|
|
- `src/app/brief/application/org-template.store.ts`
|
|
- `src/app/brief/infrastructure/org-template.adapter.ts` (+spec, `parseOrgTemplateAdminView`)
|
|
- `src/app/brief/ui/org-template-editor/*` (organism + stories)
|
|
- `src/app/brief/ui/org-template.page.ts`
|
|
- `src/app/app.routes.ts` (route `brief/huisstijl`)
|
|
|
|
## Steps
|
|
|
|
1. Machine (fields, `FieldEdited`/`MarginEdited`/`LogoSet`/`DraftLoaded`/save-publish
|
|
outcome Msgs) + spec.
|
|
2. Adapter (generated client CRUD + parse boundary) + spec.
|
|
3. Store: load (sub-org list + selected), debounced save, publish (impact confirm),
|
|
rollback.
|
|
4. Editor UI: sub-org switcher, canvas in `'template'` mode with inline-editable
|
|
regions, margins inputs, logo upload, version history + rollback, proefbrief
|
|
button, publish bar showing live version + published-at.
|
|
5. Route + capability gate + stories (axe).
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] `?role=admin` can switch sub-orgs, edit all template fields in place on the
|
|
canvas, and see the canvas update live.
|
|
- [x] Draft saves are debounced; publish asks for confirmation showing the impact
|
|
count; after publish the drafter's canvas (WP-24) reflects it on reload.
|
|
- [x] Version history lists published versions (who is faked, when is real);
|
|
rollback copies an old version into the draft.
|
|
- [x] Non-admin on `/brief/huisstijl` sees the denial alert; API would 403 anyway.
|
|
- [x] Logo upload validates type/size client-side (existing `rejectReason`) and
|
|
renders on the canvas after upload.
|
|
- [x] Machine spec covers field edits, dirty tracking, publish/rollback outcomes.
|
|
- [x] Full GREEN.
|
|
|
|
## Deviations / notes (as built)
|
|
|
|
- **`check:tokens` was already red on `main`** (WP-24's canvas landed `var(--rhc-*,
|
|
#hex)` fallbacks + an `rgb()` paper shadow, and WP-25 committed over it). Fixed here
|
|
to end GREEN: dropped the redundant hex fallbacks (the token bridge defines every
|
|
one) and marked the paper drop-shadow `token-ok`; also fixed a pre-existing
|
|
`passage-picker` `var(--rhc-color-wit, #fff)` hit.
|
|
- **Canvas edit-in-place**: `editableRegions='template'` now renders the seven
|
|
org-identity text fields as inline `<input>`/`<textarea>` controls (aria-labelled)
|
|
and shows the logo `<img>`; the letter body stays a read-only sample
|
|
(`SAMPLE_LETTER_BRIEF`). `logoUrl` input added to the canvas and wired through the
|
|
composer too, so a published logo shows to the drafter (AC2).
|
|
- **Load-effect loop (caught in the live walk)**: the page's initial-load effect must
|
|
NOT read the store model — `load()` dispatches `Loading` (a fresh object), which
|
|
would retrigger a model-reading effect into a runaway loop that saturated the page.
|
|
Gated on `canEdit()` + a plain `loadRequested` flag instead.
|
|
- **`AccessStore.ready`** added (tiny): a page-level capability gate needs to tell
|
|
"still loading `/me`" from "denied" so an admin doesn't flash the denial alert.
|
|
- **`uploadContentUrl`** pure helper extracted from `UploadAdapter.contentUrl` so
|
|
`BriefStore` can build a logo `src` without pulling `ApiClient` into its DI graph
|
|
(kept its spec green).
|
|
- **Role caching caveat**: `/me` loads once. Open the app with `?role=admin` from the
|
|
first navigation that touches the editor (the dev role stub, like `?scenario=`);
|
|
switching role mid-session won't refetch capabilities (out of scope, POC).
|
|
- **Dirty race**: `DraftSaved` carries the saved draft and clears `dirty` only if it's
|
|
reference-equal to the current draft, so an edit landing during a save round-trip
|
|
keeps its pending save.
|
|
|
|
## Verification
|
|
|
|
GREEN one-liner; manual walk: admin edits footer + margin → canvas live-updates →
|
|
proefbrief shows draft → publish (impact count) → `?role=drafter` reload shows new
|
|
appearance; second sub-org unaffected.
|
|
|
|
## Out of scope
|
|
|
|
Template approval chain (four-eyes on templates — PRD open question, out for POC).
|
|
Rendered side-by-side version diff. Soft locks. New shared overlay/modal component —
|
|
the publish confirmation uses the existing inline confirmation pattern, not a dialog.
|
|
|
|
## Risks
|
|
|
|
The editor is the first consumer of `editableRegions='template'` — WP-24 built the
|
|
input but nothing exercised it; budget for canvas fixes here. Debounced draft save +
|
|
publish can race — flush the draft save before publishing (same
|
|
`clearTimeout`+`flushSave` discipline as `BriefStore.transition`).
|