refactor(task-list): restructure TaskRow and introduce new column components
- Refactored TaskRow to simplify state management and enhance readability by extracting logic into custom hooks. - Introduced new components for rendering task columns, including DatePickerColumn, TitleColumn, and various column types for better modularity. - Improved task name editing functionality with better state handling and click outside detection. - Streamlined date handling and formatting within the task row for improved user experience.
This commit is contained in:
@@ -1,32 +1,13 @@
|
||||
import React, { memo, useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons';
|
||||
import { Checkbox, DatePicker, Tooltip, Input } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||
import AssigneeSelector from '@/components/AssigneeSelector';
|
||||
import { format } from 'date-fns';
|
||||
import AvatarGroup from '../AvatarGroup';
|
||||
import { DEFAULT_TASK_NAME } from '@/shared/constants';
|
||||
import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress';
|
||||
import TaskStatusDropdown from '@/components/task-management/task-status-dropdown';
|
||||
import TaskPriorityDropdown from '@/components/task-management/task-priority-dropdown';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { selectTaskById, toggleTaskExpansion, fetchSubTasks } from '@/features/task-management/task-management.slice';
|
||||
import { selectIsTaskSelected, toggleTaskSelection } from '@/features/task-management/selection.slice';
|
||||
import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TaskTimeTracking from './TaskTimeTracking';
|
||||
import { CustomNumberLabel, CustomColordLabel } from '@/components';
|
||||
import LabelsSelector from '@/components/LabelsSelector';
|
||||
import TaskPhaseDropdown from '@/components/task-management/task-phase-dropdown';
|
||||
import { CustomColumnCell } from './components/CustomColumnComponents';
|
||||
import { selectTaskById } from '@/features/task-management/task-management.slice';
|
||||
import { selectIsTaskSelected } from '@/features/task-management/selection.slice';
|
||||
import { useTaskRowState } from './hooks/useTaskRowState';
|
||||
import { useTaskRowActions } from './hooks/useTaskRowActions';
|
||||
import { useTaskRowColumns } from './hooks/useTaskRowColumns';
|
||||
|
||||
interface TaskRowProps {
|
||||
taskId: string;
|
||||
@@ -45,79 +26,51 @@ interface TaskRowProps {
|
||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||
}
|
||||
|
||||
interface TaskLabelsCellProps {
|
||||
labels: Task['labels'];
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
const TaskRow: React.FC<TaskRowProps> = memo(({
|
||||
taskId,
|
||||
projectId,
|
||||
visibleColumns,
|
||||
isSubtask = false,
|
||||
isFirstInGroup = false,
|
||||
updateTaskCustomColumnValue
|
||||
}) => {
|
||||
// Get task data and selection state from Redux
|
||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const isDarkMode = themeMode === 'dark';
|
||||
|
||||
const TaskLabelsCell: React.FC<TaskLabelsCellProps> = memo(({ labels, isDarkMode }) => {
|
||||
if (!labels) {
|
||||
// Early return if task is not found
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 flex-wrap">
|
||||
{labels.map((label, index) => {
|
||||
const extendedLabel = label as any;
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
// Use extracted hooks for state management
|
||||
const {
|
||||
activeDatePicker,
|
||||
setActiveDatePicker,
|
||||
editTaskName,
|
||||
setEditTaskName,
|
||||
taskName,
|
||||
setTaskName,
|
||||
taskDisplayName,
|
||||
convertedTask,
|
||||
formattedDates,
|
||||
dateValues,
|
||||
labelsAdapter,
|
||||
} = useTaskRowState(task);
|
||||
|
||||
TaskLabelsCell.displayName = 'TaskLabelsCell';
|
||||
|
||||
// Utility function to get task display name with fallbacks
|
||||
const getTaskDisplayName = (task: Task): string => {
|
||||
// Check each field and only use if it has actual content after trimming
|
||||
if (task.title && task.title.trim()) return task.title.trim();
|
||||
if (task.name && task.name.trim()) return task.name.trim();
|
||||
if (task.task_key && task.task_key.trim()) return task.task_key.trim();
|
||||
return DEFAULT_TASK_NAME;
|
||||
};
|
||||
|
||||
// Memoized date formatter to avoid repeated date parsing
|
||||
const formatDate = (dateString: string): string => {
|
||||
try {
|
||||
return format(new Date(dateString), 'MMM d, yyyy');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns, isSubtask = false, isFirstInGroup = false, updateTaskCustomColumnValue }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
||||
const { socket, connected } = useSocket();
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// State for tracking which date picker is open
|
||||
const [activeDatePicker, setActiveDatePicker] = useState<string | null>(null);
|
||||
|
||||
// State for editing task name
|
||||
const [editTaskName, setEditTaskName] = useState(false);
|
||||
const [taskName, setTaskName] = useState(task.title || task.name || '');
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (!task) {
|
||||
return null; // Don't render if task is not found in store
|
||||
}
|
||||
const {
|
||||
handleCheckboxChange,
|
||||
handleTaskNameSave,
|
||||
handleTaskNameEdit,
|
||||
} = useTaskRowActions({
|
||||
task,
|
||||
taskId,
|
||||
taskName,
|
||||
editTaskName,
|
||||
setEditTaskName,
|
||||
});
|
||||
|
||||
// Drag and drop functionality - only enable for parent tasks
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
@@ -129,6 +82,33 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
disabled: isSubtask, // Disable drag and drop for subtasks
|
||||
});
|
||||
|
||||
// Use extracted column renderer hook
|
||||
const { renderColumn } = useTaskRowColumns({
|
||||
task,
|
||||
projectId,
|
||||
isSubtask,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
visibleColumns,
|
||||
updateTaskCustomColumnValue,
|
||||
taskDisplayName,
|
||||
convertedTask,
|
||||
formattedDates,
|
||||
dateValues,
|
||||
labelsAdapter,
|
||||
activeDatePicker,
|
||||
setActiveDatePicker,
|
||||
editTaskName,
|
||||
taskName,
|
||||
setEditTaskName,
|
||||
setTaskName,
|
||||
handleCheckboxChange,
|
||||
handleTaskNameSave,
|
||||
handleTaskNameEdit,
|
||||
attributes,
|
||||
listeners,
|
||||
});
|
||||
|
||||
// Memoize style object to prevent unnecessary re-renders
|
||||
const style = useMemo(() => ({
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -136,746 +116,6 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}), [transform, transition, isDragging]);
|
||||
|
||||
// Get dark mode from Redux state
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const isDarkMode = themeMode === 'dark';
|
||||
|
||||
// Memoize task display name
|
||||
const taskDisplayName = useMemo(() => getTaskDisplayName(task), [task.title, task.name, task.task_key]);
|
||||
|
||||
// Memoize converted task for AssigneeSelector to prevent recreation
|
||||
const convertedTask = useMemo(() => ({
|
||||
id: task.id,
|
||||
name: taskDisplayName,
|
||||
task_key: task.task_key || taskDisplayName,
|
||||
assignees:
|
||||
task.assignee_names?.map((assignee: InlineMember, index: number) => ({
|
||||
team_member_id: assignee.team_member_id || `assignee-${index}`,
|
||||
id: assignee.team_member_id || `assignee-${index}`,
|
||||
project_member_id: assignee.team_member_id || `assignee-${index}`,
|
||||
name: assignee.name || '',
|
||||
})) || [],
|
||||
parent_task_id: task.parent_task_id,
|
||||
status_id: undefined,
|
||||
project_id: undefined,
|
||||
manual_progress: undefined,
|
||||
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
|
||||
|
||||
// Handle task name save
|
||||
const handleTaskNameSave = useCallback(() => {
|
||||
const newTaskName = inputRef.current?.input?.value || taskName;
|
||||
if (newTaskName?.trim() !== '' && connected && newTaskName.trim() !== (task.title || task.name || '').trim()) {
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_NAME_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
name: newTaskName.trim(),
|
||||
parent_task: task.parent_task_id,
|
||||
})
|
||||
);
|
||||
}
|
||||
setEditTaskName(false);
|
||||
}, [taskName, connected, socket, task.id, task.parent_task_id, task.title, task.name]);
|
||||
|
||||
// Handle click outside for task name editing
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
||||
handleTaskNameSave();
|
||||
}
|
||||
};
|
||||
|
||||
if (editTaskName) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [editTaskName, handleTaskNameSave]);
|
||||
|
||||
// Update local taskName state when task name changes
|
||||
useEffect(() => {
|
||||
setTaskName(task.title || task.name || '');
|
||||
}, [task.title, task.name]);
|
||||
|
||||
// Memoize formatted dates
|
||||
const formattedDates = useMemo(() => ({
|
||||
due: (() => {
|
||||
const dateValue = task.dueDate || task.due_date;
|
||||
return dateValue ? formatDate(dateValue) : null;
|
||||
})(),
|
||||
start: task.startDate ? formatDate(task.startDate) : null,
|
||||
completed: task.completedAt ? formatDate(task.completedAt) : null,
|
||||
created: (task.createdAt || task.created_at) ? formatDate(task.createdAt || task.created_at) : null,
|
||||
updated: task.updatedAt ? formatDate(task.updatedAt) : null,
|
||||
}), [task.dueDate, task.due_date, task.startDate, task.completedAt, task.createdAt, task.created_at, task.updatedAt]);
|
||||
|
||||
// Memoize date values for DatePicker
|
||||
const dateValues = useMemo(
|
||||
() => ({
|
||||
start: task.startDate ? dayjs(task.startDate) : undefined,
|
||||
due: (task.dueDate || task.due_date) ? dayjs(task.dueDate || task.due_date) : undefined,
|
||||
}),
|
||||
[task.startDate, task.dueDate, task.due_date]
|
||||
);
|
||||
|
||||
// Create labels adapter for LabelsSelector
|
||||
const labelsAdapter = useMemo(() => ({
|
||||
id: task.id,
|
||||
name: task.title || task.name,
|
||||
parent_task_id: task.parent_task_id,
|
||||
manual_progress: false,
|
||||
all_labels: task.all_labels?.map(label => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color_code: label.color_code,
|
||||
})) || [],
|
||||
labels: task.labels?.map(label => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color_code: label.color,
|
||||
})) || [],
|
||||
}), [task.id, task.title, task.name, task.parent_task_id, task.all_labels, task.labels, task.all_labels?.length, task.labels?.length]);
|
||||
|
||||
// Handle checkbox change
|
||||
const handleCheckboxChange = useCallback((e: any) => {
|
||||
e.stopPropagation(); // Prevent row click when clicking checkbox
|
||||
dispatch(toggleTaskSelection(taskId));
|
||||
}, [dispatch, taskId]);
|
||||
|
||||
// Handle task expansion toggle
|
||||
const handleToggleExpansion = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Always try to fetch subtasks when expanding, regardless of count
|
||||
if (!task.show_sub_tasks && (!task.sub_tasks || task.sub_tasks.length === 0)) {
|
||||
dispatch(fetchSubTasks({ taskId: task.id, projectId }));
|
||||
}
|
||||
|
||||
// Toggle expansion state
|
||||
dispatch(toggleTaskExpansion(task.id));
|
||||
}, [dispatch, task.id, task.sub_tasks, task.show_sub_tasks, projectId]);
|
||||
|
||||
// Handle date change
|
||||
const handleDateChange = useCallback(
|
||||
(date: dayjs.Dayjs | null, field: 'startDate' | 'dueDate') => {
|
||||
if (!connected || !socket) return;
|
||||
|
||||
const eventType =
|
||||
field === 'startDate'
|
||||
? SocketEvents.TASK_START_DATE_CHANGE
|
||||
: SocketEvents.TASK_END_DATE_CHANGE;
|
||||
const dateField = field === 'startDate' ? 'start_date' : 'end_date';
|
||||
|
||||
socket.emit(
|
||||
eventType.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
[dateField]: date?.format('YYYY-MM-DD'),
|
||||
parent_task: null,
|
||||
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
})
|
||||
);
|
||||
|
||||
// Close the date picker after selection
|
||||
setActiveDatePicker(null);
|
||||
},
|
||||
[connected, socket, task.id]
|
||||
);
|
||||
|
||||
// Memoize date picker handlers
|
||||
const datePickerHandlers = useMemo(() => ({
|
||||
setDueDate: () => setActiveDatePicker('dueDate'),
|
||||
setStartDate: () => setActiveDatePicker('startDate'),
|
||||
clearDueDate: (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDateChange(null, 'dueDate');
|
||||
},
|
||||
clearStartDate: (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDateChange(null, 'startDate');
|
||||
},
|
||||
}), [handleDateChange]);
|
||||
|
||||
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
||||
const baseStyle = { width };
|
||||
|
||||
switch (columnId) {
|
||||
case 'dragHandle':
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={baseStyle}
|
||||
{...(isSubtask ? {} : { ...attributes, ...listeners })}
|
||||
>
|
||||
{!isSubtask && <HolderOutlined className="text-gray-400 hover:text-gray-600" />}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<div className="flex items-center justify-center dark:border-gray-700" style={baseStyle}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={handleCheckboxChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'taskKey':
|
||||
return (
|
||||
<div className="flex items-center pl-3 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<span className="text-xs font-medium px-2 py-1 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 whitespace-nowrap border border-gray-200 dark:border-gray-600">
|
||||
{task.task_key || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'title':
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between group pl-1 border-r border-gray-200 dark:border-gray-700"
|
||||
style={baseStyle}
|
||||
>
|
||||
{editTaskName ? (
|
||||
/* Full cell input when editing */
|
||||
<div className="flex-1" style={{ height: '38px' }} ref={wrapperRef}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
variant="borderless"
|
||||
value={taskName}
|
||||
onChange={(e) => setTaskName(e.target.value)}
|
||||
autoFocus
|
||||
onPressEnter={handleTaskNameSave}
|
||||
onBlur={handleTaskNameSave}
|
||||
className="text-sm"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '38px',
|
||||
margin: '0',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #1677ff',
|
||||
backgroundColor: 'rgba(22, 119, 255, 0.02)',
|
||||
borderRadius: '3px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '22px',
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none',
|
||||
boxShadow: '0 0 0 2px rgba(22, 119, 255, 0.1)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Normal layout when not editing */
|
||||
<>
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
{/* Indentation for subtasks - tighter spacing */}
|
||||
{isSubtask && <div className="w-4 flex-shrink-0" />}
|
||||
|
||||
{/* Expand/Collapse button - only show for parent tasks */}
|
||||
{!isSubtask && (
|
||||
<button
|
||||
onClick={handleToggleExpansion}
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${
|
||||
task.sub_tasks_count != null && task.sub_tasks_count > 0
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
transform: task.show_sub_tasks ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
transformOrigin: 'center'
|
||||
}}
|
||||
>
|
||||
<RightOutlined className="text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Additional indentation for subtasks after the expand button space */}
|
||||
{isSubtask && <div className="w-2 flex-shrink-0" />}
|
||||
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{/* Task name with dynamic width */}
|
||||
<div className="flex-1 min-w-0" ref={wrapperRef}>
|
||||
<span
|
||||
className="text-sm text-gray-700 dark:text-gray-300 truncate cursor-text block"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setEditTaskName(true);
|
||||
}}
|
||||
title={taskDisplayName}
|
||||
>
|
||||
{taskDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Indicators container - flex-shrink-0 to prevent compression */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Subtask count indicator - only show if count > 0 */}
|
||||
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && (
|
||||
<Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}>
|
||||
<div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-medium">
|
||||
{task.sub_tasks_count}
|
||||
</span>
|
||||
<DoubleRightOutlined className="text-blue-600 dark:text-blue-400" style={{ fontSize: 10 }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Task indicators - compact layout */}
|
||||
{task.comments_count != null && task.comments_count !== 0 && (
|
||||
<Tooltip title={t(`indicators.tooltips.comments${task.comments_count === 1 ? '' : '_plural'}`, { count: task.comments_count })}>
|
||||
<CommentOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.has_subscribers && (
|
||||
<Tooltip title={t('indicators.tooltips.subscribers')}>
|
||||
<EyeOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.attachments_count != null && task.attachments_count !== 0 && (
|
||||
<Tooltip title={t(`indicators.tooltips.attachments${task.attachments_count === 1 ? '' : '_plural'}`, { count: task.attachments_count })}>
|
||||
<PaperClipOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.has_dependencies && (
|
||||
<Tooltip title={t('indicators.tooltips.dependencies')}>
|
||||
<MinusCircleOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.schedule_id && (
|
||||
<Tooltip title={t('indicators.tooltips.recurring')}>
|
||||
<RetweetOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-all duration-200 ml-2 mr-2 px-3 py-1.5 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 cursor-pointer rounded-md shadow-sm hover:shadow-md flex items-center gap-1 flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setSelectedTaskId(task.id));
|
||||
dispatch(setShowTaskDrawer(true));
|
||||
}}
|
||||
>
|
||||
<ArrowsAltOutlined />
|
||||
{t('openButton')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'description':
|
||||
return (
|
||||
<div className="flex items-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<div
|
||||
className="text-sm text-gray-600 dark:text-gray-400 truncate w-full"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxHeight: '24px',
|
||||
lineHeight: '24px',
|
||||
}}
|
||||
title={task.description || ''}
|
||||
dangerouslySetInnerHTML={{ __html: task.description || '' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'status':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<TaskStatusDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'assignees':
|
||||
return (
|
||||
<div className="flex items-center gap-1 px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<AvatarGroup
|
||||
members={task.assignee_names || []}
|
||||
maxCount={3}
|
||||
isDarkMode={isDarkMode}
|
||||
size={24}
|
||||
/>
|
||||
<AssigneeSelector
|
||||
task={convertedTask}
|
||||
groupId={null}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'priority':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<TaskPriorityDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'dueDate':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 relative group border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{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={datePickerHandlers.clearDueDate}
|
||||
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 text-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
datePickerHandlers.setDueDate();
|
||||
}}
|
||||
>
|
||||
{formattedDates.due ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{formattedDates.due}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">
|
||||
{t('setDueDate')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'progress':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{task.progress !== undefined &&
|
||||
task.progress >= 0 &&
|
||||
(task.progress === 100 ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<CheckCircleOutlined
|
||||
className="text-green-500"
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
color: '#52c41a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<TaskProgress
|
||||
progress={task.progress}
|
||||
numberOfSubTasks={task.sub_tasks?.length || 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'labels':
|
||||
const labelsColumn = visibleColumns.find(col => col.id === 'labels');
|
||||
const labelsStyle = {
|
||||
...baseStyle,
|
||||
...(labelsColumn?.width === 'auto' ? { minWidth: '200px', flexGrow: 1 } : {})
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 flex-wrap min-w-0 px-2 border-r border-gray-200 dark:border-gray-700" style={labelsStyle}>
|
||||
<TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} />
|
||||
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'phase':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<TaskPhaseDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'timeTracking':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<TaskTimeTracking taskId={task.id || ''} isDarkMode={isDarkMode} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'estimation':
|
||||
// Use timeTracking.estimated which is the converted value from backend's total_minutes
|
||||
const estimationDisplay = (() => {
|
||||
const estimatedHours = task.timeTracking?.estimated;
|
||||
|
||||
if (estimatedHours && estimatedHours > 0) {
|
||||
// Convert decimal hours to hours and minutes for display
|
||||
const hours = Math.floor(estimatedHours);
|
||||
const minutes = Math.round((estimatedHours - hours) * 60);
|
||||
|
||||
if (hours > 0 && minutes > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{estimationDisplay ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{estimationDisplay}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500">
|
||||
-
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'startDate':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 relative group border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{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={datePickerHandlers.clearStartDate}
|
||||
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 text-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
datePickerHandlers.setStartDate();
|
||||
}}
|
||||
>
|
||||
{formattedDates.start ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{formattedDates.start}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">
|
||||
{t('setStartDate')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'completedDate':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{formattedDates.completed ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{formattedDates.completed}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">-</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'createdDate':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{formattedDates.created ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{formattedDates.created}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">-</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'lastUpdated':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{formattedDates.updated ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{formattedDates.updated}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">-</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'reporter':
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
{task.reporter ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{task.reporter}</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">-</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
// Handle custom columns
|
||||
const column = visibleColumns.find(col => col.id === columnId);
|
||||
if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) {
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
||||
<CustomColumnCell
|
||||
column={column}
|
||||
task={task}
|
||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}, [
|
||||
// Essential props and state
|
||||
attributes,
|
||||
listeners,
|
||||
isSelected,
|
||||
handleCheckboxChange,
|
||||
activeDatePicker,
|
||||
isDarkMode,
|
||||
projectId,
|
||||
|
||||
// Edit task name state - CRITICAL for re-rendering
|
||||
editTaskName,
|
||||
taskName,
|
||||
|
||||
// Task data - include specific fields that might update via socket
|
||||
task,
|
||||
task.labels, // Explicit dependency for labels updates
|
||||
task.phase, // Explicit dependency for phase updates
|
||||
task.comments_count, // Explicit dependency for comments count updates
|
||||
task.has_subscribers, // Explicit dependency for subscribers updates
|
||||
task.attachments_count, // Explicit dependency for attachments count updates
|
||||
task.has_dependencies, // Explicit dependency for dependencies updates
|
||||
task.schedule_id, // Explicit dependency for recurring task updates
|
||||
taskDisplayName,
|
||||
convertedTask,
|
||||
|
||||
// Memoized values
|
||||
dateValues,
|
||||
formattedDates,
|
||||
labelsAdapter,
|
||||
|
||||
// Handlers
|
||||
handleDateChange,
|
||||
datePickerHandlers,
|
||||
handleTaskNameSave,
|
||||
|
||||
// Translation
|
||||
t,
|
||||
|
||||
// Custom columns
|
||||
visibleColumns,
|
||||
updateTaskCustomColumnValue,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import React, { memo, useCallback, useState } from 'react';
|
||||
import { DatePicker, Tooltip } from 'antd';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DatePickerColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
field: 'dueDate' | 'startDate';
|
||||
formattedDate: string | null;
|
||||
dateValue: dayjs.Dayjs | undefined;
|
||||
isDarkMode: boolean;
|
||||
activeDatePicker: string | null;
|
||||
onActiveDatePickerChange: (field: string | null) => void;
|
||||
}
|
||||
|
||||
export const DatePickerColumn: React.FC<DatePickerColumnProps> = memo(({
|
||||
width,
|
||||
task,
|
||||
field,
|
||||
formattedDate,
|
||||
dateValue,
|
||||
isDarkMode,
|
||||
activeDatePicker,
|
||||
onActiveDatePickerChange
|
||||
}) => {
|
||||
const { socket, connected } = useSocket();
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// Handle date change
|
||||
const handleDateChange = useCallback(
|
||||
(date: dayjs.Dayjs | null) => {
|
||||
if (!connected || !socket) return;
|
||||
|
||||
const eventType =
|
||||
field === 'startDate'
|
||||
? SocketEvents.TASK_START_DATE_CHANGE
|
||||
: SocketEvents.TASK_END_DATE_CHANGE;
|
||||
const dateField = field === 'startDate' ? 'start_date' : 'end_date';
|
||||
|
||||
socket.emit(
|
||||
eventType.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
[dateField]: date?.format('YYYY-MM-DD'),
|
||||
parent_task: null,
|
||||
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
})
|
||||
);
|
||||
|
||||
// Close the date picker after selection
|
||||
onActiveDatePickerChange(null);
|
||||
},
|
||||
[connected, socket, task.id, field, onActiveDatePickerChange]
|
||||
);
|
||||
|
||||
// Handle clear date
|
||||
const handleClearDate = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDateChange(null);
|
||||
}, [handleDateChange]);
|
||||
|
||||
// Handle open date picker
|
||||
const handleOpenDatePicker = useCallback(() => {
|
||||
onActiveDatePickerChange(field);
|
||||
}, [field, onActiveDatePickerChange]);
|
||||
|
||||
const isActive = activeDatePicker === field;
|
||||
const placeholder = field === 'dueDate' ? t('dueDatePlaceholder') : t('startDatePlaceholder');
|
||||
const clearTitle = field === 'dueDate' ? t('clearDueDate') : t('clearStartDate');
|
||||
const setTitle = field === 'dueDate' ? t('setDueDate') : t('setStartDate');
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 relative group border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
{isActive ? (
|
||||
<div className="w-full relative">
|
||||
<DatePicker
|
||||
{...taskManagementAntdConfig.datePickerDefaults}
|
||||
className="w-full bg-transparent border-none shadow-none"
|
||||
value={dateValue}
|
||||
onChange={handleDateChange}
|
||||
placeholder={placeholder}
|
||||
allowClear={false}
|
||||
suffixIcon={null}
|
||||
open={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onActiveDatePickerChange(null);
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
{/* Custom clear button */}
|
||||
{dateValue && (
|
||||
<button
|
||||
onClick={handleClearDate}
|
||||
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={clearTitle}
|
||||
>
|
||||
<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 text-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenDatePicker();
|
||||
}}
|
||||
>
|
||||
{formattedDate ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{formattedDate}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">
|
||||
{setTitle}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
DatePickerColumn.displayName = 'DatePickerColumn';
|
||||
@@ -0,0 +1,404 @@
|
||||
import React, { memo, useCallback, useState, useRef } from 'react';
|
||||
import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons';
|
||||
import { Checkbox, DatePicker, Tooltip, Input } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||
import AssigneeSelector from '@/components/AssigneeSelector';
|
||||
import { format } from 'date-fns';
|
||||
import AvatarGroup from '../../AvatarGroup';
|
||||
import { DEFAULT_TASK_NAME } from '@/shared/constants';
|
||||
import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress';
|
||||
import TaskStatusDropdown from '@/components/task-management/task-status-dropdown';
|
||||
import TaskPriorityDropdown from '@/components/task-management/task-priority-dropdown';
|
||||
import TaskPhaseDropdown from '@/components/task-management/task-phase-dropdown';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TaskTimeTracking from '../TaskTimeTracking';
|
||||
import { CustomNumberLabel, CustomColordLabel } from '@/components';
|
||||
import LabelsSelector from '@/components/LabelsSelector';
|
||||
import { CustomColumnCell } from './CustomColumnComponents';
|
||||
|
||||
// Utility function to get task display name with fallbacks
|
||||
export const getTaskDisplayName = (task: Task): string => {
|
||||
if (task.title && task.title.trim()) return task.title.trim();
|
||||
if (task.name && task.name.trim()) return task.name.trim();
|
||||
if (task.task_key && task.task_key.trim()) return task.task_key.trim();
|
||||
return DEFAULT_TASK_NAME;
|
||||
};
|
||||
|
||||
// Memoized date formatter to avoid repeated date parsing
|
||||
export const formatDate = (dateString: string): string => {
|
||||
try {
|
||||
return format(new Date(dateString), 'MMM d, yyyy');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
interface TaskLabelsCellProps {
|
||||
labels: Task['labels'];
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const TaskLabelsCell: React.FC<TaskLabelsCellProps> = memo(({ labels, isDarkMode }) => {
|
||||
if (!labels) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 flex-wrap">
|
||||
{labels.map((label, index) => {
|
||||
const extendedLabel = label as any;
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TaskLabelsCell.displayName = 'TaskLabelsCell';
|
||||
|
||||
interface DragHandleColumnProps {
|
||||
width: string;
|
||||
isSubtask: boolean;
|
||||
attributes: any;
|
||||
listeners: any;
|
||||
}
|
||||
|
||||
export const DragHandleColumn: React.FC<DragHandleColumnProps> = memo(({ width, isSubtask, attributes, listeners }) => (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{ width }}
|
||||
{...(isSubtask ? {} : { ...attributes, ...listeners })}
|
||||
>
|
||||
{!isSubtask && <HolderOutlined className="text-gray-400 hover:text-gray-600" />}
|
||||
</div>
|
||||
));
|
||||
|
||||
DragHandleColumn.displayName = 'DragHandleColumn';
|
||||
|
||||
interface CheckboxColumnProps {
|
||||
width: string;
|
||||
isSelected: boolean;
|
||||
onCheckboxChange: (e: any) => void;
|
||||
}
|
||||
|
||||
export const CheckboxColumn: React.FC<CheckboxColumnProps> = memo(({ width, isSelected, onCheckboxChange }) => (
|
||||
<div className="flex items-center justify-center dark:border-gray-700" style={{ width }}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={onCheckboxChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CheckboxColumn.displayName = 'CheckboxColumn';
|
||||
|
||||
interface TaskKeyColumnProps {
|
||||
width: string;
|
||||
taskKey: string;
|
||||
}
|
||||
|
||||
export const TaskKeyColumn: React.FC<TaskKeyColumnProps> = memo(({ width, taskKey }) => (
|
||||
<div className="flex items-center pl-3 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<span className="text-xs font-medium px-2 py-1 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 whitespace-nowrap border border-gray-200 dark:border-gray-600">
|
||||
{taskKey || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
|
||||
TaskKeyColumn.displayName = 'TaskKeyColumn';
|
||||
|
||||
interface DescriptionColumnProps {
|
||||
width: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const DescriptionColumn: React.FC<DescriptionColumnProps> = memo(({ width, description }) => (
|
||||
<div className="flex items-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<div
|
||||
className="text-sm text-gray-600 dark:text-gray-400 truncate w-full"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxHeight: '24px',
|
||||
lineHeight: '24px',
|
||||
}}
|
||||
title={description || ''}
|
||||
dangerouslySetInnerHTML={{ __html: description || '' }}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
DescriptionColumn.displayName = 'DescriptionColumn';
|
||||
|
||||
interface StatusColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const StatusColumn: React.FC<StatusColumnProps> = memo(({ width, task, projectId, isDarkMode }) => (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<TaskStatusDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
StatusColumn.displayName = 'StatusColumn';
|
||||
|
||||
interface AssigneesColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
convertedTask: any;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const AssigneesColumn: React.FC<AssigneesColumnProps> = memo(({ width, task, convertedTask, isDarkMode }) => (
|
||||
<div className="flex items-center gap-1 px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<AvatarGroup
|
||||
members={task.assignee_names || []}
|
||||
maxCount={3}
|
||||
isDarkMode={isDarkMode}
|
||||
size={24}
|
||||
/>
|
||||
<AssigneeSelector
|
||||
task={convertedTask}
|
||||
groupId={null}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
AssigneesColumn.displayName = 'AssigneesColumn';
|
||||
|
||||
interface PriorityColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const PriorityColumn: React.FC<PriorityColumnProps> = memo(({ width, task, projectId, isDarkMode }) => (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<TaskPriorityDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
PriorityColumn.displayName = 'PriorityColumn';
|
||||
|
||||
interface ProgressColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
}
|
||||
|
||||
export const ProgressColumn: React.FC<ProgressColumnProps> = memo(({ width, task }) => (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
{task.progress !== undefined &&
|
||||
task.progress >= 0 &&
|
||||
(task.progress === 100 ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<CheckCircleOutlined
|
||||
className="text-green-500"
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
color: '#52c41a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<TaskProgress
|
||||
progress={task.progress}
|
||||
numberOfSubTasks={task.sub_tasks?.length || 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
|
||||
ProgressColumn.displayName = 'ProgressColumn';
|
||||
|
||||
interface LabelsColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
labelsAdapter: any;
|
||||
isDarkMode: boolean;
|
||||
visibleColumns: any[];
|
||||
}
|
||||
|
||||
export const LabelsColumn: React.FC<LabelsColumnProps> = memo(({ width, task, labelsAdapter, isDarkMode, visibleColumns }) => {
|
||||
const labelsColumn = visibleColumns.find(col => col.id === 'labels');
|
||||
const labelsStyle = {
|
||||
width,
|
||||
...(labelsColumn?.width === 'auto' ? { minWidth: '200px', flexGrow: 1 } : {})
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 flex-wrap min-w-0 px-2 border-r border-gray-200 dark:border-gray-700" style={labelsStyle}>
|
||||
<TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} />
|
||||
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
LabelsColumn.displayName = 'LabelsColumn';
|
||||
|
||||
interface PhaseColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const PhaseColumn: React.FC<PhaseColumnProps> = memo(({ width, task, projectId, isDarkMode }) => (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<TaskPhaseDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
PhaseColumn.displayName = 'PhaseColumn';
|
||||
|
||||
interface TimeTrackingColumnProps {
|
||||
width: string;
|
||||
taskId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const TimeTrackingColumn: React.FC<TimeTrackingColumnProps> = memo(({ width, taskId, isDarkMode }) => (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<TaskTimeTracking taskId={taskId} isDarkMode={isDarkMode} />
|
||||
</div>
|
||||
));
|
||||
|
||||
TimeTrackingColumn.displayName = 'TimeTrackingColumn';
|
||||
|
||||
interface EstimationColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
}
|
||||
|
||||
export const EstimationColumn: React.FC<EstimationColumnProps> = memo(({ width, task }) => {
|
||||
const estimationDisplay = (() => {
|
||||
const estimatedHours = task.timeTracking?.estimated;
|
||||
|
||||
if (estimatedHours && estimatedHours > 0) {
|
||||
const hours = Math.floor(estimatedHours);
|
||||
const minutes = Math.round((estimatedHours - hours) * 60);
|
||||
|
||||
if (hours > 0 && minutes > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
{estimationDisplay ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{estimationDisplay}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500">
|
||||
-
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
EstimationColumn.displayName = 'EstimationColumn';
|
||||
|
||||
interface DateColumnProps {
|
||||
width: string;
|
||||
formattedDate: string | null;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const DateColumn: React.FC<DateColumnProps> = memo(({ width, formattedDate, placeholder = '-' }) => (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
{formattedDate ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{formattedDate}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">{placeholder}</span>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
|
||||
DateColumn.displayName = 'DateColumn';
|
||||
|
||||
interface ReporterColumnProps {
|
||||
width: string;
|
||||
reporter: string;
|
||||
}
|
||||
|
||||
export const ReporterColumn: React.FC<ReporterColumnProps> = memo(({ width, reporter }) => (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
{reporter ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{reporter}</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap">-</span>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
|
||||
ReporterColumn.displayName = 'ReporterColumn';
|
||||
|
||||
interface CustomColumnProps {
|
||||
width: string;
|
||||
column: any;
|
||||
task: Task;
|
||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||
}
|
||||
|
||||
export const CustomColumn: React.FC<CustomColumnProps> = memo(({ width, column, task, updateTaskCustomColumnValue }) => {
|
||||
if (!updateTaskCustomColumnValue) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={{ width }}>
|
||||
<CustomColumnCell
|
||||
column={column}
|
||||
task={task}
|
||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CustomColumn.displayName = 'CustomColumn';
|
||||
@@ -0,0 +1,258 @@
|
||||
import React, { memo, useCallback, useState, useRef, useEffect } from 'react';
|
||||
import { RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons';
|
||||
import { Input, Tooltip } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { toggleTaskExpansion, fetchSubTasks } from '@/features/task-management/task-management.slice';
|
||||
import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTaskDisplayName } from './TaskRowColumns';
|
||||
|
||||
interface TitleColumnProps {
|
||||
width: string;
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isSubtask: boolean;
|
||||
taskDisplayName: string;
|
||||
editTaskName: boolean;
|
||||
taskName: string;
|
||||
onEditTaskName: (editing: boolean) => void;
|
||||
onTaskNameChange: (name: string) => void;
|
||||
onTaskNameSave: () => void;
|
||||
}
|
||||
|
||||
export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
||||
width,
|
||||
task,
|
||||
projectId,
|
||||
isSubtask,
|
||||
taskDisplayName,
|
||||
editTaskName,
|
||||
taskName,
|
||||
onEditTaskName,
|
||||
onTaskNameChange,
|
||||
onTaskNameSave
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { socket, connected } = useSocket();
|
||||
const { t } = useTranslation('task-list-table');
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle task expansion toggle
|
||||
const handleToggleExpansion = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Always try to fetch subtasks when expanding, regardless of count
|
||||
if (!task.show_sub_tasks && (!task.sub_tasks || task.sub_tasks.length === 0)) {
|
||||
dispatch(fetchSubTasks({ taskId: task.id, projectId }));
|
||||
}
|
||||
|
||||
// Toggle expansion state
|
||||
dispatch(toggleTaskExpansion(task.id));
|
||||
}, [dispatch, task.id, task.sub_tasks, task.show_sub_tasks, projectId]);
|
||||
|
||||
// Handle task name save
|
||||
const handleTaskNameSave = useCallback(() => {
|
||||
const newTaskName = inputRef.current?.input?.value || taskName;
|
||||
if (newTaskName?.trim() !== '' && connected && newTaskName.trim() !== (task.title || task.name || '').trim()) {
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_NAME_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
name: newTaskName.trim(),
|
||||
parent_task: task.parent_task_id,
|
||||
})
|
||||
);
|
||||
}
|
||||
onEditTaskName(false);
|
||||
}, [taskName, connected, socket, task.id, task.parent_task_id, task.title, task.name, onEditTaskName]);
|
||||
|
||||
// Handle click outside for task name editing
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
||||
handleTaskNameSave();
|
||||
}
|
||||
};
|
||||
|
||||
if (editTaskName) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [editTaskName, handleTaskNameSave]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between group pl-1 border-r border-gray-200 dark:border-gray-700"
|
||||
style={{ width }}
|
||||
>
|
||||
{editTaskName ? (
|
||||
/* Full cell input when editing */
|
||||
<div className="flex-1" style={{ height: '38px' }} ref={wrapperRef}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
variant="borderless"
|
||||
value={taskName}
|
||||
onChange={(e) => onTaskNameChange(e.target.value)}
|
||||
autoFocus
|
||||
onPressEnter={handleTaskNameSave}
|
||||
onBlur={handleTaskNameSave}
|
||||
className="text-sm"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '38px',
|
||||
margin: '0',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #1677ff',
|
||||
backgroundColor: 'rgba(22, 119, 255, 0.02)',
|
||||
borderRadius: '3px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '22px',
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none',
|
||||
boxShadow: '0 0 0 2px rgba(22, 119, 255, 0.1)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Normal layout when not editing */
|
||||
<>
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
{/* Indentation for subtasks - tighter spacing */}
|
||||
{isSubtask && <div className="w-4 flex-shrink-0" />}
|
||||
|
||||
{/* Expand/Collapse button - only show for parent tasks */}
|
||||
{!isSubtask && (
|
||||
<button
|
||||
onClick={handleToggleExpansion}
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${
|
||||
task.sub_tasks_count != null && task.sub_tasks_count > 0
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
transform: task.show_sub_tasks ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
transformOrigin: 'center'
|
||||
}}
|
||||
>
|
||||
<RightOutlined className="text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Additional indentation for subtasks after the expand button space */}
|
||||
{isSubtask && <div className="w-2 flex-shrink-0" />}
|
||||
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{/* Task name with dynamic width */}
|
||||
<div className="flex-1 min-w-0" ref={wrapperRef}>
|
||||
<span
|
||||
className="text-sm text-gray-700 dark:text-gray-300 truncate cursor-text block"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onEditTaskName(true);
|
||||
}}
|
||||
title={taskDisplayName}
|
||||
>
|
||||
{taskDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Indicators container - flex-shrink-0 to prevent compression */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Subtask count indicator - only show if count > 0 */}
|
||||
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && (
|
||||
<Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}>
|
||||
<div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-medium">
|
||||
{task.sub_tasks_count}
|
||||
</span>
|
||||
<DoubleRightOutlined className="text-blue-600 dark:text-blue-400" style={{ fontSize: 10 }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Task indicators - compact layout */}
|
||||
{task.comments_count != null && task.comments_count !== 0 && (
|
||||
<Tooltip title={t(`indicators.tooltips.comments${task.comments_count === 1 ? '' : '_plural'}`, { count: task.comments_count })}>
|
||||
<CommentOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.has_subscribers && (
|
||||
<Tooltip title={t('indicators.tooltips.subscribers')}>
|
||||
<EyeOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.attachments_count != null && task.attachments_count !== 0 && (
|
||||
<Tooltip title={t(`indicators.tooltips.attachments${task.attachments_count === 1 ? '' : '_plural'}`, { count: task.attachments_count })}>
|
||||
<PaperClipOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.has_dependencies && (
|
||||
<Tooltip title={t('indicators.tooltips.dependencies')}>
|
||||
<MinusCircleOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{task.schedule_id && (
|
||||
<Tooltip title={t('indicators.tooltips.recurring')}>
|
||||
<RetweetOutlined
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-all duration-200 ml-2 mr-2 px-3 py-1.5 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 cursor-pointer rounded-md shadow-sm hover:shadow-md flex items-center gap-1 flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setSelectedTaskId(task.id));
|
||||
dispatch(setShowTaskDrawer(true));
|
||||
}}
|
||||
>
|
||||
<ArrowsAltOutlined />
|
||||
{t('openButton')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TitleColumn.displayName = 'TitleColumn';
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { toggleTaskSelection } from '@/features/task-management/selection.slice';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
|
||||
interface UseTaskRowActionsProps {
|
||||
task: Task;
|
||||
taskId: string;
|
||||
taskName: string;
|
||||
editTaskName: boolean;
|
||||
setEditTaskName: (editing: boolean) => void;
|
||||
}
|
||||
|
||||
export const useTaskRowActions = ({
|
||||
task,
|
||||
taskId,
|
||||
taskName,
|
||||
editTaskName,
|
||||
setEditTaskName,
|
||||
}: UseTaskRowActionsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { socket, connected } = useSocket();
|
||||
|
||||
// Handle checkbox change
|
||||
const handleCheckboxChange = useCallback((e: any) => {
|
||||
e.stopPropagation(); // Prevent row click when clicking checkbox
|
||||
dispatch(toggleTaskSelection(taskId));
|
||||
}, [dispatch, taskId]);
|
||||
|
||||
// Handle task name save
|
||||
const handleTaskNameSave = useCallback(() => {
|
||||
if (taskName?.trim() !== '' && connected && taskName.trim() !== (task.title || task.name || '').trim()) {
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_NAME_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
name: taskName.trim(),
|
||||
parent_task: task.parent_task_id,
|
||||
})
|
||||
);
|
||||
}
|
||||
setEditTaskName(false);
|
||||
}, [taskName, connected, socket, task.id, task.parent_task_id, task.title, task.name, setEditTaskName]);
|
||||
|
||||
// Handle task name edit start
|
||||
const handleTaskNameEdit = useCallback(() => {
|
||||
setEditTaskName(true);
|
||||
}, [setEditTaskName]);
|
||||
|
||||
// Handle task name change
|
||||
const handleTaskNameChange = useCallback((name: string) => {
|
||||
// This will be handled by the parent component's state setter
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleCheckboxChange,
|
||||
handleTaskNameSave,
|
||||
handleTaskNameEdit,
|
||||
handleTaskNameChange,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,320 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import {
|
||||
DragHandleColumn,
|
||||
CheckboxColumn,
|
||||
TaskKeyColumn,
|
||||
DescriptionColumn,
|
||||
StatusColumn,
|
||||
AssigneesColumn,
|
||||
PriorityColumn,
|
||||
ProgressColumn,
|
||||
LabelsColumn,
|
||||
PhaseColumn,
|
||||
TimeTrackingColumn,
|
||||
EstimationColumn,
|
||||
DateColumn,
|
||||
ReporterColumn,
|
||||
CustomColumn,
|
||||
} from '../components/TaskRowColumns';
|
||||
import { TitleColumn } from '../components/TitleColumn';
|
||||
import { DatePickerColumn } from '../components/DatePickerColumn';
|
||||
|
||||
interface UseTaskRowColumnsProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isSubtask: boolean;
|
||||
isSelected: boolean;
|
||||
isDarkMode: boolean;
|
||||
visibleColumns: Array<{
|
||||
id: string;
|
||||
width: string;
|
||||
isSticky?: boolean;
|
||||
key?: string;
|
||||
custom_column?: boolean;
|
||||
custom_column_obj?: any;
|
||||
isCustom?: boolean;
|
||||
}>;
|
||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||
|
||||
// From useTaskRowState
|
||||
taskDisplayName: string;
|
||||
convertedTask: any;
|
||||
formattedDates: any;
|
||||
dateValues: any;
|
||||
labelsAdapter: any;
|
||||
activeDatePicker: string | null;
|
||||
setActiveDatePicker: (field: string | null) => void;
|
||||
editTaskName: boolean;
|
||||
taskName: string;
|
||||
setEditTaskName: (editing: boolean) => void;
|
||||
setTaskName: (name: string) => void;
|
||||
|
||||
// From useTaskRowActions
|
||||
handleCheckboxChange: (e: any) => void;
|
||||
handleTaskNameSave: () => void;
|
||||
handleTaskNameEdit: () => void;
|
||||
|
||||
// Drag and drop
|
||||
attributes: any;
|
||||
listeners: any;
|
||||
}
|
||||
|
||||
export const useTaskRowColumns = ({
|
||||
task,
|
||||
projectId,
|
||||
isSubtask,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
visibleColumns,
|
||||
updateTaskCustomColumnValue,
|
||||
taskDisplayName,
|
||||
convertedTask,
|
||||
formattedDates,
|
||||
dateValues,
|
||||
labelsAdapter,
|
||||
activeDatePicker,
|
||||
setActiveDatePicker,
|
||||
editTaskName,
|
||||
taskName,
|
||||
setEditTaskName,
|
||||
setTaskName,
|
||||
handleCheckboxChange,
|
||||
handleTaskNameSave,
|
||||
handleTaskNameEdit,
|
||||
attributes,
|
||||
listeners,
|
||||
}: UseTaskRowColumnsProps) => {
|
||||
|
||||
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
||||
switch (columnId) {
|
||||
case 'dragHandle':
|
||||
return (
|
||||
<DragHandleColumn
|
||||
width={width}
|
||||
isSubtask={isSubtask}
|
||||
attributes={attributes}
|
||||
listeners={listeners}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<CheckboxColumn
|
||||
width={width}
|
||||
isSelected={isSelected}
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'taskKey':
|
||||
return (
|
||||
<TaskKeyColumn
|
||||
width={width}
|
||||
taskKey={task.task_key || ''}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'title':
|
||||
return (
|
||||
<TitleColumn
|
||||
width={width}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isSubtask={isSubtask}
|
||||
taskDisplayName={taskDisplayName}
|
||||
editTaskName={editTaskName}
|
||||
taskName={taskName}
|
||||
onEditTaskName={setEditTaskName}
|
||||
onTaskNameChange={setTaskName}
|
||||
onTaskNameSave={handleTaskNameSave}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'description':
|
||||
return (
|
||||
<DescriptionColumn
|
||||
width={width}
|
||||
description={task.description || ''}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'status':
|
||||
return (
|
||||
<StatusColumn
|
||||
width={width}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'assignees':
|
||||
return (
|
||||
<AssigneesColumn
|
||||
width={width}
|
||||
task={task}
|
||||
convertedTask={convertedTask}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'priority':
|
||||
return (
|
||||
<PriorityColumn
|
||||
width={width}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'dueDate':
|
||||
return (
|
||||
<DatePickerColumn
|
||||
width={width}
|
||||
task={task}
|
||||
field="dueDate"
|
||||
formattedDate={formattedDates.due}
|
||||
dateValue={dateValues.due}
|
||||
isDarkMode={isDarkMode}
|
||||
activeDatePicker={activeDatePicker}
|
||||
onActiveDatePickerChange={setActiveDatePicker}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'startDate':
|
||||
return (
|
||||
<DatePickerColumn
|
||||
width={width}
|
||||
task={task}
|
||||
field="startDate"
|
||||
formattedDate={formattedDates.start}
|
||||
dateValue={dateValues.start}
|
||||
isDarkMode={isDarkMode}
|
||||
activeDatePicker={activeDatePicker}
|
||||
onActiveDatePickerChange={setActiveDatePicker}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'progress':
|
||||
return (
|
||||
<ProgressColumn
|
||||
width={width}
|
||||
task={task}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'labels':
|
||||
return (
|
||||
<LabelsColumn
|
||||
width={width}
|
||||
task={task}
|
||||
labelsAdapter={labelsAdapter}
|
||||
isDarkMode={isDarkMode}
|
||||
visibleColumns={visibleColumns}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'phase':
|
||||
return (
|
||||
<PhaseColumn
|
||||
width={width}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'timeTracking':
|
||||
return (
|
||||
<TimeTrackingColumn
|
||||
width={width}
|
||||
taskId={task.id || ''}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'estimation':
|
||||
return (
|
||||
<EstimationColumn
|
||||
width={width}
|
||||
task={task}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'completedDate':
|
||||
return (
|
||||
<DateColumn
|
||||
width={width}
|
||||
formattedDate={formattedDates.completed}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'createdDate':
|
||||
return (
|
||||
<DateColumn
|
||||
width={width}
|
||||
formattedDate={formattedDates.created}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'lastUpdated':
|
||||
return (
|
||||
<DateColumn
|
||||
width={width}
|
||||
formattedDate={formattedDates.updated}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'reporter':
|
||||
return (
|
||||
<ReporterColumn
|
||||
width={width}
|
||||
reporter={task.reporter || ''}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
// Handle custom columns
|
||||
const column = visibleColumns.find(col => col.id === columnId);
|
||||
if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) {
|
||||
return (
|
||||
<CustomColumn
|
||||
width={width}
|
||||
column={column}
|
||||
task={task}
|
||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}, [
|
||||
task,
|
||||
projectId,
|
||||
isSubtask,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
visibleColumns,
|
||||
updateTaskCustomColumnValue,
|
||||
taskDisplayName,
|
||||
convertedTask,
|
||||
formattedDates,
|
||||
dateValues,
|
||||
labelsAdapter,
|
||||
activeDatePicker,
|
||||
setActiveDatePicker,
|
||||
editTaskName,
|
||||
taskName,
|
||||
setEditTaskName,
|
||||
setTaskName,
|
||||
handleCheckboxChange,
|
||||
handleTaskNameSave,
|
||||
handleTaskNameEdit,
|
||||
attributes,
|
||||
listeners,
|
||||
]);
|
||||
|
||||
return { renderColumn };
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||
import { dayjs } from '@/shared/antd-imports';
|
||||
import { getTaskDisplayName, formatDate } from '../components/TaskRowColumns';
|
||||
|
||||
export const useTaskRowState = (task: Task) => {
|
||||
// State for tracking which date picker is open
|
||||
const [activeDatePicker, setActiveDatePicker] = useState<string | null>(null);
|
||||
|
||||
// State for editing task name
|
||||
const [editTaskName, setEditTaskName] = useState(false);
|
||||
const [taskName, setTaskName] = useState(task.title || task.name || '');
|
||||
|
||||
// Update local taskName state when task name changes
|
||||
useEffect(() => {
|
||||
setTaskName(task.title || task.name || '');
|
||||
}, [task.title, task.name]);
|
||||
|
||||
// Memoize task display name
|
||||
const taskDisplayName = useMemo(() => getTaskDisplayName(task), [task.title, task.name, task.task_key]);
|
||||
|
||||
// Memoize converted task for AssigneeSelector to prevent recreation
|
||||
const convertedTask = useMemo(() => ({
|
||||
id: task.id,
|
||||
name: taskDisplayName,
|
||||
task_key: task.task_key || taskDisplayName,
|
||||
assignees:
|
||||
task.assignee_names?.map((assignee: InlineMember, index: number) => ({
|
||||
team_member_id: assignee.team_member_id || `assignee-${index}`,
|
||||
id: assignee.team_member_id || `assignee-${index}`,
|
||||
project_member_id: assignee.team_member_id || `assignee-${index}`,
|
||||
name: assignee.name || '',
|
||||
})) || [],
|
||||
parent_task_id: task.parent_task_id,
|
||||
status_id: undefined,
|
||||
project_id: undefined,
|
||||
manual_progress: undefined,
|
||||
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
|
||||
|
||||
// Memoize formatted dates
|
||||
const formattedDates = useMemo(() => ({
|
||||
due: (() => {
|
||||
const dateValue = task.dueDate || task.due_date;
|
||||
return dateValue ? formatDate(dateValue) : null;
|
||||
})(),
|
||||
start: task.startDate ? formatDate(task.startDate) : null,
|
||||
completed: task.completedAt ? formatDate(task.completedAt) : null,
|
||||
created: (task.createdAt || task.created_at) ? formatDate(task.createdAt || task.created_at) : null,
|
||||
updated: task.updatedAt ? formatDate(task.updatedAt) : null,
|
||||
}), [task.dueDate, task.due_date, task.startDate, task.completedAt, task.createdAt, task.created_at, task.updatedAt]);
|
||||
|
||||
// Memoize date values for DatePicker
|
||||
const dateValues = useMemo(
|
||||
() => ({
|
||||
start: task.startDate ? dayjs(task.startDate) : undefined,
|
||||
due: (task.dueDate || task.due_date) ? dayjs(task.dueDate || task.due_date) : undefined,
|
||||
}),
|
||||
[task.startDate, task.dueDate, task.due_date]
|
||||
);
|
||||
|
||||
// Create labels adapter for LabelsSelector
|
||||
const labelsAdapter = useMemo(() => ({
|
||||
id: task.id,
|
||||
name: task.title || task.name,
|
||||
parent_task_id: task.parent_task_id,
|
||||
manual_progress: false,
|
||||
all_labels: task.all_labels?.map(label => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color_code: label.color_code,
|
||||
})) || [],
|
||||
labels: task.labels?.map(label => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color_code: label.color,
|
||||
})) || [],
|
||||
}), [task.id, task.title, task.name, task.parent_task_id, task.all_labels, task.labels, task.all_labels?.length, task.labels?.length]);
|
||||
|
||||
return {
|
||||
// State
|
||||
activeDatePicker,
|
||||
setActiveDatePicker,
|
||||
editTaskName,
|
||||
setEditTaskName,
|
||||
taskName,
|
||||
setTaskName,
|
||||
|
||||
// Computed values
|
||||
taskDisplayName,
|
||||
convertedTask,
|
||||
formattedDates,
|
||||
dateValues,
|
||||
labelsAdapter,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { dayjs } from '@/shared/antd-imports';
|
||||
|
||||
export interface TaskRowColumn {
|
||||
id: string;
|
||||
width: string;
|
||||
isSticky?: boolean;
|
||||
key?: string;
|
||||
custom_column?: boolean;
|
||||
custom_column_obj?: any;
|
||||
isCustom?: boolean;
|
||||
}
|
||||
|
||||
export interface TaskRowProps {
|
||||
taskId: string;
|
||||
projectId: string;
|
||||
visibleColumns: TaskRowColumn[];
|
||||
isSubtask?: boolean;
|
||||
isFirstInGroup?: boolean;
|
||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||
}
|
||||
|
||||
export interface TaskRowState {
|
||||
activeDatePicker: string | null;
|
||||
editTaskName: boolean;
|
||||
taskName: string;
|
||||
}
|
||||
|
||||
export interface TaskRowComputedValues {
|
||||
taskDisplayName: string;
|
||||
convertedTask: ConvertedTask;
|
||||
formattedDates: FormattedDates;
|
||||
dateValues: DateValues;
|
||||
labelsAdapter: LabelsAdapter;
|
||||
}
|
||||
|
||||
export interface ConvertedTask {
|
||||
id: string;
|
||||
name: string;
|
||||
task_key: string;
|
||||
assignees: ConvertedAssignee[];
|
||||
parent_task_id?: string;
|
||||
status_id?: string;
|
||||
project_id?: string;
|
||||
manual_progress?: boolean;
|
||||
}
|
||||
|
||||
export interface ConvertedAssignee {
|
||||
team_member_id: string;
|
||||
id: string;
|
||||
project_member_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FormattedDates {
|
||||
due: string | null;
|
||||
start: string | null;
|
||||
completed: string | null;
|
||||
created: string | null;
|
||||
updated: string | null;
|
||||
}
|
||||
|
||||
export interface DateValues {
|
||||
start: dayjs.Dayjs | undefined;
|
||||
due: dayjs.Dayjs | undefined;
|
||||
}
|
||||
|
||||
export interface LabelsAdapter {
|
||||
id: string;
|
||||
name: string;
|
||||
parent_task_id?: string;
|
||||
manual_progress: boolean;
|
||||
all_labels: LabelInfo[];
|
||||
labels: LabelInfo[];
|
||||
}
|
||||
|
||||
export interface LabelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
color_code: string;
|
||||
}
|
||||
|
||||
export interface TaskRowActions {
|
||||
handleCheckboxChange: (e: any) => void;
|
||||
handleTaskNameSave: () => void;
|
||||
handleTaskNameEdit: () => void;
|
||||
handleTaskNameChange: (name: string) => void;
|
||||
}
|
||||
|
||||
export interface ColumnRendererProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isSubtask: boolean;
|
||||
isSelected: boolean;
|
||||
isDarkMode: boolean;
|
||||
visibleColumns: TaskRowColumn[];
|
||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||
taskDisplayName: string;
|
||||
convertedTask: ConvertedTask;
|
||||
formattedDates: FormattedDates;
|
||||
dateValues: DateValues;
|
||||
labelsAdapter: LabelsAdapter;
|
||||
activeDatePicker: string | null;
|
||||
setActiveDatePicker: (field: string | null) => void;
|
||||
editTaskName: boolean;
|
||||
taskName: string;
|
||||
setEditTaskName: (editing: boolean) => void;
|
||||
setTaskName: (name: string) => void;
|
||||
handleCheckboxChange: (e: any) => void;
|
||||
handleTaskNameSave: () => void;
|
||||
handleTaskNameEdit: () => void;
|
||||
attributes: any;
|
||||
listeners: any;
|
||||
}
|
||||
|
||||
export type ColumnId =
|
||||
| 'dragHandle'
|
||||
| 'checkbox'
|
||||
| 'taskKey'
|
||||
| 'title'
|
||||
| 'description'
|
||||
| 'status'
|
||||
| 'assignees'
|
||||
| 'priority'
|
||||
| 'dueDate'
|
||||
| 'startDate'
|
||||
| 'progress'
|
||||
| 'labels'
|
||||
| 'phase'
|
||||
| 'timeTracking'
|
||||
| 'estimation'
|
||||
| 'completedDate'
|
||||
| 'createdDate'
|
||||
| 'lastUpdated'
|
||||
| 'reporter'
|
||||
| string; // Allow custom column IDs
|
||||
|
||||
export interface BaseColumnProps {
|
||||
width: string;
|
||||
}
|
||||
|
||||
export interface DragHandleColumnProps extends BaseColumnProps {
|
||||
isSubtask: boolean;
|
||||
attributes: any;
|
||||
listeners: any;
|
||||
}
|
||||
|
||||
export interface CheckboxColumnProps extends BaseColumnProps {
|
||||
isSelected: boolean;
|
||||
onCheckboxChange: (e: any) => void;
|
||||
}
|
||||
|
||||
export interface TaskKeyColumnProps extends BaseColumnProps {
|
||||
taskKey: string;
|
||||
}
|
||||
|
||||
export interface TitleColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isSubtask: boolean;
|
||||
taskDisplayName: string;
|
||||
editTaskName: boolean;
|
||||
taskName: string;
|
||||
onEditTaskName: (editing: boolean) => void;
|
||||
onTaskNameChange: (name: string) => void;
|
||||
onTaskNameSave: () => void;
|
||||
}
|
||||
|
||||
export interface DescriptionColumnProps extends BaseColumnProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface StatusColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export interface AssigneesColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
convertedTask: ConvertedTask;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export interface PriorityColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export interface DatePickerColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
field: 'dueDate' | 'startDate';
|
||||
formattedDate: string | null;
|
||||
dateValue: dayjs.Dayjs | undefined;
|
||||
isDarkMode: boolean;
|
||||
activeDatePicker: string | null;
|
||||
onActiveDatePickerChange: (field: string | null) => void;
|
||||
}
|
||||
|
||||
export interface ProgressColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
}
|
||||
|
||||
export interface LabelsColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
labelsAdapter: LabelsAdapter;
|
||||
isDarkMode: boolean;
|
||||
visibleColumns: TaskRowColumn[];
|
||||
}
|
||||
|
||||
export interface PhaseColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export interface TimeTrackingColumnProps extends BaseColumnProps {
|
||||
taskId: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export interface EstimationColumnProps extends BaseColumnProps {
|
||||
task: Task;
|
||||
}
|
||||
|
||||
export interface DateColumnProps extends BaseColumnProps {
|
||||
formattedDate: string | null;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface ReporterColumnProps extends BaseColumnProps {
|
||||
reporter: string;
|
||||
}
|
||||
|
||||
export interface CustomColumnProps extends BaseColumnProps {
|
||||
column: TaskRowColumn;
|
||||
task: Task;
|
||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||
}
|
||||
|
||||
export interface TaskLabelsCellProps {
|
||||
labels: Task['labels'];
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user