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 <button> in de actions-slot met stopPropagation. Toggle gedrag (bulk add/remove in B, upsertPbiIntent in A′) blijft ongewijzigd via die knop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
583 lines
20 KiB
TypeScript
583 lines
20 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useTransition } 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, MinusSquare, Square } from 'lucide-react'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import {
|
||
BacklogFilterPopover,
|
||
PRIORITY_LABELS,
|
||
type SortDir,
|
||
} from '@/components/shared/backlog-filter-popover'
|
||
import { useShallow } from 'zustand/react/shallow'
|
||
import { useUserSettingsStore } from '@/stores/user-settings/store'
|
||
import { useProductWorkspaceStore } from '@/stores/product-workspace/store'
|
||
import {
|
||
selectPbiTriState,
|
||
selectVisiblePbis,
|
||
type PbiTriState,
|
||
} from '@/stores/product-workspace/selectors'
|
||
import type { BacklogPbi as WorkspacePbi } from '@/stores/product-workspace/types'
|
||
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 { 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'
|
||
|
||
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' },
|
||
]
|
||
|
||
type PbiStatusFilter = PbiStatusApi | 'all'
|
||
|
||
const STATUS_OPTIONS: Array<{ value: PbiStatusFilter; label: string }> = [
|
||
{ value: 'all', label: 'Alle' },
|
||
{ value: 'ready', label: 'Klaar' },
|
||
{ value: 'blocked', label: 'Geblokkeerd' },
|
||
{ value: 'done', label: 'Afgerond' },
|
||
]
|
||
|
||
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
|
||
activeSprintId?: string | null
|
||
}
|
||
|
||
// --- Sortable PBI row ---
|
||
function TriStateIcon({ state }: { state: PbiTriState }) {
|
||
if (state === 'full')
|
||
return <CheckSquare size={18} className="text-primary" />
|
||
if (state === 'partial')
|
||
return <MinusSquare size={18} className="text-primary" />
|
||
return <Square size={18} />
|
||
}
|
||
|
||
function SortablePbiRow({
|
||
pbi,
|
||
isSelected,
|
||
isDemo,
|
||
selectionMode,
|
||
triState,
|
||
onSelect,
|
||
onToggleCheck,
|
||
onEdit,
|
||
onDelete,
|
||
}: {
|
||
pbi: Pbi
|
||
isSelected: boolean
|
||
isDemo: boolean
|
||
selectionMode: boolean
|
||
triState: PbiTriState
|
||
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={isSelected}
|
||
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={
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onToggleCheck()
|
||
}}
|
||
aria-pressed={triState !== 'empty'}
|
||
aria-label={
|
||
triState === 'full'
|
||
? 'Stories uit sprint halen'
|
||
: 'Stories aan sprint toevoegen'
|
||
}
|
||
className="inline-flex items-center justify-center min-h-7 min-w-7 text-muted-foreground hover:text-foreground rounded transition-colors"
|
||
>
|
||
<TriStateIcon state={triState} />
|
||
</button>
|
||
}
|
||
/>
|
||
)
|
||
}
|
||
|
||
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 ---
|
||
// PBI-74 / T-849: leest pbis + actieve selectie uit workspace-store via
|
||
// useShallow-selector. DnD-mutaties via applyOptimisticMutation/rollback/settle.
|
||
export function PbiList({ productId, isDemo, activeSprintId = null }: PbiListProps) {
|
||
// selectVisiblePbis is gesorteerd op priority/sort_order; useShallow
|
||
// voorkomt re-render op ongerelateerde store-mutaties (G2).
|
||
const pbis = useProductWorkspaceStore(useShallow(selectVisiblePbis)) as WorkspacePbi[]
|
||
const selectedPbiId = useProductWorkspaceStore((s) => s.context.activePbiId)
|
||
const prefs = useUserSettingsStore(
|
||
useShallow((s) => s.entities.settings.views?.pbiList ?? {}),
|
||
)
|
||
const setPref = useUserSettingsStore((s) => s.setPref)
|
||
const filterPriority = prefs.filterPriority ?? 'all'
|
||
const filterStatus: PbiStatusFilter = prefs.filterStatus ?? 'all'
|
||
const sortMode: SortMode = prefs.sort ?? 'priority'
|
||
const sortDir: SortDir = prefs.sortDir ?? 'asc'
|
||
const setFilterPriority = (v: number | 'all') =>
|
||
void setPref(['views', 'pbiList', 'filterPriority'], v)
|
||
const setFilterStatus = (v: PbiStatusFilter) =>
|
||
void setPref(['views', 'pbiList', 'filterStatus'], v)
|
||
const setSortMode = (v: SortMode) => void setPref(['views', 'pbiList', 'sort'], v)
|
||
const setSortDir = (v: SortDir) => void setPref(['views', 'pbiList', 'sortDir'], v)
|
||
const [filterPopoverOpen, setFilterPopoverOpen] = useState(false)
|
||
const [dialogState, setDialogState] = useState<PbiDialogState | null>(null)
|
||
const [activeDragId, setActiveDragId] = useState<string | null>(null)
|
||
const [, startTransition] = useTransition()
|
||
|
||
// PBI-79 / ST-1337+ST-1338: selectionMode is afgeleid uit drie staten:
|
||
// A′ (pendingSprintDraft) → vinkjes muteren de draft via upsertPbiIntent.
|
||
// B (activeSprintId zonder draft) → vinkjes muteren de membership-buffer
|
||
// via toggleStorySprintMembership per child story (bulk).
|
||
// A (geen sprint, geen draft) → geen vinkjes.
|
||
const hasDraft = useUserSettingsStore(
|
||
(s) => !!s.entities.settings.workflow?.pendingSprintDraft?.[productId],
|
||
)
|
||
const upsertPbiIntent = useUserSettingsStore((s) => s.upsertPbiIntent)
|
||
const toggleStorySprintMembership = useProductWorkspaceStore(
|
||
(s) => s.toggleStorySprintMembership,
|
||
)
|
||
const stateBMode = !hasDraft && !!activeSprintId
|
||
const selectionMode = hasDraft || stateBMode
|
||
|
||
function togglePbiInDraft(id: string, currentState: PbiTriState) {
|
||
if (hasDraft) {
|
||
// A′: empty/partial → all; full → none.
|
||
const nextIntent = currentState === 'full' ? 'none' : 'all'
|
||
void upsertPbiIntent(productId, id, nextIntent)
|
||
return
|
||
}
|
||
if (stateBMode && activeSprintId) {
|
||
// State B: bulk-toggle alle child-stories naar/uit de pending buffer.
|
||
const store = useProductWorkspaceStore.getState()
|
||
const storyIds = store.relations.storyIdsByPbi[id] ?? []
|
||
const goingFull = currentState !== 'full'
|
||
for (const storyId of storyIds) {
|
||
const story = store.entities.storiesById[storyId]
|
||
if (!story) continue
|
||
const blocked = store.sprintMembership.crossSprintBlocks[storyId]
|
||
if (blocked) continue
|
||
const inSprint = story.sprint_id === activeSprintId
|
||
if (goingFull && !inSprint) {
|
||
toggleStorySprintMembership(storyId, false)
|
||
}
|
||
if (!goingFull && inSprint) {
|
||
toggleStorySprintMembership(storyId, true)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// pbis komen al gesorteerd binnen via selectVisiblePbis (priority + sort_order).
|
||
// Geen aparte order/priority maps meer — workspace-store entities zijn de waarheid.
|
||
const pbiMap = Object.fromEntries(pbis.map(p => [p.id, p]))
|
||
const orderedPbis = pbis
|
||
|
||
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) => {
|
||
let cmp = 0
|
||
if (sortMode === 'code') {
|
||
cmp = (a.code ?? '').localeCompare(b.code ?? '', 'nl', { numeric: true })
|
||
} else if (sortMode === 'date') {
|
||
cmp = new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||
} else {
|
||
cmp = a.priority !== b.priority ? a.priority - b.priority : 0
|
||
}
|
||
return sortDir === 'desc' ? -cmp : cmp
|
||
})
|
||
|
||
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 store = useProductWorkspaceStore.getState()
|
||
const prevOrder = [...store.relations.pbiIds]
|
||
const oldIndex = prevOrder.indexOf(active.id as string)
|
||
const newIndex = prevOrder.indexOf(over.id as string)
|
||
if (oldIndex === -1 || newIndex === -1) return
|
||
const newOrder = arrayMove([...prevOrder], oldIndex, newIndex)
|
||
|
||
// Snapshot rollback-info en pas optimistisch toe.
|
||
const orderMutationId = store.applyOptimisticMutation({
|
||
kind: 'pbi-order',
|
||
prevPbiIds: prevOrder,
|
||
})
|
||
useProductWorkspaceStore.setState((s) => {
|
||
s.relations.pbiIds = newOrder
|
||
})
|
||
|
||
const priorityChanged = activePbi.priority !== overPbi.priority
|
||
let priorityMutationId: string | null = null
|
||
if (priorityChanged) {
|
||
priorityMutationId = store.applyOptimisticMutation({
|
||
kind: 'entity-patch',
|
||
entity: 'pbi',
|
||
id: active.id as string,
|
||
prev: store.entities.pbisById[active.id as string],
|
||
})
|
||
useProductWorkspaceStore.setState((s) => {
|
||
const pbi = s.entities.pbisById[active.id as string]
|
||
if (pbi) pbi.priority = overPbi.priority
|
||
})
|
||
}
|
||
|
||
startTransition(async () => {
|
||
const settle = () => {
|
||
const st = useProductWorkspaceStore.getState()
|
||
if (priorityMutationId) st.settleMutation(priorityMutationId)
|
||
st.settleMutation(orderMutationId)
|
||
}
|
||
const rollback = (msg: string) => {
|
||
const st = useProductWorkspaceStore.getState()
|
||
if (priorityMutationId) st.rollbackMutation(priorityMutationId)
|
||
st.rollbackMutation(orderMutationId)
|
||
toast.error(msg)
|
||
}
|
||
|
||
if (priorityChanged) {
|
||
const result = await updatePbiPriorityAction(active.id as string, overPbi.priority, productId)
|
||
if (result.success) settle()
|
||
else rollback('Prioriteit opslaan mislukt')
|
||
} else {
|
||
const result = await reorderPbisAction(productId, newOrder)
|
||
if (result.success) settle()
|
||
else rollback('Volgorde opslaan mislukt')
|
||
}
|
||
})
|
||
}
|
||
|
||
function handleDelete(id: string) {
|
||
startTransition(async () => {
|
||
await deletePbiAction(id)
|
||
if (selectedPbiId === id) {
|
||
useProductWorkspaceStore.getState().setActivePbi(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>
|
||
)}
|
||
<BacklogFilterPopover
|
||
open={filterPopoverOpen}
|
||
onOpenChange={setFilterPopoverOpen}
|
||
filterPriority={filterPriority}
|
||
onFilterPriorityChange={setFilterPriority}
|
||
filterStatus={filterStatus}
|
||
onFilterStatusChange={setFilterStatus}
|
||
statusOptions={STATUS_OPTIONS}
|
||
sort={sortMode}
|
||
onSortChange={setSortMode}
|
||
sortDir={sortDir}
|
||
onSortDirChange={setSortDir}
|
||
sortOptions={SORT_OPTIONS}
|
||
activeFilterCount={activeFilterCount}
|
||
resetDisabled={activeFilterCount === 0}
|
||
onReset={() => {
|
||
setFilterPriority('all')
|
||
setFilterStatus('all')
|
||
setSortMode('priority')
|
||
setSortDir('asc')
|
||
}}
|
||
/>
|
||
<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 => (
|
||
<SortablePbiRowWithTriState
|
||
key={pbi.id}
|
||
pbi={pbi}
|
||
isSelected={selectedPbiId === pbi.id}
|
||
isDemo={isDemo}
|
||
selectionMode={selectionMode}
|
||
productId={productId}
|
||
onSelect={() => useProductWorkspaceStore.getState().setActivePbi(pbi.id)}
|
||
onToggle={togglePbiInDraft}
|
||
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>
|
||
|
||
<PbiDialog
|
||
state={dialogState}
|
||
onClose={() => setDialogState(null)}
|
||
isDemo={isDemo}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// PBI-79 / ST-1337: wrapper rond SortablePbiRow die zijn tri-state uit de
|
||
// workspace-store leest. Subscribed per PBI zodat alleen de relevante rij
|
||
// re-rendert bij pbiIntent/storyOverrides-mutaties.
|
||
function SortablePbiRowWithTriState({
|
||
pbi,
|
||
isSelected,
|
||
isDemo,
|
||
selectionMode,
|
||
productId,
|
||
onSelect,
|
||
onToggle,
|
||
onEdit,
|
||
onDelete,
|
||
}: {
|
||
pbi: Pbi
|
||
isSelected: boolean
|
||
isDemo: boolean
|
||
selectionMode: boolean
|
||
productId: string
|
||
onSelect: () => void
|
||
onToggle: (id: string, currentState: PbiTriState) => void
|
||
onEdit: () => void
|
||
onDelete: () => void
|
||
}) {
|
||
// Tri-state uit pendingSprintDraft (state A′) of pbiSummary (state B).
|
||
// Wanneer geen draft: leid af van pbiSummary; wanneer wel: uit pbiIntent.
|
||
const triState = useUserSettingsStore((s) => {
|
||
const draft = s.entities.settings.workflow?.pendingSprintDraft?.[productId]
|
||
if (draft) {
|
||
const intent = draft.pbiIntent[pbi.id] ?? 'none'
|
||
const override = draft.storyOverrides[pbi.id]
|
||
if (intent === 'all') {
|
||
if (override?.remove.length) return 'partial'
|
||
return 'full'
|
||
}
|
||
if (override?.add.length) return 'partial'
|
||
return 'empty'
|
||
}
|
||
return null
|
||
})
|
||
const summaryTriState = useProductWorkspaceStore((s) =>
|
||
selectPbiTriState(s, pbi.id),
|
||
)
|
||
const effectiveTriState: PbiTriState =
|
||
triState ?? (selectionMode ? summaryTriState : 'empty')
|
||
|
||
return (
|
||
<SortablePbiRow
|
||
pbi={pbi}
|
||
isSelected={isSelected}
|
||
isDemo={isDemo}
|
||
selectionMode={selectionMode}
|
||
triState={effectiveTriState}
|
||
onSelect={onSelect}
|
||
onToggleCheck={() => onToggle(pbi.id, effectiveTriState)}
|
||
onEdit={onEdit}
|
||
onDelete={onDelete}
|
||
/>
|
||
)
|
||
}
|