refactor(task-list): enhance task addition functionality in TaskListV2Table and AddTaskRow
- Introduced state management for dynamic add task rows in TaskListV2Table, allowing real-time updates when tasks are added. - Updated handleTaskAdded to manage new task row creation based on group ID. - Enhanced AddTaskRow to support auto-focus functionality and unique row identification for improved user experience during task addition. - Refactored input handling in AddTaskRow to maintain focus and streamline task creation process.
This commit is contained in:
@@ -103,6 +103,7 @@ const TaskListV2Section: React.FC = () => {
|
||||
|
||||
// State hooks
|
||||
const [initializedFromDatabase, setInitializedFromDatabase] = useState(false);
|
||||
const [addTaskRows, setAddTaskRows] = useState<{[groupId: string]: string[]}>({});
|
||||
|
||||
// Configure sensors for drag and drop
|
||||
const sensors = useSensors(
|
||||
@@ -340,9 +341,22 @@ const TaskListV2Section: React.FC = () => {
|
||||
);
|
||||
|
||||
// Add callback for task added
|
||||
const handleTaskAdded = useCallback(() => {
|
||||
const handleTaskAdded = useCallback((rowId: string) => {
|
||||
// Task is now added in real-time via socket, no need to refetch
|
||||
// The global socket handler will handle the real-time update
|
||||
|
||||
// Find the group this row belongs to
|
||||
const groupId = rowId.split('-')[2]; // Extract from rowId format: add-task-{groupId}-{index}
|
||||
|
||||
// Add a new add task row to this group
|
||||
setAddTaskRows(prev => {
|
||||
const currentRows = prev[groupId] || [];
|
||||
const newRowId = `add-task-${groupId}-${currentRows.length + 1}`;
|
||||
return {
|
||||
...prev,
|
||||
[groupId]: [...currentRows, newRowId]
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle scroll synchronization - disabled since header is now sticky inside content
|
||||
@@ -368,18 +382,37 @@ const TaskListV2Section: React.FC = () => {
|
||||
originalIndex: allTasks.indexOf(task),
|
||||
}));
|
||||
|
||||
const itemsWithAddTask = !isCurrentGroupCollapsed
|
||||
// Get add task rows for this group
|
||||
const groupAddRows = addTaskRows[group.id] || [];
|
||||
const addTaskItems = !isCurrentGroupCollapsed
|
||||
? [
|
||||
...tasksForVirtuoso,
|
||||
// Default add task row
|
||||
{
|
||||
id: `add-task-${group.id}`,
|
||||
id: `add-task-${group.id}-0`,
|
||||
isAddTaskRow: true,
|
||||
groupId: group.id,
|
||||
groupType: currentGrouping || 'status',
|
||||
groupValue: group.id, // Use the actual database ID from backend
|
||||
projectId: urlProjectId,
|
||||
rowId: `add-task-${group.id}-0`,
|
||||
autoFocus: false,
|
||||
},
|
||||
// Additional add task rows
|
||||
...groupAddRows.map((rowId, index) => ({
|
||||
id: rowId,
|
||||
isAddTaskRow: true,
|
||||
groupId: group.id,
|
||||
groupType: currentGrouping || 'status',
|
||||
groupValue: group.id,
|
||||
projectId: urlProjectId,
|
||||
rowId: rowId,
|
||||
autoFocus: index === groupAddRows.length - 1, // Auto-focus the latest row
|
||||
}))
|
||||
]
|
||||
: [];
|
||||
|
||||
const itemsWithAddTask = !isCurrentGroupCollapsed
|
||||
? [...tasksForVirtuoso, ...addTaskItems]
|
||||
: tasksForVirtuoso;
|
||||
|
||||
const groupData = {
|
||||
@@ -393,7 +426,7 @@ const TaskListV2Section: React.FC = () => {
|
||||
currentTaskIndex += itemsWithAddTask.length;
|
||||
return groupData;
|
||||
});
|
||||
}, [groups, allTasks, collapsedGroups, currentGrouping, urlProjectId]);
|
||||
}, [groups, allTasks, collapsedGroups, currentGrouping, urlProjectId, addTaskRows]);
|
||||
|
||||
const virtuosoGroupCounts = useMemo(() => {
|
||||
return virtuosoGroups.map(group => group.count);
|
||||
@@ -471,6 +504,8 @@ const TaskListV2Section: React.FC = () => {
|
||||
projectId={urlProjectId}
|
||||
visibleColumns={visibleColumns}
|
||||
onTaskAdded={handleTaskAdded}
|
||||
rowId={item.rowId}
|
||||
autoFocus={item.autoFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useState, useCallback } from 'react';
|
||||
import React, { memo, useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { selectTaskById, createSubtask, selectSubtaskLoading } from '@/features/task-management/task-management.slice';
|
||||
@@ -32,17 +32,22 @@ interface AddSubtaskRowProps {
|
||||
width: string;
|
||||
isSticky?: boolean;
|
||||
}>;
|
||||
onSubtaskAdded: () => void;
|
||||
onSubtaskAdded: (rowId: string) => void;
|
||||
rowId: string; // Unique identifier for this add subtask row
|
||||
autoFocus?: boolean; // Whether this row should auto-focus on mount
|
||||
}
|
||||
|
||||
const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
||||
parentTaskId,
|
||||
projectId,
|
||||
visibleColumns,
|
||||
onSubtaskAdded
|
||||
onSubtaskAdded,
|
||||
rowId,
|
||||
autoFocus = false
|
||||
}) => {
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [isAdding, setIsAdding] = useState(autoFocus);
|
||||
const [subtaskName, setSubtaskName] = useState('');
|
||||
const inputRef = useRef<any>(null);
|
||||
const { socket, connected } = useSocket();
|
||||
const { t } = useTranslation('task-list-table');
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -50,6 +55,16 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
||||
// Get session data for reporter_id and team_id
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
|
||||
// Auto-focus when autoFocus prop is true
|
||||
useEffect(() => {
|
||||
if (autoFocus && inputRef.current) {
|
||||
setIsAdding(true);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [autoFocus]);
|
||||
|
||||
const handleAddSubtask = useCallback(() => {
|
||||
if (!subtaskName.trim() || !currentSession) return;
|
||||
|
||||
@@ -75,14 +90,22 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
||||
}
|
||||
|
||||
setSubtaskName('');
|
||||
setIsAdding(false);
|
||||
onSubtaskAdded();
|
||||
}, [subtaskName, dispatch, parentTaskId, projectId, connected, socket, currentSession, onSubtaskAdded]);
|
||||
// Keep the input active and notify parent to create new row
|
||||
onSubtaskAdded(rowId);
|
||||
}, [subtaskName, dispatch, parentTaskId, projectId, connected, socket, currentSession, onSubtaskAdded, rowId]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setSubtaskName('');
|
||||
setIsAdding(false);
|
||||
}, []);
|
||||
if (subtaskName.trim() === '') {
|
||||
setSubtaskName('');
|
||||
setIsAdding(false);
|
||||
}
|
||||
}, [subtaskName]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
}, [handleCancel]);
|
||||
|
||||
const renderColumn = useCallback((columnId: string, width: string) => {
|
||||
const baseStyle = { width };
|
||||
@@ -114,10 +137,12 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
||||
</button>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={subtaskName}
|
||||
onChange={(e) => setSubtaskName(e.target.value)}
|
||||
onPressEnter={handleAddSubtask}
|
||||
onBlur={handleCancel}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type subtask name and press Enter to save"
|
||||
className="w-full h-full border-none shadow-none bg-transparent"
|
||||
style={{
|
||||
@@ -135,7 +160,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
||||
default:
|
||||
return <div style={baseStyle} />;
|
||||
}
|
||||
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, t]);
|
||||
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleKeyDown, t]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -160,11 +185,28 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
|
||||
const dispatch = useAppDispatch();
|
||||
const [addSubtaskRows, setAddSubtaskRows] = useState<string[]>([`add-subtask-${taskId}-0`]);
|
||||
const [activeRowId, setActiveRowId] = useState<string | null>(null);
|
||||
|
||||
const handleSubtaskAdded = useCallback(() => {
|
||||
const handleSubtaskAdded = useCallback((rowId: string) => {
|
||||
// Refresh subtasks after adding a new one
|
||||
// The socket event will handle the real-time update
|
||||
}, []);
|
||||
|
||||
// Only add a new row if this is the last (most recent) row
|
||||
setAddSubtaskRows(prev => {
|
||||
const currentIndex = prev.indexOf(rowId);
|
||||
const isLastRow = currentIndex === prev.length - 1;
|
||||
|
||||
if (isLastRow) {
|
||||
const newRowId = `add-subtask-${taskId}-${prev.length}`;
|
||||
// Set the new row as active
|
||||
setActiveRowId(newRowId);
|
||||
return [...prev, newRowId];
|
||||
}
|
||||
|
||||
return prev; // Don't add new row if this isn't the last row
|
||||
});
|
||||
}, [taskId]);
|
||||
|
||||
if (!task) {
|
||||
return null;
|
||||
@@ -204,16 +246,23 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add subtask row - only show when not loading */}
|
||||
{/* Add subtask rows - only show when not loading */}
|
||||
{!isLoadingSubtasks && (
|
||||
<div className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700">
|
||||
<AddSubtaskRow
|
||||
parentTaskId={taskId}
|
||||
projectId={projectId}
|
||||
visibleColumns={visibleColumns}
|
||||
onSubtaskAdded={handleSubtaskAdded}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{/* Render all add subtask rows */}
|
||||
{addSubtaskRows.map((rowId, index) => (
|
||||
<div key={rowId} className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700">
|
||||
<AddSubtaskRow
|
||||
parentTaskId={taskId}
|
||||
projectId={projectId}
|
||||
visibleColumns={visibleColumns}
|
||||
onSubtaskAdded={handleSubtaskAdded}
|
||||
rowId={rowId}
|
||||
autoFocus={index === addSubtaskRows.length - 1} // Auto-focus the latest row
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, memo } from 'react';
|
||||
import React, { useState, useCallback, memo, useRef, useEffect } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -16,7 +16,9 @@ interface AddTaskRowProps {
|
||||
width: string;
|
||||
isSticky?: boolean;
|
||||
}>;
|
||||
onTaskAdded: () => void;
|
||||
onTaskAdded: (rowId: string) => void;
|
||||
rowId: string; // Unique identifier for this add task row
|
||||
autoFocus?: boolean; // Whether this row should auto-focus on mount
|
||||
}
|
||||
|
||||
const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
|
||||
@@ -25,16 +27,29 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
|
||||
groupValue,
|
||||
projectId,
|
||||
visibleColumns,
|
||||
onTaskAdded
|
||||
onTaskAdded,
|
||||
rowId,
|
||||
autoFocus = false
|
||||
}) => {
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [isAdding, setIsAdding] = useState(autoFocus);
|
||||
const [taskName, setTaskName] = useState('');
|
||||
const inputRef = useRef<any>(null);
|
||||
const { socket, connected } = useSocket();
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// Get session data for reporter_id and team_id
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
|
||||
// Auto-focus when autoFocus prop is true
|
||||
useEffect(() => {
|
||||
if (autoFocus && inputRef.current) {
|
||||
setIsAdding(true);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [autoFocus]);
|
||||
|
||||
// The global socket handler (useTaskSocketHandlers) will handle task addition
|
||||
// No need for local socket listener to avoid duplicate additions
|
||||
|
||||
@@ -67,10 +82,10 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
|
||||
}
|
||||
|
||||
if (socket && connected) {
|
||||
|
||||
socket.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
||||
setTaskName('');
|
||||
setIsAdding(false);
|
||||
// Keep the input active and notify parent to create new row
|
||||
onTaskAdded(rowId);
|
||||
// Task refresh will be handled by socket response listener
|
||||
} else {
|
||||
console.warn('Socket not connected, unable to create task');
|
||||
@@ -78,12 +93,20 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
}
|
||||
}, [taskName, projectId, groupType, groupValue, socket, connected, currentSession]);
|
||||
}, [taskName, projectId, groupType, groupValue, socket, connected, currentSession, onTaskAdded, rowId]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setTaskName('');
|
||||
setIsAdding(false);
|
||||
}, []);
|
||||
if (taskName.trim() === '') {
|
||||
setTaskName('');
|
||||
setIsAdding(false);
|
||||
}
|
||||
}, [taskName]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
}, [handleCancel]);
|
||||
|
||||
const renderColumn = useCallback((columnId: string, width: string) => {
|
||||
const baseStyle = { width };
|
||||
@@ -116,10 +139,12 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
|
||||
</button>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={taskName}
|
||||
onChange={(e) => setTaskName(e.target.value)}
|
||||
onPressEnter={handleAddTask}
|
||||
onBlur={handleCancel}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type task name and press Enter to save"
|
||||
className="w-full h-full border-none shadow-none bg-transparent"
|
||||
style={{
|
||||
@@ -137,7 +162,7 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
|
||||
default:
|
||||
return <div className="border-r border-gray-200 dark:border-gray-700" style={baseStyle} />;
|
||||
}
|
||||
}, [isAdding, taskName, handleAddTask, handleCancel, t]);
|
||||
}, [isAdding, taskName, handleAddTask, handleCancel, handleKeyDown, t]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
|
||||
|
||||
Reference in New Issue
Block a user