refactor(task-list): enhance task rendering and editing functionality in TaskRow and TaskListV2Table
- Updated TaskListV2Table to pass isFirstInGroup prop to renderTask for improved task grouping logic. - Enhanced TaskRow to support inline editing of task names with a new input field and associated state management. - Implemented click outside detection to save task name changes when editing is complete. - Improved layout and styling for better user experience during task editing and display.
This commit is contained in:
@@ -457,7 +457,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renderTask = useCallback(
|
const renderTask = useCallback(
|
||||||
(taskIndex: number) => {
|
(taskIndex: number, isFirstInGroup: boolean = false) => {
|
||||||
const item = virtuosoItems[taskIndex];
|
const item = virtuosoItems[taskIndex];
|
||||||
|
|
||||||
if (!item || !urlProjectId) return null;
|
if (!item || !urlProjectId) return null;
|
||||||
@@ -480,6 +480,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
taskId={item.id}
|
taskId={item.id}
|
||||||
projectId={urlProjectId}
|
projectId={urlProjectId}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
|
isFirstInGroup={isFirstInGroup}
|
||||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -647,9 +648,12 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
virtuosoGroups.slice(0, groupIndex).reduce((sum, g) => sum + g.count, 0) +
|
virtuosoGroups.slice(0, groupIndex).reduce((sum, g) => sum + g.count, 0) +
|
||||||
taskIndex;
|
taskIndex;
|
||||||
|
|
||||||
|
// Check if this is the first actual task in the group (not AddTaskRow)
|
||||||
|
const isFirstTaskInGroup = taskIndex === 0 && !('isAddTaskRow' in task);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={task.id || `add-task-${group.id}-${taskIndex}`}>
|
<div key={task.id || `add-task-${group.id}-${taskIndex}`}>
|
||||||
{renderTask(globalTaskIndex)}
|
{renderTask(globalTaskIndex, isFirstTaskInGroup)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { memo, useMemo, useCallback, useState } from 'react';
|
import React, { memo, useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons';
|
||||||
import { Checkbox, DatePicker, Tooltip } from 'antd';
|
import { Checkbox, DatePicker, Tooltip, Input } from 'antd';
|
||||||
|
import type { InputRef } from 'antd';
|
||||||
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||||
@@ -40,6 +41,7 @@ interface TaskRowProps {
|
|||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
}>;
|
}>;
|
||||||
isSubtask?: boolean;
|
isSubtask?: boolean;
|
||||||
|
isFirstInGroup?: boolean;
|
||||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ const formatDate = (dateString: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns, isSubtask = false, updateTaskCustomColumnValue }) => {
|
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns, isSubtask = false, isFirstInGroup = false, updateTaskCustomColumnValue }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
||||||
@@ -107,6 +109,12 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
// State for tracking which date picker is open
|
// State for tracking which date picker is open
|
||||||
const [activeDatePicker, setActiveDatePicker] = useState<string | null>(null);
|
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) {
|
if (!task) {
|
||||||
return null; // Don't render if task is not found in store
|
return null; // Don't render if task is not found in store
|
||||||
}
|
}
|
||||||
@@ -153,6 +161,45 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
manual_progress: undefined,
|
manual_progress: undefined,
|
||||||
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
|
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
|
||||||
|
|
||||||
|
// 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
|
// Memoize formatted dates
|
||||||
const formattedDates = useMemo(() => ({
|
const formattedDates = useMemo(() => ({
|
||||||
due: (() => {
|
due: (() => {
|
||||||
@@ -291,7 +338,41 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
|
|
||||||
case 'title':
|
case 'title':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between group pl-1 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
|
<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">
|
<div className="flex items-center flex-1 min-w-0">
|
||||||
{/* Indentation for subtasks - tighter spacing */}
|
{/* Indentation for subtasks - tighter spacing */}
|
||||||
{isSubtask && <div className="w-4 flex-shrink-0" />}
|
{isSubtask && <div className="w-4 flex-shrink-0" />}
|
||||||
@@ -323,19 +404,23 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
|
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
{/* Task name with dynamic width */}
|
{/* Task name with dynamic width */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0" ref={wrapperRef}>
|
||||||
<Tooltip title={taskDisplayName}>
|
|
||||||
<span
|
<span
|
||||||
className="text-sm text-gray-700 dark:text-gray-300 truncate cursor-pointer block"
|
className="text-sm text-gray-700 dark:text-gray-300 truncate cursor-text block"
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
}}
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setEditTaskName(true);
|
||||||
|
}}
|
||||||
|
title={taskDisplayName}
|
||||||
>
|
>
|
||||||
{taskDisplayName}
|
{taskDisplayName}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Indicators container - flex-shrink-0 to prevent compression */}
|
{/* Indicators container - flex-shrink-0 to prevent compression */}
|
||||||
@@ -412,6 +497,8 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
<ArrowsAltOutlined />
|
<ArrowsAltOutlined />
|
||||||
{t('openButton')}
|
{t('openButton')}
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -755,6 +842,10 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
isDarkMode,
|
isDarkMode,
|
||||||
projectId,
|
projectId,
|
||||||
|
|
||||||
|
// Edit task name state - CRITICAL for re-rendering
|
||||||
|
editTaskName,
|
||||||
|
taskName,
|
||||||
|
|
||||||
// Task data - include specific fields that might update via socket
|
// Task data - include specific fields that might update via socket
|
||||||
task,
|
task,
|
||||||
task.labels, // Explicit dependency for labels updates
|
task.labels, // Explicit dependency for labels updates
|
||||||
@@ -775,6 +866,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
// Handlers
|
// Handlers
|
||||||
handleDateChange,
|
handleDateChange,
|
||||||
datePickerHandlers,
|
datePickerHandlers,
|
||||||
|
handleTaskNameSave,
|
||||||
|
|
||||||
// Translation
|
// Translation
|
||||||
t,
|
t,
|
||||||
@@ -787,8 +879,10 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={{ ...style, height: '40px' }}
|
||||||
className={`flex items-center min-w-max px-1 py-2 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 ${
|
className={`flex items-center min-w-max px-1 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 ${
|
||||||
|
isFirstInGroup ? 'border-t border-gray-200 dark:border-gray-700' : ''
|
||||||
|
} ${
|
||||||
isDragging ? 'shadow-lg border border-blue-300' : ''
|
isDragging ? 'shadow-lg border border-blue-300' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface TaskRowWithSubtasksProps {
|
|||||||
width: string;
|
width: string;
|
||||||
isSticky?: boolean;
|
isSticky?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
isFirstInGroup?: boolean;
|
||||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +154,7 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
taskId,
|
taskId,
|
||||||
projectId,
|
projectId,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
|
isFirstInGroup = false,
|
||||||
updateTaskCustomColumnValue
|
updateTaskCustomColumnValue
|
||||||
}) => {
|
}) => {
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
@@ -175,6 +177,7 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
taskId={taskId}
|
taskId={taskId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
|
isFirstInGroup={isFirstInGroup}
|
||||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user