feat(PBI-79): sprint-switch auto-select PBI/story + user-settings persist

Bij sprint-switch wordt de sprint-content server-side opgevraagd. Wanneer
de sprint precies één PBI (en die PBI exact één story binnen de sprint)
heeft, worden PBI en story automatisch geselecteerd. Alle drie keuzes
(sprint, pbi, story) worden atomair in user-settings opgeslagen zodat ze
cross-device blijven hangen.

- lib/user-settings.ts: layout krijgt nullable activePbis +
  activeStories per product.
- lib/active-sprint.ts: setActiveSelectionInSettings schrijft de drie
  keys atomair + notify pg_notify.
- actions/active-sprint.ts: switchActiveSprintAction(productId, sprintId)
  doet de server-side auto-select-resolutie (single PBI → single story)
  en returnt { sprintId, pbiId, storyId }.
- components/shared/sprint-switcher.tsx: handleSwitchSprint roept de
  nieuwe action aan en synchroniseert de workspace-store gelijk zodat
  de UI geen flash krijgt voor de SSR-refresh.
- components/backlog/active-selection-hydrator.tsx (nieuw): client-side
  effect dat user-settings.activePbis/activeStories naar workspace-store
  spiegelt; wint van de localStorage hint-restore.
- app/(app)/products/[id]/page.tsx: ActiveSelectionHydrator gemount
  binnen BacklogHydrationWrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-11 18:13:20 +02:00
parent 35c6404b14
commit d7d11124e3
6 changed files with 205 additions and 4 deletions

View file

@ -13,7 +13,11 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import { clearActiveSprintAction, setActiveSprintAction } from '@/actions/active-sprint'
import {
clearActiveSprintAction,
switchActiveSprintAction,
} from '@/actions/active-sprint'
import { useProductWorkspaceStore } from '@/stores/product-workspace/store'
import type { SprintStatusApi } from '@/lib/task-status'
import { debugProps } from '@/lib/debug'
@ -54,11 +58,25 @@ export function SprintSwitcher({
function handleSwitchSprint(sprintId: string) {
if (sprintId === activeSprint?.id) return
startTransition(async () => {
const result = await setActiveSprintAction(productId, sprintId)
if (result?.error) {
toast.error(typeof result.error === 'string' ? result.error : 'Wisselen mislukt')
const result = await switchActiveSprintAction(productId, sprintId)
if ('error' in result) {
toast.error(
typeof result.error === 'string' ? result.error : 'Wisselen mislukt',
)
return
}
// Synchroniseer de client-side workspace-store met de auto-select die
// server-side is bepaald — voorkomt korte flash van vorige selectie
// voordat router.refresh de SSR-render binnenhaalt.
const store = useProductWorkspaceStore.getState()
if (result.pbiId) {
store.setActivePbi(result.pbiId)
if (result.storyId) {
store.setActiveStory(result.storyId)
}
} else {
store.setActivePbi(null)
}
if (pathname.includes('/sprint')) {
router.push(`/products/${productId}/sprint/${sprintId}`)
} else {