refactor(task-list): enhance task row components with depth handling

- Added depth and maxDepth props to TaskRow, TaskRowWithSubtasks, and TitleColumn components to manage nested subtasks more effectively.
- Updated AddSubtaskRow to support depth for proper indentation and visual hierarchy.
- Improved styling for subtasks based on their depth level, ensuring better visual distinction.
- Adjusted task management slice to utilize actual subtask counts from the backend for accurate display.
This commit is contained in:
chamikaJ
2025-07-14 12:04:31 +05:30
parent e87f33dcc8
commit 02d814b935
6 changed files with 96 additions and 46 deletions

View File

@@ -1,6 +1,5 @@
import { useSocket } from '@/socket/socketContext'; import { useSocket } from '@/socket/socketContext';
import { ITaskPhase } from '@/types/tasks/taskPhase.types'; import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { Select } from 'antd'; import { Select } from 'antd';
import { Form } from 'antd'; import { Form } from 'antd';
@@ -27,12 +26,6 @@ const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps)
phase_id: value, phase_id: value,
parent_task: task.parent_task_id || null, parent_task: task.parent_task_id || null,
}); });
// socket?.once(SocketEvents.TASK_PHASE_CHANGE.toString(), () => {
// if(list.getCurrentGroup().value === this.list.GROUP_BY_PHASE_VALUE && this.list.isSubtasksIncluded) {
// this.list.emitRefreshSubtasksIncluded();
// }
// });
}; };
return ( return (
@@ -41,8 +34,11 @@ const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps)
allowClear allowClear
placeholder="Select Phase" placeholder="Select Phase"
options={phaseMenuItems} options={phaseMenuItems}
style={{ width: 'fit-content' }} styles={{
dropdownStyle={{ width: 'fit-content' }} root: {
width: 'fit-content',
},
}}
onChange={handlePhaseChange} onChange={handlePhaseChange}
/> />
</Form.Item> </Form.Item>

View File

@@ -24,6 +24,7 @@ interface TaskRowProps {
isSubtask?: boolean; isSubtask?: boolean;
isFirstInGroup?: boolean; isFirstInGroup?: boolean;
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
depth?: number;
} }
const TaskRow: React.FC<TaskRowProps> = memo(({ const TaskRow: React.FC<TaskRowProps> = memo(({
@@ -32,7 +33,8 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
visibleColumns, visibleColumns,
isSubtask = false, isSubtask = false,
isFirstInGroup = false, isFirstInGroup = false,
updateTaskCustomColumnValue updateTaskCustomColumnValue,
depth = 0
}) => { }) => {
// Get task data and selection state from Redux // Get task data and selection state from Redux
const task = useAppSelector(state => selectTaskById(state, taskId)); const task = useAppSelector(state => selectTaskById(state, taskId));
@@ -107,6 +109,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
handleTaskNameEdit, handleTaskNameEdit,
attributes, attributes,
listeners, listeners,
depth,
}); });
// Memoize style object to prevent unnecessary re-renders // Memoize style object to prevent unnecessary re-renders

View File

