From d587be2fb361e84a3f3b3bc09f52d82402edf6b6 Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Mon, 11 May 2026 18:56:46 +0200 Subject: [PATCH] feat(PBI-79): Product Backlog sprint-membership via vinkjes (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(PBI-79/ST-1333): active-sprint null-contract + clearActiveSprintAction - lib/user-settings.ts: activeSprints values nullable in Zod-schema. Key-aanwezigheid heeft nu betekenis (key+null = bewust geen sprint; key ontbreekt = fallback-cascade). - lib/active-sprint.ts: nieuwe readStoredActiveSprintState helper + resolveActiveSprint respecteert expliciet 'cleared' state zonder fallback. clearActiveSprintInSettings schrijft null i.p.v. de key te verwijderen. - actions/active-sprint.ts: nieuwe clearActiveSprintAction met auth + membership-check. - components/shared/sprint-switcher.tsx: '— Geen actieve sprint —'-optie in dropdown, disabled wanneer er geen actieve sprint is. - Tests: nieuwe active-sprint.test.ts (resolver-paden + clear), active-sprint-action.test.ts (action-laag), uitbreiding user-settings.test.ts. Plan: docs/plans/PBI-79-backlog-sprint-workflow.md Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1334): user-settings pendingSprintDraft-slot - lib/user-settings.ts: nieuw workflow.pendingSprintDraft veld met compacte intent-shape (pbiIntent + per-PBI storyOverrides). - actions/sprint-draft.ts: setPendingSprintDraftAction + clearPendingSprintDraftAction met product-membership-check + Zod-validatie. - stores/user-settings/store.ts: setPendingSprintDraft / clearPendingSprintDraft optimistic acties + fine-grained mutators upsertPbiIntent / upsertStoryOverride. Sprint-draft actions worden dynamisch geïmporteerd zodat jsdom-tests zonder DATABASE_URL niet falen. - Tests: nieuwe sprint-draft.test.ts (action-laag), uitbreiding user-settings store-tests (5 nieuwe cases) en schema-tests (4 cases). Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1343): sprint-conflicts helper-library - lib/sprint-conflicts.ts: drie pure/server-side helpers voor eligibility + cross-sprint detectie. - isEligibleForSprint(story): sprint_id IS NULL en status != DONE - partitionByEligibility(prisma, storyIds, excludeSprintId): split in eligible / notEligible / crossSprint met reden per story - getBlockingSprintMap(prisma, productId, storyIds, excludeSprintId): map storyId → { sprintId, sprintName } voor stories in andere OPEN sprint - Tests: __tests__/lib/sprint-conflicts.test.ts (16 cases) — alle eligibility paden + cross-sprint scoping + CLOSED-sprint filtering. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1335): sprint-membership-summary + cross-sprint-blocks endpoints Twee nieuwe GET-route handlers, beide verplicht gescoped op pbiIds (geen product-brede aanroepen). - app/api/products/[id]/sprint-membership-summary/route.ts Response: { [pbiId]: { total, inSprint } } via twee prisma.groupBy calls (totaal + binnen actieve sprint). Voor state-B tri-state. - app/api/products/[id]/cross-sprint-blocks/route.ts Response: { [storyId]: { sprintId, sprintName } } voor stories in andere OPEN sprints. UX-hint voor disabled-vinkjes; commit-acties blijven autoritatief. Tests: 13 cases dekken happy path, 400 zonder pbiIds, 400 zonder sprintId, 404 zonder product-access, auth-fail, en NOT-clause voor excludeSprintId. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1336): product-workspace sprint-membership slice + selectors Datalaag voor de vinkje-UI van state A′ en state B. types.ts: - PbiSummaryEntry, CrossSprintBlock, SprintMembershipSlice toegevoegd. store.ts: - Nieuwe slice `sprintMembership` met pbiSummary, crossSprintBlocks, pending: { adds[], removes[] }, loadedSummaryForSprintId. - Acties: setPbiSummary, setCrossSprintBlocks, toggleStorySprintMembership (cancel-out logic), resetSprintMembershipPending, fetchSprintMembershipSummary, fetchCrossSprintBlocks. - hydrateSnapshot reset óók de membership-slice. selectors.ts: - selectPbiTriState (aggregate-only zolang stories niet geladen; rekent pending mee bij loaded PBI's). - selectStoryEffectiveInSprint (DB ⊕ pending). - selectStoryIsBlocked (cross-sprint hint). - selectIsDirty, selectPendingCount. Tests: 25 cases in nieuwe sprint-membership.test.ts dekken alle selector- paden, toggle-cancel-out, fetch-helpers, en pbiId-scoping. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1337): state A′ UI — metadata dialog + sticky banner + PbiList ombouw UI-laag voor de sprint-definitie-flow (state A′). Nieuw: - NewSprintMetadataDialog (stap 1): sprint_goal + optionele dates; 'Verder' schrijft via useUserSettingsStore.setPendingSprintDraft. - SprintDefinitionBanner (sticky): toont doel + X PBI's / Y stories teller; 'Annuleren' → AlertDialog confirm → clearPendingSprintDraft; 'Sprint aanmaken' nog niet aangesloten (wacht op ST-1339). - NewSprintTrigger: button in page header die de metadata-dialog opent; verbergt zichzelf zolang er al een draft loopt. - SprintDraftBanner: client-wrapper, rendert banner alleen als draft bestaat. Wijzigingen: - lib/user-settings.ts: pendingSprintDraft startAt/endAt → z.string().date(). - PbiList: oude selectionMode + selectedIds + NewSprintDialog vervangen door hasDraft-afgeleide A′-mode met tri-state vinkjes; togglen muteert upsertPbiIntent('all'|'none') en wist storyOverrides per PBI. - StoryPanel: in A′-mode toont elke story een cherrypick-checkbox die upsertStoryOverride('add'/'remove'/'clear') aanroept; cross-sprint-blocked stories krijgen disabled-icoon met sprint-naam tooltip. - app/(app)/products/[id]/page.tsx: StartSprintButton vervangen door NewSprintTrigger; SprintDraftBanner gepositioneerd boven split-pane. Tests: bestaande tests blijven groen (806 cases) — UI-specifieke component tests volgen later. ST-1339 sluit createSprintWithSelectionAction aan. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1339): createSprintWithSelectionAction + banner wire-up actions/sprints.ts: - Nieuwe createSprintWithSelectionAction(productId, metadata, pbiIntent, storyOverrides). - Server-side intent-resolve: 1. Voor elke PBI met intent='all': fetch child-story-IDs minus storyOverrides[pbi].remove. 2. Plus storyOverrides[*].add (cross-PBI cherrypick toegestaan). - Eligibility-filter via partitionByEligibility (sprint_id IS NULL + status != DONE; stories in andere OPEN sprint → conflicts.crossSprint). - Transactie wrapt sprint.create + story.updateMany (status='IN_SPRINT') + task.updateMany (sprint_id cascade) — alles atomair. - setActiveSprintInSettings na success. - Return: { success, sprintId, affectedStoryIds, affectedPbiIds, affectedTaskIds, conflicts: { notEligible, crossSprint } } of error. components/backlog/sprint-definition-banner.tsx: - 'Sprint aanmaken'-knop sluit aan op createSprintWithSelectionAction; toast bij conflicts, success-toast anders, router.refresh() voor SSR cycle. Pending draft wordt door de action zelf nog niet expliciet gewist — dat gebeurt via revalidatePath en kan in ST-1340 finetuned worden. Tests: __tests__/actions/create-sprint-with-selection.test.ts (6 cases) dekken intent-resolve, override-respect, cross-sprint conflict, transactie- binding van story.status + task.sprint_id, return-shape, en error-pad. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1340): commitSprintMembershipAction + gerichte client-store patches actions/sprints.ts: - Nieuwe commitSprintMembershipAction(activeSprintId, adds[], removes[]). - Eligibility-filter voor adds via partitionByEligibility (sprint_id IS NULL en niet DONE; cross-sprint conflicts → notEligible). - Race-safety voor removes: alleen stories met huidige sprint_id == activeSprintId; rest → conflicts.alreadyRemoved. - Transactie wrapt twee updateMany-paren (story status mee, task.sprint_id cascade). Update-paren overgeslagen wanneer leeg. - Return: { success, affectedStoryIds, affectedPbiIds, affectedTaskIds, conflicts: { notEligible, alreadyRemoved } }. stores/product-workspace/store.ts: - applyMembershipCommitResult({ activeSprintId, addedStoryIds, removedStoryIds }) patcht entities.storiesById met juiste sprint_id + status; ledigt sprintMembership.pending. Geen task-veld omdat BacklogTask geen sprint_id-kolom heeft in de store. Tests: __tests__/actions/commit-sprint-membership.test.ts (8 cases) — happy path, DONE-conflict, cross-sprint, race-safety voor removes, transactie- inhoud (status='IN_SPRINT'/'OPEN'), task-cascade, return-shape, auth-fail. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1338): state B vinkjes-UI + 'Sprint opslaan'-knop met teller State B (actieve sprint geselecteerd, geen draft) hangt nu aan dezelfde vinkje-UI als state A′, maar muteert de transient pending-buffer in plaats van de draft. - PbiList: nieuwe prop activeSprintId. selectionMode = hasDraft || stateBMode. togglePbiInDraft routeert naar upsertPbiIntent (A′) of bulk- toggleStorySprintMembership over eligible child-stories (B, skip blocked). - StoryPanel: idem prop activeSprintId. StoryBlockWithCherrypick muteert draft via upsertStoryOverride in A′ of pending buffer via toggleStorySprintMembership in B (cross-sprint blocked = disabled). - SaveSprintButton (nieuw): client component in page header, alleen zichtbaar als er een actieve sprint is. Disabled bij clean buffer, enabled met teller bij dirty. Klikken calls commitSprintMembershipAction → applyMembershipCommitResult gericht in store + toast bij conflicts. - page.tsx: activeSprintItem.id wordt doorgegeven aan PbiList, StoryPanel en SaveSprintButton. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(PBI-79/ST-1341+ST-1342): SprintEditDialog metadata-edit + multi-OPEN sprints ST-1341 (T-946): - actions/sprints.ts: nieuwe updateSprintAction(sprintId, fields) — JSON input, accepteert optionele goal/startAt/endAt; auth + product-access check, prisma.sprint.update, revalidatePath. Type-safe return. - components/backlog/sprint-edit-dialog.tsx: Entity-Dialog-pattern voor metadata-edit van een sprint. Velden: sprint_goal, start_date, end_date. Link 'Sprint afronden… →' naar bestaande /products/[id]/sprint/[sprintId] zodat de completion-flow (per-story DONE/OPEN beslissing + PBI-promotie) niet wordt geduplicereerd. useDirtyCloseGuard. ST-1342 (T-947): - actions/sprints.ts: OPEN-uniqueness check in createSprintAction verwijderd. Een product mag nu meerdere OPEN sprints tegelijk hebben; cross-sprint-conflicts per story worden afgevangen door partitionByEligibility in de membership-commit-flow. Co-Authored-By: Claude Opus 4.7 (1M context) * test(PBI-79/ST-1344): updateSprintAction regression coverage Audits van de geplande non-regressie-tests laten zien dat alle invarianten uit het ST-1344 plan reeds gedekt zijn door eerder toegevoegde tests: - clearActiveSprintAction null-not-delete → __tests__/lib/active-sprint.test.ts + __tests__/actions/active-sprint-action.test.ts - Endpoints rejecten zonder pbiIds (400) → __tests__/api/sprint-membership-summary.test.ts + __tests__/api/cross-sprint-blocks.test.ts - Status-mutaties story.status=IN_SPRINT/OPEN met task.sprint_id cascade in dezelfde transactie → __tests__/actions/create-sprint-with-selection.test.ts + __tests__/actions/commit-sprint-membership.test.ts - Cross-sprint conflicts + DONE-eligibility → __tests__/lib/sprint-conflicts.test.ts Nieuw: __tests__/actions/update-sprint.test.ts (6 cases) dekt updateSprintAction die nog geen tests had — goal alleen, dates alleen, null-clear, 403 zonder access, lege goal weigering, leeg fields-object weigering. Handmatige E2E checklist (T-949) blijft staan voor menselijke browser- validatie tijdens PR-review. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(PBI-79): PBI-rij selecteert weer in A′/B-modus; vinkje is aparte trigger Voor PBI-79 maakte het hele PBI-kaartje in selectionMode (state A′ én B) de toggle. Daardoor: - klik op rij = bulk-toggle stories (teller liep op); - geen setActivePbi, dus StoryPanel kreeg geen content. Fix: in selectionMode wordt onClick = onSelect (PBI activeren → stories laden) en de tri-state-iconen verhuizen naar een eigen