Story 4 van PBI "Alle dialogen conform docs/patterns/dialog.md". - lib/schemas/story.ts — gedeeld zod-schema - actions/stories.ts — code+fieldErrors voor 422; code: 403 voor auth/demo - StoryDialog adopt useDirtyCloseGuard, useDialogSubmitShortcut, entityDialog* layout-classes - Inline delete-confirm vervangen door AlertDialog (§10.4) - docs/specs/dialogs/story.md — gaps weggewerkt; alleen bewuste afwijkingen blijven (header met badges, geen char-counter) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
8 KiB
Markdown
163 lines
8 KiB
Markdown
---
|
|
title: "StoryDialog Profiel"
|
|
status: active
|
|
audience: [ai-agent, contributor]
|
|
language: nl
|
|
last_updated: 2026-05-04
|
|
---
|
|
|
|
# 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
|
|
|
|
Volgt generieke spec § 10.4: klik op "Verwijderen" opent een `AlertDialog` ("Story verwijderen — bijbehorende taken worden ook verwijderd"). Bevestigen roept `deleteStoryAction` aan.
|
|
|
|
### Form-state via `useActionState`
|
|
|
|
Net als PbiDialog gebruikt StoryDialog `useActionState`, niet `react-hook-form`. Pending-state komt uit de derde return-waarde (`useActionState[2]`). Dit is een toegestaan alternatief volgens de generieke spec § 2.
|
|
|
|
### Dirty-tracking handmatig
|
|
|
|
Geen `react-hook-form`, dus `dirty` wordt op `true` gezet bij de eerste `onChange` op het form en bij wijzigingen van de hidden-state (`priority`). De `useDirtyCloseGuard` hook gebruikt deze boolean om Esc/Cancel/backdrop te beschermen.
|
|
|
|
### `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)
|
|
|
|
---
|
|
|
|
## Bewuste afwijkingen van generieke spec
|
|
|
|
- ⚠️ **Header-layout** met meerdere badges wijkt af van de sobere header in § 4. Bewuste keuze — story-context (priority + status) wil je direct zichtbaar bij record-wisselen.
|
|
- ❌ **Geen char-counter / markdown-hint** op description / acceptance_criteria — bewust weggelaten omdat stories meestal één zin lang zijn.
|
|
|
|
---
|
|
|
|
## 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/architecture.md` — datamodel `Story`
|
|
- `docs/design/styling.md` — MD3-tokens, status- en priority-kleuren
|