Scrum4Me/docs/scrum4me-task-dialog.md
Janpeter Visser 6cd98129f2
M14: TaskDialog (create/edit) + story auto-promotion (#21)
* chore(ST-1112): add deps for task dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add shared zod schema for task dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add missing MD3 tokens for task dialog

outline-variant, on-error-container, status-review (light + dark)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add saveTask and deleteTask server actions for TaskDialog

Unified create/edit action (saveTask) replaces separate formData-based
actions for the new TaskDialog. Uses shared zod schema, structured
SaveTaskResult union type, and context-aware revalidatePath for both
sprint and backlog routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add TaskDialog component (create & edit mode)

Builds the full TaskDialog on top of the existing @base-ui/react
Dialog primitive. Covers create mode, edit mode (status field +
created_at metadata + delete), dirty-check AlertDialog, delete
confirm AlertDialog, Cmd+Enter submit, and per-field char counters.
Uses react-hook-form + zodResolver against the shared taskSchema.
Priority and status are extracted to PrioritySegmented and
StatusSelect sub-components using MD3 tokens throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): refactor task-list to open TaskDialog via URL params

Replaces inline create/edit forms with router.push navigation:
- Clicking a task row → ?editTask=<id>
- "+ Taak" button → ?newTask=1&storyId=<storyId>
Removes CreateTaskForm, EditSubmitButton, updateTaskAction, and
createTaskAction from the component. Status toggle and DnD remain
unchanged. Rows now have cursor-pointer and keyboard a11y.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): wire TaskDialog into sprint page via searchParams

Sprint page now reads ?newTask, ?storyId, and ?editTask query params.
For edit mode: fetches the task server-side with productAccessFilter
scope (invalid/foreign IDs redirect to closePath). Renders TaskDialog
when either param is present. closePath is the sprint route without
query params.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add Suspense skeleton for edit-mode task loading

Extracts task fetch into EditTaskLoader (async server component) so
the sprint board renders immediately while the task loads.
TaskDialogSkeleton shows 3 grey bars during the fetch. Invalid or
out-of-scope task IDs redirect to closePath.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): render description as markdown in task-detail-dialog

Solo task detail now renders description via react-markdown +
remark-gfm with prose styling. Sanitizes script/iframe elements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ST-1112): add saveTask/deleteTask server action tests

Covers all three demo-policy layers and cross-tenant scope:
demo blocked (403), unauthenticated blocked, validation 422,
edit cross-tenant forbidden, create cross-tenant forbidden,
and happy-path for both edit and create.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add updateTaskStatusWithStoryPromotion helper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): wire story-promotion into saveTask and PATCH /api/tasks/:id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(ST-1112): add task-dialog doc and architecture note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: extend allowed tools in settings.local.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1113): add 200ms animation-delay to TaskDialogSkeleton to prevent flicker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1114): add DirtyCloseGuard reusable component for dirty-form close confirmation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1114): add shared Markdown wrapper, apply to task-detail and story-dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: allow grep -E pattern in settings.local.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 16:55:20 +02:00

24 KiB

Scrum4Me — TaskDialog Spec

Volledige design-spec voor de add/update task dialog van de inspannings monitor app. Resultaat van een grill-me sessie (15 vragen, alle beslissingen vastgelegd).


Stack

  • Framework: Next.js (App Router)
  • ORM: Prisma
  • UI components: shadcn/ui — wrappers rond @base-ui/react (zoals expliciet vastgelegd in CLAUDE.md)
  • Styling: Tailwind CSS
  • Form: react-hook-form + @hookform/resolvers/zod
  • Design language: Material Design 3 als theming-laag (geen MUI components)
  • Theming: material-color-utilities voor dynamic color, next-themes voor dark mode
  • Icons: Lucide
  • Markdown rendering: react-markdown + remark-gfm
  • Toasts: sonner (shadcn default)

Composition-regel: dit project gebruikt @base-ui/react, niet Radix. Composition gebeurt via de render-prop, niet via asChild. Zie ook CLAUDE.md "UI Library Conventions".

// ✅ goed
<TooltipTrigger render={<button />}>...</TooltipTrigger>
// ❌ fout — geeft TS-errors
<TooltipTrigger asChild><button>...</button></TooltipTrigger>

