From 70cca5d4c0f9d831927d3dcf8800f894ec8d923f Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Thu, 10 Jul 2025 12:17:22 +0530 Subject: [PATCH] refactor(task-list): restructure TaskRow and introduce new column components - Refactored TaskRow to simplify state management and enhance readability by extracting logic into custom hooks. - Introduced new components for rendering task columns, including DatePickerColumn, TitleColumn, and various column types for better modularity. - Improved task name editing functionality with better state handling and click outside detection. - Streamlined date handling and formatting within the task row for improved user experience. --- .../src/components/task-list-v2/TaskRow.tsx | 906 ++---------------- .../components/DatePickerColumn.tsx | 136 +++ .../components/TaskRowColumns.tsx | 404 ++++++++ .../task-list-v2/components/TitleColumn.tsx | 258 +++++ .../task-list-v2/hooks/useTaskRowActions.ts | 63 ++ .../task-list-v2/hooks/useTaskRowColumns.tsx | 320 +++++++ .../task-list-v2/hooks/useTaskRowState.ts | 96 ++ .../task-list-v2/types/TaskRowTypes.ts | 245 +++++ 8 files changed, 1595 insertions(+), 833 deletions(-) create mode 100644 worklenz-frontend/src/components/task-list-v2/components/DatePickerColumn.tsx create mode 100644 worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx create mode 100644 worklenz-frontend/src/components/task-list-v2/components/TitleColumn.tsx create mode 100644 worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowActions.ts create mode 100644 worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx create mode 100644 worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowState.ts create mode 100644 worklenz-frontend/src/components/task-list-v2/types/TaskRowTypes.ts diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx index ba4e4370..24571b8b 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx @@ -1,32 +1,13 @@ -import React, { memo, useMemo, useCallback, useState, useRef, useEffect } from 'react'; +import React, { memo, useMemo } 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, Input } from 'antd'; -import type { InputRef } 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'; +import { selectTaskById } from '@/features/task-management/task-management.slice'; +import { selectIsTaskSelected } from '@/features/task-management/selection.slice'; +import { useTaskRowState } from './hooks/useTaskRowState'; +import { useTaskRowActions } from './hooks/useTaskRowActions'; +import { useTaskRowColumns } from './hooks/useTaskRowColumns'; interface TaskRowProps { taskId: string; @@ -45,79 +26,51 @@ interface TaskRowProps { updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; } -interface TaskLabelsCellProps { - labels: Task['labels']; - isDarkMode: boolean; -} +const TaskRow: React.FC = memo(({ + taskId, + projectId, + visibleColumns, + isSubtask = false, + isFirstInGroup = false, + updateTaskCustomColumnValue +}) => { + // Get task data and selection state from Redux + const task = useAppSelector(state => selectTaskById(state, taskId)); + const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId)); + const themeMode = useAppSelector(state => state.themeReducer.mode); + const isDarkMode = themeMode === 'dark'; -const TaskLabelsCell: React.FC = memo(({ labels, isDarkMode }) => { - if (!labels) { + // Early return if task is not found + if (!task) { return null; } - return ( -
- {labels.map((label, index) => { - const extendedLabel = label as any; - return extendedLabel.end && extendedLabel.names && extendedLabel.name ? ( - - ) : ( - - ); - })} -
- ); -}); + // Use extracted hooks for state management + const { + activeDatePicker, + setActiveDatePicker, + editTaskName, + setEditTaskName, + taskName, + setTaskName, + taskDisplayName, + convertedTask, + formattedDates, + dateValues, + labelsAdapter, + } = useTaskRowState(task); -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, isFirstInGroup = 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); - - // State for editing task name - const [editTaskName, setEditTaskName] = useState(false); - const [taskName, setTaskName] = useState(task.title || task.name || ''); - const inputRef = useRef(null); - const wrapperRef = useRef(null); - - if (!task) { - return null; // Don't render if task is not found in store - } + const { + handleCheckboxChange, + handleTaskNameSave, + handleTaskNameEdit, + } = useTaskRowActions({ + task, + taskId, + taskName, + editTaskName, + setEditTaskName, + }); // Drag and drop functionality - only enable for parent tasks const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ @@ -129,6 +82,33 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn disabled: isSubtask, // Disable drag and drop for subtasks }); + // Use extracted column renderer hook + const { renderColumn } = useTaskRowColumns({ + task, + projectId, + isSubtask, + isSelected, + isDarkMode, + visibleColumns, + updateTaskCustomColumnValue, + taskDisplayName, + convertedTask, + formattedDates, + dateValues, + labelsAdapter, + activeDatePicker, + setActiveDatePicker, + editTaskName, + taskName, + setEditTaskName, + setTaskName, + handleCheckboxChange, + handleTaskNameSave, + handleTaskNameEdit, + attributes, + listeners, + }); + // Memoize style object to prevent unnecessary re-renders const style = useMemo(() => ({ transform: CSS.Transform.toString(transform), @@ -136,746 +116,6 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn 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]); - - // Handle task name save - const handleTaskNameSave = useCallback(() => { - const newTaskName = inputRef.current?.input?.value || taskName; - if (newTaskName?.trim() !== '' && connected && newTaskName.trim() !== (task.title || task.name || '').trim()) { - socket?.emit( - SocketEvents.TASK_NAME_CHANGE.toString(), - JSON.stringify({ - task_id: task.id, - name: newTaskName.trim(), - parent_task: task.parent_task_id, - }) - ); - } - setEditTaskName(false); - }, [taskName, connected, socket, task.id, task.parent_task_id, task.title, task.name]); - - // Handle click outside for task name editing - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { - handleTaskNameSave(); - } - }; - - if (editTaskName) { - document.addEventListener('mousedown', handleClickOutside); - inputRef.current?.focus(); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [editTaskName, handleTaskNameSave]); - - // Update local taskName state when task name changes - useEffect(() => { - setTaskName(task.title || task.name || ''); - }, [task.title, task.name]); - - // 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.createdAt || task.created_at) ? formatDate(task.createdAt || task.created_at) : null, - updated: task.updatedAt ? formatDate(task.updatedAt) : null, - }), [task.dueDate, task.due_date, task.startDate, task.completedAt, task.createdAt, 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.all_labels?.map(label => ({ - id: label.id, - name: label.name, - color_code: label.color_code, - })) || [], - 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.all_labels, task.labels, task.all_labels?.length, 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 ( -
- {editTaskName ? ( - /* Full cell input when editing */ -
- setTaskName(e.target.value)} - autoFocus - onPressEnter={handleTaskNameSave} - onBlur={handleTaskNameSave} - className="text-sm" - style={{ - width: '100%', - height: '38px', - margin: '0', - padding: '8px 12px', - border: '1px solid #1677ff', - backgroundColor: 'rgba(22, 119, 255, 0.02)', - borderRadius: '3px', - fontSize: '14px', - lineHeight: '22px', - boxSizing: 'border-box', - outline: 'none', - boxShadow: '0 0 0 2px rgba(22, 119, 255, 0.1)', - }} - /> -
- ) : ( - /* Normal layout when not editing */ - <> -
- {/* 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 &&
} - -
- {/* Task name with dynamic width */} -
- { - e.stopPropagation(); - e.preventDefault(); - setEditTaskName(true); - }} - title={taskDisplayName} - > - {taskDisplayName} - -
- - {/* Indicators container - flex-shrink-0 to prevent compression */} -
- {/* Subtask count indicator - only show if count > 0 */} - {!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && ( - -
- - {task.sub_tasks_count} - - -
-
- )} - - {/* Task indicators - compact layout */} - {task.comments_count != null && task.comments_count !== 0 && ( - - - - )} - - {task.has_subscribers && ( - - - - )} - - {task.attachments_count != null && task.attachments_count !== 0 && ( - - - - )} - - {task.has_dependencies && ( - - - - )} - - {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': - const labelsColumn = visibleColumns.find(col => col.id === 'labels'); - const labelsStyle = { - ...baseStyle, - ...(labelsColumn?.width === 'auto' ? { minWidth: '200px', flexGrow: 1 } : {}) - }; - return ( -
- - -
- ); - - case 'phase': - return ( -
- -
- ); - - case 'timeTracking': - return ( -
- -
- ); - - case 'estimation': - // Use timeTracking.estimated which is the converted value from backend's total_minutes - const estimationDisplay = (() => { - const estimatedHours = task.timeTracking?.estimated; - - if (estimatedHours && estimatedHours > 0) { - // Convert decimal hours to hours and minutes for display - const hours = Math.floor(estimatedHours); - const minutes = Math.round((estimatedHours - hours) * 60); - - if (hours > 0 && minutes > 0) { - return `${hours}h ${minutes}m`; - } else if (hours > 0) { - return `${hours}h`; - } else if (minutes > 0) { - return `${minutes}m`; - } - } - - return null; - })(); - - return ( -
- {estimationDisplay ? ( - - {estimationDisplay} - - ) : ( - - - - - )} -
- ); - - 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, - - // Edit task name state - CRITICAL for re-rendering - editTaskName, - taskName, - - // 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, - handleTaskNameSave, - - // Translation - t, - - // Custom columns - visibleColumns, - updateTaskCustomColumnValue, - ]); - return (
void; +} + +export const DatePickerColumn: React.FC = memo(({ + width, + task, + field, + formattedDate, + dateValue, + isDarkMode, + activeDatePicker, + onActiveDatePickerChange +}) => { + const { socket, connected } = useSocket(); + const { t } = useTranslation('task-list-table'); + + // Handle date change + const handleDateChange = useCallback( + (date: dayjs.Dayjs | null) => { + 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 + onActiveDatePickerChange(null); + }, + [connected, socket, task.id, field, onActiveDatePickerChange] + ); + + // Handle clear date + const handleClearDate = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + handleDateChange(null); + }, [handleDateChange]); + + // Handle open date picker + const handleOpenDatePicker = useCallback(() => { + onActiveDatePickerChange(field); + }, [field, onActiveDatePickerChange]); + + const isActive = activeDatePicker === field; + const placeholder = field === 'dueDate' ? t('dueDatePlaceholder') : t('startDatePlaceholder'); + const clearTitle = field === 'dueDate' ? t('clearDueDate') : t('clearStartDate'); + const setTitle = field === 'dueDate' ? t('setDueDate') : t('setStartDate'); + + return ( +
+ {isActive ? ( +
+ { + if (!open) { + onActiveDatePickerChange(null); + } + }} + autoFocus + /> + {/* Custom clear button */} + {dateValue && ( + + )} +
+ ) : ( +
{ + e.stopPropagation(); + handleOpenDatePicker(); + }} + > + {formattedDate ? ( + + {formattedDate} + + ) : ( + + {setTitle} + + )} +
+ )} +
+ ); +}); + +DatePickerColumn.displayName = 'DatePickerColumn'; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx b/worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx new file mode 100644 index 00000000..b22690ca --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx @@ -0,0 +1,404 @@ +import React, { memo, useCallback, useState, useRef } from 'react'; +import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons'; +import { Checkbox, DatePicker, Tooltip, Input } from 'antd'; +import type { InputRef } 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 TaskPhaseDropdown from '@/components/task-management/task-phase-dropdown'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice'; +import { useTranslation } from 'react-i18next'; +import TaskTimeTracking from '../TaskTimeTracking'; +import { CustomNumberLabel, CustomColordLabel } from '@/components'; +import LabelsSelector from '@/components/LabelsSelector'; +import { CustomColumnCell } from './CustomColumnComponents'; + +// Utility function to get task display name with fallbacks +export const getTaskDisplayName = (task: Task): string => { + 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 +export const formatDate = (dateString: string): string => { + try { + return format(new Date(dateString), 'MMM d, yyyy'); + } catch { + return ''; + } +}; + +interface TaskLabelsCellProps { + labels: Task['labels']; + isDarkMode: boolean; +} + +export 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'; + +interface DragHandleColumnProps { + width: string; + isSubtask: boolean; + attributes: any; + listeners: any; +} + +export const DragHandleColumn: React.FC = memo(({ width, isSubtask, attributes, listeners }) => ( +
+ {!isSubtask && } +
+)); + +DragHandleColumn.displayName = 'DragHandleColumn'; + +interface CheckboxColumnProps { + width: string; + isSelected: boolean; + onCheckboxChange: (e: any) => void; +} + +export const CheckboxColumn: React.FC = memo(({ width, isSelected, onCheckboxChange }) => ( +
+ e.stopPropagation()} + /> +
+)); + +CheckboxColumn.displayName = 'CheckboxColumn'; + +interface TaskKeyColumnProps { + width: string; + taskKey: string; +} + +export const TaskKeyColumn: React.FC = memo(({ width, taskKey }) => ( +
+ + {taskKey || 'N/A'} + +
+)); + +TaskKeyColumn.displayName = 'TaskKeyColumn'; + +interface DescriptionColumnProps { + width: string; + description: string; +} + +export const DescriptionColumn: React.FC = memo(({ width, description }) => ( +
+
+
+)); + +DescriptionColumn.displayName = 'DescriptionColumn'; + +interface StatusColumnProps { + width: string; + task: Task; + projectId: string; + isDarkMode: boolean; +} + +export const StatusColumn: React.FC = memo(({ width, task, projectId, isDarkMode }) => ( +
+ +
+)); + +StatusColumn.displayName = 'StatusColumn'; + +interface AssigneesColumnProps { + width: string; + task: Task; + convertedTask: any; + isDarkMode: boolean; +} + +export const AssigneesColumn: React.FC = memo(({ width, task, convertedTask, isDarkMode }) => ( +
+ + +
+)); + +AssigneesColumn.displayName = 'AssigneesColumn'; + +interface PriorityColumnProps { + width: string; + task: Task; + projectId: string; + isDarkMode: boolean; +} + +export const PriorityColumn: React.FC = memo(({ width, task, projectId, isDarkMode }) => ( +
+ +
+)); + +PriorityColumn.displayName = 'PriorityColumn'; + +interface ProgressColumnProps { + width: string; + task: Task; +} + +export const ProgressColumn: React.FC = memo(({ width, task }) => ( +
+ {task.progress !== undefined && + task.progress >= 0 && + (task.progress === 100 ? ( +
+ +
+ ) : ( + + ))} +
+)); + +ProgressColumn.displayName = 'ProgressColumn'; + +interface LabelsColumnProps { + width: string; + task: Task; + labelsAdapter: any; + isDarkMode: boolean; + visibleColumns: any[]; +} + +export const LabelsColumn: React.FC = memo(({ width, task, labelsAdapter, isDarkMode, visibleColumns }) => { + const labelsColumn = visibleColumns.find(col => col.id === 'labels'); + const labelsStyle = { + width, + ...(labelsColumn?.width === 'auto' ? { minWidth: '200px', flexGrow: 1 } : {}) + }; + + return ( +
+ + +
+ ); +}); + +LabelsColumn.displayName = 'LabelsColumn'; + +interface PhaseColumnProps { + width: string; + task: Task; + projectId: string; + isDarkMode: boolean; +} + +export const PhaseColumn: React.FC = memo(({ width, task, projectId, isDarkMode }) => ( +
+ +
+)); + +PhaseColumn.displayName = 'PhaseColumn'; + +interface TimeTrackingColumnProps { + width: string; + taskId: string; + isDarkMode: boolean; +} + +export const TimeTrackingColumn: React.FC = memo(({ width, taskId, isDarkMode }) => ( +
+ +
+)); + +TimeTrackingColumn.displayName = 'TimeTrackingColumn'; + +interface EstimationColumnProps { + width: string; + task: Task; +} + +export const EstimationColumn: React.FC = memo(({ width, task }) => { + const estimationDisplay = (() => { + const estimatedHours = task.timeTracking?.estimated; + + if (estimatedHours && estimatedHours > 0) { + const hours = Math.floor(estimatedHours); + const minutes = Math.round((estimatedHours - hours) * 60); + + if (hours > 0 && minutes > 0) { + return `${hours}h ${minutes}m`; + } else if (hours > 0) { + return `${hours}h`; + } else if (minutes > 0) { + return `${minutes}m`; + } + } + + return null; + })(); + + return ( +
+ {estimationDisplay ? ( + + {estimationDisplay} + + ) : ( + + - + + )} +
+ ); +}); + +EstimationColumn.displayName = 'EstimationColumn'; + +interface DateColumnProps { + width: string; + formattedDate: string | null; + placeholder?: string; +} + +export const DateColumn: React.FC = memo(({ width, formattedDate, placeholder = '-' }) => ( +
+ {formattedDate ? ( + + {formattedDate} + + ) : ( + {placeholder} + )} +
+)); + +DateColumn.displayName = 'DateColumn'; + +interface ReporterColumnProps { + width: string; + reporter: string; +} + +export const ReporterColumn: React.FC = memo(({ width, reporter }) => ( +
+ {reporter ? ( + {reporter} + ) : ( + - + )} +
+)); + +ReporterColumn.displayName = 'ReporterColumn'; + +interface CustomColumnProps { + width: string; + column: any; + task: Task; + updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; +} + +export const CustomColumn: React.FC = memo(({ width, column, task, updateTaskCustomColumnValue }) => { + if (!updateTaskCustomColumnValue) return null; + + return ( +
+ +
+ ); +}); + +CustomColumn.displayName = 'CustomColumn'; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/components/TitleColumn.tsx b/worklenz-frontend/src/components/task-list-v2/components/TitleColumn.tsx new file mode 100644 index 00000000..a005a1c4 --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/components/TitleColumn.tsx @@ -0,0 +1,258 @@ +import React, { memo, useCallback, useState, useRef, useEffect } from 'react'; +import { RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons'; +import { Input, Tooltip } from 'antd'; +import type { InputRef } from 'antd'; +import { Task } from '@/types/task-management.types'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { toggleTaskExpansion, fetchSubTasks } from '@/features/task-management/task-management.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 { getTaskDisplayName } from './TaskRowColumns'; + +interface TitleColumnProps { + width: string; + task: Task; + projectId: string; + isSubtask: boolean; + taskDisplayName: string; + editTaskName: boolean; + taskName: string; + onEditTaskName: (editing: boolean) => void; + onTaskNameChange: (name: string) => void; + onTaskNameSave: () => void; +} + +export const TitleColumn: React.FC = memo(({ + width, + task, + projectId, + isSubtask, + taskDisplayName, + editTaskName, + taskName, + onEditTaskName, + onTaskNameChange, + onTaskNameSave +}) => { + const dispatch = useAppDispatch(); + const { socket, connected } = useSocket(); + const { t } = useTranslation('task-list-table'); + const inputRef = useRef(null); + const wrapperRef = useRef(null); + + // 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 task name save + const handleTaskNameSave = useCallback(() => { + const newTaskName = inputRef.current?.input?.value || taskName; + if (newTaskName?.trim() !== '' && connected && newTaskName.trim() !== (task.title || task.name || '').trim()) { + socket?.emit( + SocketEvents.TASK_NAME_CHANGE.toString(), + JSON.stringify({ + task_id: task.id, + name: newTaskName.trim(), + parent_task: task.parent_task_id, + }) + ); + } + onEditTaskName(false); + }, [taskName, connected, socket, task.id, task.parent_task_id, task.title, task.name, onEditTaskName]); + + // Handle click outside for task name editing + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + handleTaskNameSave(); + } + }; + + if (editTaskName) { + document.addEventListener('mousedown', handleClickOutside); + inputRef.current?.focus(); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [editTaskName, handleTaskNameSave]); + + return ( +
+ {editTaskName ? ( + /* Full cell input when editing */ +
+ onTaskNameChange(e.target.value)} + autoFocus + onPressEnter={handleTaskNameSave} + onBlur={handleTaskNameSave} + className="text-sm" + style={{ + width: '100%', + height: '38px', + margin: '0', + padding: '8px 12px', + border: '1px solid #1677ff', + backgroundColor: 'rgba(22, 119, 255, 0.02)', + borderRadius: '3px', + fontSize: '14px', + lineHeight: '22px', + boxSizing: 'border-box', + outline: 'none', + boxShadow: '0 0 0 2px rgba(22, 119, 255, 0.1)', + }} + /> +
+ ) : ( + /* Normal layout when not editing */ + <> +
+ {/* 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 &&
} + +
+ {/* Task name with dynamic width */} +
+ { + e.stopPropagation(); + e.preventDefault(); + onEditTaskName(true); + }} + title={taskDisplayName} + > + {taskDisplayName} + +
+ + {/* Indicators container - flex-shrink-0 to prevent compression */} +
+ {/* Subtask count indicator - only show if count > 0 */} + {!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && ( + +
+ + {task.sub_tasks_count} + + +
+
+ )} + + {/* Task indicators - compact layout */} + {task.comments_count != null && task.comments_count !== 0 && ( + + + + )} + + {task.has_subscribers && ( + + + + )} + + {task.attachments_count != null && task.attachments_count !== 0 && ( + + + + )} + + {task.has_dependencies && ( + + + + )} + + {task.schedule_id && ( + + + + )} +
+
+
+ + + + )} +
+ ); +}); + +TitleColumn.displayName = 'TitleColumn'; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowActions.ts b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowActions.ts new file mode 100644 index 00000000..a5a816b6 --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowActions.ts @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { useSocket } from '@/socket/socketContext'; +import { SocketEvents } from '@/shared/socket-events'; +import { toggleTaskSelection } from '@/features/task-management/selection.slice'; +import { Task } from '@/types/task-management.types'; + +interface UseTaskRowActionsProps { + task: Task; + taskId: string; + taskName: string; + editTaskName: boolean; + setEditTaskName: (editing: boolean) => void; +} + +export const useTaskRowActions = ({ + task, + taskId, + taskName, + editTaskName, + setEditTaskName, +}: UseTaskRowActionsProps) => { + const dispatch = useAppDispatch(); + const { socket, connected } = useSocket(); + + // Handle checkbox change + const handleCheckboxChange = useCallback((e: any) => { + e.stopPropagation(); // Prevent row click when clicking checkbox + dispatch(toggleTaskSelection(taskId)); + }, [dispatch, taskId]); + + // Handle task name save + const handleTaskNameSave = useCallback(() => { + if (taskName?.trim() !== '' && connected && taskName.trim() !== (task.title || task.name || '').trim()) { + socket?.emit( + SocketEvents.TASK_NAME_CHANGE.toString(), + JSON.stringify({ + task_id: task.id, + name: taskName.trim(), + parent_task: task.parent_task_id, + }) + ); + } + setEditTaskName(false); + }, [taskName, connected, socket, task.id, task.parent_task_id, task.title, task.name, setEditTaskName]); + + // Handle task name edit start + const handleTaskNameEdit = useCallback(() => { + setEditTaskName(true); + }, [setEditTaskName]); + + // Handle task name change + const handleTaskNameChange = useCallback((name: string) => { + // This will be handled by the parent component's state setter + }, []); + + return { + handleCheckboxChange, + handleTaskNameSave, + handleTaskNameEdit, + handleTaskNameChange, + }; +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx new file mode 100644 index 00000000..344f4080 --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx @@ -0,0 +1,320 @@ +import React, { useCallback } from 'react'; +import { Task } from '@/types/task-management.types'; +import { + DragHandleColumn, + CheckboxColumn, + TaskKeyColumn, + DescriptionColumn, + StatusColumn, + AssigneesColumn, + PriorityColumn, + ProgressColumn, + LabelsColumn, + PhaseColumn, + TimeTrackingColumn, + EstimationColumn, + DateColumn, + ReporterColumn, + CustomColumn, +} from '../components/TaskRowColumns'; +import { TitleColumn } from '../components/TitleColumn'; +import { DatePickerColumn } from '../components/DatePickerColumn'; + +interface UseTaskRowColumnsProps { + task: Task; + projectId: string; + isSubtask: boolean; + isSelected: boolean; + isDarkMode: boolean; + visibleColumns: Array<{ + id: string; + width: string; + isSticky?: boolean; + key?: string; + custom_column?: boolean; + custom_column_obj?: any; + isCustom?: boolean; + }>; + updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; + + // From useTaskRowState + taskDisplayName: string; + convertedTask: any; + formattedDates: any; + dateValues: any; + labelsAdapter: any; + activeDatePicker: string | null; + setActiveDatePicker: (field: string | null) => void; + editTaskName: boolean; + taskName: string; + setEditTaskName: (editing: boolean) => void; + setTaskName: (name: string) => void; + + // From useTaskRowActions + handleCheckboxChange: (e: any) => void; + handleTaskNameSave: () => void; + handleTaskNameEdit: () => void; + + // Drag and drop + attributes: any; + listeners: any; +} + +export const useTaskRowColumns = ({ + task, + projectId, + isSubtask, + isSelected, + isDarkMode, + visibleColumns, + updateTaskCustomColumnValue, + taskDisplayName, + convertedTask, + formattedDates, + dateValues, + labelsAdapter, + activeDatePicker, + setActiveDatePicker, + editTaskName, + taskName, + setEditTaskName, + setTaskName, + handleCheckboxChange, + handleTaskNameSave, + handleTaskNameEdit, + attributes, + listeners, +}: UseTaskRowColumnsProps) => { + + const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { + switch (columnId) { + case 'dragHandle': + return ( + + ); + + case 'checkbox': + return ( + + ); + + case 'taskKey': + return ( + + ); + + case 'title': + return ( + + ); + + case 'description': + return ( + + ); + + case 'status': + return ( + + ); + + case 'assignees': + return ( + + ); + + case 'priority': + return ( + + ); + + case 'dueDate': + return ( + + ); + + case 'startDate': + return ( + + ); + + case 'progress': + return ( + + ); + + case 'labels': + return ( + + ); + + case 'phase': + return ( + + ); + + case 'timeTracking': + return ( + + ); + + case 'estimation': + return ( + + ); + + case 'completedDate': + return ( + + ); + + case 'createdDate': + return ( + + ); + + case 'lastUpdated': + return ( + + ); + + case 'reporter': + return ( + + ); + + default: + // Handle custom columns + const column = visibleColumns.find(col => col.id === columnId); + if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) { + return ( + + ); + } + return null; + } + }, [ + task, + projectId, + isSubtask, + isSelected, + isDarkMode, + visibleColumns, + updateTaskCustomColumnValue, + taskDisplayName, + convertedTask, + formattedDates, + dateValues, + labelsAdapter, + activeDatePicker, + setActiveDatePicker, + editTaskName, + taskName, + setEditTaskName, + setTaskName, + handleCheckboxChange, + handleTaskNameSave, + handleTaskNameEdit, + attributes, + listeners, + ]); + + return { renderColumn }; +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowState.ts b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowState.ts new file mode 100644 index 00000000..2919caa3 --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowState.ts @@ -0,0 +1,96 @@ +import { useState, useEffect, useMemo } from 'react'; +import { Task } from '@/types/task-management.types'; +import { InlineMember } from '@/types/teamMembers/inlineMember.types'; +import { dayjs } from '@/shared/antd-imports'; +import { getTaskDisplayName, formatDate } from '../components/TaskRowColumns'; + +export const useTaskRowState = (task: Task) => { + // State for tracking which date picker is open + const [activeDatePicker, setActiveDatePicker] = useState(null); + + // State for editing task name + const [editTaskName, setEditTaskName] = useState(false); + const [taskName, setTaskName] = useState(task.title || task.name || ''); + + // Update local taskName state when task name changes + useEffect(() => { + setTaskName(task.title || task.name || ''); + }, [task.title, task.name]); + + // 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.createdAt || task.created_at) ? formatDate(task.createdAt || task.created_at) : null, + updated: task.updatedAt ? formatDate(task.updatedAt) : null, + }), [task.dueDate, task.due_date, task.startDate, task.completedAt, task.createdAt, 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.all_labels?.map(label => ({ + id: label.id, + name: label.name, + color_code: label.color_code, + })) || [], + 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.all_labels, task.labels, task.all_labels?.length, task.labels?.length]); + + return { + // State + activeDatePicker, + setActiveDatePicker, + editTaskName, + setEditTaskName, + taskName, + setTaskName, + + // Computed values + taskDisplayName, + convertedTask, + formattedDates, + dateValues, + labelsAdapter, + }; +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/types/TaskRowTypes.ts b/worklenz-frontend/src/components/task-list-v2/types/TaskRowTypes.ts new file mode 100644 index 00000000..b9149229 --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/types/TaskRowTypes.ts @@ -0,0 +1,245 @@ +import { Task } from '@/types/task-management.types'; +import { dayjs } from '@/shared/antd-imports'; + +export interface TaskRowColumn { + id: string; + width: string; + isSticky?: boolean; + key?: string; + custom_column?: boolean; + custom_column_obj?: any; + isCustom?: boolean; +} + +export interface TaskRowProps { + taskId: string; + projectId: string; + visibleColumns: TaskRowColumn[]; + isSubtask?: boolean; + isFirstInGroup?: boolean; + updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; +} + +export interface TaskRowState { + activeDatePicker: string | null; + editTaskName: boolean; + taskName: string; +} + +export interface TaskRowComputedValues { + taskDisplayName: string; + convertedTask: ConvertedTask; + formattedDates: FormattedDates; + dateValues: DateValues; + labelsAdapter: LabelsAdapter; +} + +export interface ConvertedTask { + id: string; + name: string; + task_key: string; + assignees: ConvertedAssignee[]; + parent_task_id?: string; + status_id?: string; + project_id?: string; + manual_progress?: boolean; +} + +export interface ConvertedAssignee { + team_member_id: string; + id: string; + project_member_id: string; + name: string; +} + +export interface FormattedDates { + due: string | null; + start: string | null; + completed: string | null; + created: string | null; + updated: string | null; +} + +export interface DateValues { + start: dayjs.Dayjs | undefined; + due: dayjs.Dayjs | undefined; +} + +export interface LabelsAdapter { + id: string; + name: string; + parent_task_id?: string; + manual_progress: boolean; + all_labels: LabelInfo[]; + labels: LabelInfo[]; +} + +export interface LabelInfo { + id: string; + name: string; + color_code: string; +} + +export interface TaskRowActions { + handleCheckboxChange: (e: any) => void; + handleTaskNameSave: () => void; + handleTaskNameEdit: () => void; + handleTaskNameChange: (name: string) => void; +} + +export interface ColumnRendererProps { + task: Task; + projectId: string; + isSubtask: boolean; + isSelected: boolean; + isDarkMode: boolean; + visibleColumns: TaskRowColumn[]; + updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; + taskDisplayName: string; + convertedTask: ConvertedTask; + formattedDates: FormattedDates; + dateValues: DateValues; + labelsAdapter: LabelsAdapter; + activeDatePicker: string | null; + setActiveDatePicker: (field: string | null) => void; + editTaskName: boolean; + taskName: string; + setEditTaskName: (editing: boolean) => void; + setTaskName: (name: string) => void; + handleCheckboxChange: (e: any) => void; + handleTaskNameSave: () => void; + handleTaskNameEdit: () => void; + attributes: any; + listeners: any; +} + +export type ColumnId = + | 'dragHandle' + | 'checkbox' + | 'taskKey' + | 'title' + | 'description' + | 'status' + | 'assignees' + | 'priority' + | 'dueDate' + | 'startDate' + | 'progress' + | 'labels' + | 'phase' + | 'timeTracking' + | 'estimation' + | 'completedDate' + | 'createdDate' + | 'lastUpdated' + | 'reporter' + | string; // Allow custom column IDs + +export interface BaseColumnProps { + width: string; +} + +export interface DragHandleColumnProps extends BaseColumnProps { + isSubtask: boolean; + attributes: any; + listeners: any; +} + +export interface CheckboxColumnProps extends BaseColumnProps { + isSelected: boolean; + onCheckboxChange: (e: any) => void; +} + +export interface TaskKeyColumnProps extends BaseColumnProps { + taskKey: string; +} + +export interface TitleColumnProps extends BaseColumnProps { + task: Task; + projectId: string; + isSubtask: boolean; + taskDisplayName: string; + editTaskName: boolean; + taskName: string; + onEditTaskName: (editing: boolean) => void; + onTaskNameChange: (name: string) => void; + onTaskNameSave: () => void; +} + +export interface DescriptionColumnProps extends BaseColumnProps { + description: string; +} + +export interface StatusColumnProps extends BaseColumnProps { + task: Task; + projectId: string; + isDarkMode: boolean; +} + +export interface AssigneesColumnProps extends BaseColumnProps { + task: Task; + convertedTask: ConvertedTask; + isDarkMode: boolean; +} + +export interface PriorityColumnProps extends BaseColumnProps { + task: Task; + projectId: string; + isDarkMode: boolean; +} + +export interface DatePickerColumnProps extends BaseColumnProps { + task: Task; + field: 'dueDate' | 'startDate'; + formattedDate: string | null; + dateValue: dayjs.Dayjs | undefined; + isDarkMode: boolean; + activeDatePicker: string | null; + onActiveDatePickerChange: (field: string | null) => void; +} + +export interface ProgressColumnProps extends BaseColumnProps { + task: Task; +} + +export interface LabelsColumnProps extends BaseColumnProps { + task: Task; + labelsAdapter: LabelsAdapter; + isDarkMode: boolean; + visibleColumns: TaskRowColumn[]; +} + +export interface PhaseColumnProps extends BaseColumnProps { + task: Task; + projectId: string; + isDarkMode: boolean; +} + +export interface TimeTrackingColumnProps extends BaseColumnProps { + taskId: string; + isDarkMode: boolean; +} + +export interface EstimationColumnProps extends BaseColumnProps { + task: Task; +} + +export interface DateColumnProps extends BaseColumnProps { + formattedDate: string | null; + placeholder?: string; +} + +export interface ReporterColumnProps extends BaseColumnProps { + reporter: string; +} + +export interface CustomColumnProps extends BaseColumnProps { + column: TaskRowColumn; + task: Task; + updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; +} + +export interface TaskLabelsCellProps { + labels: Task['labels']; + isDarkMode: boolean; +} \ No newline at end of file