diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx index e8068e07..fcddff24 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD.tsx @@ -20,6 +20,7 @@ import { statusApiService } from '@/api/taskAttributes/status/status.api.service import alertService from '@/services/alerts/alertService'; import logger from '@/utils/errorLogger'; import Skeleton from 'antd/es/skeleton/Skeleton'; +import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'; const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => { const dispatch = useDispatch(); @@ -120,15 +121,19 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project setDragType('task'); e.dataTransfer.effectAllowed = 'move'; }; - const handleTaskDragOver = (e: React.DragEvent, groupId: string, taskIdx: number) => { + const handleTaskDragOver = (e: React.DragEvent, groupId: string, taskIdx: number | null) => { if (dragType !== 'task') return; e.preventDefault(); if (draggedTaskId) { setHoveredGroupId(groupId); - setHoveredTaskIdx(taskIdx); } + if(taskIdx === null) { + setHoveredTaskIdx(0); + }else{ + setHoveredTaskIdx(taskIdx); + }; }; - const handleTaskDrop = (e: React.DragEvent, targetGroupId: string, targetTaskIdx: number) => { + const handleTaskDrop = async (e: React.DragEvent, targetGroupId: string, targetTaskIdx: number | null) => { if (dragType !== 'task') return; e.preventDefault(); if (!draggedTaskId || !draggedTaskGroupId || hoveredGroupId === null || hoveredTaskIdx === null) return; @@ -138,10 +143,23 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project const targetGroup = taskGroups.find(g => g.id === targetGroupId); if (!sourceGroup || !targetGroup) return; + const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId); if (taskIdx === -1) return; const movedTask = sourceGroup.tasks[taskIdx]; + if (groupBy === 'status' && movedTask.id) { + if (sourceGroup.id !== targetGroup.id) { + const canContinue = await checkTaskDependencyStatus(movedTask.id, targetGroupId); + if (!canContinue) { + alertService.error( + 'Task is not completed', + 'Please complete the task dependencies before proceeding' + ); + return; + } + } + } let insertIdx = hoveredTaskIdx; // Handle same group reordering @@ -235,6 +253,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project task: movedTask, team_id: teamId, }); + } setDraggedTaskId(null); diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx index 190d5d84..d5e814ba 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/KanbanGroup.tsx @@ -17,12 +17,12 @@ import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service import { ITaskPhase } from '@/types/tasks/taskPhase.types'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; import { - deleteStatusToggleDrawer, - seletedStatusCategory, + deleteStatusToggleDrawer, + seletedStatusCategory, } from '@/features/projects/status/DeleteStatusSlice'; import { - fetchEnhancedKanbanGroups, - IGroupBy, + fetchEnhancedKanbanGroups, + IGroupBy, } from '@/features/enhanced-kanban/enhanced-kanban.slice'; @@ -32,8 +32,8 @@ interface KanbanGroupProps { 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; + onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void; + onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void; hoveredTaskIdx: number | null; hoveredGroupId: string | null; } @@ -229,324 +229,340 @@ const KanbanGroup: React.FC = memo(({ }, [showDropdown]); return ( -
+
+ {/* Background layer - z-index 0 */}
onGroupDragStart(e, group.id)} - onDragOver={onGroupDragOver} - onDrop={e => onGroupDrop(e, group.id)} - > + onDragOver={e => { e.preventDefault(); onTaskDragOver(e, group.id, null); }} + onDrop={e => { e.preventDefault(); onTaskDrop(e, group.id, null); }} + /> + + {/* Content layer - z-index 1 */} +
setIsHover(true)} - onMouseLeave={() => setIsHover(false)} + className="enhanced-kanban-group-header" + style={{ + backgroundColor: headerBackgroundColor, + }} + draggable + onDragStart={e => onGroupDragStart(e, group.id)} + onDragOver={onGroupDragOver} + onDrop={e => onGroupDrop(e, group.id)} >
{ - e.stopPropagation(); - if ((isProjectManager || isOwnerOrAdmin) && group.name !== t('unmapped')) - setIsEditable(true); - }} - onMouseDown={e => { - e.stopPropagation(); - }} + className="flex items-center justify-between w-full font-semibold rounded-md" + onMouseEnter={() => setIsHover(true)} + onMouseLeave={() => setIsHover(false)} > - {isLoading && ( -
- )} - {isEditable ? ( - { - e.stopPropagation(); - }} - onClick={e => { - e.stopPropagation(); - }} - /> - ) : ( -
{ - e.stopPropagation(); - e.preventDefault(); - }} - onMouseUp={e => { - e.stopPropagation(); - }} - onClick={e => { - e.stopPropagation(); - }} - > - {name} ({group.tasks.length}) -
- )} -
- -
- - - {(isOwnerOrAdmin || isProjectManager) && name !== t('unmapped') && ( -
- - - {showDropdown && ( -
-
- - - {groupBy === IGroupBy.STATUS && statusCategories && ( -
-
- {t('changeCategory')} -
- {statusCategories.map(status => ( - - ))} -
- )} - - {groupBy !== IGroupBy.PRIORITY && ( -
- -
- )} -
-
- )} -
- )} -
-
-
- - {/* Simple Delete Confirmation */} - {showDeleteConfirm && ( -
-
-
-
-
- - - -
-
-

- {t('deleteConfirmationTitle')} -

-
-
-
- - -
+ {name} ({group.tasks.length}) +
+ )} +
+ +
+ + + {(isOwnerOrAdmin || isProjectManager) && name !== t('unmapped') && ( +
+ + + {showDropdown && ( +
+
+ + + {groupBy === IGroupBy.STATUS && statusCategories && ( +
+
+ {t('changeCategory')} +
+ {statusCategories.map(status => ( + + ))} +
+ )} + + {groupBy !== IGroupBy.PRIORITY && ( +
+ +
+ )} +
+
+ )} +
+ )}
- )} -
- {/* Create card at top */} - {showNewCardTop && (isOwnerOrAdmin || isProjectManager) && ( - - )} - {/* If group is empty, render a drop zone */} - {group.tasks.length === 0 && !showNewCardTop && !showNewCardBottom &&( -
{ e.preventDefault(); onTaskDragOver(e, group.id, 0); }} - onDrop={e => { e.preventDefault(); onTaskDrop(e, group.id, 0); }} - > - {/* Drop indicator at the end of the group */} - {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && ( -
-
+ {/* Simple Delete Confirmation */} + {showDeleteConfirm && ( +
+
+
+
+
+ + + +
+
+

+ {t('deleteConfirmationTitle')} +

+
+
+
+ + +
- )} - {(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && ( - - )} -
- ) - } - - - {/* Drop indicator at the top of the group */} - {hoveredGroupId === group.id && hoveredTaskIdx === 0 && ( -
-
+
)} +
+ {/* Create card at top */} + {showNewCardTop && ( + + )} - {group.tasks.map((task, idx) => ( - - ))} + {/* If group is empty, render a drop zone */} + {group.tasks.length === 0 && !showNewCardTop && !showNewCardBottom && ( +
{ e.preventDefault(); onTaskDragOver(e, group.id, 0); }} + onDrop={e => { e.preventDefault(); onTaskDrop(e, group.id, 0); }} + > + {/* Drop indicator at the end of the group */} + {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && ( +
+
+
+ )} + {(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && ( + + )} +
+ ) + } - {/* Create card at bottom */} - {showNewCardBottom && (isOwnerOrAdmin || isProjectManager) && ( - - )} - {/* Footer Add Task Button */} - {(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && group.tasks.length > 0 && ( - - )} + {/* Drop indicator at the top of the group */} + {hoveredGroupId === group.id && hoveredTaskIdx === 0 && ( +
+
+
+ )} - {/* Drop indicator at the end of the group */} - {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && ( -
-
-
- )} + {group.tasks.map((task, idx) => ( + + ))} + + {/* Create card at bottom */} + {showNewCardBottom && ( + + )} + + {/* Footer Add Task Button */} + {!showNewCardTop && !showNewCardBottom && group.tasks.length > 0 && ( + + )} + + {/* Drop indicator at the end of the group */} + {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && ( +
+
+
+ )} +
- -
); }); diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx index 3a538463..b103aeff 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskCard.tsx @@ -203,16 +203,18 @@ const TaskCard: React.FC = memo(({ <> {isDropIndicator && (
onTaskDragStart(e, task.id!, groupId)} + onDragOver={e => onTaskDragOver(e, groupId, idx)} + onDrop={e => onTaskDrop(e, groupId, idx)} + > +
onTaskDragStart(e, task.id!, groupId)} - onDragOver={e => onTaskDragOver(e, groupId, idx)} - onDrop={e => onTaskDrop(e, groupId, idx)} - /> + }}>
+
)}
= memo(({
- {task.show_sub_tasks && ( +
{/* Loading state */} {task.sub_tasks_loading && ( @@ -469,7 +480,7 @@ const TaskCard: React.FC = memo(({
{t('noSubtasks', 'No subtasks')}
)}
- )} +
);