Merge pull request #267 from Worklenz/feature/show-multiples-levels-of-subtaks-in-task-list

Feature/show multiples levels of subtaks in task list
This commit is contained in:
Chamika J
2025-07-15 13:31:10 +05:30
committed by GitHub
17 changed files with 705 additions and 281 deletions

View File

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

View File

@@ -22,6 +22,8 @@ interface TaskRowWithSubtasksProps {
}>;
isFirstInGroup?: boolean;
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 {
@@ -32,14 +34,15 @@ interface AddSubtaskRowProps {
width: string;
isSticky?: boolean;
}>;
onSubtaskAdded: () => void; // Simplified - no rowId needed
rowId: string; // Unique identifier for this add subtask row
autoFocus?: boolean; // Whether this row should auto-focus on mount
isActive?: boolean; // Whether this row should show the input/button
onActivate?: () => void; // Simplified - no rowId needed
onSubtaskAdded: () => void;
rowId: string;
autoFocus?: boolean;
isActive?: boolean;
onActivate?: () => void;
depth?: number; // Add depth prop for proper indentation
}
const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
parentTaskId,
projectId,
visibleColumns,
@@ -47,25 +50,20 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
rowId,
autoFocus = false,
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 inputRef = useRef<any>(null);
const { socket, connected } = useSocket();
const { t } = useTranslation('task-list-table');
const dispatch = useAppDispatch();
// Get session data for reporter_id and team_id
const { socket, connected } = useSocket();
const currentSession = useAuthService().getCurrentSession();
// Auto-focus when autoFocus prop is true
useEffect(() => {
if (autoFocus && inputRef.current) {
setIsAdding(true);
setTimeout(() => {
inputRef.current?.focus();
}, 100);
inputRef.current.focus();
}
}, [autoFocus]);
@@ -141,11 +139,15 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
return (
<div className="flex items-center h-full" style={baseStyle}>
<div className="flex items-center w-full h-full">
{/* Match subtask indentation pattern - tighter spacing */}
<div className="w-4" />
{/* Match subtask indentation pattern - reduced spacing for level 1 */}
<div className="w-2" />
{/* Add additional indentation for deeper levels - increased spacing for level 2+ */}
{Array.from({ length: depth }).map((_, i) => (
<div key={i} className="w-6" />
))}
<div className="w-1" />
{isActive ? (
{isActive ? (
!isAdding ? (
<button
onClick={() => {
@@ -188,7 +190,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
default:
return <div style={baseStyle} />;
}
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate]);
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate, depth]);
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">
@@ -203,12 +205,42 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
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(({
taskId,
projectId,
visibleColumns,
isFirstInGroup = false,
updateTaskCustomColumnValue
updateTaskCustomColumnValue,
depth = 0,
maxDepth = 3
}) => {
const task = useAppSelector(state => selectTaskById(state, taskId));
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
@@ -223,6 +255,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
return null;
}
// Don't render subtasks if we've reached the maximum depth
const canHaveSubtasks = depth < maxDepth;
return (
<>
{/* Main task row */}
@@ -232,10 +267,12 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
visibleColumns={visibleColumns}
isFirstInGroup={isFirstInGroup}
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
isSubtask={depth > 0}
depth={depth}
/>
{/* Subtasks and add subtask row when expanded */}
{task.show_sub_tasks && (
{canHaveSubtasks && task.show_sub_tasks && (
<>
{/* Show loading skeleton while fetching subtasks */}
{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) => (
<div key={subtask.id} className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700">
<TaskRow
<div key={subtask.id} className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
<TaskRowWithSubtasks
taskId={subtask.id}
projectId={projectId}
visibleColumns={visibleColumns}
isSubtask={true}
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
depth={depth + 1}
maxDepth={maxDepth}
/>
</div>
))}
{/* Add subtask row - 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">
<div className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
<AddSubtaskRow
parentTaskId={taskId}
projectId={projectId}
@@ -267,8 +305,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
onSubtaskAdded={handleSubtaskAdded}
rowId={`add-subtask-${taskId}`}
autoFocus={false}
isActive={true} // Always show the add subtask row
onActivate={undefined} // Not needed anymore
isActive={true}
onActivate={undefined}
depth={depth + 1}
/>
</div>
)}

View File

@@ -24,6 +24,7 @@ interface TitleColumnProps {
onEditTaskName: (editing: boolean) => void;
onTaskNameChange: (name: string) => void;
onTaskNameSave: () => void;
depth?: number;
}
export const TitleColumn: React.FC<TitleColumnProps> = memo(({
@@ -36,7 +37,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
taskName,
onEditTaskName,
onTaskNameChange,
onTaskNameSave
onTaskNameSave,
depth = 0
}) => {
const dispatch = useAppDispatch();
const { socket, connected } = useSocket();
@@ -150,11 +152,16 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
/* Normal layout when not editing */
<>
<div className="flex items-center flex-1 min-w-0">
{/* Indentation for subtasks - tighter spacing */}
{isSubtask && <div className="w-4 flex-shrink-0" />}
{/* Indentation for subtasks - reduced spacing for level 1 */}
{isSubtask && <div className="w-2 flex-shrink-0" />}
{/* Expand/Collapse button - only show for parent tasks */}
{!isSubtask && (
{/* Additional indentation for deeper levels - increased spacing for level 2+ */}
{Array.from({ length: depth }).map((_, i) => (
<div key={i} className="w-6 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
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 ${
@@ -175,8 +182,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
</button>
)}
{/* Additional indentation for subtasks after the expand button space */}
{isSubtask && <div className="w-2 flex-shrink-0" />}
{/* Additional indentation for subtasks after the expand button space - reduced for level 1 */}
{isSubtask && <div className="w-1 flex-shrink-0" />}
<div className="flex items-center gap-2 flex-1 min-w-0">
{/* Task name with dynamic width */}
@@ -202,8 +209,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
{/* Indicators container - flex-shrink-0 to prevent compression */}
<div className="flex items-center gap-1 flex-shrink-0">
{/* Subtask count indicator - only show if count > 0 */}
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && (
{/* Subtask count indicator - show for any task that can have sub-tasks */}
{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 })}>
<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">

View File

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