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..a755c6bc 100644 --- a/worklenz-frontend/public/locales/alb/task-list-table.json +++ b/worklenz-frontend/public/locales/alb/task-list-table.json @@ -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..ff4e5dcc 100644 --- a/worklenz-frontend/public/locales/de/task-list-table.json +++ b/worklenz-frontend/public/locales/de/task-list-table.json @@ -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-list-table.json b/worklenz-frontend/public/locales/en/task-list-table.json index 45ba73f5..ed503c9f 100644 --- a/worklenz-frontend/public/locales/en/task-list-table.json +++ b/worklenz-frontend/public/locales/en/task-list-table.json @@ -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..1d98c824 100644 --- a/worklenz-frontend/public/locales/es/task-list-table.json +++ b/worklenz-frontend/public/locales/es/task-list-table.json @@ -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..3d851b4d 100644 --- a/worklenz-frontend/public/locales/pt/task-list-table.json +++ b/worklenz-frontend/public/locales/pt/task-list-table.json @@ -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..1c486746 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, }); } }, []); @@ -83,22 +90,8 @@ const AssigneeSelector: React.FC = ({ const handleScroll = () => { 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); - } - } + // Close dropdown when scrolling to prevent it from moving with the content + 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..ed59085f 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, + }); + } } }, []); @@ -61,22 +75,8 @@ const LabelsSelector: React.FC = ({ task, isDarkMode = fals const handleScroll = () => { 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); - } - } + // Close dropdown when scrolling to prevent it from moving with the content + setIsOpen(false); } }; diff --git a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx index 2d36857d..5c0950d9 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx @@ -70,7 +70,7 @@ const BASE_COLUMNS = [ { 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 }, @@ -434,6 +434,11 @@ const TaskListV2: React.FC = ({ projectId }) => { 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 ( diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx index ef5e65e7..4d761a24 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx @@ -1,15 +1,15 @@ -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 } 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'; @@ -19,6 +19,13 @@ import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { selectTaskById } from '@/features/task-management/task-management.slice'; import { selectIsTaskSelected, toggleTaskSelection } from '@/features/task-management/selection.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; @@ -42,18 +49,21 @@ 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 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 @@ -101,15 +111,24 @@ 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 formattedDueDate = useMemo(() => { + const dateValue = task.dueDate || task.due_date; + return dateValue ? formatDate(dateValue) : null; + }, [task.dueDate, task.due_date]); const formattedStartDate = useMemo(() => task.startDate ? formatDate(task.startDate) : null, [task.startDate] ); + + // 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] + ); const formattedCompletedDate = useMemo(() => task.completedAt ? formatDate(task.completedAt) : null, @@ -126,10 +145,7 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn [task.updatedAt] ); - // 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]); + // Handle checkbox change const handleCheckboxChange = useCallback((e: any) => { @@ -137,6 +153,33 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn dispatch(toggleTaskSelection(taskId)); }, [dispatch, taskId]); + // 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 status style const statusStyle = useMemo(() => ({ backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)', @@ -149,18 +192,23 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn color: task.priorityColor || 'rgb(31, 41, 55)', }), [task.priorityColor]); - // Memoize labels display - const labelsDisplay = useMemo(() => { - if (!task.labels || task.labels.length === 0) return null; - - const visibleLabels = task.labels.slice(0, 2); - const remainingCount = task.labels.length - 2; - - return { - visibleLabels, - remainingCount: remainingCount > 0 ? remainingCount : null, - }; - }, [task.labels]); + // 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]); const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { const baseStyle = { width }; @@ -248,11 +296,62 @@ 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(); + setActiveDatePicker('dueDate'); + }} + > + {formattedDueDate ? ( + + {formattedDueDate} + + ) : ( + + {t('setDueDate')} + + )} +
)}
); @@ -282,49 +381,46 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn ); case 'labels': + if (task.labels) console.log('task.labels', task.labels); return ( -
- {labelsDisplay?.visibleLabels.map((label, index) => ( - - {label.name} - - ))} - {labelsDisplay?.remainingCount && ( - - +{labelsDisplay.remainingCount} - - )} +
+ {task.labels?.map((label, index) => { + const extendedLabel = label as any; // Type assertion for extended properties + return extendedLabel.end && extendedLabel.names && extendedLabel.name ? ( + + ) : ( + + ); + })} +
); case 'phase': return (
- - {task.phase} - +
); case 'timeTracking': return ( -
- - - {task.timeTracking?.logged || 0}h - - {task.timeTracking?.estimated && ( - - /{task.timeTracking.estimated}h - - )} +
+
); @@ -341,11 +437,62 @@ 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(); + setActiveDatePicker('startDate'); + }} + > + {formattedStartDate ? ( + + {formattedStartDate} + + ) : ( + + {t('setStartDate')} + + )} +
)}
); @@ -404,7 +551,7 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn task.phase, task.reporter, task.assignee_names, - task.timeTracking, + task.progress, task.sub_tasks, taskDisplayName, @@ -415,11 +562,14 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn formattedCompletedDate, formattedCreatedDate, formattedUpdatedDate, - labelsDisplay, + labelsAdapter, isDarkMode, convertedTask, isSelected, handleCheckboxChange, + activeDatePicker, + dateValues, + handleDateChange, ]); return ( 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..b35cd836 100644 --- a/worklenz-frontend/src/features/task-management/task-management.slice.ts +++ b/worklenz-frontend/src/features/task-management/task-management.slice.ts @@ -151,11 +151,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 +164,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 +237,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 +257,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 +270,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 +286,8 @@ export const fetchTasksV3 = createAsyncThunk( has_dependencies: task.has_dependencies || false, schedule_id: task.schedule_id || null, }; + + return transformedTask; }); return { diff --git a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts index 52fe95ff..8748f51d 100644 --- a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts +++ b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts @@ -387,6 +387,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 +527,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] ); 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;