Second template axis (org identity: letterhead, footer, signature, margins) server-side: OrgTemplateStore with JSON version history, publish/rollback, sent-brief version pinning, admin role + capability, 5 admin endpoints, org-logo upload category. FE seam widened only (Role/Capability unions, interceptor); WP-24/26 consume it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
94 lines
4.6 KiB
Markdown
94 lines
4.6 KiB
Markdown
# WP-27 — Brief UX layer (undo/redo, standaardbrief, search, diff badges)
|
||
|
||
Status: todo
|
||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||
|
||
## Why
|
||
|
||
PRD Brief v2 §7: the working-day features that make the composer pleasant daily.
|
||
Several are nearly free **because** state is one immutable value — that's the
|
||
teaching payload: undo/redo is a shell-side snapshot list, the rejection diff is a
|
||
pure function over two values. Say so in code comments and stories.
|
||
|
||
## Read first
|
||
|
||
- PRD Brief v2 §7 (and the plan-review trim recorded below)
|
||
- `src/app/brief/application/brief.store.ts` (autosave + `SaveState` already exist)
|
||
- `src/app/brief/domain/brief.machine.ts` — the `Seed` Msg (undo/redo's restore path)
|
||
|
||
## Decisions (pre-made, don't relitigate)
|
||
|
||
- **Trim agreed at plan review.** IN: undo/redo, autosave retry affordance,
|
||
standaardbrief, passage search, canvas zoom controls, Ctrl+Z/Ctrl+Shift+Z,
|
||
block-level rejection-diff badges. OUT (deferred, one line each in Out of scope):
|
||
soft lock/takeover, case-context panel, 401 autosave grace, per-user usage counts,
|
||
shortcut-overlay dialog, inline character-level text diff.
|
||
- **Undo/redo is shell state, not machine state**: `past`/`future: Brief[]` in
|
||
`BriefStore` (cap 50; push on `edit()`; clear `future` on a new edit); restore
|
||
dispatches the **existing `Seed` Msg** — zero machine changes — then `scheduleSave()`.
|
||
- **Standaardbrief**: backend seeds `IsDefault` on 2–3 kern passages
|
||
(`LibraryPassageDto` gains the flag); one button, visible only while the kern
|
||
section is empty, dispatches the existing `PassagesInserted` with the default set —
|
||
one Msg, one undo step.
|
||
- **Passage search is a client-side filter** in the picker (label + content match) —
|
||
the library is small; no server search, no usage tracking.
|
||
- **Rejection diff**: pure `diffBlocks(before, after): BlockDiff[]` in
|
||
`domain/brief-diff.ts` (added/removed/changed by `blockId`); the "before" snapshot
|
||
is captured shell-side when the `Rejected` dispatch happens (POC limit: lost on
|
||
reload — comment it). Rendered as "gewijzigd sinds afwijzing" badges on the canvas;
|
||
the approver gets a "Toon wijzigingen" toggle on resubmission.
|
||
- **Autosave retry**: `SaveState.Error` already exists; add the "Opnieuw proberen"
|
||
button that calls the existing flush path. No new state.
|
||
|
||
## Files
|
||
|
||
- `src/app/brief/application/brief.store.ts` (+spec: history bounds, clear-on-edit,
|
||
redo, rejection snapshot)
|
||
- `src/app/brief/domain/brief-diff.ts` (new, +spec)
|
||
- `backend/src/BigRegister.Api/Data/BriefStore.cs` (`IsDefault` seed) +
|
||
`Contracts/Dtos.cs` (`LibraryPassageDto`) + gen:api + adapter parse
|
||
- `src/app/brief/ui/passage-picker/*` (search input)
|
||
- `src/app/brief/ui/letter-canvas/*` (diff badges, standaardbrief button, zoom controls)
|
||
- `src/app/brief/ui/brief.page.ts` (undo/redo buttons + keydown listener, retry button)
|
||
|
||
## Steps
|
||
|
||
1. `diffBlocks` + spec (added/removed/changed/unchanged; changed = same blockId,
|
||
different content).
|
||
2. Store: history + undo/redo + rejection snapshot (+spec).
|
||
3. Backend `IsDefault` + gen:api + parse.
|
||
4. UI: standaardbrief button, search, zoom, badges, keyboard, retry.
|
||
5. Stories for the new states (axe).
|
||
|
||
## Acceptance criteria
|
||
|
||
- [ ] Remove a block → Ctrl+Z restores it → Ctrl+Shift+Z re-removes; buttons mirror;
|
||
history capped at 50; a new edit clears redo; restore re-triggers autosave.
|
||
- [ ] Empty kern + "Standaardbrief invoegen" → default passages inserted as one undo
|
||
step; button gone once kern is non-empty.
|
||
- [ ] Search filters passages by label and content.
|
||
- [ ] Reject → edit → resubmit: approver toggles "Toon wijzigingen", changed/added/
|
||
removed blocks are badged (block granularity).
|
||
- [ ] Autosave failure shows "Niet opgeslagen — opnieuw proberen"; retry works;
|
||
content never lost locally.
|
||
- [ ] Full GREEN.
|
||
|
||
## Verification
|
||
|
||
GREEN one-liner; store + diff specs; manual reject→edit→diff walk with two roles.
|
||
|
||
## Out of scope (deferred, per plan review)
|
||
|
||
Soft lock/heartbeat/takeover (real session infra, no FP teaching value here).
|
||
Case-context panel (no case data exists). 401 autosave grace (auth is faked).
|
||
Per-user passage usage counts (bookkeeping, demos nothing). Shortcut overlay dialog
|
||
(no modal component exists; not worth building one). Inline character-level diff
|
||
(block granularity carries the teaching point).
|
||
|
||
## Risks
|
||
|
||
Undo history holds `Brief` snapshots — deep-frozen immutable values, so sharing is
|
||
safe, but never push non-content dispatches (status transitions, `Seed` itself) into
|
||
history or undo will replay workflow state. The rejection snapshot lives in memory
|
||
only — document it where it's captured.
|