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;
|
index?: number;
|
||||||
onSelect?: (taskId: string, selected: boolean) => void;
|
onSelect?: (taskId: string, selected: boolean) => void;
|
||||||
onToggleSubtasks?: (taskId: string) => 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
|
// Priority and status colors - moved outside component to avoid recreation
|
||||||
@@ -52,6 +55,9 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
index,
|
index,
|
||||||
onSelect,
|
onSelect,
|
||||||
onToggleSubtasks,
|
onToggleSubtasks,
|
||||||
|
columns,
|
||||||
|
fixedColumns,
|
||||||
|
scrollableColumns,
|
||||||
}) => {
|
}) => {
|
||||||
const { socket, connected } = useSocket();
|
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]">
|
<div className="flex h-10 max-h-10 overflow-visible relative min-w-[1200px]">
|
||||||
{/* Fixed Columns */}
|
{/* Fixed Columns */}
|
||||||
<div className={fixedColumnsClasses}>
|
<div
|
||||||
{/* Drag Handle */}
|
className="fixed-columns-row"
|
||||||
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
style={{
|
||||||
<Button
|
display: 'flex',
|
||||||
variant="text"
|
position: 'sticky',
|
||||||
size="small"
|
left: 0,
|
||||||
icon={<HolderOutlined />}
|
zIndex: 2,
|
||||||
className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
background: isDarkMode ? '#1a1a1a' : '#fff',
|
||||||
isDarkMode={isDarkMode}
|
width: fixedColumns?.reduce((sum, col) => sum + col.width, 0) || 0,
|
||||||
{...attributes}
|
}}
|
||||||
{...listeners}
|
>
|
||||||
/>
|
{fixedColumns?.map(col => {
|
||||||
</div>
|
switch (col.key) {
|
||||||
|
case 'drag':
|
||||||
{/* Selection Checkbox */}
|
return (
|
||||||
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
<div key={col.key} className="w-10 flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||||
<Checkbox
|
<Button
|
||||||
checked={isSelected}
|
variant="text"
|
||||||
onChange={handleSelectChange}
|
size="small"
|
||||||
isDarkMode={isDarkMode}
|
icon={<HolderOutlined />}
|
||||||
/>
|
className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
||||||
</div>
|
isDarkMode={isDarkMode}
|
||||||
|
{...attributes}
|
||||||
{/* Task Key */}
|
{...listeners}
|
||||||
<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>
|
||||||
</div>
|
);
|
||||||
</div>
|
case 'select':
|
||||||
</div>
|
return (
|
||||||
</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Columns */}
|
{/* Scrollable Columns */}
|
||||||
<div className="flex flex-1 min-w-0">
|
<div className="scrollable-columns-row" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
||||||
{/* Progress */}
|
{scrollableColumns?.map(col => {
|
||||||
<div className={`w-[90px] flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
switch (col.key) {
|
||||||
{task.progress !== undefined && task.progress >= 0 && (
|
case 'progress':
|
||||||
<Progress
|
return (
|
||||||
type="circle"
|
<div key={col.key} className="flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||||
percent={task.progress}
|
{task.progress !== undefined && task.progress >= 0 && (
|
||||||
size={24}
|
<Progress
|
||||||
strokeColor={task.progress === 100 ? '#52c41a' : '#1890ff'}
|
type="circle"
|
||||||
strokeWidth={2}
|
percent={task.progress}
|
||||||
showInfo={true}
|
size={24}
|
||||||
isDarkMode={isDarkMode}
|
strokeColor={task.progress === 100 ? '#52c41a' : '#1890ff'}
|
||||||
/>
|
strokeWidth={2}
|
||||||
)}
|
showInfo={true}
|
||||||
</div>
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
{/* Members */}
|
)}
|
||||||
<div className={`w-[150px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
);
|
||||||
{avatarGroupMembers.length > 0 && (
|
case 'members':
|
||||||
<AvatarGroup
|
return (
|
||||||
members={avatarGroupMembers}
|
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||||
size={24}
|
<div className="flex items-center gap-2">
|
||||||
maxCount={3}
|
{avatarGroupMembers.length > 0 && (
|
||||||
isDarkMode={isDarkMode}
|
<AvatarGroup
|
||||||
/>
|
members={avatarGroupMembers}
|
||||||
)}
|
size={24}
|
||||||
<button
|
maxCount={3}
|
||||||
className={`
|
isDarkMode={isDarkMode}
|
||||||
w-6 h-6 rounded-full border border-dashed flex items-center justify-center
|
/>
|
||||||
transition-colors duration-200
|
)}
|
||||||
${isDarkMode
|
<button
|
||||||
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
|
className={`
|
||||||
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
|
w-6 h-6 rounded-full border border-dashed flex items-center justify-center
|
||||||
}
|
transition-colors duration-200
|
||||||
`}
|
${isDarkMode
|
||||||
onClick={() => {
|
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
|
||||||
// TODO: Implement assignee selector functionality
|
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
|
||||||
console.log('Add assignee clicked for task:', task.id);
|
}
|
||||||
}}
|
`}
|
||||||
>
|
onClick={() => {
|
||||||
<span className="text-xs">+</span>
|
// TODO: Implement assignee selector functionality
|
||||||
</button>
|
console.log('Add assignee clicked for task:', task.id);
|
||||||
</div>
|
}}
|
||||||
</div>
|
>
|
||||||
|
<span className="text-xs">+</span>
|
||||||
{/* Labels */}
|
</button>
|
||||||
<div className={`w-[200px] max-w-[200px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
|
</div>
|
||||||
{task.labels?.map((label, index) => (
|
);
|
||||||
label.end && label.names && label.name ? (
|
case 'labels':
|
||||||
<CustomNumberLabel
|
return (
|
||||||
key={`${label.id}-${index}`}
|
<div key={col.key} className="max-w-[200px] flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||||
labelList={label.names}
|
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
|
||||||
namesString={label.name}
|
{task.labels?.map((label, index) => (
|
||||||
isDarkMode={isDarkMode}
|
label.end && label.names && label.name ? (
|
||||||
/>
|
<CustomNumberLabel
|
||||||
) : (
|
key={`${label.id}-${index}`}
|
||||||
<CustomColordLabel
|
labelList={label.names}
|
||||||
key={`${label.id}-${index}`}
|
namesString={label.name}
|
||||||
label={label}
|
isDarkMode={isDarkMode}
|
||||||
isDarkMode={isDarkMode}
|
/>
|
||||||
/>
|
) : (
|
||||||
)
|
<CustomColordLabel
|
||||||
))}
|
key={`${label.id}-${index}`}
|
||||||
<LabelsSelector
|
label={label}
|
||||||
task={taskAdapter}
|
isDarkMode={isDarkMode}
|
||||||
isDarkMode={isDarkMode}
|
/>
|
||||||
/>
|
)
|
||||||
</div>
|
))}
|
||||||
</div>
|
<LabelsSelector
|
||||||
|
task={taskAdapter}
|
||||||
{/* Status */}
|
isDarkMode={isDarkMode}
|
||||||
<div className={`w-[100px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
/>
|
||||||
<Tag
|
</div>
|
||||||
backgroundColor={getStatusColor(task.status)}
|
</div>
|
||||||
color="white"
|
);
|
||||||
className="text-xs font-medium uppercase"
|
case 'status':
|
||||||
>
|
return (
|
||||||
{task.status}
|
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||||
</Tag>
|
<Tag
|
||||||
</div>
|
backgroundColor={getStatusColor(task.status)}
|
||||||
|
color="white"
|
||||||
{/* Priority */}
|
className="text-xs font-medium uppercase"
|
||||||
<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">
|
{task.status}
|
||||||
<div
|
</Tag>
|
||||||
className="w-2 h-2 rounded-full"
|
</div>
|
||||||
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
);
|
||||||
/>
|
case 'priority':
|
||||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
return (
|
||||||
{task.priority}
|
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<div
|
||||||
</div>
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
||||||
{/* Time Tracking */}
|
/>
|
||||||
<div className={`w-[120px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||||
<div className="flex items-center gap-2 h-full overflow-hidden">
|
{task.priority}
|
||||||
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
</div>
|
||||||
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
|
</div>
|
||||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
);
|
||||||
{typeof task.timeTracking.logged === 'number'
|
case 'timeTracking':
|
||||||
? `${task.timeTracking.logged}h`
|
return (
|
||||||
: task.timeTracking.logged
|
<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">
|
||||||
</span>
|
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
||||||
</div>
|
<div className="flex items-center gap-1">
|
||||||
)}
|
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
|
||||||
</div>
|
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||||
</div>
|
{typeof task.timeTracking.logged === 'number'
|
||||||
|
? `${task.timeTracking.logged}h`
|
||||||
|
: task.timeTracking.logged
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { FixedSizeList as List } from 'react-window';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -64,120 +64,146 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
});
|
});
|
||||||
}, [group.id, groupTasks.length, height, listHeight]);
|
}, [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 }) => {
|
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
||||||
// Header row
|
const task = groupTasks[index];
|
||||||
if (index === 0) {
|
if (!task) return null;
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div
|
||||||
<div className="task-group-header">
|
className="task-row-container"
|
||||||
<div className="task-group-header-row">
|
style={{
|
||||||
<div
|
...style,
|
||||||
className="task-group-header-content"
|
'--group-color': group.color || '#f0f0f0'
|
||||||
style={{
|
} as React.CSSProperties}
|
||||||
backgroundColor: group.color || '#f0f0f0',
|
>
|
||||||
borderLeft: `4px solid ${group.color || '#f0f0f0'}`
|
<TaskRow
|
||||||
}}
|
task={task}
|
||||||
>
|
projectId={projectId}
|
||||||
<span className="task-group-header-text">
|
groupId={group.id}
|
||||||
{group.title} ({groupTasks.length})
|
currentGrouping={currentGrouping}
|
||||||
</span>
|
isSelected={selectedTaskIds.includes(task.id)}
|
||||||
</div>
|
index={index}
|
||||||
</div>
|
onSelect={onSelectTask}
|
||||||
</div>
|
onToggleSubtasks={onToggleSubtasks}
|
||||||
</div>
|
fixedColumns={fixedColumns}
|
||||||
);
|
scrollableColumns={scrollableColumns}
|
||||||
}
|
/>
|
||||||
|
</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;
|
|
||||||
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]);
|
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="virtualized-task-list" style={{ height: height }}>
|
<div className="virtualized-task-list" style={{ height: height }}>
|
||||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
{/* Group Header */}
|
||||||
<List
|
<div className="task-group-header">
|
||||||
height={listHeight}
|
<div className="task-group-header-row">
|
||||||
width={width}
|
<div
|
||||||
itemCount={getItemCount()}
|
className="task-group-header-content"
|
||||||
itemSize={TASK_ROW_HEIGHT}
|
style={{
|
||||||
overscanCount={15} // Render 15 extra items for smooth scrolling
|
backgroundColor: group.color || '#f0f0f0',
|
||||||
className="react-window-list"
|
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}
|
<div className="fixed-columns-header" style={{ display: 'flex', position: 'sticky', left: 0, zIndex: 2, background: 'inherit', width: fixedWidth }}>
|
||||||
</List>
|
{fixedColumns.map(col => (
|
||||||
</SortableContext>
|
<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 */}
|
{/* Add Task Row - Always show at the bottom */}
|
||||||
<div
|
<div
|
||||||
className="task-group-add-task"
|
className="task-group-add-task"
|
||||||
@@ -185,7 +211,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
>
|
>
|
||||||
<AddTaskListRow groupId={group.id} />
|
<AddTaskListRow groupId={group.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
.virtualized-task-list {
|
.virtualized-task-list {
|
||||||
border: 1px solid var(--task-border-primary, #e8e8e8);
|
border: 1px solid var(--task-border-primary, #e8e8e8);
|
||||||
@@ -199,11 +224,23 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.task-group-header {
|
||||||
.virtualized-task-list:last-child {
|
position: relative;
|
||||||
margin-bottom: 0;
|
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 {
|
.react-window-list {
|
||||||
outline: none;
|
outline: none;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -211,19 +248,16 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-window-list-item {
|
.react-window-list-item {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Task row container styles */
|
/* Task row container styles */
|
||||||
.task-row-container {
|
.task-row-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--task-bg-primary, white);
|
background: var(--task-bg-primary, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row-container::before {
|
.task-row-container::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -234,24 +268,12 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
background-color: var(--group-color, #f0f0f0);
|
background-color: var(--group-color, #f0f0f0);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure no gaps between list items */
|
/* Ensure no gaps between list items */
|
||||||
.react-window-list > div {
|
.react-window-list > div {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Task group header styles */
|
/* 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 {
|
.task-group-header-row {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -260,7 +282,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-group-header-content {
|
.task-group-header-content {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -273,37 +294,13 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-group-header-text {
|
.task-group-header-text {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
font-size: 13px !important;
|
font-size: 13px !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Column headers styles */
|
/* 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 {
|
.task-table-header-cell {
|
||||||
background: var(--task-bg-secondary, #f5f5f5);
|
background: var(--task-bg-secondary, #f5f5f5);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -316,7 +313,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header-text {
|
.column-header-text {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -325,7 +321,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add task row styles */
|
/* Add task row styles */
|
||||||
.task-group-add-task {
|
.task-group-add-task {
|
||||||
background: var(--task-bg-primary, white);
|
background: var(--task-bg-primary, white);
|
||||||
@@ -338,11 +333,9 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-group-add-task:hover {
|
.task-group-add-task:hover {
|
||||||
background: var(--task-hover-bg, #fafafa);
|
background: var(--task-hover-bg, #fafafa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-table-fixed-columns {
|
.task-table-fixed-columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--task-bg-secondary, #f5f5f5);
|
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);
|
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-table-scrollable-columns {
|
.task-table-scrollable-columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-table-cell {
|
.task-table-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -374,16 +365,13 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
color: var(--task-text-primary, #262626);
|
color: var(--task-text-primary, #262626);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-table-cell:last-child {
|
.task-table-cell:last-child {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Performance optimizations */
|
/* Performance optimizations */
|
||||||
.virtualized-task-list {
|
.virtualized-task-list {
|
||||||
contain: layout style paint;
|
contain: layout style paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode support */
|
/* Dark mode support */
|
||||||
:root {
|
:root {
|
||||||
--task-bg-primary: #ffffff;
|
--task-bg-primary: #ffffff;
|
||||||
@@ -402,7 +390,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
--task-drag-over-bg: #f0f8ff;
|
--task-drag-over-bg: #f0f8ff;
|
||||||
--task-drag-over-border: #40a9ff;
|
--task-drag-over-border: #40a9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .virtualized-task-list,
|
.dark .virtualized-task-list,
|
||||||
[data-theme="dark"] .virtualized-task-list {
|
[data-theme="dark"] .virtualized-task-list {
|
||||||
--task-bg-primary: #1f1f1f;
|
--task-bg-primary: #1f1f1f;
|
||||||
|
|||||||
Reference in New Issue
Block a user