Dialog-primitive: bouw de TaskDialog op de bestaande wrapper in components/ui/dialog.tsx (shadcn rond @base-ui/react). Geen directe imports uit @base-ui/react voor dialog-primitives in deze feature — anders krijg je twee parallelle dialog-implementaties die uit de pas gaan lopen qua animatie, focus-trap en theming.


Dependency-impact

De volgende packages staan nog niet in package.json en moeten direct als runtime-dependencies worden toegevoegd voordat de eerste commit van deze feature gemerged wordt (CLAUDE.md "Dependencies"-regel). Voeg ze in dezelfde change toe waarin ze geïmporteerd worden, en vermeld ze in de docs-sync.

Package Doel Scope
react-hook-form form-state management voor TaskDialog runtime
@hookform/resolvers zod-resolver voor react-hook-form runtime
react-textarea-autosize auto-grow textareas voor description / implementation_plan runtime
react-markdown markdown rendering elders in de app (taakdetail, hover-card) runtime
remark-gfm GFM-extensies (tabellen, taken, strikethrough) runtime
@tailwindcss/typography prose-classes voor markdown-styling runtime (Tailwind v4 plugin)

Bewust niet meegenomen:

  • material-color-utilities — dynamic color valt buiten v1 (zie Theming hieronder).
  • nuqs — start met native searchParams; als de URL-state-handling te omslachtig wordt, dan pas nuqs als losse refactor-task introduceren. Niet in deze feature mengen.

Reeds aanwezig en gebruikt: @base-ui/react, next-themes, lucide-react, sonner, zod, prisma.


Component-API

Eén component TaskDialog, mode afgeleid uit task?: Task prop:

<TaskDialog task={editTask} />   // task undefined = create mode, task aanwezig = edit mode

Open/close-state komt uit de URL via nuqs of searchParams. Taken leven binnen de context van een sprint of een PBI/story — er is geen zelfstandige /tasks-route:

/sprint/<sprintId>?newTask=1               → create-dialog open binnen sprint-context
/sprint/<sprintId>?editTask=<taskId>       → edit-dialog open binnen sprint-context
/products/<productId>/backlog?newTask=1    → create-dialog open binnen backlog-context
/products/<productId>/backlog?editTask=<taskId>

