diff --git a/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx b/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx index cc09e29e..0859270b 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskListV2Table.tsx @@ -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} /> ); } diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRowWithSubtasks.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRowWithSubtasks.tsx index f0a95fdb..b9a511e6 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskRowWithSubtasks.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskRowWithSubtasks.tsx @@ -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 = 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(null); const { socket, connected } = useSocket(); const { t } = useTranslation('task-list-table'); const dispatch = useAppDispatch(); @@ -50,6 +55,16 @@ const AddSubtaskRow: React.FC = 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 = 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 = memo(({ ) : ( 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 = memo(({ default: return
; } - }, [isAdding, subtaskName, handleAddSubtask, handleCancel, t]); + }, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleKeyDown, t]); return (
@@ -160,11 +185,28 @@ const TaskRowWithSubtasks: React.FC = memo(({ const task = useAppSelector(state => selectTaskById(state, taskId)); const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId)); const dispatch = useAppDispatch(); + const [addSubtaskRows, setAddSubtaskRows] = useState([`add-subtask-${taskId}-0`]); + const [activeRowId, setActiveRowId] = useState(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 = memo(({
))} - {/* Add subtask row - only show when not loading */} + {/* Add subtask rows - only show when not loading */} {!isLoadingSubtasks && ( -
- -
+ <> + {/* Render all add subtask rows */} + {addSubtaskRows.map((rowId, index) => ( +
+ +
+ ))} + )} )} diff --git a/worklenz-frontend/src/components/task-list-v2/components/AddTaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/components/AddTaskRow.tsx index 1107bf22..72ae7e3e 100644 --- a/worklenz-frontend/src/components/task-list-v2/components/AddTaskRow.tsx +++ b/worklenz-frontend/src/components/task-list-v2/components/AddTaskRow.tsx @@ -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 = memo(({ @@ -25,16 +27,29 @@ const AddTaskRow: React.FC = 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(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 = 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 = 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 = memo(({ ) : ( 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 = memo(({ default: return
; } - }, [isAdding, taskName, handleAddTask, handleCancel, t]); + }, [isAdding, taskName, handleAddTask, handleCancel, handleKeyDown, t]); return (