feat(PBI-91): expliciete schermstaat + draft-zichtbaarheid PB-page (#210)

* docs(ST-1369): plan PBI-91 — expliciete schermstaat + draft-zichtbaarheid

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1369): screen-state module — ScreenState + deriveScreenState()

Pure afleidingslaag die de verspreide schermstaat-derivatie van de Product
Backlog page consolideert tot één testbaar ScreenState-model. Nog geen
consumers — die volgen in T-1035/T-1036.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ST-1369): unit-tests voor deriveScreenState()

Dekt alle vier de kinds (NO_SPRINT, DRAFT, ACTIVE, EDITING), de building-flag
en de draft-voorrang boven een actieve sprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1369): SprintSwitcher op deriveScreenState + draft op trigger (G5)

De trigger-knop toont nu de concept-sprint zodra er een sprint-draft loopt,
niet langer alleen de (disabled) dropdown-regel. Schermstaat-afleiding loopt
via de pure deriveScreenState() i.p.v. losse flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1369): NewSprintTrigger achter isActiveProduct-gate (G6)

De "Nieuwe sprint"-knop rendert niet langer op een niet-actief product —
een sprint-draft starten daar was verwarrend. page.tsx geeft de bestaande
isActiveProduct-flag door.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ST-1369): component-tests voor draft-op-trigger (G5) en isActiveProduct-gate (G6)

sprint-switcher: trigger toont concept-sprint bij een pending draft, en geen
concept-label zonder draft. new-sprint-trigger: nieuw testbestand — rendert
niet op een niet-actief product, wel op een actief product zonder draft.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-15 01:45:35 +02:00 committed by GitHub
parent 2a6386163c
commit 3d5c22382c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 351 additions and 8 deletions

View file

@ -48,6 +48,7 @@ Auto-generated on 2026-05-14 from front-matter and headings.
| [Plan v3.5 — Bootstrap-wizard voor nieuwe Product-repo (Scrum4Me feature)](./plans/M8-bootstrap-wizard.md) | reviewed | — |
| [PBI-80 — Demo-gebruiker mag eigen UI-voorkeuren wijzigen](./plans/PBI-80-demo-prefs.md) | — | — |
| [Plan — `code` wordt bindende volgorde voor stories & taken; drag-and-drop eruit](./plans/PBI-84-code-binding-order.md) | — | — |
| [Plan — Expliciete schermstaat + draft-zichtbaarheid op de Product Backlog page](./plans/PBI-91-pb-screen-state.md) | — | — |
| [Queue-loop verplaatsen van Claude naar runner](./plans/queue-loop-extraction.md) | — | — |
| [Sprint MCP-tools — create_sprint & update_sprint](./plans/sprint-mcp-tools.md) | draft | 2026-05-11 |
| [Advies - SprintRun, PR en worktree lifecycle als state machines](./plans/sprint-pr-worktree-state-machines.md) | draft | 2026-05-06 |

View file

@ -0,0 +1,98 @@
# 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 15).
>
> 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](../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**`NewSprintTrigger` achter een `isActiveProduct`-gate.
- **G3***uitgesteld*, vastgelegd als follow-up.
## Aanpak
### Nieuwe bestanden
**`stores/product-workspace/screen-state.ts`** — pure module, géén React, spiegelt `selectors.ts`:
```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](../../__tests__/lib/product-switch-path.test.ts) (vitest, geen mocks).
### Te wijzigen bestanden
| Bestand | Wijziging |
|---|---|
| [components/shared/sprint-switcher.tsx](../../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](../../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](../../app/(app)/products/%5Bid%5D/page.tsx) | Regel 134: `isActiveProduct={isActiveProduct}` doorgeven aan `NewSprintTrigger` (`isActiveProduct` bestaat al op regel 49). **(G6)** |
| [__tests__/components/shared/sprint-switcher.test.tsx](../../__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](../../stores/product-workspace/selectors.ts:166)
- Bestaande `draftGoal`-selector — [components/shared/sprint-switcher.tsx:57](../../components/shared/sprint-switcher.tsx:57)
- Bestaande `buildingSet`-logica voor dropdown-items — [components/shared/sprint-switcher.tsx:51](../../components/shared/sprint-switcher.tsx:51)
- `sprintMembership.pending` shape `{ adds, removes }` — bestaande store-slice
- Test-patroon pure functie — [__tests__/lib/product-switch-path.test.ts](../../__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 build` kan in een worktree falen op ontbrekende `DATABASE_URL`)
- `npm test -- screen-state` — de nieuwe pure-functie-tests geïsoleerd
- `npm 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.