diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index d95a281e..c2d5b54e 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -26,6 +26,9 @@ interface TaskRowProps { index?: number; onSelect?: (taskId: string, selected: boolean) => void; onToggleSubtasks?: (taskId: string) => void; + columns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>; + fixedColumns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>; + scrollableColumns?: Array<{ key: string; label: string; width: number; fixed?: boolean }>; } // Priority and status colors - moved outside component to avoid recreation @@ -52,6 +55,9 @@ const TaskRow: React.FC = React.memo(({ index, onSelect, onToggleSubtasks, + columns, + fixedColumns, + scrollableColumns, }) => { const { socket, connected } = useSocket(); @@ -217,189 +223,222 @@ const TaskRow: React.FC = React.memo(({ >
{/* Fixed Columns */} -
- {/* Drag Handle */} -
-
- - {/* Selection Checkbox */} -
- -
- - {/* Task Key */} -
- - {task.task_key} - -
- - {/* Task Name */} -
-
-
-
- {!editTaskName ? ( - setEditTaskName(true)} - className={taskNameClasses} - style={{ cursor: 'pointer' }} - > - {task.title} - - ) : ( - ) => setTaskName(e.target.value)} - onPressEnter={handleTaskNameSave} - className={`${isDarkMode ? 'bg-gray-800 text-gray-100 border-gray-600' : 'bg-white text-gray-900 border-gray-300'}`} - style={{ - width: '100%', - padding: '2px 4px', - fontSize: '14px', - fontWeight: 500, - }} +
sum + col.width, 0) || 0, + }} + > + {fixedColumns?.map(col => { + switch (col.key) { + case 'drag': + return ( +
+
-
-
-
+
+ ); + case 'select': + return ( +
+ +
+ ); + case 'key': + return ( +
+ + {task.task_key} + +
+ ); + case 'task': + return ( +
+
+
+
+ {!editTaskName ? ( + setEditTaskName(true)} + className={taskNameClasses} + style={{ cursor: 'pointer' }} + > + {task.title} + + ) : ( + ) => setTaskName(e.target.value)} + onPressEnter={handleTaskNameSave} + className={`${isDarkMode ? 'bg-gray-800 text-gray-100 border-gray-600' : 'bg-white text-gray-900 border-gray-300'}`} + style={{ + width: '100%', + padding: '2px 4px', + fontSize: '14px', + fontWeight: 500, + }} + /> + )} +
+
+
+
+ ); + default: + return null; + } + })}
- {/* Scrollable Columns */} -
- {/* Progress */} -
- {task.progress !== undefined && task.progress >= 0 && ( - - )} -
- - {/* Members */} -
-
- {avatarGroupMembers.length > 0 && ( - - )} - -
-
- - {/* Labels */} -
-
- {task.labels?.map((label, index) => ( - label.end && label.names && label.name ? ( - - ) : ( - - ) - ))} - -
-
- - {/* Status */} -
- - {task.status} - -
- - {/* Priority */} -
-
-
- - {task.priority} - -
-
- - {/* Time Tracking */} -
-
- {task.timeTracking?.logged && task.timeTracking.logged > 0 && ( -
- - - {typeof task.timeTracking.logged === 'number' - ? `${task.timeTracking.logged}h` - : task.timeTracking.logged - } - -
- )} -
-
+
sum + col.width, 0) || 0 }}> + {scrollableColumns?.map(col => { + switch (col.key) { + case 'progress': + return ( +
+ {task.progress !== undefined && task.progress >= 0 && ( + + )} +
+ ); + case 'members': + return ( +
+
+ {avatarGroupMembers.length > 0 && ( + + )} + +
+
+ ); + case 'labels': + return ( +
+
+ {task.labels?.map((label, index) => ( + label.end && label.names && label.name ? ( + + ) : ( + + ) + ))} + +
+
+ ); + case 'status': + return ( +
+ + {task.status} + +
+ ); + case 'priority': + return ( +
+
+
+ + {task.priority} + +
+
+ ); + case 'timeTracking': + return ( +
+
+ {task.timeTracking?.logged && task.timeTracking.logged > 0 && ( +
+ + + {typeof task.timeTracking.logged === 'number' + ? `${task.timeTracking.logged}h` + : task.timeTracking.logged + } + +
+ )} +
+
+ ); + default: + return null; + } + })}
diff --git a/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx b/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx index 5fe42380..47579249 100644 --- a/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx +++ b/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback, useEffect } from 'react'; +import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { FixedSizeList as List } from 'react-window'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useSelector } from 'react-redux'; @@ -64,120 +64,146 @@ const VirtualizedTaskList: React.FC = React.memo(({ }); }, [group.id, groupTasks.length, height, listHeight]); - // Row renderer for virtualization + const scrollContainerRef = useRef(null); + const headerScrollRef = useRef(null); + + // Synchronize header scroll with body scroll + useEffect(() => { + const handleScroll = () => { + if (headerScrollRef.current && scrollContainerRef.current) { + headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft; + } + }; + const scrollDiv = scrollContainerRef.current; + if (scrollDiv) { + scrollDiv.addEventListener('scroll', handleScroll); + } + return () => { + if (scrollDiv) { + scrollDiv.removeEventListener('scroll', handleScroll); + } + }; + }, []); + + // Define columns array for alignment + const columns = [ + { key: 'drag', label: '', width: 40, fixed: true }, + { key: 'select', label: '', width: 40, fixed: true }, + { key: 'key', label: 'KEY', width: 80, fixed: true }, + { key: 'task', label: 'TASK', width: 475, fixed: true }, + { key: 'progress', label: 'PROGRESS', width: 90 }, + { key: 'members', label: 'MEMBERS', width: 150 }, + { key: 'labels', label: 'LABELS', width: 200 }, + { key: 'status', label: 'STATUS', width: 100 }, + { key: 'priority', label: 'PRIORITY', width: 100 }, + { key: 'timeTracking', label: 'TIME TRACKING', width: 120 }, + ]; + const fixedColumns = columns.filter(col => col.fixed); + const scrollableColumns = columns.filter(col => !col.fixed); + const fixedWidth = fixedColumns.reduce((sum, col) => sum + col.width, 0); + const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0); + const totalTableWidth = fixedWidth + scrollableWidth; + + // Row renderer for virtualization (remove header/column header rows) const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => { - // Header row - if (index === 0) { - return ( -
-
-
-
- - {group.title} ({groupTasks.length}) - -
-
-
-
- ); - } - - // Column headers row - if (index === 1) { - return ( -
-
-
-
-
-
-
- Key -
-
- Task -
-
-
-
- Progress -
-
- Members -
-
- Labels -
-
- Status -
-
- Priority -
-
- Time Tracking -
-
-
-
-
- ); - } - - // Task rows - const taskIndex = index - 2; - if (taskIndex >= 0 && taskIndex < groupTasks.length) { - const task = groupTasks[taskIndex]; - return ( -
- -
- ); - } - - return null; + const task = groupTasks[index]; + if (!task) return null; + return ( +
+ +
+ ); }, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]); return (
- - +
+
+ + {group.title} ({groupTasks.length}) + +
+
+
+ {/* Column Headers (sync scroll) */} +
+
- {Row} - - - +
+ {fixedColumns.map(col => ( +
+ {col.label} +
+ ))} +
+
+ {scrollableColumns.map(col => ( +
+ {col.label} +
+ ))} +
+
+
+ {/* Scrollable List */} +
+ + + {Row} + + +
{/* Add Task Row - Always show at the bottom */}
= React.memo(({ >
-