import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { DndContext, DragOverlay, DragStartEvent, DragEndEvent, DragOverEvent, closestCorners, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'; import { Card, Spin, Empty } from 'antd'; import { RootState } from '@/app/store'; import { taskManagementSelectors, reorderTasks, moveTaskToGroup, optimisticTaskMove, setLoading, fetchTasks, fetchTasksV3, selectTaskGroupsV3, selectCurrentGroupingV3, } from '@/features/task-management/task-management.slice'; import { selectTaskGroups, selectCurrentGrouping, setCurrentGrouping, } from '@/features/task-management/grouping.slice'; import { selectSelectedTaskIds, toggleTaskSelection, clearSelection, } from '@/features/task-management/selection.slice'; import { Task } from '@/types/task-management.types'; import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers'; import TaskRow from './task-row'; // import BulkActionBar from './bulk-action-bar'; import VirtualizedTaskList from './virtualized-task-list'; import { AppDispatch } from '@/app/store'; // Import the improved TaskListFilters component const ImprovedTaskFilters = React.lazy( () => import('./improved-task-filters') ); interface TaskListBoardProps { projectId: string; className?: string; } interface DragState { activeTask: Task | null; activeGroupId: string | null; } // Throttle utility for performance optimization const throttle = void>(func: T, delay: number): T => { let timeoutId: NodeJS.Timeout | null = null; let lastExecTime = 0; return ((...args: any[]) => { const currentTime = Date.now(); if (currentTime - lastExecTime > delay) { func(...args); lastExecTime = currentTime; } else { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout( () => { func(...args); lastExecTime = Date.now(); }, delay - (currentTime - lastExecTime) ); } }) as T; }; const TaskListBoard: React.FC = ({ projectId, className = '' }) => { const dispatch = useDispatch(); const [dragState, setDragState] = useState({ activeTask: null, activeGroupId: null, }); // Refs for performance optimization const dragOverTimeoutRef = useRef(null); const containerRef = useRef(null); // Enable real-time socket updates for task changes useTaskSocketHandlers(); // Redux selectors using V3 API (pre-processed data, minimal loops) const tasks = useSelector(taskManagementSelectors.selectAll); const taskGroups = useSelector(selectTaskGroupsV3); // Pre-processed groups from backend const currentGrouping = useSelector(selectCurrentGroupingV3); // Current grouping from backend const selectedTaskIds = useSelector(selectSelectedTaskIds); const loading = useSelector((state: RootState) => state.taskManagement.loading); const error = useSelector((state: RootState) => state.taskManagement.error); // Get theme from Redux store const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); // Drag and Drop sensors - optimized for better performance const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 3, // Reduced from 8 for more responsive dragging }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // Fetch task groups when component mounts or dependencies change useEffect(() => { if (projectId) { // Fetch real tasks from V3 API (minimal processing needed) dispatch(fetchTasksV3(projectId)); } }, [dispatch, projectId, currentGrouping]); // Memoized calculations - optimized const allTaskIds = useMemo(() => tasks.map(task => task.id), [tasks]); const totalTasksCount = useMemo(() => tasks.length, [tasks]); const hasSelection = selectedTaskIds.length > 0; // Memoized handlers for better performance const handleGroupingChange = useCallback( (newGroupBy: 'status' | 'priority' | 'phase') => { dispatch(setCurrentGrouping(newGroupBy)); }, [dispatch] ); const handleDragStart = useCallback( (event: DragStartEvent) => { const { active } = event; const taskId = active.id as string; // Find the task and its group const activeTask = tasks.find(t => t.id === taskId) || null; 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}`; } } setDragState({ activeTask, activeGroupId, }); }, [tasks, currentGrouping] ); // Throttled drag over handler for better performance const handleDragOver = useCallback( throttle((event: DragOverEvent) => { const { active, over } = event; if (!over || !dragState.activeTask) return; const activeTaskId = active.id as string; const overContainer = over.id as string; // Clear any existing timeout if (dragOverTimeoutRef.current) { clearTimeout(dragOverTimeoutRef.current); } // Optimistic update with throttling dragOverTimeoutRef.current = setTimeout(() => { // Only update if we're hovering over a different container const targetTask = tasks.find(t => t.id === overContainer); let targetGroupId = overContainer; if (targetTask) { if (currentGrouping === 'status') { targetGroupId = `status-${targetTask.status}`; } else if (currentGrouping === 'priority') { targetGroupId = `priority-${targetTask.priority}`; } else if (currentGrouping === 'phase') { targetGroupId = `phase-${targetTask.phase}`; } } if (targetGroupId !== dragState.activeGroupId) { // Perform optimistic update for visual feedback const targetGroup = taskGroups.find(g => g.id === targetGroupId); if (targetGroup) { dispatch( optimisticTaskMove({ taskId: activeTaskId, newGroupId: targetGroupId, newIndex: targetGroup.taskIds.length, }) ); } } }, 50); // 50ms throttle for drag over events }, 50), [dragState, tasks, taskGroups, currentGrouping, dispatch] ); const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; // Clear any pending drag over timeouts if (dragOverTimeoutRef.current) { clearTimeout(dragOverTimeoutRef.current); dragOverTimeoutRef.current = null; } // Reset drag state immediately for better UX const currentDragState = dragState; setDragState({ activeTask: null, activeGroupId: null, }); if (!over || !currentDragState.activeTask || !currentDragState.activeGroupId) { return; } const activeTaskId = active.id as string; const overContainer = 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; let targetIndex = -1; // Check if dropping on a task or a group const targetTask = tasks.find(t => t.id === overContainer); 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}`; } // Find the index of the target task within its group const targetGroup = taskGroups.find(g => g.id === targetGroupId); if (targetGroup) { targetIndex = targetGroup.taskIds.indexOf(targetTask.id); } } 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 const sourceGroup = taskGroups.find(g => g.id === currentDragState.activeGroupId); const targetGroup = taskGroups.find(g => g.id === targetGroupId); if (sourceGroup && targetGroup && targetIndex !== -1) { const sourceIndex = sourceGroup.taskIds.indexOf(activeTaskId); const finalTargetIndex = targetIndex === -1 ? targetGroup.taskIds.length : targetIndex; // 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 => tasks.find(t => 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 dispatch( reorderTasks({ taskIds: [activeTaskId, ...allTasksInTargetGroup.map(t => t.id)], newOrder: [currentDragState.activeTask!.order, ...newOrder], }) ); } } }, [dragState, tasks, taskGroups, currentGrouping, dispatch] ); const handleSelectTask = useCallback( (taskId: string, selected: boolean) => { dispatch(toggleTaskSelection(taskId)); }, [dispatch] ); const handleToggleSubtasks = useCallback((taskId: string) => { // Implementation for toggling subtasks console.log('Toggle subtasks for task:', taskId); }, []); // Memoized DragOverlay content for better performance const dragOverlayContent = useMemo(() => { if (!dragState.activeTask || !dragState.activeGroupId) return null; return ( ); }, [dragState.activeTask, dragState.activeGroupId, projectId, currentGrouping]); // Cleanup effect useEffect(() => { return () => { if (dragOverTimeoutRef.current) { clearTimeout(dragOverTimeoutRef.current); } }; }, []); if (error) { return ( ); } return (
{/* Task Filters */}
Loading filters...
}>
{/* Virtualized Task Groups Container */}
{loading ? (
) : taskGroups.length === 0 ? ( ) : (
{taskGroups.map((group, index) => { // Calculate dynamic height for each group const groupTasks = group.taskIds.length; const baseHeight = 120; // Header + column headers + add task row const taskRowsHeight = groupTasks * 40; // 40px per task row const minGroupHeight = 300; // Minimum height for better visual appearance const maxGroupHeight = 600; // Increased maximum height per group const calculatedHeight = baseHeight + taskRowsHeight; const groupHeight = Math.max( minGroupHeight, Math.min(calculatedHeight, maxGroupHeight) ); return ( ); })}
)}
{dragOverlayContent} ); }; export default TaskListBoard;