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 // State hooks
const [initializedFromDatabase, setInitializedFromDatabase] = useState(false); const [initializedFromDatabase, setInitializedFromDatabase] = useState(false);
const [addTaskRows, setAddTaskRows] = useState<{[groupId: string]: string[]}>({});
// Configure sensors for drag and drop // Configure sensors for drag and drop
const sensors = useSensors( const sensors = useSensors(
@@ -340,9 +341,22 @@ const TaskListV2Section: React.FC = () => {
); );
// Add callback for task added // 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 // Task is now added in real-time via socket, no need to refetch
// The global socket handler will handle the real-time update // 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 // Handle scroll synchronization - disabled since header is now sticky inside content
@@ -368,18 +382,37 @@ const TaskListV2Section: React.FC = () => {
originalIndex: allTasks.indexOf(task), 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, isAddTaskRow: true,
groupId: group.id, groupId: group.id,
groupType: currentGrouping || 'status', groupType: currentGrouping || 'status',
groupValue: group.id, // Use the actual database ID from backend groupValue: group.id, // Use the actual database ID from backend
projectId: urlProjectId, 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; : tasksForVirtuoso;
const groupData = { const groupData = {
@@ -393,7 +426,7 @@ const TaskListV2Section: React.FC = () => {
currentTaskIndex += itemsWithAddTask.length; currentTaskIndex += itemsWithAddTask.length;
return groupData; return groupData;
}); });
}, [groups, allTasks, collapsedGroups, currentGrouping, urlProjectId]); }, [groups, allTasks, collapsedGroups, currentGrouping, urlProjectId, addTaskRows]);
const virtuosoGroupCounts = useMemo(() => { const virtuosoGroupCounts = useMemo(() => {
return virtuosoGroups.map(group => group.count); return virtuosoGroups.map(group => group.count);
@@ -471,6 +504,8 @@ const TaskListV2Section: React.FC = () => {
projectId={urlProjectId} projectId={urlProjectId}
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
onTaskAdded={handleTaskAdded} 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 { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { selectTaskById, createSubtask, selectSubtaskLoading } from '@/features/task-management/task-management.slice'; import { selectTaskById, createSubtask, selectSubtaskLoading } from '@/features/task-management/task-management.slice';
@@ -32,17 +32,22 @@ interface AddSubtaskRowProps {
width: string; width: string;
isSticky?: boolean; 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(({ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
parentTaskId, parentTaskId,
projectId, projectId,
visibleColumns, visibleColumns,
onSubtaskAdded onSubtaskAdded,
rowId,
autoFocus = false
}) => { }) => {
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(autoFocus);
const [subtaskName, setSubtaskName] = useState(''); const [subtaskName, setSubtaskName] = useState('');
const inputRef = useRef<any>(null);
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
const { t } = useTranslation('task-list-table'); const { t } = useTranslation('task-list-table');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -50,6 +55,16 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
// Get session data for reporter_id and team_id // Get session data for reporter_id and team_id
const currentSession = useAuthService().getCurrentSession(); 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(() => { const handleAddSubtask = useCallback(() => {
if (!subtaskName.trim() || !currentSession) return; if (!subtaskName.trim() || !currentSession) return;
@@ -75,14 +90,22 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
} }
setSubtaskName(''); setSubtaskName('');
setIsAdding(false); // Keep the input active and notify parent to create new row
onSubtaskAdded(); onSubtaskAdded(rowId);
}, [subtaskName, dispatch, parentTaskId, projectId, connected, socket, currentSession, onSubtaskAdded]); }, [subtaskName, dispatch, parentTaskId, projectId, connected, socket, currentSession, onSubtaskAdded, rowId]);
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
if (subtaskName.trim() === '') {
setSubtaskName(''); setSubtaskName('');
setIsAdding(false); setIsAdding(false);
}, []); }
}, [subtaskName]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
handleCancel();
}
}, [handleCancel]);
const renderColumn = useCallback((columnId: string, width: string) => { const renderColumn = useCallback((columnId: string, width: string) => {
const baseStyle = { width }; const baseStyle = { width };
@@ -114,10 +137,12 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
</button> </button>
) : ( ) : (
<Input <Input
ref={inputRef}
value={subtaskName} value={subtaskName}
onChange={(e) => setSubtaskName(e.target.value)} onChange={(e) => setSubtaskName(e.target.value)}
onPressEnter={handleAddSubtask} onPressEnter={handleAddSubtask}
onBlur={handleCancel} onBlur={handleCancel}
onKeyDown={handleKeyDown}
placeholder="Type subtask name and press Enter to save" placeholder="Type subtask name and press Enter to save"
className="w-full h-full border-none shadow-none bg-transparent" className="w-full h-full border-none shadow-none bg-transparent"
style={{ style={{
@@ -135,7 +160,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
default: default:
return <div style={baseStyle} />; return <div style={baseStyle} />;
} }
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, t]); }, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleKeyDown, t]);
return ( 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"> <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 task = useAppSelector(state => selectTaskById(state, taskId));
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId)); const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
const dispatch = useAppDispatch(); 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 // Refresh subtasks after adding a new one
// The socket event will handle the real-time update // 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) { if (!task) {
return null; return null;
@@ -204,16 +246,23 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
</div> </div>
))} ))}
{/* Add subtask row - only show when not loading */} {/* Add subtask rows - only show when not loading */}
{!isLoadingSubtasks && ( {!isLoadingSubtasks && (
<div className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700"> <>
{/* 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 <AddSubtaskRow
parentTaskId={taskId} parentTaskId={taskId}
projectId={projectId} projectId={projectId}
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
onSubtaskAdded={handleSubtaskAdded} onSubtaskAdded={handleSubtaskAdded}
rowId={rowId}
autoFocus={index === addSubtaskRows.length - 1} // Auto-focus the latest row
/> />
</div> </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 { Input } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -16,7 +16,9 @@ interface AddTaskRowProps {
width: string; width: string;
isSticky?: boolean; 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(({ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
@@ -25,16 +27,29 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
groupValue, groupValue,
projectId, projectId,
visibleColumns, visibleColumns,
onTaskAdded onTaskAdded,
rowId,
autoFocus = false
}) => { }) => {
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(autoFocus);
const [taskName, setTaskName] = useState(''); const [taskName, setTaskName] = useState('');
const inputRef = useRef<any>(null);
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
const { t } = useTranslation('task-list-table'); const { t } = useTranslation('task-list-table');
// Get session data for reporter_id and team_id // Get session data for reporter_id and team_id
const currentSession = useAuthService().getCurrentSession(); 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 // The global socket handler (useTaskSocketHandlers) will handle task addition
// No need for local socket listener to avoid duplicate additions // No need for local socket listener to avoid duplicate additions
@@ -67,10 +82,10 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
} }
if (socket && connected) { if (socket && connected) {
socket.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body)); socket.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
setTaskName(''); 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 // Task refresh will be handled by socket response listener
} else { } else {
console.warn('Socket not connected, unable to create task'); console.warn('Socket not connected, unable to create task');
@@ -78,12 +93,20 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
} catch (error) { } catch (error) {
console.error('Error creating task:', 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(() => { const handleCancel = useCallback(() => {
if (taskName.trim() === '') {
setTaskName(''); setTaskName('');
setIsAdding(false); setIsAdding(false);
}, []); }
}, [taskName]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
handleCancel();
}
}, [handleCancel]);
const renderColumn = useCallback((columnId: string, width: string) => { const renderColumn = useCallback((columnId: string, width: string) => {
const baseStyle = { width }; const baseStyle = { width };
@@ -116,10 +139,12 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
</button> </button>
) : ( ) : (
<Input <Input
ref={inputRef}
value={taskName} value={taskName}
onChange={(e) => setTaskName(e.target.value)} onChange={(e) => setTaskName(e.target.value)}
onPressEnter={handleAddTask} onPressEnter={handleAddTask}
onBlur={handleCancel} onBlur={handleCancel}
onKeyDown={handleKeyDown}
placeholder="Type task name and press Enter to save" placeholder="Type task name and press Enter to save"
className="w-full h-full border-none shadow-none bg-transparent" className="w-full h-full border-none shadow-none bg-transparent"
style={{ style={{
@@ -137,7 +162,7 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
default: default:
return <div className="border-r border-gray-200 dark:border-gray-700" style={baseStyle} />; 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 ( 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"> <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">