docs: introduce generic entity-dialog pattern + entity-profiles (#45)
* docs(dialog-pattern): add generic entity-dialog spec Introduceert docs/patterns/dialog.md als bron-of-truth voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject. Bevat 14 secties: uitgangspunten, stack, component- architectuur, layout, validatie, drielaagse demo-policy, submission, dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit profile-template, out-of-scope, en een verificatie-checklist. Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel zodat Claude (en mensen) de spec verplicht raadplegen voor elke nieuwe dialog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(dialog-pattern): convert task spec + add pbi/story entity-profiles Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document bevat nu alleen Task-specifieke velden, URL-pattern, status-veld, server actions, triggers en bewuste out-of-scope-keuzes. Voegt twee nieuwe entity-profielen toe voor bestaande dialogen: - docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij, PbiStatusSelect, geen delete in v1) - docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met status/priority badges, inline activity-log, demo-readonly-fallback, inline-delete-confirm i.p.v. AlertDialog) Beide profielen documenteren expliciet de "Bekende gaps t.o.v. generieke spec" zodat opvolgende PR's de afwijkingen kunnen rechtzetten of bewust kunnen accorderen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Added pdevelopment docs --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c6c8b96b7
commit
55a1ee035c
8 changed files with 1241 additions and 469 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -72,3 +72,4 @@ jp.sh
|
||||||
|
|
||||||
# Lokale scratch-bestanden
|
# Lokale scratch-bestanden
|
||||||
Brainstro
|
Brainstro
|
||||||
|
/graphify-out
|
||||||
|
|
@ -111,6 +111,7 @@ Lees het relevante patroon vóór je begint. Nooit uit het hoofd schrijven.
|
||||||
| Middleware (route protection) | `docs/patterns/middleware.md` |
|
| Middleware (route protection) | `docs/patterns/middleware.md` |
|
||||||
| QR-pairing (unauth-SSE + pre-auth cookie) | `docs/patterns/qr-login.md` |
|
| QR-pairing (unauth-SSE + pre-auth cookie) | `docs/patterns/qr-login.md` |
|
||||||
| Bidirectionele async-comms MCP-agent ↔ user | `docs/patterns/claude-question-channel.md` |
|
| Bidirectionele async-comms MCP-agent ↔ user | `docs/patterns/claude-question-channel.md` |
|
||||||
|
| **Entity Dialog (verplicht voor élke create/edit/detail-dialog)** | `docs/patterns/dialog.md` — bron-of-truth; per entiteit één profile-doc (bv. `docs/scrum4me-task-dialog.md`) |
|
||||||
| Status-enum mapping (DB ↔ API) | `lib/task-status.ts` |
|
| Status-enum mapping (DB ↔ API) | `lib/task-status.ts` |
|
||||||
| Client/server module-boundary | `*-server.ts` bevat DB-calls of node-only deps; `*.ts` is pure (client-safe). Nooit `import { ... } from '@/lib/foo-server'` in een client-component, anders krijg je `Module not found: 'dns'`/`'pg'`-style runtime fouten |
|
| Client/server module-boundary | `*-server.ts` bevat DB-calls of node-only deps; `*.ts` is pure (client-safe). Nooit `import { ... } from '@/lib/foo-server'` in een client-component, anders krijg je `Module not found: 'dns'`/`'pg'`-style runtime fouten |
|
||||||
|
|
||||||
|
|
|
||||||
387
docs/patterns/dialog.md
Normal file
387
docs/patterns/dialog.md
Normal file
|
|
@ -0,0 +1,387 @@
|
||||||
|
# Pattern — Entity Dialog
|
||||||
|
|
||||||
|
Deze pagina is **bindend** voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject (PBI, Story, Task, Todo, 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](#per-entiteit-profile-verplicht)). Voorbeeld: `docs/scrum4me-task-dialog.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`) wordt afgeleid uit één input — een prop, een `state`-object of een `searchParam`. **Niet** twee aparte componenten. | 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/scrum4me-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/scrum4me-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 (640–1024px) | `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`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 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 — Footer
|
||||||
|
|
||||||
|
### 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 "<Entity> 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:
|
||||||
|
|
||||||
|
### 11.1 URL-based (voorkeur voor deep-link-bare dialogen)
|
||||||
|
|
||||||
|
```
|
||||||
|
?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)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
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/<scrum4me-<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
|
||||||
|
|
||||||
|
```md
|
||||||
|
# <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.4–8.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/scrum4me-styling.md` — MD3-tokens, kleurklassen
|
||||||
|
- `docs/scrum4me-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/scrum4me-task-dialog.md` — voorbeeld-profile voor entiteit "Task"
|
||||||
133
docs/plans/ST-1114-copilot-reviews.md
Normal file
133
docs/plans/ST-1114-copilot-reviews.md
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
# Plan — ST-1114 · Copilot reviews op dashboard
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Als ontwerper wil je een overzicht zien van GitHub Copilot's PR-reviews om per stuk te beslissen of je 'm implementeert of overslaat. De codebase heeft nu **nul** GitHub-integratie — alleen `product.repo_url` als string voor hyperlinks. We bouwen een minimale, hobby-vriendelijke architectuur.
|
||||||
|
|
||||||
|
## Architectuurkeuzes (via AskUserQuestion bevestigd)
|
||||||
|
|
||||||
|
- **Auth**: lokaal script met `GITHUB_TOKEN` — webapp heeft GEEN GitHub-credentials. Het script draai je lokaal wanneer je wil verversen.
|
||||||
|
- **Fetch**: on-demand op dashboard-load (server-side `prisma.copilotReview.findMany`, geen externe call)
|
||||||
|
- **Decision**: alleen visuele toggle in `localStorage` (geen DB-persistentie)
|
||||||
|
- **Scope**: MVP — tonen + lokale toggle. Geen cron, geen webhook, geen GitHub-auth in productie.
|
||||||
|
|
||||||
|
## Architectuur
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ octokit ┌────────────┐ API token ┌─────────────┐
|
||||||
|
│ scripts/ │ ──────────▶ │ GitHub │ │ Scrum4Me │
|
||||||
|
│ sync-copilot │ │ REST API │ │ /api/ │
|
||||||
|
│ -reviews.ts │ ◀────────── │ │ │ copilot- │
|
||||||
|
└──────────────┘ reviews └────────────┘ POST batch │ reviews │
|
||||||
|
│ │ │
|
||||||
|
└──────────────────────────────────────────────────▶ DB upsert │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
┌──────▼──────┐
|
||||||
|
│ /dashboard │
|
||||||
|
│ server-side │
|
||||||
|
│ findMany │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Het script is de enige plek waar GitHub-credentials nodig zijn. Productie kent alleen Scrum4Me-data.
|
||||||
|
|
||||||
|
## Datamodel
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model CopilotReview {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||||
|
product_id String
|
||||||
|
pr_number Int
|
||||||
|
pr_title String
|
||||||
|
pr_url String
|
||||||
|
pr_state String // 'open' | 'closed' | 'merged'
|
||||||
|
author_login String // bv. 'copilot-pull-request-reviewer[bot]'
|
||||||
|
review_state String // 'COMMENTED' | 'APPROVED' | 'CHANGES_REQUESTED'
|
||||||
|
body String @db.Text
|
||||||
|
submitted_at DateTime
|
||||||
|
html_url String // directe link naar de review
|
||||||
|
fetched_at DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([product_id, pr_number, submitted_at])
|
||||||
|
@@index([product_id, submitted_at])
|
||||||
|
@@map("copilot_reviews")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`@@unique` zorgt voor idempotency — script kan herhaald draaien zonder dupes. Geen `decision`-veld: dat staat in `localStorage`.
|
||||||
|
|
||||||
|
## Script
|
||||||
|
|
||||||
|
`scripts/sync-copilot-reviews.ts` — TypeScript via `tsx`, leest env, gebruikt Octokit, POST naar eigen API.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GITHUB_TOKEN=ghp_... \
|
||||||
|
SCRUM4ME_API_URL=http://localhost:3000 \
|
||||||
|
SCRUM4ME_API_TOKEN=s4m_... \
|
||||||
|
npx tsx scripts/sync-copilot-reviews.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Stappen:
|
||||||
|
1. `GET /api/products` (Bearer-auth) — lijst toegankelijke producten met `repo_url`
|
||||||
|
2. Per product: parse `owner/repo` uit `repo_url`, `octokit.pulls.list({state: 'all', per_page: 50})`
|
||||||
|
3. Per PR: `octokit.pulls.listReviews(...)`, filter op `user.type === 'Bot' && user.login.includes('copilot')`
|
||||||
|
4. `POST /api/copilot-reviews` met `{ product_id, reviews: [...] }` — endpoint doet `deleteMany` + `createMany` per product (atomic replace)
|
||||||
|
5. Print samenvatting: aantal reviews per product + totale runtime
|
||||||
|
|
||||||
|
## API endpoint
|
||||||
|
|
||||||
|
`app/api/copilot-reviews/route.ts`:
|
||||||
|
|
||||||
|
- **POST**: Bearer-auth, demo-block, payload `{ product_id, reviews: CopilotReview[] }`. Atomic transaction: delete-all-for-product → createMany. Validatie via Zod.
|
||||||
|
- **GET**: niet nodig — dashboard leest direct via Prisma server-side. Endpoint kan komen voor toekomstige clients.
|
||||||
|
|
||||||
|
## Dashboard widget
|
||||||
|
|
||||||
|
Boven of onder de bestaande product-grid een nieuwe sectie "Copilot reviews".
|
||||||
|
|
||||||
|
`components/dashboard/copilot-reviews.tsx` (client component):
|
||||||
|
- Props: `reviews: CopilotReview[]` (server-fetched)
|
||||||
|
- Lijst met cards: PR-titel + nummer (link naar PR), Copilot's body (truncated of accordion), state-badge, "Implementeer" / "Skip"-knoppen
|
||||||
|
- Decision-state in `localStorage` keyed op `review.id`: `'implement' | 'skip' | undefined` (default: ongezien)
|
||||||
|
- Cards met decision='skip' visueel gedimmed; cards met 'implement' krijgen een groen randje
|
||||||
|
- Filter-toggles bovenaan: "Alle / Te beoordelen / Implementeren / Skip"
|
||||||
|
- Empty state: "Geen Copilot-reviews gevonden — draai het sync-script."
|
||||||
|
|
||||||
|
`app/(app)/dashboard/page.tsx` past `prisma.copilotReview.findMany({ where: { product_id: { in: accessibleIds } }, orderBy: { submitted_at: 'desc' } })` en geeft door.
|
||||||
|
|
||||||
|
## Voorgestelde sub-tasks
|
||||||
|
|
||||||
|
| Code | Onderwerp |
|
||||||
|
|---|---|
|
||||||
|
| ST-1114.2 | DB: `CopilotReview` model + migration |
|
||||||
|
| ST-1114.3 | API: `POST /api/copilot-reviews` (Bearer-auth + demo-block + replace-by-product) |
|
||||||
|
| ST-1114.4 | Script: `scripts/sync-copilot-reviews.ts` met octokit |
|
||||||
|
| ST-1114.5 | UI: dashboard-widget met cards, localStorage-decision, filter-toggle |
|
||||||
|
| ST-1114.6 | Tests: API endpoint (auth, demo-block, validation), dashboard-widget snapshot |
|
||||||
|
| ST-1114.7 | Docs: README-sectie over script + env-vars; CLAUDE.md-update |
|
||||||
|
|
||||||
|
## M11-keuzes voor de implementerende sessie
|
||||||
|
|
||||||
|
Drie open beslissingen die niet kritiek zijn voor het plan zelf:
|
||||||
|
|
||||||
|
1. **PR-state filter**: alle PR's of alleen `state=open`? (closed-PRs hebben oude reviews die misschien niet meer relevant zijn)
|
||||||
|
2. **Markdown-rendering**: react-markdown, of plain `<pre>`? (react-markdown is +35KB bundle)
|
||||||
|
3. **localStorage-key-vorm**: `scrum4me:copilot-decision:{review_id}` per review, of één map-object onder één key?
|
||||||
|
|
||||||
|
## Branch + PR
|
||||||
|
|
||||||
|
- Branch: `feat/M14-copilot-reviews` (M14 = nieuwe milestone)
|
||||||
|
- 6 commits (.2 t/m .7), één per laag
|
||||||
|
- PR pas openen na handmatige test door gebruiker
|
||||||
|
|
||||||
|
## Verificatie (end-to-end)
|
||||||
|
|
||||||
|
1. `npm run dev`
|
||||||
|
2. `GITHUB_TOKEN=... SCRUM4ME_API_TOKEN=... npx tsx scripts/sync-copilot-reviews.ts` — toont `n reviews opgeslagen`
|
||||||
|
3. Browser refresht dashboard → "Copilot reviews"-sectie toont cards met PR-titels
|
||||||
|
4. Klik "Implementeer" → kaart krijgt groen randje, decision in localStorage
|
||||||
|
5. Refresh → state blijft (localStorage)
|
||||||
|
6. Filter toggle "Alleen te beoordelen" → cards met decision verdwijnen
|
||||||
|
7. Demo-user: kan reviews zien, maar `POST /api/copilot-reviews` weigert (al via middleware-guard van ST-1110)
|
||||||
346
docs/plans/Tweede Claude Agent — Planning Agent.md
Normal file
346
docs/plans/Tweede Claude Agent — Planning Agent.md
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
# Plan: Tweede Claude Agent — Planning Agent (PBI/Story → children)
|
||||||
|
|
||||||
|
> **Eerder goedgekeurd plan in deze file:** *Scrum4Me v1.0 Release* (mobile shell + sprint-snapshots + release-discipline). Beschikbaar in chat-history; te verhuizen naar `docs/plans/v1-release.md` op een later moment. Dit nieuwe plan vervangt de plan-file inhoudelijk niet — het v1.0-werk blijft van kracht parallel hieraan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**Wat de gebruiker wil:** twee gespecialiseerde Claude-agents, elk met eigen context.
|
||||||
|
|
||||||
|
| Agent | Doel | Context | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Implementation Agent** | Codeert één task af → branch + PR | Code-repo (filesystem), `implementation_plan`, story, pbi, sprint, repo_url | ✅ Live (M13 / ST-1111) |
|
||||||
|
| **Planning Agent** *(nieuw)* | Genereert children: PBI→stories of story→tasks (incl. `implementation_plan` per task) | Specs + architectuur + patterns (filesystem in Scrum4Me-checkout), parent-record, bestaande children | ❌ Te bouwen |
|
||||||
|
|
||||||
|
**Wat al staat (hergebruikbaar):**
|
||||||
|
|
||||||
|
- `ClaudeJob`-model met state-machine `QUEUED→CLAIMED→RUNNING→DONE/FAILED`, `CANCELLED`-pad, stale-cleanup >30min
|
||||||
|
- `ClaudeWorker`-model voor presence-heartbeat
|
||||||
|
- SSE-pijplijn op `/api/realtime/solo` met payload-routing op `user_id + product_id`
|
||||||
|
- MCP-tools `wait_for_job`, `update_job_status`, `get_claude_context`, `create_pbi`, `create_story`, `create_task`, `update_task_plan`, `log_implementation`
|
||||||
|
- Idempotency-pattern: max 1 actieve job per resource
|
||||||
|
|
||||||
|
**Vastgelegde keuzes (uit AskUserQuestion-sessies):**
|
||||||
|
|
||||||
|
1. Agent kan **beide** niveaus: PBI→stories én story→tasks (één agent, twee modi via `target_type`)
|
||||||
|
2. Output: items **direct in DB** aanmaken via bestaande `create_*`-tools — geen review-stap in v1
|
||||||
|
3. Context: **lokaal draaien + filesystem-toegang** tot Scrum4Me-checkout (zoals impl-agent al doet)
|
||||||
|
4. Rol-scheiding: **`ClaudeJob.kind` enum** (`IMPLEMENTATION` | `PLANNING`) — één table, polymorf
|
||||||
|
5. Bestaande children: **aanvullen** — agent leest bestaande titels en voegt alleen ontbrekende toe
|
||||||
|
6. Live feedback: **stille SSE + status-pill** op de PBI/Story-card; geen aparte modal
|
||||||
|
7. MCP-shape: **bestaande `wait_for_job` uitbreiden** met `accept_kinds: string[]`-arg, default `['IMPLEMENTATION']` (backwards-compat)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Approach (8 stappen)
|
||||||
|
|
||||||
|
### Stap 1 — Schema-uitbreiding
|
||||||
|
|
||||||
|
**`prisma/schema.prisma`:**
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum ClaudeJobKind {
|
||||||
|
IMPLEMENTATION
|
||||||
|
PLANNING
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlanningTargetType {
|
||||||
|
PBI
|
||||||
|
STORY
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ApiTokenKind {
|
||||||
|
IMPLEMENTATION
|
||||||
|
PLANNING
|
||||||
|
// beide kinds simultaan = afzonderlijke tokens; eenvoudiger dan multi-kind-flag
|
||||||
|
}
|
||||||
|
|
||||||
|
model ClaudeJob {
|
||||||
|
// ... bestaande velden ...
|
||||||
|
kind ClaudeJobKind @default(IMPLEMENTATION)
|
||||||
|
task_id String? // wordt nullable — planning-jobs hebben geen task
|
||||||
|
task Task? @relation(fields: [task_id], references: [id], onDelete: Cascade)
|
||||||
|
planning_target_type PlanningTargetType?
|
||||||
|
planning_target_id String?
|
||||||
|
|
||||||
|
@@index([kind, status])
|
||||||
|
@@index([planning_target_type, planning_target_id, status])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ApiToken {
|
||||||
|
// ... bestaande velden ...
|
||||||
|
kind ApiTokenKind @default(IMPLEMENTATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
model ClaudeWorker {
|
||||||
|
// ... bestaande velden ...
|
||||||
|
// accepted_kinds wordt afgeleid uit token.kind (geen extra kolom nodig)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Constraint via DB-check (CHECK constraint of app-level):** een `ClaudeJob` heeft óf `task_id` (kind=IMPLEMENTATION) óf `planning_target_*` (kind=PLANNING). Nooit beide leeg, nooit beide gevuld.
|
||||||
|
|
||||||
|
**Migratie:** alle bestaande rijen krijgen `kind=IMPLEMENTATION` + `apiToken.kind=IMPLEMENTATION` als default. Backwards-compatible.
|
||||||
|
|
||||||
|
**Bestand:** `prisma/migrations/<date>_planning_job_kind/migration.sql`
|
||||||
|
|
||||||
|
### Stap 2 — Status-mappers + Zod-schemas
|
||||||
|
|
||||||
|
- `lib/claude-job-status.ts` — voeg `kind` toe aan API-shape (lowercase: `implementation` | `planning`)
|
||||||
|
- `lib/schemas/claude-job.ts` (NEW of MODIFY) — discriminated union op `kind`
|
||||||
|
- `lib/schemas/planning-target.ts` (NEW) — `{ type: 'PBI'|'STORY', id: string }` validator
|
||||||
|
|
||||||
|
### Stap 3 — Server actions
|
||||||
|
|
||||||
|
**`actions/claude-jobs.ts`** uitbreiden:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export async function enqueuePlanningJobAction(input: {
|
||||||
|
productId: string
|
||||||
|
target: { type: 'PBI' | 'STORY', id: string }
|
||||||
|
}): Promise<EnqueueResult>
|
||||||
|
```
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. Auth-scope-check (`productAccessFilter`) — target moet binnen product zitten
|
||||||
|
2. Demo-block (`session.isDemo` → 403)
|
||||||
|
3. Idempotency: weiger als er al een `PLANNING`-job actief is voor dit `(target_type, target_id)`
|
||||||
|
4. Insert `ClaudeJob` met `kind=PLANNING`, `task_id=null`, `planning_target_*` ingevuld
|
||||||
|
5. `pg_notify('scrum4me_changes', { type: 'claude_job_enqueued', kind: 'planning', ... })`
|
||||||
|
|
||||||
|
`cancelClaudeJobAction` (bestaand) blijft werken — accepteert nu ook PLANNING-jobs (zelfde state-machine).
|
||||||
|
|
||||||
|
### Stap 4 — SSE-routing
|
||||||
|
|
||||||
|
**`app/api/realtime/solo/route.ts`:**
|
||||||
|
|
||||||
|
- Bestaande `claude_job_*`-events krijgen `kind` in payload
|
||||||
|
- Bij connect: `claude_jobs_initial`-event bevat ook actieve PLANNING-jobs van vandaag
|
||||||
|
- Filter blijft `user_id + product_id` — geen extra topic nodig
|
||||||
|
|
||||||
|
### Stap 5 — UI: triggers + status-pills
|
||||||
|
|
||||||
|
**Trigger in beide dialog-profielen** (geprofileerd in PR #45):
|
||||||
|
|
||||||
|
| Locatie | Knop-label | Target |
|
||||||
|
|---|---|---|
|
||||||
|
| `components/backlog/story-dialog.tsx` (edit-mode) | `🤖 Genereer taken met Claude` | `{ type: 'STORY', id: story.id }` |
|
||||||
|
| `components/backlog/pbi-dialog.tsx` (edit-mode) | `🤖 Genereer stories met Claude` | `{ type: 'PBI', id: pbi.id }` |
|
||||||
|
|
||||||
|
Knop-gedrag:
|
||||||
|
- `<DemoTooltip show={isDemo}>` rond knop (laag 3 demo-policy)
|
||||||
|
- `disabled` als er al een PLANNING-job actief is voor deze target (live via SSE-store)
|
||||||
|
- Tooltip bij disabled: "Plan-job al gestart — wachten op resultaat"
|
||||||
|
- Klik → `enqueuePlanningJobAction` → toast "Plan gestart" → dialog blijft open zodat user resultaat ziet binnenkomen
|
||||||
|
|
||||||
|
**Status-pill component (NEW):**
|
||||||
|
|
||||||
|
`components/shared/planning-job-pill.tsx` — kleine badge die de status van een lopende PLANNING-job toont:
|
||||||
|
- `QUEUED` — grijs, "In wachtrij"
|
||||||
|
- `CLAIMED / RUNNING` — blauw met spinner, "Plan wordt gegenereerd…"
|
||||||
|
- `DONE` — groen, fade-out na 5s
|
||||||
|
- `FAILED` — rood, klikbaar voor error-detail
|
||||||
|
- `CANCELLED` — niet getoond (verwijdert pill)
|
||||||
|
|
||||||
|
Plaatsing:
|
||||||
|
- Op `PbiList`-card naast PBI-titel (rechts)
|
||||||
|
- Op `StoryPanel`-card naast story-titel (rechts)
|
||||||
|
- In `PbiDialog` / `StoryDialog`-header (edit-mode) als large variant
|
||||||
|
|
||||||
|
**Live updates:** bestaande `useClaudeJobsStore` (Zustand, populated uit SSE) — alleen `kind` toevoegen aan filter-helpers.
|
||||||
|
|
||||||
|
### Stap 6 — MCP-tools (`scrum4me-mcp` repo, aparte PR)
|
||||||
|
|
||||||
|
**Wijziging 1 — bestaande tool uitbreiden:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// wait_for_job tool input schema
|
||||||
|
{
|
||||||
|
accept_kinds?: ('IMPLEMENTATION' | 'PLANNING')[] // default: ['IMPLEMENTATION']
|
||||||
|
wait_seconds?: number // bestaand
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Server-side: `WHERE kind = ANY($1) AND status = 'QUEUED'` in de `FOR UPDATE SKIP LOCKED`-query. Token-kind moet ook compatibel zijn (token.kind `IN` accept_kinds-overlap).
|
||||||
|
|
||||||
|
Response-shape voegt `kind` toe; voor `PLANNING`-jobs vervangt `task` door `planning_target` met embedded record:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
job_id: string
|
||||||
|
kind: 'IMPLEMENTATION' | 'PLANNING'
|
||||||
|
product: { id, name, repo_url, ... }
|
||||||
|
// IMPL-only:
|
||||||
|
task?: { ..., implementation_plan, story, pbi, sprint }
|
||||||
|
// PLANNING-only:
|
||||||
|
planning_target?: {
|
||||||
|
type: 'PBI' | 'STORY'
|
||||||
|
pbi?: { id, code, title, description, priority, status, existing_stories: [{ id, code, title, priority }] }
|
||||||
|
story?: { id, code, title, description, acceptance_criteria, priority, status, pbi: {...}, existing_tasks: [{ id, title, priority, status }] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wijziging 2 — nieuwe MCP-tool:**
|
||||||
|
|
||||||
|
`get_planning_context(target_type, target_id)` — losstaande lookup voor agent die handmatig een planning wil starten zonder job-claim. Optioneel; `wait_for_job` retourneert dezelfde data al.
|
||||||
|
|
||||||
|
**Geen nieuwe write-tools nodig:** bestaande `create_story` + `create_task` + `update_task_plan` werken al.
|
||||||
|
|
||||||
|
**Schema-sync:** vendor/scrum4me submodule update na Scrum4Me-PR merge.
|
||||||
|
|
||||||
|
### Stap 7 — Agent-prompt + lokale Claude-command
|
||||||
|
|
||||||
|
In de Scrum4Me-checkout (of in een gedeelde plek voor agent-prompts) twee Claude Code commands:
|
||||||
|
|
||||||
|
**`/implement-next-story`** — bestaand, gebruikt `wait_for_job({ accept_kinds: ['IMPLEMENTATION'] })`
|
||||||
|
|
||||||
|
**`/generate-plan`** — nieuw:
|
||||||
|
|
||||||
|
Korte prompt-flow:
|
||||||
|
1. `wait_for_job({ accept_kinds: ['PLANNING'], wait_seconds: 600 })` — claim
|
||||||
|
2. Lees `planning_target` uit response (PBI of STORY) + `existing_*`
|
||||||
|
3. **Lees lokale docs uit Scrum4Me-checkout:**
|
||||||
|
- `docs/scrum4me-functional-spec.md` (functioneel kader)
|
||||||
|
- `docs/scrum4me-architecture.md` (technisch kader)
|
||||||
|
- `docs/patterns/*.md` (relevante patterns op basis van target-titel/-beschrijving)
|
||||||
|
- `docs/scrum4me-styling.md` als target UI-werk betreft
|
||||||
|
4. Bedenk children:
|
||||||
|
- Voor `STORY`-target: 3-7 taken met titel, korte beschrijving, `implementation_plan` (verwijst naar relevante patterns + bestanden), priority
|
||||||
|
- Voor `PBI`-target: 2-5 stories met titel, beschrijving in user-story-format, acceptance_criteria, priority
|
||||||
|
5. Filter ontbrekende items: skip wat overlapt met `existing_*` (titel-match)
|
||||||
|
6. Voor elk: `create_task` of `create_story` via MCP
|
||||||
|
7. `update_job_status({ status: 'DONE', summary: 'Aangemaakt: 4 taken / 0 overgeslagen (titel-overlap)' })`
|
||||||
|
|
||||||
|
Bij failure: `update_job_status({ status: 'FAILED', error })` + toast voor user.
|
||||||
|
|
||||||
|
**Mens-rolverdeling:** twee Claude Code-sessies tegelijk draaien (één met `/implement-next-story` running, één met `/generate-plan` running). Beide claimen alleen hun eigen kind via `accept_kinds`. Dezelfde gebruiker-token of twee aparte (afhankelijk van hoe je workers wilt scheiden).
|
||||||
|
|
||||||
|
### Stap 8 — Tests
|
||||||
|
|
||||||
|
| Test | Locatie |
|
||||||
|
|---|---|
|
||||||
|
| `enqueuePlanningJobAction` — auth, demo, idempotency, scope | `__tests__/actions/claude-jobs-planning.test.ts` |
|
||||||
|
| Schema-mapper voor `kind` + `planning_target_*` | `__tests__/lib/claude-job-status.test.ts` |
|
||||||
|
| SSE-event format met `kind` | `__tests__/api/realtime-solo-planning.test.ts` |
|
||||||
|
| Status-pill rendering per status | `__tests__/components/shared/planning-job-pill.test.tsx` |
|
||||||
|
| Knop disabled-state in StoryDialog/PbiDialog bij actieve job | `__tests__/components/backlog/dialog-planning-button.test.tsx` |
|
||||||
|
|
||||||
|
MCP-tools testen in `scrum4me-mcp` repo (aparte PR).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical files
|
||||||
|
|
||||||
|
### Scrum4Me-repo
|
||||||
|
|
||||||
|
| File | Action | Reden |
|
||||||
|
|---|---|---|
|
||||||
|
| `prisma/schema.prisma` | MODIFY | `ClaudeJobKind`, `PlanningTargetType`, `ApiTokenKind` enums + nullable `task_id` + `planning_target_*` velden |
|
||||||
|
| `prisma/migrations/<date>_planning_job_kind/` | NEW | Migratie + check-constraint |
|
||||||
|
| `lib/claude-job-status.ts` | MODIFY | `kind` in API-shape |
|
||||||
|
| `lib/schemas/claude-job.ts` | NEW/MODIFY | Discriminated union op `kind` |
|
||||||
|
| `lib/schemas/planning-target.ts` | NEW | Target-validator |
|
||||||
|
| `actions/claude-jobs.ts` | MODIFY | `enqueuePlanningJobAction` toevoegen, idempotency uitbreiden |
|
||||||
|
| `app/api/realtime/solo/route.ts` | MODIFY | `kind` in payload, initial-state ook PLANNING-jobs |
|
||||||
|
| `stores/claude-jobs-store.ts` (of vergelijkbaar) | MODIFY | `kind`-filter helpers |
|
||||||
|
| `components/backlog/story-dialog.tsx` | MODIFY | "Genereer taken"-knop + status-pill in header |
|
||||||
|
| `components/backlog/pbi-dialog.tsx` | MODIFY | "Genereer stories"-knop + status-pill in header |
|
||||||
|
| `components/backlog/story-panel.tsx` | MODIFY | Status-pill op story-card |
|
||||||
|
| `components/backlog/pbi-list.tsx` | MODIFY | Status-pill op pbi-card |
|
||||||
|
| `components/shared/planning-job-pill.tsx` | NEW | Generic pill-component |
|
||||||
|
| `docs/patterns/claude-agent-roles.md` | NEW | Pattern-doc: één table, kind-enum, accept_kinds-arg, lokale agent-prompts |
|
||||||
|
| `docs/scrum4me-architecture.md` | MODIFY | Sectie "Claude Agents" uitbreiden — twee rollen, schema, queue, prompts |
|
||||||
|
| `docs/scrum4me-pbi-dialog.md` | MODIFY | Sectie "Speciale gedragingen → Planning-trigger" toevoegen |
|
||||||
|
| `docs/scrum4me-story-dialog.md` | MODIFY | Idem |
|
||||||
|
| `docs/scrum4me-task-dialog.md` | MODIFY | Vermelden dat tasks ook door planning-agent kunnen ontstaan |
|
||||||
|
| `__tests__/actions/claude-jobs-planning.test.ts` | NEW | |
|
||||||
|
| `__tests__/lib/claude-job-status.test.ts` | MODIFY | `kind`-mapping testen |
|
||||||
|
| `__tests__/api/realtime-solo-planning.test.ts` | NEW | |
|
||||||
|
| `__tests__/components/shared/planning-job-pill.test.tsx` | NEW | |
|
||||||
|
| `__tests__/components/backlog/dialog-planning-button.test.tsx` | NEW | |
|
||||||
|
|
||||||
|
### scrum4me-mcp repo (aparte PR, na Scrum4Me-merge)
|
||||||
|
|
||||||
|
| File | Action |
|
||||||
|
|---|---|
|
||||||
|
| `src/tools/wait_for_job.ts` | MODIFY — `accept_kinds`-arg + polymorf response |
|
||||||
|
| `src/tools/get_planning_context.ts` | NEW (optioneel, helper) |
|
||||||
|
| `src/types/job.ts` | MODIFY — kind + planning_target |
|
||||||
|
| `src/prompts/generate-plan.md` | NEW — Claude command-prompt |
|
||||||
|
| `vendor/scrum4me/` (submodule) | UPDATE — na Scrum4Me-merge |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Volgorde van uitvoering
|
||||||
|
|
||||||
|
1. **Schema + migratie + status-mapper** (Stap 1+2) — eigen commit, geen UI-impact
|
||||||
|
2. **Server action `enqueuePlanningJobAction` + tests** (Stap 3) — werkt headless via curl/test
|
||||||
|
3. **SSE-payload uitbreiden + claude-jobs-store** (Stap 4) — backend pipe klaar
|
||||||
|
4. **Status-pill component + tests** (Stap 5a) — losstaande primitive
|
||||||
|
5. **Trigger-knoppen in StoryDialog + PbiDialog** (Stap 5b) — UI-trigger werkt, agent nog niet
|
||||||
|
6. **Pause** — verifieer end-to-end met handmatig insert in `claude_jobs` (kind=PLANNING) of via mock-MCP-call
|
||||||
|
7. **MCP-PR in scrum4me-mcp repo** (Stap 6) — `wait_for_job` uitbreiden, types updaten
|
||||||
|
8. **Lokaal `/generate-plan`-command schrijven + testen** (Stap 7) — agent claimt, leest, schrijft
|
||||||
|
9. **End-to-end test** (Stap 8) — story → klik knop → agent rendert taken → SSE → live in TaskPanel
|
||||||
|
10. **Docs-PR** — pattern-doc `claude-agent-roles.md`, architecture-update, dialog-profielen aanvullen
|
||||||
|
|
||||||
|
Branch-naming: `feat/M15-planning-agent` (Scrum4Me) + `feat/planning-agent` (scrum4me-mcp).
|
||||||
|
|
||||||
|
Conform CLAUDE.md "branch-per-milestone": commits accumuleren lokaal, pushen pas na gebruikerstest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. `npm run lint && npm test && npm run build` groen
|
||||||
|
2. **Schema-migratie:** bestaande `claude_jobs`-rijen krijgen `kind=IMPLEMENTATION`; check-constraint blokkeert ongeldige combinaties
|
||||||
|
3. **Idempotency:** twee keer klikken op "Genereer taken" → tweede klik geeft toast "Plan-job al gestart", knop disabled
|
||||||
|
4. **Demo-block:** demo-user ziet knop disabled met DemoTooltip; server action returnt 403 als je 'm toch aanroept
|
||||||
|
5. **SSE live:** trigger planning-job → status-pill verschijnt op story-card binnen 1s zonder refresh
|
||||||
|
6. **End-to-end:** lokale `/generate-plan` agent claimt job, leest target via `wait_for_job`, leest 3-4 docs uit Scrum4Me-checkout, maakt 3-5 taken via `create_task`, status DONE → taken zichtbaar in TaskPanel zonder refresh
|
||||||
|
7. **Cancel-flow:** gebruiker kan vanuit dialog een running PLANNING-job cancellen → status-pill verdwijnt, agent ziet job-status `CANCELLED` bij volgende `update_job_status`
|
||||||
|
8. **Cross-kind isolation:** een implementation-agent met `accept_kinds=['IMPLEMENTATION']` (default) ziet PLANNING-jobs niet; idem omgekeerd
|
||||||
|
9. **Aanvullen-policy:** trigger op story die al 2 taken heeft — agent voegt alleen ontbrekende toe (logged in summary: "Aangemaakt: 3 / Overgeslagen: 2 (titel-overlap)")
|
||||||
|
10. **Documentatie:** `docs/patterns/claude-agent-roles.md` beschrijft beide agent-rollen, hun job-flow, en hoe je een derde agent zou toevoegen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open punten (na approval expliciet maken)
|
||||||
|
|
||||||
|
1. **Token-strategie:** krijgt elke agent-rol een eigen ApiToken (cleaner, twee credentials), of bestaat er één multi-kind-token per user? *Voorstel: aparte tokens — één per kind. Gebruiker beheert ze in Settings.*
|
||||||
|
2. **Concurrency in 1 worker:** mag `accept_kinds: ['IMPLEMENTATION', 'PLANNING']` (één worker pakt allebei)? *Voorstel: ja, technisch toegestaan, maar in v1 raad je af want context-pollution. Documenteren als "kan, maar gebruik gescheiden processen".*
|
||||||
|
3. **Doc-selectie:** hoe bepaalt de agent welke `docs/patterns/*.md` relevant zijn? *Voorstel: lees `docs/patterns/`-index op + match op keywords uit target-titel/-beschrijving. Geen embeddings in v1.*
|
||||||
|
4. **Hoeveel children per run?** *Voorstel: hard cap 8 in de prompt (anders gaat 'ie speculeren). Gebruiker kan opnieuw klikken voor meer.*
|
||||||
|
5. **Editable plan-text:** wanneer agent `implementation_plan` invult op een nieuwe task, kan de gebruiker die later via TaskDialog editen — dat werkt al, geen extra werk.
|
||||||
|
6. **Failure-recovery:** wat als agent halverwege crasht? Stale-cleanup >30min werkt al; partial-children blijven aangemaakt. *Voorstel: accepteer partial — gebruiker kan opnieuw triggeren, aanvullen-policy filtert duplicaten.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (v1 van deze feature)
|
||||||
|
|
||||||
|
- ❌ Diff-review-flow (vervangen-modus uit eerdere AskUserQuestion)
|
||||||
|
- ❌ Live streaming-output van agent (alleen status-events, geen tekst-stream)
|
||||||
|
- ❌ Meerdere parallele PLANNING-jobs op dezelfde target (idempotency blokkeert)
|
||||||
|
- ❌ Custom prompts per product (vaste prompt-template `/generate-plan`)
|
||||||
|
- ❌ Embeddings / vector-search over docs (agent leest plain files)
|
||||||
|
- ❌ Cross-user planning (agent werkt altijd binnen eigen product-scope)
|
||||||
|
- ❌ Auto-trigger (bv. "elke nieuwe lege story krijgt automatisch een plan-job") — handmatige trigger blijft regel
|
||||||
|
- ❌ Agent-tot-agent-orkestratie (planning-agent kan geen impl-agent triggeren) — gebruiker blijft in the loop
|
||||||
|
- ❌ Planning op product-niveau (PBI's genereren) — pas zinvol als product-spec-input bestaat
|
||||||
|
- ❌ Planning-agent UI in Solo Paneel (Solo blijft impl-only) — triggers zitten in backlog-dialogs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migratie-pad voor toekomstige derde agent (referentie)
|
||||||
|
|
||||||
|
Als er ooit een derde agent komt (bv. **review-agent** die een PR review't):
|
||||||
|
|
||||||
|
1. `ClaudeJobKind` enum uitbreiden met `REVIEW`
|
||||||
|
2. `ApiTokenKind` enum uitbreiden met `REVIEW`
|
||||||
|
3. `enqueueReviewJobAction` aanmaken (kopieert pattern van planning)
|
||||||
|
4. `wait_for_job` accepteert nieuwe `kind` automatisch via `accept_kinds`
|
||||||
|
5. Pattern-doc `claude-agent-roles.md` uitbreiden met de derde rol
|
||||||
|
|
||||||
|
Geen schema-revolutie nodig — `kind`-enum is het uitbreidingspunt.
|
||||||
120
docs/scrum4me-pbi-dialog.md
Normal file
120
docs/scrum4me-pbi-dialog.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# PbiDialog Profiel
|
||||||
|
|
||||||
|
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
|
||||||
|
> Dit document beschrijft alleen de PBI-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
|
||||||
|
|
||||||
|
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Velden
|
||||||
|
|
||||||
|
| Veld | Type | Mode | Validatie |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create (server kent dan zelf een code toe) |
|
||||||
|
| `title` | `string` (required) | beide | trim, 1-200 chars |
|
||||||
|
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (kan via `defaultPriority`-prop bij create) |
|
||||||
|
| `status` | `PbiStatusApi` enum | beide | enum, default `'ready'` |
|
||||||
|
| `description` | `string \| null` | beide | optional, max 2000 chars, plain textarea (geen markdown rendering binnen de dialog) |
|
||||||
|
|
||||||
|
`PbiStatusApi` enum (lowercase, mapped via `lib/task-status.ts`): zie `<PbiStatusSelect>` voor de waarden.
|
||||||
|
|
||||||
|
### Veld-specifiek gedrag
|
||||||
|
|
||||||
|
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
|
||||||
|
- **Prioriteit + Status** in één rij (`grid-cols-2`)
|
||||||
|
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive, géén segmented buttons in deze dialog)
|
||||||
|
- **Status** via `<PbiStatusSelect>` (PBI-specifieke wrapper rond gedeelde select)
|
||||||
|
- **Description** is `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint, géén char-counter (afwijking van generieke spec; rationale: PBI-descriptions zijn doorgaans kort en richtinggevend)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## URL- of state-pattern
|
||||||
|
|
||||||
|
- **Gekozen:** state-based (`state: PbiDialogState | null` prop, gerendeerd binnen `PbiList`)
|
||||||
|
- **Reden:** PBI-dialog leeft altijd binnen `PbiList` op de product-backlog-pagina; deep-linking is niet vereist en zou een tweede edit-flow toevoegen.
|
||||||
|
- **State-shape:**
|
||||||
|
```ts
|
||||||
|
type PbiDialogState =
|
||||||
|
| { mode: 'create'; productId: string; defaultPriority?: number }
|
||||||
|
| { mode: 'edit'; pbi: PbiDialogPbi; productId: string }
|
||||||
|
```
|
||||||
|
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `PbiList`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status-veld
|
||||||
|
|
||||||
|
- **Default bij create:** `'ready'` (PBI-default state)
|
||||||
|
- **Geen verberging in create-mode** — anders dan TaskDialog wordt status hier wél getoond bij create, omdat een PBI zonder expliciete status onhandig is voor backlog-grooming
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server actions
|
||||||
|
|
||||||
|
| Actie | Locatie | Form-binding | Revalidatie |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `createPbiAction` | `actions/pbis.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
|
||||||
|
| `updatePbiAction` | `actions/pbis.ts` | idem | idem |
|
||||||
|
| ~~`deletePbiAction`~~ | **(ontbreekt)** | n.v.t. | n.v.t. |
|
||||||
|
|
||||||
|
Beide acties moeten de drielaagse demo-policy volgen (zie § Bekende gaps).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Speciale gedragingen
|
||||||
|
|
||||||
|
### Form-state via `useActionState`
|
||||||
|
|
||||||
|
PbiDialog gebruikt het `useActionState` + `useFormStatus`-patroon (Server Actions / native React), niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2. Field-errors worden gemapt via een lokale `fieldError(field)`-helper die `result.error` als `Record<string, string[]>` interpreteert wanneer 'm geen string is.
|
||||||
|
|
||||||
|
### `key`-prop op `<form>`
|
||||||
|
|
||||||
|
Het `<form>`-element heeft `key={isEdit ? pbi!.id : 'create'}` — dit reset native form-state (defaultValues) wanneer de dialog tussen create en edit wisselt of wanneer een ander record bewerkt wordt.
|
||||||
|
|
||||||
|
### Hidden inputs voor server-binding
|
||||||
|
|
||||||
|
`priority` en `status` worden via `<input type="hidden">` doorgegeven aan de Server Action (de UI-controls zijn JS-state, niet directe form-fields).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
- **Create-trigger:** `+ PBI`-knop in `PanelNavBar` van `PbiList` → `setPbiDialogState({ mode: 'create', ... })`
|
||||||
|
- **Edit-trigger:** edit-icoon op een PBI-rij in `PbiList` → `setPbiDialogState({ mode: 'edit', pbi, ... })`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bekende gaps t.o.v. generieke spec
|
||||||
|
|
||||||
|
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
|
||||||
|
|
||||||
|
- ❌ **Geen `<DemoTooltip>`** rond submit-knop — laag 3 van de drielaagse demo-policy ontbreekt voor PBI-create/update. Dat betekent dat een demo-user de knop kan klikken; de server action blokkeert nog steeds (laag 2), maar de UX is suboptimaal.
|
||||||
|
- ❌ **Geen delete-knop / `deletePbiAction`** — alleen create + update. Of dat bewust is (PBI's worden nooit verwijderd, alleen status veranderd) of een gat, moet expliciet worden besloten en in dit profiel vastgelegd.
|
||||||
|
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
|
||||||
|
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
|
||||||
|
- ❌ **Geen char-counter / markdown-hint** op description — bewust weggelaten omdat PBI-descriptions kort zijn, maar verdient expliciete bevestiging.
|
||||||
|
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-md` i.p.v. de `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bewust NIET in v1
|
||||||
|
|
||||||
|
Specifiek voor PbiDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
|
||||||
|
|
||||||
|
- ❌ Inline aanmaken van child-stories binnen de PBI-dialog (gebeurt via StoryDialog vanuit `StoryPanel`)
|
||||||
|
- ❌ Bulk-status-update over meerdere PBI's
|
||||||
|
- ❌ PBI-templates / kopiëren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Referenties
|
||||||
|
|
||||||
|
- `components/backlog/pbi-dialog.tsx` — implementatie
|
||||||
|
- `actions/pbis.ts` — server actions
|
||||||
|
- `components/shared/priority-select.tsx` — gedeelde priority-control
|
||||||
|
- `components/shared/pbi-status-select.tsx` — PBI-status-select
|
||||||
|
- `lib/task-status.ts` — `PbiStatusApi`-mapper
|
||||||
|
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
|
||||||
|
- `docs/scrum4me-architecture.md` — datamodel `Pbi`
|
||||||
|
- `docs/scrum4me-styling.md` — MD3-tokens, status- en priority-kleuren
|
||||||
163
docs/scrum4me-story-dialog.md
Normal file
163
docs/scrum4me-story-dialog.md
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
# StoryDialog Profiel
|
||||||
|
|
||||||
|
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
|
||||||
|
> Dit document beschrijft alleen de Story-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
|
||||||
|
|
||||||
|
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Velden
|
||||||
|
|
||||||
|
| Veld | Type | Mode | Validatie |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create |
|
||||||
|
| `title` | `string` (required) | beide | trim, 1-200 chars |
|
||||||
|
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (overschrijfbaar via `defaultPriority`-prop bij create) |
|
||||||
|
| `description` | `string \| null` | beide | optional, plain textarea, placeholder `Als… wil ik… zodat…` (user-story-template) |
|
||||||
|
| `acceptance_criteria` | `string \| null` | beide | optional, plain textarea, placeholder `- Gegeven… Als… Dan…` (Gherkin-template) |
|
||||||
|
| `status` | `StoryStatus` enum | alleen edit | read-only badge in header, niet bewerkbaar in deze dialog |
|
||||||
|
|
||||||
|
`StoryStatus` enum: `OPEN | IN_SPRINT | DONE` (uppercase in DB).
|
||||||
|
|
||||||
|
### Veld-specifiek gedrag
|
||||||
|
|
||||||
|
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
|
||||||
|
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive)
|
||||||
|
- **Description** als `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint binnen de dialog (afwijking van generieke spec; rationale: stories zijn meestal één zin)
|
||||||
|
- **Acceptatiecriteria** idem — géén auto-grow, géén char-counter
|
||||||
|
- **Status** wordt **niet bewerkt** vanuit deze dialog. Status verandert via lijst-acties (sleep naar sprint = IN_SPRINT, taak-completion = DONE). Read-only badge in dialog-header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## URL- of state-pattern
|
||||||
|
|
||||||
|
- **Gekozen:** state-based (`state: StoryDialogState | null` prop, gerendeerd binnen `StoryPanel`)
|
||||||
|
- **Reden:** StoryDialog leeft binnen `StoryPanel` met live-store-data (geselecteerde PBI bepaalt zichtbare stories); deep-linking zou parallelle data-fetch-paden vereisen.
|
||||||
|
- **State-shape:**
|
||||||
|
```ts
|
||||||
|
type StoryDialogState =
|
||||||
|
| { mode: 'create'; pbiId: string; productId: string; defaultPriority?: number }
|
||||||
|
| { mode: 'edit'; story: Story; productId: string }
|
||||||
|
```
|
||||||
|
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `StoryPanel`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status-veld
|
||||||
|
|
||||||
|
- **Niet bewerkbaar in deze dialog** — alleen weergegeven als badge in de header (edit-mode)
|
||||||
|
- **Default bij create:** `OPEN` (server-default, niet expliciet gezet vanuit form)
|
||||||
|
- Status-overgangen lopen via:
|
||||||
|
- `OPEN → IN_SPRINT` — drag-and-drop naar een sprint of `sprint-id` zetten via story-actions
|
||||||
|
- `IN_SPRINT → DONE` — alle taken op `DONE` zetten triggert auto-promotion (zie story-status-logic in `actions/stories.ts`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server actions
|
||||||
|
|
||||||
|
| Actie | Locatie | Form-binding | Revalidatie |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `createStoryAction` | `actions/stories.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
|
||||||
|
| `updateStoryAction` | `actions/stories.ts` | idem | idem |
|
||||||
|
| `deleteStoryAction` | `actions/stories.ts` | aangeroepen vanuit `useTransition` (geen form) | server-side `revalidatePath` |
|
||||||
|
| `getStoryLogsAction` | `actions/stories.ts` | aangeroepen on-mount in edit-mode | n.v.t. (read-only) |
|
||||||
|
|
||||||
|
Alle write-acties zijn drielaags afgedekt (proxy-guard + server-action-check + DemoTooltip op submit-knop).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Speciale gedragingen
|
||||||
|
|
||||||
|
### Header-presentatie (afwijking van generieke spec)
|
||||||
|
|
||||||
|
In edit-mode toont de dialog-header **drie elementen** boven op de standaard titel:
|
||||||
|
|
||||||
|
1. Story-titel als dialog-title (groot)
|
||||||
|
2. Story-code als monospace-badge rechtsboven (klein)
|
||||||
|
3. Twee badges direct onder de titel: priority-badge (kleur via `PRIORITY_COLORS`) en status-badge (kleur via `STATUS_COLORS`)
|
||||||
|
|
||||||
|
Generieke spec gaat uit van een sobere header met alleen `headline-small` titel + optioneel een `created_at`-meta-string. StoryDialog wijkt hier bewust van af omdat status + priority belangrijke context zijn voor de gebruiker bij het openen van een story (vaak wisselt iemand vlot tussen meerdere stories).
|
||||||
|
|
||||||
|
### Demo-modus = read-only weergave
|
||||||
|
|
||||||
|
Wanneer `isDemo === true` én `isEdit === true`, wordt het form **vervangen** door een read-only weergave:
|
||||||
|
|
||||||
|
- `description` via gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`)
|
||||||
|
- `acceptance_criteria` als plain whitespace-pre-line tekst
|
||||||
|
|
||||||
|
In create-mode is er voor demo-users niets te tonen — de dialog wordt alsnog geopend maar de submit-knop is `disabled` met `<DemoTooltip>`.
|
||||||
|
|
||||||
|
> Dit "read-only-fallback"-patroon is uniek voor StoryDialog tot nu toe. Het zou geadopteerd kunnen worden door andere edit-dialogs zodra demo-flow-vereisten dat rechtvaardigen.
|
||||||
|
|
||||||
|
### Activity-log (StoryLog) inline
|
||||||
|
|
||||||
|
In edit-mode wordt onder het form een `<StoryLog>`-paneel getoond met de chronologische logs van deze story (commit-hashes, status-transitions, etc.). Logs worden lazy-fetched via `getStoryLogsAction(story.id)` zodra de dialog opent.
|
||||||
|
|
||||||
|
Dit is een **read-only side-panel** en valt binnen de uitzondering die de generieke spec § 13 maakt voor `<StoryLog>`-style activity-rendering.
|
||||||
|
|
||||||
|
### Delete-flow (afwijking van generieke spec)
|
||||||
|
|
||||||
|
Generieke spec § 10.4 vereist een **`AlertDialog`** voor delete-confirmatie. StoryDialog gebruikt in plaats daarvan een **inline-confirm** in dezelfde footer-rij:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ Weet je het zeker? Taken worden ook verwijderd. [Verwijderen] [Annuleren] ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Een `AlertDialog` zou een tweede modale laag toevoegen die in deze context onhandig voelt (de dialog zelf is al een interruptive overlay). De inline-confirm is een **bewuste afwijking** van de generieke spec.
|
||||||
|
|
||||||
|
### Form-state via `useActionState`
|
||||||
|
|
||||||
|
Net als PbiDialog gebruikt StoryDialog `useActionState` + `useFormStatus`, niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2.
|
||||||
|
|
||||||
|
### `key`-prop op `<form>`
|
||||||
|
|
||||||
|
Het `<form>` heeft `key={isEdit ? story!.id : 'create'}` — reset native form-state bij record-wissel of mode-switch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
- **Create-trigger:** `+ Story`-knop in `PanelNavBar` van `StoryPanel` → `setStoryDialogState({ mode: 'create', pbiId, productId, defaultPriority: 2 })`
|
||||||
|
- **Edit-trigger:** edit-icoon op een story-card in `StoryPanel` → `setStoryDialogState({ mode: 'edit', story, productId })`
|
||||||
|
- **Empty-state-trigger:** `Maak je eerste story aan`-knop in `EmptyPanel` (zelfde state als create-trigger)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bekende gaps t.o.v. generieke spec
|
||||||
|
|
||||||
|
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
|
||||||
|
|
||||||
|
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
|
||||||
|
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
|
||||||
|
- ❌ **Geen char-counter / markdown-hint** op description / acceptance_criteria — bewust weggelaten, maar verdient expliciete bevestiging als design-keuze.
|
||||||
|
- ⚠️ **Inline-delete-confirm** in plaats van AlertDialog (zie § Speciale gedragingen). Bewuste afwijking; de generieke spec mag deze variant expliciet toestaan, of dit profile moet als precedent gelden voor toekomstige dialogen.
|
||||||
|
- ⚠️ **Header-layout** met meerdere badges wijkt af van de sobere header in § 4. Bewuste afwijking — context-zwaar bij story-wisselen.
|
||||||
|
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-lg` met eigen `max-h-[90vh]` + `flex flex-col` i.p.v. de exacte `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bewust NIET in v1
|
||||||
|
|
||||||
|
Specifiek voor StoryDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
|
||||||
|
|
||||||
|
- ❌ Status bewerken vanuit de dialog (gebeurt via lijst-acties / drag-and-drop / auto-promotion)
|
||||||
|
- ❌ Inline aanmaken van child-tasks (gebeurt via TaskDialog vanuit `TaskPanel`)
|
||||||
|
- ❌ Bulk-edit over meerdere stories
|
||||||
|
- ❌ Story-templates
|
||||||
|
- ❌ Linking aan externe issues (GitHub / Linear) — staat op v1.1+ roadmap
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Referenties
|
||||||
|
|
||||||
|
- `components/backlog/story-dialog.tsx` — implementatie
|
||||||
|
- `actions/stories.ts` — server actions (incl. `getStoryLogsAction`)
|
||||||
|
- `components/shared/priority-select.tsx` — gedeelde priority-control
|
||||||
|
- `components/shared/story-log.tsx` — activity-log paneel
|
||||||
|
- `components/shared/demo-tooltip.tsx` — demo-policy laag 3
|
||||||
|
- `components/markdown.tsx` — gedeelde markdown-wrapper
|
||||||
|
- `lib/task-status.ts` — status-enum-mapper
|
||||||
|
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
|
||||||
|
- `docs/scrum4me-architecture.md` — datamodel `Story`
|
||||||
|
- `docs/scrum4me-styling.md` — MD3-tokens, status- en priority-kleuren
|
||||||
|
|
@ -1,506 +1,127 @@
|
||||||
# Scrum4Me — TaskDialog Spec
|
# TaskDialog Profiel
|
||||||
|
|
||||||
> Volledige design-spec voor de add/update task dialog van de inspannings monitor app.
|
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
|
||||||
> Resultaat van een grill-me sessie (15 vragen, alle beslissingen vastgelegd).
|
> Dit document beschrijft alleen de Task-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
|
||||||
|
|
||||||
---
|
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
|
||||||
|
|
||||||
## 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".
|
|
||||||
>
|
|
||||||
> ```tsx
|
|
||||||
> // ✅ 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:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<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
|
## Velden
|
||||||
|
|
||||||
In volgorde van boven naar beneden:
|
| Veld | Type | Mode | Validatie |
|
||||||
|
|
||||||
| Veld | Control | Mode | Validatie |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `title` | `Input` (single-line) | beide | required, trim, 1-120 chars |
|
| `title` | `string` (required) | beide | trim, 1-120 chars |
|
||||||
| `description` | `Textarea` (auto-grow, 3-6 regels) | beide | optional, max 2.000 chars, markdown |
|
| `description` | `string \| null` | beide | optional, max 2.000 chars, markdown |
|
||||||
| `implementation_plan` | `Textarea` (auto-grow, 5-12 regels) | beide | optional, max 10.000 chars, markdown |
|
| `implementation_plan` | `string \| null` | beide | optional, max 10.000 chars, markdown |
|
||||||
| `priority` | Segmented buttons (P1/P2/P3/P4) | beide | int 1-4, default 3 |
|
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 3 |
|
||||||
| `status` | `Select` met gekleurde dot | alleen edit | enum, default TO_DO |
|
| `status` | `TaskStatus` enum | alleen edit (default `TO_DO` op create, niet getoond) | enum |
|
||||||
|
| `created_at` | `Date` | alleen edit | read-only metadata in header |
|
||||||
|
|
||||||
Verberg `status` in create-mode (default = TO_DO is genoeg).
|
`TaskStatus` enum: `TO_DO | IN_PROGRESS | REVIEW | DONE`.
|
||||||
|
|
||||||
### Auto-grow textareas
|
### Veld-specifiek gedrag
|
||||||
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
|
- **Auto-grow textareas** (`description`, `implementation_plan`) via `react-textarea-autosize`. Max 6 regels (description) / 12 regels (implementation_plan), daarna `overflow-y-auto`.
|
||||||
Alleen tonen vanaf 75% van de limiet. Klein, rechtsonder in het veld, `muted-foreground` kleur. Bv. `1547 / 2000`.
|
- **Karakter-counter** vanaf 75% van de limiet, klein, rechtsonder, `text-muted-foreground`. Bv. `1547 / 2000`.
|
||||||
|
- **Markdown-hint** onder elk textarea: `Markdown ondersteund (lijstjes, **vet**, \`code\`)`.
|
||||||
### Markdown hint
|
- **Priority** als segmented buttons via `<PrioritySelect>` / `<PrioritySegmented>`. Default P3 (Medium).
|
||||||
Onder elk textarea: `Markdown ondersteund (lijstjes, **vet**, \`code\`)` — klein, muted.
|
- **Status** met gekleurde dot:
|
||||||
|
- `TO_DO` — grijs
|
||||||
### Priority segmented buttons
|
- `IN_PROGRESS` — `status-in-progress` (blauw)
|
||||||
```
|
- `REVIEW` — paars
|
||||||
[ P1 Critical ] [ P2 High ] [ P3 Medium ] [ P4 Low ]
|
- `DONE` — `status-done` (groen)
|
||||||
error tertiary primary outline
|
- **`created_at` als header-metadata** in edit-mode, naast de titel: `Aangemaakt: 23 apr 2026`. Klein, `muted-foreground`, géén form-veld.
|
||||||
```
|
|
||||||
- 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
|
## URL- of state-pattern
|
||||||
|
|
||||||
- **Gedeeld zod-schema** in `lib/schemas/task.ts`, geïmporteerd door zowel form als server action
|
- **Gekozen:** URL-based (`searchParams`)
|
||||||
- react-hook-form mode: `onTouched` (eerste validatie bij blur, daarna onChange)
|
- **Reden:** TaskDialog wordt geopend vanuit twee context-pagina's (sprint-detail en product-backlog) en moet deep-linkable zijn voor share/refresh-scenario's. Suspense + skeleton voor edit-mode loading is gewenst.
|
||||||
- Errors onder het veld, in error-color, met label en outline van het veld in dezelfde kleur
|
- **Routes:**
|
||||||
- Geen toasts voor field-level errors
|
```
|
||||||
- Submit-button blijft enabled bij errors — klik scrollt naar eerste error-veld + focus
|
/sprint/<sprintId>?newTask=1 → create
|
||||||
|
/sprint/<sprintId>?editTask=<taskId> → edit
|
||||||
```ts
|
/products/<productId>/backlog?newTask=1 → create
|
||||||
// lib/schemas/task.ts (richtlijn)
|
/products/<productId>/backlog?editTask=<taskId> → edit
|
||||||
export const taskSchema = z.object({
|
```
|
||||||
title: z.string().trim().min(1, "Verplicht").max(120),
|
- **Sluiten:** `router.push(<base-route>)` zonder query-params.
|
||||||
description: z.string().max(2000).optional(),
|
- **Server-side fetch in edit-mode:** server component fetcht de taak vóór render mét `productAccessFilter(userId)`. Bestaat de taak niet of valt 'm buiten scope → toast + redirect naar de context-route.
|
||||||
implementation_plan: z.string().max(10000).optional(),
|
- Optioneel: `nuqs` als de query-state-handling te omslachtig wordt — pas introduceren als losse refactor-task, niet inline.
|
||||||
priority: z.number().int().min(1).max(4),
|
|
||||||
status: z.nativeEnum(TaskStatus).optional(), // alleen in edit
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Submission
|
## Status-veld
|
||||||
|
|
||||||
### Auth-scoping (verplicht)
|
Verberg `status` in **create-mode** (default = `TO_DO` is genoeg). Toon alleen in edit-mode als `<Select>` met gekleurde dot per optie.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// 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:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
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
|
## Server actions
|
||||||
|
|
||||||
### Sluiten met dirty state
|
| Actie | Locatie | Context-arg | Revalidatie |
|
||||||
- Form niet aangeraakt → Esc / backdrop-klik / Cancel sluiten direct
|
|---|---|---|---|
|
||||||
- Form `isDirty` → Esc / backdrop-klik / Cancel triggeren `AlertDialog`: *"Wijzigingen niet opgeslagen — weggooien?"*
|
| `saveTask` | `app/actions/tasks.ts` | `{ sprintId?: string; productId?: string }` | `revalidatePath('/sprint/<sprintId>')` óf `revalidatePath('/products/<productId>/backlog')` afhankelijk van context |
|
||||||
|
| `deleteTask` | `app/actions/tasks.ts` | idem | idem |
|
||||||
|
|
||||||
### Keyboard shortcuts
|
Beide acties volgen de drielaagse demo-policy + auth-scoping uit `docs/patterns/dialog.md` § 6–7.
|
||||||
- **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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Footer
|
## Speciale gedragingen
|
||||||
|
|
||||||
### Edit-mode
|
### Triggers (bestaande UI vervangen)
|
||||||
```
|
|
||||||
[ Verwijderen ] [ Annuleren ] [ Opslaan ]
|
|
||||||
tonal (error-container) text filled (primary)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create-mode
|
Deze TaskDialog is de **enige** create/edit-flow voor taken in beide contexten (sprint én backlog). Bestaande inline-edit-paden in `components/sprint/task-list.tsx` en het backlog-equivalent worden vervangen, niet ernaast geplaatst.
|
||||||
```
|
|
||||||
[ Annuleren ] [ Aanmaken ]
|
|
||||||
text filled (primary)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Delete-flow
|
- **Create-trigger:** filled button `+ Nieuwe taak` in tasklist-header → zet `?newTask=1` op huidige route
|
||||||
- Klik op "Verwijderen" → `AlertDialog`: *"Weet je zeker? Dit kan niet ongedaan worden."*
|
- **Edit-trigger:** klik op de hele rij in de tasklist (geen apart edit-icoon) → zet `?editTask=<id>` op huidige route
|
||||||
- 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"
|
- **Loading edit-mode:** Suspense met minimale skeleton (3 grijze balken), `200ms`-delay zodat snelle fetches geen flicker tonen
|
||||||
- Geen undo in v1
|
|
||||||
|
|
||||||
---
|
### Markdown-rendering elders
|
||||||
|
|
||||||
## Triggers (hoe komt de user erbij?)
|
`description` en `implementation_plan` worden buiten de dialog (taakdetail, hover-card) gerenderd via de gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`). Niet in de dialog zelf.
|
||||||
|
|
||||||
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.):
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
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 | IN_SPRINT | DONE`) en `PbiStatus` (`OPEN | IN_SPRINT | DONE` + `BLOCKED`) hebben andere enums en horen bij eigen select-componenten. |
|
|
||||||
|
|
||||||
### 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)
|
## Implementatie-volgorde (suggestie)
|
||||||
|
|
||||||
1. Dependencies toevoegen aan `package.json` (zie "Dependency-impact"); commit als `chore(ST-XXX): add deps for task dialog`
|
Hergebruik dit als checklist bij het bouwen of refactoren van TaskDialog:
|
||||||
2. zod-schema in `lib/schemas/task.ts`
|
|
||||||
3. `productAccessFilter` helper checken/uitbreiden in `lib/auth/`
|
1. Dependencies in `package.json` (zie `docs/patterns/dialog.md` § 2)
|
||||||
4. Server actions (`saveTask`, `deleteTask`) met **auth-scoping én demo-check** (laag 2) — testen via thunk
|
2. zod-schema in `lib/schemas/task.ts` — gedeeld door form en action
|
||||||
5. `proxy.ts` middleware-guard voor demo-routes (laag 1) — alleen als nog niet aanwezig voor deze routes
|
3. `productAccessFilter`-helper checken in `lib/auth/`
|
||||||
6. Eventueel ontbrekende MD3-tokens aanvullen in `app/styles/theme.css` (geen dynamic color in v1)
|
4. `saveTask` / `deleteTask` in `app/actions/tasks.ts` met auth-scoping + demo-check (laag 2)
|
||||||
7. `<DemoTooltip>`-wrapper component (laag 3)
|
5. `proxy.ts`-guard voor demo-write-routes (laag 1) — alleen als nog niet aanwezig
|
||||||
8. TaskDialog — create-mode eerst (minder edge cases), bovenop bestaande `components/ui/dialog.tsx`-wrapper
|
6. Eventueel ontbrekende MD3-tokens in `app/styles/theme.css` aanvullen
|
||||||
9. Edit-mode toevoegen (status field, delete-knop, `created_at`-metadata)
|
7. `<DemoTooltip>` rond submit/delete-knoppen (laag 3)
|
||||||
10. URL-state via native `searchParams` binnen sprint en backlog routes (geen `nuqs` in v1)
|
8. TaskDialog — create-mode eerst (minder edge cases)
|
||||||
11. **Bestaande task-row / tasklist-trigger refactoren** — `components/sprint/task-list.tsx` (en backlog-equivalent) klikbaar maken zodat ze de dialog openen via query-param; oude inline-edit-paden verwijderen
|
9. Edit-mode toevoegen (status, delete, `created_at`-metadata)
|
||||||
12. Suspense + skeleton voor edit-mode loading + scope-check op fetch
|
10. URL-state via native `searchParams` op beide context-pagina's
|
||||||
13. Dirty-check + AlertDialog
|
11. Bestaande task-row trigger refactoren (klikbaar maken naar dialog)
|
||||||
|
12. Suspense + skeleton voor edit-mode + scope-check op fetch
|
||||||
|
13. Dirty-close-guard
|
||||||
14. Keyboard shortcuts (Cmd+Enter)
|
14. Keyboard shortcuts (Cmd+Enter)
|
||||||
15. Markdown rendering elders (out-of-scope voor dialog zelf, maar related)
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bewust NIET in v1
|
||||||
|
|
||||||
|
Specifiek voor TaskDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
|
||||||
|
|
||||||
|
- ❌ Sub-tasks / parent-child relaties tussen taken
|
||||||
|
- ❌ Tags / labels / categorieën op taken
|
||||||
|
- ❌ Due dates / reminders per taak
|
||||||
|
- ❌ Time tracking (uren-registratie) — wel relevant voor inspannings-monitor, eigen feature
|
||||||
|
- ❌ Sharing / collaboration per taak
|
||||||
|
- ❌ Templates voor terugkerende taken
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Referenties
|
||||||
|
|
||||||
|
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth voor alles wat hier niet beschreven is)
|
||||||
|
- `docs/scrum4me-architecture.md` — datamodel `Task`
|
||||||
|
- `docs/scrum4me-styling.md` — MD3-tokens, status- en priority-kleuren
|
||||||
|
- `lib/task-status.ts` — enum-mapper DB ↔ API
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue