Scrum4Me/docs/patterns/dialog.md
Madhura68 3ad85d0167 docs: sync data-model, glossary en specs met huidig schema
Brengt de docs gelijk met de werkelijkheid na PBI-46/47/50/58/59/61/63
en M12. Belangrijkste fixes:

- data-model.md herschreven naar prisma/schema.prisma: nieuwe entiteiten
  (Idea, IdeaLog, IdeaProduct, UserQuestion, ClaudeQuestion, ClaudeJob,
  SprintRun, SprintTaskExecution, ClaudeWorker, LoginPairing,
  PushSubscription, ModelPrice, ProductMember), nieuwe enums
  (FAILED/EXCLUDED, OPEN/CLOSED/ARCHIVED, ADMIN, etc.) en codes
  (PBI/ST/T/SP-N) toegevoegd; verwijderde todos-tabel verwijderd.
- glossary.md: Sprint zonder "max 1 actief" (PBI-63), Story/Task incl.
  FAILED/EXCLUDED, Todo verwijderd, Idea/SprintRun/ClaudeJob/
  verify_result toegevoegd.
- project-structure.md: app/(app)/todos vervangen door
  ideas/insights/jobs/manual/admin/solo; api-tree volledig.
- overview.md: "geen realtime in v1" en Docker-rationale herschreven —
  Postgres LISTEN/NOTIFY + SSE, claude_jobs als queue, opt-in
  Docker-deploy-flow.
- functional.md: F-08 Todo-lijst -> Ideeen-laag, F-09 multi-sprint,
  F-10 Task-status incl. FAILED/EXCLUDED, F-11 endpoint-lijst,
  navigatiestructuur, datamodel-schets en Flow 3 bijgewerkt.
- README.md API-tabel: /api/todos weg, ideas/jobs/users/profile/health
  toegevoegd, kort over realtime/auth-pair/internal/cron.
- patterns + mcp-integration runbook: Todo-/ACTIVE-references vervangen
  door Idea/OPEN; create_todo MCP-tool note over verwijdering.

