import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { FixedSizeList as List, FixedSizeList } from 'react-window'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { Empty, Button, Input } from 'antd'; import { RightOutlined, DownOutlined } from '@ant-design/icons'; import { taskManagementSelectors } from '@/features/task-management/task-management.slice'; import { toggleGroupCollapsed } from '@/features/task-management/grouping.slice'; import { Task } from '@/types/task-management.types'; import TaskRow from './task-row'; import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row'; import { RootState } from '@/app/store'; import { TaskListField } from '@/features/task-management/taskListFields.slice'; import { Checkbox } from '@/components'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; interface VirtualizedTaskListProps { group: any; projectId: string; currentGrouping: 'status' | 'priority' | 'phase'; selectedTaskIds: string[]; onSelectTask: (taskId: string, selected: boolean) => void; onToggleSubtasks: (taskId: string) => void; height: number; width: number; tasksById: Record; } const VirtualizedTaskList: React.FC = React.memo(({ group, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, height, width, tasksById }) => { const dispatch = useDispatch(); const { t } = useTranslation('task-management'); // Get theme from Redux store const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); // Get field visibility from taskListFields slice const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[]; // Get group collapse state from Redux const groupStates = useSelector((state: RootState) => state.grouping.groupStates); const isCollapsed = groupStates[group.id]?.collapsed || false; // PERFORMANCE OPTIMIZATION: Improved virtualization for better user experience const VIRTUALIZATION_THRESHOLD = 25; // Increased threshold - virtualize when there are more tasks const TASK_ROW_HEIGHT = 40; const HEADER_HEIGHT = 40; const COLUMN_HEADER_HEIGHT = 40; const ADD_TASK_ROW_HEIGHT = 40; // PERFORMANCE OPTIMIZATION: Batch rendering to prevent long tasks const RENDER_BATCH_SIZE = 5; // Render max 5 tasks per frame const FRAME_BUDGET_MS = 8; const [showAddSubtaskForTaskId, setShowAddSubtaskForTaskId] = React.useState(null); const [newSubtaskName, setNewSubtaskName] = React.useState(''); const addSubtaskInputRef = React.useRef(null); const { socket, connected } = useSocket(); const handleAddSubtask = (parentTaskId: string) => { if (!newSubtaskName.trim() || !connected || !socket) return; const currentSession = JSON.parse(localStorage.getItem('session') || '{}'); const requestBody = { project_id: group.project_id || group.projectId || projectId, name: newSubtaskName.trim(), reporter_id: currentSession.id, team_id: currentSession.team_id, parent_task_id: parentTaskId, }; socket.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(requestBody)); // Listen for the response and clear input/collapse row socket.once(SocketEvents.QUICK_TASK.toString(), (response: any) => { setNewSubtaskName(''); setShowAddSubtaskForTaskId(null); // Optionally: trigger a refresh or update tasks in parent }); }; const handleCancelAddSubtask = () => { setNewSubtaskName(''); setShowAddSubtaskForTaskId(null); }; // Handle collapse/expand toggle const handleToggleCollapse = useCallback(() => { dispatch(toggleGroupCollapsed(group.id)); }, [dispatch, group.id]); // PERFORMANCE OPTIMIZATION: Add early return for empty groups if (!group || !group.taskIds || group.taskIds.length === 0) { const emptyGroupHeight = HEADER_HEIGHT + COLUMN_HEADER_HEIGHT + 120 + ADD_TASK_ROW_HEIGHT; // 120px for empty state return (
{/* Sticky Group Color Border */}
{/* Column Headers */}
TASKS
{/* Empty State */}
{t('noTasksInGroup')}
{t('noTasksInGroupDescription')}
} style={{ margin: 0, padding: '12px' }} />
); } // Get tasks for this group using memoization for performance const groupTasks = useMemo(() => { return group.taskIds .map((taskId: string) => tasksById[taskId]) .filter((task: Task | undefined): task is Task => task !== undefined); }, [group.taskIds, tasksById, group.id]); // Calculate selection state for the group checkbox const selectionState = useMemo(() => { if (groupTasks.length === 0) { return { isAllSelected: false, isIndeterminate: false }; } const selectedTasksInGroup = groupTasks.filter((task: Task) => selectedTaskIds.includes(task.id)); const isAllSelected = selectedTasksInGroup.length === groupTasks.length; const isIndeterminate = selectedTasksInGroup.length > 0 && selectedTasksInGroup.length < groupTasks.length; return { isAllSelected, isIndeterminate }; }, [groupTasks, selectedTaskIds]); // Handle select all tasks in group - optimized with useCallback const handleSelectAllInGroup = useCallback((checked: boolean) => { // PERFORMANCE OPTIMIZATION: Batch selection updates const tasksToUpdate: Array<{ taskId: string; selected: boolean }> = []; if (checked) { // Select all tasks in the group for (let i = 0; i < groupTasks.length; i++) { const task = groupTasks[i]; if (!selectedTaskIds.includes(task.id)) { tasksToUpdate.push({ taskId: task.id, selected: true }); } } } else { // Deselect all tasks in the group for (let i = 0; i < groupTasks.length; i++) { const task = groupTasks[i]; if (selectedTaskIds.includes(task.id)) { tasksToUpdate.push({ taskId: task.id, selected: false }); } } } // Batch update all selections tasksToUpdate.forEach(({ taskId, selected }) => { onSelectTask(taskId, selected); }); }, [groupTasks, selectedTaskIds, onSelectTask]); // PERFORMANCE OPTIMIZATION: Use passed height prop and calculate available space for tasks const taskRowsHeight = groupTasks.length * TASK_ROW_HEIGHT; const groupHeight = height; // Use the height passed from parent const availableTaskRowsHeight = Math.max(0, groupHeight - HEADER_HEIGHT - COLUMN_HEADER_HEIGHT - ADD_TASK_ROW_HEIGHT); // PERFORMANCE OPTIMIZATION: Limit visible columns for large lists const maxVisibleColumns = groupTasks.length > 50 ? 6 : 12; // Further reduce columns for large lists // Define all possible columns const allFixedColumns = [ { key: 'drag', label: '', width: 40, alwaysVisible: true }, { key: 'select', label: '', width: 40, alwaysVisible: true }, { key: 'key', label: 'KEY', width: 80, fieldKey: 'KEY' }, { key: 'task', label: 'TASK', width: 474, alwaysVisible: true }, ]; const allScrollableColumns = [ { key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' }, { key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' }, { key: 'status', label: 'Status', width: 140, fieldKey: 'STATUS' }, { key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' }, { key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' }, { key: 'phase', label: 'Phase', width: 100, fieldKey: 'PHASE' }, { key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' }, { key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' }, { key: 'estimation', label: 'Estimation', width: 100, fieldKey: 'ESTIMATION' }, { key: 'startDate', label: 'Start Date', width: 120, fieldKey: 'START_DATE' }, { key: 'dueDate', label: 'Due Date', width: 120, fieldKey: 'DUE_DATE' }, { key: 'dueTime', label: 'Due Time', width: 100, fieldKey: 'DUE_TIME' }, { key: 'completedDate', label: 'Completed Date', width: 130, fieldKey: 'COMPLETED_DATE' }, { key: 'createdDate', label: 'Created Date', width: 120, fieldKey: 'CREATED_DATE' }, { key: 'lastUpdated', label: 'Last Updated', width: 130, fieldKey: 'LAST_UPDATED' }, { key: 'reporter', label: 'Reporter', width: 100, fieldKey: 'REPORTER' }, ]; // Filter columns based on field visibility const fixedColumns = useMemo(() => { return allFixedColumns.filter(col => { // Always show columns marked as alwaysVisible if (col.alwaysVisible) return true; // For other columns, check field visibility if (col.fieldKey) { const field = taskListFields.find(f => f.key === col.fieldKey); return field?.visible ?? false; } return false; }); }, [taskListFields, allFixedColumns]); const scrollableColumns = useMemo(() => { const filtered = allScrollableColumns.filter(col => { // For scrollable columns, check field visibility if (col.fieldKey) { const field = taskListFields.find(f => f.key === col.fieldKey); return field?.visible ?? false; } return false; }); // PERFORMANCE OPTIMIZATION: Limit columns for large lists return filtered.slice(0, maxVisibleColumns); }, [taskListFields, allScrollableColumns, maxVisibleColumns]); const fixedWidth = fixedColumns.reduce((sum, col) => sum + col.width, 0); const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0); const totalTableWidth = fixedWidth + scrollableWidth; // PERFORMANCE OPTIMIZATION: Enhanced overscan for smoother scrolling experience const shouldVirtualize = groupTasks.length > VIRTUALIZATION_THRESHOLD; const overscanCount = useMemo(() => { if (groupTasks.length <= 20) return 5; // Small lists: 5 items overscan if (groupTasks.length <= 100) return 10; // Medium lists: 10 items overscan if (groupTasks.length <= 500) return 15; // Large lists: 15 items overscan return 20; // Very large lists: 20 items overscan for smooth scrolling }, [groupTasks.length]); // Build displayRows array const displayRows: Array<{ type: 'task'; task: Task } | { type: 'add-subtask'; parentTask: Task }> = []; for (let i = 0; i < groupTasks.length; i++) { const task = groupTasks[i]; displayRows.push({ type: 'task', task }); if (showAddSubtaskForTaskId === task.id) { displayRows.push({ type: 'add-subtask', parentTask: task }); } } const scrollContainerRef = useRef(null); const headerScrollRef = useRef(null); const listRef = useRef(null); // PERFORMANCE OPTIMIZATION: Throttled scroll handler const handleScroll = useCallback(() => { if (headerScrollRef.current && scrollContainerRef.current) { headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft; } }, []); // Synchronize header scroll with body scroll useEffect(() => { const scrollDiv = scrollContainerRef.current; if (scrollDiv) { scrollDiv.addEventListener('scroll', handleScroll, { passive: true }); } return () => { if (scrollDiv) { scrollDiv.removeEventListener('scroll', handleScroll); } }; }, [handleScroll]); // If group is collapsed, show only header if (isCollapsed) { return (
{/* Sticky Group Color Border */}
{/* Group Header */}
); } return (
{/* Sticky Group Color Border */}
{/* Group Header */}
{/* Column Headers (sync scroll) */}
0 ? '2px solid var(--task-border-primary, #e8e8e8)' : 'none', boxShadow: scrollableColumns.length > 0 ? '2px 0 4px rgba(0, 0, 0, 0.1)' : 'none', }} > {fixedColumns.map(col => (
{col.key === 'select' ? (
) : ( {col.label} )}
))}
{scrollableColumns.map(col => (
{col.label}
))}
{/* Scrollable List - only task rows */}
0 ? availableTaskRowsHeight : 'auto', contain: 'layout style', // CSS containment for better performance }} > {shouldVirtualize ? ( {({ index, style }) => { const row = displayRows[index]; if (row.type === 'task') { return (
setShowAddSubtaskForTaskId(row.task.id)} />
); } if (row.type === 'add-subtask') { return (
{(fixedColumns ?? []).map((col, index) => { const borderClasses = `border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; if (col.key === 'task') { return (
setNewSubtaskName(e.target.value)} onPressEnter={() => handleAddSubtask(row.parentTask.id)} onBlur={handleCancelAddSubtask} className={`add-subtask-input flex-1 ${isDarkMode ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300 text-gray-900'}`} size="small" autoFocus />
); } else { return (
); } })} {(scrollableColumns ?? []).map((col, index) => { const borderClasses = `border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; return (
); })}
); } return null; }} ) : ( // PERFORMANCE OPTIMIZATION: Use React.Fragment to reduce DOM nodes {displayRows.map((row, idx) => { if (row.type === 'task') { return ( setShowAddSubtaskForTaskId(row.task.id)} /> ); } if (row.type === 'add-subtask') { return (
{(fixedColumns ?? []).map((col, index) => { const borderClasses = `border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; if (col.key === 'task') { return (
setNewSubtaskName(e.target.value)} onPressEnter={() => handleAddSubtask(row.parentTask.id)} onBlur={handleCancelAddSubtask} className={`add-subtask-input flex-1 ${isDarkMode ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300 text-gray-900'}`} size="small" autoFocus />
); } else { return (
); } })} {(scrollableColumns ?? []).map((col, index) => { const borderClasses = `border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; return (
); })}
); } return null; })} )}
{/* Add Task Row - Always show at the bottom */}
); }); export default VirtualizedTaskList;