feat(tasks): optimize task retrieval and performance metrics logging
- Updated `getList` and `getTasksOnly` methods to skip expensive progress calculations by default, enhancing performance. - Introduced logging for performance metrics, including method execution times and warnings for deprecated methods. - Added new `getTaskProgressStatus` endpoint to provide basic progress stats without heavy calculations. - Implemented performance optimizations in the frontend, including lazy loading and improved rendering for task rows. - Enhanced task management slice with reset actions for better state management. - Added localization support for task management messages in multiple languages.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
||||
import { Card, Spin, Empty } from 'antd';
|
||||
import { Card, Spin, Empty, Alert } from 'antd';
|
||||
import { RootState } from '@/app/store';
|
||||
import {
|
||||
taskManagementSelectors,
|
||||
@@ -42,12 +43,15 @@ import TaskRow from './task-row';
|
||||
// import BulkActionBar from './bulk-action-bar';
|
||||
import VirtualizedTaskList from './virtualized-task-list';
|
||||
import { AppDispatch } from '@/app/store';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
// Import the improved TaskListFilters component
|
||||
const ImprovedTaskFilters = React.lazy(
|
||||
() => import('./improved-task-filters')
|
||||
);
|
||||
|
||||
|
||||
|
||||
interface TaskListBoardProps {
|
||||
projectId: string;
|
||||
className?: string;
|
||||
@@ -84,11 +88,16 @@ const throttle = <T extends (...args: any[]) => void>(func: T, delay: number): T
|
||||
|
||||
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { t } = useTranslation('task-management');
|
||||
const [dragState, setDragState] = useState<DragState>({
|
||||
activeTask: null,
|
||||
activeGroupId: null,
|
||||
});
|
||||
|
||||
// Prevent duplicate API calls in React StrictMode
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
|
||||
// Refs for performance optimization
|
||||
const dragOverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -98,10 +107,10 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
|
||||
// Redux selectors using V3 API (pre-processed data, minimal loops)
|
||||
const tasks = useSelector(taskManagementSelectors.selectAll);
|
||||
const taskGroups = useSelector(selectTaskGroupsV3); // Pre-processed groups from backend
|
||||
const currentGrouping = useSelector(selectCurrentGroupingV3); // Current grouping from backend
|
||||
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
|
||||
const currentGrouping = useSelector(selectCurrentGroupingV3, shallowEqual);
|
||||
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
||||
const loading = useSelector((state: RootState) => state.taskManagement.loading);
|
||||
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
|
||||
const error = useSelector((state: RootState) => state.taskManagement.error);
|
||||
|
||||
// Get theme from Redux store
|
||||
@@ -121,16 +130,20 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
|
||||
// Fetch task groups when component mounts or dependencies change
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
if (projectId && !hasInitialized.current) {
|
||||
hasInitialized.current = true;
|
||||
|
||||
// Fetch real tasks from V3 API (minimal processing needed)
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
}, [dispatch, projectId, currentGrouping]);
|
||||
}, [projectId, dispatch]);
|
||||
|
||||
// Memoized calculations - optimized
|
||||
const allTaskIds = useMemo(() => tasks.map(task => task.id), [tasks]);
|
||||
const totalTasksCount = useMemo(() => tasks.length, [tasks]);
|
||||
const hasSelection = selectedTaskIds.length > 0;
|
||||
const totalTasks = useMemo(() => {
|
||||
return taskGroups.reduce((total, g) => total + g.taskIds.length, 0);
|
||||
}, [taskGroups]);
|
||||
|
||||
const hasAnyTasks = useMemo(() => totalTasks > 0, [totalTasks]);
|
||||
|
||||
// Memoized handlers for better performance
|
||||
const handleGroupingChange = useCallback(
|
||||
@@ -299,7 +312,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (sourceGroup.id !== targetGroup.id || sourceIndex !== finalTargetIndex) {
|
||||
// Calculate new order values - simplified
|
||||
const allTasksInTargetGroup = targetGroup.taskIds.map(
|
||||
id => tasks.find(t => t.id === id)!
|
||||
(id: string) => tasks.find((t: any) => t.id === id)!
|
||||
);
|
||||
const newOrder = allTasksInTargetGroup.map((task, index) => {
|
||||
if (index < finalTargetIndex) return task.order;
|
||||
@@ -310,7 +323,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
// Dispatch reorder action
|
||||
dispatch(
|
||||
reorderTasks({
|
||||
taskIds: [activeTaskId, ...allTasksInTargetGroup.map(t => t.id)],
|
||||
taskIds: [activeTaskId, ...allTasksInTargetGroup.map((t: any) => t.id)],
|
||||
newOrder: [currentDragState.activeTask!.order, ...newOrder],
|
||||
})
|
||||
);
|
||||
@@ -374,6 +387,10 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Task Filters */}
|
||||
<div className="mb-4">
|
||||
<React.Suspense fallback={<div>Loading filters...</div>}>
|
||||
@@ -391,17 +408,32 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
</Card>
|
||||
) : taskGroups.length === 0 ? (
|
||||
<Card>
|
||||
<Empty description="No tasks found" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty
|
||||
description={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '14px', fontWeight: 500, marginBottom: '4px' }}>
|
||||
No task groups available
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--task-text-secondary, #595959)' }}>
|
||||
Create tasks to see them organized in groups
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="virtualized-task-groups">
|
||||
{taskGroups.map((group, index) => {
|
||||
// Calculate dynamic height for each group
|
||||
// PERFORMANCE OPTIMIZATION: Optimized height calculations
|
||||
const groupTasks = group.taskIds.length;
|
||||
const baseHeight = 120; // Header + column headers + add task row
|
||||
const taskRowsHeight = groupTasks * 40; // 40px per task row
|
||||
const minGroupHeight = 300; // Minimum height for better visual appearance
|
||||
const maxGroupHeight = 600; // Increased maximum height per group
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Dynamic height based on task count and virtualization
|
||||
const shouldVirtualizeGroup = groupTasks > 20;
|
||||
const minGroupHeight = shouldVirtualizeGroup ? 200 : 150; // Smaller minimum for non-virtualized
|
||||
const maxGroupHeight = shouldVirtualizeGroup ? 800 : 400; // Different max based on virtualization
|
||||
const calculatedHeight = baseHeight + taskRowsHeight;
|
||||
const groupHeight = Math.max(
|
||||
minGroupHeight,
|
||||
@@ -457,12 +489,14 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
position: relative;
|
||||
/* GPU acceleration for drag operations */
|
||||
transform: translateZ(0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.virtualized-task-group {
|
||||
.virtualized-task-list {
|
||||
border: 1px solid var(--task-border-primary, #e8e8e8);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--task-bg-primary, white);
|
||||
box-shadow: 0 1px 3px var(--task-shadow, rgba(0, 0, 0, 0.1));
|
||||
overflow: hidden;
|
||||
@@ -470,10 +504,6 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.virtualized-task-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Task group header styles */
|
||||
.task-group-header {
|
||||
background: var(--task-bg-primary, white);
|
||||
@@ -631,6 +661,15 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Empty state styles */
|
||||
.empty-tasks-container .ant-empty-description {
|
||||
color: var(--task-text-secondary, #595959);
|
||||
}
|
||||
|
||||
.empty-tasks-container .ant-empty-image svg {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
:root {
|
||||
--task-bg-primary: #ffffff;
|
||||
@@ -669,6 +708,17 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
--task-drag-over-border: #40a9ff;
|
||||
}
|
||||
|
||||
/* Dark mode empty state */
|
||||
.dark .empty-tasks-container .ant-empty-description,
|
||||
[data-theme="dark"] .empty-tasks-container .ant-empty-description {
|
||||
color: var(--task-text-secondary, #d9d9d9);
|
||||
}
|
||||
|
||||
.dark .empty-tasks-container .ant-empty-image svg,
|
||||
[data-theme="dark"] .empty-tasks-container .ant-empty-image svg {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Performance optimizations */
|
||||
.virtualized-task-group {
|
||||
contain: layout style paint;
|
||||
|
||||
@@ -25,6 +25,38 @@
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/* PERFORMANCE OPTIMIZATION: Progressive loading states */
|
||||
.task-row-optimized.initial-load {
|
||||
contain: strict;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
.task-row-optimized.fully-loaded {
|
||||
contain: layout style;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Optimize initial render performance */
|
||||
.task-row-optimized.initial-load * {
|
||||
contain: layout;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
.task-row-optimized.fully-loaded * {
|
||||
contain: layout style;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* Skeleton loading animations for initial render */
|
||||
.task-row-optimized.initial-load .animate-pulse {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.task-name-edit-active {
|
||||
contain: none; /* Disable containment during editing for proper focus */
|
||||
}
|
||||
@@ -91,6 +123,20 @@
|
||||
will-change: background-color;
|
||||
}
|
||||
|
||||
/* PERFORMANCE OPTIMIZATION: Intersection observer optimizations */
|
||||
.task-row-optimized.intersection-observed {
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.task-row-optimized.intersection-observed.visible {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.task-row-optimized.intersection-observed.hidden {
|
||||
will-change: auto;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
/* Dark mode optimizations */
|
||||
.dark .task-row-optimized {
|
||||
contain: layout style;
|
||||
@@ -106,6 +152,10 @@
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.task-row-optimized .animate-pulse {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High DPI display optimizations */
|
||||
@@ -125,18 +175,21 @@
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
/* Intersection observer optimizations */
|
||||
.task-row-optimized.intersection-observed {
|
||||
contain: layout style paint;
|
||||
/* PERFORMANCE OPTIMIZATION: GPU acceleration for better scrolling */
|
||||
.task-row-optimized {
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
transform-style: preserve-3d;
|
||||
-webkit-transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.task-row-optimized.intersection-observed.visible {
|
||||
will-change: transform, opacity;
|
||||
/* Optimize rendering layers */
|
||||
.task-row-optimized.initial-load {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.task-row-optimized.intersection-observed.hidden {
|
||||
will-change: auto;
|
||||
contain: strict;
|
||||
.task-row-optimized.fully-loaded {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
/* Performance debugging */
|
||||
@@ -154,4 +207,32 @@
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* PERFORMANCE OPTIMIZATION: Optimize text rendering */
|
||||
.task-row-optimized {
|
||||
text-rendering: optimizeSpeed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Optimize for mobile devices */
|
||||
@media (max-width: 768px) {
|
||||
.task-row-optimized {
|
||||
contain: strict;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
.task-row-optimized.initial-load {
|
||||
contain: strict;
|
||||
}
|
||||
}
|
||||
|
||||
/* PERFORMANCE OPTIMIZATION: Reduce reflows during resize */
|
||||
.task-row-optimized {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.task-row-optimized * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -158,8 +158,6 @@ const TaskReporter = React.memo<{ reporter?: string; isDarkMode: boolean }>(({ r
|
||||
</div>
|
||||
));
|
||||
|
||||
|
||||
|
||||
const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
task,
|
||||
projectId,
|
||||
@@ -174,6 +172,12 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
fixedColumns,
|
||||
scrollableColumns,
|
||||
}) => {
|
||||
// PERFORMANCE OPTIMIZATION: Implement progressive loading
|
||||
const [isFullyLoaded, setIsFullyLoaded] = useState(false);
|
||||
const [isIntersecting, setIsIntersecting] = useState(false);
|
||||
const rowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible
|
||||
const { socket, connected } = useSocket();
|
||||
|
||||
// Edit task name state
|
||||
@@ -182,6 +186,40 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Intersection Observer for lazy loading
|
||||
useEffect(() => {
|
||||
if (!rowRef.current) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting && !isIntersecting) {
|
||||
setIsIntersecting(true);
|
||||
// Delay full loading slightly to prioritize visible content
|
||||
const timeoutId = setTimeout(() => {
|
||||
setIsFullyLoaded(true);
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '100px', // Start loading 100px before coming into view
|
||||
threshold: 0.1,
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(rowRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [isIntersecting]);
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Skip expensive operations during initial render
|
||||
const shouldRenderFull = isFullyLoaded || isDragOverlay || editTaskName;
|
||||
|
||||
// Optimized drag and drop setup with better performance
|
||||
const {
|
||||
attributes,
|
||||
@@ -197,7 +235,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
taskId: task.id,
|
||||
groupId,
|
||||
},
|
||||
disabled: isDragOverlay,
|
||||
disabled: isDragOverlay || !shouldRenderFull, // Disable drag until fully loaded
|
||||
// Optimize animation performance
|
||||
animateLayoutChanges: () => false, // Disable layout animations for better performance
|
||||
});
|
||||
@@ -205,9 +243,9 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
// Get theme from Redux store - memoized selector
|
||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||
|
||||
// Optimized click outside detection
|
||||
// PERFORMANCE OPTIMIZATION: Only setup click outside detection when editing
|
||||
useEffect(() => {
|
||||
if (!editTaskName) return;
|
||||
if (!editTaskName || !shouldRenderFull) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
||||
@@ -221,7 +259,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [editTaskName]);
|
||||
}, [editTaskName, shouldRenderFull]);
|
||||
|
||||
// Optimized task name save handler
|
||||
const handleTaskNameSave = useCallback(() => {
|
||||
@@ -313,8 +351,92 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
assignee: createAssigneeAdapter(task),
|
||||
}), [task]);
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Simplified column rendering for initial load
|
||||
const renderColumnSimple = useCallback((col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number) => {
|
||||
const isLast = index === totalColumns - 1;
|
||||
const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
||||
|
||||
// Only render essential columns during initial load
|
||||
switch (col.key) {
|
||||
case 'drag':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className="w-4 h-4 opacity-30 bg-gray-300 rounded"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={handleSelectChange}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'key':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<TaskKey taskKey={task.task_key} isDarkMode={isDarkMode} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'task':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} 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 className="flex-1 min-w-0">
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: task.title }}
|
||||
className={styleClasses.taskName}
|
||||
>
|
||||
{task.title}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'status':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{task.status || 'Todo'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'progress':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{task.progress || 0}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
// For non-essential columns, show placeholder during initial load
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className={`w-8 h-3 rounded ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'} animate-pulse`}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [isDarkMode, task, isSelected, handleSelectChange, styleClasses]);
|
||||
|
||||
// Optimized column rendering with better performance
|
||||
const renderColumn = useCallback((col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number) => {
|
||||
// Use simplified rendering for initial load
|
||||
if (!shouldRenderFull) {
|
||||
return renderColumnSimple(col, isFixed, index, totalColumns);
|
||||
}
|
||||
|
||||
// Full rendering logic (existing code)
|
||||
const isLast = index === totalColumns - 1;
|
||||
const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
||||
|
||||
@@ -467,12 +589,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
|
||||
case 'status':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses} overflow-visible`} style={{ width: col.width }}>
|
||||
<TaskStatusDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses} overflow-visible`} style={{ width: col.width, minWidth: col.width }}>
|
||||
<div className="w-full">
|
||||
<TaskStatusDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -534,32 +658,32 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'completedDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.completedAt ? utilFormatDate(task.completedAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'createdDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.createdAt ? utilFormatDate(task.createdAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'lastUpdated':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.updatedAt ? utilFormatDateTime(task.updatedAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'completedDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.completedAt ? utilFormatDate(task.completedAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'createdDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.createdAt ? utilFormatDate(task.createdAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'lastUpdated':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.updatedAt ? utilFormatDateTime(task.updatedAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'reporter':
|
||||
return (
|
||||
@@ -572,17 +696,19 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
return null;
|
||||
}
|
||||
}, [
|
||||
isDarkMode, task, isSelected, editTaskName, taskName, adapters, groupId, projectId,
|
||||
shouldRenderFull, renderColumnSimple, isDarkMode, task, isSelected, editTaskName, taskName, adapters, groupId, projectId,
|
||||
attributes, listeners, handleSelectChange, handleTaskNameSave, handleDateChange,
|
||||
dateValues, styleClasses
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
ref={(node) => {
|
||||
setNodeRef(node);
|
||||
rowRef.current = node;
|
||||
}}
|
||||
style={dragStyle}
|
||||
className={`${styleClasses.container} task-row-optimized`}
|
||||
// Add CSS containment for better performance
|
||||
className={`${styleClasses.container} task-row-optimized ${shouldRenderFull ? 'fully-loaded' : 'initial-load'}`}
|
||||
data-task-id={task.id}
|
||||
>
|
||||
<div className="flex h-10 max-h-10 overflow-visible relative">
|
||||
@@ -611,13 +737,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
// Optimized comparison function for better performance
|
||||
// Only compare essential props that affect rendering
|
||||
// PERFORMANCE OPTIMIZATION: Enhanced comparison function
|
||||
// Skip comparison during initial renders to reduce CPU load
|
||||
if (!prevProps.task.id || !nextProps.task.id) return false;
|
||||
|
||||
// Quick identity checks first
|
||||
if (prevProps.task.id !== nextProps.task.id) return false;
|
||||
if (prevProps.isSelected !== nextProps.isSelected) return false;
|
||||
if (prevProps.isDragOverlay !== nextProps.isDragOverlay) return false;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { updateTask, selectCurrentGroupingV3 } from '@/features/task-management/task-management.slice';
|
||||
|
||||
interface TaskStatusDropdownProps {
|
||||
task: Task;
|
||||
@@ -16,6 +18,7 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
||||
projectId,
|
||||
isDarkMode = false
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { socket, connected } = useSocket();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||||
@@ -23,14 +26,8 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
||||
const currentGroupingV3 = useAppSelector(selectCurrentGroupingV3);
|
||||
|
||||
// Debug log only when statusList changes, not on every render
|
||||
useEffect(() => {
|
||||
if (statusList.length > 0) {
|
||||
console.log('Status list loaded:', statusList.length, 'statuses');
|
||||
}
|
||||
}, [statusList]);
|
||||
|
||||
// Find current status details
|
||||
const currentStatus = useMemo(() => {
|
||||
return statusList.find(status =>
|
||||
@@ -43,6 +40,8 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
||||
const handleStatusChange = useCallback((statusId: string, statusName: string) => {
|
||||
if (!task.id || !statusId || !connected) return;
|
||||
|
||||
console.log('🎯 Status change initiated:', { taskId: task.id, statusId, statusName });
|
||||
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
@@ -120,14 +119,15 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
||||
}}
|
||||
className={`
|
||||
inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium
|
||||
transition-all duration-200 hover:opacity-80 border-0 min-w-[70px] justify-center
|
||||
transition-all duration-200 hover:opacity-80 border-0 min-w-[70px] max-w-full justify-center
|
||||
whitespace-nowrap
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: currentStatus ? getStatusColor(currentStatus) : (isDarkMode ? '#4b5563' : '#9ca3af'),
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<span>{currentStatus ? formatStatusName(currentStatus.name || '') : getStatusDisplayName(task.status)}</span>
|
||||
<span className="truncate">{currentStatus ? formatStatusName(currentStatus.name || '') : getStatusDisplayName(task.status)}</span>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
|
||||
@@ -2,6 +2,8 @@ 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';
|
||||
@@ -32,6 +34,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
width
|
||||
}) => {
|
||||
const allTasks = useSelector(taskManagementSelectors.selectAll);
|
||||
const { t } = useTranslation('task-management');
|
||||
|
||||
// Get theme from Redux store
|
||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||
@@ -39,40 +42,119 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
// Get field visibility from taskListFields slice
|
||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Reduce virtualization threshold for better performance
|
||||
const VIRTUALIZATION_THRESHOLD = 20; // Reduced from 100 to 20 - virtualize even smaller lists
|
||||
const TASK_ROW_HEIGHT = 40;
|
||||
const HEADER_HEIGHT = 40;
|
||||
const COLUMN_HEADER_HEIGHT = 40;
|
||||
const ADD_TASK_ROW_HEIGHT = 40;
|
||||
|
||||
// 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 }}>
|
||||
<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',
|
||||
borderLeft: `4px solid ${group?.color || '#f0f0f0'}`
|
||||
}}
|
||||
>
|
||||
<span className="task-group-header-text">
|
||||
{group?.title || 'Empty Group'} (0)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column Headers */}
|
||||
<div className="task-group-column-headers" style={{
|
||||
borderLeft: `4px solid ${group?.color || '#f0f0f0'}`,
|
||||
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',
|
||||
borderLeft: `4px solid ${group?.color || '#f0f0f0'}`,
|
||||
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={{ borderLeft: `4px solid ${group?.color || '#f0f0f0'}`, height: ADD_TASK_ROW_HEIGHT }}>
|
||||
<AddTaskListRow groupId={group?.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Get tasks for this group using memoization for performance
|
||||
const groupTasks = useMemo(() => {
|
||||
return group.taskIds
|
||||
const tasks = group.taskIds
|
||||
.map((taskId: string) => allTasks.find((task: Task) => task.id === taskId))
|
||||
.filter((task: Task | undefined): task is Task => task !== undefined);
|
||||
|
||||
return tasks;
|
||||
}, [group.taskIds, allTasks]);
|
||||
|
||||
// Calculate selection state for the group checkbox
|
||||
const { isAllSelected, isIndeterminate } = useMemo(() => {
|
||||
// PERFORMANCE OPTIMIZATION: Only calculate selection state when needed
|
||||
const selectionState = useMemo(() => {
|
||||
if (groupTasks.length === 0) {
|
||||
return { isAllSelected: false, isIndeterminate: false };
|
||||
}
|
||||
|
||||
const selectedTasksInGroup = groupTasks.filter(task => selectedTaskIds.includes(task.id));
|
||||
const selectedTasksInGroup = groupTasks.filter((task: Task) => selectedTaskIds.includes(task.id));
|
||||
const isAllSelected = selectedTasksInGroup.length === groupTasks.length;
|
||||
const isIndeterminate = selectedTasksInGroup.length > 0 && selectedTasksInGroup.length < groupTasks.length;
|
||||
|
||||
return { isAllSelected, isIndeterminate };
|
||||
}, [groupTasks, selectedTaskIds]);
|
||||
|
||||
// Handle select all tasks in group
|
||||
// Handle select all tasks in group - optimized with useCallback
|
||||
const handleSelectAllInGroup = useCallback((checked: boolean) => {
|
||||
if (checked) {
|
||||
// Select all tasks in the group
|
||||
groupTasks.forEach(task => {
|
||||
groupTasks.forEach((task: Task) => {
|
||||
if (!selectedTaskIds.includes(task.id)) {
|
||||
onSelectTask(task.id, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Deselect all tasks in the group
|
||||
groupTasks.forEach(task => {
|
||||
groupTasks.forEach((task: Task) => {
|
||||
if (selectedTaskIds.includes(task.id)) {
|
||||
onSelectTask(task.id, false);
|
||||
}
|
||||
@@ -80,11 +162,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
}
|
||||
}, [groupTasks, selectedTaskIds, onSelectTask]);
|
||||
|
||||
const TASK_ROW_HEIGHT = 40;
|
||||
const HEADER_HEIGHT = 40;
|
||||
const COLUMN_HEADER_HEIGHT = 40;
|
||||
const ADD_TASK_ROW_HEIGHT = 40;
|
||||
|
||||
// Calculate dynamic height for the group
|
||||
const taskRowsHeight = groupTasks.length * TASK_ROW_HEIGHT;
|
||||
const groupHeight = HEADER_HEIGHT + COLUMN_HEADER_HEIGHT + taskRowsHeight + ADD_TASK_ROW_HEIGHT;
|
||||
@@ -100,7 +177,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
const allScrollableColumns = [
|
||||
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
||||
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
||||
{ key: 'status', label: 'Status', width: 100, fieldKey: 'STATUS' },
|
||||
{ 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' },
|
||||
@@ -148,18 +225,22 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
|
||||
const totalTableWidth = fixedWidth + scrollableWidth;
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Increase overscanCount for better perceived performance
|
||||
const shouldVirtualize = groupTasks.length > VIRTUALIZATION_THRESHOLD;
|
||||
const overscanCount = shouldVirtualize ? Math.min(10, Math.ceil(groupTasks.length * 0.1)) : 0; // Dynamic overscan
|
||||
|
||||
|
||||
// Row renderer for virtualization (only task rows)
|
||||
// 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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="task-row-container"
|
||||
style={{
|
||||
...style,
|
||||
'--group-color': group.color || '#f0f0f0'
|
||||
'--group-color': group.color || '#f0f0f0',
|
||||
contain: 'layout style', // CSS containment for better performance
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<TaskRow
|
||||
@@ -176,7 +257,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, fixedColumns, scrollableColumns]);
|
||||
}, [group.id, group.color, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, fixedColumns, scrollableColumns]);
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const headerScrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -199,9 +280,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const VIRTUALIZATION_THRESHOLD = 20;
|
||||
const shouldVirtualize = groupTasks.length > VIRTUALIZATION_THRESHOLD;
|
||||
|
||||
return (
|
||||
<div className="virtualized-task-list" style={{ height: groupHeight }}>
|
||||
{/* Group Header */}
|
||||
@@ -240,10 +318,10 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
{col.key === 'select' ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
checked={selectionState.isAllSelected}
|
||||
onChange={handleSelectAllInGroup}
|
||||
isDarkMode={isDarkMode}
|
||||
indeterminate={isIndeterminate}
|
||||
indeterminate={selectionState.isIndeterminate}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -275,6 +353,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
width: '100%',
|
||||
minWidth: totalTableWidth,
|
||||
height: groupTasks.length > 0 ? taskRowsHeight : 'auto',
|
||||
contain: 'layout style', // CSS containment for better performance
|
||||
}}
|
||||
>
|
||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
||||
@@ -284,36 +363,53 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
width={width}
|
||||
itemCount={groupTasks.length}
|
||||
itemSize={TASK_ROW_HEIGHT}
|
||||
overscanCount={50}
|
||||
overscanCount={overscanCount} // Dynamic overscan
|
||||
className="react-window-list"
|
||||
style={{ minWidth: totalTableWidth }}
|
||||
// PERFORMANCE OPTIMIZATION: Add performance-focused props
|
||||
useIsScrolling={true}
|
||||
itemData={{
|
||||
groupTasks,
|
||||
group,
|
||||
projectId,
|
||||
currentGrouping,
|
||||
selectedTaskIds,
|
||||
onSelectTask,
|
||||
onToggleSubtasks,
|
||||
fixedColumns,
|
||||
scrollableColumns
|
||||
}}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
) : (
|
||||
groupTasks.map((task: Task, index: number) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="task-row-container"
|
||||
style={{
|
||||
height: TASK_ROW_HEIGHT,
|
||||
'--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>
|
||||
))
|
||||
// PERFORMANCE OPTIMIZATION: Use React.Fragment to reduce DOM nodes
|
||||
<React.Fragment>
|
||||
{groupTasks.map((task: Task, index: number) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="task-row-container"
|
||||
style={{
|
||||
height: TASK_ROW_HEIGHT,
|
||||
'--group-color': group.color || '#f0f0f0',
|
||||
contain: 'layout style', // CSS containment
|
||||
} 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>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</SortableContext>
|
||||
</div>
|
||||
@@ -328,7 +424,6 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
.virtualized-task-list {
|
||||
border: 1px solid var(--task-border-primary, #e8e8e8);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--task-bg-primary, white);
|
||||
box-shadow: 0 1px 3px var(--task-shadow, rgba(0, 0, 0, 0.1));
|
||||
overflow: hidden;
|
||||
@@ -487,6 +582,19 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
/* 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 {
|
||||
|
||||
Reference in New Issue
Block a user