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
|
// 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user