refactor(sort-orders): remove outdated deployment and implementation guides
- Deleted the `DEPLOYMENT_GUIDE_SORT_ORDERS.md` and `SEPARATE_SORT_ORDERS_IMPLEMENTATION.md` files as they are no longer relevant following the recent updates to the sort orders feature. - Introduced new migration scripts to address duplicate sort orders and ensure data integrity across the updated task sorting system. - Updated database schema to include new sort order columns and constraints for improved performance and organization. - Enhanced backend functions and frontend components to support the new sorting logic and maintain user experience during task organization.
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
"addTaskText": "Shto Detyrë",
|
||||
"addSubTaskText": "+ Shto Nën-Detyrë",
|
||||
"noTasksInGroup": "Nuk ka detyra në këtë grup",
|
||||
"dropTaskHere": "Lëshoje detyrën këtu",
|
||||
"addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter",
|
||||
|
||||
"openButton": "Hap",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"addSubTaskText": "+ Unteraufgabe hinzufügen",
|
||||
"addTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
|
||||
"noTasksInGroup": "Keine Aufgaben in dieser Gruppe",
|
||||
"dropTaskHere": "Aufgabe hier ablegen",
|
||||
|
||||
"openButton": "Öffnen",
|
||||
"okButton": "OK",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"addSubTaskText": "Add Sub Task",
|
||||
"addTaskInputPlaceholder": "Type your task and hit enter",
|
||||
"noTasksInGroup": "No tasks in this group",
|
||||
"dropTaskHere": "Drop task here",
|
||||
|
||||
"openButton": "Open",
|
||||
"okButton": "Ok",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"addTaskText": "Agregar tarea",
|
||||
"addSubTaskText": "Agregar subtarea",
|
||||
"noTasksInGroup": "No hay tareas en este grupo",
|
||||
"dropTaskHere": "Soltar tarea aquí",
|
||||
"addTaskInputPlaceholder": "Escribe tu tarea y presiona enter",
|
||||
|
||||
"openButton": "Abrir",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"addTaskText": "Adicionar Tarefa",
|
||||
"addSubTaskText": "+ Adicionar Subtarefa",
|
||||
"noTasksInGroup": "Nenhuma tarefa neste grupo",
|
||||
"dropTaskHere": "Soltar tarefa aqui",
|
||||
"addTaskInputPlaceholder": "Digite sua tarefa e pressione enter",
|
||||
|
||||
"openButton": "Abrir",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"addSubTaskText": "+ 添加子任务",
|
||||
"addTaskInputPlaceholder": "输入任务并按回车键",
|
||||
"noTasksInGroup": "此组中没有任务",
|
||||
"dropTaskHere": "将任务拖到这里",
|
||||
"openButton": "打开",
|
||||
"okButton": "确定",
|
||||
"noLabelsFound": "未找到标签",
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Label } from '@/types/task-management.types';
|
||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||
import { ALPHA_CHANNEL } from '@/shared/constants';
|
||||
|
||||
interface CustomColordLabelProps {
|
||||
label: Label | ITaskLabel;
|
||||
@@ -14,36 +15,21 @@ const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelPro
|
||||
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
|
||||
|
||||
// Handle different color property names for different types
|
||||
const backgroundColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color
|
||||
const baseColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color
|
||||
|
||||
// Function to determine if we should use white or black text based on background color
|
||||
const getTextColor = (bgColor: string): string => {
|
||||
// Remove # if present
|
||||
const color = bgColor.replace('#', '');
|
||||
|
||||
// Convert to RGB
|
||||
const r = parseInt(color.substr(0, 2), 16);
|
||||
const g = parseInt(color.substr(2, 2), 16);
|
||||
const b = parseInt(color.substr(4, 2), 16);
|
||||
|
||||
// Calculate luminance
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
|
||||
// Return white for dark backgrounds, black for light backgrounds
|
||||
return luminance > 0.5 ? '#000000' : '#ffffff';
|
||||
};
|
||||
|
||||
const textColor = getTextColor(backgroundColor);
|
||||
// Add alpha channel to the base color
|
||||
const backgroundColor = baseColor + ALPHA_CHANNEL;
|
||||
const textColor = baseColor;
|
||||
|
||||
return (
|
||||
<Tooltip title={label.name}>
|
||||
<span
|
||||
ref={ref}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium shrink-0 max-w-[100px]"
|
||||
style={{
|
||||
backgroundColor,
|
||||
color: textColor,
|
||||
border: `1px solid ${backgroundColor}`,
|
||||
border: `1px solid ${baseColor}`,
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{truncatedName}</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { NumbersColorMap } from '@/shared/constants';
|
||||
import { NumbersColorMap, ALPHA_CHANNEL } from '@/shared/constants';
|
||||
|
||||
interface CustomNumberLabelProps {
|
||||
labelList: string[];
|
||||
@@ -12,17 +12,24 @@ interface CustomNumberLabelProps {
|
||||
const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
|
||||
({ labelList, namesString, isDarkMode = false, color }, ref) => {
|
||||
// Use provided color, or fall back to NumbersColorMap based on first digit
|
||||
const backgroundColor = color || (() => {
|
||||
const baseColor = color || (() => {
|
||||
const firstDigit = namesString.match(/\d/)?.[0] || '0';
|
||||
return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
|
||||
})();
|
||||
|
||||
// Add alpha channel to the base color
|
||||
const backgroundColor = baseColor + ALPHA_CHANNEL;
|
||||
|
||||
return (
|
||||
<Tooltip title={labelList.join(', ')}>
|
||||
<span
|
||||
ref={ref}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
|
||||
style={{ backgroundColor }}
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium cursor-help"
|
||||
style={{
|
||||
backgroundColor,
|
||||
color: baseColor,
|
||||
border: `1px solid ${baseColor}`,
|
||||
}}
|
||||
>
|
||||
{namesString}
|
||||
</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
KeyboardSensor,
|
||||
TouchSensor,
|
||||
closestCenter,
|
||||
useDroppable,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
@@ -67,6 +68,101 @@ import { AddCustomColumnButton, CustomColumnHeader } from './components/CustomCo
|
||||
import TaskListSkeleton from './components/TaskListSkeleton';
|
||||
import ConvertToSubtaskDrawer from '@/components/task-list-common/convert-to-subtask-drawer/convert-to-subtask-drawer';
|
||||
|
||||
// Empty Group Drop Zone Component
|
||||
const EmptyGroupDropZone: React.FC<{
|
||||
groupId: string;
|
||||
visibleColumns: any[];
|
||||
t: (key: string) => string;
|
||||
}> = ({ groupId, visibleColumns, t }) => {
|
||||
const { setNodeRef, isOver, active } = useDroppable({
|
||||
id: `empty-group-${groupId}`,
|
||||
data: {
|
||||
type: 'group',
|
||||
groupId: groupId,
|
||||
isEmpty: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`relative w-full transition-colors duration-200 ${
|
||||
isOver && active ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center min-w-max px-1 py-6">
|
||||
{visibleColumns.map((column, index) => {
|
||||
const emptyColumnStyle = {
|
||||
width: column.width,
|
||||
flexShrink: 0,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={`empty-${column.id}`}
|
||||
className="border-r border-gray-200 dark:border-gray-700"
|
||||
style={emptyColumnStyle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="absolute left-4 top-1/2 transform -translate-y-1/2 flex items-center">
|
||||
<div
|
||||
className={`text-sm px-4 py-3 rounded-md border shadow-sm transition-colors duration-200 ${
|
||||
isOver && active
|
||||
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 border-blue-300 dark:border-blue-600'
|
||||
: 'text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
>
|
||||
{isOver && active ? t('dropTaskHere') || 'Drop task here' : t('noTasksInGroup')}
|
||||
</div>
|
||||
</div>
|
||||
{isOver && active && (
|
||||
<div className="absolute inset-0 border-2 border-dashed border-blue-400 dark:border-blue-500 rounded-md pointer-events-none" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Placeholder Drop Indicator Component
|
||||
const PlaceholderDropIndicator: React.FC<{
|
||||
isVisible: boolean;
|
||||
visibleColumns: any[];
|
||||
}> = ({ isVisible, visibleColumns }) => {
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center min-w-max px-1 border-2 border-dashed border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/20 rounded-md mx-1 my-1 transition-all duration-200 ease-in-out"
|
||||
style={{ minWidth: 'max-content', height: '40px' }}
|
||||
>
|
||||
{visibleColumns.map((column, index) => {
|
||||
const columnStyle = {
|
||||
width: column.width,
|
||||
flexShrink: 0,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={`placeholder-${column.id}`}
|
||||
className="flex items-center justify-center h-full"
|
||||
style={columnStyle}
|
||||
>
|
||||
{/* Show "Drop task here" message in the title column */}
|
||||
{column.id === 'title' && (
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400 font-medium opacity-75">
|
||||
Drop task here
|
||||
</div>
|
||||
)}
|
||||
{/* Show subtle placeholder content in other columns */}
|
||||
{column.id !== 'title' && column.id !== 'dragHandle' && (
|
||||
<div className="w-full h-4 mx-1 bg-white dark:bg-gray-700 rounded opacity-50" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Hooks and utilities
|
||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
@@ -127,7 +223,7 @@ const TaskListV2Section: React.FC = () => {
|
||||
);
|
||||
|
||||
// Custom hooks
|
||||
const { activeId, handleDragStart, handleDragOver, handleDragEnd } = useDragAndDrop(
|
||||
const { activeId, overId, handleDragStart, handleDragOver, handleDragEnd } = useDragAndDrop(
|
||||
allTasks,
|
||||
groups
|
||||
);
|
||||
@@ -465,31 +561,11 @@ const TaskListV2Section: React.FC = () => {
|
||||
projectId={urlProjectId || ''}
|
||||
/>
|
||||
{isGroupEmpty && !isGroupCollapsed && (
|
||||
<div className="relative w-full">
|
||||
<div className="flex items-center min-w-max px-1 py-6">
|
||||
{visibleColumns.map((column, index) => {
|
||||
const emptyColumnStyle = {
|
||||
width: column.width,
|
||||
flexShrink: 0,
|
||||
...(column.id === 'labels' && column.width === 'auto'
|
||||
? { minWidth: '200px', flexGrow: 1 }
|
||||
: {}),
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={`empty-${column.id}`}
|
||||
className="border-r border-gray-200 dark:border-gray-700"
|
||||
style={emptyColumnStyle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="absolute left-4 top-1/2 transform -translate-y-1/2 flex items-center">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900 px-4 py-3 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
{t('noTasksInGroup')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyGroupDropZone
|
||||
groupId={group.id}
|
||||
visibleColumns={visibleColumns}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -546,12 +622,6 @@ const TaskListV2Section: React.FC = () => {
|
||||
const columnStyle: ColumnStyle = {
|
||||
width: column.width,
|
||||
flexShrink: 0,
|
||||
...(column.id === 'labels' && column.width === 'auto'
|
||||
? {
|
||||
minWidth: '200px',
|
||||
flexGrow: 1,
|
||||
}
|
||||
: {}),
|
||||
...((column as any).minWidth && { minWidth: (column as any).minWidth }),
|
||||
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
|
||||
};
|
||||
@@ -687,8 +757,9 @@ const TaskListV2Section: React.FC = () => {
|
||||
{renderGroup(groupIndex)}
|
||||
|
||||
{/* Group Tasks */}
|
||||
{!collapsedGroups.has(group.id) &&
|
||||
group.tasks.map((task, taskIndex) => {
|
||||
{!collapsedGroups.has(group.id) && (
|
||||
group.tasks.length > 0 ? (
|
||||
group.tasks.map((task, taskIndex) => {
|
||||
const globalTaskIndex =
|
||||
virtuosoGroups.slice(0, groupIndex).reduce((sum, g) => sum + g.count, 0) +
|
||||
taskIndex;
|
||||
@@ -696,12 +767,41 @@ const TaskListV2Section: React.FC = () => {
|
||||
// Check if this is the first actual task in the group (not AddTaskRow)
|
||||
const isFirstTaskInGroup = taskIndex === 0 && !('isAddTaskRow' in task);
|
||||
|
||||
// Check if we should show drop indicators
|
||||
const isTaskBeingDraggedOver = overId === task.id;
|
||||
const isGroupBeingDraggedOver = overId === group.id;
|
||||
const isFirstTaskInGroupBeingDraggedOver = isFirstTaskInGroup && isTaskBeingDraggedOver;
|
||||
|
||||
return (
|
||||
<div key={task.id || `add-task-${group.id}-${taskIndex}`}>
|
||||
{/* Placeholder drop indicator before first task in group */}
|
||||
{isFirstTaskInGroupBeingDraggedOver && (
|
||||
<PlaceholderDropIndicator isVisible={true} visibleColumns={visibleColumns} />
|
||||
)}
|
||||
|
||||
{/* Placeholder drop indicator between tasks */}
|
||||
{isTaskBeingDraggedOver && !isFirstTaskInGroup && (
|
||||
<PlaceholderDropIndicator isVisible={true} visibleColumns={visibleColumns} />
|
||||
)}
|
||||
|
||||
{renderTask(globalTaskIndex, isFirstTaskInGroup)}
|
||||
|
||||
{/* Placeholder drop indicator at end of group when dragging over group */}
|
||||
{isGroupBeingDraggedOver && taskIndex === group.tasks.length - 1 && (
|
||||
<PlaceholderDropIndicator isVisible={true} visibleColumns={visibleColumns} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
) : (
|
||||
// Handle empty groups with placeholder drop indicator
|
||||
overId === group.id && (
|
||||
<div style={{ minWidth: 'max-content' }}>
|
||||
<PlaceholderDropIndicator isVisible={true} visibleColumns={visibleColumns} />
|
||||
</div>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -710,12 +810,12 @@ const TaskListV2Section: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Drag Overlay */}
|
||||
<DragOverlay dropAnimation={null}>
|
||||
<DragOverlay dropAnimation={{ duration: 200, easing: 'ease-in-out' }}>
|
||||
{activeId ? (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-xl rounded-md border-2 border-blue-400 opacity-95">
|
||||
<div className="bg-white dark:bg-gray-800 shadow-2xl rounded-lg border-2 border-blue-500 dark:border-blue-400 scale-105">
|
||||
<div className="px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<HolderOutlined className="text-blue-500" />
|
||||
<HolderOutlined className="text-blue-500 dark:text-blue-400" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{allTasks.find(task => task.id === activeId)?.name ||
|
||||
|
||||
@@ -113,7 +113,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
|
||||
const style = useMemo(() => ({
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
opacity: isDragging ? 0 : 1, // Completely hide the original task while dragging
|
||||
}), [transform, transition, isDragging]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -252,10 +252,9 @@ interface LabelsColumnProps {
|
||||
}
|
||||
|
||||
export const LabelsColumn: React.FC<LabelsColumnProps> = memo(({ width, task, labelsAdapter, isDarkMode, visibleColumns }) => {
|
||||
const labelsColumn = visibleColumns.find(col => col.id === 'labels');
|
||||
const labelsStyle = {
|
||||
width,
|
||||
...(labelsColumn?.width === 'auto' ? { minWidth: '200px', flexGrow: 1 } : {})
|
||||
flexShrink: 0
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,10 +19,10 @@ export const BASE_COLUMNS = [
|
||||
{ id: 'title', label: 'taskColumn', width: '470px', isSticky: true, key: COLUMN_KEYS.NAME },
|
||||
{ id: 'description', label: 'descriptionColumn', width: '260px', key: COLUMN_KEYS.DESCRIPTION },
|
||||
{ id: 'progress', label: 'progressColumn', width: '120px', key: COLUMN_KEYS.PROGRESS },
|
||||
{ id: 'assignees', label: 'assigneesColumn', width: '150px', key: COLUMN_KEYS.ASSIGNEES },
|
||||
{ id: 'labels', label: 'labelsColumn', width: 'auto', key: COLUMN_KEYS.LABELS },
|
||||
{ id: 'phase', label: 'phaseColumn', width: '120px', key: COLUMN_KEYS.PHASE },
|
||||
{ id: 'status', label: 'statusColumn', width: '120px', key: COLUMN_KEYS.STATUS },
|
||||
{ id: 'assignees', label: 'assigneesColumn', width: '150px', key: COLUMN_KEYS.ASSIGNEES },
|
||||
{ id: 'labels', label: 'labelsColumn', width: '250px', key: COLUMN_KEYS.LABELS },
|
||||
{ id: 'phase', label: 'phaseColumn', width: '120px', key: COLUMN_KEYS.PHASE },
|
||||
{ id: 'priority', label: 'priorityColumn', width: '120px', key: COLUMN_KEYS.PRIORITY },
|
||||
{ id: 'timeTracking', label: 'timeTrackingColumn', width: '120px', key: COLUMN_KEYS.TIME_TRACKING },
|
||||
{ id: 'estimation', label: 'estimationColumn', width: '120px', key: COLUMN_KEYS.ESTIMATION },
|
||||
|
||||
@@ -17,6 +17,7 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [overId, setOverId] = useState<string | null>(null);
|
||||
|
||||
// Helper function to emit socket event for persistence
|
||||
const emitTaskSortChange = useCallback(
|
||||
@@ -35,35 +36,67 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
// 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;
|
||||
// Use new bulk update approach - recalculate ALL task orders to prevent duplicates
|
||||
const taskUpdates = [];
|
||||
|
||||
// Create a copy of all groups and perform the move operation
|
||||
const updatedGroups = groups.map(group => ({
|
||||
...group,
|
||||
taskIds: [...group.taskIds]
|
||||
}));
|
||||
|
||||
// Find the source and target groups in our copy
|
||||
const sourceGroupCopy = updatedGroups.find(g => g.id === sourceGroup.id)!;
|
||||
const targetGroupCopy = updatedGroups.find(g => g.id === targetGroup.id)!;
|
||||
|
||||
if (sourceGroup.id === targetGroup.id) {
|
||||
// Same group - reorder within the group
|
||||
const sourceIndex = sourceGroupCopy.taskIds.indexOf(taskId);
|
||||
// Remove task from old position
|
||||
sourceGroupCopy.taskIds.splice(sourceIndex, 1);
|
||||
// Insert at new position
|
||||
sourceGroupCopy.taskIds.splice(insertIndex, 0, taskId);
|
||||
} 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;
|
||||
// Different groups - move task between groups
|
||||
// Remove from source group
|
||||
const sourceIndex = sourceGroupCopy.taskIds.indexOf(taskId);
|
||||
sourceGroupCopy.taskIds.splice(sourceIndex, 1);
|
||||
|
||||
// Add to target group
|
||||
targetGroupCopy.taskIds.splice(insertIndex, 0, taskId);
|
||||
}
|
||||
|
||||
// Now assign sequential sort orders to ALL tasks across ALL groups
|
||||
let currentSortOrder = 0;
|
||||
updatedGroups.forEach(group => {
|
||||
group.taskIds.forEach(id => {
|
||||
const update: any = {
|
||||
task_id: id,
|
||||
sort_order: currentSortOrder
|
||||
};
|
||||
|
||||
// Add group-specific fields for the moved task if it changed groups
|
||||
if (id === taskId && sourceGroup.id !== targetGroup.id) {
|
||||
if (currentGrouping === 'status') {
|
||||
update.status_id = targetGroup.id;
|
||||
} else if (currentGrouping === 'priority') {
|
||||
update.priority_id = targetGroup.id;
|
||||
} else if (currentGrouping === 'phase') {
|
||||
update.phase_id = targetGroup.id;
|
||||
}
|
||||
}
|
||||
|
||||
taskUpdates.push(update);
|
||||
currentSortOrder++;
|
||||
});
|
||||
});
|
||||
|
||||
const socketData = {
|
||||
project_id: projectId,
|
||||
from_index: fromIndex,
|
||||
to_index: toIndex,
|
||||
to_last_index: toLastIndex,
|
||||
group_by: currentGrouping || 'status',
|
||||
task_updates: taskUpdates,
|
||||
from_group: sourceGroup.id,
|
||||
to_group: targetGroup.id,
|
||||
group_by: currentGrouping || 'status',
|
||||
task: {
|
||||
id: task.id,
|
||||
project_id: projectId,
|
||||
@@ -76,7 +109,7 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
console.log('Emitting TASK_SORT_ORDER_CHANGE:', socketData);
|
||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), socketData);
|
||||
},
|
||||
[socket, connected, projectId, allTasks, currentGrouping, currentSession]
|
||||
[socket, connected, projectId, allTasks, groups, currentGrouping, currentSession]
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
@@ -87,11 +120,17 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
(event: DragOverEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over) return;
|
||||
if (!over) {
|
||||
setOverId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeId = active.id;
|
||||
const overId = over.id;
|
||||
|
||||
// Set the overId for drop indicators
|
||||
setOverId(overId as string);
|
||||
|
||||
// Find the active task and the item being dragged over
|
||||
const activeTask = allTasks.find(task => task.id === activeId);
|
||||
if (!activeTask) return;
|
||||
@@ -126,6 +165,7 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
setActiveId(null);
|
||||
setOverId(null);
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
@@ -148,11 +188,16 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're dropping on a task or a group
|
||||
// Check if we're dropping on a task, group, or empty group
|
||||
const overTask = allTasks.find(task => task.id === overId);
|
||||
const overGroup = groups.find(group => group.id === overId);
|
||||
|
||||
// Check if dropping on empty group drop zone
|
||||
const isEmptyGroupDrop = typeof overId === 'string' && overId.startsWith('empty-group-');
|
||||
const emptyGroupId = isEmptyGroupDrop ? overId.replace('empty-group-', '') : null;
|
||||
const emptyGroup = emptyGroupId ? groups.find(group => group.id === emptyGroupId) : null;
|
||||
|
||||
let targetGroup = overGroup;
|
||||
let targetGroup = overGroup || emptyGroup;
|
||||
let insertIndex = 0;
|
||||
|
||||
if (overTask) {
|
||||
@@ -165,6 +210,10 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
// Dropping on a group (at the end)
|
||||
targetGroup = overGroup;
|
||||
insertIndex = targetGroup.taskIds.length;
|
||||
} else if (emptyGroup) {
|
||||
// Dropping on an empty group
|
||||
targetGroup = emptyGroup;
|
||||
insertIndex = 0; // First position in empty group
|
||||
}
|
||||
|
||||
if (!targetGroup) {
|
||||
@@ -238,6 +287,7 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
||||
|
||||
return {
|
||||
activeId,
|
||||
overId,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragEnd,
|
||||
|
||||
@@ -14,10 +14,10 @@ const DEFAULT_FIELDS: TaskListField[] = [
|
||||
{ key: 'KEY', label: 'Key', visible: false, order: 1 },
|
||||
{ key: 'DESCRIPTION', label: 'Description', visible: false, order: 2 },
|
||||
{ key: 'PROGRESS', label: 'Progress', visible: true, order: 3 },
|
||||
{ key: 'ASSIGNEES', label: 'Assignees', visible: true, order: 4 },
|
||||
{ key: 'LABELS', label: 'Labels', visible: true, order: 5 },
|
||||
{ key: 'PHASE', label: 'Phase', visible: true, order: 6 },
|
||||
{ key: 'STATUS', label: 'Status', visible: true, order: 7 },
|
||||
{ key: 'STATUS', label: 'Status', visible: true, order: 4 },
|
||||
{ key: 'ASSIGNEES', label: 'Assignees', visible: true, order: 5 },
|
||||
{ key: 'LABELS', label: 'Labels', visible: true, order: 6 },
|
||||
{ key: 'PHASE', label: 'Phase', visible: true, order: 7 },
|
||||
{ key: 'PRIORITY', label: 'Priority', visible: true, order: 8 },
|
||||
{ key: 'TIME_TRACKING', label: 'Time Tracking', visible: true, order: 9 },
|
||||
{ key: 'ESTIMATION', label: 'Estimation', visible: false, order: 10 },
|
||||
|
||||
Reference in New Issue
Block a user