Scrum4Me/docs/plans/PBI-91-pb-screen-state.md
Madhura68 e10b010884 docs(ST-1369): plan PBI-91 — expliciete schermstaat + draft-zichtbaarheid
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:21:23 +02:00

6.9 KiB
Raw Blame History

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

  • G1deriveScreenState(): één pure functie die de vandaag verspreide schermstaat-afleiding consolideert.
  • G5 — draft-status zichtbaar op de SprintSwitcher-trigger (de oorspronkelijke bug).
  • G6NewSprintTrigger achter een isActiveProduct-gate.
  • G3uitgesteld, 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)

Taken (Story ST-1369)

Code Taak Kern
T-1033 screen-state.tsScreenState 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.