Dialog sluit door dezelfde route opnieuw te pushen zonder de newTask / editTask query-params (bv. router.push(\/sprint/${sprintId}`)`).


Velden die de dialog gebruikt

De dialog leest en schrijft uitsluitend deze velden van het Task-record. Het volledige datamodel valt buiten scope van deze spec.

Veld Type Mode
title string (required) beide
description string | null beide
implementation_plan string | null beide
priority int (1-4, P1 = hoogste) beide
status TaskStatus enum alleen edit (default TO_DO op create, niet getoond)
created_at Date alleen edit, read-only metadata in header

TaskStatus enum-waarden: TO_DO | IN_PROGRESS | REVIEW | DONE.


Layout & responsive gedrag

Breakpoint Breedte Hoogte
Mobiel (<640px) full-screen full-screen
Tablet (640-1024px) 90vw max-h-[85vh]
Desktop (≥1024px) max-w-[50vw], min-w-[480px] max-h-[85vh]
  • Padding: p-6 rondom
  • Veld-spacing binnen blok: space-y-6 (24px)
  • Sticky header (titel + close) en sticky footer (knoppen)
  • Body scrollt als content de max-h overschrijdt
  • Footer heeft top-border in outline-variant kleur

Velden

In volgorde van boven naar beneden:

Veld Control Mode Validatie
title Input (single-line) beide required, trim, 1-120 chars
description Textarea (auto-grow, 3-6 regels) beide optional, max 2.000 chars, markdown
implementation_plan Textarea (auto-grow, 5-12 regels) beide optional, max 10.000 chars, markdown
priority Segmented buttons (P1/P2/P3/P4) beide int 1-4, default 3
status Select met gekleurde dot alleen edit enum, default TO_DO

Verberg status in create-mode (default = TO_DO is genoeg).

Auto-grow textareas

Gebruik react-textarea-autosize. Bereikt het veld zijn max-regels, dan overflow-y-auto (interne scroll). De dialog-body scrollt onafhankelijk; je krijgt zelden geneste scrolls.

Karakter-counter

Alleen tonen vanaf 75% van de limiet. Klein, rechtsonder in het veld, muted-foreground kleur. Bv. 1547 / 2000.

Markdown hint

Onder elk textarea: Markdown ondersteund (lijstjes, **vet**, \code`)` — klein, muted.

Priority segmented buttons

[ P1 Critical ]  [ P2 High ]  [ P3 Medium ]  [ P4 Low ]
   error           tertiary      primary        outline
  • Lager getal = hoger prio (industriestandaard, Linear/Jira-conform)
  • Default geselecteerd: P3 Medium
  • Geen 0-waarde toestaan

Status select (alleen edit)

  • TO_DO — grijze dot
  • IN_PROGRESS — blauwe dot
  • REVIEW — paarse dot
  • DONE — groene dot

created_at als header-metadata

In edit-mode tonen in de dialog-header naast de titel:

Taak bewerken                          Aangemaakt: 23 apr 2026

Klein, muted-foreground, niet als form-veld.


Validatie

  • Gedeeld zod-schema in lib/schemas/task.ts, geïmporteerd door zowel form als server action
  • react-hook-form mode: onTouched (eerste validatie bij blur, daarna onChange)
  • Errors onder het veld, in error-color, met label en outline van het veld in dezelfde kleur
  • Geen toasts voor field-level errors
  • Submit-button blijft enabled bij errors — klik scrollt naar eerste error-veld + focus
// lib/schemas/task.ts (richtlijn)
export const taskSchema = z.object({
  title: z.string().trim().min(1, "Verplicht").max(120),
  description: z.string().max(2000).optional(),
  implementation_plan: z.string().max(10000).optional(),
  priority: z.number().int().min(1).max(4),
  status: z.nativeEnum(TaskStatus).optional(), // alleen in edit
});

Submission

Auth-scoping (verplicht)

Elke server action — zowel saveTask als deleteTask — moet de operatie scope-en op de huidige user. Cross-tenant writes voorkomen via productAccessFilter(userId) (of het project-equivalent), zodat een user geen task kan schrijven of verwijderen die niet onder zijn product-scope valt.

Concreet: de Prisma-mutatie staat nóóit alleen op where: { id: taskId }. De scope wordt verplicht gecombineerd in elke update/delete/create-call.

Demo read-only enforcement (drie lagen — ST-1110)

Elke write-flow moet door deze drie lagen:

  1. Middleware-guard in proxy.ts — blokkeert demo-sessies op write-routes vóór de server action überhaupt loopt. Returnt 403.
  2. session.isDemo-check in de server action zelf — defense-in-depth voor het geval een write-flow buiten een proxy-route loopt (bv. directe action-invocation). Returnt 403.
  3. <DemoTooltip> op de save- en delete-knoppen — UI-laag: knoppen zijn zichtbaar disabled met tooltip "Demo-modus: opslaan uitgeschakeld". Vermijdt onnodige round-trips.

Server Action

// app/actions/tasks.ts
"use server"

export async function saveTask(
  input: TaskInput,
  context: { sprintId?: string; productId?: string }, // voor revalidatePath en scope
): Promise<SaveTaskResult> {
  const session = await getSession();
  if (session.isDemo) return { ok: false, code: 403, error: "demo_readonly" };

  const scope = await productAccessFilter(session.userId); // verplicht
  // ... validate met taskSchema → Prisma write binnen `scope`
}

type SaveTaskResult =
  | { ok: true; task: Task }
  | { 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" }

Foutcodes (volgens CLAUDE.md "Foutcodes API")

Code Wanneer UI-respons
422 zod-validatiefout (server-side dubbelcheck) fieldErrors mappen naar form.setError(), geen toast
403 demo-sessie probeert te schrijven, of cross-tenant write geblokkeerd toast "Niet toegestaan in demo-modus" / "Geen toegang", form blijft open
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.

Revalidation

revalidatePath op de context-route waarin de dialog werd geopend, niet op een statische /tasks-path:

if (context.sprintId)  revalidatePath(`/sprint/${context.sprintId}`);
if (context.productId) revalidatePath(`/products/${context.productId}/backlog`);

De aanroepende client geeft de relevante sprintId of productId mee als argument bij elke save/delete. Geen hard-coded paths in de action zelf.

Flow

  • Synchroon (geen optimistic update in v1)
  • Tijdens submit: cancel- en save-knop disabled, spinner in save-knop met "Opslaan...", velden blijven enabled
  • Server saniteert en valideert opnieuw met hetzelfde zod-schema
  • Field-level server errors (bv. unique constraint op title binnen scope) → code: 422 met fieldErrors, terugmappen naar form.setError()

Error handling

  • 422 → field errors inline tonen, geen toast
  • 403 → toast met passende boodschap, form blijft open, ingevulde waarden behouden
  • 500 / netwerk → toast met "Opnieuw proberen"-knop, form-state behouden, knoppen weer enabled

Dialog-gedrag

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

Keyboard shortcuts

  • Esc — sluit (met dirty-check)
  • Cmd/Ctrl+Enter — submit vanuit elk veld
  • Enter in title-input — submit niet (alleen Cmd/Ctrl+Enter)
  • Enter in textarea — newline (default browser behavior, niet overriden)
  • Tab — title → description → implementation_plan → priority → (status) → cancel → save

Focus management

  • Bij openen: focus op title-input
  • Edit-mode: cursor aan einde van bestaande titel, geen auto-select (anders typt user per ongeluk de titel 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

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)

Backdrop

Scrim rgba(0,0,0,0.4) (iets sterker dan MD3-default 0.32 voor betere contrast op licht/donker).


Edit-mode

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

Create-mode

                                      [ Annuleren ]   [ Aanmaken ]
                                        text            filled (primary)

Delete-flow

  • Klik op "Verwijderen" → AlertDialog: "Weet je zeker? Dit kan niet ongedaan worden."
  • Bevestigen → deleteTask server action (zelfde auth-scoping en demo-checks als saveTask) → revalidatePath op de context-route (/sprint/<sprintId> of /products/<productId>/backlog) → dialog sluit → toast "Taak verwijderd"
  • Geen undo in v1

Triggers (hoe komt de user erbij?)

De dialog wordt vanuit twee context-pagina's geopend: een sprint-detail (/sprint/<sprintId>) of een product-backlog (/products/<productId>/backlog).

Vervangt bestaande create/edit-flows. Deze TaskDialog is de enige flow voor het aanmaken en bewerken van taken in beide contexten. Bestaande inline-edit-paden in components/sprint/task-list.tsx (en eventueel in de backlog) worden door deze dialog vervangen — niet er naast geplaatst. De huidige task-row-rendering wordt aangepast om bij klik de dialog te openen via ?editTask=<id>; geen aparte edit-icon, geen inline form. Een eventuele "+ Nieuwe taak"-knop in de bestaande tasklist-header wordt eveneens omgeleid naar ?newTask=1 op dezelfde route.

  • Create: filled button + Nieuwe taak rechtsboven in de tasklist-header van de huidige context (FAB op mobiel optioneel later). Klik zet de juiste query-param (?newTask=1) op de huidige route.
  • Edit: klik op de hele rij in de tasklist (geen apart edit-icoon). Klik zet ?editTask=<id> op de huidige route.
  • Loading edit-mode: Suspense met minimale skeleton (3 grijze balken voor inputs), 200ms delay zodat snelle fetches geen flicker tonen

Server-fetch

Bij ?editTask=<id>: server component fetcht de taak vóór render — inclusief auth-scoping via productAccessFilter(userId) zodat een user nooit een task uit een ander product kan openen via een geraden ID. Bestaat de taak niet of valt 'm buiten scope → toast + redirect naar de context-route zonder query-param (bv. /sprint/<sprintId>).


Theming (Material Design 3 tokens)

Bron-of-truth in v1: de bestaande statische tokens in app/styles/theme.css zijn canoniek. De TaskDialog consumeert deze tokens en voegt er geen nieuwe aan toe. Dynamic color (material-color-utilities) valt buiten v1 — niet introduceren in deze feature.

Color

  • TaskDialog gebruikt de bestaande MD3-tokens uit app/styles/theme.css: --primary, --on-primary, --surface-container, --surface-container-high, --surface-container-low, --error-container, --on-error-container, --outline-variant, plus de project-specifieke --status-* en --priority-* tokens
  • Eventueel ontbrekende tokens (bv. een specifieke surface-container-high als die er nog niet is) worden in dezelfde commit als de feature aan theme.css toegevoegd, niet ad-hoc per component gehard-codeerd
  • Verboden: willekeurige Tailwind-kleuren (bg-blue-500, etc.). Altijd semantische tokens — zie docs/scrum4me-styling.md

Dark mode

  • next-themes is al in de stack; TaskDialog erft automatisch de actieve kleurmodus via de bestaande tokens
  • Geen extra setup nodig in deze feature

Surface elevation

Hybrid (tonal surface + zachte shadow):

  • Dialog: surface-container-high background + shadow-2xl met getemperde opacity
  • Form inputs: surface-container-low background, geen shadow
  • Geen pure tonal-only (voelt te plat op desktop)

Buttons

  • Filled (Save/Aanmaken): primary background, on-primary tekst
  • Text (Cancel): geen background, primary tekst
  • Tonal error (Delete): error-container background, on-error-container tekst

Density

Comfortable (geen compact):

  • Single-line input-hoogte: 56px (MD3 outlined text field default)
  • Veld-spacing: 24px (space-y-6)
  • Dialog-padding: 24px alle kanten (p-6)

Typography

  • Font: Inter via next/font/google (geen Roboto-dwang)
  • Schaal (beperkt):
    • headline-small (24px) — dialog-titel
    • body-large (16px) — form-input tekst
    • body-medium (14px) — helptext, counter
  • Geen Material-specifieke letter-spacing tweaks; Inter-defaults voldoen

Iconen

Lucide (shadcn default). Geen Material Symbols importeren — ~150kb winst en visueel neutraal genoeg om in MD3-themed app te passen.


Markdown rendering (buiten de dialog)

Voor weergave van description en implementation_plan elders in de app (taakdetail, hover-card, etc.):

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  disallowedElements={["script", "iframe"]}
  className="prose prose-sm dark:prose-invert"
>
  {content}
</ReactMarkdown>
  • Tailwind Typography (prose prose-sm) voor styling
  • remark-gfm voor tabellen, taken, strikethrough
  • react-markdown saniteert by default; disallowedElements als extra defense-in-depth

Hergebruik & generalisatie

De TaskDialog is de eerste van naar verwachting meerdere entity-dialogs (PBI, Story, Todo volgen logisch). Bouw daarom vanaf dag 1 een dunne scheiding tussen generic shell+primitives en entity-specifieke form-body. Dit is geen speculatieve abstractie: de breuklijn tussen "dialog-mechanica" en "welke velden horen bij deze entiteit" is natuurlijk en levert per nieuwe entiteit ~70% codebesparing op.

Wat generic wordt (components/entity-dialog/)

Component Waarom generic
entity-dialog.tsx Shell: sticky header/footer, responsive layout, motion, backdrop, dirty-close-guard, keyboard-shortcuts, focus-management. Slot-props voor body en footer-actions.
priority-segmented.tsx P1-P4 segmented buttons; priority: int 1-4 is identiek over Task / PBI / Story / Todo.
auto-grow-textarea.tsx Wrapper rond react-textarea-autosize met char-counter (vanaf 75%) en markdown-hint. Generic — neemt min/max regels en max-chars als props.
dirty-close-guard.tsx AlertDialog "Wijzigingen niet opgeslagen — weggooien?" — entity-agnostisch.

Deze primitives importeren alleen uit components/ui/* en hebben geen kennis van Task / Story / PBI.

Wat entity-specifiek blijft (components/tasks/)

Component Waarom niet generic
task-dialog.tsx Dunne wrapper: kiest body, koppelt saveTask/deleteTask, levert label-strings ("Taak bewerken" / "Aangemaakt: …"). Geen mechanica meer in dit bestand.
task-form.tsx Velden zijn task-specifiek (title, description, implementation_plan, priority, status). Andere entiteiten (Story heeft acceptance_criteria, PBI heeft alleen description) krijgen elk hun eigen *-form.tsx.
task-status-select.tsx TaskStatus enum met 4 specifieke waarden + dot-kleurmapping. StoryStatus (`OPEN

Wat niet abstraheren in v1

  • URL-state pattern?newTask=1 / ?editTask=<id> per route. Een toekomstige PBI-dialog krijgt ?newPbi=1 / ?editPbi=<id> op zijn eigen routes. Copy-paste tussen 2-3 pages is goedkoper dan een generic helper die je later toch moet generaliseren.
  • Save/delete-flows — auth-scoping, demo-checks en revalidatePath verschillen subtiel per entiteit (verschillende productAccessFilter-paden, verschillende context-routes). Per entiteit een eigen actions-file in app/actions/<entity>.ts.

Per-entiteit kostenplaatje

Wanneer er straks een PbiDialog of StoryDialog gebouwd wordt, kost dat alleen:

  1. components/<entity>/<entity>-form.tsx — de velden + zod-schema
  2. components/<entity>/<entity>-status-select.tsx — als de entiteit een status-veld heeft
  3. components/<entity>/<entity>-dialog.tsx — dunne wrapper rond EntityDialog met de juiste form en save/delete-handler
  4. app/actions/<entity>.ts — server actions
  5. URL-state uitbreiding op de relevante page(s)

Geen herhaling van layout, motion, dirty-check, keyboard-shortcuts, of segmented/textarea-primitives.


Bewust NIET in v1

Om scope te bewaken:

  • Bulk-edit (meerdere taken tegelijk)
  • Drag-and-drop herorderen
  • Sub-tasks / parent-child relaties
  • Tags / labels / categorieën
  • Due dates / reminders
  • Attachments / file uploads
  • Comments / activity log
  • Sharing / collaboration
  • Undo na delete (toast met undo-actie)
  • Cmd+K keyboard-driven creation zonder dialog
  • Templates voor terugkerende taken
  • Time tracking (uren-registratie) — wel relevant voor inspannings-monitor, maar apart feature
  • Telemetrie / analytics
  • Optimistic locking — niet geïmplementeerd in v1 (last-write-wins binnen scope)
  • Tabs voor secties — alleen spacing-gebaseerde groepering
  • Section-headers — implicit via spacing, geen labels

Heroverweeg deze keuzes pas als de app groeit. Niet om je te beperken, maar om elke "ja maar moeten we niet ook…"-impuls een bewuste afweging te maken.


File structuur (richtlijn)

app/
├── sprint/
│   └── [id]/
│       └── page.tsx                # leest searchParams, rendert TaskDialog
├── products/
│   └── [id]/
│       └── backlog/
│           └── page.tsx            # leest searchParams, rendert TaskDialog
├── actions/
│   └── tasks.ts                    # saveTask, deleteTask server actions (auth-scoped)
components/
├── ui/
│   ├── dialog.tsx                  # bestaande @base-ui/react-wrapper
│   └── demo-tooltip.tsx            # wrapper voor save/delete-knoppen in demo-mode
├── entity-dialog/                  # GENERIC — geen kennis van Task/Story/PBI
│   ├── entity-dialog.tsx           # shell: header/footer/motion/dirty-check/keyboard
│   ├── priority-segmented.tsx     # P1-P4 segmented buttons
│   ├── auto-grow-textarea.tsx     # textarea met counter + markdown-hint
│   └── dirty-close-guard.tsx      # AlertDialog bij dirty close
├── tasks/                          # ENTITY-SPECIFIEK
│   ├── task-dialog.tsx             # dunne wrapper rond EntityDialog
│   ├── task-form.tsx               # task-velden + react-hook-form binding
│   └── task-status-select.tsx      # TaskStatus enum + dot-kleuren
lib/
├── schemas/
│   └── task.ts                     # gedeeld zod-schema (form + server action)
├── auth/
│   └── product-access-filter.ts    # scope-helper, gedeeld door page-fetches en actions
proxy.ts                            # demo-readonly middleware-guard (laag 1 van 3)

Implementatie-volgorde (suggestie)

  1. Dependencies toevoegen aan package.json (zie "Dependency-impact"); commit als chore(ST-XXX): add deps for task dialog
  2. zod-schema in lib/schemas/task.ts
  3. productAccessFilter helper checken/uitbreiden in lib/auth/
  4. Server actions (saveTask, deleteTask) met auth-scoping én demo-check (laag 2) — testen via thunk
  5. proxy.ts middleware-guard voor demo-routes (laag 1) — alleen als nog niet aanwezig voor deze routes
  6. Eventueel ontbrekende MD3-tokens aanvullen in app/styles/theme.css (geen dynamic color in v1)
  7. <DemoTooltip>-wrapper component (laag 3)
  8. TaskDialog — create-mode eerst (minder edge cases), bovenop bestaande components/ui/dialog.tsx-wrapper
  9. Edit-mode toevoegen (status field, delete-knop, created_at-metadata)
  10. URL-state via native searchParams binnen sprint en backlog routes (geen nuqs in v1)
  11. Bestaande task-row / tasklist-trigger refactorencomponents/sprint/task-list.tsx (en backlog-equivalent) klikbaar maken zodat ze de dialog openen via query-param; oude inline-edit-paden verwijderen
  12. Suspense + skeleton voor edit-mode loading + scope-check op fetch
  13. Dirty-check + AlertDialog
  14. Keyboard shortcuts (Cmd+Enter)
  15. Markdown rendering elders (out-of-scope voor dialog zelf, maar related)