feat(task-management): implement task reordering and group updates via API

- Added API methods for reordering tasks and updating task groups (status, priority, phase).
- Enhanced task management slice with async thunks for handling task reordering and group movements.
- Updated task list board to support real-time updates during drag-and-drop operations, emitting socket events for task sort order changes.
- Refactored task-related components to utilize shared Ant Design imports for consistency and maintainability.
- Removed unused Ant Design imports and optimized drag-and-drop CSS for improved performance.
This commit is contained in:
chamikaJ
2025-07-02 15:17:21 +05:30
parent 2064c0833c
commit 0452dbd179
13 changed files with 462 additions and 703 deletions

View File

@@ -21,6 +21,7 @@ import {
reorderTasks,
moveTaskToGroup,
optimisticTaskMove,
reorderTasksInGroup,
setLoading,
fetchTasks,
fetchTasksV3,
@@ -39,6 +40,8 @@ import {
} from '@/features/task-management/selection.slice';
import { Task } from '@/types/task-management.types';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import TaskRow from './task-row';
// import BulkActionBar from './bulk-action-bar';
import OptimizedBulkActionBar from './optimized-bulk-action-bar';
@@ -136,7 +139,6 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const renderCountRef = useRef(0);
const [shouldThrottle, setShouldThrottle] = useState(false);
// Refs for performance optimization
const dragOverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
@@ -144,6 +146,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Enable real-time socket updates for task changes
useTaskSocketHandlers();
// Socket connection for drag and drop
const { socket, connected } = useSocket();
// Redux selectors using V3 API (pre-processed data, minimal loops)
const tasks = useSelector(taskManagementSelectors.selectAll);
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
@@ -151,7 +156,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const selectedTaskIds = useSelector(selectSelectedTaskIds);
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
const error = useSelector((state: RootState) => state.taskManagement.error);
// Bulk action selectors
const statusList = useSelector((state: RootState) => state.taskStatusReducer.status);
const priorityList = useSelector((state: RootState) => state.priorityReducer.priorities);
@@ -234,8 +239,12 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
[dispatch]
);
// Add isDragging state
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = useCallback(
(event: DragStartEvent) => {
setIsDragging(true);
const { active } = event;
const taskId = active.id as string;
@@ -244,13 +253,12 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
let activeGroupId: string | null = null;
if (activeTask) {
// Determine group ID based on current grouping
if (currentGrouping === 'status') {
activeGroupId = `status-${activeTask.status}`;
} else if (currentGrouping === 'priority') {
activeGroupId = `priority-${activeTask.priority}`;
} else if (currentGrouping === 'phase') {
activeGroupId = `phase-${activeTask.phase}`;
// Find which group contains this task by looking through all groups
for (const group of taskGroups) {
if (group.taskIds.includes(taskId)) {
activeGroupId = group.id;
break;
}
}
}
@@ -259,7 +267,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
activeGroupId,
});
},
[tasks, currentGrouping]
[tasks, currentGrouping, taskGroups]
);
// Throttled drag over handler for smoother performance
@@ -270,15 +278,14 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (!over || !dragState.activeTask) return;
const activeTaskId = active.id as string;
const overContainer = over.id as string;
const overId = over.id as string;
// PERFORMANCE OPTIMIZATION: Immediate response for instant UX
// Only update if we're hovering over a different container
const targetTask = tasks.find(t => t.id === overContainer);
let targetGroupId = overContainer;
// Check if we're hovering over a task or a group container
const targetTask = tasks.find(t => t.id === overId);
let targetGroupId = overId;
if (targetTask) {
// PERFORMANCE OPTIMIZATION: Use switch instead of multiple if statements
// We're hovering over a task, determine its group
switch (currentGrouping) {
case 'status':
targetGroupId = `status-${targetTask.status}`;
@@ -291,29 +298,13 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
break;
}
}
if (targetGroupId !== dragState.activeGroupId) {
// PERFORMANCE OPTIMIZATION: Use findIndex for better performance
const targetGroupIndex = taskGroups.findIndex(g => g.id === targetGroupId);
if (targetGroupIndex !== -1) {
const targetGroup = taskGroups[targetGroupIndex];
dispatch(
optimisticTaskMove({
taskId: activeTaskId,
newGroupId: targetGroupId,
newIndex: targetGroup.taskIds.length,
})
);
}
}
}, 16), // 60fps throttling for smooth performance
[dragState, tasks, taskGroups, currentGrouping, dispatch]
[dragState, tasks, taskGroups, currentGrouping]
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
setIsDragging(false);
// Clear any pending drag over timeouts
if (dragOverTimeoutRef.current) {
clearTimeout(dragOverTimeoutRef.current);
@@ -327,36 +318,27 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
activeGroupId: null,
});
if (!over || !currentDragState.activeTask || !currentDragState.activeGroupId) {
if (!event.over || !currentDragState.activeTask || !currentDragState.activeGroupId) {
return;
}
const { active, over } = event;
const activeTaskId = active.id as string;
const overContainer = over.id as string;
const overId = over.id as string;
// Parse the group ID to get group type and value - optimized
const parseGroupId = (groupId: string) => {
const [groupType, ...groupValueParts] = groupId.split('-');
return {
groupType: groupType as 'status' | 'priority' | 'phase',
groupValue: groupValueParts.join('-'),
};
};
// Determine target group
let targetGroupId = overContainer;
// Determine target group and position
let targetGroupId = overId;
let targetIndex = -1;
// Check if dropping on a task or a group
const targetTask = tasks.find(t => t.id === overContainer);
const targetTask = tasks.find(t => t.id === overId);
if (targetTask) {
// Dropping on a task, determine its group
if (currentGrouping === 'status') {
targetGroupId = `status-${targetTask.status}`;
} else if (currentGrouping === 'priority') {
targetGroupId = `priority-${targetTask.priority}`;
} else if (currentGrouping === 'phase') {
targetGroupId = `phase-${targetTask.phase}`;
// Dropping on a task, find which group contains this task
for (const group of taskGroups) {
if (group.taskIds.includes(targetTask.id)) {
targetGroupId = group.id;
break;
}
}
// Find the index of the target task within its group
@@ -364,23 +346,15 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (targetGroup) {
targetIndex = targetGroup.taskIds.indexOf(targetTask.id);
}
} else {
// Dropping on a group container, add to the end
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
if (targetGroup) {
targetIndex = targetGroup.taskIds.length;
}
}
const sourceGroupInfo = parseGroupId(currentDragState.activeGroupId);
const targetGroupInfo = parseGroupId(targetGroupId);
// If moving between different groups, update the task's group property
if (currentDragState.activeGroupId !== targetGroupId) {
dispatch(
moveTaskToGroup({
taskId: activeTaskId,
groupType: targetGroupInfo.groupType,
groupValue: targetGroupInfo.groupValue,
})
);
}
// Handle reordering within the same group or between groups
// Find source and target groups
const sourceGroup = taskGroups.find(g => g.id === currentDragState.activeGroupId);
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
@@ -390,27 +364,41 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Only reorder if actually moving to a different position
if (sourceGroup.id !== targetGroup.id || sourceIndex !== finalTargetIndex) {
// Calculate new order values - simplified
const allTasksInTargetGroup = targetGroup.taskIds.map(
(id: string) => tasks.find((t: any) => t.id === id)!
);
const newOrder = allTasksInTargetGroup.map((task, index) => {
if (index < finalTargetIndex) return task.order;
if (index === finalTargetIndex) return currentDragState.activeTask!.order;
return task.order + 1;
});
// Dispatch reorder action
// Use the new reorderTasksInGroup action that properly handles group arrays
dispatch(
reorderTasks({
taskIds: [activeTaskId, ...allTasksInTargetGroup.map((t: any) => t.id)],
newOrder: [currentDragState.activeTask!.order, ...newOrder],
reorderTasksInGroup({
taskId: activeTaskId,
fromGroupId: currentDragState.activeGroupId,
toGroupId: targetGroupId,
fromIndex: sourceIndex,
toIndex: finalTargetIndex,
groupType: targetGroup.groupType,
groupValue: targetGroup.groupValue,
})
);
// Emit socket event to backend
if (connected && socket && currentDragState.activeTask) {
const currentSession = JSON.parse(localStorage.getItem('session') || '{}');
const socketData = {
from_index: sourceIndex,
to_index: finalTargetIndex,
to_last_index: finalTargetIndex >= targetGroup.taskIds.length,
from_group: currentDragState.activeGroupId,
to_group: targetGroupId,
group_by: currentGrouping,
project_id: projectId,
task: currentDragState.activeTask,
team_id: currentSession.team_id,
};
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), socketData);
}
}
}
},
[dragState, tasks, taskGroups, currentGrouping, dispatch]
[dragState, tasks, taskGroups, currentGrouping, dispatch, connected, socket, projectId]
);
const handleSelectTask = useCallback(
@@ -651,17 +639,14 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Additional handlers for new actions
const handleBulkDuplicate = useCallback(async () => {
// This would need to be implemented in the API service
console.log('Bulk duplicate not yet implemented in API:', selectedTaskIds);
}, [selectedTaskIds]);
const handleBulkExport = useCallback(async () => {
// This would need to be implemented in the API service
console.log('Bulk export not yet implemented in API:', selectedTaskIds);
}, [selectedTaskIds]);
const handleBulkSetDueDate = useCallback(async (date: string) => {
// This would need to be implemented in the API service
console.log('Bulk set due date not yet implemented in API:', date, selectedTaskIds);
}, [selectedTaskIds]);
// Cleanup effect
@@ -689,24 +674,19 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
autoScroll={false}
>
{/* Task Filters */}
<div className="mb-4">
<ImprovedTaskFilters position="list" />
</div>
{/* Performance Analysis - Only show in development */}
{/* {process.env.NODE_ENV === 'development' && (
<PerformanceAnalysis projectId={projectId} />
)} */}
{/* Fixed Height Task Groups Container - Asana Style */}
<div className="task-groups-container-fixed">
<div className="task-groups-scrollable">
<div className={`task-groups-scrollable${isDragging ? ' lock-scroll' : ''}`}>
{loading ? (
<div className="loading-container">
<div className="flex justify-center items-center py-8">
@@ -775,14 +755,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
<DragOverlay
adjustScale={false}
dropAnimation={{
duration: 200,
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
}}
style={{
cursor: 'grabbing',
zIndex: 9999,
}}
dropAnimation={null}
>
{dragOverlayContent}
</DragOverlay>
@@ -1277,6 +1250,10 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
.react-window-list-item {
contain: layout style;
}
.task-groups-scrollable.lock-scroll {
overflow: hidden !important;
}
`}</style>
</div>
);