import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; 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, 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 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 { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice'; 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 { 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 } = useTranslation('task-management'); 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(); // 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); const selectedTaskIds = useSelector(selectSelectedTaskIds); 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); console.log(`[TASK-LIST-BOARD] Total tasks in groups: ${total}, Total tasks in store: ${tasks.length}, Groups: ${taskGroups.length}`); 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] ); 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 smoother 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; // PERFORMANCE OPTIMIZATION: Immediate response for instant UX // Only update if we're hovering over a different container const targetTask = tasks.find(t => t.id === overContainer); let targetGroupId = overContainer; if (targetTask) { // PERFORMANCE OPTIMIZATION: Use switch instead of multiple if statements switch (currentGrouping) { case 'status': targetGroupId = `status-${targetTask.status}`; break; case 'priority': targetGroupId = `priority-${targetTask.priority}`; break; case 'phase': targetGroupId = `phase-${targetTask.phase}`; break; } } if (targetGroupId !== dragState.activeGroupId) { // PERFORMANCE OPTIMIZATION: Use findIndex for better performance const targetGroupIndex = taskGroups.findIndex(g => g.id === targetGroupId); if (targetGroupIndex !== -1) { const targetGroup = taskGroups[targetGroupIndex]; dispatch( optimisticTaskMove({ taskId: activeTaskId, newGroupId: targetGroupId, newIndex: targetGroup.taskIds.length, }) ); } } }, 16), // 60fps throttling for smooth performance [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: string) => tasks.find((t: any) => 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: any) => 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 }, []); // 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(deselectAll()); 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(deselectAll()); 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(deselectAll()); 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(deselectAll()); 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(deselectAll()); 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(deselectAll()); 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(deselectAll()); 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(deselectAll()); 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(deselectAll()); 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 console.log('Bulk duplicate not yet implemented in API:', selectedTaskIds); }, [selectedTaskIds]); const handleBulkExport = useCallback(async () => { // This would need to be implemented in the API service console.log('Bulk export not yet implemented in API:', selectedTaskIds); }, [selectedTaskIds]); const handleBulkSetDueDate = useCallback(async (date: string) => { // This would need to be implemented in the API service console.log('Bulk set due date not yet implemented in API:', date, selectedTaskIds); }, [selectedTaskIds]); // Cleanup effect useEffect(() => { return () => { if (dragOverTimeoutRef.current) { clearTimeout(dragOverTimeoutRef.current); } }; }, []); 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;