import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useTaskManagementTranslations } from '@/hooks/useTranslationPreloader'; 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, Alert } from 'antd'; import { RootState } from '@/app/store'; import { taskManagementSelectors, reorderTasks, moveTaskToGroup, optimisticTaskMove, reorderTasksInGroup, 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 { selectTaskIds, selectTasks, deselectAll as deselectAllBulk, } from '@/features/projects/bulkActions/bulkActionSlice'; 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'; // import OptimizedBulkActionBar from './optimized-bulk-action-bar'; import VirtualizedTaskList from './virtualized-task-list'; import { AppDispatch } from '@/app/store'; import { shallowEqual } from 'react-redux'; import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; import { evt_project_task_list_bulk_archive, evt_project_task_list_bulk_assign_me, evt_project_task_list_bulk_assign_members, evt_project_task_list_bulk_change_phase, evt_project_task_list_bulk_change_priority, evt_project_task_list_bulk_change_status, evt_project_task_list_bulk_delete, evt_project_task_list_bulk_update_labels, } from '@/shared/worklenz-analytics-events'; import { IBulkTasksLabelsRequest, IBulkTasksPhaseChangeRequest, IBulkTasksPriorityChangeRequest, IBulkTasksStatusChangeRequest, } from '@/types/tasks/bulk-action-bar.types'; import { ITaskStatus } from '@/types/tasks/taskStatus.types'; import { ITaskPriority } from '@/types/tasks/taskPriority.types'; import { ITaskPhase } from '@/types/tasks/taskPhase.types'; import { ITaskLabel } from '@/types/tasks/taskLabel.types'; import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'; import alertService from '@/services/alerts/alertService'; import logger from '@/utils/errorLogger'; import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice'; import { performanceMonitor } from '@/utils/performance-monitor'; import debugPerformance from '@/utils/debug-performance'; // Import the improved TaskListFilters component synchronously to avoid suspense import ImprovedTaskFilters from './improved-task-filters'; import PerformanceAnalysis from './performance-analysis'; // Import drag and drop performance optimizations import './drag-drop-optimized.css'; import './optimized-bulk-action-bar.css'; 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 { t, ready, isLoading } = useTaskManagementTranslations(); const { trackMixpanelEvent } = useMixpanelTracking(); const [dragState, setDragState] = useState({ activeTask: null, activeGroupId: null, }); // Prevent duplicate API calls in React StrictMode const hasInitialized = useRef(false); // PERFORMANCE OPTIMIZATION: Frame rate monitoring and throttling const frameTimeRef = useRef(performance.now()); const renderCountRef = useRef(0); const [shouldThrottle, setShouldThrottle] = useState(false); // Refs for performance optimization const dragOverTimeoutRef = useRef(null); const containerRef = useRef(null); // 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); const currentGrouping = useSelector(selectCurrentGroupingV3, shallowEqual); // Use bulk action slice for selected tasks instead of selection slice const selectedTaskIds = useSelector((state: RootState) => state.bulkActionReducer.selectedTaskIdsList); const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks); 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); const phaseList = useSelector((state: RootState) => state.phaseReducer.phaseList); const labelsList = useSelector((state: RootState) => state.taskLabelsReducer.labels); const members = useSelector((state: RootState) => state.teamMembersReducer.teamMembers); const archived = useSelector((state: RootState) => state.taskReducer.archived); // Get theme from Redux store const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); const themeClass = isDarkMode ? 'dark' : 'light'; // PERFORMANCE OPTIMIZATION: Build a tasksById map with memory-conscious approach const tasksById = useMemo(() => { const map: Record = {}; // Cache all tasks for full functionality - performance optimizations are handled at the virtualization level tasks.forEach(task => { map[task.id] = task; }); return map; }, [tasks]); // Drag and Drop sensors - optimized for smoother experience const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 3, // Small distance to prevent accidental drags delay: 0, // No delay for immediate activation tolerance: 5, // Tolerance for small movements }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // PERFORMANCE OPTIMIZATION: Monitor frame rate and enable throttling if needed useEffect(() => { const monitorPerformance = () => { const now = performance.now(); const frameTime = now - frameTimeRef.current; renderCountRef.current++; // If frame time is consistently over 16.67ms (60fps), enable throttling if (frameTime > 20 && renderCountRef.current > 10) { setShouldThrottle(true); } else if (frameTime < 12 && renderCountRef.current > 50) { setShouldThrottle(false); renderCountRef.current = 0; // Reset counter } frameTimeRef.current = now; }; const interval = setInterval(monitorPerformance, 100); return () => clearInterval(interval); }, []); // Fetch task groups when component mounts or dependencies change useEffect(() => { if (projectId && !hasInitialized.current) { hasInitialized.current = true; // Fetch real tasks from V3 API (minimal processing needed) dispatch(fetchTasksV3(projectId)); } }, [projectId, dispatch]); // Memoized calculations - optimized const totalTasks = useMemo(() => { const total = taskGroups.reduce((sum, g) => sum + g.taskIds.length, 0); return total; }, [taskGroups, tasks.length]); const hasAnyTasks = useMemo(() => totalTasks > 0, [totalTasks]); // Memoized handlers for better performance const handleGroupingChange = useCallback( (newGroupBy: 'status' | 'priority' | 'phase') => { dispatch(setCurrentGrouping(newGroupBy)); }, [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; // Find the task and its group const activeTask = tasks.find(t => t.id === taskId) || null; let activeGroupId: string | null = null; if (activeTask) { // 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; } } } setDragState({ activeTask, activeGroupId, }); }, [tasks, currentGrouping, taskGroups] ); // Throttled drag over handler for smoother performance const handleDragOver = useCallback( throttle((event: DragOverEvent) => { const { active, over } = event; if (!over || !dragState.activeTask) return; const activeTaskId = active.id as string; const overId = over.id as string; // 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) { // We're hovering over a task, determine its group switch (currentGrouping) { case 'status': targetGroupId = `status-${targetTask.status}`; break; case 'priority': targetGroupId = `priority-${targetTask.priority}`; break; case 'phase': targetGroupId = `phase-${targetTask.phase}`; break; } } }, 16), // 60fps throttling for smooth performance [dragState, tasks, taskGroups, currentGrouping] ); const handleDragEnd = useCallback( (event: DragEndEvent) => { setIsDragging(false); // 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 (!event.over || !currentDragState.activeTask || !currentDragState.activeGroupId) { return; } const { active, over } = event; const activeTaskId = active.id as string; const overId = over.id as string; // 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 === overId); if (targetTask) { // 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 const targetGroup = taskGroups.find(g => g.id === targetGroupId); 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; } } // Find source and target 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) { // Use the new reorderTasksInGroup action that properly handles group arrays dispatch( 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, connected, socket, projectId] ); const handleSelectTask = useCallback( (taskId: string, selected: boolean) => { if (selected) { // Add task to bulk selection const task = tasks.find(t => t.id === taskId); if (task) { // Convert Task to IProjectTask format for bulk actions const projectTask: IProjectTask = { id: task.id, name: task.title, // Always use title as the name task_key: task.task_key, status: task.status, status_id: task.status, priority: task.priority, phase_id: task.phase, phase_name: task.phase, description: task.description, start_date: task.startDate, end_date: task.dueDate, total_hours: task.timeTracking.estimated || 0, total_minutes: task.timeTracking.logged || 0, progress: task.progress, sub_tasks_count: task.sub_tasks_count || 0, assignees: task.assignees.map(assigneeId => ({ id: assigneeId, name: '', email: '', avatar_url: '', team_member_id: assigneeId, project_member_id: assigneeId, })), labels: task.labels, manual_progress: false, // Default value for Task type created_at: task.createdAt, updated_at: task.updatedAt, sort_order: task.order, }; dispatch(selectTasks([...selectedTasks, projectTask])); dispatch(selectTaskIds([...selectedTaskIds, taskId])); } } else { // Remove task from bulk selection const updatedTasks = selectedTasks.filter(t => t.id !== taskId); const updatedTaskIds = selectedTaskIds.filter(id => id !== taskId); dispatch(selectTasks(updatedTasks)); dispatch(selectTaskIds(updatedTaskIds)); } }, [dispatch, selectedTasks, selectedTaskIds, tasks] ); const handleToggleSubtasks = useCallback((taskId: string) => { // Implementation for toggling subtasks }, []); // Memoized DragOverlay content for better performance const dragOverlayContent = useMemo(() => { if (!dragState.activeTask || !dragState.activeGroupId) return null; return ( ); }, [dragState.activeTask, dragState.activeGroupId, projectId, currentGrouping]); // Bulk action handlers - implementing real functionality from task-list-bulk-actions-bar const handleClearSelection = useCallback(() => { dispatch(deselectAllBulk()); dispatch(clearSelection()); }, [dispatch]); const handleBulkStatusChange = useCallback(async (statusId: string) => { if (!statusId || !projectId) return; try { // Find the status object const status = statusList.find(s => s.id === statusId); if (!status || !status.id) return; const body: IBulkTasksStatusChangeRequest = { tasks: selectedTaskIds, status_id: status.id, }; // Check task dependencies first for (const taskId of selectedTaskIds) { const canContinue = await checkTaskDependencyStatus(taskId, status.id); if (!canContinue) { if (selectedTaskIds.length > 1) { alertService.warning( 'Incomplete Dependencies!', 'Some tasks were not updated. Please ensure all dependent tasks are completed before proceeding.' ); } else { alertService.error( 'Task is not completed', 'Please complete the task dependencies before proceeding' ); } return; } } const res = await taskListBulkActionsApiService.changeStatus(body, projectId); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_change_status); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); } } catch (error) { logger.error('Error changing status:', error); } }, [selectedTaskIds, statusList, projectId, trackMixpanelEvent, dispatch]); const handleBulkPriorityChange = useCallback(async (priorityId: string) => { if (!priorityId || !projectId) return; try { const priority = priorityList.find(p => p.id === priorityId); if (!priority || !priority.id) return; const body: IBulkTasksPriorityChangeRequest = { tasks: selectedTaskIds, priority_id: priority.id, }; const res = await taskListBulkActionsApiService.changePriority(body, projectId); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_change_priority); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); } } catch (error) { logger.error('Error changing priority:', error); } }, [selectedTaskIds, priorityList, projectId, trackMixpanelEvent, dispatch]); const handleBulkPhaseChange = useCallback(async (phaseId: string) => { if (!phaseId || !projectId) return; try { const phase = phaseList.find(p => p.id === phaseId); if (!phase || !phase.id) return; const body: IBulkTasksPhaseChangeRequest = { tasks: selectedTaskIds, phase_id: phase.id, }; const res = await taskListBulkActionsApiService.changePhase(body, projectId); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_change_phase); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); } } catch (error) { logger.error('Error changing phase:', error); } }, [selectedTaskIds, phaseList, projectId, trackMixpanelEvent, dispatch]); const handleBulkAssignToMe = useCallback(async () => { if (!projectId) return; try { const body = { tasks: selectedTaskIds, project_id: projectId, }; const res = await taskListBulkActionsApiService.assignToMe(body); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_assign_me); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); } } catch (error) { logger.error('Error assigning to me:', error); } }, [selectedTaskIds, projectId, trackMixpanelEvent, dispatch]); const handleBulkAssignMembers = useCallback(async (memberIds: string[]) => { if (!projectId || !members?.data) return; try { // Convert memberIds to member objects with proper type checking const selectedMembers = members.data.filter(member => member.id && memberIds.includes(member.id) ); const body = { tasks: selectedTaskIds, project_id: projectId, members: selectedMembers.map(member => ({ id: member.id!, name: member.name || '', email: member.email || '', avatar_url: member.avatar_url || '', team_member_id: member.id!, project_member_id: member.id!, })), }; const res = await taskListBulkActionsApiService.assignTasks(body); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_assign_members); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); } } catch (error) { logger.error('Error assigning tasks:', error); } }, [selectedTaskIds, projectId, members, trackMixpanelEvent, dispatch]); const handleBulkAddLabels = useCallback(async (labelIds: string[]) => { if (!projectId) return; try { // Convert labelIds to label objects with proper type checking const selectedLabels = labelsList.filter(label => label.id && labelIds.includes(label.id) ); const body: IBulkTasksLabelsRequest = { tasks: selectedTaskIds, labels: selectedLabels, text: null, }; const res = await taskListBulkActionsApiService.assignLabels(body, projectId); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_update_labels); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); dispatch(fetchLabels()); } } catch (error) { logger.error('Error updating labels:', error); } }, [selectedTaskIds, projectId, labelsList, trackMixpanelEvent, dispatch]); const handleBulkArchive = useCallback(async () => { if (!projectId) return; try { const body = { tasks: selectedTaskIds, project_id: projectId, }; const res = await taskListBulkActionsApiService.archiveTasks(body, archived); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_archive); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); } } catch (error) { logger.error('Error archiving tasks:', error); } }, [selectedTaskIds, projectId, archived, trackMixpanelEvent, dispatch]); const handleBulkDelete = useCallback(async () => { if (!projectId) return; try { const body = { tasks: selectedTaskIds, project_id: projectId, }; const res = await taskListBulkActionsApiService.deleteTasks(body, projectId); if (res.done) { trackMixpanelEvent(evt_project_task_list_bulk_delete); dispatch(deselectAllBulk()); dispatch(clearSelection()); dispatch(fetchTasksV3(projectId)); } } catch (error) { logger.error('Error deleting tasks:', error); } }, [selectedTaskIds, projectId, trackMixpanelEvent, dispatch]); // Additional handlers for new actions const handleBulkDuplicate = useCallback(async () => { // This would need to be implemented in the API service }, [selectedTaskIds]); const handleBulkExport = useCallback(async () => { // This would need to be implemented in the API service }, [selectedTaskIds]); const handleBulkSetDueDate = useCallback(async (date: string) => { // This would need to be implemented in the API service }, [selectedTaskIds]); // Cleanup effect useEffect(() => { return () => { if (dragOverTimeoutRef.current) { clearTimeout(dragOverTimeoutRef.current); } }; }, []); // Don't render until translations are ready to prevent Suspense if (!ready || isLoading) { return (
); } if (error) { return ( ); } return (
{/* Task Filters */}
{/* Performance Analysis - Only show in development */} {/* {process.env.NODE_ENV === 'development' && ( )} */} {/* Fixed Height Task Groups Container - Asana Style */}
{loading ? (
) : taskGroups.length === 0 ? (
No task groups available
Create tasks to see them organized in groups
} image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
{taskGroups.map((group, index) => { // PERFORMANCE OPTIMIZATION: More aggressive height calculation for better performance const groupTasks = group.taskIds.length; const baseHeight = 120; // Header + column headers + add task row const taskRowsHeight = groupTasks * 40; // 40px per task row // PERFORMANCE OPTIMIZATION: Enhanced virtualization threshold for better UX const shouldVirtualizeGroup = groupTasks > 25; // Increased threshold for smoother experience const minGroupHeight = shouldVirtualizeGroup ? 200 : 120; // Minimum height for virtualized groups const maxGroupHeight = shouldVirtualizeGroup ? 600 : 1000; // Allow more height for virtualized groups const calculatedHeight = baseHeight + taskRowsHeight; const groupHeight = Math.max( minGroupHeight, Math.min(calculatedHeight, maxGroupHeight) ); // PERFORMANCE OPTIMIZATION: Removed group throttling to show all tasks // Virtualization within each group handles performance for large task lists // PERFORMANCE OPTIMIZATION: Memoize group rendering return ( ); })}
)}
{dragOverlayContent} {/* Optimized Bulk Action Bar */} ); }; export default TaskListBoard;