Linkcheck groen (105 files), INDEX hergegenereerd (98 docs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:59:18 +02:00

20 KiB
Raw Blame History

title status audience language last_updated when_to_read
Entity Dialog active
ai-agent
contributor
nl 2026-05-08 Before building any create/edit/detail dialog component.

Pattern — Entity Dialog

Deze pagina is bindend voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject (PBI, Story, Task, Idea, Sprint, Product, User, of toekomstige entiteiten). Een nieuwe dialog die hier niet aan voldoet, hoort niet gemerged te worden.

Doel: elke dialog voelt identiek aan voor de gebruiker, hergebruikt dezelfde primitives, en heeft de drielaagse demo-policy + auth-scoping standaard ingebakken.

Voor entity-specifieke afwijkingen of velden: schrijf één begeleidende doc per entiteit (zie sectie § Per-entiteit profile). Voorbeeld: docs/specs/dialogs/task.md is het Task-profiel.


1 — Verplichte uitgangspunten

# Regel Bron / waarom
1.1 Bouw op components/ui/dialog.tsx (de bestaande shadcn/@base-ui/react-wrapper). Geen directe imports van dialog-primitives uit @base-ui/react. Voorkomt twee parallelle dialog-implementaties met inconsistente animatie/focus-trap/theming
1.2 Gebruik composition via de render-prop (zie CLAUDE.md "UI Library Conventions"). Nooit Radix' asChild. Project gebruikt @base-ui/react, niet Radix
1.3 Mode (create vs edit vs detail vs inspector) wordt afgeleid uit één input — een prop, een state-object of een searchParam. Niet twee aparte componenten. Voor inspector: zie § 4a. Voorkomt code-duplicatie en inconsistente labels/footer-layouts
1.4 Auth-scoping op elke server action via productAccessFilter(userId) (of het scope-helper-equivalent). Cross-tenant writes mogen onmogelijk zijn. CLAUDE.md "Toegangsmodel" + docs/patterns/server-action.md
1.5 Drielaagse demo-policy (verplicht — zie § 6) op elke write-actie. CLAUDE.md "Demo-check" + docs/architecture.md#demo-user-policy
1.6 Validatie via één gedeeld zod-schema (lib/schemas/<entity>.ts) — gebruikt door zowel form als server action. CLAUDE.md "Validatie"
1.7 Foutcodes volgen de project-conventie (§ 5). CLAUDE.md "Foutcodes API"
1.8 Geen willekeurige Tailwind-kleuren (bg-blue-500 etc.). Alleen MD3-tokens uit app/styles/theme.css. docs/design/styling.md

2 — Stack & dependencies

Toegestane runtime-deps voor dialog-werk (al aanwezig of standaard pattern):

Doel Voorkeur Acceptabele alternatief
Form-state react-hook-form + @hookform/resolvers/zod useActionState + useFormStatus (Server Actions, native React)
Auto-grow textarea react-textarea-autosize
Markdown-rendering (preview) react-markdown + remark-gfm (via bestaande <Markdown>-wrapper)
Toasts sonner
Iconen lucide-react

Per-dialog mag je kiezen tussen react-hook-form of useActionState. Beide patronen draaien al in deze codebase. Kies één per dialog en blijf consistent binnen dat bestand. Mix ze niet.

Verboden in dialog-context (v1):

  • material-color-utilities (dynamic color valt buiten v1)
  • Nieuwe form-libraries — geen formik, final-form, etc.

3 — Component-architectuur

3.1 Reusables (components/entity-dialog/ of components/shared/)

Deze primitives kennen géén entity-specifieke types en mogen door élke dialog gebruikt worden:

Primitive Locatie Verantwoordelijkheid
<Dialog> + <DialogContent> etc. components/ui/dialog.tsx Shell, motion, focus-trap, backdrop
<PrioritySelect> / <PrioritySegmented> components/shared/priority-select.tsx P1-P4 — identiek over alle entiteiten
<DemoTooltip> components/shared/demo-tooltip.tsx Wrapper rond write-knoppen voor demo-modus (laag 3 van 3)
Auto-grow textarea (toe te voegen wanneer nodig in components/shared/) Wrapper rond react-textarea-autosize met char-counter + markdown-hint
Dirty-close-guard (gedeelde AlertDialog-flow) "Wijzigingen niet opgeslagen — weggooien?"
<Markdown> components/markdown.tsx react-markdown + remark-gfm voor description/criteria-preview

Wanneer je een primitive twee keer kopieert tussen entity-dialogs, promote 'm meteen naar components/shared/ (of components/entity-dialog/). Drie keer is te laat.

3.2 Entity-specifieke laag (components/<domain>/<entity>-dialog.tsx)

Per entiteit één wrapper-bestand dat:

  1. De juiste form/body rendert
  2. De juiste server actions koppelt (save<Entity>Action, delete<Entity>Action)
  3. Entiteit-specifieke labels levert ("Story bewerken", "PBI aanmaken", etc.)

Een entity-dialog bevat geen layout-mechanica, motion-config of dirty-check zelf — die komen uit § 3.1.


4 — Layout & responsive gedrag

Identiek voor élke dialog (geen entity-specifieke variaties tenzij expliciet beargumenteerd in het entity-profile):

Breakpoint Breedte Hoogte
Mobiel (< 640px) full-screen full-screen
Tablet (6401024px) 90vw max-h-[85vh]
Desktop (≥ 1024px) max-w-[50vw], min-w-[480px] max-h-[85vh]

Verplicht:

  • Padding p-6 rondom (24px)
  • Veld-spacing in body space-y-6 (24px)
  • Sticky header (titel + close) en sticky footer (knoppen)
  • Body scrollt onafhankelijk; geneste scrolls vermijden
  • Footer heeft border-t in outline-variant

4a — Inspector-mode (hybrid detail + inline-edit)

Een inspector-dialog is een detail-overlay met inline-bewerkbare velden voor een lopend record (typisch een taak, run of job). Onderscheidt zich op drie punten van create/edit/detail:

Aspect Create/Edit/Detail Inspector
Persistence submit-knop in footer roept één Server Action aan per-veld blur-save via Route Handler (PATCH) of fine-grained Server Actions
Footer statisch (Annuleren + Opslaan/Aanmaken/Verwijderen) dynamisch — bevat status-indicatoren en context-knoppen (bv. "Voer uit", "Annuleer agent", "Open PR") afhankelijk van een job/run-status
Body sequentieel form (één entiteit invullen) gegroepeerde secties: read-only metadata + bewerkbare controls + activity-status
Dirty-guard verplicht (§8.1) n.v.t. — wijzigingen worden direct gepersisteerd
Submit-shortcut Cmd/Ctrl+Enter verplicht (§8.2) n.v.t. — geen submit
Validatie 422-fieldErrors in form toast bij PATCH-fout, optimistisch terugdraaien

Wanneer kiezen voor inspector i.p.v. detail-mode?

  • Het record is "actief" (bv. agent draait erop) en meta-edits moeten direct effect hebben zonder save-cycle
  • Verschillende velden gaan naar verschillende endpoints en willen niet gebundeld worden
  • De footer toont liveness-info (job-status) i.p.v. acties op het hele record

Layout-eisen (verplicht, gelijk aan §4):

  • Bouw op components/ui/dialog.tsx
  • <DialogContent className={entityDialogContentClasses}>
  • Sticky header met entityDialogHeaderClasses of equivalent (shrink-0 + border-b border-outline-variant)
  • Body in entityDialogBodyClasses (flex-1 overflow-y-auto px-6 py-6 space-y-6)
  • Footer in entityDialogFooterClasses + extra modifiers voor wrap-gedrag bij dynamische knoppen (flex flex-wrap items-center gap-2)

Wat blijft hetzelfde als bij andere modi:

  • Drielaagse demo-policy (§6) — proxy-guard, server/route-handler session.isDemo-check, <DemoTooltip> rond bewerkbare controls
  • MD3-tokens (§9), motion (§8.4), backdrop (§8.5), focus return (§8.3)
  • Auth-scoping op elke write (§1.4)
  • Eén entity-profile in docs/specs/dialogs/<entity>.md

Wat je expliciet niet doet in inspector-mode:

  • Geen useDirtyCloseGuard (geen dirty-state) — Esc/backdrop sluit direct
  • Geen useDialogSubmitShortcut (geen submit)
  • Geen verplichte lib/schemas/<entity>.ts voor het hele record — wél schema's per PATCH-veld of per fine-grained action
  • Geen footer met statische save/cancel-knoppen — die suggereren bundle-save

Voorbeeld in deze codebase: components/solo/task-detail-dialog.tsx — opent een lopende solo-taak, plan-textarea slaat op blur op via PATCH /api/tasks/:id, verify-toggles direct via dezelfde route, footer toont job-status met context-acties (Voer uit / Wacht op agent / Annuleer / Open PR / Mislukt).

Profiel: docs/specs/dialogs/task-detail.md.


5 — Validatie & foutcodes

5.1 zod-schema

Eén lib/schemas/<entity>.ts per entiteit. Geïmporteerd door zowel form als server action — geen aparte definities.

5.2 Foutcodes (verplicht)

Code Wanneer UI-respons
422 zod-validatiefout (server-side dubbelcheck) fieldErrors mappen naar form.setError(), géén toast
403 demo-sessie probeert te schrijven, of cross-tenant write geblokkeerd toast "Niet toegestaan in demo-modus" of "Geen toegang", form blijft open
400 malformed JSON-body (request.json() faalt) — alleen bij REST-route-handlers toast "Ongeldige aanvraag"
500 onverwachte serverfout toast met "Opnieuw proberen"-knop, form-state behouden

Field-level errors zijn alleen geldig bij code: 422. Bij andere codes is fieldErrors ongedefinieerd.

5.3 Field-level rendering

  • Errors onder het veld, in text-error, met border-error op het input-element
  • Géén toast voor field-level errors
  • Submit-knop blijft enabled bij errors — klik scrollt naar eerste error-veld + focus
  • react-hook-form mode: onTouched (eerste validatie bij blur, daarna onChange)

6 — Drielaagse demo-policy (verplicht voor write-dialogs)

Elke dialog die schrijft (create / edit / delete) MOET door alle drie de lagen heen:

  1. Middleware-guard in proxy.ts — blokkeert demo-sessies op write-routes vóór de server action loopt. Returnt 403.
  2. session.isDemo-check binnen elke save<Entity>Action / delete<Entity>Action zelf — defense-in-depth voor het geval een actie buiten een proxy-route loopt. Returnt 403.
  3. <DemoTooltip show={isDemo}> rond de submit- en delete-knoppen — UI-laag: knoppen disabled met tooltip "Demo-modus: opslaan uitgeschakeld".

Eén laag missen = bug. Reviewers moeten alle drie de lagen kunnen aanwijzen in de PR.


7 — Submission-flow

7.1 Server Action (template)

// actions/<entity>.ts
'use server'

export async function save<Entity>Action(
  input: <Entity>Input,
  context: { /* ids voor revalidatePath en scope */ },
): Promise<Save<Entity>Result> {
  const session = await getSession()
  if (!session.userId) return { ok: false, code: 403, error: 'forbidden' }
  if (session.isDemo)  return { ok: false, code: 403, error: 'demo_readonly' }

  const scope = await productAccessFilter(session.userId) // verplicht
  const parsed = <entity>Schema.safeParse(input)
  if (!parsed.success) {
    return { ok: false, code: 422, error: 'validation', fieldErrors: parsed.error.flatten().fieldErrors }
  }
  // ... Prisma write binnen `scope` ...
  // revalidatePath(...) op de context-route
  return { ok: true, <entity>: row }
}

type Save<Entity>Result =
  | { ok: true; <entity>: <Entity> }
  | { ok: false; code: 422; error: 'validation'; fieldErrors: Record<string, string[]> }
  | { ok: false; code: 403; error: 'demo_readonly' | 'forbidden' }
  | { ok: false; code: 500; error: 'server_error' }

7.2 Revalidation

revalidatePath op de context-route waarin de dialog werd geopend, niet op een statisch path. Context wordt door de aanroepende client meegegeven (geen hard-coded paths in de action).

7.3 Submit-flow

  • Synchroon (geen optimistic update in v1, behalve waar het store-patroon usePlannerStore al bestaat)
  • Tijdens submit: cancel- en submit-knop disabled, spinner of "…" in submit-knop, velden blijven enabled
  • Server saniteert en valideert opnieuw met hetzelfde zod-schema

8 — Dialog-gedrag (UX-regels)

8.1 Sluiten met dirty state

  • Form niet aangeraakt → Esc / backdrop-klik / Cancel sluiten direct
  • Form isDirty → Esc / backdrop-klik / Cancel triggeren AlertDialog: "Wijzigingen niet opgeslagen — weggooien?"

8.2 Keyboard shortcuts

Toets Actie
Esc Sluiten (met dirty-check)
Cmd/Ctrl + Enter Submit vanuit elk veld
Enter in <input type=text> Geen submit (alleen Cmd/Ctrl+Enter)
Enter in <textarea> Newline (default browser, niet overriden)
Tab Top-naar-bottom door velden, dan Cancel → Submit (en Delete in edit-mode)

8.3 Focus management

  • Bij openen: focus op het eerste tekstveld (meestal title / name)
  • Edit-mode: cursor aan het einde van bestaande waarde, geen auto-select (anders typt user per ongeluk weg)
  • Bij sluiten: focus terug naar het element dat de dialog opende (@base-ui/react doet dit by default — niet breken)
  • Bij submit-error: focus naar eerste error-veld

8.4 Motion (MD3-conform)

  • Open: 250ms, easing cubic-bezier(0.2, 0, 0, 1), scale 0.95→1 + opacity 0→1
  • Close: 200ms, easing cubic-bezier(0.4, 0, 1, 1)

8.5 Backdrop

rgba(0,0,0,0.4) (iets sterker dan MD3-default 0.32 voor contrast op zowel licht als donker).


9 — Theming (MD3-tokens)

Surface Token
Dialog-background surface-container-high + shadow-2xl (getemperde opacity)
Form input-background surface-container-low, geen shadow
Footer top-border outline-variant
Submit-knop (filled) primary background + on-primary tekst
Cancel-knop (text) geen background + primary tekst
Delete-knop (tonal error) error-container background + on-error-container tekst

Density: comfortable (geen compact). Single-line input h-14 (56px MD3-default).

Typography:

  • headline-small (24px) — dialog-titel
  • body-large (16px) — form-input tekst
  • body-medium (14px) — helptext, counter

10.1 Edit-mode

[ Verwijderen ]                    [ Annuleren ]   [ Opslaan ]
  tonal (error-container)            text            filled (primary)

10.2 Create-mode

                                   [ Annuleren ]   [ Aanmaken ]
                                     text            filled (primary)

10.3 Detail-mode (read-only)

                                                   [ Sluiten ]
                                                     filled (primary)

10.4 Delete-flow (verplicht patroon)

  1. Klik "Verwijderen" → AlertDialog: "Weet je zeker? Dit kan niet ongedaan worden."
  2. Bevestigen → delete<Entity>Action (zelfde auth-scoping én demo-checks als save) → revalidatePath op context-route → dialog sluit → toast " verwijderd"
  3. Geen undo in v1

11 — Triggers & URL-state

Elke dialog wordt geopend vanuit een context-pagina (sprint, backlog, dashboard, …). Twee patronen zijn toegestaan:

?new<Entity>=1                         → create-dialog open
?edit<Entity>=<id>                     → edit-dialog open

Kies dit patroon wanneer de dialog gedeeld of gebookmarkt moet kunnen worden, of wanneer Suspense + skeleton voor edit-mode gewenst is.

Sluiten = dezelfde route opnieuw pushen zonder de query-params.

11.2 State-based (voor pure UI-dialogen binnen één client-component)

const [dialogState, setDialogState] = useState<DialogState | null>(null)
<EntityDialog state={dialogState} onClose={() => setDialogState(null)} />

Kies dit patroon voor dialogen die altijd binnen één parent-component leven en geen deep-linking nodig hebben (bv. PBI-dialog binnen PbiList, Story-dialog binnen StoryPanel).

Mix de twee patronen niet binnen één entity-dialog. Documenteer in het entity-profile welk patroon gekozen is en waarom.

11.3 Server-side fetch in edit-mode

Bij URL-based pattern: server component fetcht het record vóór render — inclusief auth-scoping. Bestaat het record niet of valt het buiten scope → toast + redirect naar context-route zonder query-param.

Voor state-based pattern: het record komt al uit de parent (in-memory store of server-prop), dus geen aparte fetch nodig.


12 — Per-entiteit profile (verplicht)

Voor elke entiteit met een dialog hoort één profiel-doc te bestaan: docs/<<entity>-dialog>.md (of vergelijkbaar). Het profiel vult de generieke spec aan en bevat alleen de entity-specifieke onderdelen.

Verplichte secties van het entity-profile

# <Entity>Dialog Profiel

> Volgt `docs/patterns/dialog.md`. Dit document beschrijft alleen de afwijkingen en entity-specifieke keuzes.

## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| ... | ... | create / edit / both | ... |

## URL- of state-pattern
- Gekozen: <URL-based | state-based>
- Reden: <korte motivatie>
- Routes (indien URL-based): `?new<Entity>=1` op `<route>`, `?edit<Entity>=<id>` op `<route>`

## Status-veld (indien aanwezig)
- Enum: ...
- Dot-kleur-mapping: ...
- Default bij create: ...

## Server actions
- `save<Entity>Action` — locatie, context-arg, revalidate-paden
- `delete<Entity>Action` — locatie, context-arg, revalidate-paden

## Speciale gedragingen (alleen indien de generieke spec niet volstaat)
- ... (bv. story-log-tab, claude-question-historie, file-upload, etc.)

## Bewust NIET in v1
- ...

Geen entity-profile = geen merge. Reviewers checken op het bestaan van het profiel-bestand bij een PR die een nieuwe dialog introduceert.


13 — Bewust niet in deze pattern (out of scope)

Deze items zijn over het algemeen niet welkom in een dialog. Een entity-profile mag ze toevoegen, maar moet dan de afweging documenteren:

  • Bulk-edit (meerdere records tegelijk)
  • Drag-and-drop binnen de dialog (drag hoort op de lijst-view)
  • Tabs voor secties — alleen spacing-gebaseerde groepering
  • Section-headers — implicit via spacing, geen labels
  • Sub-entiteiten (parent-child binnen dezelfde dialog)
  • File uploads (uitzondering: avatar — eigen pattern)
  • Comments / activity log binnen de dialog (mag wel als read-only side-panel; zie StoryLog)
  • Optimistic locking — last-write-wins binnen scope
  • Cmd+K / quick-create zonder dialog
  • Templates voor terugkerende records
  • Telemetrie / per-veld analytics

14 — Verificatie-checklist (per nieuwe dialog)

Reviewer en bouwer lopen deze door vóór merge:

  • Bouwt op components/ui/dialog.tsx (geen directe @base-ui/react-imports)
  • Composition via render-prop waar nodig
  • Layout volgt § 4 (responsive breakpoints, padding, sticky header/footer)
  • zod-schema in lib/schemas/<entity>.ts, gedeeld door form en action
  • Server action heeft productAccessFilter(userId) + session.isDemo-check
  • proxy.ts heeft een guard voor de bijbehorende write-route(s) (laag 1)
  • Submit/Delete-knoppen omwikkeld met <DemoTooltip show={isDemo}> (laag 3)
  • Foutcodes 422/403/500 correct teruggemapt naar UI (zie § 5)
  • Dirty-close-guard actief
  • Cmd/Ctrl+Enter submit, Esc sluit (met dirty-check)
  • Focus management: opent op eerste veld, geen auto-select in edit-mode
  • Motion + backdrop conform § 8.48.5
  • Alleen MD3-tokens; geen bg-blue-500-style classes
  • Footer-layout per mode conform § 10
  • Delete-flow: AlertDialog → action → toast
  • URL- of state-pattern gekozen + gedocumenteerd in entity-profile
  • Entity-profile bestaat in docs/ en is up-to-date
  • Test gedekt: server action met validatie + demo + scope, en component-test op render + submit-flow

15 — Referenties

  • CLAUDE.md — UI Library Conventions, Demo-check, Foutcodes API, Validatie
  • docs/design/styling.md — MD3-tokens, kleurklassen
  • docs/architecture.md — Demo user policy, scope-helpers
  • docs/patterns/server-action.md — Server Action template (auth + Zod)
  • docs/patterns/zustand-optimistic.md — voor lijst-views die de dialog aanroepen
  • docs/specs/dialogs/task.md — voorbeeld-profile voor entiteit "Task"