6.9 KiB
Plan — Expliciete schermstaat + draft-zichtbaarheid op de Product Backlog page
Status: goedgekeurd 2026-05-15 · gematerialiseerd via Scrum4Me-MCP. PBI-91 · Story ST-1369 (OPEN — in de productbacklog, geen sprint) · Taken T-1033 t/m T-1037 (uitvoervolgorde = sort_order 1–5).
Vervolg op PBI-88 ("Product Backlog page workflow & states", PR #208) — implementeert 3 van de 4 niet-bindende aanbevelingen uit
docs/architecture/product-backlog-workflow.md. G3 uitgesteld.
Context
PBI-88 leverde een as-is/to-be analyse op: docs/architecture/product-backlog-workflow.md. Dat doc sluit af met vier niet-bindende aanbevelingen (G1, G3, G5, G6) als "input voor latere PBI's".
Deze PBI implementeert er drie van. G3 (expliciete ERROR-schermstaat) wordt uitgesteld — het doc zegt zelf "alleen oppakken als falende commits of SSE-verlies een echt UX-probleem blijken", en het is meer een ontwerpkeuze dan een heldere implementatie.
De directe aanleiding voor PBI-88 was een bug: de concept-sprint (pendingSprintDraft) is niet zichtbaar op de SprintSwitcher-trigger-knop — alleen in de (disabled) dropdown. Dat is G5 en wordt hier opgelost.
Scope:
- G1 —
deriveScreenState(): één pure functie die de vandaag verspreide schermstaat-afleiding consolideert. - G5 — draft-status zichtbaar op de SprintSwitcher-trigger (de oorspronkelijke bug).
- G6 —
NewSprintTriggerachter eenisActiveProduct-gate. - G3 — uitgesteld, vastgelegd als follow-up.
Aanpak
Nieuwe bestanden
stores/product-workspace/screen-state.ts — pure module, géén React, spiegelt selectors.ts:
export type ScreenState =
| { kind: 'NO_SPRINT' }
| { kind: 'DRAFT' }
| { kind: 'ACTIVE'; building: boolean }
| { kind: 'EDITING'; building: boolean }
export interface ScreenStateInput {
activeSprintItem: { id: string } | null // SSR-prop uit page.tsx
buildingSprintIds: string[] // SSR-prop uit page.tsx
hasPendingDraft: boolean // user-settings store
pendingAdds: string[] // product-workspace store
pendingRemoves: string[] // product-workspace store
}
export function deriveScreenState(i: ScreenStateInput): ScreenState {
if (i.hasPendingDraft) return { kind: 'DRAFT' } // draft wint van alles
if (i.activeSprintItem) {
const building = i.buildingSprintIds.includes(i.activeSprintItem.id)
const dirty = i.pendingAdds.length > 0 || i.pendingRemoves.length > 0
return dirty ? { kind: 'EDITING', building } : { kind: 'ACTIVE', building }
}
return { kind: 'NO_SPRINT' }
}
Bewust geen useScreenState-hook: consumers roepen deriveScreenState() inline aan met store-slices + SSR-props. ScreenStateInput is daar precies voor ontworpen. Hook-extractie is "straks" als meer componenten meedoen — niet nu. PRODUCT_NOT_ACTIVE en DEMO_MODE blijven buiten ScreenState (gates, geen knopen — conform het doc).
__tests__/stores/product-workspace/screen-state.test.ts — pure input→output tests, patroon van tests/lib/product-switch-path.test.ts (vitest, geen mocks).
Te wijzigen bestanden
| Bestand | Wijziging |
|---|---|
| components/shared/sprint-switcher.tsx | Leest pendingAdds/pendingRemoves uit useProductWorkspaceStore; hasPendingDraft = bestaand draftGoal !== null (regel 57). Roept deriveScreenState() aan. Trigger-label (regel 142-156) vertakt op screenState.kind: bij DRAFT toont de trigger ⚙ Concept — {draftGoal} i.p.v. "Selecteer sprint"; status/BUILDING-badge verborgen in DRAFT. (G1 + G5) |
| components/backlog/new-sprint-trigger.tsx | isActiveProduct: boolean toevoegen aan props; if (!isActiveProduct) return null — spiegelt het bestaande if (hasDraft) return null patroon (regel 25). (G6) |
| app/(app)/products/[id]/page.tsx | Regel 134: isActiveProduct={isActiveProduct} doorgeven aan NewSprintTrigger (isActiveProduct bestaat al op regel 49). (G6) |
| tests/components/shared/sprint-switcher.test.tsx | Uitbreiden: trigger toont "⚙ Concept" als er een draft is. (G5) |
__tests__/components/backlog/new-sprint-trigger.test.tsx |
Toevoegen of uitbreiden: component returnt null bij isActiveProduct={false}. (G6) |
Hergebruik (niets nieuws bouwen)
- Pure-selector-patroon:
selectIsDirty/selectPendingCount— stores/product-workspace/selectors.ts:166 - Bestaande
draftGoal-selector — components/shared/sprint-switcher.tsx:57 - Bestaande
buildingSet-logica voor dropdown-items — components/shared/sprint-switcher.tsx:51 sprintMembership.pendingshape{ adds, removes }— bestaande store-slice- Test-patroon pure functie — tests/lib/product-switch-path.test.ts
Taken (Story ST-1369)
| Code | Taak | Kern |
|---|---|---|
| T-1033 | screen-state.ts — ScreenState type + pure deriveScreenState() |
Nieuw bestand, pure module |
| T-1034 | Unit tests screen-state.test.ts |
4 kinds + building-flag + precedence (draft wint) |
| T-1035 | SprintSwitcher op deriveScreenState() + G5: draft op de trigger-knop |
G1-wiring + G5 |
| T-1036 | NewSprintTrigger achter isActiveProduct-gate (component + page.tsx) |
G6 |
| T-1037 | Component-tests: sprint-switcher (G5) + new-sprint-trigger (G6) | Regressie-dekking |
Verificatie
npm run verify— lint + typecheck + test (de lokale gate;npm run buildkan in een worktree falen op ontbrekendeDATABASE_URL)npm test -- screen-state— de nieuwe pure-functie-tests geïsoleerdnpm run dev+ browser:- G5: start een nieuwe sprint-draft → de SprintSwitcher-trigger toont "⚙ Concept — [goal]" (niet alleen de dropdown)
- G5: annuleer de draft → trigger valt terug op sprint-code / "Selecteer sprint"
- G6: open een niet-actief product → de "Nieuwe sprint"-knop is afwezig; activeer het product → knop verschijnt
- regressie: actieve sprint zonder draft toont gewoon code + status/BUILDING-badge
Uitgesteld (follow-up)
G3 — expliciete ERROR-schermstaat. Vandaag: server-action-fout → toast.error, scherm blijft in huidige state. Reden uitstel: het doc adviseert dit alleen op te pakken als falende commits of SSE-verlies een aantoonbaar UX-probleem blijken; ERROR past bovendien niet natuurlijk in de pure deriveScreenState() (een fout is geen afgeleide van de input-flags). Vereist eerst een aparte ontwerpkeuze.