import React, { memo, useMemo, useCallback, useState } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons'; import { Checkbox, DatePicker, Tooltip } from 'antd'; import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports'; import { Task } from '@/types/task-management.types'; import { InlineMember } from '@/types/teamMembers/inlineMember.types'; import AssigneeSelector from '@/components/AssigneeSelector'; import { format } from 'date-fns'; import AvatarGroup from '../AvatarGroup'; import { DEFAULT_TASK_NAME } from '@/shared/constants'; import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress'; import TaskStatusDropdown from '@/components/task-management/task-status-dropdown'; import TaskPriorityDropdown from '@/components/task-management/task-priority-dropdown'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { selectTaskById, toggleTaskExpansion, fetchSubTasks } from '@/features/task-management/task-management.slice'; import { selectIsTaskSelected, toggleTaskSelection } from '@/features/task-management/selection.slice'; import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import { useTranslation } from 'react-i18next'; import TaskTimeTracking from './TaskTimeTracking'; import { CustomNumberLabel, CustomColordLabel } from '@/components'; import LabelsSelector from '@/components/LabelsSelector'; import TaskPhaseDropdown from '@/components/task-management/task-phase-dropdown'; import { CustomColumnCell } from './components/CustomColumnComponents'; interface TaskRowProps { taskId: string; projectId: string; visibleColumns: Array<{ id: string; width: string; isSticky?: boolean; key?: string; custom_column?: boolean; custom_column_obj?: any; isCustom?: boolean; }>; isSubtask?: boolean; updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; } interface TaskLabelsCellProps { labels: Task['labels']; isDarkMode: boolean; } const TaskLabelsCell: React.FC = memo(({ labels, isDarkMode }) => { if (!labels) { return null; } return (
{labels.map((label, index) => { const extendedLabel = label as any; return extendedLabel.end && extendedLabel.names && extendedLabel.name ? ( ) : ( ); })}
); }); TaskLabelsCell.displayName = 'TaskLabelsCell'; // Utility function to get task display name with fallbacks const getTaskDisplayName = (task: Task): string => { // Check each field and only use if it has actual content after trimming if (task.title && task.title.trim()) return task.title.trim(); if (task.name && task.name.trim()) return task.name.trim(); if (task.task_key && task.task_key.trim()) return task.task_key.trim(); return DEFAULT_TASK_NAME; }; // Memoized date formatter to avoid repeated date parsing const formatDate = (dateString: string): string => { try { return format(new Date(dateString), 'MMM d, yyyy'); } catch { return ''; } }; const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumns, isSubtask = false, updateTaskCustomColumnValue }) => { const dispatch = useAppDispatch(); const task = useAppSelector(state => selectTaskById(state, taskId)); const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId)); const { socket, connected } = useSocket(); const { t } = useTranslation('task-list-table'); // State for tracking which date picker is open const [activeDatePicker, setActiveDatePicker] = useState(null); if (!task) { return null; // Don't render if task is not found in store } // Drag and drop functionality - only enable for parent tasks const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id, data: { type: 'task', task, }, disabled: isSubtask, // Disable drag and drop for subtasks }); // Memoize style object to prevent unnecessary re-renders const style = useMemo(() => ({ transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }), [transform, transition, isDragging]); // Get dark mode from Redux state const themeMode = useAppSelector(state => state.themeReducer.mode); const isDarkMode = themeMode === 'dark'; // Memoize task display name const taskDisplayName = useMemo(() => getTaskDisplayName(task), [task.title, task.name, task.task_key]); // Memoize converted task for AssigneeSelector to prevent recreation const convertedTask = useMemo(() => ({ id: task.id, name: taskDisplayName, task_key: task.task_key || taskDisplayName, assignees: task.assignee_names?.map((assignee: InlineMember, index: number) => ({ team_member_id: assignee.team_member_id || `assignee-${index}`, id: assignee.team_member_id || `assignee-${index}`, project_member_id: assignee.team_member_id || `assignee-${index}`, name: assignee.name || '', })) || [], parent_task_id: task.parent_task_id, status_id: undefined, project_id: undefined, manual_progress: undefined, }), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]); // Memoize formatted dates const formattedDates = useMemo(() => ({ due: (() => { const dateValue = task.dueDate || task.due_date; return dateValue ? formatDate(dateValue) : null; })(), start: task.startDate ? formatDate(task.startDate) : null, completed: task.completedAt ? formatDate(task.completedAt) : null, created: task.created_at ? formatDate(task.created_at) : null, updated: task.updatedAt ? formatDate(task.updatedAt) : null, }), [task.dueDate, task.due_date, task.startDate, task.completedAt, task.created_at, task.updatedAt]); // Memoize date values for DatePicker const dateValues = useMemo( () => ({ start: task.startDate ? dayjs(task.startDate) : undefined, due: (task.dueDate || task.due_date) ? dayjs(task.dueDate || task.due_date) : undefined, }), [task.startDate, task.dueDate, task.due_date] ); // Create labels adapter for LabelsSelector const labelsAdapter = useMemo(() => ({ id: task.id, name: task.title || task.name, parent_task_id: task.parent_task_id, manual_progress: false, all_labels: task.labels?.map(label => ({ id: label.id, name: label.name, color_code: label.color, })) || [], labels: task.labels?.map(label => ({ id: label.id, name: label.name, color_code: label.color, })) || [], }), [task.id, task.title, task.name, task.parent_task_id, task.labels, task.labels?.length]); // Handle checkbox change const handleCheckboxChange = useCallback((e: any) => { e.stopPropagation(); // Prevent row click when clicking checkbox dispatch(toggleTaskSelection(taskId)); }, [dispatch, taskId]); // Handle task expansion toggle const handleToggleExpansion = useCallback((e: React.MouseEvent) => { e.stopPropagation(); // Always try to fetch subtasks when expanding, regardless of count if (!task.show_sub_tasks && (!task.sub_tasks || task.sub_tasks.length === 0)) { dispatch(fetchSubTasks({ taskId: task.id, projectId })); } // Toggle expansion state dispatch(toggleTaskExpansion(task.id)); }, [dispatch, task.id, task.sub_tasks, task.show_sub_tasks, projectId]); // Handle date change 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, }) ); // Close the date picker after selection setActiveDatePicker(null); }, [connected, socket, task.id] ); // Memoize date picker handlers const datePickerHandlers = useMemo(() => ({ setDueDate: () => setActiveDatePicker('dueDate'), setStartDate: () => setActiveDatePicker('startDate'), clearDueDate: (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); handleDateChange(null, 'dueDate'); }, clearStartDate: (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); handleDateChange(null, 'startDate'); }, }), [handleDateChange]); const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { const baseStyle = { width }; switch (columnId) { case 'dragHandle': return (
{!isSubtask && }
); case 'checkbox': return (
e.stopPropagation()} />
); case 'taskKey': return (
{task.task_key || 'N/A'}
); case 'title': return (
{/* Indentation for subtasks - tighter spacing */} {isSubtask &&
} {/* Expand/Collapse button - only show for parent tasks */} {!isSubtask && ( )} {/* Additional indentation for subtasks after the expand button space */} {isSubtask &&
}
{taskDisplayName} {/* Subtask count indicator - only show if count > 1 */} {!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count !== 0 && (
{task.sub_tasks_count}
)} {/* Task indicators */}
{/* Comments count indicator - only show if count > 1 */} {task.comments_count != null && task.comments_count !== 0 && (
)} {/* Subscribers indicator */} {task.has_subscribers && ( )} {/* Attachments count indicator - only show if count > 1 */} {task.attachments_count != null && task.attachments_count !== 0 && (
)} {/* Dependencies indicator */} {task.has_dependencies && ( )} {/* Recurring task indicator */} {task.schedule_id && ( )}
); case 'description': return (
); case 'status': return (
); case 'assignees': return (
); case 'priority': return (
); case 'dueDate': return (
{activeDatePicker === 'dueDate' ? (
handleDateChange(date, 'dueDate')} placeholder={t('dueDatePlaceholder')} allowClear={false} suffixIcon={null} open={true} onOpenChange={(open) => { if (!open) { setActiveDatePicker(null); } }} autoFocus /> {/* Custom clear button */} {dateValues.due && ( )}
) : (
{ e.stopPropagation(); datePickerHandlers.setDueDate(); }} > {formattedDates.due ? ( {formattedDates.due} ) : ( {t('setDueDate')} )}
)}
); case 'progress': return (
{task.progress !== undefined && task.progress >= 0 && (task.progress === 100 ? (
) : ( ))}
); case 'labels': return (
); case 'phase': return (
); case 'timeTracking': return (
); case 'estimation': return (
{task.timeTracking?.estimated && ( {task.timeTracking.estimated}h )}
); case 'startDate': return (
{activeDatePicker === 'startDate' ? (
handleDateChange(date, 'startDate')} placeholder={t('startDatePlaceholder')} allowClear={false} suffixIcon={null} open={true} onOpenChange={(open) => { if (!open) { setActiveDatePicker(null); } }} autoFocus /> {/* Custom clear button */} {dateValues.start && ( )}
) : (
{ e.stopPropagation(); datePickerHandlers.setStartDate(); }} > {formattedDates.start ? ( {formattedDates.start} ) : ( {t('setStartDate')} )}
)}
); case 'completedDate': return (
{formattedDates.completed && ( {formattedDates.completed} )}
); case 'createdDate': return (
{formattedDates.created && ( {formattedDates.created} )}
); case 'lastUpdated': return (
{formattedDates.updated && ( {formattedDates.updated} )}
); case 'reporter': return (
{task.reporter && ( {task.reporter} )}
); default: // Handle custom columns const column = visibleColumns.find(col => col.id === columnId); if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) { return (
); } return null; } }, [ // Essential props and state attributes, listeners, isSelected, handleCheckboxChange, activeDatePicker, isDarkMode, projectId, // Task data - include specific fields that might update via socket task, task.labels, // Explicit dependency for labels updates task.phase, // Explicit dependency for phase updates task.comments_count, // Explicit dependency for comments count updates task.has_subscribers, // Explicit dependency for subscribers updates task.attachments_count, // Explicit dependency for attachments count updates task.has_dependencies, // Explicit dependency for dependencies updates task.schedule_id, // Explicit dependency for recurring task updates taskDisplayName, convertedTask, // Memoized values dateValues, formattedDates, labelsAdapter, // Handlers handleDateChange, datePickerHandlers, // Translation t, // Custom columns visibleColumns, updateTaskCustomColumnValue, ]); return (
{visibleColumns.map((column, index) => ( {renderColumn(column.id, column.width, column.isSticky, index)} ))}
); }); TaskRow.displayName = 'TaskRow'; export default TaskRow;