feat(ST-507): show code badges on cards, lists and dialogs across the app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
66063f035a
commit
b71eb53fa8
15 changed files with 122 additions and 38 deletions
|
|
@ -35,6 +35,7 @@ export default async function ProductBacklogPage({ params }: Props) {
|
|||
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
description: true,
|
||||
acceptance_criteria: true,
|
||||
|
|
@ -87,7 +88,7 @@ export default async function ProductBacklogPage({ params }: Props) {
|
|||
left={
|
||||
<PbiList
|
||||
productId={id}
|
||||
pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, title: p.title, priority: p.priority, description: p.description }))}
|
||||
pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, description: p.description }))}
|
||||
isDemo={isDemo}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,14 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
},
|
||||
},
|
||||
include: {
|
||||
story: { select: { id: true, title: true } },
|
||||
story: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ story: { sort_order: 'asc' } },
|
||||
|
|
@ -52,6 +59,7 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
where: { sprint_id: sprint.id, assignee_id: null },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
title: true,
|
||||
tasks: {
|
||||
select: { id: true, title: true, description: true, priority: true, status: true },
|
||||
|
|
@ -62,20 +70,28 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
}),
|
||||
])
|
||||
|
||||
const tasks: SoloTask[] = rawTasks.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
implementation_plan: t.implementation_plan,
|
||||
priority: t.priority,
|
||||
sort_order: t.sort_order,
|
||||
status: t.status as SoloTask['status'],
|
||||
story_id: t.story.id,
|
||||
story_title: t.story.title,
|
||||
}))
|
||||
const tasks: SoloTask[] = rawTasks.map(t => {
|
||||
const positionInStory = t.story.tasks.findIndex(st => st.id === t.id)
|
||||
const taskCode =
|
||||
t.story.code && positionInStory >= 0 ? `${t.story.code}.${positionInStory + 1}` : null
|
||||
return {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
implementation_plan: t.implementation_plan,
|
||||
priority: t.priority,
|
||||
sort_order: t.sort_order,
|
||||
status: t.status as SoloTask['status'],
|
||||
story_id: t.story.id,
|
||||
story_code: t.story.code,
|
||||
story_title: t.story.title,
|
||||
task_code: taskCode,
|
||||
}
|
||||
})
|
||||
|
||||
const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({
|
||||
id: s.id,
|
||||
code: s.code,
|
||||
title: s.title,
|
||||
tasks: s.tasks.map(t => ({
|
||||
id: t.id,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export default async function SprintBoardPage({ params }: Props) {
|
|||
|
||||
const sprintStoryItems: SprintStory[] = sprintStories.map(s => ({
|
||||
id: s.id,
|
||||
code: s.code,
|
||||
title: s.title,
|
||||
priority: s.priority,
|
||||
status: s.status,
|
||||
|
|
@ -86,9 +87,11 @@ export default async function SprintBoardPage({ params }: Props) {
|
|||
.filter(pbi => pbi.stories.length > 0)
|
||||
.map(pbi => ({
|
||||
id: pbi.id,
|
||||
code: pbi.code,
|
||||
title: pbi.title,
|
||||
stories: pbi.stories.map(s => ({
|
||||
id: s.id,
|
||||
code: s.code,
|
||||
title: s.title,
|
||||
priority: s.priority,
|
||||
status: s.status,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { forwardRef } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
|
||||
export const PRIORITY_BORDER: Record<number, string> = {
|
||||
1: 'border-l-4 border-l-priority-critical',
|
||||
|
|
@ -13,6 +14,7 @@ export const PRIORITY_BORDER: Record<number, string> = {
|
|||
interface BacklogCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title: string
|
||||
priority: number
|
||||
code?: string | null
|
||||
isSelected?: boolean
|
||||
isDragging?: boolean
|
||||
badge?: React.ReactNode
|
||||
|
|
@ -20,7 +22,7 @@ interface BacklogCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||
}
|
||||
|
||||
export const BacklogCard = forwardRef<HTMLDivElement, BacklogCardProps>(function BacklogCard(
|
||||
{ title, priority, isSelected, isDragging, badge, actions, className, ...rest },
|
||||
{ title, priority, code, isSelected, isDragging, badge, actions, className, ...rest },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
|
|
@ -37,7 +39,10 @@ export const BacklogCard = forwardRef<HTMLDivElement, BacklogCardProps>(function
|
|||
)}
|
||||
{...rest}
|
||||
>
|
||||
<p className="text-sm leading-snug line-clamp-2">{title}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm leading-snug line-clamp-2 flex-1">{title}</p>
|
||||
{code && <CodeBadge code={code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
{(badge || actions) && (
|
||||
<div className="flex items-center justify-between gap-1 mt-1.5">
|
||||
<div className="flex items-center gap-1">{badge}</div>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ const PRIORITY_COLORS: Record<number, string> = {
|
|||
|
||||
interface Pbi {
|
||||
id: string
|
||||
code: string | null
|
||||
title: string
|
||||
priority: number
|
||||
description?: string | null
|
||||
|
|
@ -92,6 +93,7 @@ function SortablePbiRow({
|
|||
{...attributes}
|
||||
{...listeners}
|
||||
title={pbi.title}
|
||||
code={pbi.code}
|
||||
priority={pbi.priority}
|
||||
isSelected={isSelected}
|
||||
isDragging={isDragging}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const STATUS_LABELS: Record<string, string> = {
|
|||
|
||||
export interface Story {
|
||||
id: string
|
||||
code: string | null
|
||||
title: string
|
||||
description: string | null
|
||||
acceptance_criteria: string | null
|
||||
|
|
@ -91,6 +92,7 @@ function SortableStoryBlock({
|
|||
{...attributes}
|
||||
{...listeners}
|
||||
title={story.title}
|
||||
code={story.code}
|
||||
priority={story.priority}
|
||||
isDragging={isDragging}
|
||||
onClick={onClick}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import { useRouter } from 'next/navigation'
|
|||
import { useTransition } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import { restoreProductAction } from '@/actions/products'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
code: string | null
|
||||
description: string | null
|
||||
repo_url: string | null
|
||||
}
|
||||
|
|
@ -61,9 +63,12 @@ export function ProductList({ products, isDemo, showArchived = false }: ProductL
|
|||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-foreground group-hover:text-primary transition-colors truncate">
|
||||
{product.name}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{product.code && <CodeBadge code={product.code} />}
|
||||
<p className="font-medium text-foreground group-hover:text-primary transition-colors truncate">
|
||||
{product.name}
|
||||
</p>
|
||||
</div>
|
||||
{product.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-1">
|
||||
{product.description.slice(0, 80)}{product.description.length > 80 ? '…' : ''}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ export interface SoloTask {
|
|||
sort_order: number
|
||||
status: 'TO_DO' | 'IN_PROGRESS' | 'REVIEW' | 'DONE'
|
||||
story_id: string
|
||||
story_code: string | null
|
||||
story_title: string
|
||||
task_code: string | null
|
||||
}
|
||||
|
||||
export interface SoloBoardProps {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useDraggable } from '@dnd-kit/core'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import type { SoloTask } from './solo-board'
|
||||
|
||||
const PRIORITY_BORDER: Record<number, string> = {
|
||||
|
|
@ -39,8 +40,14 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
|
|||
)}
|
||||
{...(!isDemo ? { ...attributes, ...listeners } : {})}
|
||||
>
|
||||
<p className="text-sm text-foreground leading-snug">{task.title}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 truncate">{task.story_title}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm text-foreground leading-snug flex-1">{task.title}</p>
|
||||
{task.task_code && <CodeBadge code={task.task_code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 truncate">
|
||||
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
|
||||
{task.story_title}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -53,8 +60,14 @@ export function SoloTaskCardOverlay({ task }: { task: SoloTask }) {
|
|||
PRIORITY_BORDER[task.priority],
|
||||
)}
|
||||
>
|
||||
<p className="text-sm text-foreground leading-snug">{task.title}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 truncate">{task.story_title}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm text-foreground leading-snug flex-1">{task.title}</p>
|
||||
{task.task_code && <CodeBadge code={task.task_code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 truncate">
|
||||
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
|
||||
{task.story_title}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,11 +77,19 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
<DialogTitle className="text-sm font-medium leading-snug flex-1">
|
||||
{task.title}
|
||||
</DialogTitle>
|
||||
{task.task_code && (
|
||||
<span className="font-mono text-[11px] text-muted-foreground border border-border rounded-md bg-surface-container px-1.5 py-0.5 shrink-0">
|
||||
{task.task_code}
|
||||
</span>
|
||||
)}
|
||||
<Badge className={cn('text-xs border shrink-0', STATUS_COLORS[task.status])}>
|
||||
{STATUS_LABELS[task.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{task.story_title}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
|
||||
{task.story_title}
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
{task.description && (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Sheet, SheetContent, SheetHeader, SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import { claimStoryAction } from '@/actions/stories'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export interface UnassignedStoryTask {
|
|||
|
||||
export interface UnassignedStory {
|
||||
id: string
|
||||
code: string | null
|
||||
title: string
|
||||
tasks: UnassignedStoryTask[]
|
||||
}
|
||||
|
|
@ -119,7 +121,10 @@ function ClaimStoryRow({
|
|||
<div className="rounded-lg border border-border bg-surface-container overflow-hidden">
|
||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">{story.title}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm font-medium text-foreground truncate flex-1">{story.title}</p>
|
||||
{story.code && <CodeBadge code={story.code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{story.tasks.length} {story.tasks.length === 1 ? 'taak' : 'taken'}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-
|
|||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { toast } from 'sonner'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import {
|
||||
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent,
|
||||
|
|
@ -35,6 +36,7 @@ const PRIORITY_LABELS: Record<number, string> = { 1: 'Kritiek', 2: 'Hoog', 3: 'G
|
|||
|
||||
export interface SprintStory {
|
||||
id: string
|
||||
code: string | null
|
||||
title: string
|
||||
priority: number
|
||||
status: string
|
||||
|
|
@ -51,6 +53,7 @@ export interface ProductMember {
|
|||
|
||||
export interface PbiWithStories {
|
||||
id: string
|
||||
code: string | null
|
||||
title: string
|
||||
stories: SprintStory[]
|
||||
}
|
||||
|
|
@ -140,7 +143,10 @@ function SortableSprintRow({
|
|||
</span>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm truncate">{story.title}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm truncate flex-1">{story.title}</p>
|
||||
{story.code && <CodeBadge code={story.code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<Badge className={cn('text-[10px] px-1.5 py-0 border', PRIORITY_COLORS[story.priority])}>
|
||||
{PRIORITY_LABELS[story.priority]}
|
||||
|
|
@ -338,7 +344,10 @@ function DraggablePbiStoryRow({
|
|||
</span>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm truncate">{story.title}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm truncate flex-1">{story.title}</p>
|
||||
{story.code && <CodeBadge code={story.code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<Badge className={cn('text-[10px] px-1.5 py-0 border mt-0.5', STATUS_COLORS[story.status])}>
|
||||
{STATUS_LABELS[story.status]}
|
||||
</Badge>
|
||||
|
|
@ -387,6 +396,7 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on
|
|||
>
|
||||
<span className="text-xs">{collapsed.has(pbi.id) ? '▶' : '▼'}</span>
|
||||
<span className="text-sm font-medium truncate flex-1">{pbi.title}</span>
|
||||
{pbi.code && <CodeBadge code={pbi.code} />}
|
||||
<span className="text-xs text-muted-foreground">{pbi.stories.length}</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -400,7 +410,10 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on
|
|||
>
|
||||
<div className="w-[14px] shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm truncate">{story.title}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm truncate flex-1">{story.title}</p>
|
||||
{story.code && <CodeBadge code={story.code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<Badge className={cn('text-[10px] px-1.5 py-0 border mt-0.5', STATUS_COLORS[story.status])}>
|
||||
{STATUS_LABELS[story.status]}
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ export function SprintBoardClient({
|
|||
selectedStoryId ? (
|
||||
<TaskList
|
||||
storyId={selectedStoryId}
|
||||
storyCode={stories.find(s => s.id === selectedStoryId)?.code ?? null}
|
||||
sprintId={sprintId}
|
||||
productId={productId}
|
||||
tasks={selectedTasks}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
|
|||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{sprintStories.map(story => (
|
||||
<div key={story.id} className="flex items-center justify-between gap-3 p-2 bg-surface-container-low rounded-lg">
|
||||
{story.code && <span className="font-mono text-[11px] text-muted-foreground shrink-0">{story.code}</span>}
|
||||
<span className="text-sm truncate flex-1">{story.title}</span>
|
||||
<div className="flex gap-1 shrink-0">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import { toast } from 'sonner'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import { PanelNavBar } from '@/components/shared/panel-nav-bar'
|
||||
import { deriveTaskCode } from '@/lib/code'
|
||||
import { useSprintStore } from '@/stores/sprint-store'
|
||||
import {
|
||||
createTaskAction, updateTaskStatusAction, updateTaskAction,
|
||||
|
|
@ -49,6 +51,7 @@ export interface Task {
|
|||
|
||||
interface TaskListProps {
|
||||
storyId: string
|
||||
storyCode: string | null
|
||||
sprintId: string
|
||||
productId: string
|
||||
tasks: Task[]
|
||||
|
|
@ -56,8 +59,8 @@ interface TaskListProps {
|
|||
}
|
||||
|
||||
function SortableTaskRow({
|
||||
task, isDemo, onStatusToggle, onDelete,
|
||||
}: { task: Task; isDemo: boolean; onStatusToggle: () => void; onDelete: () => void }) {
|
||||
task, code, isDemo, onStatusToggle, onDelete,
|
||||
}: { task: Task; code: string | null; isDemo: boolean; onStatusToggle: () => void; onDelete: () => void }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id })
|
||||
const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 }
|
||||
|
|
@ -88,23 +91,26 @@ function SortableTaskRow({
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="group flex items-center gap-3 px-4 py-2.5 border-b border-border hover:bg-surface-container/50 transition-colors">
|
||||
<div ref={setNodeRef} style={style} className="group flex items-start gap-3 px-4 py-2.5 border-b border-border hover:bg-surface-container/50 transition-colors">
|
||||
{!isDemo && (
|
||||
<span {...attributes} {...listeners} className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 text-sm select-none">⠿</span>
|
||||
<span {...attributes} {...listeners} className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 text-sm select-none mt-0.5">⠿</span>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={cn('text-sm truncate', task.status === 'DONE' && 'line-through text-muted-foreground')}>
|
||||
{task.title}
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className={cn('text-sm truncate flex-1', task.status === 'DONE' && 'line-through text-muted-foreground')}>
|
||||
{task.title}
|
||||
</p>
|
||||
{code && <CodeBadge code={code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{PRIORITY_LABELS[task.priority]}</span>
|
||||
</div>
|
||||
<button onClick={onStatusToggle} disabled={isDemo} aria-label={`Status: ${STATUS_LABELS[task.status]}`}>
|
||||
<button onClick={onStatusToggle} disabled={isDemo} aria-label={`Status: ${STATUS_LABELS[task.status]}`} className="mt-0.5">
|
||||
<Badge className={cn('text-xs border cursor-pointer hover:opacity-80 transition-opacity', STATUS_COLORS[task.status])}>
|
||||
{STATUS_LABELS[task.status]}
|
||||
</Badge>
|
||||
</button>
|
||||
{!isDemo && (
|
||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
|
||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1 mt-0.5">
|
||||
<button onClick={() => setEditing(true)} className="text-xs text-muted-foreground hover:text-foreground">Bewerk</button>
|
||||
<button onClick={onDelete} aria-label="Verwijder taak" className="text-xs text-muted-foreground hover:text-error">×</button>
|
||||
</div>
|
||||
|
|
@ -150,7 +156,7 @@ function CreateSubmitButton() {
|
|||
return <Button type="submit" size="sm" className="h-7" disabled={pending}>{pending ? '…' : 'Toevoegen'}</Button>
|
||||
}
|
||||
|
||||
export function TaskList({ storyId, sprintId, productId: _productId, tasks, isDemo }: TaskListProps) {
|
||||
export function TaskList({ storyId, storyCode, sprintId, productId: _productId, tasks, isDemo }: TaskListProps) {
|
||||
const { taskOrder, initTasks, reorderTasks, rollbackTasks } = useSprintStore()
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [activeDragId, setActiveDragId] = useState<string | null>(null)
|
||||
|
|
@ -232,10 +238,11 @@ export function TaskList({ storyId, sprintId, productId: _productId, tasks, isDe
|
|||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={orderedTasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
|
||||
{orderedTasks.map(task => (
|
||||
{orderedTasks.map((task, idx) => (
|
||||
<SortableTaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
code={deriveTaskCode(storyCode, idx + 1)}
|
||||
isDemo={isDemo}
|
||||
onStatusToggle={() => handleStatusToggle(task)}
|
||||
onDelete={() => handleDelete(task.id)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue