refactor(task-drag-and-drop): remove unused drag-and-drop hook and simplify task group handling

- Deleted `useTaskDragAndDrop` hook to streamline drag-and-drop functionality.
- Updated `TaskGroupWrapperOptimized` to remove drag-and-drop context and simplify rendering.
- Refactored `TaskListTable` to integrate drag-and-drop directly, enhancing performance and maintainability.
- Adjusted task rendering logic to ensure proper handling of task states during drag operations.
This commit is contained in:
chamiakJ
2025-06-09 07:19:15 +05:30
parent 5e4d78c6f5
commit de28f87c62
4 changed files with 264 additions and 271 deletions

View File

@@ -1,146 +0,0 @@
import { useMemo, useCallback } from 'react';
import {
DndContext,
DragEndEvent,
DragOverEvent,
DragStartEvent,
PointerSensor,
useSensor,
useSensors,
KeyboardSensor,
TouchSensor,
} from '@dnd-kit/core';
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { updateTaskStatus } from '@/features/tasks/tasks.slice';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
export const useTaskDragAndDrop = () => {
const dispatch = useAppDispatch();
const { taskGroups, groupBy } = useAppSelector(state => ({
taskGroups: state.taskReducer.taskGroups,
groupBy: state.taskReducer.groupBy,
}));
// Memoize sensors configuration for better performance
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
})
);
const handleDragStart = useCallback((event: DragStartEvent) => {
// Add visual feedback for drag start
const { active } = event;
if (active) {
document.body.style.cursor = 'grabbing';
}
}, []);
const handleDragOver = useCallback((event: DragOverEvent) => {
// Handle drag over logic if needed
// This can be used for visual feedback during drag
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
// Reset cursor
document.body.style.cursor = '';
const { active, over } = event;
if (!active || !over || !taskGroups) {
return;
}
try {
const activeId = active.id as string;
const overId = over.id as string;
// Find the task being dragged
let draggedTask: IProjectTask | null = null;
let sourceGroupId: string | null = null;
for (const group of taskGroups) {
const task = group.tasks?.find((t: IProjectTask) => t.id === activeId);
if (task) {
draggedTask = task;
sourceGroupId = group.id;
break;
}
}
if (!draggedTask || !sourceGroupId) {
console.warn('Could not find dragged task');
return;
}
// Determine target group
let targetGroupId: string | null = null;
// Check if dropped on a group container
const targetGroup = taskGroups.find((group: ITaskListGroup) => group.id === overId);
if (targetGroup) {
targetGroupId = targetGroup.id;
} else {
// Check if dropped on another task
for (const group of taskGroups) {
const targetTask = group.tasks?.find((t: IProjectTask) => t.id === overId);
if (targetTask) {
targetGroupId = group.id;
break;
}
}
}
if (!targetGroupId || targetGroupId === sourceGroupId) {
return; // No change needed
}
// Update task status based on group change
const targetGroupData = taskGroups.find((group: ITaskListGroup) => group.id === targetGroupId);
if (targetGroupData && groupBy === 'status') {
const updatePayload: any = {
task_id: draggedTask.id,
status_id: targetGroupData.id,
};
if (draggedTask.parent_task_id) {
updatePayload.parent_task = draggedTask.parent_task_id;
}
dispatch(updateTaskStatus(updatePayload));
}
} catch (error) {
console.error('Error handling drag end:', error);
}
},
[taskGroups, groupBy, dispatch]
);
// Memoize the drag and drop configuration
const dragAndDropConfig = useMemo(
() => ({
sensors,
onDragStart: handleDragStart,
onDragOver: handleDragOver,
onDragEnd: handleDragEnd,
}),
[sensors, handleDragStart, handleDragOver, handleDragEnd]
);
return dragAndDropConfig;
};

View File

@@ -3,11 +3,6 @@ import { createPortal } from 'react-dom';
import Flex from 'antd/es/flex'; import Flex from 'antd/es/flex';
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
import {
DndContext,
pointerWithin,
} from '@dnd-kit/core';
import { ITaskListGroup } from '@/types/tasks/taskList.types'; import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
@@ -16,7 +11,6 @@ import TaskListBulkActionsBar from '@/components/taskListCommon/task-list-bulk-a
import TaskTemplateDrawer from '@/components/task-templates/task-template-drawer'; import TaskTemplateDrawer from '@/components/task-templates/task-template-drawer';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers'; import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { useTaskDragAndDrop } from '@/hooks/useTaskDragAndDrop';
interface TaskGroupWrapperOptimizedProps { interface TaskGroupWrapperOptimizedProps {
taskGroups: ITaskListGroup[]; taskGroups: ITaskListGroup[];
@@ -28,14 +22,6 @@ const TaskGroupWrapperOptimized = ({ taskGroups, groupBy }: TaskGroupWrapperOpti
// Use extracted hooks // Use extracted hooks
useTaskSocketHandlers(); useTaskSocketHandlers();
const {
activeId,
sensors,
handleDragStart,
handleDragEnd,
handleDragOver,
resetTaskRowStyles,
} = useTaskDragAndDrop({ taskGroups, groupBy });
// Memoize task groups with colors // Memoize task groups with colors
const taskGroupsWithColors = useMemo(() => const taskGroupsWithColors = useMemo(() =>
@@ -46,18 +32,17 @@ const TaskGroupWrapperOptimized = ({ taskGroups, groupBy }: TaskGroupWrapperOpti
[taskGroups, themeMode] [taskGroups, themeMode]
); );
// Add drag styles // Add drag styles without animations
useEffect(() => { useEffect(() => {
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
.task-row[data-is-dragging="true"] { .task-row[data-is-dragging="true"] {
opacity: 0.5 !important; opacity: 0.5 !important;
transform: rotate(5deg) !important;
z-index: 1000 !important; z-index: 1000 !important;
position: relative !important; position: relative !important;
} }
.task-row { .task-row {
transition: transform 0.2s ease, opacity 0.2s ease; /* Remove transitions during drag operations */
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
@@ -67,45 +52,31 @@ const TaskGroupWrapperOptimized = ({ taskGroups, groupBy }: TaskGroupWrapperOpti
}; };
}, []); }, []);
// Handle animation cleanup after drag ends // Remove the animation cleanup since we're simplifying the approach
useIsomorphicLayoutEffect(() => {
if (activeId === null) {
const timeoutId = setTimeout(resetTaskRowStyles, 50);
return () => clearTimeout(timeoutId);
}
}, [activeId, resetTaskRowStyles]);
return ( return (
<DndContext <Flex gap={24} vertical>
sensors={sensors} {taskGroupsWithColors.map(taskGroup => (
collisionDetection={pointerWithin} <TaskListTableWrapper
onDragStart={handleDragStart} key={taskGroup.id}
onDragEnd={handleDragEnd} taskList={taskGroup.tasks}
onDragOver={handleDragOver} tableId={taskGroup.id}
> name={taskGroup.name}
<Flex gap={24} vertical> groupBy={groupBy}
{taskGroupsWithColors.map(taskGroup => ( statusCategory={taskGroup.category_id}
<TaskListTableWrapper color={taskGroup.displayColor}
key={taskGroup.id} activeId={null}
taskList={taskGroup.tasks} />
tableId={taskGroup.id} ))}
name={taskGroup.name}
groupBy={groupBy}
statusCategory={taskGroup.category_id}
color={taskGroup.displayColor}
activeId={activeId}
/>
))}
{createPortal(<TaskListBulkActionsBar />, document.body, 'bulk-action-container')} {createPortal(<TaskListBulkActionsBar />, document.body, 'bulk-action-container')}
{createPortal( {createPortal(
<TaskTemplateDrawer showDrawer={false} selectedTemplateId="" onClose={() => {}} />, <TaskTemplateDrawer showDrawer={false} selectedTemplateId="" onClose={() => {}} />,
document.body, document.body,
'task-template-drawer' 'task-template-drawer'
)} )}
</Flex> </Flex>
</DndContext>
); );
}; };

View File

@@ -249,7 +249,7 @@ const TaskListTableWrapper = ({
className={`border-l-[3px] relative after:content after:absolute after:h-full after:w-1 after:z-10 after:top-0 after:left-0`} className={`border-l-[3px] relative after:content after:absolute after:h-full after:w-1 after:z-10 after:top-0 after:left-0`}
color={color} color={color}
> >
<TaskListTable taskList={taskList} tableId={tableId} activeId={activeId} /> <TaskListTable taskList={taskList} tableId={tableId} activeId={activeId} groupBy={groupBy} />
</Collapsible> </Collapsible>
</Flex> </Flex>
</ConfigProvider> </ConfigProvider>

View File

@@ -12,8 +12,8 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { DraggableAttributes, UniqueIdentifier } from '@dnd-kit/core'; import { DraggableAttributes, UniqueIdentifier } from '@dnd-kit/core';
import { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities'; import { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
import { DragOverlay } from '@dnd-kit/core'; import { DragOverlay, DndContext, PointerSensor, useSensor, useSensors, KeyboardSensor, TouchSensor } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { SortableContext, verticalListSortingStrategy, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { DragEndEvent } from '@dnd-kit/core'; import { DragEndEvent } from '@dnd-kit/core';
import { List, Card, Avatar, Dropdown, Empty, Divider, Button } from 'antd'; import { List, Card, Avatar, Dropdown, Empty, Divider, Button } from 'antd';
@@ -50,19 +50,20 @@ import StatusDropdown from '@/components/task-list-common/status-dropdown/status
import PriorityDropdown from '@/components/task-list-common/priorityDropdown/priority-dropdown'; import PriorityDropdown from '@/components/task-list-common/priorityDropdown/priority-dropdown';
import AddCustomColumnButton from './custom-columns/custom-column-modal/add-custom-column-button'; import AddCustomColumnButton from './custom-columns/custom-column-modal/add-custom-column-button';
import { fetchSubTasks, reorderTasks, toggleTaskRowExpansion, updateCustomColumnValue } from '@/features/tasks/tasks.slice'; import { fetchSubTasks, reorderTasks, toggleTaskRowExpansion, updateCustomColumnValue } from '@/features/tasks/tasks.slice';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { useAuthService } from '@/hooks/useAuth'; import { useAuthService } from '@/hooks/useAuth';
import ConfigPhaseButton from '@/features/projects/singleProject/phase/ConfigPhaseButton'; import ConfigPhaseButton from '@/features/projects/singleProject/phase/ConfigPhaseButton';
import PhaseDropdown from '@/components/taskListCommon/phase-dropdown/phase-dropdown'; import PhaseDropdown from '@/components/taskListCommon/phase-dropdown/phase-dropdown';
import CustomColumnModal from './custom-columns/custom-column-modal/custom-column-modal'; import CustomColumnModal from './custom-columns/custom-column-modal/custom-column-modal';
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice'; import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
import SingleAvatar from '@/components/common/single-avatar/single-avatar'; import SingleAvatar from '@/components/common/single-avatar/single-avatar';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
interface TaskListTableProps { interface TaskListTableProps {
taskList: IProjectTask[] | null; taskList: IProjectTask[] | null;
tableId: string; tableId: string;
activeId?: string | null; activeId?: string | null;
groupBy?: string;
} }
interface DraggableRowProps { interface DraggableRowProps {
@@ -71,44 +72,50 @@ interface DraggableRowProps {
groupId: string; groupId: string;
} }
// Add a simplified EmptyRow component that doesn't use hooks // Remove the EmptyRow component and fix the DraggableRow
const EmptyRow = () => null;
// Simplify DraggableRow to eliminate conditional hook calls
const DraggableRow = ({ task, children, groupId }: DraggableRowProps) => { const DraggableRow = ({ task, children, groupId }: DraggableRowProps) => {
// Return the EmptyRow component without using any hooks // Always call hooks in the same order - never conditionally
if (!task?.id) return <EmptyRow />;
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: task.id as UniqueIdentifier, id: task?.id || 'empty-task', // Provide fallback ID
data: { data: {
type: 'task', type: 'task',
task, task,
groupId, groupId,
}, },
disabled: !task?.id, // Disable dragging for invalid tasks
transition: null, // Disable sortable transitions
}); });
// If task is invalid, return null to not render anything
if (!task?.id) {
return null;
}
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition: isDragging ? 'none' : transition, // Disable transition during drag
opacity: isDragging ? 0.3 : 1, opacity: isDragging ? 0.3 : 1,
position: 'relative' as const, position: 'relative' as const,
zIndex: isDragging ? 1 : 'auto', zIndex: isDragging ? 1 : 'auto',
backgroundColor: isDragging ? 'var(--dragging-bg)' : undefined, backgroundColor: isDragging ? 'var(--dragging-bg)' : undefined,
}; // Handle border styling to avoid conflicts between shorthand and individual properties
...(isDragging ? {
// Handle border styling separately to avoid conflicts borderTopWidth: '1px',
const borderStyle = { borderRightWidth: '1px',
borderStyle: isDragging ? 'solid' : undefined, borderBottomWidth: '1px',
borderWidth: isDragging ? '1px' : undefined, borderLeftWidth: '1px',
borderColor: isDragging ? 'var(--border-color)' : undefined, borderStyle: 'solid',
borderBottomWidth: document.documentElement.getAttribute('data-theme') === 'light' && !isDragging ? '2px' : undefined borderColor: 'var(--border-color)',
} : {
// Only set borderBottomWidth when not dragging to avoid conflicts
borderBottomWidth: document.documentElement.getAttribute('data-theme') === 'light' ? '2px' : undefined
})
}; };
return ( return (
<tr <tr
ref={setNodeRef} ref={setNodeRef}
style={{ ...style, ...borderStyle }} style={style}
className={`task-row h-[42px] ${isDragging ? 'shadow-lg' : ''}`} className={`task-row h-[42px] ${isDragging ? 'shadow-lg' : ''}`}
data-is-dragging={isDragging ? 'true' : 'false'} data-is-dragging={isDragging ? 'true' : 'false'}
data-group-id={groupId} data-group-id={groupId}
@@ -1208,12 +1215,33 @@ const renderCustomColumnContent = (
return customComponents[fieldType] ? customComponents[fieldType]() : null; return customComponents[fieldType] ? customComponents[fieldType]() : null;
}; };
const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, activeId }) => { const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, activeId, groupBy }) => {
const { t } = useTranslation('task-list-table'); const { t } = useTranslation('task-list-table');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const currentSession = useAuthService().getCurrentSession(); const currentSession = useAuthService().getCurrentSession();
const { socket } = useSocket(); const { socket } = useSocket();
// Add drag state
const [dragActiveId, setDragActiveId] = useState<string | null>(null);
// Configure sensors for drag and drop
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
})
);
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const columnList = useAppSelector(state => state.taskReducer.columns); const columnList = useAppSelector(state => state.taskReducer.columns);
const visibleColumns = columnList.filter(column => column.pinned); const visibleColumns = columnList.filter(column => column.pinned);
@@ -1525,27 +1553,8 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
// Use the tasks from the current group if available, otherwise fall back to taskList prop // Use the tasks from the current group if available, otherwise fall back to taskList prop
const displayTasks = currentGroup?.tasks || taskList || []; const displayTasks = currentGroup?.tasks || taskList || [];
const handleDragEnd = (event: DragEndEvent) => { // Remove the local handleDragEnd as it conflicts with the main DndContext
const { active, over } = event; // All drag handling is now done at the TaskGroupWrapperOptimized level
if (!over || active.id === over.id) return;
const activeIndex = displayTasks.findIndex(task => task.id === active.id);
const overIndex = displayTasks.findIndex(task => task.id === over.id);
if (activeIndex !== -1 && overIndex !== -1) {
dispatch(
reorderTasks({
activeGroupId: tableId,
overGroupId: tableId,
fromIndex: activeIndex,
toIndex: overIndex,
task: displayTasks[activeIndex],
updatedSourceTasks: displayTasks,
updatedTargetTasks: displayTasks,
})
);
}
};
const handleCustomColumnSettings = (columnKey: string) => { const handleCustomColumnSettings = (columnKey: string) => {
if (!columnKey) return; if (!columnKey) return;
@@ -1554,12 +1563,169 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
dispatch(toggleCustomColumnModalOpen(true)); dispatch(toggleCustomColumnModalOpen(true));
}; };
// Drag and drop handlers
const handleDragStart = (event: any) => {
setDragActiveId(event.active.id);
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setDragActiveId(null);
if (!over || !active || active.id === over.id) {
return;
}
const activeTask = displayTasks.find(task => task.id === active.id);
if (!activeTask) {
console.error('Active task not found:', { activeId: active.id, displayTasks: displayTasks.map(t => ({ id: t.id, name: t.name })) });
return;
}
console.log('Found activeTask:', {
id: activeTask.id,
name: activeTask.name,
status_id: activeTask.status_id,
status: activeTask.status,
priority: activeTask.priority,
project_id: project?.id,
team_id: project?.team_id,
fullProject: project
});
// Use the tableId directly as the group ID (it should be the group ID)
const currentGroupId = tableId;
console.log('Drag operation:', {
activeId: active.id,
overId: over.id,
tableId,
currentGroupId,
displayTasksLength: displayTasks.length
});
// Check if this is a reorder within the same group
const overTask = displayTasks.find(task => task.id === over.id);
if (overTask) {
// Reordering within the same group
const oldIndex = displayTasks.findIndex(task => task.id === active.id);
const newIndex = displayTasks.findIndex(task => task.id === over.id);
console.log('Reorder details:', { oldIndex, newIndex, activeTask: activeTask.name });
if (oldIndex !== newIndex && oldIndex !== -1 && newIndex !== -1) {
// Get the actual sort_order values from the tasks
const fromSortOrder = activeTask.sort_order || oldIndex;
const overTaskAtNewIndex = displayTasks[newIndex];
const toSortOrder = overTaskAtNewIndex?.sort_order || newIndex;
console.log('Sort order details:', {
oldIndex,
newIndex,
fromSortOrder,
toSortOrder,
activeTaskSortOrder: activeTask.sort_order,
overTaskSortOrder: overTaskAtNewIndex?.sort_order
});
// Create updated task list with reordered tasks
const updatedTasks = [...displayTasks];
const [movedTask] = updatedTasks.splice(oldIndex, 1);
updatedTasks.splice(newIndex, 0, movedTask);
console.log('Dispatching reorderTasks with:', {
activeGroupId: currentGroupId,
overGroupId: currentGroupId,
fromIndex: oldIndex,
toIndex: newIndex,
taskName: activeTask.name
});
// Update local state immediately for better UX
dispatch(reorderTasks({
activeGroupId: currentGroupId,
overGroupId: currentGroupId,
fromIndex: oldIndex,
toIndex: newIndex,
task: activeTask,
updatedSourceTasks: updatedTasks,
updatedTargetTasks: updatedTasks
}));
// Send socket event for backend sync
if (socket && project?.id && active.id && activeTask.id) {
// Helper function to validate UUID or return null
const validateUUID = (value: string | undefined | null): string | null => {
if (!value || value.trim() === '') return null;
// Basic UUID format check (8-4-4-4-12 characters)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value) ? value : null;
};
const body = {
from_index: fromSortOrder,
to_index: toSortOrder,
project_id: project.id,
from_group: currentGroupId,
to_group: currentGroupId,
group_by: groupBy || 'status', // Use the groupBy prop
to_last_index: false,
task: {
id: activeTask.id, // Use activeTask.id instead of active.id to ensure it's valid
project_id: project.id,
status: validateUUID(activeTask.status_id || activeTask.status),
priority: validateUUID(activeTask.priority)
},
team_id: project.team_id || currentSession?.team_id || ''
};
// Validate required fields before sending
if (!body.task.id) {
console.error('Cannot send socket event: task.id is missing', { activeTask, active });
return;
}
console.log('Validated values:', {
from_index: body.from_index,
to_index: body.to_index,
status: body.task.status,
priority: body.task.priority,
team_id: body.team_id,
originalStatus: activeTask.status_id || activeTask.status,
originalPriority: activeTask.priority,
originalTeamId: project.team_id,
sessionTeamId: currentSession?.team_id,
finalTeamId: body.team_id
});
console.log('Sending socket event:', body);
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
} else {
console.error('Cannot send socket event: missing required data', {
hasSocket: !!socket,
hasProjectId: !!project?.id,
hasActiveId: !!active.id,
hasActiveTaskId: !!activeTask.id,
activeTask,
active
});
}
}
}
};
return ( return (
<div className={`border-x border-b ${customBorderColor}`}> <div className={`border-x border-b ${customBorderColor}`}>
<SortableContext <DndContext
items={(displayTasks?.map(t => t.id).filter(Boolean) || []) as string[]} sensors={sensors}
strategy={verticalListSortingStrategy} onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
autoScroll={false} // Disable auto-scroll animations
> >
<SortableContext
items={(displayTasks?.filter(t => t?.id).map(t => t.id).filter(Boolean) || []) as string[]}
strategy={verticalListSortingStrategy}
>
<div className={`tasklist-container-${tableId} min-h-0 max-w-full overflow-x-auto`}> <div className={`tasklist-container-${tableId} min-h-0 max-w-full overflow-x-auto`}>
<table className="rounded-2 w-full min-w-max border-collapse relative"> <table className="rounded-2 w-full min-w-max border-collapse relative">
<thead className="h-[42px]"> <thead className="h-[42px]">
@@ -1611,25 +1777,29 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
</thead> </thead>
<tbody> <tbody>
{displayTasks && displayTasks.length > 0 ? ( {displayTasks && displayTasks.length > 0 ? (
displayTasks.map(task => { displayTasks
const updatedTask = findTaskInGroups(task.id || '') || task; .filter(task => task?.id) // Filter out tasks without valid IDs
.map(task => {
const updatedTask = findTaskInGroups(task.id || '') || task;
return ( return (
<React.Fragment key={updatedTask.id}> <React.Fragment key={updatedTask.id}>
{renderTaskRow(updatedTask)} {renderTaskRow(updatedTask)}
{updatedTask.show_sub_tasks && ( {updatedTask.show_sub_tasks && (
<> <>
{updatedTask?.sub_tasks?.map(subtask => renderTaskRow(subtask, true))} {updatedTask?.sub_tasks?.map(subtask =>
<tr> subtask?.id ? renderTaskRow(subtask, true) : null
<td colSpan={visibleColumns.length + 1}> )}
<AddTaskListRow groupId={tableId} parentTask={updatedTask.id} /> <tr key={`add-subtask-${updatedTask.id}`}>
</td> <td colSpan={visibleColumns.length + 1}>
<AddTaskListRow groupId={tableId} parentTask={updatedTask.id} />
</td>
</tr> </tr>
</> </>
)} )}
</React.Fragment> </React.Fragment>
); );
}) })
) : ( ) : (
<tr> <tr>
<td colSpan={visibleColumns.length + 1} className="ps-2 py-2"> <td colSpan={visibleColumns.length + 1} className="ps-2 py-2">
@@ -1643,17 +1813,15 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
</SortableContext> </SortableContext>
<DragOverlay <DragOverlay
dropAnimation={{ dropAnimation={null} // Disable drop animation
duration: 200,
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
}}
> >
{activeId && displayTasks?.length ? ( {dragActiveId ? (
<table className="w-full"> <div className="bg-white dark:bg-gray-800 shadow-lg rounded border p-2 opacity-90">
<tbody>{renderTaskRow(displayTasks.find(t => t.id === activeId))}</tbody> <span className="text-sm font-medium">Moving task...</span>
</table> </div>
) : null} ) : null}
</DragOverlay> </DragOverlay>
</DndContext>
{/* Add task row is positioned outside of the scrollable area */} {/* Add task row is positioned outside of the scrollable area */}
<div className={`border-t ${customBorderColor}`}> <div className={`border-t ${customBorderColor}`}>