From 0b84352b094fc524c6f49136e819d68bd3a161ae Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sat, 7 Mar 2026 17:42:27 +0800 Subject: [PATCH] Fix KanbanBoard: actually wire TouchSensor into useSensors The import was added but the sensors config replacement failed silently due to line ending mismatch. TouchSensor now properly registered with 200ms delay / 5px tolerance alongside PointerSensor. Co-Authored-By: Claude Opus 4.6 --- .../src/components/projects/KanbanBoard.tsx | 435 +++++++++--------- 1 file changed, 218 insertions(+), 217 deletions(-) diff --git a/frontend/src/components/projects/KanbanBoard.tsx b/frontend/src/components/projects/KanbanBoard.tsx index 91b258c..a27a3df 100644 --- a/frontend/src/components/projects/KanbanBoard.tsx +++ b/frontend/src/components/projects/KanbanBoard.tsx @@ -1,218 +1,219 @@ -import { - DndContext, - closestCorners, +import { + DndContext, + closestCorners, PointerSensor, - TouchSensor, - useSensor, - useSensors, - type DragEndEvent, - useDroppable, - useDraggable, -} from '@dnd-kit/core'; -import { format, parseISO } from 'date-fns'; -import type { ProjectTask } from '@/types'; -import { Badge } from '@/components/ui/badge'; - -const COLUMNS: { id: string; label: string; color: string }[] = [ - { id: 'pending', label: 'Pending', color: 'text-gray-400' }, - { id: 'in_progress', label: 'In Progress', color: 'text-blue-400' }, - { id: 'blocked', label: 'Blocked', color: 'text-red-400' }, - { id: 'on_hold', label: 'On Hold', color: 'text-orange-400' }, - { id: 'review', label: 'Review', color: 'text-yellow-400' }, - { id: 'completed', label: 'Completed', color: 'text-green-400' }, -]; - -const priorityColors: Record = { - none: 'bg-gray-500/20 text-gray-400', - low: 'bg-green-500/20 text-green-400', - medium: 'bg-yellow-500/20 text-yellow-400', - high: 'bg-red-500/20 text-red-400', -}; - -interface KanbanBoardProps { - tasks: ProjectTask[]; - selectedTaskId: number | null; - kanbanParentTask?: ProjectTask | null; - onSelectTask: (taskId: number) => void; - onStatusChange: (taskId: number, status: string) => void; - onBackToAllTasks?: () => void; -} - -function KanbanColumn({ - column, - tasks, - selectedTaskId, - onSelectTask, -}: { - column: (typeof COLUMNS)[0]; - tasks: ProjectTask[]; - selectedTaskId: number | null; - onSelectTask: (taskId: number) => void; -}) { - const { setNodeRef, isOver } = useDroppable({ id: column.id }); - - return ( -
- {/* Column header */} -
-
- - {column.label} - - - {tasks.length} - -
-
- - {/* Cards */} -
- {tasks.map((task) => ( - onSelectTask(task.id)} - /> - ))} -
-
- ); -} - -function KanbanCard({ - task, - isSelected, - onSelect, -}: { - task: ProjectTask; - isSelected: boolean; - onSelect: () => void; -}) { - const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ - id: task.id, - data: { task }, - }); - - const style = transform - ? { - transform: `translate(${transform.x}px, ${transform.y}px)`, - opacity: isDragging ? 0.5 : 1, - } - : undefined; - - const completedSubtasks = task.subtasks?.filter((s) => s.status === 'completed').length ?? 0; - const totalSubtasks = task.subtasks?.length ?? 0; - - return ( -
-

{task.title}

-
- - {task.priority} - - {task.due_date && ( - - {format(parseISO(task.due_date), 'MMM d')} - - )} - {totalSubtasks > 0 && ( - - {completedSubtasks}/{totalSubtasks} - - )} -
-
- ); -} - -export default function KanbanBoard({ - tasks, - selectedTaskId, - kanbanParentTask, - onSelectTask, - onStatusChange, - onBackToAllTasks, -}: KanbanBoardProps) { - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) - ); - - // Subtask view is driven by kanbanParentTask (decoupled from selected task) - const isSubtaskView = kanbanParentTask != null && (kanbanParentTask.subtasks?.length ?? 0) > 0; - const activeTasks: ProjectTask[] = isSubtaskView ? (kanbanParentTask.subtasks ?? []) : tasks; - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - if (!over) return; - - const taskId = active.id as number; - const newStatus = over.id as string; - - const task = activeTasks.find((t) => t.id === taskId); - if (task && task.status !== newStatus && COLUMNS.some((c) => c.id === newStatus)) { - onStatusChange(taskId, newStatus); - } - }; - - const tasksByStatus = COLUMNS.map((col) => ({ - column: col, - tasks: activeTasks.filter((t) => t.status === col.id), - })); - - return ( -
- {/* Subtask view header */} - {isSubtaskView && kanbanParentTask && ( -
- - / - - Subtasks of: {kanbanParentTask.title} - -
- )} - - -
- {tasksByStatus.map(({ column, tasks: colTasks }) => ( - - ))} -
-
-
- ); -} + TouchSensor, + useSensor, + useSensors, + type DragEndEvent, + useDroppable, + useDraggable, +} from '@dnd-kit/core'; +import { format, parseISO } from 'date-fns'; +import type { ProjectTask } from '@/types'; +import { Badge } from '@/components/ui/badge'; + +const COLUMNS: { id: string; label: string; color: string }[] = [ + { id: 'pending', label: 'Pending', color: 'text-gray-400' }, + { id: 'in_progress', label: 'In Progress', color: 'text-blue-400' }, + { id: 'blocked', label: 'Blocked', color: 'text-red-400' }, + { id: 'on_hold', label: 'On Hold', color: 'text-orange-400' }, + { id: 'review', label: 'Review', color: 'text-yellow-400' }, + { id: 'completed', label: 'Completed', color: 'text-green-400' }, +]; + +const priorityColors: Record = { + none: 'bg-gray-500/20 text-gray-400', + low: 'bg-green-500/20 text-green-400', + medium: 'bg-yellow-500/20 text-yellow-400', + high: 'bg-red-500/20 text-red-400', +}; + +interface KanbanBoardProps { + tasks: ProjectTask[]; + selectedTaskId: number | null; + kanbanParentTask?: ProjectTask | null; + onSelectTask: (taskId: number) => void; + onStatusChange: (taskId: number, status: string) => void; + onBackToAllTasks?: () => void; +} + +function KanbanColumn({ + column, + tasks, + selectedTaskId, + onSelectTask, +}: { + column: (typeof COLUMNS)[0]; + tasks: ProjectTask[]; + selectedTaskId: number | null; + onSelectTask: (taskId: number) => void; +}) { + const { setNodeRef, isOver } = useDroppable({ id: column.id }); + + return ( +
+ {/* Column header */} +
+
+ + {column.label} + + + {tasks.length} + +
+
+ + {/* Cards */} +
+ {tasks.map((task) => ( + onSelectTask(task.id)} + /> + ))} +
+
+ ); +} + +function KanbanCard({ + task, + isSelected, + onSelect, +}: { + task: ProjectTask; + isSelected: boolean; + onSelect: () => void; +}) { + const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ + id: task.id, + data: { task }, + }); + + const style = transform + ? { + transform: `translate(${transform.x}px, ${transform.y}px)`, + opacity: isDragging ? 0.5 : 1, + } + : undefined; + + const completedSubtasks = task.subtasks?.filter((s) => s.status === 'completed').length ?? 0; + const totalSubtasks = task.subtasks?.length ?? 0; + + return ( +
+

{task.title}

+
+ + {task.priority} + + {task.due_date && ( + + {format(parseISO(task.due_date), 'MMM d')} + + )} + {totalSubtasks > 0 && ( + + {completedSubtasks}/{totalSubtasks} + + )} +
+
+ ); +} + +export default function KanbanBoard({ + tasks, + selectedTaskId, + kanbanParentTask, + onSelectTask, + onStatusChange, + onBackToAllTasks, +}: KanbanBoardProps) { + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) , + useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }) + ); + + // Subtask view is driven by kanbanParentTask (decoupled from selected task) + const isSubtaskView = kanbanParentTask != null && (kanbanParentTask.subtasks?.length ?? 0) > 0; + const activeTasks: ProjectTask[] = isSubtaskView ? (kanbanParentTask.subtasks ?? []) : tasks; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over) return; + + const taskId = active.id as number; + const newStatus = over.id as string; + + const task = activeTasks.find((t) => t.id === taskId); + if (task && task.status !== newStatus && COLUMNS.some((c) => c.id === newStatus)) { + onStatusChange(taskId, newStatus); + } + }; + + const tasksByStatus = COLUMNS.map((col) => ({ + column: col, + tasks: activeTasks.filter((t) => t.status === col.id), + })); + + return ( +
+ {/* Subtask view header */} + {isSubtaskView && kanbanParentTask && ( +
+ + / + + Subtasks of: {kanbanParentTask.title} + +
+ )} + + +
+ {tasksByStatus.map(({ column, tasks: colTasks }) => ( + + ))} +
+
+
+ ); +}