Merge pull request #231 from Worklenz/fix/task-drag-and-drop-improvement

Fix/task drag and drop improvement
This commit is contained in:
Chamika J
2025-07-04 10:34:30 +05:30
committed by GitHub
18 changed files with 462 additions and 149 deletions

View File

@@ -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),

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -61,9 +61,16 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
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<AssigneeSelectorProps> = ({
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);
}
};

View File

@@ -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<CustomColordLabelProps> = ({ 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 (
<Tooltip title={label.name}>
<span
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium text-white shrink-0 max-w-[120px]"
style={{ backgroundColor: label.color }}
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
style={{
backgroundColor,
color: textColor,
border: `1px solid ${backgroundColor}`,
}}
>
<span className="truncate">{truncatedName}</span>
</span>

View File

@@ -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<CustomNumberLabelProps> = ({
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 (
<Tooltip title={labelList.join(', ')}>
<span
className={`
inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
${isDarkMode ? 'bg-gray-600 text-gray-100' : 'bg-gray-200 text-gray-700'}
cursor-help
`}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
style={{ backgroundColor }}
>
{namesString}
</span>

View File

@@ -39,10 +39,24 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({ 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<LabelsSelectorProps> = ({ 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);
}
};

View File

@@ -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<TaskListV2Props> = ({ 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 (

View File

@@ -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<TaskRowProps> = 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<string | null>(null);
if (!task) {
return null; // Don't render if task is not found in store
@@ -101,15 +111,24 @@ const TaskRow: React.FC<TaskRowProps> = 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<TaskRowProps> = 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<TaskRowProps> = 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<TaskRowProps> = 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<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'dueDate':
return (
<div style={baseStyle}>
{formattedDueDate && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{formattedDueDate}
</span>
<div style={baseStyle} className="relative group">
{activeDatePicker === 'dueDate' ? (
<div className="w-full relative">
<DatePicker
{...taskManagementAntdConfig.datePickerDefaults}
className="w-full bg-transparent border-none shadow-none"
value={dateValues.due}
onChange={date => handleDateChange(date, 'dueDate')}
placeholder={t('dueDatePlaceholder')}
allowClear={false}
suffixIcon={null}
open={true}
onOpenChange={(open) => {
if (!open) {
setActiveDatePicker(null);
}
}}
autoFocus
/>
{/* Custom clear button */}
{dateValues.due && (
<button
onClick={e => {
e.preventDefault();
e.stopPropagation();
handleDateChange(null, 'dueDate');
}}
className={`absolute right-1 top-1/2 transform -translate-y-1/2 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
isDarkMode
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
title={t('clearDueDate')}
>
<CloseOutlined style={{ fontSize: '10px' }} />
</button>
)}
</div>
) : (
<div
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded px-2 py-1 transition-colors"
onClick={(e) => {
e.stopPropagation();
setActiveDatePicker('dueDate');
}}
>
{formattedDueDate ? (
<span className="text-sm text-gray-500 dark:text-gray-400">
{formattedDueDate}
</span>
) : (
<span className="text-sm text-gray-400 dark:text-gray-500">
{t('setDueDate')}
</span>
)}
</div>
)}
</div>
);
@@ -282,49 +381,46 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
);
case 'labels':
if (task.labels) console.log('task.labels', task.labels);
return (
<div className="flex items-center gap-1" style={baseStyle}>
{labelsDisplay?.visibleLabels.map((label, index) => (
<span
key={`${label.id}-${index}`}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: label.color ? `${label.color}20` : 'rgb(229, 231, 235)',
color: label.color || 'rgb(31, 41, 55)',
}}
>
{label.name}
</span>
))}
{labelsDisplay?.remainingCount && (
<span className="text-xs text-gray-500 dark:text-gray-400">
+{labelsDisplay.remainingCount}
</span>
)}
<div className="flex items-center gap-1 flex-wrap min-w-0" style={{ ...baseStyle, minWidth: '200px' }}>
{task.labels?.map((label, index) => {
const extendedLabel = label as any; // Type assertion for extended properties
return extendedLabel.end && extendedLabel.names && extendedLabel.name ? (
<CustomNumberLabel
key={`${label.id}-${index}`}
labelList={extendedLabel.names}
namesString={extendedLabel.name}
isDarkMode={isDarkMode}
color={label.color}
/>
) : (
<CustomColordLabel
key={`${label.id}-${index}`}
label={label}
isDarkMode={isDarkMode}
/>
);
})}
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
</div>
);
case 'phase':
return (
<div style={baseStyle}>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
{task.phase}
</span>
<TaskPhaseDropdown
task={task}
projectId={projectId}
isDarkMode={isDarkMode}
/>
</div>
);
case 'timeTracking':
return (
<div className="flex items-center gap-1" style={baseStyle}>
<ClockIcon className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500 dark:text-gray-400">
{task.timeTracking?.logged || 0}h
</span>
{task.timeTracking?.estimated && (
<span className="text-sm text-gray-400 dark:text-gray-500">
/{task.timeTracking.estimated}h
</span>
)}
<div style={baseStyle}>
<TaskTimeTracking taskId={task.id || ''} isDarkMode={isDarkMode} />
</div>
);
@@ -341,11 +437,62 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'startDate':
return (
<div style={baseStyle}>
{formattedStartDate && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{formattedStartDate}
</span>
<div style={baseStyle} className="relative group">
{activeDatePicker === 'startDate' ? (
<div className="w-full relative">
<DatePicker
{...taskManagementAntdConfig.datePickerDefaults}
className="w-full bg-transparent border-none shadow-none"
value={dateValues.start}
onChange={date => handleDateChange(date, 'startDate')}
placeholder={t('startDatePlaceholder')}
allowClear={false}
suffixIcon={null}
open={true}
onOpenChange={(open) => {
if (!open) {
setActiveDatePicker(null);
}
}}
autoFocus
/>
{/* Custom clear button */}
{dateValues.start && (
<button
onClick={e => {
e.preventDefault();
e.stopPropagation();
handleDateChange(null, 'startDate');
}}
className={`absolute right-1 top-1/2 transform -translate-y-1/2 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
isDarkMode
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
title={t('clearStartDate')}
>
<CloseOutlined style={{ fontSize: '10px' }} />
</button>
)}
</div>
) : (
<div
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded px-2 py-1 transition-colors"
onClick={(e) => {
e.stopPropagation();
setActiveDatePicker('startDate');
}}
>
{formattedStartDate ? (
<span className="text-sm text-gray-500 dark:text-gray-400">
{formattedStartDate}
</span>
) : (
<span className="text-sm text-gray-400 dark:text-gray-500">
{t('setStartDate')}
</span>
)}
</div>
)}
</div>
);
@@ -404,7 +551,7 @@ const TaskRow: React.FC<TaskRowProps> = 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<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
formattedCompletedDate,
formattedCreatedDate,
formattedUpdatedDate,
labelsDisplay,
labelsAdapter,
isDarkMode,
convertedTask,
isSelected,
handleCheckboxChange,
activeDatePicker,
dateValues,
handleDateChange,
]);
return (

View File

@@ -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<TaskTimeTrackingProps> = React.memo(({ taskId, isDarkMode }) => {
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimer(
taskId,
null // The hook will get the timer start time from Redux
);
return (
<TaskTimer
taskId={taskId}
started={started}
handleStartTimer={handleStartTimer}
handleStopTimer={handleStopTimer}
timeString={timeString}
/>
);
});
TaskTimeTracking.displayName = 'TaskTimeTracking';
export default TaskTimeTracking;

View File

@@ -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<TaskRowProps> = React.memo(
className={`flex items-center px-2 ${borderClasses}`}
style={{ width: col.width }}
>
<TaskKey taskKey={task.task_key} isDarkMode={isDarkMode} />
<TaskKey taskKey={task.task_key || ''} isDarkMode={isDarkMode} />
</div>
);
@@ -892,7 +893,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
className={`flex items-center px-2 ${borderClasses}`}
style={{ width: col.width }}
>
<TaskKey taskKey={task.task_key} isDarkMode={isDarkMode} />
<TaskKey taskKey={task.task_key || ''} isDarkMode={isDarkMode} />
</div>
);
@@ -1316,13 +1317,35 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
className={`flex items-center px-2 ${borderClasses}`}
style={{ width: col.width }}
>
<DatePicker
{...taskManagementAntdConfig.datePickerDefaults}
className="w-full bg-transparent border-none shadow-none"
value={dateValues.start}
onChange={date => handleDateChange(date, 'startDate')}
placeholder="Start Date"
/>
<div className="w-full relative group">
<DatePicker
{...taskManagementAntdConfig.datePickerDefaults}
className="w-full bg-transparent border-none shadow-none"
value={dateValues.start}
onChange={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 && (
<button
onClick={e => {
e.preventDefault();
e.stopPropagation();
handleDateChange(null, 'startDate');
}}
className={`absolute right-1 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
isDarkMode
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
title="Clear start date"
>
<CloseOutlined style={{ fontSize: '10px' }} />
</button>
)}
</div>
</div>
);
@@ -1333,13 +1356,35 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
className={`flex items-center px-2 ${borderClasses}`}
style={{ width: col.width }}
>
<DatePicker
{...taskManagementAntdConfig.datePickerDefaults}
className="w-full bg-transparent border-none shadow-none"
value={dateValues.due}
onChange={date => handleDateChange(date, 'dueDate')}
placeholder="Due Date"
/>
<div className="w-full relative group">
<DatePicker
{...taskManagementAntdConfig.datePickerDefaults}
className="w-full bg-transparent border-none shadow-none"
value={dateValues.due}
onChange={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 && (
<button
onClick={e => {
e.preventDefault();
e.stopPropagation();
handleDateChange(null, 'dueDate');
}}
className={`absolute right-1 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
isDarkMode
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
title="Clear due date"
>
<CloseOutlined style={{ fontSize: '10px' }} />
</button>
)}
</div>
</div>
);

View File

@@ -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 {

View File

@@ -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]
);

View File

@@ -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;