feat(sort-orders): implement separate sort orders for task groupings
- Introduced new columns for `status_sort_order`, `priority_sort_order`, `phase_sort_order`, and `member_sort_order` in the tasks table to maintain distinct sort orders for each grouping type. - Updated database functions to handle grouping-specific sort orders and avoid unique constraint violations. - Enhanced backend socket handlers to emit changes based on the selected grouping. - Modified frontend components to support drag-and-drop functionality with the new sort order fields, ensuring task organization is preserved across different views. - Added comprehensive migration scripts and verification steps to ensure smooth deployment and backward compatibility.
This commit is contained in:
@@ -1,13 +1,84 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { DragEndEvent, DragOverEvent, DragStartEvent } from '@dnd-kit/core';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { reorderTasksInGroup, moveTaskBetweenGroups } from '@/features/task-management/task-management.slice';
|
||||
import { Task, TaskGroup } from '@/types/task-management.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { reorderTasksInGroup } from '@/features/task-management/task-management.slice';
|
||||
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
||||
import { Task, TaskGroup, getSortOrderField } from '@/types/task-management.types';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
|
||||
export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { socket, connected } = useSocket();
|
||||
const { projectId } = useParams();
|
||||
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
// Helper function to emit socket event for persistence
|
||||
const emitTaskSortChange = useCallback(
|
||||
(taskId: string, sourceGroup: TaskGroup, targetGroup: TaskGroup, insertIndex: number) => {
|
||||
if (!socket || !connected || !projectId) {
|
||||
console.warn('Socket not connected or missing project ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const task = allTasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
console.error('Task not found for socket emission:', taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get team_id from current session
|
||||
const teamId = currentSession?.team_id || '';
|
||||
|
||||
// Calculate sort orders for socket emission using the appropriate sort field
|
||||
const sortField = getSortOrderField(currentGrouping);
|
||||
const fromIndex = (task as any)[sortField] || task.order || 0;
|
||||
let toIndex = 0;
|
||||
let toLastIndex = false;
|
||||
|
||||
if (targetGroup.taskIds.length === 0) {
|
||||
toIndex = 0;
|
||||
toLastIndex = true;
|
||||
} else if (insertIndex >= targetGroup.taskIds.length) {
|
||||
// Dropping at the end
|
||||
const lastTask = allTasks.find(t => t.id === targetGroup.taskIds[targetGroup.taskIds.length - 1]);
|
||||
toIndex = ((lastTask as any)?.[sortField] || lastTask?.order || 0) + 1;
|
||||
toLastIndex = true;
|
||||
} else {
|
||||
// Dropping at specific position
|
||||
const targetTask = allTasks.find(t => t.id === targetGroup.taskIds[insertIndex]);
|
||||
toIndex = (targetTask as any)?.[sortField] || targetTask?.order || insertIndex;
|
||||
toLastIndex = false;
|
||||
}
|
||||
|
||||
const socketData = {
|
||||
project_id: projectId,
|
||||
from_index: fromIndex,
|
||||
to_index: toIndex,
|
||||
to_last_index: toLastIndex,
|
||||
from_group: sourceGroup.id,
|
||||
to_group: targetGroup.id,
|
||||
group_by: currentGrouping || 'status',
|
||||
task: {
|
||||
id: task.id,
|
||||
project_id: projectId,
|
||||
status: task.status || '',
|
||||
priority: task.priority || '',
|
||||
},
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
console.log('Emitting TASK_SORT_ORDER_CHANGE:', socketData);
|
||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), socketData);
|
||||
},
|
||||
[socket, connected, projectId, allTasks, currentGrouping, currentSession]
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string);
|
||||
}, []);
|
||||
@@ -124,16 +195,8 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
newPosition: insertIndex,
|
||||
});
|
||||
|
||||
// Move task to the target group
|
||||
dispatch(
|
||||
moveTaskBetweenGroups({
|
||||
taskId: activeId as string,
|
||||
sourceGroupId: activeGroup.id,
|
||||
targetGroupId: targetGroup.id,
|
||||
})
|
||||
);
|
||||
|
||||
// Reorder task within target group at drop position
|
||||
// reorderTasksInGroup handles both same-group and cross-group moves
|
||||
// No need for separate moveTaskBetweenGroups call
|
||||
dispatch(
|
||||
reorderTasksInGroup({
|
||||
sourceTaskId: activeId as string,
|
||||
@@ -142,6 +205,9 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
destinationGroupId: targetGroup.id,
|
||||
})
|
||||
);
|
||||
|
||||
// Emit socket event for persistence
|
||||
emitTaskSortChange(activeId as string, activeGroup, targetGroup, insertIndex);
|
||||
} else {
|
||||
// Reordering within the same group
|
||||
console.log('Reordering task within same group:', {
|
||||
@@ -161,10 +227,13 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
destinationGroupId: activeGroup.id,
|
||||
})
|
||||
);
|
||||
|
||||
// Emit socket event for persistence
|
||||
emitTaskSortChange(activeId as string, activeGroup, targetGroup, insertIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
[allTasks, groups, dispatch]
|
||||
[allTasks, groups, dispatch, emitTaskSortChange]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
EntityId,
|
||||
createSelector,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
|
||||
import { Task, TaskManagementState, TaskGroup, TaskGrouping, getSortOrderField } from '@/types/task-management.types';
|
||||
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||
import { RootState } from '@/app/store';
|
||||
import {
|
||||
@@ -661,11 +661,11 @@ const taskManagementSlice = createSlice({
|
||||
newTasks.splice(newTasks.indexOf(destinationTaskId), 0, removed);
|
||||
group.taskIds = newTasks;
|
||||
|
||||
// Update order for affected tasks. Assuming simple reordering affects order.
|
||||
// This might need more sophisticated logic based on how `order` is used.
|
||||
// Update order for affected tasks using the appropriate sort field
|
||||
const sortField = getSortOrderField(state.grouping?.id);
|
||||
newTasks.forEach((id, index) => {
|
||||
if (newEntities[id]) {
|
||||
newEntities[id] = { ...newEntities[id], order: index };
|
||||
newEntities[id] = { ...newEntities[id], [sortField]: index };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -723,12 +723,13 @@ const taskManagementSlice = createSlice({
|
||||
newEntities[sourceTaskId] = updatedTask;
|
||||
}
|
||||
|
||||
// Update order for affected tasks in both groups if necessary
|
||||
// Update order for affected tasks in both groups using the appropriate sort field
|
||||
const sortField = getSortOrderField(state.grouping?.id);
|
||||
sourceGroup.taskIds.forEach((id, index) => {
|
||||
if (newEntities[id]) newEntities[id] = { ...newEntities[id], order: index };
|
||||
if (newEntities[id]) newEntities[id] = { ...newEntities[id], [sortField]: index };
|
||||
});
|
||||
destinationGroup.taskIds.forEach((id, index) => {
|
||||
if (newEntities[id]) newEntities[id] = { ...newEntities[id], order: index };
|
||||
if (newEntities[id]) newEntities[id] = { ...newEntities[id], [sortField]: index };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ export interface Task {
|
||||
has_subscribers?: boolean;
|
||||
schedule_id?: string | null;
|
||||
order?: number;
|
||||
status_sort_order?: number; // Sort order when grouped by status
|
||||
priority_sort_order?: number; // Sort order when grouped by priority
|
||||
phase_sort_order?: number; // Sort order when grouped by phase
|
||||
member_sort_order?: number; // Sort order when grouped by members
|
||||
reporter?: string; // Reporter field
|
||||
timeTracking?: { // Time tracking information
|
||||
logged?: number;
|
||||
@@ -173,3 +177,21 @@ export interface BulkAction {
|
||||
value?: any;
|
||||
taskIds: string[];
|
||||
}
|
||||
|
||||
// Helper function to get the appropriate sort order field based on grouping
|
||||
export function getSortOrderField(grouping: string | undefined): keyof Task {
|
||||
switch (grouping) {
|
||||
case 'status':
|
||||
return 'status_sort_order';
|
||||
case 'priority':
|
||||
return 'priority_sort_order';
|
||||
case 'phase':
|
||||
return 'phase_sort_order';
|
||||
case 'members':
|
||||
return 'member_sort_order';
|
||||
case 'general':
|
||||
return 'order'; // explicit general sorting
|
||||
default:
|
||||
return 'status_sort_order'; // Default to status sorting to match backend
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user