From bf42609d6d87a317dca2ba89272f436ac3c72a07 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Thu, 14 May 2026 22:18:43 +0200 Subject: [PATCH] =?UTF-8?q?docs(T-1015):=20PB-workflow=20doc=20=E2=80=94?= =?UTF-8?q?=20as-is=20workflow-states,=20transitions=20en=20diagram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tweede laag van het Product Backlog page workflow-doc (PBI-88 / ST-1363): de zeven impliciete workflow-states met preconditie en UI-gedrag, de transition-tabel, en een Mermaid stateDiagram-v2. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/architecture/product-backlog-workflow.md | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/docs/architecture/product-backlog-workflow.md b/docs/architecture/product-backlog-workflow.md index 70a194e..3ef0afa 100644 --- a/docs/architecture/product-backlog-workflow.md +++ b/docs/architecture/product-backlog-workflow.md @@ -92,3 +92,78 @@ Spiegelbeeld voor de sprint-board: stories/taken *binnen* één sprint. Zelfde s Gebruikersvoorkeuren + cross-tab sync. Voor dit scherm cruciaal: **`workflow.pendingSprintDraft[productId]`** — de concept-sprint die ontstaat bij "Nieuwe sprint". Sinds PBI-79 is die draft **session-only**: `setPendingSprintDraft` / `clearPendingSprintDraft` schrijven alleen lokaal (geen server-roundtrip), en `hydrate()` stript legacy DB-entries weg zodat de draft niet "spookt". `upsertPbiIntent` / `upsertStoryOverride` muteren de draft-selectie. Slice-details van het workspace-patroon (`ensure*Loaded`, selectors, optimistic mutations, gotchas) staan in [workspace-store.md](../patterns/workspace-store.md) — hier niet gedupliceerd. + +## Workflow-states (as-is) + +Het scherm kent vandaag **geen expliciete `screenState`**. De zichtbare toestand wordt per render ad-hoc afgeleid uit SSR-flags in `app/(app)/products/[id]/page.tsx` (`isActiveProduct`, `hasOpenSprint`, `isDemo`, plus `sprintItems` / `activeSprintItem` / `buildingSprintIds` uit `getSprintSwitcherData`) en store-flags (`pendingSprintDraft` uit `user-settings`, `sprintMembership.pending` uit `product-workspace`). Daaruit zijn **zeven** herkenbare states te destilleren. + +### 1. `PRODUCT_NOT_ACTIVE` +**Preconditie:** `isActiveProduct === false` (`user.active_product_id !== id`). +**UI:** `SprintSwitcher` wordt niet gerenderd (`{isActiveProduct && …}`); `ActivateProductButton` zichtbaar. De PBI/Story/Task-panes werken normaal. `NewSprintTrigger` is wél zichtbaar (alleen demo-gated, geen `isActiveProduct`-gate); `SaveSprintButton` niet. + +### 2. `PRODUCT_ACTIVE_NO_SPRINTS` +**Preconditie:** `isActiveProduct === true` && `sprintItems.length === 0`. +**UI:** `SprintSwitcher` rendert maar valt terug op de "Geen sprints"-tooltip (early return in `sprint-switcher.tsx`). `NewSprintTrigger` enabled. Geen `SaveSprintButton`, geen "Sprint actief →"-link (`hasOpenSprint === false`). + +### 3. `SPRINT_DRAFT_PENDING` +**Preconditie:** `user-settings.workflow.pendingSprintDraft[productId]` is truthy (session-only). +**UI:** sticky `SprintDefinitionBanner` onder de header (`sprint-draft-banner.tsx`); `NewSprintTrigger` verbergt zichzelf (`if (hasDraft) return null`); `SprintSwitcher` toont een *disabled* "⚙ Concept — [goal]"-item; `SprintDraftLeaveGuard` registreert een `beforeunload`-waarschuwing. De cherrypick-checkboxes in de PBI/Story-panes schakelen naar draft-selectie-modus. + +### 4. `ACTIVE_SPRINT_CLEAN` +**Preconditie:** `isActiveProduct` && `activeSprintItem !== null` && `selectIsDirty === false`. +**UI:** `SprintSwitcher` toont de actieve sprint-code + status; `SaveSprintButton` zichtbaar maar **disabled** (`disabled={!isDirty || …}`), label "Sprint opslaan"; "Sprint actief →"-link zichtbaar zolang er een open sprint is. + +### 5. `ACTIVE_SPRINT_DIRTY` +**Preconditie:** als 4, maar `selectIsDirty === true` — `sprintMembership.pending.adds`/`removes` is niet leeg. +**UI:** als 4, maar `SaveSprintButton` **enabled** met teller — label `Sprint opslaan (N)`. + +### 6. `ACTIVE_SPRINT_BUILDING` +**Preconditie:** `activeSprintItem` && `buildingSprintIds.includes(activeSprintItem.id)` — er loopt een `SprintRun` met status `QUEUED`/`RUNNING` (`getSprintSwitcherData`). +**UI:** als 4/5, maar `SprintSwitcher` toont een gele **"BUILDING"**-badge i.p.v. de sprint-status. Cosmetisch — er worden geen knoppen geblokkeerd. + +### 7. `DEMO_MODE` +**Preconditie:** `session.isDemo === true`. +**Strikt genomen geen state** maar een **cross-cutting constraint** over alle bovenstaande states: schrijf-knoppen (`ActivateProductButton`, `SaveSprintButton`, `NewSprintTrigger`, `EditProductButton`) zijn verborgen of `disabled`; de `SprintSwitcher` navigeert i.p.v. een server-action aan te roepen; en de server-actions zelf returnen vroeg met `{ error: 'Niet beschikbaar in demo-modus' }`. + +## Transitions (as-is) + +Er is geen centrale overgangs-tabel; elke transitie is een server-action of store-mutatie gevolgd door een re-render: + +| Van → naar | Trigger | +|---|---| +| `PRODUCT_NOT_ACTIVE` → `PRODUCT_ACTIVE_*` | `setActiveProductAction` (`actions/active-product.ts`) → `revalidatePath('/', 'layout')` | +| `PRODUCT_ACTIVE_NO_SPRINTS` / `ACTIVE_SPRINT_*` → `SPRINT_DRAFT_PENDING` | `NewSprintTrigger` → `NewSprintMetadataDialog` → `setPendingSprintDraft` (`user-settings` store, session-only) | +| `SPRINT_DRAFT_PENDING` → vorige state | `SprintDefinitionBanner` "Annuleren" → `clearPendingSprintDraft` | +| `SPRINT_DRAFT_PENDING` → `ACTIVE_SPRINT_CLEAN` | `SprintDefinitionBanner` "Sprint aanmaken" → `createSprintWithSelectionAction` (`actions/sprints.ts`): maakt de sprint, koppelt de geselecteerde stories, en de nieuwe sprint wordt de actieve sprint | +| `ACTIVE_SPRINT_CLEAN` ↔ `ACTIVE_SPRINT_DIRTY` | `toggleStorySprintMembership` (`product-workspace` store) maakt dirty; `commitSprintMembershipAction` via `SaveSprintButton` → `applyMembershipCommitResult` maakt weer clean | +| `ACTIVE_SPRINT_*` → andere `ACTIVE_SPRINT_*` | `SprintSwitcher` dropdown → `switchActiveSprintAction` (`actions/active-sprint.ts`) | +| `ACTIVE_SPRINT_*` → `PRODUCT_ACTIVE_NO_SPRINTS` (geen actieve sprint) | `SprintSwitcher` "— Geen actieve sprint —" → `clearActiveSprintAction` | +| `ACTIVE_SPRINT_CLEAN/DIRTY` ↔ `ACTIVE_SPRINT_BUILDING` | extern: een `SprintRun` gaat naar `QUEUED`/`RUNNING` resp. rondt af — zichtbaar bij de volgende SSR-render | + +Server-actions roepen `revalidatePath` aan; de client doet daarnaast `router.refresh()` (bv. na sprint-creatie). De realtime-laag (SSE → `applyRealtimeEvent`) dekt *externe* wijzigingen — zie de sectie Architectuur-lagen hierboven. + +## State-diagram + +```mermaid +stateDiagram-v2 + [*] --> PRODUCT_NOT_ACTIVE + PRODUCT_NOT_ACTIVE --> PRODUCT_ACTIVE_NO_SPRINTS: setActiveProductAction + + PRODUCT_ACTIVE_NO_SPRINTS --> SPRINT_DRAFT_PENDING: Nieuwe sprint / setPendingSprintDraft + SPRINT_DRAFT_PENDING --> PRODUCT_ACTIVE_NO_SPRINTS: Annuleren / clearPendingSprintDraft + SPRINT_DRAFT_PENDING --> ACTIVE_SPRINT: Sprint aanmaken / createSprintWithSelectionAction + + state ACTIVE_SPRINT { + [*] --> CLEAN + CLEAN --> DIRTY: toggleStorySprintMembership + DIRTY --> CLEAN: commitSprintMembershipAction + CLEAN --> BUILDING: SprintRun QUEUED/RUNNING + BUILDING --> CLEAN: SprintRun klaar + } + + ACTIVE_SPRINT --> SPRINT_DRAFT_PENDING: Nieuwe sprint / setPendingSprintDraft + ACTIVE_SPRINT --> ACTIVE_SPRINT: switchActiveSprintAction + ACTIVE_SPRINT --> PRODUCT_ACTIVE_NO_SPRINTS: clearActiveSprintAction +``` + +`DEMO_MODE` staat bewust niet in het diagram: het is geen knoop in de graaf maar een read-only constraint die over álle states heen ligt.