Enhance EnhancedKanbanBoardNativeDnD to support phase reordering
- Integrated phase reordering functionality within the EnhancedKanbanBoardNativeDnD component. - Added logic to fetch phases by project ID and update phase order through API calls. - Updated drag-and-drop handling to accommodate reordering of phases alongside existing status-based reordering.
This commit is contained in:
@@ -21,6 +21,9 @@ import alertService from '@/services/alerts/alertService';
|
|||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||||
|
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
|
||||||
|
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||||
|
import { fetchPhasesByProjectId, updatePhaseListOrder } from '@/features/projects/singleProject/phase/phases.slice';
|
||||||
|
|
||||||
const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => {
|
const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -34,6 +37,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
loadingGroups,
|
loadingGroups,
|
||||||
error,
|
error,
|
||||||
} = useSelector((state: RootState) => state.enhancedKanbanReducer);
|
} = useSelector((state: RootState) => state.enhancedKanbanReducer);
|
||||||
|
const { phaseList, loadingPhases } = useAppSelector(state => state.phaseReducer);
|
||||||
const [draggedGroupId, setDraggedGroupId] = useState<string | null>(null);
|
const [draggedGroupId, setDraggedGroupId] = useState<string | null>(null);
|
||||||
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
||||||
const [draggedTaskGroupId, setDraggedTaskGroupId] = useState<string | null>(null);
|
const [draggedTaskGroupId, setDraggedTaskGroupId] = useState<string | null>(null);
|
||||||
@@ -56,6 +60,9 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
if (!statusCategories.length) {
|
if (!statusCategories.length) {
|
||||||
dispatch(fetchStatusesCategories() as any);
|
dispatch(fetchStatusesCategories() as any);
|
||||||
}
|
}
|
||||||
|
if ( groupBy === 'phase' && !phaseList.length) {
|
||||||
|
dispatch(fetchPhasesByProjectId(projectId) as any);
|
||||||
|
}
|
||||||
}, [dispatch, projectId]);
|
}, [dispatch, projectId]);
|
||||||
// Reset drag state if taskGroups changes (e.g., real-time update)
|
// Reset drag state if taskGroups changes (e.g., real-time update)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -90,9 +97,9 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
reorderedGroups.splice(toIdx, 0, moved);
|
reorderedGroups.splice(toIdx, 0, moved);
|
||||||
dispatch(reorderGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }));
|
dispatch(reorderGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }));
|
||||||
dispatch(reorderEnhancedKanbanGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }) as any);
|
dispatch(reorderEnhancedKanbanGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }) as any);
|
||||||
|
|
||||||
// API call for group order
|
// API call for group order
|
||||||
try {
|
try {
|
||||||
|
if (groupBy === 'status') {
|
||||||
const columnOrder = reorderedGroups.map(group => group.id);
|
const columnOrder = reorderedGroups.map(group => group.id);
|
||||||
const requestBody = { status_order: columnOrder };
|
const requestBody = { status_order: columnOrder };
|
||||||
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
|
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
|
||||||
@@ -104,6 +111,22 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
dispatch(reorderGroups({ fromIndex: toIdx, toIndex: fromIdx, reorderedGroups: revertedGroups }));
|
dispatch(reorderGroups({ fromIndex: toIdx, toIndex: fromIdx, reorderedGroups: revertedGroups }));
|
||||||
alertService.error('Failed to update column order', 'Please try again');
|
alertService.error('Failed to update column order', 'Please try again');
|
||||||
}
|
}
|
||||||
|
} else if (groupBy === 'phase') {
|
||||||
|
const newPhaseList = [...phaseList];
|
||||||
|
const [movedItem] = newPhaseList.splice(fromIdx, 1);
|
||||||
|
newPhaseList.splice(toIdx, 0, movedItem);
|
||||||
|
dispatch(updatePhaseListOrder(newPhaseList));
|
||||||
|
const requestBody = {
|
||||||
|
from_index: fromIdx,
|
||||||
|
to_index: toIdx,
|
||||||
|
phases: newPhaseList,
|
||||||
|
project_id: projectId,
|
||||||
|
};
|
||||||
|
const response = await phasesApiService.updatePhaseOrder(projectId, requestBody);
|
||||||
|
if (!response.done) {
|
||||||
|
alertService.error('Failed to update phase order', 'Please try again');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Revert the change if API call fails
|
// Revert the change if API call fails
|
||||||
const revertedGroups = [...reorderedGroups];
|
const revertedGroups = [...reorderedGroups];
|
||||||
@@ -119,7 +142,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Utility to recalculate all task orders for all groups
|
// Utility to recalculate all task orders for all groups
|
||||||
function getAllTaskUpdates(allGroups, groupBy) {
|
function getAllTaskUpdates(allGroups: ITaskListGroup[], groupBy: string) {
|
||||||
const taskUpdates = [];
|
const taskUpdates = [];
|
||||||
let currentSortOrder = 0;
|
let currentSortOrder = 0;
|
||||||
for (const group of allGroups) {
|
for (const group of allGroups) {
|
||||||
|
|||||||
@@ -0,0 +1,583 @@
|
|||||||
|
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
|
||||||
|
import TaskListFilters from '../taskList/task-list-filters/task-list-filters';
|
||||||
|
import { Flex, Skeleton } from 'antd';
|
||||||
|
import BoardSectionCardContainer from './board-section/board-section-container';
|
||||||
|
import {
|
||||||
|
fetchBoardTaskGroups,
|
||||||
|
reorderTaskGroups,
|
||||||
|
moveTaskBetweenGroups,
|
||||||
|
IGroupBy,
|
||||||
|
updateTaskProgress,
|
||||||
|
} from '@features/board/board-slice';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragOverEvent,
|
||||||
|
DragStartEvent,
|
||||||
|
closestCenter,
|
||||||
|
DragOverlay,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
getFirstCollision,
|
||||||
|
pointerWithin,
|
||||||
|
rectIntersection,
|
||||||
|
UniqueIdentifier,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import BoardViewTaskCard from './board-section/board-task-card/board-view-task-card';
|
||||||
|
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||||
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
|
import alertService from '@/services/alerts/alertService';
|
||||||
|
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||||
|
import { evt_project_board_visit, evt_project_task_list_drag_and_move } from '@/shared/worklenz-analytics-events';
|
||||||
|
import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-request';
|
||||||
|
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
||||||
|
import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice';
|
||||||
|
|
||||||
|
interface DroppableContainer {
|
||||||
|
id: UniqueIdentifier;
|
||||||
|
data: {
|
||||||
|
current?: {
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectViewBoard = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { projectView } = useTabSearchParam();
|
||||||
|
const { socket } = useSocket();
|
||||||
|
const authService = useAuthService();
|
||||||
|
const currentSession = authService.getCurrentSession();
|
||||||
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
|
const [currentTaskIndex, setCurrentTaskIndex] = useState(-1);
|
||||||
|
// Add local loading state to immediately show skeleton
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||||
|
const { taskGroups, groupBy, loadingGroups, search, archived } = useAppSelector(state => state.boardReducer);
|
||||||
|
const { statusCategories, loading: loadingStatusCategories } = useAppSelector(
|
||||||
|
state => state.taskStatusReducer
|
||||||
|
);
|
||||||
|
const [activeItem, setActiveItem] = useState<any>(null);
|
||||||
|
|
||||||
|
// Store the original source group ID when drag starts
|
||||||
|
const originalSourceGroupIdRef = useRef<string | null>(null);
|
||||||
|
const lastOverId = useRef<UniqueIdentifier | null>(null);
|
||||||
|
const recentlyMovedToNewContainer = useRef(false);
|
||||||
|
const [clonedItems, setClonedItems] = useState<any>(null);
|
||||||
|
const isDraggingRef = useRef(false);
|
||||||
|
|
||||||
|
// Update loading state based on all loading conditions
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(loadingGroups || loadingStatusCategories);
|
||||||
|
}, [loadingGroups, loadingStatusCategories]);
|
||||||
|
|
||||||
|
// Load data efficiently with async/await and Promise.all
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
if (projectId && groupBy && projectView === 'kanban') {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
if (!loadingGroups) {
|
||||||
|
promises.push(dispatch(fetchBoardTaskGroups(projectId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusCategories.length) {
|
||||||
|
promises.push(dispatch(fetchStatusesCategories()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all data to load
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}, [dispatch, projectId, groupBy, projectView, search, archived]);
|
||||||
|
|
||||||
|
// Create sensors with memoization to prevent unnecessary re-renders
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 10,
|
||||||
|
delay: 100,
|
||||||
|
tolerance: 5,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 250,
|
||||||
|
tolerance: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const collisionDetectionStrategy = useCallback(
|
||||||
|
(args: {
|
||||||
|
active: { id: UniqueIdentifier; data: { current?: { type?: string } } };
|
||||||
|
droppableContainers: DroppableContainer[];
|
||||||
|
}) => {
|
||||||
|
if (activeItem?.type === 'section') {
|
||||||
|
return closestCenter({
|
||||||
|
...args,
|
||||||
|
droppableContainers: args.droppableContainers.filter(
|
||||||
|
(container: DroppableContainer) => container.data.current?.type === 'section'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start by finding any intersecting droppable
|
||||||
|
const pointerIntersections = pointerWithin(args);
|
||||||
|
const intersections =
|
||||||
|
pointerIntersections.length > 0
|
||||||
|
? pointerIntersections
|
||||||
|
: rectIntersection(args);
|
||||||
|
let overId = getFirstCollision(intersections, 'id');
|
||||||
|
|
||||||
|
if (overId !== null) {
|
||||||
|
const overContainer = args.droppableContainers.find(
|
||||||
|
(container: DroppableContainer) => container.id === overId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overContainer?.data.current?.type === 'section') {
|
||||||
|
const containerItems = taskGroups.find(
|
||||||
|
(group) => group.id === overId
|
||||||
|
)?.tasks || [];
|
||||||
|
|
||||||
|
if (containerItems.length > 0) {
|
||||||
|
overId = closestCenter({
|
||||||
|
...args,
|
||||||
|
droppableContainers: args.droppableContainers.filter(
|
||||||
|
(container: DroppableContainer) =>
|
||||||
|
container.id !== overId &&
|
||||||
|
container.data.current?.type === 'task'
|
||||||
|
),
|
||||||
|
})[0]?.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOverId.current = overId;
|
||||||
|
return [{ id: overId }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentlyMovedToNewContainer.current) {
|
||||||
|
lastOverId.current = activeItem?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastOverId.current ? [{ id: lastOverId.current }] : [];
|
||||||
|
},
|
||||||
|
[activeItem, taskGroups]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTaskProgress = (data: {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
complete_ratio: number;
|
||||||
|
completed_count: number;
|
||||||
|
total_tasks_count: number;
|
||||||
|
parent_task: string;
|
||||||
|
}) => {
|
||||||
|
dispatch(updateTaskProgress(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounced move task function to prevent rapid updates
|
||||||
|
const debouncedMoveTask = useCallback(
|
||||||
|
debounce((taskId: string, sourceGroupId: string, targetGroupId: string, targetIndex: number) => {
|
||||||
|
dispatch(
|
||||||
|
moveTaskBetweenGroups({
|
||||||
|
taskId,
|
||||||
|
sourceGroupId,
|
||||||
|
targetGroupId,
|
||||||
|
targetIndex,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, 100),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragStart = (event: DragStartEvent) => {
|
||||||
|
const { active } = event;
|
||||||
|
isDraggingRef.current = true;
|
||||||
|
setActiveItem(active.data.current);
|
||||||
|
setCurrentTaskIndex(active.data.current?.sortable.index);
|
||||||
|
if (active.data.current?.type === 'task') {
|
||||||
|
originalSourceGroupIdRef.current = active.data.current.sectionId;
|
||||||
|
}
|
||||||
|
setClonedItems(taskGroups);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findGroupForId = (id: string) => {
|
||||||
|
// If id is a sectionId
|
||||||
|
if (taskGroups.some(group => group.id === id)) return id;
|
||||||
|
// If id is a taskId, find the group containing it
|
||||||
|
const group = taskGroups.find(g => g.tasks.some(t => t.id === id));
|
||||||
|
return group?.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragOverEvent) => {
|
||||||
|
try {
|
||||||
|
if (!isDraggingRef.current) return;
|
||||||
|
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over) return;
|
||||||
|
|
||||||
|
// Get the ids
|
||||||
|
const activeId = active.id;
|
||||||
|
const overId = over.id;
|
||||||
|
|
||||||
|
// Find the group (section) for each
|
||||||
|
const activeGroupId = findGroupForId(activeId as string);
|
||||||
|
const overGroupId = findGroupForId(overId as string);
|
||||||
|
|
||||||
|
// Only move if both groups exist and are different, and the active is a task
|
||||||
|
if (
|
||||||
|
activeGroupId &&
|
||||||
|
overGroupId &&
|
||||||
|
active.data.current?.type === 'task'
|
||||||
|
) {
|
||||||
|
// Find the target index in the over group
|
||||||
|
const targetGroup = taskGroups.find(g => g.id === overGroupId);
|
||||||
|
let targetIndex = 0;
|
||||||
|
if (targetGroup) {
|
||||||
|
// If over is a task, insert before it; if over is a section, append to end
|
||||||
|
if (over.data.current?.type === 'task') {
|
||||||
|
targetIndex = targetGroup.tasks.findIndex(t => t.id === overId);
|
||||||
|
if (targetIndex === -1) targetIndex = targetGroup.tasks.length;
|
||||||
|
} else {
|
||||||
|
targetIndex = targetGroup.tasks.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use debounced move task to prevent rapid updates
|
||||||
|
debouncedMoveTask(
|
||||||
|
activeId as string,
|
||||||
|
activeGroupId,
|
||||||
|
overGroupId,
|
||||||
|
targetIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('handleDragOver error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePriorityChange = (taskId: string, priorityId: string) => {
|
||||||
|
if (!taskId || !priorityId || !socket) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
task_id: taskId,
|
||||||
|
priority_id: priorityId,
|
||||||
|
team_id: currentSession?.team_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.emit(SocketEvents.TASK_PRIORITY_CHANGE.toString(), JSON.stringify(payload));
|
||||||
|
socket.once(SocketEvents.TASK_PRIORITY_CHANGE.toString(), (data: ITaskListPriorityChangeResponse) => {
|
||||||
|
dispatch(updateBoardTaskPriority(data));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = async (event: DragEndEvent) => {
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (!over || !projectId) {
|
||||||
|
setActiveItem(null);
|
||||||
|
originalSourceGroupIdRef.current = null;
|
||||||
|
setClonedItems(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActiveTask = active.data.current?.type === 'task';
|
||||||
|
const isActiveSection = active.data.current?.type === 'section';
|
||||||
|
|
||||||
|
// Handle task dragging between columns
|
||||||
|
if (isActiveTask) {
|
||||||
|
const task = active.data.current?.task;
|
||||||
|
|
||||||
|
// Use the original source group ID from ref instead of the potentially modified one
|
||||||
|
const sourceGroupId = originalSourceGroupIdRef.current || active.data.current?.sectionId;
|
||||||
|
|
||||||
|
// Fix: Ensure we correctly identify the target group ID
|
||||||
|
let targetGroupId;
|
||||||
|
if (over.data.current?.type === 'task') {
|
||||||
|
// If dropping on a task, get its section ID
|
||||||
|
targetGroupId = over.data.current?.sectionId;
|
||||||
|
} else if (over.data.current?.type === 'section') {
|
||||||
|
// If dropping directly on a section
|
||||||
|
targetGroupId = over.id;
|
||||||
|
} else {
|
||||||
|
// Fallback to the over ID if type is not specified
|
||||||
|
targetGroupId = over.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find source and target groups
|
||||||
|
const sourceGroup = taskGroups.find(group => group.id === sourceGroupId);
|
||||||
|
const targetGroup = taskGroups.find(group => group.id === targetGroupId);
|
||||||
|
|
||||||
|
if (!sourceGroup || !targetGroup || !task) {
|
||||||
|
logger.error('Could not find source or target group, or task is undefined');
|
||||||
|
setActiveItem(null);
|
||||||
|
originalSourceGroupIdRef.current = null; // Reset the ref
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetGroupId !== sourceGroupId) {
|
||||||
|
const canContinue = await checkTaskDependencyStatus(task.id, targetGroupId);
|
||||||
|
if (!canContinue) {
|
||||||
|
alertService.error(
|
||||||
|
'Task is not completed',
|
||||||
|
'Please complete the task dependencies before proceeding'
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
moveTaskBetweenGroups({
|
||||||
|
taskId: task.id,
|
||||||
|
sourceGroupId: targetGroupId, // Current group (where it was moved optimistically)
|
||||||
|
targetGroupId: sourceGroupId, // Move it back to the original source group
|
||||||
|
targetIndex: currentTaskIndex !== -1 ? currentTaskIndex : 0, // Original position or append to end
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setActiveItem(null);
|
||||||
|
originalSourceGroupIdRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find indices
|
||||||
|
let fromIndex = sourceGroup.tasks.findIndex(t => t.id === task.id);
|
||||||
|
// Handle case where task is not found in source group (might have been moved already in UI)
|
||||||
|
if (fromIndex === -1) {
|
||||||
|
logger.info('Task not found in source group. Using task sort_order from task object.');
|
||||||
|
|
||||||
|
// Use the sort_order from the task object itself
|
||||||
|
const fromSortOrder = task.sort_order;
|
||||||
|
|
||||||
|
// Calculate target index and position
|
||||||
|
let toIndex = -1;
|
||||||
|
if (over.data.current?.type === 'task') {
|
||||||
|
const overTaskId = over.data.current?.task.id;
|
||||||
|
toIndex = targetGroup.tasks.findIndex(t => t.id === overTaskId);
|
||||||
|
} else {
|
||||||
|
// If dropping on a section, append to the end
|
||||||
|
toIndex = targetGroup.tasks.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate toPos similar to Angular implementation
|
||||||
|
const toPos = targetGroup.tasks[toIndex]?.sort_order ||
|
||||||
|
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
|
||||||
|
-1;
|
||||||
|
|
||||||
|
// Prepare socket event payload
|
||||||
|
const body = {
|
||||||
|
project_id: projectId,
|
||||||
|
from_index: fromSortOrder,
|
||||||
|
to_index: toPos,
|
||||||
|
to_last_index: !toPos,
|
||||||
|
from_group: sourceGroupId,
|
||||||
|
to_group: targetGroupId,
|
||||||
|
group_by: groupBy || 'status',
|
||||||
|
task,
|
||||||
|
team_id: currentSession?.team_id
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit socket event
|
||||||
|
if (socket) {
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
|
||||||
|
|
||||||
|
// Set up listener for task progress update
|
||||||
|
socket.once(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), () => {
|
||||||
|
if (task.is_sub_task) {
|
||||||
|
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
|
||||||
|
} else {
|
||||||
|
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle priority change if groupBy is priority
|
||||||
|
if (groupBy === IGroupBy.PRIORITY) {
|
||||||
|
handlePriorityChange(task.id, targetGroupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track analytics event
|
||||||
|
trackMixpanelEvent(evt_project_task_list_drag_and_move);
|
||||||
|
|
||||||
|
setActiveItem(null);
|
||||||
|
originalSourceGroupIdRef.current = null; // Reset the ref
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate target index and position
|
||||||
|
let toIndex = -1;
|
||||||
|
if (over.data.current?.type === 'task') {
|
||||||
|
const overTaskId = over.data.current?.task.id;
|
||||||
|
toIndex = targetGroup.tasks.findIndex(t => t.id === overTaskId);
|
||||||
|
} else {
|
||||||
|
// If dropping on a section, append to the end
|
||||||
|
toIndex = targetGroup.tasks.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate toPos similar to Angular implementation
|
||||||
|
const toPos = targetGroup.tasks[toIndex]?.sort_order ||
|
||||||
|
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
|
||||||
|
-1;
|
||||||
|
// Prepare socket event payload
|
||||||
|
const body = {
|
||||||
|
project_id: projectId,
|
||||||
|
from_index: sourceGroup.tasks[fromIndex].sort_order,
|
||||||
|
to_index: toPos,
|
||||||
|
to_last_index: !toPos,
|
||||||
|
from_group: sourceGroupId, // Use the direct IDs instead of group objects
|
||||||
|
to_group: targetGroupId, // Use the direct IDs instead of group objects
|
||||||
|
group_by: groupBy || 'status', // Use the current groupBy value
|
||||||
|
task,
|
||||||
|
team_id: currentSession?.team_id
|
||||||
|
};
|
||||||
|
// Emit socket event
|
||||||
|
if (socket) {
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
|
||||||
|
|
||||||
|
// Set up listener for task progress update
|
||||||
|
socket.once(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), () => {
|
||||||
|
if (task.is_sub_task) {
|
||||||
|
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
|
||||||
|
} else {
|
||||||
|
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Track analytics event
|
||||||
|
trackMixpanelEvent(evt_project_task_list_drag_and_move);
|
||||||
|
}
|
||||||
|
// Handle column reordering
|
||||||
|
else if (isActiveSection) {
|
||||||
|
// Don't allow reordering if groupBy is phases
|
||||||
|
if (groupBy === IGroupBy.PHASE) {
|
||||||
|
setActiveItem(null);
|
||||||
|
originalSourceGroupIdRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionId = active.id;
|
||||||
|
const fromIndex = taskGroups.findIndex(group => group.id === sectionId);
|
||||||
|
const toIndex = taskGroups.findIndex(group => group.id === over.id);
|
||||||
|
|
||||||
|
if (fromIndex !== -1 && toIndex !== -1) {
|
||||||
|
// Create a new array with the reordered groups
|
||||||
|
const reorderedGroups = [...taskGroups];
|
||||||
|
const [movedGroup] = reorderedGroups.splice(fromIndex, 1);
|
||||||
|
reorderedGroups.splice(toIndex, 0, movedGroup);
|
||||||
|
|
||||||
|
// Dispatch action to reorder columns with the new array
|
||||||
|
dispatch(reorderTaskGroups(reorderedGroups));
|
||||||
|
|
||||||
|
// Prepare column order for API
|
||||||
|
const columnOrder = reorderedGroups.map(group => group.id);
|
||||||
|
|
||||||
|
// Call API to update status order
|
||||||
|
try {
|
||||||
|
// Use the correct API endpoint based on the Angular code
|
||||||
|
const requestBody: ITaskStatusCreateRequest = {
|
||||||
|
status_order: columnOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
|
||||||
|
if (!response.done) {
|
||||||
|
const revertedGroups = [...reorderedGroups];
|
||||||
|
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
|
||||||
|
revertedGroups.splice(fromIndex, 0, movedBackGroup);
|
||||||
|
dispatch(reorderTaskGroups(revertedGroups));
|
||||||
|
alertService.error('Failed to update column order', 'Please try again');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Revert the change if API call fails
|
||||||
|
const revertedGroups = [...reorderedGroups];
|
||||||
|
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
|
||||||
|
revertedGroups.splice(fromIndex, 0, movedBackGroup);
|
||||||
|
dispatch(reorderTaskGroups(revertedGroups));
|
||||||
|
alertService.error('Failed to update column order', 'Please try again');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveItem(null);
|
||||||
|
originalSourceGroupIdRef.current = null; // Reset the ref
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragCancel = () => {
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
if (clonedItems) {
|
||||||
|
dispatch(reorderTaskGroups(clonedItems));
|
||||||
|
}
|
||||||
|
setActiveItem(null);
|
||||||
|
setClonedItems(null);
|
||||||
|
originalSourceGroupIdRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset the recently moved flag after animation frame
|
||||||
|
useEffect(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
recentlyMovedToNewContainer.current = false;
|
||||||
|
});
|
||||||
|
}, [taskGroups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.on(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket?.off(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress);
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
// Track analytics event on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
trackMixpanelEvent(evt_project_board_visit);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Cleanup debounced function on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedMoveTask.cancel();
|
||||||
|
};
|
||||||
|
}, [debouncedMoveTask]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical gap={16}>
|
||||||
|
<TaskListFilters position={'board'} />
|
||||||
|
<Skeleton active loading={isLoading} className='mt-4 p-4'>
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={collisionDetectionStrategy}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
|
>
|
||||||
|
<BoardSectionCardContainer
|
||||||
|
datasource={taskGroups}
|
||||||
|
group={groupBy as 'status' | 'priority' | 'phases'}
|
||||||
|
/>
|
||||||
|
<DragOverlay>
|
||||||
|
{activeItem?.type === 'task' && (
|
||||||
|
<BoardViewTaskCard task={activeItem.task} sectionId={activeItem.sectionId} />
|
||||||
|
)}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
|
</Skeleton>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectViewBoard;
|
||||||
Reference in New Issue
Block a user