docs/specs/dialogs/idea.md: - Velden-table with bron-zod links - URL/state-pattern: dedicated route /ideas/[id] (afwijking van generieke modal-spec — rationale documented) - 4-tab layout spec - Full state-machine table with transition triggers + server actions - Server-action catalog with preconditions + foutcodes - 3-layer demo-policy (proxy + isDemo-guard + DemoTooltip), incl. wat demo WEL mag (download-md is read-only) - Special behaviors: Cmd/Ctrl+S, localStorage draft (lazy seed), useMemo-derived validation, status-badge tokens, connectedWorkers via solo-store - Realtime routing notes - Test-fixture inventory (90+ cases across 7 test files) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.2 KiB
| title | status | audience | language | last_updated | ||
|---|---|---|---|---|---|---|
| IdeaDialog Profiel | active |
|
nl | 2026-05-04 |
IdeaDialog / IdeaDetailLayout Profiel
Volgt
docs/patterns/dialog.md(de generieke spec voor élke entity-dialog in Scrum4Me). Dit document beschrijft alleen de Idea-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming) 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 | Bron-zod |
|---|---|---|---|---|
title |
string (required) |
beide | trim, 1-200 chars | ideaCreateSchema.title |
description |
string | null |
beide | optional, max 4000 chars, plain textarea | ideaCreateSchema.description |
product_id |
string | null |
beide | optional cuid; vereist voordat Grill/Make Plan kan starten (M12 grill-keuze 3) | ideaCreateSchema.product_id |
code |
string (auto) |
read-only | IDEA-NNN, server-generated via nextIdeaCode(userId) op User.idea_code_counter |
n.v.t. |
status |
IdeaStatus enum |
read-only | door server gezet via state-machine | lib/idea-status.ts canTransition |
grill_md |
string | null |
edit-tab | bewerkbaar in GRILLED | PLAN_READY |
n.v.t. |
plan_md |
string | null |
edit-tab | bewerkbaar in PLAN_READY + yaml-frontmatter must parse |
ideaPlanMdFrontmatterSchema |
archived |
boolean |
read-only | via archive-actie | n.v.t. |
pbi_id |
string | null |
read-only | gezet door materializeIdeaPlanAction, SetNull als PBI verwijderd |
n.v.t. |
URL- of state-pattern
Afwijking van generieke spec: Idee gebruikt een dedicated route /ideas/[id] ipv een modal-dialog. Reden: het detail-scherm is rijker dan een modal kan dragen (4 tabs incl. md-editor + timeline) en de planningsgeschiedenis is een leesbaar artifact dat verdiend om bookmarkable te zijn.
- Lijst-create: state-based inline form bovenaan
/ideaslijst (IdeaList.showCreate). - Detail / edit: route
/ideas/[id]met tab-switcher via query-param (?tab=idee|grill|plan|timeline). - Geen modal: dus geen
Cmd/Ctrl+Enter-submit op de detail-form (alleen op md-editor);Escdoet niets in het detail-scherm.
Tabs (alleen op detail-route)
| Tab | Content | Editable in |
|---|---|---|
idee |
inline form (title, description, product_id) | DRAFT | GRILL_FAILED | GRILLED | PLAN_FAILED | PLAN_READY |
grill |
grill_md markdown render + Bewerk-knop |
GRILLED | PLAN_READY |
plan |
plan_md markdown render + Bewerk-knop |
PLAN_READY |
timeline |
UNION van IdeaLog + ClaudeQuestion chronologisch |
n.v.t. (read-only) |
isIdeaEditable, isGrillMdEditable en isPlanMdEditable helpers in lib/idea-status.ts bepalen de exacte regels.
Status-machine
DRAFT ──Grill──▶ GRILLING ─done──▶ GRILLED ──Make Plan──▶ PLANNING ─done──▶ PLAN_READY ──Materialiseer──▶ PLANNED
│ fail │ fail ▲ │
▼ ▼ │ │
GRILL_FAILED PLAN_FAILED └─── re-grill / re-plan (append-context) │
│
PLANNED ◀── (PBI verwijderd: pbi_id=null, status blijft PLANNED tot Re-link) ────────────────────────────────┘
| Van | Naar | Trigger | Server-action |
|---|---|---|---|
DRAFT |
GRILLING |
"Grill" knop | startGrillJobAction |
GRILLING |
GRILLED |
worker → update_idea_grill_md |
(MCP) |
GRILLING |
GRILL_FAILED |
worker → update_job_status('failed') |
(MCP) |
GRILLED / PLAN_FAILED / PLAN_READY |
GRILLING |
"Grill" knop (re-grill) | startGrillJobAction |
GRILLED / PLAN_FAILED / PLAN_READY |
PLANNING |
"Plan" knop | startMakePlanJobAction |
PLANNING |
PLAN_READY |
worker → update_idea_plan_md (parser ok) |
(MCP) |
PLANNING |
PLAN_FAILED |
worker → update_job_status('failed') of parse-fail |
(MCP) |
PLAN_READY |
PLANNED |
"Maak PBI" knop | materializeIdeaPlanAction |
PLANNED (pbi_id=null) |
PLAN_READY |
"Plan opnieuw beschikbaar maken" knop | relinkIdeaPlanAction |
| any | * archived |
Archive-knop | archiveIdeaAction |
Server actions
actions/ideas.ts:
| Actie | Precondition | Effect |
|---|---|---|
createIdeaAction(input) |
auth + niet-demo | nieuwe DRAFT-idea + auto-code |
updateIdeaAction(id, input) |
isIdeaEditable(status) |
update title/description/product_id |
archiveIdeaAction(id) / unarchiveIdeaAction(id) |
scoped on user_id | flip archived |
deleteIdeaAction(id) |
pbi_id === null |
hard delete (cascades naar IdeaLog) |
updateGrillMdAction(id, md) |
isGrillMdEditable(status) |
update + IdeaLog{NOTE} |
updatePlanMdAction(id, md) |
isPlanMdEditable(status) + parsePlanMd.ok |
update + IdeaLog{NOTE} |
startGrillJobAction(id) |
product+repo + worker actief + status in GRILL_TRIGGERABLE_FROM |
enqueue ClaudeJob{kind:IDEA_GRILL} |
startMakePlanJobAction(id) |
idem + status in MAKE_PLAN_TRIGGERABLE_FROM |
enqueue ClaudeJob{kind:IDEA_MAKE_PLAN} |
cancelIdeaJobAction(id) |
actieve job aanwezig | job→CANCELLED + status revert |
materializeIdeaPlanAction(id) |
status===PLAN_READY + plan_md parseable |
atomic create PBI + stories + tasks; idea→PLANNED |
relinkIdeaPlanAction(id) |
status===PLANNED && pbi_id===null |
status→PLAN_READY |
downloadIdeaMdAction(id, kind) |
scope (demo OK, read-only) | return md-string |
promoteTodoToIdeaAction(todoId) (in actions/todos.ts) |
todo niet archived + niet-demo | DRAFT-idea + Todo→archived |
Foutcodes: 400 = JSON parse, 401 = auth, 403 = demo, 404 = scope/not-found, 409 = idempotency/race, 422 = validatie/status-mismatch, 429 = rate-limit.
Demo-policy (3-laag)
| Laag | Wat | Waar |
|---|---|---|
| 1 | proxy.ts blokt POST/PATCH/DELETE /api/ideas* |
proxy.ts catch-all rule |
| 2 | session.isDemo guard in elke muteer-actie |
actions/ideas.ts |
| 3 | <DemoTooltip show={isDemo}> rondom muteer-knoppen |
idea-row-actions.tsx, idea-list.tsx, idea-detail-layout.tsx, download-md-button.tsx (NIET — read-only mag) |
Demo-user MAG: lijst zien, idee zien, navigeren tussen tabs, downloaden van md. Demo-user MAG NIET: aanmaken, bewerken, archiveren, Grill, Plan, Materialiseer, Re-link, Promote-from-Todo.
Special behaviors
IdeaMdEditor
- Cmd/Ctrl+S triggert save (alleen in editor, niet in detail-form).
- localStorage draft per
(idea_id, kind): lazy read-on-mount viauseState(() => readSeed(...))om setState-in-effect te vermijden. Drift met server → toast info bij restore. - Live yaml-validate voor plan-kind:
useMemo(() => parsePlanMd(value))→ derived state, geen useEffect. - Submit-errors los van validation-errors in state — server-side details overschrijven client-side validate als die er zijn.
IdeaPbiLinkCard
- Drie states: PLANNED+pbi (groene link), PLANNED+pbi-null (oranje banner met Re-link knop), niet-PLANNED (return null).
Status badges
- Status-tokens via
lib/idea-status-colors.ts→getIdeaStatusBadge(status)→{ label, classes, pulse? }. GRILLINGenPLANNING→animate-pulseom "actief" te signaleren.
Connected workers
IdeaRowActionsleestuseSoloStore(s => s.connectedWorkers)(M12 grill-keuze 16 — geen lift naar gedeelde store voor v1).- Zonder worker: Grill / Make Plan disabled met tooltip "Geen Claude-worker actief". Materialiseer is server-side synchroon en heeft géén worker nodig.
Realtime
SSE-stream /api/realtime/notifications levert idea-events (M12 T-502). Routing in lib/realtime/use-notifications-realtime.ts:
claude_job_*payloads metkind=IDEA_*→useIdeaStore.handleIdeaJobEvententity:'question'payloads metidea_idset →useIdeaStore.handleIdeaQuestionEvent- Story-questions blijven in
useNotificationsStore
useIdeaStore houdt optimistic state: jobByIdea, ideaStatuses, openQuestionsByIdea. Voor de detail-pagina is de server-state na router.refresh() source-of-truth — de store is een UI-cache.
Test-fixtures
__tests__/actions/ideas-crud.test.ts(39 cases) — alle CRUD + job-trigger + materialize + relink paden__tests__/api/ideas.test.ts(13 cases) — REST-laag__tests__/stores/idea-store.test.ts(7 cases) — Zustand event-handling__tests__/lib/idea-status.test.ts(15+ cases) — status mappers + transition guards__tests__/lib/idea-schemas.test.ts(16+ cases) — zod-validatie__tests__/lib/idea-plan-parser.test.ts(6 cases) — yaml-frontmatter__tests__/proxy/demo-guard.test.ts(9 cases) — incl. 3 idea-cases
Geen Playwright/MSW E2E voor v1 — handmatig E2E-script staat in docs/plans/M12-ideas.md "Verificatie".