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:
chamikaJ
2025-07-09 17:02:37 +05:30
parent cdd22e5f2f
commit db9b481e8d
3 changed files with 147 additions and 38 deletions

View File

@@ -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}
/>
);
}

View File

@@ -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>
))}
</>
)}
</>
)}

View File

@@ -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">