Scrum4Me/components/backlog/pbi-list.tsx
Janpeter Visser d292e445d9
Sprint: Verbeteren debug mode (#179)
* feat(PBI-49): add debugProps helper + Vitest test

Adds lib/debug.ts with debugProps(id, component, file) that returns
data-debug-id and data-debug-label attrs in dev mode, empty object in
production. Adds __tests__/lib/debug.test.ts covering both modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(PBI-49): add debug-id pattern doc + CLAUDE.md reference

Adds docs/patterns/debug-id.md documenting the named-component boundary
rule (6 punten), helper-voorbeeld, skip-criteria en motivatie voor
handmatige pad-argumenten. Voegt verwijzing toe aan CLAUDE.md
patterns-tabel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): migrate 17 shared/ components to debugProps helper

Replace hardcoded data-debug-id + data-debug-label attribute pairs with
{...debugProps(id, component, file)} spread in all 17 components/shared/
files. Existing debug-ids preserved unchanged.

* feat(PBI-49): add debugProps to backlog/, sprint/, solo/ components

* feat(PBI-49): add debugProps to jobs/ + ideas/ components

* feat(PBI-49): add debugProps to products/ + settings/ + notifications/ components

* feat(PBI-49): add debugProps to admin/ + dashboard/ + dialogs/ + mobile/ + split-pane/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(PBI-49): use attr(data-debug-id) for debug tooltip in globals.css

