feat(task-management): enhance task date handling and UI components in TaskListV2
- Added startDate and dueDate fields to task data structure for improved date management. - Updated TaskRow to include date pickers for start and due dates with clear functionality. - Enhanced LabelsSelector to support dynamic label rendering and improved visual feedback. - Refactored AssigneeSelector and CustomColordLabel components for better integration with task data. - Improved dropdown positioning logic in LabelsSelector for better user experience. - Added translations for new date-related UI elements in multiple languages.
This commit is contained in:
@@ -1031,6 +1031,8 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Transform tasks with all necessary data preprocessing
|
// Transform tasks with all necessary data preprocessing
|
||||||
const transformStartTime = performance.now();
|
const transformStartTime = performance.now();
|
||||||
const transformedTasks = tasks.map((task, index) => {
|
const transformedTasks = tasks.map((task, index) => {
|
||||||
@@ -1076,7 +1078,8 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
end: l.end,
|
end: l.end,
|
||||||
names: l.names
|
names: l.names
|
||||||
})) || [],
|
})) || [],
|
||||||
dueDate: task.end_date,
|
dueDate: task.end_date || task.END_DATE,
|
||||||
|
startDate: task.start_date,
|
||||||
timeTracking: {
|
timeTracking: {
|
||||||
estimated: convertTimeValue(task.total_time),
|
estimated: convertTimeValue(task.total_time),
|
||||||
logged: convertTimeValue(task.time_spent),
|
logged: convertTimeValue(task.time_spent),
|
||||||
|
|||||||
@@ -59,5 +59,11 @@
|
|||||||
"convertToTask": "Shndërro në Detyrë",
|
"convertToTask": "Shndërro në Detyrë",
|
||||||
"delete": "Fshi",
|
"delete": "Fshi",
|
||||||
"searchByNameInputPlaceholder": "Kërko sipas emrit"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,5 +59,11 @@
|
|||||||
"convertToTask": "In Aufgabe umwandeln",
|
"convertToTask": "In Aufgabe umwandeln",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"searchByNameInputPlaceholder": "Nach Namen suchen"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,5 +59,11 @@
|
|||||||
"convertToTask": "Convert to Task",
|
"convertToTask": "Convert to Task",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"searchByNameInputPlaceholder": "Search by name"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,5 +59,11 @@
|
|||||||
"convertToTask": "Convertir en tarea",
|
"convertToTask": "Convertir en tarea",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"searchByNameInputPlaceholder": "Buscar por nombre"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,5 +59,11 @@
|
|||||||
"convertToTask": "Converter em Tarefa",
|
"convertToTask": "Converter em Tarefa",
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"searchByNameInputPlaceholder": "Buscar por nome"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,9 +61,16 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
const updateDropdownPosition = useCallback(() => {
|
const updateDropdownPosition = useCallback(() => {
|
||||||
if (buttonRef.current) {
|
if (buttonRef.current) {
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
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({
|
setDropdownPosition({
|
||||||
top: rect.bottom + window.scrollY + 2,
|
top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4,
|
||||||
left: rect.left + window.scrollX,
|
left: rect.left,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -83,22 +90,8 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
// Check if the button is still visible in the viewport
|
// Close dropdown when scrolling to prevent it from moving with the content
|
||||||
if (buttonRef.current) {
|
setIsOpen(false);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import { Label } from '@/types/task-management.types';
|
import { Label } from '@/types/task-management.types';
|
||||||
|
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||||
|
|
||||||
interface CustomColordLabelProps {
|
interface CustomColordLabelProps {
|
||||||
label: Label;
|
label: Label | ITaskLabel;
|
||||||
isDarkMode?: boolean;
|
isDarkMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,11 +12,37 @@ const CustomColordLabel: React.FC<CustomColordLabelProps> = ({ label, isDarkMode
|
|||||||
const truncatedName =
|
const truncatedName =
|
||||||
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
|
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 (
|
return (
|
||||||
<Tooltip title={label.name}>
|
<Tooltip title={label.name}>
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium text-white shrink-0 max-w-[120px]"
|
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
|
||||||
style={{ backgroundColor: label.color }}
|
style={{
|
||||||
|
backgroundColor,
|
||||||
|
color: textColor,
|
||||||
|
border: `1px solid ${backgroundColor}`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="truncate">{truncatedName}</span>
|
<span className="truncate">{truncatedName}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
|
import { NumbersColorMap } from '@/shared/constants';
|
||||||
|
|
||||||
interface CustomNumberLabelProps {
|
interface CustomNumberLabelProps {
|
||||||
labelList: string[];
|
labelList: string[];
|
||||||
namesString: string;
|
namesString: string;
|
||||||
isDarkMode?: boolean;
|
isDarkMode?: boolean;
|
||||||
|
color?: string; // Add color prop for label color
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomNumberLabel: React.FC<CustomNumberLabelProps> = ({
|
const CustomNumberLabel: React.FC<CustomNumberLabelProps> = ({
|
||||||
labelList,
|
labelList,
|
||||||
namesString,
|
namesString,
|
||||||
isDarkMode = false,
|
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 (
|
return (
|
||||||
<Tooltip title={labelList.join(', ')}>
|
<Tooltip title={labelList.join(', ')}>
|
||||||
<span
|
<span
|
||||||
className={`
|
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
|
||||||
inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
|
style={{ backgroundColor }}
|
||||||
${isDarkMode ? 'bg-gray-600 text-gray-100' : 'bg-gray-200 text-gray-700'}
|
|
||||||
cursor-help
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
{namesString}
|
{namesString}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -39,10 +39,24 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({ task, isDarkMode = fals
|
|||||||
const updateDropdownPosition = useCallback(() => {
|
const updateDropdownPosition = useCallback(() => {
|
||||||
if (buttonRef.current) {
|
if (buttonRef.current) {
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
setDropdownPosition({
|
const dropdownHeight = 300; // Approximate height of dropdown (max-height + padding)
|
||||||
top: rect.bottom + window.scrollY + 2,
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
left: rect.left + window.scrollX,
|
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 = () => {
|
const handleScroll = () => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
// Check if the button is still visible in the viewport
|
// Close dropdown when scrolling to prevent it from moving with the content
|
||||||
if (buttonRef.current) {
|
setIsOpen(false);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const BASE_COLUMNS = [
|
|||||||
{ id: 'priority', label: 'Priority', width: '120px', key: COLUMN_KEYS.PRIORITY },
|
{ id: 'priority', label: 'Priority', width: '120px', key: COLUMN_KEYS.PRIORITY },
|
||||||
{ id: 'dueDate', label: 'Due Date', width: '120px', key: COLUMN_KEYS.DUE_DATE },
|
{ id: 'dueDate', label: 'Due Date', width: '120px', key: COLUMN_KEYS.DUE_DATE },
|
||||||
{ id: 'progress', label: 'Progress', width: '120px', key: COLUMN_KEYS.PROGRESS },
|
{ 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: 'phase', label: 'Phase', width: '120px', key: COLUMN_KEYS.PHASE },
|
||||||
{ id: 'timeTracking', label: 'Time Tracking', width: '120px', key: COLUMN_KEYS.TIME_TRACKING },
|
{ id: 'timeTracking', label: 'Time Tracking', width: '120px', key: COLUMN_KEYS.TIME_TRACKING },
|
||||||
{ id: 'estimation', label: 'Estimation', width: '120px', key: COLUMN_KEYS.ESTIMATION },
|
{ id: 'estimation', label: 'Estimation', width: '120px', key: COLUMN_KEYS.ESTIMATION },
|
||||||
@@ -434,6 +434,11 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
const columnStyle: ColumnStyle = {
|
const columnStyle: ColumnStyle = {
|
||||||
width: column.width,
|
width: column.width,
|
||||||
flexShrink: 0, // Prevent columns from shrinking
|
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 (
|
return (
|
||||||
|
|||||||
@@ -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 { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { CheckCircleOutlined, HolderOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined, HolderOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import { Checkbox } from 'antd';
|
import { Checkbox, DatePicker } from 'antd';
|
||||||
|
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import AssigneeSelector from '@/components/AssigneeSelector';
|
import AssigneeSelector from '@/components/AssigneeSelector';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Bars3Icon } from '@heroicons/react/24/outline';
|
import { Bars3Icon } from '@heroicons/react/24/outline';
|
||||||
import { ClockIcon } from '@heroicons/react/24/outline';
|
|
||||||
import AvatarGroup from '../AvatarGroup';
|
import AvatarGroup from '../AvatarGroup';
|
||||||
import { DEFAULT_TASK_NAME } from '@/shared/constants';
|
import { DEFAULT_TASK_NAME } from '@/shared/constants';
|
||||||
import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress';
|
import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress';
|
||||||
@@ -19,6 +19,12 @@ import { useAppSelector } from '@/hooks/useAppSelector';
|
|||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { selectTaskById } from '@/features/task-management/task-management.slice';
|
import { selectTaskById } from '@/features/task-management/task-management.slice';
|
||||||
import { selectIsTaskSelected, toggleTaskSelection } from '@/features/task-management/selection.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';
|
||||||
|
|
||||||
interface TaskRowProps {
|
interface TaskRowProps {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
@@ -42,18 +48,21 @@ const getTaskDisplayName = (task: Task): string => {
|
|||||||
// Memoized date formatter to avoid repeated date parsing
|
// Memoized date formatter to avoid repeated date parsing
|
||||||
const formatDate = (dateString: string): string => {
|
const formatDate = (dateString: string): string => {
|
||||||
try {
|
try {
|
||||||
return format(new Date(dateString), 'MMM d');
|
return format(new Date(dateString), 'MMM d, yyyy');
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memoized date formatter to avoid repeated date parsing
|
|
||||||
|
|
||||||
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns }) => {
|
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
const isSelected = useAppSelector(state => selectIsTaskSelected(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) {
|
if (!task) {
|
||||||
return null; // Don't render if task is not found in store
|
return null; // Don't render if task is not found in store
|
||||||
@@ -101,15 +110,24 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
|
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
|
||||||
|
|
||||||
// Memoize formatted dates
|
// Memoize formatted dates
|
||||||
const formattedDueDate = useMemo(() =>
|
const formattedDueDate = useMemo(() => {
|
||||||
task.dueDate ? formatDate(task.dueDate) : null,
|
const dateValue = task.dueDate || task.due_date;
|
||||||
[task.dueDate]
|
return dateValue ? formatDate(dateValue) : null;
|
||||||
);
|
}, [task.dueDate, task.due_date]);
|
||||||
|
|
||||||
const formattedStartDate = useMemo(() =>
|
const formattedStartDate = useMemo(() =>
|
||||||
task.startDate ? formatDate(task.startDate) : null,
|
task.startDate ? formatDate(task.startDate) : null,
|
||||||
[task.startDate]
|
[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(() =>
|
const formattedCompletedDate = useMemo(() =>
|
||||||
task.completedAt ? formatDate(task.completedAt) : null,
|
task.completedAt ? formatDate(task.completedAt) : null,
|
||||||
@@ -126,10 +144,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
[task.updatedAt]
|
[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
|
// Handle checkbox change
|
||||||
const handleCheckboxChange = useCallback((e: any) => {
|
const handleCheckboxChange = useCallback((e: any) => {
|
||||||
@@ -137,6 +152,33 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
dispatch(toggleTaskSelection(taskId));
|
dispatch(toggleTaskSelection(taskId));
|
||||||
}, [dispatch, 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
|
// Memoize status style
|
||||||
const statusStyle = useMemo(() => ({
|
const statusStyle = useMemo(() => ({
|
||||||
backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)',
|
backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)',
|
||||||
@@ -149,18 +191,23 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
color: task.priorityColor || 'rgb(31, 41, 55)',
|
color: task.priorityColor || 'rgb(31, 41, 55)',
|
||||||
}), [task.priorityColor]);
|
}), [task.priorityColor]);
|
||||||
|
|
||||||
// Memoize labels display
|
// Create labels adapter for LabelsSelector
|
||||||
const labelsDisplay = useMemo(() => {
|
const labelsAdapter = useMemo(() => ({
|
||||||
if (!task.labels || task.labels.length === 0) return null;
|
id: task.id,
|
||||||
|
name: task.title || task.name,
|
||||||
const visibleLabels = task.labels.slice(0, 2);
|
parent_task_id: task.parent_task_id,
|
||||||
const remainingCount = task.labels.length - 2;
|
manual_progress: false,
|
||||||
|
all_labels: task.labels?.map(label => ({
|
||||||
return {
|
id: label.id,
|
||||||
visibleLabels,
|
name: label.name,
|
||||||
remainingCount: remainingCount > 0 ? remainingCount : null,
|
color_code: label.color,
|
||||||
};
|
})) || [],
|
||||||
}, [task.labels]);
|
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 renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
||||||
const baseStyle = { width };
|
const baseStyle = { width };
|
||||||
@@ -248,11 +295,62 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
|
|
||||||
case 'dueDate':
|
case 'dueDate':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle} className="relative group">
|
||||||
{formattedDueDate && (
|
{activeDatePicker === 'dueDate' ? (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="w-full relative">
|
||||||
{formattedDueDate}
|
<DatePicker
|
||||||
</span>
|
{...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>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -282,25 +380,28 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'labels':
|
case 'labels':
|
||||||
|
if (task.labels) console.log('task.labels', task.labels);
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1" style={baseStyle}>
|
<div className="flex items-center gap-1 flex-wrap min-w-0" style={{ ...baseStyle, minWidth: '200px' }}>
|
||||||
{labelsDisplay?.visibleLabels.map((label, index) => (
|
{task.labels?.map((label, index) => {
|
||||||
<span
|
const extendedLabel = label as any; // Type assertion for extended properties
|
||||||
key={`${label.id}-${index}`}
|
return extendedLabel.end && extendedLabel.names && extendedLabel.name ? (
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
<CustomNumberLabel
|
||||||
style={{
|
key={`${label.id}-${index}`}
|
||||||
backgroundColor: label.color ? `${label.color}20` : 'rgb(229, 231, 235)',
|
labelList={extendedLabel.names}
|
||||||
color: label.color || 'rgb(31, 41, 55)',
|
namesString={extendedLabel.name}
|
||||||
}}
|
isDarkMode={isDarkMode}
|
||||||
>
|
color={label.color}
|
||||||
{label.name}
|
/>
|
||||||
</span>
|
) : (
|
||||||
))}
|
<CustomColordLabel
|
||||||
{labelsDisplay?.remainingCount && (
|
key={`${label.id}-${index}`}
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
label={label}
|
||||||
+{labelsDisplay.remainingCount}
|
isDarkMode={isDarkMode}
|
||||||
</span>
|
/>
|
||||||
)}
|
);
|
||||||
|
})}
|
||||||
|
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -315,16 +416,8 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
|
|
||||||
case 'timeTracking':
|
case 'timeTracking':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1" style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
<ClockIcon className="w-4 h-4 text-gray-400" />
|
<TaskTimeTracking taskId={task.id || ''} isDarkMode={isDarkMode} />
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -341,11 +434,62 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
|
|
||||||
case 'startDate':
|
case 'startDate':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle} className="relative group">
|
||||||
{formattedStartDate && (
|
{activeDatePicker === 'startDate' ? (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="w-full relative">
|
||||||
{formattedStartDate}
|
<DatePicker
|
||||||
</span>
|
{...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>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -404,7 +548,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
task.phase,
|
task.phase,
|
||||||
task.reporter,
|
task.reporter,
|
||||||
task.assignee_names,
|
task.assignee_names,
|
||||||
task.timeTracking,
|
|
||||||
task.progress,
|
task.progress,
|
||||||
task.sub_tasks,
|
task.sub_tasks,
|
||||||
taskDisplayName,
|
taskDisplayName,
|
||||||
@@ -415,11 +559,14 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
formattedCompletedDate,
|
formattedCompletedDate,
|
||||||
formattedCreatedDate,
|
formattedCreatedDate,
|
||||||
formattedUpdatedDate,
|
formattedUpdatedDate,
|
||||||
labelsDisplay,
|
labelsAdapter,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
convertedTask,
|
convertedTask,
|
||||||
isSelected,
|
isSelected,
|
||||||
handleCheckboxChange,
|
handleCheckboxChange,
|
||||||
|
activeDatePicker,
|
||||||
|
dateValues,
|
||||||
|
handleDateChange,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
DownOutlined, // Added DownOutlined for expand/collapse
|
DownOutlined, // Added DownOutlined for expand/collapse
|
||||||
|
CloseOutlined, // Added CloseOutlined for clear button
|
||||||
} from '@/shared/antd-imports';
|
} from '@/shared/antd-imports';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Task } from '@/types/task-management.types';
|
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}`}
|
className={`flex items-center px-2 ${borderClasses}`}
|
||||||
style={{ width: col.width }}
|
style={{ width: col.width }}
|
||||||
>
|
>
|
||||||
<TaskKey taskKey={task.task_key} isDarkMode={isDarkMode} />
|
<TaskKey taskKey={task.task_key || ''} isDarkMode={isDarkMode} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -892,7 +893,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
|||||||
className={`flex items-center px-2 ${borderClasses}`}
|
className={`flex items-center px-2 ${borderClasses}`}
|
||||||
style={{ width: col.width }}
|
style={{ width: col.width }}
|
||||||
>
|
>
|
||||||
<TaskKey taskKey={task.task_key} isDarkMode={isDarkMode} />
|
<TaskKey taskKey={task.task_key || ''} isDarkMode={isDarkMode} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1316,13 +1317,35 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
|||||||
className={`flex items-center px-2 ${borderClasses}`}
|
className={`flex items-center px-2 ${borderClasses}`}
|
||||||
style={{ width: col.width }}
|
style={{ width: col.width }}
|
||||||
>
|
>
|
||||||
<DatePicker
|
<div className="w-full relative group">
|
||||||
{...taskManagementAntdConfig.datePickerDefaults}
|
<DatePicker
|
||||||
className="w-full bg-transparent border-none shadow-none"
|
{...taskManagementAntdConfig.datePickerDefaults}
|
||||||
value={dateValues.start}
|
className="w-full bg-transparent border-none shadow-none"
|
||||||
onChange={date => handleDateChange(date, 'startDate')}
|
value={dateValues.start}
|
||||||
placeholder="Start Date"
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1333,13 +1356,35 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
|||||||
className={`flex items-center px-2 ${borderClasses}`}
|
className={`flex items-center px-2 ${borderClasses}`}
|
||||||
style={{ width: col.width }}
|
style={{ width: col.width }}
|
||||||
>
|
>
|
||||||
<DatePicker
|
<div className="w-full relative group">
|
||||||
{...taskManagementAntdConfig.datePickerDefaults}
|
<DatePicker
|
||||||
className="w-full bg-transparent border-none shadow-none"
|
{...taskManagementAntdConfig.datePickerDefaults}
|
||||||
value={dateValues.due}
|
className="w-full bg-transparent border-none shadow-none"
|
||||||
onChange={date => handleDateChange(date, 'dueDate')}
|
value={dateValues.due}
|
||||||
placeholder="Due Date"
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,12 @@ export const fetchTasks = createAsyncThunk(
|
|||||||
task.labels?.map((l: any) => ({
|
task.labels?.map((l: any) => ({
|
||||||
id: l.id || l.label_id,
|
id: l.id || l.label_id,
|
||||||
name: l.name,
|
name: l.name,
|
||||||
color: l.color_code || '#1890ff',
|
color: l.color || '#1890ff',
|
||||||
end: l.end,
|
end: l.end,
|
||||||
names: l.names,
|
names: l.names,
|
||||||
})) || [],
|
})) || [],
|
||||||
dueDate: task.end_date,
|
dueDate: task.dueDate,
|
||||||
|
startDate: task.startDate,
|
||||||
timeTracking: {
|
timeTracking: {
|
||||||
estimated: convertTimeValue(task.total_time),
|
estimated: convertTimeValue(task.total_time),
|
||||||
logged: convertTimeValue(task.time_spent),
|
logged: convertTimeValue(task.time_spent),
|
||||||
@@ -163,6 +164,8 @@ export const fetchTasks = createAsyncThunk(
|
|||||||
customFields: {},
|
customFields: {},
|
||||||
createdAt: task.created_at || new Date().toISOString(),
|
createdAt: task.created_at || new Date().toISOString(),
|
||||||
updatedAt: task.updated_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,
|
order: typeof task.sort_order === 'number' ? task.sort_order : 0,
|
||||||
// Ensure all Task properties are mapped, even if undefined in API response
|
// Ensure all Task properties are mapped, even if undefined in API response
|
||||||
sub_tasks: task.sub_tasks || [],
|
sub_tasks: task.sub_tasks || [],
|
||||||
@@ -234,16 +237,13 @@ export const fetchTasksV3 = createAsyncThunk(
|
|||||||
|
|
||||||
const response = await tasksApiService.getTaskListV3(config);
|
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
|
// Ensure tasks are properly normalized
|
||||||
const tasks: Task[] = response.body.allTasks.map((task: any) => {
|
const tasks: Task[] = response.body.allTasks.map((task: any) => {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
return {
|
const transformedTask = {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
task_key: task.task_key || task.key || '',
|
task_key: task.task_key || task.key || '',
|
||||||
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
|
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[] }) => ({
|
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,
|
id: l.id || l.label_id,
|
||||||
name: l.name,
|
name: l.name,
|
||||||
color: l.color_code || '#1890ff',
|
color: l.color || '#1890ff',
|
||||||
end: l.end,
|
end: l.end,
|
||||||
names: l.names,
|
names: l.names,
|
||||||
})) || [],
|
})) || [],
|
||||||
dueDate: task.end_date,
|
dueDate: task.dueDate,
|
||||||
|
startDate: task.startDate,
|
||||||
timeTracking: {
|
timeTracking: {
|
||||||
estimated: convertTimeValue(task.total_time),
|
estimated: convertTimeValue(task.total_time),
|
||||||
logged: convertTimeValue(task.time_spent),
|
logged: convertTimeValue(task.time_spent),
|
||||||
@@ -269,6 +270,8 @@ export const fetchTasksV3 = createAsyncThunk(
|
|||||||
customFields: {},
|
customFields: {},
|
||||||
createdAt: task.created_at || now,
|
createdAt: task.created_at || now,
|
||||||
updatedAt: task.updated_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,
|
order: typeof task.sort_order === 'number' ? task.sort_order : 0,
|
||||||
sub_tasks: task.sub_tasks || [],
|
sub_tasks: task.sub_tasks || [],
|
||||||
sub_tasks_count: task.sub_tasks_count || 0,
|
sub_tasks_count: task.sub_tasks_count || 0,
|
||||||
@@ -283,6 +286,8 @@ export const fetchTasksV3 = createAsyncThunk(
|
|||||||
has_dependencies: task.has_dependencies || false,
|
has_dependencies: task.has_dependencies || false,
|
||||||
schedule_id: task.schedule_id || null,
|
schedule_id: task.schedule_id || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return transformedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -387,6 +387,16 @@ export const useTaskSocketHandlers = () => {
|
|||||||
|
|
||||||
// Update enhanced kanban slice
|
// Update enhanced kanban slice
|
||||||
dispatch(updateEnhancedKanbanTaskEndDate({ task: taskWithProgress }));
|
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]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@@ -517,6 +527,16 @@ export const useTaskSocketHandlers = () => {
|
|||||||
|
|
||||||
dispatch(updateTaskStartDate({ task: taskWithProgress }));
|
dispatch(updateTaskStartDate({ task: taskWithProgress }));
|
||||||
dispatch(setStartDate(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]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
import CustomColordLabel from '@/components/taskListCommon/labelsSelector/CustomColordLabel';
|
|
||||||
import CustomNumberLabel from '@/components/taskListCommon/labelsSelector/CustomNumberLabel';
|
import CustomNumberLabel from '@/components/taskListCommon/labelsSelector/CustomNumberLabel';
|
||||||
import LabelsSelector from '@/components/taskListCommon/labelsSelector/LabelsSelector';
|
import LabelsSelector from '@/components/taskListCommon/labelsSelector/LabelsSelector';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { CustomColordLabel } from '@/components';
|
||||||
|
|
||||||
interface TaskListLabelsCellProps {
|
interface TaskListLabelsCellProps {
|
||||||
task: IProjectTask;
|
task: IProjectTask;
|
||||||
|
|||||||
Reference in New Issue
Block a user