Files
atomic-design-poc/docs/backlog/WP-26-org-template-editor.md
Edwin van den Houdt f6c837f281
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
feat(fp): WP-26 — admin org-template editor
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>
2026-07-06 08:22:55 +02:00

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 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

  • ?role=admin can 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/huisstijl sees 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: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).