feat(task-management): enhance task row and virtualized list components for improved layout and performance
- Added support for customizable columns in `TaskRow` component, allowing for fixed and scrollable columns. - Implemented synchronized scrolling between header and body in `VirtualizedTaskList` for better user experience. - Refactored column header rendering to dynamically generate based on column definitions, improving maintainability. - Enhanced styles for task group headers and column headers to ensure consistent appearance and responsiveness.
This commit is contained in:
@@ -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<TaskRowProps> = React.memo(({
|
||||
index,
|
||||
onSelect,
|
||||
onToggleSubtasks,
|
||||
columns,
|
||||
fixedColumns,
|
||||
scrollableColumns,
|
||||
}) => {
|
||||
const { socket, connected } = useSocket();
|
||||
|
||||
@@ -217,189 +223,222 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
>
|
||||
<div className="flex h-10 max-h-10 overflow-visible relative min-w-[1200px]">
|
||||
{/* Fixed Columns */}
|
||||
<div className={fixedColumnsClasses}>
|
||||
{/* Drag Handle */}
|
||||
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
icon={<HolderOutlined />}
|
||||
className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
||||
isDarkMode={isDarkMode}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selection Checkbox */}
|
||||
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={handleSelectChange}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Task Key */}
|
||||
<div className={`w-20 flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Tag
|
||||
backgroundColor={isDarkMode ? "#374151" : "#f0f0f0"}
|
||||
color={isDarkMode ? "#d1d5db" : "#666"}
|
||||
className="truncate whitespace-nowrap max-w-full"
|
||||
>
|
||||
{task.task_key}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* Task Name */}
|
||||
<div className={`w-[475px] flex items-center px-2 ${editTaskName ? (isDarkMode ? 'bg-blue-900/10 border border-blue-500' : 'bg-blue-50/20 border border-blue-500') : ''}`}>
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
||||
<div className="flex items-center gap-2 h-5 overflow-hidden">
|
||||
<div ref={wrapperRef} className="flex-1 min-w-0">
|
||||
{!editTaskName ? (
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: task.title }}
|
||||
onClick={() => setEditTaskName(true)}
|
||||
className={taskNameClasses}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{task.title}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
variant="borderless"
|
||||
value={taskName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => 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,
|
||||
}}
|
||||
<div
|
||||
className="fixed-columns-row"
|
||||
style={{
|
||||
display: 'flex',
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
zIndex: 2,
|
||||
background: isDarkMode ? '#1a1a1a' : '#fff',
|
||||
width: fixedColumns?.reduce((sum, col) => sum + col.width, 0) || 0,
|
||||
}}
|
||||
>
|
||||
{fixedColumns?.map(col => {
|
||||
switch (col.key) {
|
||||
case 'drag':
|
||||
return (
|
||||
<div key={col.key} className="w-10 flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
icon={<HolderOutlined />}
|
||||
className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
||||
isDarkMode={isDarkMode}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'select':
|
||||
return (
|
||||
<div key={col.key} className="w-10 flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={handleSelectChange}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'key':
|
||||
return (
|
||||
<div key={col.key} className="w-20 flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<Tag
|
||||
backgroundColor={isDarkMode ? "#374151" : "#f0f0f0"}
|
||||
color={isDarkMode ? "#d1d5db" : "#666"}
|
||||
className="truncate whitespace-nowrap max-w-full"
|
||||
>
|
||||
{task.task_key}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
case 'task':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2" style={{ width: col.width }}>
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
||||
<div className="flex items-center gap-2 h-5 overflow-hidden">
|
||||
<div ref={wrapperRef} className="flex-1 min-w-0">
|
||||
{!editTaskName ? (
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: task.title }}
|
||||
onClick={() => setEditTaskName(true)}
|
||||
className={taskNameClasses}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{task.title}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
variant="borderless"
|
||||
value={taskName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => 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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Scrollable Columns */}
|
||||
<div className="flex flex-1 min-w-0">
|
||||
{/* Progress */}
|
||||
<div className={`w-[90px] flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
{task.progress !== undefined && task.progress >= 0 && (
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={task.progress}
|
||||
size={24}
|
||||
strokeColor={task.progress === 100 ? '#52c41a' : '#1890ff'}
|
||||
strokeWidth={2}
|
||||
showInfo={true}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Members */}
|
||||
<div className={`w-[150px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
{avatarGroupMembers.length > 0 && (
|
||||
<AvatarGroup
|
||||
members={avatarGroupMembers}
|
||||
size={24}
|
||||
maxCount={3}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className={`
|
||||
w-6 h-6 rounded-full border border-dashed flex items-center justify-center
|
||||
transition-colors duration-200
|
||||
${isDarkMode
|
||||
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
|
||||
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
// TODO: Implement assignee selector functionality
|
||||
console.log('Add assignee clicked for task:', task.id);
|
||||
}}
|
||||
>
|
||||
<span className="text-xs">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div className={`w-[200px] max-w-[200px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
|
||||
{task.labels?.map((label, index) => (
|
||||
label.end && label.names && label.name ? (
|
||||
<CustomNumberLabel
|
||||
key={`${label.id}-${index}`}
|
||||
labelList={label.names}
|
||||
namesString={label.name}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<CustomColordLabel
|
||||
key={`${label.id}-${index}`}
|
||||
label={label}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
<LabelsSelector
|
||||
task={taskAdapter}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className={`w-[100px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Tag
|
||||
backgroundColor={getStatusColor(task.status)}
|
||||
color="white"
|
||||
className="text-xs font-medium uppercase"
|
||||
>
|
||||
{task.status}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div className={`w-[100px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
||||
/>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
{task.priority}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time Tracking */}
|
||||
<div className={`w-[120px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-2 h-full overflow-hidden">
|
||||
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
{typeof task.timeTracking.logged === 'number'
|
||||
? `${task.timeTracking.logged}h`
|
||||
: task.timeTracking.logged
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="scrollable-columns-row" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
||||
{scrollableColumns?.map(col => {
|
||||
switch (col.key) {
|
||||
case 'progress':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||
{task.progress !== undefined && task.progress >= 0 && (
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={task.progress}
|
||||
size={24}
|
||||
strokeColor={task.progress === 100 ? '#52c41a' : '#1890ff'}
|
||||
strokeWidth={2}
|
||||
showInfo={true}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case 'members':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-2">
|
||||
{avatarGroupMembers.length > 0 && (
|
||||
<AvatarGroup
|
||||
members={avatarGroupMembers}
|
||||
size={24}
|
||||
maxCount={3}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className={`
|
||||
w-6 h-6 rounded-full border border-dashed flex items-center justify-center
|
||||
transition-colors duration-200
|
||||
${isDarkMode
|
||||
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
|
||||
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
// TODO: Implement assignee selector functionality
|
||||
console.log('Add assignee clicked for task:', task.id);
|
||||
}}
|
||||
>
|
||||
<span className="text-xs">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'labels':
|
||||
return (
|
||||
<div key={col.key} className="max-w-[200px] flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
|
||||
{task.labels?.map((label, index) => (
|
||||
label.end && label.names && label.name ? (
|
||||
<CustomNumberLabel
|
||||
key={`${label.id}-${index}`}
|
||||
labelList={label.names}
|
||||
namesString={label.name}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<CustomColordLabel
|
||||
key={`${label.id}-${index}`}
|
||||
label={label}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
<LabelsSelector
|
||||
task={taskAdapter}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'status':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<Tag
|
||||
backgroundColor={getStatusColor(task.status)}
|
||||
color="white"
|
||||
className="text-xs font-medium uppercase"
|
||||
>
|
||||
{task.status}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
case 'priority':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
||||
/>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
{task.priority}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'timeTracking':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-2 h-full overflow-hidden">
|
||||
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
{typeof task.timeTracking.logged === 'number'
|
||||
? `${task.timeTracking.logged}h`
|
||||
: task.timeTracking.logged
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<VirtualizedTaskListProps> = React.memo(({
|
||||
});
|
||||
}, [group.id, groupTasks.length, height, listHeight]);
|
||||
|
||||
// Row renderer for virtualization
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const headerScrollRef = useRef<HTMLDivElement>(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 (
|
||||
<div style={style}>
|
||||
<div className="task-group-header">
|
||||
<div className="task-group-header-row">
|
||||
<div
|
||||
className="task-group-header-content"
|
||||
style={{
|
||||
backgroundColor: group.color || '#f0f0f0',
|
||||
borderLeft: `4px solid ${group.color || '#f0f0f0'}`
|
||||
}}
|
||||
>
|
||||
<span className="task-group-header-text">
|
||||
{group.title} ({groupTasks.length})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Column headers row
|
||||
if (index === 1) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<div
|
||||
className="task-group-column-headers"
|
||||
style={{ borderLeft: `4px solid ${group.color || '#f0f0f0'}` }}
|
||||
>
|
||||
<div className="task-group-column-headers-row">
|
||||
<div className="task-table-fixed-columns">
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '80px' }}>
|
||||
<span className="column-header-text">Key</span>
|
||||
</div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '475px' }}>
|
||||
<span className="column-header-text">Task</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="task-table-scrollable-columns">
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
|
||||
<span className="column-header-text">Progress</span>
|
||||
</div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
||||
<span className="column-header-text">Members</span>
|
||||
</div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '200px' }}>
|
||||
<span className="column-header-text">Labels</span>
|
||||
</div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
||||
<span className="column-header-text">Status</span>
|
||||
</div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
||||
<span className="column-header-text">Priority</span>
|
||||
</div>
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
|
||||
<span className="column-header-text">Time Tracking</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Task rows
|
||||
const taskIndex = index - 2;
|
||||
if (taskIndex >= 0 && taskIndex < groupTasks.length) {
|
||||
const task = groupTasks[taskIndex];
|
||||
return (
|
||||
<div
|
||||
className="task-row-container"
|
||||
style={{
|
||||
...style,
|
||||
'--group-color': group.color || '#f0f0f0'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<TaskRow
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id)}
|
||||
index={taskIndex}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
const task = groupTasks[index];
|
||||
if (!task) return null;
|
||||
return (
|
||||
<div
|
||||
className="task-row-container"
|
||||
style={{
|
||||
...style,
|
||||
'--group-color': group.color || '#f0f0f0'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<TaskRow
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
fixedColumns={fixedColumns}
|
||||
scrollableColumns={scrollableColumns}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]);
|
||||
|
||||
return (
|
||||
<div className="virtualized-task-list" style={{ height: height }}>
|
||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
||||
<List
|
||||
height={listHeight}
|
||||
width={width}
|
||||
itemCount={getItemCount()}
|
||||
itemSize={TASK_ROW_HEIGHT}
|
||||
overscanCount={15} // Render 15 extra items for smooth scrolling
|
||||
className="react-window-list"
|
||||
{/* Group Header */}
|
||||
<div className="task-group-header">
|
||||
<div className="task-group-header-row">
|
||||
<div
|
||||
className="task-group-header-content"
|
||||
style={{
|
||||
backgroundColor: group.color || '#f0f0f0',
|
||||
borderLeft: `4px solid ${group.color || '#f0f0f0'}`
|
||||
}}
|
||||
>
|
||||
<span className="task-group-header-text">
|
||||
{group.title} ({groupTasks.length})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Column Headers (sync scroll) */}
|
||||
<div
|
||||
className="task-group-column-headers-scroll"
|
||||
ref={headerScrollRef}
|
||||
style={{ overflowX: 'auto', overflowY: 'hidden' }}
|
||||
>
|
||||
<div
|
||||
className="task-group-column-headers"
|
||||
style={{ borderLeft: `4px solid ${group.color || '#f0f0f0'}`, minWidth: totalTableWidth, display: 'flex', position: 'relative' }}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
</SortableContext>
|
||||
|
||||
<div className="fixed-columns-header" style={{ display: 'flex', position: 'sticky', left: 0, zIndex: 2, background: 'inherit', width: fixedWidth }}>
|
||||
{fixedColumns.map(col => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="task-table-cell task-table-header-cell fixed-column"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<span className="column-header-text">{col.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="scrollable-columns-header" style={{ display: 'flex', minWidth: scrollableWidth }}>
|
||||
{scrollableColumns.map(col => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="task-table-cell task-table-header-cell"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<span className="column-header-text">{col.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Scrollable List */}
|
||||
<div
|
||||
className="task-list-scroll-container"
|
||||
ref={scrollContainerRef}
|
||||
style={{ overflowX: 'auto', overflowY: 'hidden', width: '100%', minWidth: totalTableWidth }}
|
||||
>
|
||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
||||
<List
|
||||
height={listHeight}
|
||||
width={width}
|
||||
itemCount={groupTasks.length}
|
||||
itemSize={TASK_ROW_HEIGHT}
|
||||
overscanCount={15}
|
||||
className="react-window-list"
|
||||
style={{ minWidth: totalTableWidth }}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
</SortableContext>
|
||||
</div>
|
||||
{/* Add Task Row - Always show at the bottom */}
|
||||
<div
|
||||
className="task-group-add-task"
|
||||
@@ -185,7 +211,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
>
|
||||
<AddTaskListRow groupId={group.id} />
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.virtualized-task-list {
|
||||
border: 1px solid var(--task-border-primary, #e8e8e8);
|
||||
@@ -199,11 +224,23 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.virtualized-task-list:last-child {
|
||||
margin-bottom: 0;
|
||||
.task-group-header {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
.task-group-column-headers-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
.task-group-column-headers {
|
||||
background: var(--task-bg-secondary, #f5f5f5);
|
||||
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 1200px;
|
||||
}
|
||||
.task-list-scroll-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-window-list {
|
||||
outline: none;
|
||||
flex: 1;
|
||||
@@ -211,19 +248,16 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.react-window-list-item {
|
||||
contain: layout style;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Task row container styles */
|
||||
.task-row-container {
|
||||
position: relative;
|
||||
background: var(--task-bg-primary, white);
|
||||
}
|
||||
|
||||
.task-row-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -234,24 +268,12 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
background-color: var(--group-color, #f0f0f0);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Ensure no gaps between list items */
|
||||
.react-window-list > div {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Task group header styles */
|
||||
.task-group-header {
|
||||
background: var(--task-bg-primary, white);
|
||||
transition: background-color 0.3s ease;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.task-group-header-row {
|
||||
display: inline-flex;
|
||||
height: auto;
|
||||
@@ -260,7 +282,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-group-header-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -273,37 +294,13 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.task-group-header-text {
|
||||
color: white !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Column headers styles */
|
||||
.task-group-column-headers {
|
||||
background: var(--task-bg-secondary, #f5f5f5);
|
||||
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
|
||||
transition: background-color 0.3s ease;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: sticky;
|
||||
top: 40px;
|
||||
z-index: 19;
|
||||
}
|
||||
|
||||
.task-group-column-headers-row {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
max-height: 40px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
min-width: 1200px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-table-header-cell {
|
||||
background: var(--task-bg-secondary, #f5f5f5);
|
||||
font-weight: 600;
|
||||
@@ -316,7 +313,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.column-header-text {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@@ -325,7 +321,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Add task row styles */
|
||||
.task-group-add-task {
|
||||
background: var(--task-bg-primary, white);
|
||||
@@ -338,11 +333,9 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.task-group-add-task:hover {
|
||||
background: var(--task-hover-bg, #fafafa);
|
||||
}
|
||||
|
||||
.task-table-fixed-columns {
|
||||
display: flex;
|
||||
background: var(--task-bg-secondary, #f5f5f5);
|
||||
@@ -353,13 +346,11 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.task-table-scrollable-columns {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-table-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -374,16 +365,13 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
color: var(--task-text-primary, #262626);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.task-table-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Performance optimizations */
|
||||
.virtualized-task-list {
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
:root {
|
||||
--task-bg-primary: #ffffff;
|
||||
@@ -402,7 +390,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
--task-drag-over-bg: #f0f8ff;
|
||||
--task-drag-over-border: #40a9ff;
|
||||
}
|
||||
|
||||
.dark .virtualized-task-list,
|
||||
[data-theme="dark"] .virtualized-task-list {
|
||||
--task-bg-primary: #1f1f1f;
|
||||
|
||||
Reference in New Issue
Block a user