@@ -22,6 +22,8 @@ interface TaskRowWithSubtasksProps {
}>; }>;
isFirstInGroup?: boolean; isFirstInGroup?: boolean;
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
depth?: number; // Add depth prop to track nesting level
maxDepth?: number; // Add maxDepth prop to limit nesting
} }
interface AddSubtaskRowProps { interface AddSubtaskRowProps {
@@ -32,14 +34,15 @@ interface AddSubtaskRowProps {
width: string; width: string;
isSticky?: boolean; isSticky?: boolean;
}>; }>;
onSubtaskAdded: () => void; // Simplified - no rowId needed onSubtaskAdded: () => void;
rowId: string; // Unique identifier for this add subtask row rowId: string;
autoFocus?: boolean; // Whether this row should auto-focus on mount autoFocus?: boolean;
isActive?: boolean; // Whether this row should show the input/button isActive?: boolean;
onActivate?: () => void; // Simplified - no rowId needed onActivate?: () => void;
depth?: number; // Add depth prop for proper indentation
} }
const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
parentTaskId, parentTaskId,
projectId, projectId,
visibleColumns, visibleColumns,
@@ -47,25 +50,20 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
rowId, rowId,
autoFocus = false, autoFocus = false,
isActive = true, isActive = true,
onActivate onActivate,
depth = 0
}) => { }) => {
const [isAdding, setIsAdding] = useState(autoFocus); const { t } = useTranslation('task-list-table');
const [isAdding, setIsAdding] = useState(false);
const [subtaskName, setSubtaskName] = useState(''); const [subtaskName, setSubtaskName] = useState('');
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
const { socket, connected } = useSocket();
const { t } = useTranslation('task-list-table');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { socket, connected } = useSocket();
// 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(() => { useEffect(() => {
if (autoFocus && inputRef.current) { if (autoFocus && inputRef.current) {
setIsAdding(true); inputRef.current.focus();
setTimeout(() => {
inputRef.current?.focus();
}, 100);
} }
}, [autoFocus]); }, [autoFocus]);
@@ -142,10 +140,14 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
<div className="flex items-center h-full" style={baseStyle}> <div className="flex items-center h-full" style={baseStyle}>
<div className="flex items-center w-full h-full"> <div className="flex items-center w-full h-full">
{/* Match subtask indentation pattern - tighter spacing */} {/* Match subtask indentation pattern - tighter spacing */}
<div className="w-4" /> <div className="w-3" />
{/* Add additional indentation for deeper levels - 16px per level */}
{Array.from({ length: depth }).map((_, i) => (
<div key={i} className="w-4" />
))}
<div className="w-2" /> <div className="w-2" />
{isActive ? ( {isActive ? (
!isAdding ? ( !isAdding ? (
<button <button
onClick={() => { onClick={() => {
@@ -188,7 +190,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
default: default:
return <div style={baseStyle} />; return <div style={baseStyle} />;
} }
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate]); }, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate, depth]);
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">
@@ -203,12 +205,42 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
AddSubtaskRow.displayName = 'AddSubtaskRow'; AddSubtaskRow.displayName = 'AddSubtaskRow';
// Helper function to get background color based on depth
const getSubtaskBackgroundColor = (depth: number) => {
switch (depth) {
case 1:
return 'bg-gray-50 dark:bg-gray-800/50';
case 2:
return 'bg-blue-50 dark:bg-blue-900/20';
case 3:
return 'bg-green-50 dark:bg-green-900/20';
default:
return 'bg-gray-50 dark:bg-gray-800/50';
}
};
// Helper function to get border color based on depth
const getBorderColor = (depth: number) => {
switch (depth) {
case 1:
return 'border-blue-200 dark:border-blue-700';
case 2:
return 'border-green-200 dark:border-green-700';
case 3:
return 'border-purple-200 dark:border-purple-700';
default:
return 'border-blue-200 dark:border-blue-700';
}
};
const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
taskId, taskId,
projectId, projectId,
visibleColumns, visibleColumns,
isFirstInGroup = false, isFirstInGroup = false,
updateTaskCustomColumnValue updateTaskCustomColumnValue,
depth = 0,
maxDepth = 3
}) => { }) => {
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));
@@ -223,6 +255,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
return null; return null;
} }
// Don't render subtasks if we've reached the maximum depth
const canHaveSubtasks = depth < maxDepth;
return ( return (
<> <>
{/* Main task row */} {/* Main task row */}
@@ -232,10 +267,12 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
isFirstInGroup={isFirstInGroup} isFirstInGroup={isFirstInGroup}
updateTaskCustomColumnValue={updateTaskCustomColumnValue} updateTaskCustomColumnValue={updateTaskCustomColumnValue}
isSubtask={depth > 0}
depth={depth}
/> />
{/* Subtasks and add subtask row when expanded */} {/* Subtasks and add subtask row when expanded */}
{task.show_sub_tasks && ( {canHaveSubtasks && task.show_sub_tasks && (
<> <>
{/* Show loading skeleton while fetching subtasks */} {/* Show loading skeleton while fetching subtasks */}
{isLoadingSubtasks && ( {isLoadingSubtasks && (
@@ -244,22 +281,23 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
</> </>
)} )}
{/* Render existing subtasks when not loading */} {/* Render existing subtasks when not loading - RECURSIVELY */}
{!isLoadingSubtasks && task.sub_tasks?.map((subtask: Task) => ( {!isLoadingSubtasks && task.sub_tasks?.map((subtask: Task) => (
<div key={subtask.id} className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700"> <div key={subtask.id} className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
<TaskRow <TaskRowWithSubtasks
taskId={subtask.id} taskId={subtask.id}
projectId={projectId} projectId={projectId}
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
isSubtask={true}
updateTaskCustomColumnValue={updateTaskCustomColumnValue} updateTaskCustomColumnValue={updateTaskCustomColumnValue}
depth={depth + 1}
maxDepth={maxDepth}
/> />
</div> </div>
))} ))}
{/* Add subtask row - only show when not loading */} {/* Add subtask row - 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"> <div className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
<AddSubtaskRow <AddSubtaskRow
parentTaskId={taskId} parentTaskId={taskId}
projectId={projectId} projectId={projectId}
@@ -267,8 +305,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
onSubtaskAdded={handleSubtaskAdded} onSubtaskAdded={handleSubtaskAdded}
rowId={`add-subtask-${taskId}`} rowId={`add-subtask-${taskId}`}
autoFocus={false} autoFocus={false}
isActive={true} // Always show the add subtask row isActive={true}
onActivate={undefined} // Not needed anymore onActivate={undefined}
depth={depth + 1}
/> />
</div> </div>
)} )}

View File

@@ -24,6 +24,7 @@ interface TitleColumnProps {
onEditTaskName: (editing: boolean) => void; onEditTaskName: (editing: boolean) => void;
onTaskNameChange: (name: string) => void; onTaskNameChange: (name: string) => void;
onTaskNameSave: () => void; onTaskNameSave: () => void;
depth?: number;
} }
export const TitleColumn: React.FC<TitleColumnProps> = memo(({ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
@@ -36,7 +37,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
taskName, taskName,
onEditTaskName, onEditTaskName,
onTaskNameChange, onTaskNameChange,
onTaskNameSave onTaskNameSave,
depth = 0
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
@@ -151,10 +153,15 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
<> <>
<div className="flex items-center flex-1 min-w-0"> <div className="flex items-center flex-1 min-w-0">
{/* Indentation for subtasks - tighter spacing */} {/* Indentation for subtasks - tighter spacing */}
{isSubtask && <div className="w-4 flex-shrink-0" />} {isSubtask && <div className="w-3 flex-shrink-0" />}
{/* Expand/Collapse button - only show for parent tasks */} {/* Additional indentation for deeper levels - 16px per level */}
{!isSubtask && ( {Array.from({ length: depth }).map((_, i) => (
<div key={i} className="w-4 flex-shrink-0" />
))}
{/* Expand/Collapse button - show for any task that can have sub-tasks */}
{depth < 2 && ( // Only show if not at maximum depth (can still have children)
<button <button
onClick={handleToggleExpansion} onClick={handleToggleExpansion}
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${ className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${
@@ -202,8 +209,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
{/* Indicators container - flex-shrink-0 to prevent compression */} {/* Indicators container - flex-shrink-0 to prevent compression */}
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0">
{/* Subtask count indicator - only show if count > 0 */} {/* Subtask count indicator - show for any task that can have sub-tasks */}
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && ( {depth < 2 && task.sub_tasks_count != null && task.sub_tasks_count > 0 && (
<Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}> <Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}>
<div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs"> <div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs">
<span className="text-blue-600 dark:text-blue-400 font-medium"> <span className="text-blue-600 dark:text-blue-400 font-medium">

View File

@@ -58,6 +58,9 @@ interface UseTaskRowColumnsProps {
// Drag and drop // Drag and drop
attributes: any; attributes: any;
listeners: any; listeners: any;
// Depth for nested subtasks
depth?: number;
} }
export const useTaskRowColumns = ({ export const useTaskRowColumns = ({
@@ -84,6 +87,7 @@ export const useTaskRowColumns = ({
handleTaskNameEdit, handleTaskNameEdit,
attributes, attributes,
listeners, listeners,
depth = 0,
}: UseTaskRowColumnsProps) => { }: UseTaskRowColumnsProps) => {
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
@@ -128,6 +132,7 @@ export const useTaskRowColumns = ({
onEditTaskName={setEditTaskName} onEditTaskName={setEditTaskName}
onTaskNameChange={setTaskName} onTaskNameChange={setTaskName}
onTaskNameSave={handleTaskNameSave} onTaskNameSave={handleTaskNameSave}
depth={depth}
/> />
); );

View File

@@ -1010,7 +1010,7 @@ const taskManagementSlice = createSlice({
order: subtask.sort_order || subtask.order || 0, order: subtask.sort_order || subtask.order || 0,
parent_task_id: parentTaskId, parent_task_id: parentTaskId,
is_sub_task: true, is_sub_task: true,
sub_tasks_count: 0, sub_tasks_count: subtask.sub_tasks_count || 0, // Use actual count from backend
show_sub_tasks: false, show_sub_tasks: false,
})); }));