diff --git a/worklenz-backend/src/controllers/tasks-controller-v2.ts b/worklenz-backend/src/controllers/tasks-controller-v2.ts index 536ee723..d8a4b3bb 100644 --- a/worklenz-backend/src/controllers/tasks-controller-v2.ts +++ b/worklenz-backend/src/controllers/tasks-controller-v2.ts @@ -1031,6 +1031,8 @@ export default class TasksControllerV2 extends TasksControllerBase { } } + + // Transform tasks with all necessary data preprocessing const transformStartTime = performance.now(); const transformedTasks = tasks.map((task, index) => { @@ -1076,7 +1078,8 @@ export default class TasksControllerV2 extends TasksControllerBase { end: l.end, names: l.names })) || [], - dueDate: task.end_date, + dueDate: task.end_date || task.END_DATE, + startDate: task.start_date, timeTracking: { estimated: convertTimeValue(task.total_time), logged: convertTimeValue(task.time_spent), diff --git a/worklenz-frontend/public/locales/alb/task-list-table.json b/worklenz-frontend/public/locales/alb/task-list-table.json index cfb2d398..f385c0c6 100644 --- a/worklenz-frontend/public/locales/alb/task-list-table.json +++ b/worklenz-frontend/public/locales/alb/task-list-table.json @@ -36,7 +36,7 @@ "selectText": "Zgjidh", "labelsSelectorInputTip": "Shtyp Enter për të krijuar!", - "addTaskText": "+ Shto Detyrë", + "addTaskText": "Shto Detyrë", "addSubTaskText": "+ Shto Nën-Detyrë", "addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter", @@ -59,5 +59,11 @@ "convertToTask": "Shndërro në Detyrë", "delete": "Fshi", "searchByNameInputPlaceholder": "Kërko sipas emrit" - } + }, + "setDueDate": "Cakto datën e afatit", + "setStartDate": "Cakto datën e fillimit", + "clearDueDate": "Pastro datën e afatit", + "clearStartDate": "Pastro datën e fillimit", + "dueDatePlaceholder": "Data e afatit", + "startDatePlaceholder": "Data e fillimit" } diff --git a/worklenz-frontend/public/locales/de/task-list-table.json b/worklenz-frontend/public/locales/de/task-list-table.json index d399dea4..19b6e8c3 100644 --- a/worklenz-frontend/public/locales/de/task-list-table.json +++ b/worklenz-frontend/public/locales/de/task-list-table.json @@ -36,7 +36,7 @@ "selectText": "Auswählen", "labelsSelectorInputTip": "Enter drücken zum Erstellen!", - "addTaskText": "+ Aufgabe hinzufügen", + "addTaskText": "Aufgabe hinzufügen", "addSubTaskText": "+ Unteraufgabe hinzufügen", "addTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken", @@ -59,5 +59,11 @@ "convertToTask": "In Aufgabe umwandeln", "delete": "Löschen", "searchByNameInputPlaceholder": "Nach Namen suchen" - } + }, + "setDueDate": "Fälligkeitsdatum festlegen", + "setStartDate": "Startdatum festlegen", + "clearDueDate": "Fälligkeitsdatum löschen", + "clearStartDate": "Startdatum löschen", + "dueDatePlaceholder": "Fälligkeitsdatum", + "startDatePlaceholder": "Startdatum" } diff --git a/worklenz-frontend/public/locales/en/task-drawer/task-drawer-info-tab.json b/worklenz-frontend/public/locales/en/task-drawer/task-drawer-info-tab.json index 8592bd8b..f88ecde9 100644 --- a/worklenz-frontend/public/locales/en/task-drawer/task-drawer-info-tab.json +++ b/worklenz-frontend/public/locales/en/task-drawer/task-drawer-info-tab.json @@ -24,7 +24,7 @@ }, "subTasks": { "title": "Sub Tasks", - "add-sub-task": "+ Add Sub Task", + "add-sub-task": "Add Sub Task", "refresh-sub-tasks": "Refresh Sub Tasks" } } diff --git a/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json b/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json index 06575ee1..ab271810 100644 --- a/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json +++ b/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json @@ -43,7 +43,7 @@ }, "subTasks": { "title": "Sub Tasks", - "addSubTask": "+ Add Sub Task", + "addSubTask": "Add Sub Task", "addSubTaskInputPlaceholder": "Type your task and hit enter", "refreshSubTasks": "Refresh Sub Tasks", "edit": "Edit", diff --git a/worklenz-frontend/public/locales/en/task-list-table.json b/worklenz-frontend/public/locales/en/task-list-table.json index 45ba73f5..7881208f 100644 --- a/worklenz-frontend/public/locales/en/task-list-table.json +++ b/worklenz-frontend/public/locales/en/task-list-table.json @@ -36,8 +36,8 @@ "selectText": "Select", "labelsSelectorInputTip": "Hit enter to create!", - "addTaskText": "+ Add Task", - "addSubTaskText": "+ Add Sub Task", + "addTaskText": "Add Task", + "addSubTaskText": "Add Sub Task", "addTaskInputPlaceholder": "Type your task and hit enter", "openButton": "Open", @@ -59,5 +59,11 @@ "convertToTask": "Convert to Task", "delete": "Delete", "searchByNameInputPlaceholder": "Search by name" - } + }, + "setDueDate": "Set due date", + "setStartDate": "Set start date", + "clearDueDate": "Clear due date", + "clearStartDate": "Clear start date", + "dueDatePlaceholder": "Due Date", + "startDatePlaceholder": "Start Date" } diff --git a/worklenz-frontend/public/locales/es/task-list-table.json b/worklenz-frontend/public/locales/es/task-list-table.json index f6ae2339..21f7e6b2 100644 --- a/worklenz-frontend/public/locales/es/task-list-table.json +++ b/worklenz-frontend/public/locales/es/task-list-table.json @@ -36,8 +36,8 @@ "selectText": "Seleccionar", "labelsSelectorInputTip": "¡Presiona enter para crear!", - "addTaskText": "+ Agregar tarea", - "addSubTaskText": "+ Agregar subtarea", + "addTaskText": "Agregar tarea", + "addSubTaskText": "Agregar subtarea", "addTaskInputPlaceholder": "Escribe tu tarea y presiona enter", "openButton": "Abrir", @@ -59,5 +59,11 @@ "convertToTask": "Convertir en tarea", "delete": "Eliminar", "searchByNameInputPlaceholder": "Buscar por nombre" - } + }, + "setDueDate": "Establecer fecha de vencimiento", + "setStartDate": "Establecer fecha de inicio", + "clearDueDate": "Limpiar fecha de vencimiento", + "clearStartDate": "Limpiar fecha de inicio", + "dueDatePlaceholder": "Fecha de vencimiento", + "startDatePlaceholder": "Fecha de inicio" } diff --git a/worklenz-frontend/public/locales/pt/task-list-table.json b/worklenz-frontend/public/locales/pt/task-list-table.json index 01972b99..72c7a5db 100644 --- a/worklenz-frontend/public/locales/pt/task-list-table.json +++ b/worklenz-frontend/public/locales/pt/task-list-table.json @@ -36,7 +36,7 @@ "selectText": "Selecionar", "labelsSelectorInputTip": "Pressione enter para criar!", - "addTaskText": "+ Adicionar Tarefa", + "addTaskText": "Adicionar Tarefa", "addSubTaskText": "+ Adicionar Subtarefa", "addTaskInputPlaceholder": "Digite sua tarefa e pressione enter", @@ -59,5 +59,11 @@ "convertToTask": "Converter em Tarefa", "delete": "Excluir", "searchByNameInputPlaceholder": "Buscar por nome" - } + }, + "setDueDate": "Definir data de vencimento", + "setStartDate": "Definir data de início", + "clearDueDate": "Limpar data de vencimento", + "clearStartDate": "Limpar data de início", + "dueDatePlaceholder": "Data de vencimento", + "startDatePlaceholder": "Data de início" } diff --git a/worklenz-frontend/src/components/AssigneeSelector.tsx b/worklenz-frontend/src/components/AssigneeSelector.tsx index d92a4ca0..1ca0cdf8 100644 --- a/worklenz-frontend/src/components/AssigneeSelector.tsx +++ b/worklenz-frontend/src/components/AssigneeSelector.tsx @@ -61,9 +61,16 @@ const AssigneeSelector: React.FC = ({ const updateDropdownPosition = useCallback(() => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const dropdownHeight = 280; // More accurate height: header(40) + max-height(192) + footer(40) + padding + + // Check if dropdown would go below viewport + const spaceBelow = viewportHeight - rect.bottom; + const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight; + setDropdownPosition({ - top: rect.bottom + window.scrollY + 2, - left: rect.left + window.scrollX, + top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4, + left: rect.left, }); } }, []); @@ -81,23 +88,11 @@ const AssigneeSelector: React.FC = ({ } }; - const handleScroll = () => { + const handleScroll = (event: Event) => { if (isOpen) { - // Check if the button is still visible in the viewport - if (buttonRef.current) { - const rect = buttonRef.current.getBoundingClientRect(); - const isVisible = - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth; - - if (isVisible) { - updateDropdownPosition(); - } else { - // Hide dropdown if button is not visible - setIsOpen(false); - } + // Only close dropdown if scrolling happens outside the dropdown + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); } } }; diff --git a/worklenz-frontend/src/components/CustomColordLabel.tsx b/worklenz-frontend/src/components/CustomColordLabel.tsx index d6fba40b..aa7afba5 100644 --- a/worklenz-frontend/src/components/CustomColordLabel.tsx +++ b/worklenz-frontend/src/components/CustomColordLabel.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { Tooltip } from 'antd'; import { Label } from '@/types/task-management.types'; +import { ITaskLabel } from '@/types/tasks/taskLabel.types'; interface CustomColordLabelProps { - label: Label; + label: Label | ITaskLabel; isDarkMode?: boolean; } @@ -11,11 +12,37 @@ const CustomColordLabel: React.FC = ({ label, isDarkMode const truncatedName = label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name; + // Ensure we have a valid color, fallback to a default if not + const backgroundColor = label.color || '#6b7280'; // Default to gray-500 if no color + + // Function to determine if we should use white or black text based on background color + const getTextColor = (bgColor: string): string => { + // Remove # if present + const color = bgColor.replace('#', ''); + + // Convert to RGB + const r = parseInt(color.substr(0, 2), 16); + const g = parseInt(color.substr(2, 2), 16); + const b = parseInt(color.substr(4, 2), 16); + + // Calculate luminance + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + + // Return white for dark backgrounds, black for light backgrounds + return luminance > 0.5 ? '#000000' : '#ffffff'; + }; + + const textColor = getTextColor(backgroundColor); + return ( {truncatedName} diff --git a/worklenz-frontend/src/components/CustomNumberLabel.tsx b/worklenz-frontend/src/components/CustomNumberLabel.tsx index f39edc80..16521087 100644 --- a/worklenz-frontend/src/components/CustomNumberLabel.tsx +++ b/worklenz-frontend/src/components/CustomNumberLabel.tsx @@ -1,25 +1,31 @@ import React from 'react'; import { Tooltip } from 'antd'; +import { NumbersColorMap } from '@/shared/constants'; interface CustomNumberLabelProps { labelList: string[]; namesString: string; isDarkMode?: boolean; + color?: string; // Add color prop for label color } const CustomNumberLabel: React.FC = ({ labelList, namesString, isDarkMode = false, + color, }) => { + // Use provided color, or fall back to NumbersColorMap based on first digit + const backgroundColor = color || (() => { + const firstDigit = namesString.match(/\d/)?.[0] || '0'; + return NumbersColorMap[firstDigit] || NumbersColorMap['0']; + })(); + return ( {namesString} diff --git a/worklenz-frontend/src/components/LabelsSelector.tsx b/worklenz-frontend/src/components/LabelsSelector.tsx index b6fe3a5d..960970ae 100644 --- a/worklenz-frontend/src/components/LabelsSelector.tsx +++ b/worklenz-frontend/src/components/LabelsSelector.tsx @@ -39,10 +39,24 @@ const LabelsSelector: React.FC = ({ task, isDarkMode = fals const updateDropdownPosition = useCallback(() => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); - setDropdownPosition({ - top: rect.bottom + window.scrollY + 2, - left: rect.left + window.scrollX, - }); + const dropdownHeight = 300; // Approximate height of dropdown (max-height + padding) + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + + // Position dropdown above button if there's not enough space below + const shouldPositionAbove = spaceBelow < dropdownHeight && spaceAbove > dropdownHeight; + + if (shouldPositionAbove) { + setDropdownPosition({ + top: rect.top + window.scrollY - dropdownHeight - 2, + left: rect.left + window.scrollX, + }); + } else { + setDropdownPosition({ + top: rect.bottom + window.scrollY + 2, + left: rect.left + window.scrollX, + }); + } } }, []); @@ -59,23 +73,11 @@ const LabelsSelector: React.FC = ({ task, isDarkMode = fals } }; - const handleScroll = () => { + const handleScroll = (event: Event) => { if (isOpen) { - // Check if the button is still visible in the viewport - if (buttonRef.current) { - const rect = buttonRef.current.getBoundingClientRect(); - const isVisible = - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth; - - if (isVisible) { - updateDropdownPosition(); - } else { - // Hide dropdown if button is not visible - setIsOpen(false); - } + // Only close dropdown if scrolling happens outside the dropdown + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); } } }; diff --git a/worklenz-frontend/src/components/task-list-v2/SubtaskLoadingSkeleton.tsx b/worklenz-frontend/src/components/task-list-v2/SubtaskLoadingSkeleton.tsx new file mode 100644 index 00000000..66df7e5e --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/SubtaskLoadingSkeleton.tsx @@ -0,0 +1,139 @@ +import React from 'react'; + +interface SubtaskLoadingSkeletonProps { + visibleColumns: Array<{ + id: string; + width: string; + isSticky?: boolean; + }>; +} + +const SubtaskLoadingSkeleton: React.FC = ({ visibleColumns }) => { + const renderColumn = (columnId: string, width: string) => { + const baseStyle = { width }; + + switch (columnId) { + case 'dragHandle': + return
; + case 'checkbox': + return
; + case 'taskKey': + return ( +
+
+
+ ); + case 'title': + return ( +
+ {/* Subtask indentation */} +
+
+
+
+ ); + case 'status': + return ( +
+
+
+ ); + case 'assignees': + return ( +
+
+
+
+ ); + case 'priority': + return ( +
+
+
+ ); + case 'dueDate': + return ( +
+
+
+ ); + case 'progress': + return ( +
+
+
+ ); + case 'labels': + return ( +
+
+
+
+ ); + case 'phase': + return ( +
+
+
+ ); + case 'timeTracking': + return ( +
+
+
+ ); + case 'estimation': + return ( +
+
+
+ ); + case 'startDate': + return ( +
+
+
+ ); + case 'completedDate': + return ( +
+
+
+ ); + case 'createdDate': + return ( +
+
+
+ ); + case 'lastUpdated': + return ( +
+
+
+ ); + case 'reporter': + return ( +
+
+
+ ); + default: + return
; + } + }; + + return ( +
+
+ {visibleColumns.map((column) => ( +
+ {renderColumn(column.id, column.width)} +
+ ))} +
+
+ ); +}; + +export default SubtaskLoadingSkeleton; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx index 2d36857d..c9550039 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx @@ -47,7 +47,7 @@ import { selectRange, clearSelection, } from '@/features/task-management/selection.slice'; -import TaskRow from './TaskRow'; +import TaskRowWithSubtasks from './TaskRowWithSubtasks'; import TaskGroupHeader from './TaskGroupHeader'; import { Task, TaskGroup } from '@/types/task-management.types'; import { RootState } from '@/app/store'; @@ -64,13 +64,13 @@ const BASE_COLUMNS = [ { id: 'dragHandle', label: '', width: '32px', isSticky: true, key: 'dragHandle' }, { id: 'checkbox', label: '', width: '40px', isSticky: true, key: 'checkbox' }, { id: 'taskKey', label: 'Key', width: '100px', key: COLUMN_KEYS.KEY }, - { id: 'title', label: 'Title', width: '300px', isSticky: true, key: COLUMN_KEYS.NAME }, + { id: 'title', label: 'Title', width: '470px', isSticky: true, key: COLUMN_KEYS.NAME }, { id: 'status', label: 'Status', width: '120px', key: COLUMN_KEYS.STATUS }, { id: 'assignees', label: 'Assignees', width: '150px', key: COLUMN_KEYS.ASSIGNEES }, { id: 'priority', label: 'Priority', width: '120px', key: COLUMN_KEYS.PRIORITY }, { id: 'dueDate', label: 'Due Date', width: '120px', key: COLUMN_KEYS.DUE_DATE }, { id: 'progress', label: 'Progress', width: '120px', key: COLUMN_KEYS.PROGRESS }, - { id: 'labels', label: 'Labels', width: '150px', key: COLUMN_KEYS.LABELS }, + { id: 'labels', label: 'Labels', width: 'auto', key: COLUMN_KEYS.LABELS }, { id: 'phase', label: 'Phase', width: '120px', key: COLUMN_KEYS.PHASE }, { id: 'timeTracking', label: 'Time Tracking', width: '120px', key: COLUMN_KEYS.TIME_TRACKING }, { id: 'estimation', label: 'Estimation', width: '120px', key: COLUMN_KEYS.ESTIMATION }, @@ -91,17 +91,13 @@ type ColumnStyle = { flexShrink?: number; }; -interface TaskListV2Props { - projectId: string; -} - -const TaskListV2: React.FC = ({ projectId }) => { +const TaskListV2: React.FC = () => { const dispatch = useAppDispatch(); const { projectId: urlProjectId } = useParams(); - + // Drag and drop state const [activeId, setActiveId] = useState(null); - + // Configure sensors for drag and drop const sensors = useSensors( useSensor(PointerSensor, { @@ -119,7 +115,7 @@ const TaskListV2: React.FC = ({ projectId }) => { }, }) ); - + // Using Redux state for collapsedGroups instead of local state const collapsedGroups = useAppSelector(selectCollapsedGroups); @@ -159,176 +155,190 @@ const TaskListV2: React.FC = ({ projectId }) => { }, [dispatch, urlProjectId]); // Handlers - const handleTaskSelect = useCallback((taskId: string, event: React.MouseEvent) => { - if (event.ctrlKey || event.metaKey) { - dispatch(toggleTaskSelection(taskId)); - } else if (event.shiftKey && lastSelectedTaskId) { - const taskIds = allTasks.map(t => t.id); // Use allTasks here - const startIdx = taskIds.indexOf(lastSelectedTaskId); - const endIdx = taskIds.indexOf(taskId); - const rangeIds = taskIds.slice( - Math.min(startIdx, endIdx), - Math.max(startIdx, endIdx) + 1 - ); - dispatch(selectRange(rangeIds)); - } else { - dispatch(clearSelection()); - dispatch(selectTask(taskId)); - } - }, [dispatch, lastSelectedTaskId, allTasks]); + const handleTaskSelect = useCallback( + (taskId: string, event: React.MouseEvent) => { + if (event.ctrlKey || event.metaKey) { + dispatch(toggleTaskSelection(taskId)); + } else if (event.shiftKey && lastSelectedTaskId) { + const taskIds = allTasks.map(t => t.id); // Use allTasks here + const startIdx = taskIds.indexOf(lastSelectedTaskId); + const endIdx = taskIds.indexOf(taskId); + const rangeIds = taskIds.slice(Math.min(startIdx, endIdx), Math.max(startIdx, endIdx) + 1); + dispatch(selectRange(rangeIds)); + } else { + dispatch(clearSelection()); + dispatch(selectTask(taskId)); + } + }, + [dispatch, lastSelectedTaskId, allTasks] + ); - const handleGroupCollapse = useCallback((groupId: string) => { - dispatch(toggleGroupCollapsed(groupId)); // Dispatch Redux action to toggle collapsed state - }, [dispatch]); + const handleGroupCollapse = useCallback( + (groupId: string) => { + dispatch(toggleGroupCollapsed(groupId)); // Dispatch Redux action to toggle collapsed state + }, + [dispatch] + ); // Drag and drop handlers const handleDragStart = useCallback((event: DragStartEvent) => { setActiveId(event.active.id as string); }, []); - const handleDragOver = useCallback((event: DragOverEvent) => { - const { active, over } = event; - - if (!over) return; + const handleDragOver = useCallback( + (event: DragOverEvent) => { + const { active, over } = event; - const activeId = active.id; - const overId = over.id; + if (!over) return; - // Find the active task and the item being dragged over - const activeTask = allTasks.find(task => task.id === activeId); - if (!activeTask) return; + const activeId = active.id; + const overId = over.id; - // Check if we're dragging over a task or a group - const overTask = allTasks.find(task => task.id === overId); - const overGroup = groups.find(group => group.id === overId); + // Find the active task and the item being dragged over + const activeTask = allTasks.find(task => task.id === activeId); + if (!activeTask) return; - // Find the groups - const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id)); - let targetGroup = overGroup; + // Check if we're dragging over a task or a group + const overTask = allTasks.find(task => task.id === overId); + const overGroup = groups.find(group => group.id === overId); - if (overTask) { - targetGroup = groups.find(group => group.taskIds.includes(overTask.id)); - } + // Find the groups + const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id)); + let targetGroup = overGroup; - if (!activeGroup || !targetGroup) return; - - // If dragging to a different group, we need to handle cross-group movement - if (activeGroup.id !== targetGroup.id) { - console.log('Cross-group drag detected:', { - activeTask: activeTask.id, - fromGroup: activeGroup.id, - toGroup: targetGroup.id, - }); - } - }, [allTasks, groups]); - - const handleDragEnd = useCallback((event: DragEndEvent) => { - const { active, over } = event; - setActiveId(null); - - if (!over || active.id === over.id) { - return; - } - - const activeId = active.id; - const overId = over.id; - - // Find the active task - const activeTask = allTasks.find(task => task.id === activeId); - if (!activeTask) { - console.error('Active task not found:', activeId); - return; - } - - // Find the groups - const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id)); - if (!activeGroup) { - console.error('Could not find active group for task:', activeId); - return; - } - - // Check if we're dropping on a task or a group - const overTask = allTasks.find(task => task.id === overId); - const overGroup = groups.find(group => group.id === overId); - - let targetGroup = overGroup; - let insertIndex = 0; - - if (overTask) { - // Dropping on a task - targetGroup = groups.find(group => group.taskIds.includes(overTask.id)); - if (targetGroup) { - insertIndex = targetGroup.taskIds.indexOf(overTask.id); + if (overTask) { + targetGroup = groups.find(group => group.taskIds.includes(overTask.id)); } - } else if (overGroup) { - // Dropping on a group (at the end) - targetGroup = overGroup; - insertIndex = targetGroup.taskIds.length; - } - if (!targetGroup) { - console.error('Could not find target group'); - return; - } + if (!activeGroup || !targetGroup) return; - const isCrossGroup = activeGroup.id !== targetGroup.id; - const activeIndex = activeGroup.taskIds.indexOf(activeTask.id); - - console.log('Drag operation:', { - activeId, - overId, - activeTask: activeTask.name || activeTask.title, - activeGroup: activeGroup.id, - targetGroup: targetGroup.id, - activeIndex, - insertIndex, - isCrossGroup, - }); - - if (isCrossGroup) { - // Moving task between groups - console.log('Moving task between groups:', { - task: activeTask.name || activeTask.title, - from: activeGroup.title, - to: targetGroup.title, - newPosition: insertIndex, - }); - - // Move task to the target group - dispatch(moveTaskBetweenGroups({ - taskId: activeId as string, - sourceGroupId: activeGroup.id, - targetGroupId: targetGroup.id, - })); - - // Reorder task within target group at drop position - dispatch(reorderTasksInGroup({ - sourceTaskId: activeId as string, - destinationTaskId: over.id as string, - sourceGroupId: activeGroup.id, - destinationGroupId: targetGroup.id, - })); - } else { - // Reordering within the same group - console.log('Reordering task within same group:', { - task: activeTask.name || activeTask.title, - group: activeGroup.title, - from: activeIndex, - to: insertIndex, - }); - - if (activeIndex !== insertIndex) { - // Reorder task within same group at drop position - dispatch(reorderTasksInGroup({ - sourceTaskId: activeId as string, - destinationTaskId: over.id as string, - sourceGroupId: activeGroup.id, - destinationGroupId: activeGroup.id, - })); + // If dragging to a different group, we need to handle cross-group movement + if (activeGroup.id !== targetGroup.id) { + console.log('Cross-group drag detected:', { + activeTask: activeTask.id, + fromGroup: activeGroup.id, + toGroup: targetGroup.id, + }); } - } + }, + [allTasks, groups] + ); - }, [allTasks, groups]); + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + setActiveId(null); + + if (!over || active.id === over.id) { + return; + } + + const activeId = active.id; + const overId = over.id; + + // Find the active task + const activeTask = allTasks.find(task => task.id === activeId); + if (!activeTask) { + console.error('Active task not found:', activeId); + return; + } + + // Find the groups + const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id)); + if (!activeGroup) { + console.error('Could not find active group for task:', activeId); + return; + } + + // Check if we're dropping on a task or a group + const overTask = allTasks.find(task => task.id === overId); + const overGroup = groups.find(group => group.id === overId); + + let targetGroup = overGroup; + let insertIndex = 0; + + if (overTask) { + // Dropping on a task + targetGroup = groups.find(group => group.taskIds.includes(overTask.id)); + if (targetGroup) { + insertIndex = targetGroup.taskIds.indexOf(overTask.id); + } + } else if (overGroup) { + // Dropping on a group (at the end) + targetGroup = overGroup; + insertIndex = targetGroup.taskIds.length; + } + + if (!targetGroup) { + console.error('Could not find target group'); + return; + } + + const isCrossGroup = activeGroup.id !== targetGroup.id; + const activeIndex = activeGroup.taskIds.indexOf(activeTask.id); + + console.log('Drag operation:', { + activeId, + overId, + activeTask: activeTask.name || activeTask.title, + activeGroup: activeGroup.id, + targetGroup: targetGroup.id, + activeIndex, + insertIndex, + isCrossGroup, + }); + + if (isCrossGroup) { + // Moving task between groups + console.log('Moving task between groups:', { + task: activeTask.name || activeTask.title, + from: activeGroup.title, + to: targetGroup.title, + newPosition: insertIndex, + }); + + // Move task to the target group + dispatch( + moveTaskBetweenGroups({ + taskId: activeId as string, + sourceGroupId: activeGroup.id, + targetGroupId: targetGroup.id, + }) + ); + + // Reorder task within target group at drop position + dispatch( + reorderTasksInGroup({ + sourceTaskId: activeId as string, + destinationTaskId: over.id as string, + sourceGroupId: activeGroup.id, + destinationGroupId: targetGroup.id, + }) + ); + } else { + // Reordering within the same group + console.log('Reordering task within same group:', { + task: activeTask.name || activeTask.title, + group: activeGroup.title, + from: activeIndex, + to: insertIndex, + }); + + if (activeIndex !== insertIndex) { + // Reorder task within same group at drop position + dispatch( + reorderTasksInGroup({ + sourceTaskId: activeId as string, + destinationTaskId: over.id as string, + sourceGroupId: activeGroup.id, + destinationGroupId: activeGroup.id, + }) + ); + } + } + }, + [allTasks, groups] + ); // Bulk action handlers const handleClearSelection = useCallback(() => { @@ -395,14 +405,14 @@ const TaskListV2: React.FC = ({ projectId }) => { let currentTaskIndex = 0; return groups.map(group => { const isCurrentGroupCollapsed = collapsedGroups.has(group.id); - + // Order tasks according to group.taskIds array to maintain proper order - const visibleTasksInGroup = isCurrentGroupCollapsed - ? [] + const visibleTasksInGroup = isCurrentGroupCollapsed + ? [] : group.taskIds .map(taskId => allTasks.find(task => task.id === taskId)) .filter((task): task is Task => task !== undefined); // Type guard to filter out undefined tasks - + const tasksForVirtuoso = visibleTasksInGroup.map(task => ({ ...task, originalIndex: allTasks.indexOf(task), @@ -428,71 +438,87 @@ const TaskListV2: React.FC = ({ projectId }) => { }, [virtuosoGroups]); // Memoize column headers to prevent unnecessary re-renders - const columnHeaders = useMemo(() => ( -
- {visibleColumns.map((column) => { - const columnStyle: ColumnStyle = { - width: column.width, - flexShrink: 0, // Prevent columns from shrinking - }; + const columnHeaders = useMemo( + () => ( +
+ {visibleColumns.map(column => { + const columnStyle: ColumnStyle = { + width: column.width, + flexShrink: 0, // Prevent columns from shrinking + // Add specific styling for labels column with auto width + ...(column.id === 'labels' && column.width === 'auto' + ? { + minWidth: '200px', // Ensure minimum width for labels + flexGrow: 1, // Allow it to grow + } + : {}), + }; - return ( -
- {column.id === 'dragHandle' ? ( - - ) : column.id === 'checkbox' ? ( - // Empty for checkbox column header - ) : ( - column.label - )} -
- ); - })} -
- ), [visibleColumns]); + return ( +
+ {column.id === 'dragHandle' ? ( + + ) : column.id === 'checkbox' ? ( + // Empty for checkbox column header + ) : ( + column.label + )} +
+ ); + })} +
+ ), + [visibleColumns] + ); // Render functions - const renderGroup = useCallback((groupIndex: number) => { - const group = virtuosoGroups[groupIndex]; - const isGroupEmpty = group.count === 0; - - return ( -
0 ? 'mt-2' : ''}> - handleGroupCollapse(group.id)} - /> - {/* Empty group drop zone */} - {isGroupEmpty && !collapsedGroups.has(group.id) && ( -
-
Drop tasks here
-
- )} -
- ); - }, [virtuosoGroups, collapsedGroups, handleGroupCollapse]); + const renderGroup = useCallback( + (groupIndex: number) => { + const group = virtuosoGroups[groupIndex]; + const isGroupEmpty = group.count === 0; - const renderTask = useCallback((taskIndex: number) => { - const task = virtuosoItems[taskIndex]; // Get task from the flattened virtuosoItems - if (!task) return null; // Should not happen if logic is correct - return ( - - ); - }, [virtuosoItems, visibleColumns]); + return ( +
0 ? 'mt-2' : ''}> + handleGroupCollapse(group.id)} + /> + {/* Empty group drop zone */} + {isGroupEmpty && !collapsedGroups.has(group.id) && ( +
+
Drop tasks here
+
+ )} +
+ ); + }, + [virtuosoGroups, collapsedGroups, handleGroupCollapse] + ); + + const renderTask = useCallback( + (taskIndex: number) => { + const task = virtuosoItems[taskIndex]; // Get task from the flattened virtuosoItems + if (!task || !urlProjectId) return null; // Should not happen if logic is correct + return ( + + ); + }, + [virtuosoItems, visibleColumns] + ); if (loading) return
Loading...
; if (error) return
Error: {error}
; @@ -522,7 +548,10 @@ const TaskListV2: React.FC = ({ projectId }) => { {/* Task List - Scrollable content */}
task.id).filter((id): id is string => id !== undefined)} + items={virtuosoItems + .filter(task => !task.parent_task_id) + .map(task => task.id) + .filter((id): id is string => id !== undefined)} strategy={verticalListSortingStrategy} > = ({ projectId }) => { groupContent={renderGroup} itemContent={renderTask} components={{ - List: React.forwardRef(({ style, children }, ref) => ( -
+ List: React.forwardRef< + HTMLDivElement, + { style?: React.CSSProperties; children?: React.ReactNode } + >(({ style, children }, ref) => ( +
{children}
)), @@ -556,9 +584,9 @@ const TaskListV2: React.FC = ({ projectId }) => {
- {allTasks.find(task => task.id === activeId)?.name || - allTasks.find(task => task.id === activeId)?.title || - 'Task'} + {allTasks.find(task => task.id === activeId)?.name || + allTasks.find(task => task.id === activeId)?.title || + 'Task'}
{allTasks.find(task => task.id === activeId)?.task_key} @@ -571,11 +599,11 @@ const TaskListV2: React.FC = ({ projectId }) => { {/* Bulk Action Bar */} - {selectedTaskIds.length > 0 && ( + {selectedTaskIds.length > 0 && urlProjectId && ( = ({ projectId }) => { ); }; -export default TaskListV2; \ No newline at end of file +export default TaskListV2; diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx index ef5e65e7..4df9d0a3 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx @@ -1,15 +1,13 @@ -import React, { memo, useMemo, useCallback } from 'react'; +import React, { memo, useMemo, useCallback, useState } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { CheckCircleOutlined, HolderOutlined } from '@ant-design/icons'; -import { Checkbox } from 'antd'; +import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons'; +import { Checkbox, DatePicker } from 'antd'; +import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports'; import { Task } from '@/types/task-management.types'; import { InlineMember } from '@/types/teamMembers/inlineMember.types'; -import Avatar from '@/components/Avatar'; import AssigneeSelector from '@/components/AssigneeSelector'; import { format } from 'date-fns'; -import { Bars3Icon } from '@heroicons/react/24/outline'; -import { ClockIcon } from '@heroicons/react/24/outline'; import AvatarGroup from '../AvatarGroup'; import { DEFAULT_TASK_NAME } from '@/shared/constants'; import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress'; @@ -17,8 +15,16 @@ import TaskStatusDropdown from '@/components/task-management/task-status-dropdow import TaskPriorityDropdown from '@/components/task-management/task-priority-dropdown'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; -import { selectTaskById } from '@/features/task-management/task-management.slice'; +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'; interface TaskRowProps { taskId: string; @@ -28,8 +34,45 @@ interface TaskRowProps { width: string; isSticky?: boolean; }>; + isSubtask?: boolean; } +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 @@ -42,30 +85,34 @@ const getTaskDisplayName = (task: Task): string => { // Memoized date formatter to avoid repeated date parsing const formatDate = (dateString: string): string => { try { - return format(new Date(dateString), 'MMM d'); + return format(new Date(dateString), 'MMM d, yyyy'); } catch { return ''; } }; -// Memoized date formatter to avoid repeated date parsing - -const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumns }) => { +const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumns, isSubtask = false }) => { 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 + // 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 @@ -101,35 +148,43 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn }), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]); // Memoize formatted dates - const formattedDueDate = useMemo(() => - task.dueDate ? formatDate(task.dueDate) : null, - [task.dueDate] - ); - - const formattedStartDate = useMemo(() => - task.startDate ? formatDate(task.startDate) : null, - [task.startDate] - ); - - const formattedCompletedDate = useMemo(() => - task.completedAt ? formatDate(task.completedAt) : null, - [task.completedAt] - ); - - const formattedCreatedDate = useMemo(() => - task.created_at ? formatDate(task.created_at) : null, - [task.created_at] - ); - - const formattedUpdatedDate = useMemo(() => - task.updatedAt ? formatDate(task.updatedAt) : null, - [task.updatedAt] + 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] ); - // Debugging: Log assignee_names whenever the task prop changes - React.useEffect(() => { - console.log(`Task ${task.id} assignees:`, task.assignee_names); - }, [task.id, task.assignee_names]); + // 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]); // Handle checkbox change const handleCheckboxChange = useCallback((e: any) => { @@ -137,30 +192,61 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn dispatch(toggleTaskSelection(taskId)); }, [dispatch, taskId]); - // Memoize status style - const statusStyle = useMemo(() => ({ - backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)', - color: task.statusColor || 'rgb(31, 41, 55)', - }), [task.statusColor]); - - // Memoize priority style - const priorityStyle = useMemo(() => ({ - backgroundColor: task.priorityColor ? `${task.priorityColor}20` : 'rgb(229, 231, 235)', - color: task.priorityColor || 'rgb(31, 41, 55)', - }), [task.priorityColor]); - - // Memoize labels display - const labelsDisplay = useMemo(() => { - if (!task.labels || task.labels.length === 0) return null; + // Handle task expansion toggle + const handleToggleExpansion = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); - const visibleLabels = task.labels.slice(0, 2); - const remainingCount = task.labels.length - 2; + // 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 })); + } - return { - visibleLabels, - remainingCount: remainingCount > 0 ? remainingCount : null, - }; - }, [task.labels]); + // 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 }; @@ -169,12 +255,11 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'dragHandle': return (
- + {!isSubtask && }
); @@ -200,10 +285,63 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'title': return ( -
- - {taskDisplayName} - +
+
+ {/* Indentation for subtasks - increased padding */} + {isSubtask &&
} + + {/* Expand/Collapse button - only show for parent tasks */} + {!isSubtask && ( + + )} + + {/* Additional indentation for subtasks after the expand button space */} + {isSubtask &&
} + +
+ + {taskDisplayName} + + + {/* Subtask count indicator */} + {!isSubtask && task.sub_tasks_count && task.sub_tasks_count > 0 && ( +
+ + {task.sub_tasks_count} + + +
+ )} +
+
+ +
); @@ -248,11 +386,58 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'dueDate': return ( -
- {formattedDueDate && ( - - {formattedDueDate} - +
+ {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')} + + )} +
)}
); @@ -283,48 +468,27 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'labels': return ( -
- {labelsDisplay?.visibleLabels.map((label, index) => ( - - {label.name} - - ))} - {labelsDisplay?.remainingCount && ( - - +{labelsDisplay.remainingCount} - - )} +
+ +
); case 'phase': return (
- - {task.phase} - +
); case 'timeTracking': return ( -
- - - {task.timeTracking?.logged || 0}h - - {task.timeTracking?.estimated && ( - - /{task.timeTracking.estimated}h - - )} +
+
); @@ -341,11 +505,58 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'startDate': return ( -
- {formattedStartDate && ( - - {formattedStartDate} - +
+ {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')} + + )} +
)}
); @@ -353,9 +564,9 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'completedDate': return (
- {formattedCompletedDate && ( + {formattedDates.completed && ( - {formattedCompletedDate} + {formattedDates.completed} )}
@@ -364,9 +575,9 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'createdDate': return (
- {formattedCreatedDate && ( + {formattedDates.created && ( - {formattedCreatedDate} + {formattedDates.created} )}
@@ -375,9 +586,9 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn case 'lastUpdated': return (
- {formattedUpdatedDate && ( + {formattedDates.updated && ( - {formattedUpdatedDate} + {formattedDates.updated} )}
@@ -396,30 +607,31 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn return null; } }, [ + // Essential props and state attributes, listeners, - task.task_key, - task.status, - task.priority, - task.phase, - task.reporter, - task.assignee_names, - task.timeTracking, - task.progress, - task.sub_tasks, - taskDisplayName, - statusStyle, - priorityStyle, - formattedDueDate, - formattedStartDate, - formattedCompletedDate, - formattedCreatedDate, - formattedUpdatedDate, - labelsDisplay, - isDarkMode, - convertedTask, isSelected, handleCheckboxChange, + activeDatePicker, + isDarkMode, + projectId, + + // Task data + task, + taskDisplayName, + convertedTask, + + // Memoized values + dateValues, + formattedDates, + labelsAdapter, + + // Handlers + handleDateChange, + datePickerHandlers, + + // Translation + t, ]); return ( diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRowWithSubtasks.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRowWithSubtasks.tsx new file mode 100644 index 00000000..b35cdb61 --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/TaskRowWithSubtasks.tsx @@ -0,0 +1,208 @@ +import React, { memo, useState, useCallback } from 'react'; +import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { selectTaskById, createSubtask, selectSubtaskLoading } from '@/features/task-management/task-management.slice'; +import TaskRow from './TaskRow'; +import SubtaskLoadingSkeleton from './SubtaskLoadingSkeleton'; +import { Task } from '@/types/task-management.types'; +import { Input, Button } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { useSocket } from '@/socket/socketContext'; +import { SocketEvents } from '@/shared/socket-events'; +import { useTranslation } from 'react-i18next'; + +interface TaskRowWithSubtasksProps { + taskId: string; + projectId: string; + visibleColumns: Array<{ + id: string; + width: string; + isSticky?: boolean; + }>; +} + +interface AddSubtaskRowProps { + parentTaskId: string; + projectId: string; + visibleColumns: Array<{ + id: string; + width: string; + isSticky?: boolean; + }>; + onSubtaskAdded: () => void; +} + +const AddSubtaskRow: React.FC = memo(({ + parentTaskId, + projectId, + visibleColumns, + onSubtaskAdded +}) => { + const [isAdding, setIsAdding] = useState(false); + const [subtaskName, setSubtaskName] = useState(''); + const { socket, connected } = useSocket(); + const { t } = useTranslation('task-list-table'); + const dispatch = useAppDispatch(); + + const handleAddSubtask = useCallback(() => { + if (!subtaskName.trim()) return; + + // Create optimistic subtask immediately for better UX + dispatch(createSubtask({ + parentTaskId, + name: subtaskName.trim(), + projectId + })); + + // Emit socket event for server-side creation + if (connected && socket) { + socket.emit( + SocketEvents.QUICK_TASK.toString(), + JSON.stringify({ + name: subtaskName.trim(), + project_id: projectId, + parent_task_id: parentTaskId, + }) + ); + } + + setSubtaskName(''); + setIsAdding(false); + onSubtaskAdded(); + }, [subtaskName, dispatch, parentTaskId, projectId, connected, socket, onSubtaskAdded]); + + const handleCancel = useCallback(() => { + setSubtaskName(''); + setIsAdding(false); + }, []); + + const renderColumn = useCallback((columnId: string, width: string) => { + const baseStyle = { width }; + + switch (columnId) { + case 'dragHandle': + return
; + case 'checkbox': + return
; + case 'taskKey': + return
; + case 'title': + return ( +
+
+ {/* Match subtask indentation pattern - same as TaskRow for subtasks */} +
+ + {!isAdding ? ( + + ) : ( + setSubtaskName(e.target.value)} + onPressEnter={handleAddSubtask} + onBlur={handleCancel} + placeholder="Type subtask name and press Enter to save" + className="w-full h-full border-none shadow-none bg-transparent" + style={{ + height: '100%', + minHeight: '42px', + padding: '0', + fontSize: '14px' + }} + autoFocus + /> + )} +
+
+ ); + default: + return
; + } + }, [isAdding, subtaskName, handleAddSubtask, handleCancel, t]); + + return ( +
+ {visibleColumns.map((column) => + renderColumn(column.id, column.width) + )} +
+ ); +}); + +AddSubtaskRow.displayName = 'AddSubtaskRow'; + +const TaskRowWithSubtasks: React.FC = memo(({ + taskId, + projectId, + visibleColumns +}) => { + const task = useAppSelector(state => selectTaskById(state, taskId)); + const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId)); + const dispatch = useAppDispatch(); + + const handleSubtaskAdded = useCallback(() => { + // Refresh subtasks after adding a new one + // The socket event will handle the real-time update + }, []); + + if (!task) { + return null; + } + + return ( + <> + {/* Main task row */} + + + {/* Subtasks and add subtask row when expanded */} + {task.show_sub_tasks && ( + <> + {/* Show loading skeleton while fetching subtasks */} + {isLoadingSubtasks && ( + <> + + + )} + + {/* Render existing subtasks when not loading */} + {!isLoadingSubtasks && task.sub_tasks?.map((subtask: Task) => ( +
+ +
+ ))} + + {/* Add subtask row - only show when not loading */} + {!isLoadingSubtasks && ( +
+ +
+ )} + + )} + + ); +}); + +TaskRowWithSubtasks.displayName = 'TaskRowWithSubtasks'; + +export default TaskRowWithSubtasks; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/TaskTimeTracking.tsx b/worklenz-frontend/src/components/task-list-v2/TaskTimeTracking.tsx new file mode 100644 index 00000000..3bb44f57 --- /dev/null +++ b/worklenz-frontend/src/components/task-list-v2/TaskTimeTracking.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import TaskTimer from '@/components/taskListCommon/task-timer/task-timer'; +import { useTaskTimer } from '@/hooks/useTaskTimer'; + +interface TaskTimeTrackingProps { + taskId: string; + isDarkMode: boolean; +} + +const TaskTimeTracking: React.FC = React.memo(({ taskId, isDarkMode }) => { + const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimer( + taskId, + null // The hook will get the timer start time from Redux + ); + + return ( + + ); +}); + +TaskTimeTracking.displayName = 'TaskTimeTracking'; + +export default TaskTimeTracking; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index e95e7189..884d364b 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -24,6 +24,7 @@ import { EyeOutlined, RetweetOutlined, DownOutlined, // Added DownOutlined for expand/collapse + CloseOutlined, // Added CloseOutlined for clear button } from '@/shared/antd-imports'; import { useTranslation } from 'react-i18next'; import { Task } from '@/types/task-management.types'; @@ -752,7 +753,7 @@ const TaskRow: React.FC = React.memo( className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }} > - +
); @@ -892,7 +893,7 @@ const TaskRow: React.FC = React.memo( className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }} > - +
); @@ -1316,13 +1317,35 @@ const TaskRow: React.FC = React.memo( className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }} > - handleDateChange(date, 'startDate')} - placeholder="Start Date" - /> +
+ handleDateChange(date, 'startDate')} + placeholder="Start Date" + allowClear={false} // We'll handle clear manually + suffixIcon={null} + /> + {/* Custom clear button - only show when there's a value */} + {dateValues.start && ( + + )} +
); @@ -1333,13 +1356,35 @@ const TaskRow: React.FC = React.memo( className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }} > - handleDateChange(date, 'dueDate')} - placeholder="Due Date" - /> +
+ handleDateChange(date, 'dueDate')} + placeholder="Due Date" + allowClear={false} // We'll handle clear manually + suffixIcon={null} + /> + {/* Custom clear button - only show when there's a value */} + {dateValues.due && ( + + )} +
); diff --git a/worklenz-frontend/src/components/taskListCommon/labelsSelector/CustomColordLabel.tsx b/worklenz-frontend/src/components/taskListCommon/labelsSelector/CustomColordLabel.old.tsx similarity index 100% rename from worklenz-frontend/src/components/taskListCommon/labelsSelector/CustomColordLabel.tsx rename to worklenz-frontend/src/components/taskListCommon/labelsSelector/CustomColordLabel.old.tsx diff --git a/worklenz-frontend/src/features/task-management/task-management.slice.ts b/worklenz-frontend/src/features/task-management/task-management.slice.ts index 52bf7f1a..fc7a7365 100644 --- a/worklenz-frontend/src/features/task-management/task-management.slice.ts +++ b/worklenz-frontend/src/features/task-management/task-management.slice.ts @@ -55,6 +55,7 @@ const initialState: TaskManagementState = { grouping: undefined, selectedPriorities: [], search: '', + loadingSubtasks: {}, }; // Async thunk to fetch tasks from API @@ -151,11 +152,12 @@ export const fetchTasks = createAsyncThunk( task.labels?.map((l: any) => ({ id: l.id || l.label_id, name: l.name, - color: l.color_code || '#1890ff', + color: l.color || '#1890ff', end: l.end, names: l.names, })) || [], - dueDate: task.end_date, + dueDate: task.dueDate, + startDate: task.startDate, timeTracking: { estimated: convertTimeValue(task.total_time), logged: convertTimeValue(task.time_spent), @@ -163,6 +165,8 @@ export const fetchTasks = createAsyncThunk( customFields: {}, createdAt: task.created_at || new Date().toISOString(), updatedAt: task.updated_at || new Date().toISOString(), + created_at: task.created_at || new Date().toISOString(), + updated_at: task.updated_at || new Date().toISOString(), order: typeof task.sort_order === 'number' ? task.sort_order : 0, // Ensure all Task properties are mapped, even if undefined in API response sub_tasks: task.sub_tasks || [], @@ -234,16 +238,13 @@ export const fetchTasksV3 = createAsyncThunk( const response = await tasksApiService.getTaskListV3(config); - // Log raw response for debugging - console.log('Raw API response:', response.body); - console.log('Sample task from backend:', response.body.allTasks?.[0]); - console.log('Task key from backend:', response.body.allTasks?.[0]?.task_key); + // Ensure tasks are properly normalized const tasks: Task[] = response.body.allTasks.map((task: any) => { const now = new Date().toISOString(); - return { + const transformedTask = { id: task.id, task_key: task.task_key || task.key || '', title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME, @@ -257,11 +258,12 @@ export const fetchTasksV3 = createAsyncThunk( labels: task.labels?.map((l: { id: string; label_id: string; name: string; color_code: string; end: boolean; names: string[] }) => ({ id: l.id || l.label_id, name: l.name, - color: l.color_code || '#1890ff', + color: l.color || '#1890ff', end: l.end, names: l.names, })) || [], - dueDate: task.end_date, + dueDate: task.dueDate, + startDate: task.startDate, timeTracking: { estimated: convertTimeValue(task.total_time), logged: convertTimeValue(task.time_spent), @@ -269,6 +271,8 @@ export const fetchTasksV3 = createAsyncThunk( customFields: {}, createdAt: task.created_at || now, updatedAt: task.updated_at || now, + created_at: task.created_at || now, + updated_at: task.updated_at || now, order: typeof task.sort_order === 'number' ? task.sort_order : 0, sub_tasks: task.sub_tasks || [], sub_tasks_count: task.sub_tasks_count || 0, @@ -283,6 +287,8 @@ export const fetchTasksV3 = createAsyncThunk( has_dependencies: task.has_dependencies || false, schedule_id: task.schedule_id || null, }; + + return transformedTask; }); return { @@ -698,6 +704,68 @@ const taskManagementSlice = createSlice({ parent.sub_tasks_count = (parent.sub_tasks_count || 0) + 1; } }, + createSubtask: ( + state, + action: PayloadAction<{ parentTaskId: string; name: string; projectId: string }> + ) => { + const { parentTaskId, name, projectId } = action.payload; + const parent = state.entities[parentTaskId]; + if (parent) { + // Create a temporary subtask - the real one will come from the socket + const tempId = `temp-${Date.now()}`; + const tempSubtask: Task = { + id: tempId, + task_key: '', + title: name, + name: name, + description: '', + status: 'todo', + priority: 'low', + phase: 'Development', + progress: 0, + assignees: [], + assignee_names: [], + labels: [], + dueDate: undefined, + due_date: undefined, + startDate: undefined, + timeTracking: { + estimated: 0, + logged: 0, + }, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + order: 0, + parent_task_id: parentTaskId, + is_sub_task: true, + sub_tasks_count: 0, + show_sub_tasks: false, + isTemporary: true, // Mark as temporary + }; + + // Add temporary subtask for immediate UI feedback + if (!parent.sub_tasks) { + parent.sub_tasks = []; + } + parent.sub_tasks.push(tempSubtask); + parent.sub_tasks_count = (parent.sub_tasks_count || 0) + 1; + state.entities[tempId] = tempSubtask; + state.ids.push(tempId); + } + }, + removeTemporarySubtask: ( + state, + action: PayloadAction<{ parentTaskId: string; tempId: string }> + ) => { + const { parentTaskId, tempId } = action.payload; + const parent = state.entities[parentTaskId]; + if (parent && parent.sub_tasks) { + parent.sub_tasks = parent.sub_tasks.filter(subtask => subtask.id !== tempId); + parent.sub_tasks_count = Math.max((parent.sub_tasks_count || 0) - 1, 0); + delete state.entities[tempId]; + state.ids = state.ids.filter(id => id !== tempId); + } + }, updateTaskAssignees: (state, action: PayloadAction<{ taskId: string; assigneeIds: string[]; @@ -714,6 +782,7 @@ const taskManagementSlice = createSlice({ }; } }, + }, extraReducers: builder => { builder @@ -737,20 +806,66 @@ const taskManagementSlice = createSlice({ state.groups = []; }) .addCase(fetchSubTasks.pending, (state, action) => { - // Don't set global loading state for subtasks + // Set loading state for specific task + const { taskId } = action.meta.arg; + state.loadingSubtasks[taskId] = true; state.error = null; }) .addCase(fetchSubTasks.fulfilled, (state, action) => { const { parentTaskId, subtasks } = action.payload; const parentTask = state.entities[parentTaskId]; - if (parentTask) { - parentTask.sub_tasks = subtasks; - parentTask.sub_tasks_count = subtasks.length; - parentTask.show_sub_tasks = true; + // Clear loading state + state.loadingSubtasks[parentTaskId] = false; + if (parentTask && subtasks) { + // Convert subtasks to the proper format + const convertedSubtasks = subtasks.map(subtask => ({ + id: subtask.id || '', + task_key: subtask.task_key || '', + title: subtask.name || subtask.title || '', + name: subtask.name || subtask.title || '', + description: subtask.description || '', + status: subtask.status || 'todo', + priority: subtask.priority || 'low', + phase: subtask.phase_name || subtask.phase || 'Development', + progress: subtask.complete_ratio || subtask.progress || 0, + assignees: subtask.assignees || [], + assignee_names: subtask.assignee_names || subtask.names || [], + labels: subtask.labels || [], + dueDate: subtask.end_date || subtask.dueDate, + due_date: subtask.end_date || subtask.due_date, + startDate: subtask.start_date || subtask.startDate, + timeTracking: subtask.timeTracking || { + estimated: 0, + logged: 0, + }, + createdAt: subtask.created_at || subtask.createdAt || new Date().toISOString(), + created_at: subtask.created_at || subtask.createdAt || new Date().toISOString(), + updatedAt: subtask.updated_at || subtask.updatedAt || new Date().toISOString(), + updated_at: subtask.updated_at || subtask.updatedAt || new Date().toISOString(), + order: subtask.sort_order || subtask.order || 0, + parent_task_id: parentTaskId, + is_sub_task: true, + sub_tasks_count: 0, + show_sub_tasks: false, + })); + + // Update parent task with subtasks + parentTask.sub_tasks = convertedSubtasks; + parentTask.sub_tasks_count = convertedSubtasks.length; + + // Add subtasks to entities so they can be accessed by ID + convertedSubtasks.forEach(subtask => { + state.entities[subtask.id] = subtask; + if (!state.ids.includes(subtask.id)) { + state.ids.push(subtask.id); + } + }); } }) .addCase(fetchSubTasks.rejected, (state, action) => { - // Set error but don't clear task data + // Clear loading state and set error + const { taskId } = action.meta.arg; + state.loadingSubtasks[taskId] = false; state.error = action.error.message || action.payload || 'Failed to fetch subtasks. Please try again.'; }) .addCase(fetchTasks.pending, (state) => { @@ -796,6 +911,8 @@ export const { toggleTaskExpansion, addSubtaskToParent, updateTaskAssignees, + createSubtask, + removeTemporarySubtask, } = taskManagementSlice.actions; // Export the selectors @@ -809,6 +926,7 @@ export const selectLoading = (state: RootState) => state.taskManagement.loading; export const selectError = (state: RootState) => state.taskManagement.error; export const selectSelectedPriorities = (state: RootState) => state.taskManagement.selectedPriorities; export const selectSearch = (state: RootState) => state.taskManagement.search; +export const selectSubtaskLoading = (state: RootState, taskId: string) => state.taskManagement.loadingSubtasks[taskId] || false; // Memoized selectors export const selectTasksByStatus = (state: RootState, status: string) => diff --git a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts index 52fe95ff..30ec508c 100644 --- a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts +++ b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts @@ -200,49 +200,57 @@ export const useTaskSocketHandlers = () => { // Update enhanced kanban slice dispatch(updateEnhancedKanbanTaskStatus(response)); - // For the task management slice, move task between groups without resetting + // For the task management slice, update the task entity and handle group movement const state = store.getState(); const groups = state.taskManagement.groups; const currentTask = state.taskManagement.entities[response.id]; + const currentGrouping = state.taskManagement.grouping; - if (groups && groups.length > 0 && currentTask && response.status_id) { - // Find current group containing the task - const currentGroup = groups.find(group => group.taskIds.includes(response.id)); - - // Find target group based on new status ID - // The status_id from response is the UUID of the new status - const targetGroup = groups.find(group => group.id === response.status_id); - - if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { - // Determine the new status value based on status category - let newStatusValue: 'todo' | 'doing' | 'done' = 'todo'; - if (response.statusCategory) { - if (response.statusCategory.is_done) { - newStatusValue = 'done'; - } else if (response.statusCategory.is_doing) { - newStatusValue = 'doing'; - } else { - newStatusValue = 'todo'; - } + if (currentTask) { + // Determine the new status value based on status category + let newStatusValue: 'todo' | 'doing' | 'done' = 'todo'; + if (response.statusCategory) { + if (response.statusCategory.is_done) { + newStatusValue = 'done'; + } else if (response.statusCategory.is_doing) { + newStatusValue = 'doing'; + } else { + newStatusValue = 'todo'; } + } - // Use the new action to move task between groups - dispatch( - moveTaskBetweenGroups({ - taskId: response.id, - fromGroupId: currentGroup.id, - toGroupId: targetGroup.id, - taskUpdate: { - status: newStatusValue, - progress: response.complete_ratio || currentTask.progress, - }, - }) - ); - } else if (!currentGroup || !targetGroup) { - // Remove unnecessary refetch that causes data thrashing - // if (projectId) { - // dispatch(fetchTasksV3(projectId)); - // } + // Update the task entity first + dispatch( + updateTask({ + ...currentTask, + status: newStatusValue, + progress: response.complete_ratio || currentTask.progress, + updatedAt: new Date().toISOString(), + }) + ); + + // Handle group movement ONLY if grouping by status + if (groups && groups.length > 0 && currentGrouping === 'status') { + // Find current group containing the task + const currentGroup = groups.find(group => group.taskIds.includes(response.id)); + + // Find target group based on new status value (not UUID) + const targetGroup = groups.find(group => group.groupValue === newStatusValue); + + if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { + // Use the action to move task between groups + dispatch( + moveTaskBetweenGroups({ + taskId: response.id, + sourceGroupId: currentGroup.id, + targetGroupId: targetGroup.id, + }) + ); + } else { + console.log('🔧 No group movement needed for status change'); + } + } else { + console.log('🔧 Not grouped by status, skipping group movement'); } } }, @@ -310,9 +318,10 @@ export const useTaskSocketHandlers = () => { // Update enhanced kanban slice dispatch(updateEnhancedKanbanTaskPriority(response)); - // For the task management slice, always update the task entity first + // For the task management slice, update the task entity and handle group movement const state = store.getState(); const currentTask = state.taskManagement.entities[response.id]; + const currentGrouping = state.taskManagement.grouping; if (currentTask) { // Get priority list to map priority_id to priority name @@ -327,20 +336,17 @@ export const useTaskSocketHandlers = () => { } } - // Update the task entity + // Update the task entity first dispatch( updateTask({ - id: response.id, - changes: { - priority: newPriorityValue, - updatedAt: new Date().toISOString(), - }, + ...currentTask, + priority: newPriorityValue, + updatedAt: new Date().toISOString(), }) ); // Handle group movement ONLY if grouping by priority const groups = state.taskManagement.groups; - const currentGrouping = state.taskManagement.grouping; if (groups && groups.length > 0 && currentGrouping === 'priority') { // Find current group containing the task @@ -348,18 +354,15 @@ export const useTaskSocketHandlers = () => { // Find target group based on new priority value const targetGroup = groups.find( - group => group.groupValue.toLowerCase() === newPriorityValue.toLowerCase() + group => group.groupValue?.toLowerCase() === newPriorityValue.toLowerCase() ); if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { dispatch( moveTaskBetweenGroups({ taskId: response.id, - fromGroupId: currentGroup.id, - toGroupId: targetGroup.id, - taskUpdate: { - priority: newPriorityValue, - }, + sourceGroupId: currentGroup.id, + targetGroupId: targetGroup.id, }) ); } else { @@ -387,6 +390,16 @@ export const useTaskSocketHandlers = () => { // Update enhanced kanban slice dispatch(updateEnhancedKanbanTaskEndDate({ task: taskWithProgress })); + + // Update task-management slice for task-list-v2 components + const currentTask = store.getState().taskManagement.entities[task.id]; + if (currentTask) { + dispatch(updateTask({ + ...currentTask, + dueDate: task.end_date, + updatedAt: new Date().toISOString(), + })); + } }, [dispatch] ); @@ -517,6 +530,16 @@ export const useTaskSocketHandlers = () => { dispatch(updateTaskStartDate({ task: taskWithProgress })); dispatch(setStartDate(taskWithProgress)); + + // Update task-management slice for task-list-v2 components + const currentTask = store.getState().taskManagement.entities[task.id]; + if (currentTask) { + dispatch(updateTask({ + ...currentTask, + startDate: task.start_date, + updatedAt: new Date().toISOString(), + })); + } }, [dispatch] ); @@ -599,7 +622,7 @@ export const useTaskSocketHandlers = () => { parent_task_id: data.parent_task_id, is_sub_task: true, }; - dispatch(addSubtaskToParent({ subtask, parentTaskId: data.parent_task_id })); + dispatch(addSubtaskToParent({ parentId: data.parent_task_id, subtask })); // Also update enhanced kanban slice for subtask creation dispatch( diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-labels-cell/task-list-labels-cell.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-labels-cell/task-list-labels-cell.tsx index 23ea8b50..1104e7b6 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-labels-cell/task-list-labels-cell.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-labels-cell/task-list-labels-cell.tsx @@ -1,8 +1,8 @@ import { Flex } from 'antd'; -import CustomColordLabel from '@/components/taskListCommon/labelsSelector/CustomColordLabel'; import CustomNumberLabel from '@/components/taskListCommon/labelsSelector/CustomNumberLabel'; import LabelsSelector from '@/components/taskListCommon/labelsSelector/LabelsSelector'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; +import { CustomColordLabel } from '@/components'; interface TaskListLabelsCellProps { task: IProjectTask; diff --git a/worklenz-frontend/src/styles/task-management.css b/worklenz-frontend/src/styles/task-management.css index a4737a71..1cfc2669 100644 --- a/worklenz-frontend/src/styles/task-management.css +++ b/worklenz-frontend/src/styles/task-management.css @@ -310,8 +310,6 @@ } .dark .ant-btn { - background-color: #262626; - border-color: #404040; color: rgba(255, 255, 255, 0.85); } diff --git a/worklenz-frontend/src/types/task-management.types.ts b/worklenz-frontend/src/types/task-management.types.ts index 0c82492f..45f5a91d 100644 --- a/worklenz-frontend/src/types/task-management.types.ts +++ b/worklenz-frontend/src/types/task-management.types.ts @@ -96,6 +96,7 @@ export interface TaskManagementState { grouping: string | undefined; selectedPriorities: string[]; search: string; + loadingSubtasks: Record; // Track loading state for individual tasks } export interface TaskGroupsState {