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:
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user