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(
|
||||
(taskIndex: number) => {
|
||||
(taskIndex: number, isFirstInGroup: boolean = false) => {
|
||||
const item = virtuosoItems[taskIndex];
|
||||
|
||||
if (!item || !urlProjectId) return null;
|
||||
@@ -480,6 +480,7 @@ const TaskListV2Section: React.FC = () => {
|
||||
taskId={item.id}
|
||||
projectId={urlProjectId}
|
||||
visibleColumns={visibleColumns}
|
||||
isFirstInGroup={isFirstInGroup}
|
||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||
/>
|
||||
);
|
||||
@@ -647,9 +648,12 @@ const TaskListV2Section: React.FC = () => {
|
||||
virtuosoGroups.slice(0, groupIndex).reduce((sum, g) => sum + g.count, 0) +
|
||||
taskIndex;
|
||||
|
||||
// Check if this is the first actual task in the group (not AddTaskRow)
|
||||
const isFirstTaskInGroup = taskIndex === 0 && !('isAddTaskRow' in task);
|
||||
|
||||
return (
|
||||
<div key={task.id || `add-task-${group.id}-${taskIndex}`}>
|
||||
{renderTask(globalTaskIndex)}
|
||||
{renderTask(globalTaskIndex, isFirstTaskInGroup)}
|
||||
</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 { 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 } from 'antd';
|
||||
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';
|
||||
@@ -40,6 +41,7 @@ interface TaskRowProps {
|
||||
isCustom?: boolean;
|
||||
}>;
|
||||
isSubtask?: boolean;
|
||||
isFirstInGroup?: boolean;
|
||||
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 task = useAppSelector(state => selectTaskById(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
|
||||
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
|
||||
}
|
||||
@@ -153,6 +161,45 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
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: (() => {
|
||||
@@ -291,7 +338,41 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
|
||||
case 'title':
|
||||
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">
|
||||
{/* Indentation for subtasks - tighter spacing */}
|
||||
{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">
|
||||
{/* Task name with dynamic width */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Tooltip title={taskDisplayName}>
|
||||
<div className="flex-1 min-w-0" ref={wrapperRef}>
|
||||
<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={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setEditTaskName(true);
|
||||
}}
|
||||
title={taskDisplayName}
|
||||
>
|
||||
{taskDisplayName}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Indicators container - flex-shrink-0 to prevent compression */}
|
||||
@@ -412,6 +497,8 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
<ArrowsAltOutlined />
|
||||
{t('openButton')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -755,6 +842,10 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
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
|
||||
@@ -775,6 +866,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
// Handlers
|
||||
handleDateChange,
|
||||
datePickerHandlers,
|
||||
handleTaskNameSave,
|
||||
|
||||
// Translation
|
||||
t,
|
||||
@@ -787,8 +879,10 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
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 ${
|
||||
style={{ ...style, height: '40px' }}
|
||||
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' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -20,6 +20,7 @@ interface TaskRowWithSubtasksProps {
|
||||
width: string;
|
||||
isSticky?: boolean;
|
||||
}>;
|
||||
isFirstInGroup?: boolean;
|
||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||
}
|
||||
|
||||
@@ -153,6 +154,7 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
||||
taskId,
|
||||
projectId,
|
||||
visibleColumns,
|
||||
isFirstInGroup = false,
|
||||
updateTaskCustomColumnValue
|
||||
}) => {
|
||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||
@@ -175,6 +177,7 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
||||
taskId={taskId}
|
||||
projectId={projectId}
|
||||
visibleColumns={visibleColumns}
|
||||
isFirstInGroup={isFirstInGroup}
|
||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user