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, ClockCircleOutlined, UserOutlined, type InputRef } from './antd-imports'; import { DownOutlined, RightOutlined, ExpandAltOutlined, DoubleRightOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { Task } from '@/types/task-management.types'; import { RootState } from '@/app/store'; import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tooltip } from '@/components'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import TaskStatusDropdown from './task-status-dropdown'; import TaskPriorityDropdown from './task-priority-dropdown'; import TaskPhaseDropdown from './task-phase-dropdown'; import { formatDate as utilFormatDate, formatDateTime as utilFormatDateTime, createLabelsAdapter, createAssigneeAdapter, PRIORITY_COLORS as UTIL_PRIORITY_COLORS, performanceMonitor, taskPropsEqual } 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'; 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; 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 }>; } // 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 better performance const DragHandle = React.memo<{ isDarkMode: boolean; attributes: any; listeners: any }>(({ isDarkMode, attributes, listeners }) => ( {/* 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} )}
{/* Indicators section */} {!editTaskName && (
{/* Subtasks count */} {(task as any).subtasks_count && (task as any).subtasks_count > 0 && (
{ e.preventDefault(); e.stopPropagation(); handleToggleSubtasks?.(); }} > {(task as any).subtasks_count}
)} {/* Comments indicator */} {(task as any).comments_count && (task as any).comments_count > 0 && (
{(task as any).comments_count}
)} {/* Attachments indicator */} {(task as any).attachments_count && (task as any).attachments_count > 0 && (
{(task as any).attachments_count}
)}
)} {/* Right section with open button - CSS hover only */} {!editTaskName && (
)} ); case 'description': return (
); case 'progress': return (
{task.progress !== undefined && task.progress >= 0 && ( )}
); case 'members': return (
{task.assignee_names && task.assignee_names.length > 0 && ( )}
); case 'labels': return (
{task.labels?.map((label, index) => ( label.end && label.names && label.name ? ( ) : ( ) ))}
); case 'phase': return (
); case 'status': return (
); case 'priority': return (
); 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, renderColumnSimple, isDarkMode, task, isSelected, editTaskName, taskName, adapters, groupId, projectId, attributes, listeners, handleSelectChange, handleTaskNameSave, handleDateChange, dateValues, styleClasses ]); return (
{ setNodeRef(node); rowRef.current = node; }} style={dragStyle} className={`${styleClasses.container} task-row-optimized ${shouldRenderFull ? 'fully-loaded' : 'initial-load'} ${hasBeenFullyLoadedOnce.current ? 'stable-content' : ''}`} data-task-id={task.id} >
{/* Fixed Columns */} {fixedColumns && fixedColumns.length > 0 && (
sum + col.width, 0), }} > {fixedColumns.map((col, index) => renderColumn(col, true, index, fixedColumns.length))}
)} {/* Scrollable Columns */} {scrollableColumns && scrollableColumns.length > 0 && (
sum + col.width, 0) }} > {scrollableColumns.map((col, index) => renderColumn(col, false, index, scrollableColumns.length))}
)}
{/* Add Subtask Row */} {showAddSubtask && (
{/* Fixed Columns for Add Subtask */} {fixedColumns && fixedColumns.length > 0 && (
sum + col.width, 0), }} > {fixedColumns.map((col, index) => { // Fix border logic for add subtask row: fixed columns should have right border if scrollable columns exist const isActuallyLast = index === fixedColumns.length - 1 && (!scrollableColumns || scrollableColumns.length === 0); const borderClasses = `${isActuallyLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; if (col.key === 'task') { return (
setNewSubtaskName(e.target.value)} onPressEnter={handleAddSubtask} 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 (
); } })}
)} {/* Scrollable Columns for Add Subtask */} {scrollableColumns && scrollableColumns.length > 0 && (
sum + col.width, 0) }} > {scrollableColumns.map((col, index) => { const isLast = index === scrollableColumns.length - 1; const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; return (
); })}
)}
)}
); }, (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; // 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']; 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 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;