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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user