import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { FixedSizeList as List } from 'react-window'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { Empty } from 'antd'; import { taskManagementSelectors } from '@/features/task-management/task-management.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'; 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 { 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[]; // 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; // Leave 8ms per frame for other operations // 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 */}
{group?.title || 'Empty Group'} (0)
{/* Column Headers */}
TASKS
{/* Empty State */}
{t('noTasksInGroup')}
{t('noTasksInGroupDescription')}
} style={{ margin: 0, padding: '12px' }} />
); } // PERFORMANCE OPTIMIZATION: Get tasks for this group using direct lookup (no mapping/filtering) const groupTasks = useMemo(() => { // PERFORMANCE OPTIMIZATION: Use for loop instead of map for better performance const tasks: Task[] = []; for (let i = 0; i < group.taskIds.length; i++) { const task = tasksById[group.taskIds[i]]; if (task) { tasks.push(task); } } return tasks; }, [group.taskIds, tasksById]); // PERFORMANCE OPTIMIZATION: Only calculate selection state when needed const selectionState = useMemo(() => { if (groupTasks.length === 0) { return { isAllSelected: false, isIndeterminate: false }; } // PERFORMANCE OPTIMIZATION: Use for loop instead of filter for better performance let selectedCount = 0; for (let i = 0; i < groupTasks.length; i++) { if (selectedTaskIds.includes(groupTasks[i].id)) { selectedCount++; } } const isAllSelected = selectedCount === groupTasks.length; const isIndeterminate = selectedCount > 0 && selectedCount < 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]); // PERFORMANCE OPTIMIZATION: Memoize row renderer with better dependency management const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => { const task: Task | undefined = groupTasks[index]; if (!task) return null; // PERFORMANCE OPTIMIZATION: Pre-calculate selection state const isSelected = selectedTaskIds.includes(task.id); return (
); }, [group.id, group.color, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, fixedColumns, scrollableColumns]); const scrollContainerRef = useRef(null); const headerScrollRef = 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]); return (
{/* Sticky Group Color Border */}
{/* Group Header */}
{group.title} ({groupTasks.length})
{/* 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 ? ( {}} onScroll={() => {}} > {Row} ) : ( // PERFORMANCE OPTIMIZATION: Use React.Fragment to reduce DOM nodes {groupTasks.map((task: Task, index: number) => { // PERFORMANCE OPTIMIZATION: Pre-calculate selection state const isSelected = selectedTaskIds.includes(task.id); return (
); })}
)}
{/* Add Task Row - Always show at the bottom */}
); }); export default VirtualizedTaskList;