diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD.tsx new file mode 100644 index 00000000..568c0cab --- /dev/null +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD.tsx @@ -0,0 +1,208 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '@/app/store'; +import { ITaskListGroup } from '@/types/tasks/taskList.types'; +import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; +import './EnhancedKanbanBoard.css'; +import './EnhancedKanbanGroup.css'; +import './EnhancedKanbanTaskCard.css'; + +// Minimal task card for prototype (reuse your styles) +const TaskCard: React.FC<{ + task: IProjectTask; + onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void; + onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void; + onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void; + groupId: string; + isDropIndicator: boolean; +}> = ({ task, onTaskDragStart, onTaskDragOver, onTaskDrop, groupId, isDropIndicator }) => { + const themeMode = useSelector((state: RootState) => state.themeReducer.mode); + const background = themeMode === 'dark' ? '#23272f' : '#fff'; + const color = themeMode === 'dark' ? '#fff' : '#23272f'; + return ( + <> + {isDropIndicator && ( +
+ )} +
onTaskDragStart(e, task.id!, groupId)} + onDragOver={e => onTaskDragOver(e, groupId, -1)} + onDrop={e => onTaskDrop(e, groupId, -1)} + style={{ background, color }} + > +
+
{task.name}
+
{task.task_key}
+
+ {task.assignees?.map(a => a.name).join(', ')} +
+
+
+ + ); +}; + +// Minimal group column for prototype +const KanbanGroup: React.FC<{ + group: ITaskListGroup; + onGroupDragStart: (e: React.DragEvent, groupId: string) => void; + onGroupDragOver: (e: React.DragEvent) => void; + onGroupDrop: (e: React.DragEvent, groupId: string) => void; + onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void; + onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void; + onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void; + hoveredTaskIdx: number | null; + hoveredGroupId: string | null; +}> = ({ group, onGroupDragStart, onGroupDragOver, onGroupDrop, onTaskDragStart, onTaskDragOver, onTaskDrop, hoveredTaskIdx, hoveredGroupId }) => ( +
+
onGroupDragStart(e, group.id)} + onDragOver={onGroupDragOver} + onDrop={e => onGroupDrop(e, group.id)} + > +

{group.name}

+ {group.tasks.length} +
+
onTaskDragOver(e, group.id, 0)} + onDrop={e => onTaskDrop(e, group.id, 0)} + > + {/* Drop indicator at the top of the group */} + {hoveredGroupId === group.id && hoveredTaskIdx === 0 && ( +
+ )} + {group.tasks.map((task, idx) => ( + + + + ))} + {/* Drop indicator at the end of the group */} + {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && ( +
+ )} +
+
+); + +const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => { + // Get initial groups from Redux + const reduxGroups = useSelector((state: RootState) => state.enhancedKanbanReducer.taskGroups); + // Local state for groups/tasks + const [groups, setGroups] = useState([]); + // Drag state + const [draggedGroupId, setDraggedGroupId] = useState(null); + const [draggedTaskId, setDraggedTaskId] = useState(null); + const [draggedTaskGroupId, setDraggedTaskGroupId] = useState(null); + const [hoveredGroupId, setHoveredGroupId] = useState(null); + const [hoveredTaskIdx, setHoveredTaskIdx] = useState(null); + const [dragType, setDragType] = useState<'group' | 'task' | null>(null); + + // Sync local state with Redux on mount or when reduxGroups or projectId change + useEffect(() => { + setGroups(reduxGroups.map(g => ({ ...g, tasks: [...g.tasks] }))); + }, [reduxGroups, projectId]); + + // Group drag handlers + const handleGroupDragStart = (e: React.DragEvent, groupId: string) => { + setDraggedGroupId(groupId); + setDragType('group'); + e.dataTransfer.effectAllowed = 'move'; + }; + const handleGroupDragOver = (e: React.DragEvent) => { + if (dragType !== 'group') return; + e.preventDefault(); + }; + const handleGroupDrop = (e: React.DragEvent, targetGroupId: string) => { + if (dragType !== 'group') return; + e.preventDefault(); + if (!draggedGroupId || draggedGroupId === targetGroupId) return; + const updated = [...groups]; + const fromIdx = updated.findIndex(g => g.id === draggedGroupId); + const [moved] = updated.splice(fromIdx, 1); + const toIdx = updated.findIndex(g => g.id === targetGroupId); + updated.splice(toIdx, 0, moved); + setGroups(updated); + setDraggedGroupId(null); + setDragType(null); + }; + + // Task drag handlers + const handleTaskDragStart = (e: React.DragEvent, taskId: string, groupId: string) => { + setDraggedTaskId(taskId); + setDraggedTaskGroupId(groupId); + setDragType('task'); + e.dataTransfer.effectAllowed = 'move'; + }; + const handleTaskDragOver = (e: React.DragEvent, groupId: string, taskIdx: number) => { + if (dragType !== 'task') return; + e.preventDefault(); + if (draggedTaskId) { + setHoveredGroupId(groupId); + setHoveredTaskIdx(taskIdx); + } + }; + const handleTaskDrop = (e: React.DragEvent, targetGroupId: string, targetTaskIdx: number) => { + if (dragType !== 'task') return; + e.preventDefault(); + if (!draggedTaskId || !draggedTaskGroupId || hoveredGroupId === null || hoveredTaskIdx === null) return; + const updated = [...groups]; + const sourceGroup = updated.find(g => g.id === draggedTaskGroupId); + const targetGroup = updated.find(g => g.id === targetGroupId); + if (!sourceGroup || !targetGroup) return; + // Remove from source + const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId); + if (taskIdx === -1) return; + const [movedTask] = sourceGroup.tasks.splice(taskIdx, 1); + // Insert into target at the correct index + let insertIdx = targetTaskIdx; + if (sourceGroup.id === targetGroup.id && taskIdx < insertIdx) { + insertIdx--; + } + if (insertIdx < 0) insertIdx = 0; + if (insertIdx > targetGroup.tasks.length) insertIdx = targetGroup.tasks.length; + targetGroup.tasks.splice(insertIdx, 0, movedTask); + setGroups(updated); + setDraggedTaskId(null); + setDraggedTaskGroupId(null); + setHoveredGroupId(null); + setHoveredTaskIdx(null); + setDragType(null); + }; + + return ( +
+
+ {groups.map(group => ( + + ))} +
+
+ ); +}; + +export default EnhancedKanbanBoardNativeDnD; \ No newline at end of file diff --git a/worklenz-frontend/src/pages/projects/projectView/enhancedBoard/project-view-enhanced-board.tsx b/worklenz-frontend/src/pages/projects/projectView/enhancedBoard/project-view-enhanced-board.tsx index 704802bd..27db13e4 100644 --- a/worklenz-frontend/src/pages/projects/projectView/enhancedBoard/project-view-enhanced-board.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/enhancedBoard/project-view-enhanced-board.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useAppSelector } from '@/hooks/useAppSelector'; import EnhancedKanbanBoard from '@/components/enhanced-kanban/EnhancedKanbanBoard'; +import EnhancedKanbanBoardNativeDnD from '@/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD'; const ProjectViewEnhancedBoard: React.FC = () => { const { project } = useAppSelector(state => state.projectReducer); @@ -15,7 +16,8 @@ const ProjectViewEnhancedBoard: React.FC = () => { return (
- + {/* */} +
); };