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>
6.2 KiB
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 followssrc/app/shared/ui/upload/single-upload(logo upload reuse)
Decisions (pre-made, don't relitigate)
- Lives in the
briefcontext (route/brief/huisstijl, lazy) — same bounded capability, no new context. Gated byAccessStore.can('orgtemplate:edit')with a denial alert (deny-by-default); no new route guard. - House form-machine idiom:
org-template.machine.ts(OrgTemplateState/OrgTemplateMsg, purereduce+ spec) — draft fields are form state, not brief state. Store (org-template.store.ts, root singleton) does debounced draft save (mirrorBriefStore.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-uploadagainst theorg-logocategory; 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.tssrc/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.tssrc/app/app.routes.ts(routebrief/huisstijl)
Steps
- Machine (fields,
FieldEdited/MarginEdited/LogoSet/DraftLoaded/save-publish outcome Msgs) + spec. - Adapter (generated client CRUD + parse boundary) + spec.
- Store: load (sub-org list + selected), debounced save, publish (impact confirm), rollback.
- 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. - Route + capability gate + stories (axe).
Acceptance criteria
?role=admincan switch sub-orgs, edit all template fields in place on the canvas, and see the canvas update live.- Draft saves are debounced; publish asks for confirmation showing the impact count; after publish the drafter's canvas (WP-24) reflects it on reload.
- Version history lists published versions (who is faked, when is real); rollback copies an old version into the draft.
- Non-admin on
/brief/huisstijlsees the denial alert; API would 403 anyway. - Logo upload validates type/size client-side (existing
rejectReason) and renders on the canvas after upload. - Machine spec covers field edits, dirty tracking, publish/rollback outcomes.
- Full GREEN.
Deviations / notes (as built)
check:tokenswas already red onmain(WP-24's canvas landedvar(--rhc-*, #hex)fallbacks + anrgb()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-shadowtoken-ok; also fixed a pre-existingpassage-pickervar(--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).logoUrlinput 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()dispatchesLoading(a fresh object), which would retrigger a model-reading effect into a runaway loop that saturated the page. Gated oncanEdit()+ a plainloadRequestedflag instead. AccessStore.readyadded (tiny): a page-level capability gate needs to tell "still loading/me" from "denied" so an admin doesn't flash the denial alert.uploadContentUrlpure helper extracted fromUploadAdapter.contentUrlsoBriefStorecan build a logosrcwithout pullingApiClientinto its DI graph (kept its spec green).- Role caching caveat:
/meloads once. Open the app with?role=adminfrom 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:
DraftSavedcarries the saved draft and clearsdirtyonly 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).