- Updated import statements for consistency and clarity. - Refined task sorting and update logic to improve responsiveness. - Enhanced error logging for better debugging during task sort order changes. - Increased overscan count in virtualized task lists for smoother scrolling experience. - Introduced lazy loading for heavy components to reduce initial load times. - Improved CSS styles for better responsiveness and user interaction across task management components.
742 lines
27 KiB
TypeScript
742 lines
27 KiB
TypeScript
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';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Empty } from 'antd';
|
|
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
|
import { Task } from '@/types/task-management.types';
|
|
import TaskRow from './task-row';
|
|
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
|
import { RootState } from '@/app/store';
|
|
import { TaskListField } from '@/features/task-management/taskListFields.slice';
|
|
import { Checkbox } from '@/components';
|
|
|
|
interface VirtualizedTaskListProps {
|
|
group: any;
|
|
projectId: string;
|
|
currentGrouping: 'status' | 'priority' | 'phase';
|
|
selectedTaskIds: string[];
|
|
onSelectTask: (taskId: string, selected: boolean) => void;
|
|
onToggleSubtasks: (taskId: string) => void;
|
|
height: number;
|
|
width: number;
|
|
tasksById: Record<string, Task>;
|
|
}
|
|
|
|
const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|
group,
|
|
projectId,
|
|
currentGrouping,
|
|
selectedTaskIds,
|
|
onSelectTask,
|
|
onToggleSubtasks,
|
|
height,
|
|
width,
|
|
tasksById
|
|
}) => {
|
|
const { t } = useTranslation('task-management');
|
|
|
|
// Get theme from Redux store
|
|
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
|
|
|
// Get field visibility from taskListFields slice
|
|
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
|
|
|
// PERFORMANCE OPTIMIZATION: Improved virtualization for better user experience
|
|
const VIRTUALIZATION_THRESHOLD = 25; // Increased threshold - virtualize when there are more tasks
|
|
const TASK_ROW_HEIGHT = 40;
|
|
const HEADER_HEIGHT = 40;
|
|
const COLUMN_HEADER_HEIGHT = 40;
|
|
const ADD_TASK_ROW_HEIGHT = 40;
|
|
|
|
// PERFORMANCE OPTIMIZATION: Batch rendering to prevent long tasks
|
|
const RENDER_BATCH_SIZE = 5; // Render max 5 tasks per frame
|
|
const FRAME_BUDGET_MS = 8; // Leave 8ms per frame for other operations
|
|
|
|
// PERFORMANCE OPTIMIZATION: Add early return for empty groups
|
|
if (!group || !group.taskIds || group.taskIds.length === 0) {
|
|
const emptyGroupHeight = HEADER_HEIGHT + COLUMN_HEADER_HEIGHT + 120 + ADD_TASK_ROW_HEIGHT; // 120px for empty state
|
|
|
|
return (
|
|
<div className="virtualized-task-list empty-group" style={{ height: emptyGroupHeight, position: 'relative' }}>
|
|
{/* Sticky Group Color Border */}
|
|
<div
|
|
className="sticky-group-border"
|
|
style={{
|
|
position: 'absolute',
|
|
left: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
width: '4px',
|
|
backgroundColor: group?.color || '#f0f0f0',
|
|
zIndex: 15,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
|
|
<div className="task-group-header" style={{ height: HEADER_HEIGHT }}>
|
|
<div className="task-group-header-row">
|
|
<div
|
|
className="task-group-header-content"
|
|
style={{
|
|
backgroundColor: group?.color || '#f0f0f0',
|
|
// No margin - header should overlap the sticky border
|
|
}}
|
|
>
|
|
<span className="task-group-header-text">
|
|
{group?.title || 'Empty Group'} (0)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Column Headers */}
|
|
<div className="task-group-column-headers" style={{
|
|
marginLeft: '4px', // Account for sticky border
|
|
height: COLUMN_HEADER_HEIGHT,
|
|
background: 'var(--task-bg-secondary, #f5f5f5)',
|
|
borderBottom: '1px solid var(--task-border-tertiary, #d9d9d9)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
paddingLeft: '12px'
|
|
}}>
|
|
<span className="column-header-text" style={{ fontSize: '11px', fontWeight: 600, color: 'var(--task-text-secondary, #595959)' }}>
|
|
TASKS
|
|
</span>
|
|
</div>
|
|
|
|
{/* Empty State */}
|
|
<div className="empty-tasks-container" style={{
|
|
height: '120px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginLeft: '4px', // Account for sticky border
|
|
backgroundColor: 'var(--task-bg-primary, white)'
|
|
}}>
|
|
<Empty
|
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
description={
|
|
<div style={{ textAlign: 'center' }}>
|
|
<div style={{ fontSize: '14px', fontWeight: 500, color: 'var(--task-text-primary, #262626)', marginBottom: '4px' }}>
|
|
{t('noTasksInGroup')}
|
|
</div>
|
|
<div style={{ fontSize: '12px', color: 'var(--task-text-secondary, #595959)' }}>
|
|
{t('noTasksInGroupDescription')}
|
|
</div>
|
|
</div>
|
|
}
|
|
style={{
|
|
margin: 0,
|
|
padding: '12px'
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="task-group-add-task" style={{ marginLeft: '4px', height: ADD_TASK_ROW_HEIGHT }}>
|
|
<AddTaskListRow groupId={group?.id} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// PERFORMANCE OPTIMIZATION: Get tasks for this group using direct lookup (no mapping/filtering)
|
|
const groupTasks = useMemo(() => {
|
|
// PERFORMANCE OPTIMIZATION: Use for loop instead of map for better performance
|
|
const tasks: Task[] = [];
|
|
for (let i = 0; i < group.taskIds.length; i++) {
|
|
const task = tasksById[group.taskIds[i]];
|
|
if (task) {
|
|
tasks.push(task);
|
|
}
|
|
}
|
|
return tasks;
|
|
}, [group.taskIds, tasksById]);
|
|
|
|
// PERFORMANCE OPTIMIZATION: Only calculate selection state when needed
|
|
const selectionState = useMemo(() => {
|
|
if (groupTasks.length === 0) {
|
|
return { isAllSelected: false, isIndeterminate: false };
|
|
}
|
|
|
|
// PERFORMANCE OPTIMIZATION: Use for loop instead of filter for better performance
|
|
let selectedCount = 0;
|
|
for (let i = 0; i < groupTasks.length; i++) {
|
|
if (selectedTaskIds.includes(groupTasks[i].id)) {
|
|
selectedCount++;
|
|
}
|
|
}
|
|
|
|
const isAllSelected = selectedCount === groupTasks.length;
|
|
const isIndeterminate = selectedCount > 0 && selectedCount < groupTasks.length;
|
|
|
|
return { isAllSelected, isIndeterminate };
|
|
}, [groupTasks, selectedTaskIds]);
|
|
|
|
// Handle select all tasks in group - optimized with useCallback
|
|
const handleSelectAllInGroup = useCallback((checked: boolean) => {
|
|
// PERFORMANCE OPTIMIZATION: Batch selection updates
|
|
const tasksToUpdate: Array<{ taskId: string; selected: boolean }> = [];
|
|
|
|
if (checked) {
|
|
// Select all tasks in the group
|
|
for (let i = 0; i < groupTasks.length; i++) {
|
|
const task = groupTasks[i];
|
|
if (!selectedTaskIds.includes(task.id)) {
|
|
tasksToUpdate.push({ taskId: task.id, selected: true });
|
|
}
|
|
}
|
|
} else {
|
|
// Deselect all tasks in the group
|
|
for (let i = 0; i < groupTasks.length; i++) {
|
|
const task = groupTasks[i];
|
|
if (selectedTaskIds.includes(task.id)) {
|
|
tasksToUpdate.push({ taskId: task.id, selected: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Batch update all selections
|
|
tasksToUpdate.forEach(({ taskId, selected }) => {
|
|
onSelectTask(taskId, selected);
|
|
});
|
|
}, [groupTasks, selectedTaskIds, onSelectTask]);
|
|
|
|
// PERFORMANCE OPTIMIZATION: Use passed height prop and calculate available space for tasks
|
|
const taskRowsHeight = groupTasks.length * TASK_ROW_HEIGHT;
|
|
const groupHeight = height; // Use the height passed from parent
|
|
const availableTaskRowsHeight = Math.max(0, groupHeight - HEADER_HEIGHT - COLUMN_HEADER_HEIGHT - ADD_TASK_ROW_HEIGHT);
|
|
|
|
// PERFORMANCE OPTIMIZATION: Limit visible columns for large lists
|
|
const maxVisibleColumns = groupTasks.length > 50 ? 6 : 12; // Further reduce columns for large lists
|
|
|
|
// Define all possible columns
|
|
const allFixedColumns = [
|
|
{ key: 'drag', label: '', width: 40, alwaysVisible: true },
|
|
{ key: 'select', label: '', width: 40, alwaysVisible: true },
|
|
{ key: 'key', label: 'KEY', width: 80, fieldKey: 'KEY' },
|
|
{ key: 'task', label: 'TASK', width: 474, alwaysVisible: true },
|
|
];
|
|
|
|
const allScrollableColumns = [
|
|
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
|
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
|
{ key: 'status', label: 'Status', width: 140, fieldKey: 'STATUS' },
|
|
{ key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
|
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
|
{ key: 'phase', label: 'Phase', width: 100, fieldKey: 'PHASE' },
|
|
{ key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
|
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
|
{ key: 'estimation', label: 'Estimation', width: 100, fieldKey: 'ESTIMATION' },
|
|
{ key: 'startDate', label: 'Start Date', width: 120, fieldKey: 'START_DATE' },
|
|
{ key: 'dueDate', label: 'Due Date', width: 120, fieldKey: 'DUE_DATE' },
|
|
{ key: 'dueTime', label: 'Due Time', width: 100, fieldKey: 'DUE_TIME' },
|
|
{ key: 'completedDate', label: 'Completed Date', width: 130, fieldKey: 'COMPLETED_DATE' },
|
|
{ key: 'createdDate', label: 'Created Date', width: 120, fieldKey: 'CREATED_DATE' },
|
|
{ key: 'lastUpdated', label: 'Last Updated', width: 130, fieldKey: 'LAST_UPDATED' },
|
|
{ key: 'reporter', label: 'Reporter', width: 100, fieldKey: 'REPORTER' },
|
|
];
|
|
|
|
// Filter columns based on field visibility
|
|
const fixedColumns = useMemo(() => {
|
|
return allFixedColumns.filter(col => {
|
|
// Always show columns marked as alwaysVisible
|
|
if (col.alwaysVisible) return true;
|
|
|
|
// For other columns, check field visibility
|
|
if (col.fieldKey) {
|
|
const field = taskListFields.find(f => f.key === col.fieldKey);
|
|
return field?.visible ?? false;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}, [taskListFields, allFixedColumns]);
|
|
|
|
const scrollableColumns = useMemo(() => {
|
|
const filtered = allScrollableColumns.filter(col => {
|
|
// For scrollable columns, check field visibility
|
|
if (col.fieldKey) {
|
|
const field = taskListFields.find(f => f.key === col.fieldKey);
|
|
return field?.visible ?? false;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
// PERFORMANCE OPTIMIZATION: Limit columns for large lists
|
|
return filtered.slice(0, maxVisibleColumns);
|
|
}, [taskListFields, allScrollableColumns, maxVisibleColumns]);
|
|
|
|
const fixedWidth = fixedColumns.reduce((sum, col) => sum + col.width, 0);
|
|
const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
|
|
const totalTableWidth = fixedWidth + scrollableWidth;
|
|
|
|
// PERFORMANCE OPTIMIZATION: Enhanced overscan for smoother scrolling experience
|
|
const shouldVirtualize = groupTasks.length > VIRTUALIZATION_THRESHOLD;
|
|
const overscanCount = useMemo(() => {
|
|
if (groupTasks.length <= 20) return 5; // Small lists: 5 items overscan
|
|
if (groupTasks.length <= 100) return 10; // Medium lists: 10 items overscan
|
|
if (groupTasks.length <= 500) return 15; // Large lists: 15 items overscan
|
|
return 20; // Very large lists: 20 items overscan for smooth scrolling
|
|
}, [groupTasks.length]);
|
|
|
|
// PERFORMANCE OPTIMIZATION: Memoize row renderer with better dependency management
|
|
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
|
const task: Task | undefined = groupTasks[index];
|
|
if (!task) return null;
|
|
|
|
// PERFORMANCE OPTIMIZATION: Pre-calculate selection state
|
|
const isSelected = selectedTaskIds.includes(task.id);
|
|
|
|
return (
|
|
<div
|
|
className="task-row-container"
|
|
style={{
|
|
...style,
|
|
marginLeft: '4px', // Account for sticky border
|
|
'--group-color': group.color || '#f0f0f0',
|
|
contain: 'layout style', // CSS containment for better performance
|
|
} as React.CSSProperties}
|
|
>
|
|
<TaskRow
|
|
task={task}
|
|
projectId={projectId}
|
|
groupId={group.id}
|
|
currentGrouping={currentGrouping}
|
|
isSelected={isSelected}
|
|
index={index}
|
|
onSelect={onSelectTask}
|
|
onToggleSubtasks={onToggleSubtasks}
|
|
fixedColumns={fixedColumns}
|
|
scrollableColumns={scrollableColumns}
|
|
/>
|
|
</div>
|
|
);
|
|
}, [group.id, group.color, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, fixedColumns, scrollableColumns]);
|
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
const headerScrollRef = useRef<HTMLDivElement>(null);
|
|
|
|
// PERFORMANCE OPTIMIZATION: Throttled scroll handler
|
|
const handleScroll = useCallback(() => {
|
|
if (headerScrollRef.current && scrollContainerRef.current) {
|
|
headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft;
|
|
}
|
|
}, []);
|
|
|
|
// Synchronize header scroll with body scroll
|
|
useEffect(() => {
|
|
const scrollDiv = scrollContainerRef.current;
|
|
if (scrollDiv) {
|
|
scrollDiv.addEventListener('scroll', handleScroll, { passive: true });
|
|
}
|
|
return () => {
|
|
if (scrollDiv) {
|
|
scrollDiv.removeEventListener('scroll', handleScroll);
|
|
}
|
|
};
|
|
}, [handleScroll]);
|
|
|
|
return (
|
|
<div className="virtualized-task-list" style={{ height: groupHeight, position: 'relative' }}>
|
|
{/* Sticky Group Color Border */}
|
|
<div
|
|
className="sticky-group-border"
|
|
style={{
|
|
position: 'absolute',
|
|
left: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
width: '4px',
|
|
backgroundColor: group.color || '#f0f0f0',
|
|
zIndex: 15,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
|
|
{/* Group Header */}
|
|
<div className="task-group-header" style={{ height: HEADER_HEIGHT }}>
|
|
<div className="task-group-header-row">
|
|
<div
|
|
className="task-group-header-content"
|
|
style={{
|
|
backgroundColor: group.color || '#f0f0f0',
|
|
// No margin - header should overlap the sticky border
|
|
}}
|
|
>
|
|
<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', height: COLUMN_HEADER_HEIGHT }}
|
|
>
|
|
<div
|
|
className="task-group-column-headers"
|
|
style={{ marginLeft: '4px', minWidth: totalTableWidth, display: 'flex', position: 'relative' }}
|
|
>
|
|
<div
|
|
className="task-table-fixed-columns fixed-columns-header"
|
|
style={{
|
|
display: 'flex',
|
|
position: 'sticky',
|
|
left: 0,
|
|
zIndex: 12,
|
|
background: isDarkMode ? 'var(--task-bg-secondary, #141414)' : 'var(--task-bg-secondary, #f5f5f5)',
|
|
width: fixedWidth,
|
|
borderRight: scrollableColumns.length > 0 ? '2px solid var(--task-border-primary, #e8e8e8)' : 'none',
|
|
boxShadow: scrollableColumns.length > 0 ? '2px 0 4px rgba(0, 0, 0, 0.1)' : 'none',
|
|
}}
|
|
>
|
|
{fixedColumns.map(col => (
|
|
<div
|
|
key={col.key}
|
|
className="task-table-cell task-table-header-cell fixed-column"
|
|
style={{ width: col.width }}
|
|
>
|
|
{col.key === 'select' ? (
|
|
<div className="flex items-center justify-center h-full">
|
|
<Checkbox
|
|
checked={selectionState.isAllSelected}
|
|
onChange={handleSelectAllInGroup}
|
|
isDarkMode={isDarkMode}
|
|
indeterminate={selectionState.isIndeterminate}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<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 - only task rows */}
|
|
<div
|
|
className="task-list-scroll-container"
|
|
ref={scrollContainerRef}
|
|
style={{
|
|
overflowX: 'auto',
|
|
overflowY: 'auto',
|
|
width: '100%',
|
|
minWidth: totalTableWidth,
|
|
height: groupTasks.length > 0 ? availableTaskRowsHeight : 'auto',
|
|
contain: 'layout style', // CSS containment for better performance
|
|
}}
|
|
>
|
|
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
|
{shouldVirtualize ? (
|
|
<List
|
|
height={availableTaskRowsHeight}
|
|
width={totalTableWidth}
|
|
itemCount={groupTasks.length}
|
|
itemSize={TASK_ROW_HEIGHT}
|
|
overscanCount={overscanCount}
|
|
className="react-window-list"
|
|
style={{ minWidth: totalTableWidth }}
|
|
// PERFORMANCE OPTIMIZATION: Remove all expensive props for maximum performance
|
|
useIsScrolling={false}
|
|
itemData={undefined}
|
|
// Disable all animations and transitions
|
|
onItemsRendered={() => {}}
|
|
onScroll={() => {}}
|
|
>
|
|
{Row}
|
|
</List>
|
|
) : (
|
|
// PERFORMANCE OPTIMIZATION: Use React.Fragment to reduce DOM nodes
|
|
<React.Fragment>
|
|
{groupTasks.map((task: Task, index: number) => {
|
|
// PERFORMANCE OPTIMIZATION: Pre-calculate selection state
|
|
const isSelected = selectedTaskIds.includes(task.id);
|
|
|
|
return (
|
|
<div
|
|
key={task.id}
|
|
className="task-row-container"
|
|
style={{
|
|
height: TASK_ROW_HEIGHT,
|
|
marginLeft: '4px', // Account for sticky border
|
|
'--group-color': group.color || '#f0f0f0',
|
|
contain: 'layout style', // CSS containment
|
|
} as React.CSSProperties}
|
|
>
|
|
<TaskRow
|
|
task={task}
|
|
projectId={projectId}
|
|
groupId={group.id}
|
|
currentGrouping={currentGrouping}
|
|
isSelected={isSelected}
|
|
index={index}
|
|
onSelect={onSelectTask}
|
|
onToggleSubtasks={onToggleSubtasks}
|
|
fixedColumns={fixedColumns}
|
|
scrollableColumns={scrollableColumns}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</React.Fragment>
|
|
)}
|
|
</SortableContext>
|
|
</div>
|
|
{/* Add Task Row - Always show at the bottom */}
|
|
<div
|
|
className="task-group-add-task"
|
|
style={{ marginLeft: '4px', height: ADD_TASK_ROW_HEIGHT }}
|
|
>
|
|
<AddTaskListRow groupId={group.id} />
|
|
</div>
|
|
<style>{`
|
|
.virtualized-task-list {
|
|
border: 1px solid var(--task-border-primary, #e8e8e8);
|
|
border-radius: 8px;
|
|
background: var(--task-bg-primary, white);
|
|
box-shadow: 0 1px 3px var(--task-shadow, rgba(0, 0, 0, 0.1));
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.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 {
|
|
scrollbar-width: none; /* Firefox */
|
|
}
|
|
.task-list-scroll-container::-webkit-scrollbar {
|
|
display: none; /* Chrome, Safari, Opera */
|
|
}
|
|
.react-window-list {
|
|
outline: none;
|
|
flex: 1;
|
|
min-height: 0;
|
|
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);
|
|
}
|
|
/* Ensure no gaps between list items */
|
|
.react-window-list > div {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
/* Task group header styles */
|
|
.task-group-header-row {
|
|
display: inline-flex;
|
|
height: inherit;
|
|
max-height: none;
|
|
overflow: hidden;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.task-group-header-content {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
border-radius: 6px 6px 0 0;
|
|
color: white;
|
|
font-weight: 500;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.3s ease;
|
|
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-table-header-cell {
|
|
background: var(--task-bg-secondary, #f5f5f5);
|
|
font-weight: 600;
|
|
color: var(--task-text-secondary, #595959);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
|
|
height: 32px;
|
|
max-height: 32px;
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
.column-header-text {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--task-text-secondary, #595959);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
transition: color 0.3s ease;
|
|
}
|
|
/* Add task row styles - Fixed width responsive design */
|
|
.task-group-add-task {
|
|
background: var(--task-bg-primary, white);
|
|
border-top: 1px solid var(--task-border-secondary, #f0f0f0);
|
|
transition: all 0.3s ease;
|
|
padding: 0 12px;
|
|
width: 100%;
|
|
max-width: 500px; /* Fixed maximum width */
|
|
min-width: 300px; /* Minimum width for mobile */
|
|
min-height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
border-radius: 0 0 6px 6px;
|
|
margin-left: 0;
|
|
position: relative;
|
|
}
|
|
.task-group-add-task:hover {
|
|
background: var(--task-hover-bg, #fafafa);
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
/* Responsive adjustments for add task row */
|
|
@media (max-width: 768px) {
|
|
.task-group-add-task {
|
|
max-width: 400px;
|
|
min-width: 280px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.task-group-add-task {
|
|
max-width: calc(100vw - 40px);
|
|
min-width: 250px;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1200px) {
|
|
.task-group-add-task {
|
|
max-width: 600px;
|
|
}
|
|
}
|
|
.task-table-fixed-columns {
|
|
display: flex;
|
|
background: var(--task-bg-secondary, #f5f5f5);
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 11;
|
|
border-right: 2px solid var(--task-border-primary, #e8e8e8);
|
|
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;
|
|
padding: 0 12px;
|
|
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
height: 40px;
|
|
max-height: 40px;
|
|
min-height: 40px;
|
|
overflow: hidden;
|
|
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;
|
|
will-change: scroll-position;
|
|
}
|
|
.task-row-container {
|
|
contain: layout style;
|
|
will-change: transform;
|
|
}
|
|
.react-window-list {
|
|
contain: strict;
|
|
}
|
|
/* Reduce repaints during scrolling */
|
|
.task-list-scroll-container {
|
|
contain: layout style;
|
|
transform: translateZ(0); /* Force GPU layer */
|
|
}
|
|
/* Dark mode support */
|
|
:root {
|
|
--task-bg-primary: #ffffff;
|
|
--task-bg-secondary: #f5f5f5;
|
|
--task-bg-tertiary: #f8f9fa;
|
|
--task-border-primary: #e8e8e8;
|
|
--task-border-secondary: #f0f0f0;
|
|
--task-border-tertiary: #d9d9d9;
|
|
--task-text-primary: #262626;
|
|
--task-text-secondary: #595959;
|
|
--task-text-tertiary: #8c8c8c;
|
|
--task-shadow: rgba(0, 0, 0, 0.1);
|
|
--task-hover-bg: #fafafa;
|
|
--task-selected-bg: #e6f7ff;
|
|
--task-selected-border: #1890ff;
|
|
--task-drag-over-bg: #f0f8ff;
|
|
--task-drag-over-border: #40a9ff;
|
|
}
|
|
.dark .virtualized-task-list,
|
|
[data-theme="dark"] .virtualized-task-list {
|
|
--task-bg-primary: #1f1f1f;
|
|
--task-bg-secondary: #141414;
|
|
--task-bg-tertiary: #262626;
|
|
--task-border-primary: #303030;
|
|
--task-border-secondary: #404040;
|
|
--task-border-tertiary: #505050;
|
|
--task-text-primary: #ffffff;
|
|
--task-text-secondary: #d9d9d9;
|
|
--task-text-tertiary: #8c8c8c;
|
|
--task-shadow: rgba(0, 0, 0, 0.3);
|
|
--task-hover-bg: #2a2a2a;
|
|
--task-selected-bg: #1a2332;
|
|
--task-selected-border: #1890ff;
|
|
--task-drag-over-bg: #1a2332;
|
|
--task-drag-over-border: #40a9ff;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export default VirtualizedTaskList;
|