* refactor(PBI-49): remove data-debug-label from debugProps helper + test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): strip unused component/file args from debugProps in shared/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to StatusBar, NavBar, PanelNavBar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/sprint/*

- new-sprint-dialog: __submit on submit button
- sprint-backlog: __list on SprintBacklogLeft + SprintBacklogRight scroll areas
- sprint-board-client: root wrapper div (display:contents) + __drag-overlay
- sprint-header: __title on goal button, __dates on dates button, __actions on action cluster
- sprint-run-controls: root on controls div, __start/__cancel on action buttons; __blockers-dialog on dialog content
- start-sprint-button: root on trigger button, __dialog on dialog content, __submit on submit button
- sync-active-sprint-cookie: no debug-id (returns null, side-effect only), comment added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/backlog/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/ideas/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/dashboard/* + components/markdown.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to new-product-button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/solo/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-elements to nav-status-indicators

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/jobs/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/products/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/notifications/*

- answer-modal: __content (scroll area), __submit (footer)
- notifications-bridge: skip comment (bridge, non-rendering wrapper)
- notifications-realtime-mount: skip comment (returns null)
- notifications-sheet: __header, __items (questions list)
- push-toggle: __switch (button), __label (button text) on subscribed/unsubscribed states

* feat(PBI-49): add BEM sub-element data-debug-id to components/settings/*

- leave-product-button: root only (single-button component)
- min-quota-editor: __input (number input), __save (save button)
- profile-editor: __username (bio/short-description input), __save (submit)
- role-manager: __roles (checkbox list), __add (save button)
- token-manager: __tokens (active tokens list), __generate (create button)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to admin, auth, dialogs, entity-dialog, mobile, split-pane

* docs(PBI-49): add debug-labels BEM pattern doc + CLAUDE.md entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:46:29 +02:00

635 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useTransition, useEffect } from 'react'
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
closestCenter,
} from '@dnd-kit/core'
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
arrayMove,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { toast } from 'sonner'
import { CheckSquare, Square } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useSelectionStore } from '@/stores/selection-store'
import { usePlannerStore } from '@/stores/planner-store'
import { useBacklogStore } from '@/stores/backlog-store'
import { deletePbiAction } from '@/actions/pbis'
import { reorderPbisAction, updatePbiPriorityAction } from '@/actions/stories'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
import { PbiDialog, type PbiDialogState } from './pbi-dialog'
import { BacklogCard } from './backlog-card'
import { EmptyPanel } from './empty-panel'
import { NewSprintDialog } from '@/components/sprint/new-sprint-dialog'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { PRIORITY_COLORS } from '@/components/shared/priority-select'
import { PBI_STATUS_LABELS, PBI_STATUS_COLORS } from '@/components/shared/pbi-status-select'
import type { PbiStatusApi } from '@/lib/task-status'
const PRIORITY_LABELS: Record<number, string> = {
1: 'Kritiek',
2: 'Hoog',
3: 'Gemiddeld',
4: 'Laag',
}
type SortMode = 'priority' | 'code' | 'date'
const SORT_OPTIONS: Array<{ value: SortMode; label: string }> = [
{ value: 'priority', label: 'Prioriteit' },
{ value: 'code', label: 'Code' },
{ value: 'date', label: 'Datum' },
]
const PRIORITY_OPTIONS: Array<{ value: number | 'all'; label: string }> = [
{ value: 'all', label: 'Alle' },
{ value: 1, label: 'Kritiek' },
{ value: 2, label: 'Hoog' },
{ value: 3, label: 'Gemiddeld' },
{ value: 4, label: 'Laag' },
]
const STATUS_OPTIONS: Array<{ value: PbiStatusApi | 'all'; label: string }> = [
{ value: 'all', label: 'Alle' },
{ value: 'ready', label: 'Klaar' },
{ value: 'blocked', label: 'Geblokkeerd' },
{ value: 'done', label: 'Afgerond' },
]
function FilterPills<T extends string | number>({
label,
options,
value,
onChange,
}: {
label: string
options: Array<{ value: T; label: string }>
value: T
onChange: (v: T) => void
}) {
return (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<div className="flex flex-wrap gap-1.5">
{options.map((opt) => (
<button
key={String(opt.value)}
type="button"
onClick={() => onChange(opt.value)}
className={cn(
'text-xs px-2.5 py-1 rounded-full border transition-colors',
value === opt.value
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent border-border hover:bg-surface-container'
)}
>
{opt.label}
</button>
))}
</div>
</div>
)
}
interface Pbi {
id: string
code: string | null
title: string
priority: number
description?: string | null
created_at: Date
status: PbiStatusApi
}
interface PbiListProps {
productId: string
isDemo: boolean
}
// --- Sortable PBI row ---
function SortablePbiRow({
pbi,
isSelected,
isDemo,
selectionMode,
isChecked,
onSelect,
onToggleCheck,
onEdit,
onDelete,
}: {
pbi: Pbi
isSelected: boolean
isDemo: boolean
selectionMode: boolean
isChecked: boolean
onSelect: () => void
onToggleCheck: () => void
onEdit: () => void
onDelete: () => void
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: pbi.id,
disabled: selectionMode,
})
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
if (selectionMode) {
return (
<BacklogCard
ref={setNodeRef}
style={style}
title={pbi.title}
code={pbi.code}
priority={pbi.priority}
isSelected={isChecked}
role="button"
tabIndex={0}
aria-pressed={isChecked}
onClick={onToggleCheck}
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleCheck() } }}
badge={
<Badge className={cn('text-xs font-normal', PBI_STATUS_COLORS[pbi.status])}>
{PBI_STATUS_LABELS[pbi.status]}
</Badge>
}
actions={
<div
className="inline-flex items-center justify-center min-h-7 min-w-7 text-muted-foreground"
aria-hidden="true"
>
{isChecked ? <CheckSquare size={18} className="text-primary" /> : <Square size={18} />}
</div>
}
/>
)
}
return (
<BacklogCard
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
title={pbi.title}
code={pbi.code}
priority={pbi.priority}
isSelected={isSelected}
isDragging={isDragging}
role="button"
tabIndex={0}
aria-pressed={isSelected}
onClick={onSelect}
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect() } }}
badge={
<Badge className={cn('text-xs font-normal', PBI_STATUS_COLORS[pbi.status])}>
{PBI_STATUS_LABELS[pbi.status]}
</Badge>
}
actions={
<div className="flex items-center gap-1">
<DemoTooltip show={isDemo}>
<button
onClick={(e) => { e.stopPropagation(); if (!isDemo) onEdit() }}
className="inline-flex items-center justify-center min-h-7 min-w-7 border border-border rounded text-xs text-muted-foreground hover:text-foreground hover:bg-surface-container transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Bewerk PBI"
disabled={isDemo}
>
</button>
</DemoTooltip>
<DemoTooltip show={isDemo}>
<button
onClick={(e) => { e.stopPropagation(); if (!isDemo) onDelete() }}
className="inline-flex items-center justify-center min-h-7 min-w-7 text-muted-foreground hover:text-error text-base leading-none disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Verwijder PBI"
disabled={isDemo}
>
×
</button>
</DemoTooltip>
</div>
}
/>
)
}
// --- Main component ---
export function PbiList({ productId, isDemo }: PbiListProps) {
const pbis = useBacklogStore((s) => s.pbis)
const { selectedPbiId, selectPbi } = useSelectionStore()
const { pbiOrder, pbiPriority, initPbis, reorderPbis, rollbackPbis, updatePbiPriority } = usePlannerStore()
// Defaults match SSR; persisted values applied post-mount in the loader effect below.
// This avoids hydration mismatch when localStorage holds non-default values.
const [filterPriority, setFilterPriority] = useState<number | 'all'>('all')
const [filterStatus, setFilterStatus] = useState<PbiStatusApi | 'all'>('all')
const [sortMode, setSortMode] = useState<SortMode>('priority')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
const [prefsLoaded, setPrefsLoaded] = useState(false)
const [dialogState, setDialogState] = useState<PbiDialogState | null>(null)
const [activeDragId, setActiveDragId] = useState<string | null>(null)
const [selectionMode, setSelectionMode] = useState(false)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [newSprintOpen, setNewSprintOpen] = useState(false)
const [, startTransition] = useTransition()
function exitSelection() {
setSelectionMode(false)
setSelectedIds(new Set())
}
function toggleCheck(id: string) {
setSelectedIds(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
// Load persisted preferences once after mount (client-only).
// setState calls here are intentional: hydrating from localStorage on first paint.
useEffect(() => {
const savedSort = localStorage.getItem('scrum4me:pbi_sort')
if (savedSort === 'priority' || savedSort === 'code' || savedSort === 'date') {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSortMode(savedSort)
}
const savedPriority = localStorage.getItem('scrum4me:pbi_filter_priority')
if (savedPriority && savedPriority !== 'all') {
const n = parseInt(savedPriority, 10)
if (Number.isInteger(n) && n >= 1 && n <= 4) setFilterPriority(n)
}
const savedStatus = localStorage.getItem('scrum4me:pbi_filter_status')
if (savedStatus === 'ready' || savedStatus === 'blocked' || savedStatus === 'done') {
setFilterStatus(savedStatus)
}
const savedDir = localStorage.getItem('scrum4me:pbi_sort_dir')
if (savedDir === 'asc' || savedDir === 'desc') setSortDir(savedDir)
setPrefsLoaded(true)
}, [])
// Persist on change, but skip the initial render so we don't overwrite saved values with defaults.
useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:pbi_sort', sortMode) }, [sortMode, prefsLoaded])
useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:pbi_filter_priority', String(filterPriority)) }, [filterPriority, prefsLoaded])
useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:pbi_filter_status', filterStatus) }, [filterStatus, prefsLoaded])
useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:pbi_sort_dir', sortDir) }, [sortDir, prefsLoaded])
// Sync server data into store — use stable string dep to avoid infinite loop
const pbiIdKey = pbis.map(p => p.id).join(',')
useEffect(() => {
initPbis(productId, pbiIdKey ? pbiIdKey.split(',') : [])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [productId, pbiIdKey])
// Build ordered PBI list from store (or fall back to server order)
const order = pbiOrder[productId] ?? pbis.map(p => p.id)
const pbiMap = Object.fromEntries(pbis.map(p => [p.id, p]))
// Apply priority overrides from store
const orderedPbis = order
.map(id => pbiMap[id])
.filter(Boolean)
.map(p => ({ ...p, priority: pbiPriority[p.id] ?? p.priority }))
const base = orderedPbis.filter(p => {
if (filterPriority !== 'all' && p.priority !== filterPriority) return false
if (filterStatus !== 'all' && p.status !== filterStatus) return false
return true
})
const activeFilterCount =
(filterPriority !== 'all' ? 1 : 0) +
(filterStatus !== 'all' ? 1 : 0) +
(sortMode !== 'priority' ? 1 : 0) +
(sortDir !== 'asc' ? 1 : 0)
const filtered = [...base].sort((a, b) => {
if (sortMode === 'code') {
return (a.code ?? '').localeCompare(b.code ?? '', 'nl', { numeric: true })
}
if (sortMode === 'date') {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
}
// priority: sort by priority asc, then drag-and-drop sort_order within group
return a.priority !== b.priority ? a.priority - b.priority : 0
})
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)
function handleDragStart(event: DragStartEvent) {
setActiveDragId(event.active.id as string)
}
function handleDragEnd(event: DragEndEvent) {
setActiveDragId(null)
const { active, over } = event
if (!over || active.id === over.id) return
const activePbi = pbiMap[active.id as string]
const overPbi = pbiMap[over.id as string]
if (!activePbi || !overPbi) return
const prevOrder = [...order]
const oldIndex = order.indexOf(active.id as string)
const newIndex = order.indexOf(over.id as string)
const newOrder = arrayMove([...order], oldIndex, newIndex)
// Optimistic update
reorderPbis(productId, newOrder)
const priorityChanged = activePbi.priority !== overPbi.priority
startTransition(async () => {
if (priorityChanged) {
updatePbiPriority(active.id as string, overPbi.priority)
const result = await updatePbiPriorityAction(active.id as string, overPbi.priority, productId)
if (!result.success) {
rollbackPbis(productId, prevOrder)
toast.error('Prioriteit opslaan mislukt')
}
} else {
const result = await reorderPbisAction(productId, newOrder)
if (!result.success) {
rollbackPbis(productId, prevOrder)
toast.error('Volgorde opslaan mislukt')
}
}
})
}
function handleDelete(id: string) {
startTransition(async () => {
await deletePbiAction(id)
if (selectedPbiId === id) selectPbi(null)
})
}
const activePbi = activeDragId ? pbiMap[activeDragId] : null
return (
<div className="flex flex-col h-full" {...debugProps('pbi-list', 'PbiList', 'components/backlog/pbi-list.tsx')}>
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-border bg-surface-container-low shrink-0" {...debugProps('pbi-list__header')}>
{filterPriority !== 'all' && (
<button
onClick={() => setFilterPriority('all')}
className="flex items-center gap-1 text-xs text-primary hover:underline"
aria-label="Wis prioriteitsfilter"
>
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
{PRIORITY_LABELS[filterPriority]}
</Badge>
<span>×</span>
</button>
)}
{filterStatus !== 'all' && (
<button
onClick={() => setFilterStatus('all')}
className="flex items-center gap-1 text-xs text-primary hover:underline"
aria-label="Wis statusfilter"
>
<Badge className={cn('text-xs', PBI_STATUS_COLORS[filterStatus])}>
{PBI_STATUS_LABELS[filterStatus]}
</Badge>
<span>×</span>
</button>
)}
<Popover>
<PopoverTrigger
render={
<Button variant="outline" size="sm" className="h-7 text-xs">
{`Filters${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`}
</Button>
}
/>
<PopoverContent align="end" className="w-72 space-y-4">
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">Sorteren op</p>
<div className="flex gap-1">
<button
type="button"
onClick={() => setSortDir('asc')}
className={cn(
'text-xs px-2 py-0.5 rounded border transition-colors',
sortDir === 'asc'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent border-border hover:bg-surface-container'
)}
aria-label="Oplopend sorteren"
>
</button>
<button
type="button"
onClick={() => setSortDir('desc')}
className={cn(
'text-xs px-2 py-0.5 rounded border transition-colors',
sortDir === 'desc'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent border-border hover:bg-surface-container'
)}
aria-label="Aflopend sorteren"
>
</button>
</div>
</div>
<div className="flex flex-wrap gap-1.5">
{SORT_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setSortMode(opt.value)}
className={cn(
'text-xs px-2.5 py-1 rounded-full border transition-colors',
sortMode === opt.value
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent border-border hover:bg-surface-container'
)}
>
{opt.label}
</button>
))}
</div>
</div>
<FilterPills
label="Prioriteit"
options={PRIORITY_OPTIONS}
value={filterPriority}
onChange={setFilterPriority}
/>
<FilterPills
label="Status"
options={STATUS_OPTIONS}
value={filterStatus}
onChange={setFilterStatus}
/>
<div className="flex justify-end pt-1 border-t border-border">
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 text-xs"
disabled={activeFilterCount === 0}
onClick={() => {
setFilterPriority('all')
setFilterStatus('all')
setSortMode('priority')
setSortDir('asc')
}}
>
Wis filters
</Button>
</div>
</PopoverContent>
</Popover>
<DemoTooltip show={isDemo}>
<Button
size="sm"
variant={selectionMode ? 'default' : 'outline'}
className="h-7 text-xs"
disabled={isDemo}
onClick={() => {
if (isDemo) return
if (selectionMode) exitSelection()
else setSelectionMode(true)
}}
>
{selectionMode ? 'Selecteren stoppen' : "Selecteer PBI's"}
</Button>
</DemoTooltip>
<DemoTooltip show={isDemo}>
<Button
size="sm"
className="h-7 text-xs"
disabled={isDemo || selectionMode}
onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
>
+ PBI
</Button>
</DemoTooltip>
</div>
<div className="flex-1 overflow-y-auto">
{pbis.length === 0 ? (
<EmptyPanel
message="Nog geen PBI's aangemaakt."
action={{ label: 'Maak je eerste PBI aan', onClick: () => setDialogState({ mode: 'create', productId, defaultPriority: 2 }), disabled: isDemo }}
/>
) : (
<DndContext
id="pbi-list"
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={filtered.map(p => p.id)}
strategy={verticalListSortingStrategy}
>
<div className="p-3 flex flex-col gap-2" {...debugProps('pbi-list__items')}>
{filtered.map(pbi => (
<SortablePbiRow
key={pbi.id}
pbi={pbi}
isSelected={selectedPbiId === pbi.id}
isDemo={isDemo}
selectionMode={selectionMode}
isChecked={selectedIds.has(pbi.id)}
onSelect={() => selectPbi(pbi.id)}
onToggleCheck={() => toggleCheck(pbi.id)}
onEdit={() => setDialogState({ mode: 'edit', productId, pbi })}
onDelete={() => handleDelete(pbi.id)}
/>
))}
</div>
</SortableContext>
<DragOverlay>
{activePbi && (
<BacklogCard
title={activePbi.title}
priority={activePbi.priority}
className="border-primary shadow-xl opacity-90"
/>
)}
</DragOverlay>
</DndContext>
)}
</div>
{selectionMode && (
<div className="border-t border-border bg-surface-container px-4 py-2 flex items-center justify-between gap-2 shrink-0">
<span className="text-sm text-foreground">
{selectedIds.size} geselecteerd
</span>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
className="h-7 text-xs"
onClick={exitSelection}
>
Annuleer
</Button>
<Button
size="sm"
className="h-7 text-xs"
disabled={selectedIds.size === 0}
onClick={() => setNewSprintOpen(true)}
>
Nieuwe sprint
</Button>
</div>
</div>
)}
<PbiDialog
state={dialogState}
onClose={() => setDialogState(null)}
isDemo={isDemo}
/>
<NewSprintDialog
open={newSprintOpen}
productId={productId}
pbiIds={Array.from(selectedIds)}
onOpenChange={(open) => {
setNewSprintOpen(open)
if (!open) {
// Sluit selectie bij geslaagde aanmaak; bij annuleren laat de selectie staan
}
}}
onCreated={() => {
setNewSprintOpen(false)
exitSelection()
}}
/>
</div>
)
}