import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useSelector } from 'react-redux'; import DOMPurify from 'dompurify'; import { Input, Typography, DatePicker, dayjs, taskManagementAntdConfig, HolderOutlined, MessageOutlined, PaperClipOutlined, UserOutlined, type InputRef, Tooltip, } from '@/shared/antd-imports'; import { RightOutlined, ExpandAltOutlined, CheckCircleOutlined, MinusCircleOutlined, EyeOutlined, RetweetOutlined, DownOutlined, // Added DownOutlined for expand/collapse } from '@/shared/antd-imports'; import { useTranslation } from 'react-i18next'; import { Task } from '@/types/task-management.types'; import { RootState } from '@/app/store'; import { AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, Progress, } from '@/components'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import TaskTimer from '@/components/taskListCommon/task-timer/task-timer'; import { useTaskTimer } from '@/hooks/useTaskTimer'; import { formatDate as utilFormatDate, formatDateTime as utilFormatDateTime, createLabelsAdapter, createAssigneeAdapter, PRIORITY_COLORS as UTIL_PRIORITY_COLORS, } from './task-row-utils'; import './task-row-optimized.css'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { setSelectedTaskId, setShowTaskDrawer, fetchTask, } from '@/features/task-drawer/task-drawer.slice'; import useDragCursor from '@/hooks/useDragCursor'; import TaskContextMenu from './task-context-menu/task-context-menu'; interface TaskRowProps { task: Task; projectId: string; groupId: string; currentGrouping: 'status' | 'priority' | 'phase'; isSelected: boolean; isDragOverlay?: boolean; index?: number; onSelect?: (taskId: string, selected: boolean) => void; onToggleSubtasks?: (taskId: string) => void; // Modified prop columns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>; fixedColumns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>; scrollableColumns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>; onExpandSubtaskInput?: (taskId: string) => void; level?: number; // Added level prop for indentation } // Priority and status colors - moved outside component to avoid recreation const PRIORITY_COLORS = { critical: '#ff4d4f', high: '#ff7a45', medium: '#faad14', low: '#52c41a', } as const; const STATUS_COLORS = { todo: '#f0f0f0', doing: '#1890ff', done: '#52c41a', } as const; // Memoized sub-components for maximum performance const DragHandle = React.memo<{ isDarkMode: boolean; attributes: any; listeners: any }>( ({ isDarkMode, attributes, listeners }) => { return (
); } ); const TaskKey = React.memo<{ taskKey: string; isDarkMode: boolean }>(({ taskKey, isDarkMode }) => ( {taskKey} )); const TaskDescription = React.memo<{ description?: string; isDarkMode: boolean }>( ({ description, isDarkMode }) => { if (!description) return null; const sanitizedDescription = DOMPurify.sanitize(description); return ( ); } ); const TaskProgress = React.memo<{ progress: number; isDarkMode: boolean }>( ({ progress, isDarkMode }) => ( ) ); const TaskTimeTracking = React.memo<{ taskId: string; isDarkMode: boolean }>( ({ taskId, isDarkMode }) => { const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimer( taskId, null // The hook will get the timer start time from Redux ); return ( ); } ); const TaskReporter = React.memo<{ reporter?: string; isDarkMode: boolean }>( ({ reporter, isDarkMode }) => (
{reporter || '-'}
) ); // PERFORMANCE OPTIMIZATION: Lightweight placeholder components for better performance const AssigneePlaceholder = React.memo<{ isDarkMode: boolean; memberCount?: number }>( ({ isDarkMode, memberCount = 0 }) => (
{memberCount > 0 ? (
{Array.from({ length: Math.min(memberCount, 3) }).map((_, i) => (
))} {memberCount > 3 && (
+{memberCount - 3}
)}
) : (
)}
) ); const StatusPlaceholder = React.memo<{ status?: string; isDarkMode: boolean }>( ({ status, isDarkMode }) => (
{status || '...'}
) ); const PriorityPlaceholder = React.memo<{ priority?: string; isDarkMode: boolean }>( ({ priority, isDarkMode }) => (
{priority || '...'}
) ); const PhasePlaceholder = React.memo<{ phase?: string; isDarkMode: boolean }>( ({ phase, isDarkMode }) => (
{phase || '...'}
) ); const LabelsPlaceholder = React.memo<{ labelCount?: number; isDarkMode: boolean }>( ({ labelCount = 0, isDarkMode }) => (
{labelCount > 0 ? ( Array.from({ length: Math.min(labelCount, 3) }).map((_, i) => (
)) ) : (
)}
) ); // PERFORMANCE OPTIMIZATION: Simplified placeholders without animations under memory pressure const SimplePlaceholder = React.memo<{ width: number; height: number; isDarkMode: boolean }>( ({ width, height, isDarkMode }) => (
) ); // Lazy-loaded components with Suspense fallbacks const LazyAssigneeSelector = React.lazy(() => import('./lazy-assignee-selector').then(module => ({ default: module.default })) ); const LazyTaskStatusDropdown = React.lazy(() => import('./task-status-dropdown').then(module => ({ default: module.default })) ); const LazyTaskPriorityDropdown = React.lazy(() => import('./task-priority-dropdown').then(module => ({ default: module.default })) ); const LazyTaskPhaseDropdown = React.lazy(() => import('./task-phase-dropdown').then(module => ({ default: module.default })) ); const LazyLabelsSelector = React.lazy(() => import('@/components/LabelsSelector').then(module => ({ default: module.default })) ); // Enhanced component wrapper with progressive loading const ProgressiveComponent = React.memo<{ isLoaded: boolean; placeholder: React.ReactNode; children: React.ReactNode; fallback?: React.ReactNode; }>(({ isLoaded, placeholder, children, fallback }) => { if (!isLoaded) { return <>{placeholder}; } return {children}; }); // PERFORMANCE OPTIMIZATION: Frame-rate aware rendering hooks const useFrameRateOptimizedLoading = (index?: number) => { const [canRender, setCanRender] = useState((index !== undefined && index < 3) || false); const renderRequestRef = useRef(undefined); useEffect(() => { if (index === undefined || canRender) return; // Use requestIdleCallback for non-critical rendering const scheduleRender = () => { if ('requestIdleCallback' in window) { (window as any).requestIdleCallback( () => { setCanRender(true); }, { timeout: 100 } ); } else { // Fallback for browsers without requestIdleCallback setTimeout(() => setCanRender(true), 50); } }; renderRequestRef.current = requestAnimationFrame(scheduleRender); return () => { if (renderRequestRef.current) { cancelAnimationFrame(renderRequestRef.current); } }; }, [index, canRender]); return canRender; }; // PERFORMANCE OPTIMIZATION: Memory pressure detection const useMemoryPressure = () => { const [isUnderPressure, setIsUnderPressure] = useState(false); useEffect(() => { if (!('memory' in performance)) return; const checkMemory = () => { const memory = (performance as any).memory; if (memory) { const usedRatio = memory.usedJSHeapSize / memory.jsHeapSizeLimit; setIsUnderPressure(usedRatio > 0.6); // Conservative threshold } }; checkMemory(); const interval = setInterval(checkMemory, 2000); return () => clearInterval(interval); }, []); return isUnderPressure; }; const TaskRow: React.FC = React.memo( ({ task, projectId, groupId, currentGrouping, isSelected, isDragOverlay = false, index, onSelect, onToggleSubtasks, columns, fixedColumns, scrollableColumns, onExpandSubtaskInput, level = 0, // Initialize level to 0 }) => { // PERFORMANCE OPTIMIZATION: Frame-rate aware loading const canRenderComplex = useFrameRateOptimizedLoading(index); const isMemoryPressured = useMemoryPressure(); // PERFORMANCE OPTIMIZATION: More aggressive performance - only load first 2 immediately const [isFullyLoaded, setIsFullyLoaded] = useState((index !== undefined && index < 2) || false); const [isIntersecting, setIsIntersecting] = useState(false); const rowRef = useRef(null); const hasBeenFullyLoadedOnce = useRef((index !== undefined && index < 2) || false); // PERFORMANCE OPTIMIZATION: Conditional component loading based on memory pressure const [shouldShowComponents, setShouldShowComponents] = useState( (index !== undefined && index < 2) || false ); // PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible const { socket, connected } = useSocket(); // Redux dispatch const dispatch = useAppDispatch(); // Edit task name state const [editTaskName, setEditTaskName] = useState(false); const [taskName, setTaskName] = useState(task.title || ''); const [showAddSubtask, setShowAddSubtask] = useState(false); const [newSubtaskName, setNewSubtaskName] = useState(''); const inputRef = useRef(null); const addSubtaskInputRef = useRef(null); const wrapperRef = useRef(null); // Context menu state const [showContextMenu, setShowContextMenu] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); // PERFORMANCE OPTIMIZATION: Intersection Observer for lazy loading useEffect(() => { // Skip intersection observer if already fully loaded if (!rowRef.current || hasBeenFullyLoadedOnce.current) return; const observer = new IntersectionObserver( entries => { const [entry] = entries; if (entry.isIntersecting && !isIntersecting && !hasBeenFullyLoadedOnce.current) { setIsIntersecting(true); // Immediate loading when intersecting - no delay setIsFullyLoaded(true); hasBeenFullyLoadedOnce.current = true; // Mark as fully loaded once // Add a tiny delay for component loading to prevent browser freeze setTimeout(() => { setShouldShowComponents(true); }, 8); // Half frame delay for even more responsive experience } }, { root: null, rootMargin: '200px', // Increased to load components earlier before they're visible threshold: 0, // Load as soon as any part enters the extended viewport } ); observer.observe(rowRef.current); return () => { observer.disconnect(); }; }, [isIntersecting, hasBeenFullyLoadedOnce.current]); // PERFORMANCE OPTIMIZATION: Skip expensive operations during initial render // Once fully loaded, always render full to prevent blanking during real-time updates const shouldRenderFull = (isFullyLoaded && shouldShowComponents) || hasBeenFullyLoadedOnce.current || isDragOverlay || editTaskName; // PERFORMANCE OPTIMIZATION: Minimal initial render for non-visible tasks // Only render essential columns during initial load to reduce DOM nodes const shouldRenderMinimal = !shouldRenderFull && !isDragOverlay; // DRAG OVERLAY: When dragging, show only task name for cleaner experience const shouldRenderDragOverlay = isDragOverlay; // REAL-TIME UPDATES: Ensure content stays loaded during socket updates useEffect(() => { if (shouldRenderFull && !hasBeenFullyLoadedOnce.current) { hasBeenFullyLoadedOnce.current = true; } }, [shouldRenderFull]); // Optimized drag and drop setup with maximum performance const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id, data: { type: 'task', taskId: task.id, groupId, }, disabled: isDragOverlay || !shouldRenderFull, // Disable drag until fully loaded // PERFORMANCE OPTIMIZATION: Disable all animations for maximum performance animateLayoutChanges: () => false, // Disable layout animations transition: null, // Disable transitions }); // Get theme from Redux store - memoized selector const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); // Translation hook const { t } = useTranslation('task-management'); // Optimized task name save handler const handleTaskNameSave = useCallback(() => { const newTaskName = taskName?.trim(); if (newTaskName && connected && newTaskName !== task.title) { socket?.emit( SocketEvents.TASK_NAME_CHANGE.toString(), JSON.stringify({ task_id: task.id, name: newTaskName, parent_task: null, }) ); } setEditTaskName(false); }, [connected, socket, task.id, task.title, taskName]); // PERFORMANCE OPTIMIZATION: Only setup click outside detection when editing useEffect(() => { if (!editTaskName || !shouldRenderFull) return; const handleClickOutside = (event: MouseEvent) => { if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { handleTaskNameSave(); } }; document.addEventListener('mousedown', handleClickOutside, { passive: true }); inputRef.current?.focus(); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [editTaskName, shouldRenderFull, handleTaskNameSave]); // Handle canceling add subtask const handleCancelAddSubtask = useCallback(() => { setNewSubtaskName(''); setShowAddSubtask(false); }, []); // Optimized style calculations with maximum performance const dragStyle = useMemo(() => { if (!isDragging && !transform) return {}; return { transform: CSS.Transform.toString(transform), opacity: isDragging ? 0.5 : 1, zIndex: isDragging ? 1000 : 'auto', }; }, [transform, isDragging]); // Memoized event handlers with better dependency tracking const handleSelectChange = useCallback( (checked: boolean) => { onSelect?.(task.id, checked); }, [onSelect, task.id] ); // Modified handleToggleSubtasks to use Redux state const handleToggleSubtasks = useCallback(() => { if (!task.id) return; onToggleSubtasks?.(task.id); }, [task.id, onToggleSubtasks]); const handleContextMenu = useCallback((e: React.MouseEvent) => { setShowContextMenu(true); setContextMenuPosition({ x: e.clientX, y: e.clientY }); }, []); // Handle successful subtask creation const handleSubtaskCreated = useCallback( (newTask: any) => { if (newTask && newTask.id) { // Update parent task progress socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id); // Clear form and hide add subtask row setNewSubtaskName(''); setShowAddSubtask(false); // The global socket handler will automatically add the subtask to the parent task // and update the UI through Redux // After creating the first subtask, the task now has subtasks // so we should expand it to show the new subtask if (task.sub_tasks_count === 0 || !task.sub_tasks_count) { // Trigger expansion to show the newly created subtask setTimeout(() => { onToggleSubtasks?.(task.id, true); // Pass true to expand }, 100); } } }, [socket, task.id, task.sub_tasks_count, onToggleSubtasks] ); // Handle adding new subtask const handleAddSubtask = useCallback(() => { const subtaskName = newSubtaskName?.trim(); if (subtaskName && connected && projectId) { // Get current session for reporter_id and team_id const currentSession = JSON.parse(localStorage.getItem('session') || '{}'); const requestBody = { project_id: projectId, name: subtaskName, reporter_id: currentSession.id, team_id: currentSession.team_id, parent_task_id: task.id, }; socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(requestBody)); // Handle the response socket?.once(SocketEvents.QUICK_TASK.toString(), handleSubtaskCreated); } }, [newSubtaskName, connected, socket, task.id, projectId, handleSubtaskCreated]); // Handle expand/collapse or add subtask const handleExpandClick = useCallback(() => { // Always show add subtask row when clicking expand icon setShowAddSubtask(!showAddSubtask); if (!showAddSubtask) { // Focus the input after state update setTimeout(() => { addSubtaskInputRef.current?.focus(); }, 100); } }, [showAddSubtask]); // Handle opening task drawer const handleOpenTask = useCallback(() => { if (!task.id) return; dispatch(setSelectedTaskId(task.id)); dispatch(setShowTaskDrawer(true)); // Fetch task data - this is necessary for detailed task drawer information // that's not available in the list view (comments, attachments, etc.) dispatch(fetchTask({ taskId: task.id, projectId })); }, [task.id, projectId, dispatch]); // Optimized date handling with better memoization const dateValues = useMemo( () => ({ start: task.startDate ? dayjs(task.startDate) : undefined, due: task.dueDate ? dayjs(task.dueDate) : undefined, }), [task.startDate, task.dueDate] ); const handleDateChange = useCallback( (date: dayjs.Dayjs | null, field: 'startDate' | 'dueDate') => { if (!connected || !socket) return; const eventType = field === 'startDate' ? SocketEvents.TASK_START_DATE_CHANGE : SocketEvents.TASK_END_DATE_CHANGE; const dateField = field === 'startDate' ? 'start_date' : 'end_date'; socket.emit( eventType.toString(), JSON.stringify({ task_id: task.id, [dateField]: date?.format('YYYY-MM-DD'), parent_task: null, time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone, }) ); }, [connected, socket, task.id] ); // Optimized class name calculations with better memoization const styleClasses = useMemo(() => { const base = 'border-b transition-all duration-150'; // Reduced duration for better performance const theme = isDarkMode ? 'border-gray-600 hover:bg-gray-800' : 'border-gray-300 hover:bg-gray-50'; const background = isDarkMode ? 'bg-[#18181b]' : 'bg-white'; const selected = isSelected ? (isDarkMode ? 'bg-blue-900/20' : 'bg-blue-50') : ''; const overlay = isDragOverlay ? `rounded-sm shadow-lg border-2 ${isDarkMode ? 'border-gray-600 shadow-2xl' : 'border-gray-300 shadow-2xl'}` : ''; return { container: `${base} ${theme} ${background} ${selected} ${overlay}`, taskName: `text-sm font-medium flex-1 overflow-hidden text-ellipsis whitespace-nowrap transition-colors duration-150 cursor-pointer ${ isDarkMode ? 'text-gray-100 hover:text-blue-400' : 'text-gray-900 hover:text-blue-600' } ${task.progress === 100 ? `line-through ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}` : ''}`, }; }, [isDarkMode, isSelected, isDragOverlay, task.progress]); // Memoized adapters for better performance const adapters = useMemo( () => ({ labels: createLabelsAdapter(task), assignee: createAssigneeAdapter(task), }), [task] ); // PERFORMANCE OPTIMIZATION: Minimal column rendering for initial load const renderMinimalColumn = useCallback( ( col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number ) => { const isActuallyLast = isFixed ? index === totalColumns - 1 && (!scrollableColumns || scrollableColumns.length === 0) : index === totalColumns - 1; const borderClasses = `${isActuallyLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; // Only render essential columns during minimal load switch (col.key) { case 'drag': return (
); case 'select': return (
); case 'key': return (
); case 'task': return (
{/* Always reserve space for expand icon */}
{task.title} {(task as any).sub_tasks_count > 0 && (
{(task as any).sub_tasks_count} {'»'}
)}
); case 'progress': return (
{task.progress !== undefined && task.progress >= 0 && (task.progress === 100 ? (
) : (
))}
); default: // For non-essential columns, show minimal placeholder return (
); } }, [isDarkMode, task, isSelected, handleSelectChange, styleClasses, scrollableColumns] ); // Optimized column rendering with better performance const renderColumn = useCallback( ( col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number ) => { // Use simplified rendering for initial load if (!shouldRenderFull) { return renderMinimalColumn(col, isFixed, index, totalColumns); } // Full rendering logic (existing code) // Simplified border logic - no fixed columns const isLast = index === totalColumns - 1; const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; switch (col.key) { case 'drag': return (
); case 'select': return (
); case 'key': return (
); case 'task': const cellStyle = editTaskName ? { width: col.width, borderTop: '1px solid #1890ff', borderBottom: '1px solid #1890ff', borderLeft: '1px solid #1890ff', background: isDarkMode ? '#232b3a' : '#f0f7ff', transition: 'border 0.2s', } : { width: col.width }; return (
{/* Left section with expand icon and task content */}
{/* Expand/Collapse Icon - Smart visibility */} {typeof task.sub_tasks_count === 'number' ? (
) : (
// Placeholder for alignment )} {/* Task name and input */}
{editTaskName ? ( setTaskName(e.target.value)} onBlur={handleTaskNameSave} onPressEnter={handleTaskNameSave} variant="borderless" style={{ color: isDarkMode ? '#ffffff' : '#262626', padding: 0, }} autoFocus /> ) : ( <> setEditTaskName(true)} className={styleClasses.taskName} style={{ cursor: 'pointer' }} > {task.title} {(task as any).sub_tasks_count > 0 && (
{(task as any).sub_tasks_count} {'»'}
)} {/* Indicators section */} {!editTaskName && (
{/* Comments indicator */} {(task as any).comments_count > 0 && ( )} {/* Attachments indicator */} {(task as any).attachments_count > 0 && ( )} {/* Dependencies indicator */} {(task as any).has_dependencies && ( )} {/* Subscribers indicator */} {(task as any).has_subscribers && ( )} {/* Recurring indicator */} {(task as any).schedule_id && ( )}
)} )}
)}
{/* Indicators section */} {!editTaskName && (
{/* Comments indicator */} {(task as any).comments_count > 0 && ( )} {/* Attachments indicator */} {(task as any).attachments_count > 0 && ( )} {/* Dependencies indicator */} {(task as any).has_dependencies && ( )} {/* Subscribers indicator */} {(task as any).has_subscribers && ( )} {/* Recurring indicator */} {(task as any).schedule_id && ( )}
)} )}
{/* Right section with open button - CSS hover only */} {!editTaskName && (
)}
); case 'description': return (
); case 'progress': return (
{task.progress !== undefined && task.progress >= 0 && (task.progress === 100 ? (
) : ( ))}
); case 'members': return (
} fallback={ } >
{task.assignee_names && task.assignee_names.length > 0 && ( )}
); case 'labels': return (
} fallback={ } > <> {task.labels?.map((label, index) => label.end && label.names && label.name ? ( ) : ( ) )}
); case 'phase': return (
} fallback={} >
); case 'status': return (
} fallback={} >
); case 'priority': return (
} fallback={ } >
); case 'timeTracking': return (
); case 'estimation': return (
{task.timeTracking?.estimated ? `${task.timeTracking.estimated}h` : '-'}
); case 'startDate': return (
handleDateChange(date, 'startDate')} placeholder="Start Date" />
); case 'dueDate': return (
handleDateChange(date, 'dueDate')} placeholder="Due Date" />
); case 'dueTime': return (
{task.dueDate ? dayjs(task.dueDate).format('HH:mm') : '-'}
); case 'completedDate': return (
{task.completedAt ? utilFormatDate(task.completedAt) : '-'}
); case 'createdDate': return (
{task.createdAt ? utilFormatDate(task.createdAt) : '-'}
); case 'lastUpdated': return (
{task.updatedAt ? utilFormatDateTime(task.updatedAt) : '-'}
); case 'reporter': return (
); default: return null; } }, [ shouldRenderFull, renderMinimalColumn, isDarkMode, task, isSelected, editTaskName, taskName, adapters, groupId, projectId, attributes, listeners, handleSelectChange, handleTaskNameSave, handleDateChange, handleToggleSubtasks, // Added handleToggleSubtasks dateValues, styleClasses, onExpandSubtaskInput, level, // Added level ] ); // Apply global cursor style when dragging useDragCursor(isDragging); // Compute theme class const themeClass = isDarkMode ? 'dark' : ''; // DRAG OVERLAY: Render simplified version when dragging if (isDragOverlay) { return (
{task.title}
); } return ( <>
{ setNodeRef(node); rowRef.current = node; }} style={dragStyle} className={`task-row task-row-optimized ${themeClass} ${isSelected ? 'selected' : ''} ${isDragOverlay ? 'drag-overlay' : ''} ${isDragging ? 'is-dragging' : ''}`} data-dnd-draggable="true" data-dnd-dragging={isDragging ? 'true' : 'false'} data-task-id={task.id} data-group-id={groupId} onContextMenu={handleContextMenu} >
{/* All Columns - No Fixed Positioning */}
{/* Fixed Columns (now scrollable) */} {(fixedColumns ?? []).length > 0 && ( <> {(fixedColumns ?? []).map((col, index) => shouldRenderMinimal ? renderMinimalColumn(col, false, index, (fixedColumns ?? []).length) : renderColumn(col, false, index, (fixedColumns ?? []).length) )} )} {/* Scrollable Columns */} {(scrollableColumns ?? []).length > 0 && ( <> {(scrollableColumns ?? []).map((col, index) => shouldRenderMinimal ? renderMinimalColumn(col, false, index, (scrollableColumns ?? []).length) : renderColumn(col, false, index, (scrollableColumns ?? []).length) )} )}
{showContextMenu && ( setShowContextMenu(false)} /> )} ); }, (prevProps, nextProps) => { // PERFORMANCE OPTIMIZATION: Enhanced comparison function // Skip comparison during initial renders to reduce CPU load if (!prevProps.task.id || !nextProps.task.id) return false; // Quick identity checks first if (prevProps.task.id !== nextProps.task.id) return false; if (prevProps.isSelected !== nextProps.isSelected) return false; if (prevProps.isDragOverlay !== nextProps.isDragOverlay) return false; if (prevProps.groupId !== nextProps.groupId) return false; if (prevProps.level !== nextProps.level) return false; // Compare level // REAL-TIME UPDATES: Always re-render if updatedAt changed (indicates real-time update) if (prevProps.task.updatedAt !== nextProps.task.updatedAt) return false; // Deep comparison for task properties that commonly change const taskProps = [ 'title', 'progress', 'status', 'priority', 'description', 'startDate', 'dueDate', 'sub_tasks_count', 'show_sub_tasks', ]; // Added sub_tasks_count and show_sub_tasks for (const prop of taskProps) { if (prevProps.task[prop as keyof Task] !== nextProps.task[prop as keyof Task]) { return false; } } // REAL-TIME UPDATES: Compare assignees and labels content (not just length) if (prevProps.task.assignees?.length !== nextProps.task.assignees?.length) return false; if (prevProps.task.assignees?.length > 0) { // Deep compare assignee IDs - create copies before sorting to avoid mutating read-only arrays const prevAssigneeIds = [...prevProps.task.assignees].sort(); const nextAssigneeIds = [...nextProps.task.assignees].sort(); for (let i = 0; i < prevAssigneeIds.length; i++) { if (prevAssigneeIds[i] !== nextAssigneeIds[i]) return false; } } if (prevProps.task.assignee_names?.length !== nextProps.task.assignee_names?.length) return false; if ( prevProps.task.assignee_names && nextProps.task.assignee_names && prevProps.task.assignee_names.length > 0 ) { // Deep compare assignee names for (let i = 0; i < prevProps.task.assignee_names.length; i++) { if (prevProps.task.assignee_names[i] !== nextProps.task.assignee_names[i]) return false; } } if (prevProps.task.labels?.length !== nextProps.task.labels?.length) return false; if (prevProps.task.labels?.length > 0) { // Deep compare label IDs and names for (let i = 0; i < prevProps.task.labels.length; i++) { const prevLabel = prevProps.task.labels[i]; const nextLabel = nextProps.task.labels[i]; if ( prevLabel.id !== nextLabel.id || prevLabel.name !== nextLabel.name || prevLabel.color !== nextLabel.color ) { return false; } } } // Compare column configurations if (prevProps.fixedColumns?.length !== nextProps.fixedColumns?.length) return false; if (prevProps.scrollableColumns?.length !== nextProps.scrollableColumns?.length) return false; // If we reach here, props are effectively equal return true; } ); TaskRow.displayName = 'TaskRow'; export default TaskRow;