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