Merge branch 'main' into feat/demo-prefs

This commit is contained in:
Janpeter Visser 2026-05-12 20:03:31 +02:00 committed by GitHub
commit 14f539441f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 6871 additions and 191 deletions

View file

@ -13,7 +13,11 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import { setActiveSprintAction } from '@/actions/active-sprint'
import {
clearActiveSprintAction,
switchActiveSprintAction,
} from '@/actions/active-sprint'
import { useProductWorkspaceStore } from '@/stores/product-workspace/store'
import { useUserSettingsStore } from '@/stores/user-settings/store'
import type { SprintStatusApi } from '@/lib/task-status'
import { debugProps } from '@/lib/debug'
@ -47,6 +51,13 @@ export function SprintSwitcher({
const buildingSet = new Set(buildingSprintIds)
const isDemo = useUserSettingsStore(s => s.context.isDemo)
// PBI-79: zolang er een sprint-draft loopt tonen we 'Concept — [goal]'
// bovenaan de dropdown. De draft staat alleen in deze session-store; bij
// page-refresh/leave is hij weg.
const draftGoal = useUserSettingsStore(
(s) => s.entities.settings.workflow?.pendingSprintDraft?.[productId]?.goal ?? null,
)
const visibleSprints = sprints.filter(s => {
if (showClosed) return true
if (s.id === activeSprint?.id) return true
@ -60,13 +71,43 @@ export function SprintSwitcher({
return
}
startTransition(async () => {
const result = await setActiveSprintAction(productId, sprintId)
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 {
router.refresh()
}
})
}
function handleClearActiveSprint() {
if (!activeSprint) return
startTransition(async () => {
const result = await clearActiveSprintAction(productId)
if (result?.error) {
toast.error(typeof result.error === 'string' ? result.error : 'Wisselen mislukt')
return
}
if (pathname.includes('/sprint')) {
router.push(`/products/${productId}/sprint/${sprintId}`)
router.push(`/products/${productId}`)
} else {
router.refresh()
}
@ -133,6 +174,30 @@ export function SprintSwitcher({
Toon afgeronde sprints
</button>
<DropdownMenuSeparator />
{draftGoal && (
<>
<DropdownMenuItem
disabled
className="italic text-tertiary opacity-90 cursor-default"
data-debug-id="sprint-switcher__concept"
>
<span className="shrink-0"> Concept </span>
<span className="truncate">{draftGoal}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onClick={handleClearActiveSprint}
disabled={!activeSprint || isPending}
className={cn(
'italic text-muted-foreground',
!activeSprint && 'opacity-50 cursor-not-allowed',
)}
>
Geen actieve sprint
</DropdownMenuItem>
<DropdownMenuSeparator />
{visibleSprints.length === 0 ? (
<div className="px-2 py-2 text-sm text-muted-foreground/70 italic">
Geen open sprints