From 734b5f807b0f89a8cb9d0159a9cfb85fb6ff9f03 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Wed, 25 Jun 2025 15:22:38 +0530 Subject: [PATCH] feat(task-management): enhance task components with performance optimizations and new status field - Rearranged the order of task fields in the dropdown for better usability. - Introduced a new CSS file for task row optimizations, improving rendering performance and responsiveness. - Added utility functions for date formatting and performance monitoring to enhance task management efficiency. - Updated TaskRow and TaskStatusDropdown components to improve rendering and user experience, including better handling of status display and dark mode support. - Integrated new status field into various task management components, ensuring consistent visibility and functionality across the application. --- .../show-fields-filter-dropdown.tsx | 8 +- .../components/task-management/task-group.tsx | 2 +- .../task-management/task-row-optimized.css | 157 +++ .../task-management/task-row-utils.ts | 349 +++++++ .../components/task-management/task-row.tsx | 954 +++++++++--------- .../task-management/task-status-dropdown.tsx | 158 ++- .../virtualized-task-group.tsx | 6 +- .../task-management/virtualized-task-list.tsx | 2 +- .../taskListColumns/taskColumnsSlice.ts | 14 +- .../task-list-columns/task-list-columns.tsx | 36 +- .../taskListTable/columns/columnList.ts | 10 +- 11 files changed, 1134 insertions(+), 562 deletions(-) create mode 100644 worklenz-frontend/src/components/task-management/task-row-optimized.css create mode 100644 worklenz-frontend/src/components/task-management/task-row-utils.ts diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx index 8ad5a6bc..cc57faa9 100644 --- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx +++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx @@ -32,10 +32,10 @@ const DEFAULT_COLUMN_CONFIG: ColumnConfig[] = [ { key: 'TASK', label: 'Task', showInDropdown: false, order: 2, category: 'basic' }, // Always visible, not in dropdown { key: 'DESCRIPTION', label: 'Description', showInDropdown: true, order: 3, category: 'basic' }, { key: 'PROGRESS', label: 'Progress', showInDropdown: true, order: 4, category: 'basic' }, - { key: 'ASSIGNEES', label: 'Assignees', showInDropdown: true, order: 5, category: 'basic' }, - { key: 'LABELS', label: 'Labels', showInDropdown: true, order: 6, category: 'basic' }, - { key: 'PHASE', label: 'Phase', showInDropdown: true, order: 7, category: 'basic' }, - { key: 'STATUS', label: 'Status', showInDropdown: true, order: 8, category: 'basic' }, + { key: 'STATUS', label: 'Status', showInDropdown: true, order: 5, category: 'basic' }, + { key: 'ASSIGNEES', label: 'Assignees', showInDropdown: true, order: 6, category: 'basic' }, + { key: 'LABELS', label: 'Labels', showInDropdown: true, order: 7, category: 'basic' }, + { key: 'PHASE', label: 'Phase', showInDropdown: true, order: 8, category: 'basic' }, { key: 'PRIORITY', label: 'Priority', showInDropdown: true, order: 9, category: 'basic' }, { key: 'TIME_TRACKING', label: 'Time Tracking', showInDropdown: true, order: 10, category: 'time' }, { key: 'ESTIMATION', label: 'Estimation', showInDropdown: true, order: 11, category: 'time' }, diff --git a/worklenz-frontend/src/components/task-management/task-group.tsx b/worklenz-frontend/src/components/task-management/task-group.tsx index bbff1138..7df7af83 100644 --- a/worklenz-frontend/src/components/task-management/task-group.tsx +++ b/worklenz-frontend/src/components/task-management/task-group.tsx @@ -91,10 +91,10 @@ const TaskGroup: React.FC = 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: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' }, { key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' }, { key: 'phase', label: 'Phase', width: 100, fieldKey: 'PHASE' }, - { key: 'status', label: 'Status', width: 100, fieldKey: 'STATUS' }, { 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' }, diff --git a/worklenz-frontend/src/components/task-management/task-row-optimized.css b/worklenz-frontend/src/components/task-management/task-row-optimized.css new file mode 100644 index 00000000..12cfc246 --- /dev/null +++ b/worklenz-frontend/src/components/task-management/task-row-optimized.css @@ -0,0 +1,157 @@ +/* TaskRow Performance Optimizations CSS */ + +.task-row-optimized { + contain: layout style; + will-change: transform; + transform: translateZ(0); /* Force GPU acceleration */ +} + +.task-row-optimized:hover { + contain: layout style paint; +} + +.task-row-optimized.task-row-dragging { + contain: layout; + will-change: transform; + z-index: 1000; +} + +.task-row-optimized.task-row-virtualized { + contain: strict; + will-change: transform; +} + +.task-row-optimized.task-row-selected { + contain: layout style; +} + +.task-name-edit-active { + contain: none; /* Disable containment during editing for proper focus */ +} + +.task-name-input { + contain: layout; + will-change: contents; +} + +/* Performance optimizations for drag and drop */ +.task-row-optimized [data-dnd-draggable] { + will-change: transform; + transform: translateZ(0); +} + +.task-row-optimized [data-dnd-drag-handle] { + cursor: grab; + will-change: transform; +} + +.task-row-optimized [data-dnd-drag-handle]:active { + cursor: grabbing; +} + +/* Virtualization specific optimizations */ +.task-row-virtualized { + contain: strict; + will-change: transform; +} + +.task-row-virtualized * { + contain: layout style; +} + +/* Smooth scrolling for virtualized lists */ +.virtualized-task-container { + scroll-behavior: smooth; + contain: layout style paint; +} + +/* Optimize frequent updates */ +.task-progress-circle { + contain: layout style paint; + will-change: stroke-dasharray; +} + +.task-status-dropdown { + contain: layout style; + will-change: background-color; +} + +.task-assignee-avatar { + contain: layout style paint; + will-change: transform; +} + +/* Reduce layout thrashing */ +.task-cell { + contain: layout; + will-change: auto; +} + +.task-cell:hover { + will-change: background-color; +} + +/* Dark mode optimizations */ +.dark .task-row-optimized { + contain: layout style; +} + +.dark .task-row-optimized:hover { + contain: layout style paint; +} + +/* Animation performance */ +@media (prefers-reduced-motion: reduce) { + .task-row-optimized { + transition: none !important; + animation: none !important; + } +} + +/* High DPI display optimizations */ +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { + .task-row-optimized { + contain: layout style paint; + } +} + +/* Memory optimization for large lists */ +.task-list-container { + contain: layout style; + overflow: hidden; +} + +.task-list-container.large-list { + contain: strict; +} + +/* 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; +} + +/* Performance debugging */ +.task-row-debug { + outline: 1px solid red !important; +} + +.task-row-debug::before { + content: "DEBUG"; + position: absolute; + top: 0; + right: 0; + background: red; + color: white; + font-size: 10px; + padding: 2px 4px; + z-index: 9999; +} \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/task-row-utils.ts b/worklenz-frontend/src/components/task-management/task-row-utils.ts new file mode 100644 index 00000000..e793d384 --- /dev/null +++ b/worklenz-frontend/src/components/task-management/task-row-utils.ts @@ -0,0 +1,349 @@ +import { Task } from '@/types/task-management.types'; +import { dayjs } from './antd-imports'; + +// Performance constants +export const PERFORMANCE_CONSTANTS = { + CACHE_CLEAR_INTERVAL: 300000, // 5 minutes + VIRTUALIZATION_THRESHOLD: 50, + DRAG_THROTTLE_MS: 50, + RENDER_TIMEOUT_MS: 16, // 60fps target + MAX_CACHE_SIZE: 1000, +} as const; + +// Priority and status color constants +export const PRIORITY_COLORS = { + critical: '#ff4d4f', + high: '#ff7a45', + medium: '#faad14', + low: '#52c41a', +} as const; + +export const STATUS_COLORS = { + todo: '#f0f0f0', + doing: '#1890ff', + done: '#52c41a', +} as const; + +// Cache management for date formatting +class DateFormatCache { + private cache = new Map(); + private maxSize: number; + + constructor(maxSize: number = PERFORMANCE_CONSTANTS.MAX_CACHE_SIZE) { + this.maxSize = maxSize; + } + + get(key: string): string | undefined { + return this.cache.get(key); + } + + set(key: string, value: string): void { + // LRU eviction when cache is full + if (this.cache.size >= this.maxSize) { + const firstKey = this.cache.keys().next().value as string; + if (firstKey) { + this.cache.delete(firstKey); + } + } + this.cache.set(key, value); + } + + has(key: string): boolean { + return this.cache.has(key); + } + + clear(): void { + this.cache.clear(); + } + + size(): number { + return this.cache.size; + } +} + +// Global cache instances +export const formatDateCache = new DateFormatCache(); +export const formatDateTimeCache = new DateFormatCache(); + +// Optimized date formatters with caching +export const formatDate = (dateString?: string): string => { + if (!dateString) return ''; + + if (formatDateCache.has(dateString)) { + return formatDateCache.get(dateString)!; + } + + const formatted = dayjs(dateString).format('MMM DD, YYYY'); + formatDateCache.set(dateString, formatted); + return formatted; +}; + +export const formatDateTime = (dateString?: string): string => { + if (!dateString) return ''; + + if (formatDateTimeCache.has(dateString)) { + return formatDateTimeCache.get(dateString)!; + } + + const formatted = dayjs(dateString).format('MMM DD, YYYY HH:mm'); + formatDateTimeCache.set(dateString, formatted); + return formatted; +}; + +// Performance monitoring utilities +export class PerformanceMonitor { + private static instance: PerformanceMonitor; + private metrics: Map = new Map(); + + static getInstance(): PerformanceMonitor { + if (!PerformanceMonitor.instance) { + PerformanceMonitor.instance = new PerformanceMonitor(); + } + return PerformanceMonitor.instance; + } + + startTiming(operation: string): () => void { + const startTime = performance.now(); + + return () => { + const endTime = performance.now(); + const duration = endTime - startTime; + + if (!this.metrics.has(operation)) { + this.metrics.set(operation, []); + } + + const operationMetrics = this.metrics.get(operation)!; + operationMetrics.push(duration); + + // Keep only last 100 measurements + if (operationMetrics.length > 100) { + operationMetrics.shift(); + } + }; + } + + getAverageTime(operation: string): number { + const times = this.metrics.get(operation); + if (!times || times.length === 0) return 0; + + const sum = times.reduce((acc, time) => acc + time, 0); + return sum / times.length; + } + + getMetrics(): Record { + const result: Record = {}; + + this.metrics.forEach((times, operation) => { + if (times.length > 0) { + result[operation] = { + average: this.getAverageTime(operation), + count: times.length, + latest: times[times.length - 1], + }; + } + }); + + return result; + } + + logMetrics(): void { + const metrics = this.getMetrics(); + console.table(metrics); + } + + clearMetrics(): void { + this.metrics.clear(); + } +} + +// Task comparison utilities for React.memo +export const taskPropsEqual = (prevTask: Task, nextTask: Task): boolean => { + // Quick identity check + if (prevTask === nextTask) return true; + if (prevTask.id !== nextTask.id) return false; + + // Check commonly changing properties + const criticalProps: (keyof Task)[] = [ + 'title', 'progress', 'status', 'priority', 'description', + 'startDate', 'dueDate', 'updatedAt' + ]; + + for (const prop of criticalProps) { + if (prevTask[prop] !== nextTask[prop]) { + return false; + } + } + + // Check array lengths (fast path) + if (prevTask.labels?.length !== nextTask.labels?.length) return false; + if (prevTask.assignee_names?.length !== nextTask.assignee_names?.length) return false; + + return true; +}; + +// Throttle utility for drag operations +export const throttle = any>( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: NodeJS.Timeout | null = null; + let lastExecTime = 0; + + return (...args: Parameters) => { + const currentTime = Date.now(); + + const execute = () => { + lastExecTime = currentTime; + func(...args); + }; + + if (currentTime - lastExecTime > delay) { + execute(); + } else { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(execute, delay - (currentTime - lastExecTime)); + } + }; +}; + +// Debounce utility for input operations +export const debounce = any>( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: NodeJS.Timeout | null = null; + + return (...args: Parameters) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => func(...args), delay); + }; +}; + +// Virtualization utilities +export const shouldVirtualize = (itemCount: number): boolean => { + return itemCount > PERFORMANCE_CONSTANTS.VIRTUALIZATION_THRESHOLD; +}; + +export const calculateVirtualizedHeight = ( + itemCount: number, + itemHeight: number, + maxHeight: number = 600 +): number => { + const totalHeight = itemCount * itemHeight; + return Math.min(totalHeight, maxHeight); +}; + +// CSS class utilities for performance +export const getOptimizedClasses = ( + isDragging: boolean, + isVirtualized: boolean, + isSelected: boolean +): string => { + const classes = ['task-row-optimized']; + + if (isDragging) { + classes.push('task-row-dragging'); + } + + if (isVirtualized) { + classes.push('task-row-virtualized'); + } + + if (isSelected) { + classes.push('task-row-selected'); + } + + return classes.join(' '); +}; + +// Memory management +export const clearAllCaches = (): void => { + formatDateCache.clear(); + formatDateTimeCache.clear(); + PerformanceMonitor.getInstance().clearMetrics(); +}; + +// Performance debugging helpers +export const logPerformanceWarning = (operation: string, duration: number): void => { + if (duration > PERFORMANCE_CONSTANTS.RENDER_TIMEOUT_MS) { + console.warn( + `Performance warning: ${operation} took ${duration.toFixed(2)}ms (target: ${PERFORMANCE_CONSTANTS.RENDER_TIMEOUT_MS}ms)` + ); + } +}; + +// Auto-cleanup setup +let cleanupInterval: NodeJS.Timeout | null = null; + +export const setupAutoCleanup = (): void => { + if (cleanupInterval) { + clearInterval(cleanupInterval); + } + + cleanupInterval = setInterval(() => { + clearAllCaches(); + }, PERFORMANCE_CONSTANTS.CACHE_CLEAR_INTERVAL); +}; + +export const teardownAutoCleanup = (): void => { + if (cleanupInterval) { + clearInterval(cleanupInterval); + cleanupInterval = null; + } +}; + +// Initialize auto-cleanup +if (typeof window !== 'undefined') { + setupAutoCleanup(); + + // Cleanup on page unload + window.addEventListener('beforeunload', teardownAutoCleanup); +} + +// Export performance monitor instance +export const performanceMonitor = PerformanceMonitor.getInstance(); + +// Task adapter utilities +export const createLabelsAdapter = (task: Task) => ({ + id: task.id, + name: task.title, + parent_task_id: undefined, + manual_progress: false, + all_labels: task.labels?.map(label => ({ + id: label.id, + name: label.name, + color_code: label.color + })) || [], + labels: task.labels?.map(label => ({ + id: label.id, + name: label.name, + color_code: label.color + })) || [], +} as any); + +export const createAssigneeAdapter = (task: Task) => ({ + id: task.id, + name: task.title, + parent_task_id: undefined, + manual_progress: false, + assignees: task.assignee_names?.map(member => ({ + team_member_id: member.team_member_id || '', + id: member.team_member_id || '', + project_member_id: member.team_member_id || '', + name: member.name, + })) || [], +} as any); + +// Color utilities +export const getPriorityColor = (priority: string): string => { + return PRIORITY_COLORS[priority as keyof typeof PRIORITY_COLORS] || '#d9d9d9'; +}; + +export const getStatusColor = (status: string): string => { + return STATUS_COLORS[status as keyof typeof STATUS_COLORS] || '#d9d9d9'; +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index 30a06565..ec4cdfdb 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -21,6 +21,16 @@ import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLa import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import TaskStatusDropdown from './task-status-dropdown'; +import { + formatDate as utilFormatDate, + formatDateTime as utilFormatDateTime, + createLabelsAdapter, + createAssigneeAdapter, + PRIORITY_COLORS as UTIL_PRIORITY_COLORS, + performanceMonitor, + taskPropsEqual +} from './task-row-utils'; +import './task-row-optimized.css'; interface TaskRowProps { task: Task; @@ -51,6 +61,98 @@ const STATUS_COLORS = { done: '#52c41a', } as const; +// Memoized sub-components for better performance +const DragHandle = React.memo<{ isDarkMode: boolean; attributes: any; listeners: any }>(({ isDarkMode, attributes, listeners }) => ( + - {/* Dropdown Menu - Rendered in Portal */} + {/* Dropdown Menu - Redesigned */} {isOpen && createPortal(
-
- {statusList.map((status) => ( -
- - {/* Current Status Indicator */} - {(status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status) && ( -
-
-
- )} - - ))} -
+ {/* Status Color Indicator */} +
+ + {/* Status Name */} + + {formatStatusName(status.name || '')} + + + {/* Current Status Badge */} + {isSelected && ( +
+
+ + Current + +
+ )} + + ); + })} +
, document.body )} + + {/* CSS Animations - Injected as style tag */} + {isOpen && createPortal( + , + document.head + )} ); }; diff --git a/worklenz-frontend/src/components/task-management/virtualized-task-group.tsx b/worklenz-frontend/src/components/task-management/virtualized-task-group.tsx index 70ae7c2c..2f5d9868 100644 --- a/worklenz-frontend/src/components/task-management/virtualized-task-group.tsx +++ b/worklenz-frontend/src/components/task-management/virtualized-task-group.tsx @@ -83,15 +83,15 @@ const VirtualizedTaskGroup: React.FC = React.memo(({
Progress
+
+ Status +
Members
Labels
-
- Status -
Priority
diff --git a/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx b/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx index e9d2746f..4bfc71fd 100644 --- a/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx +++ b/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx @@ -100,10 +100,10 @@ const VirtualizedTaskList: React.FC = 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: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' }, { key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' }, { key: 'phase', label: 'Phase', width: 100, fieldKey: 'PHASE' }, - { key: 'status', label: 'Status', width: 100, fieldKey: 'STATUS' }, { 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' }, diff --git a/worklenz-frontend/src/features/projects/singleProject/taskListColumns/taskColumnsSlice.ts b/worklenz-frontend/src/features/projects/singleProject/taskListColumns/taskColumnsSlice.ts index 9d850bf9..26df6827 100644 --- a/worklenz-frontend/src/features/projects/singleProject/taskListColumns/taskColumnsSlice.ts +++ b/worklenz-frontend/src/features/projects/singleProject/taskListColumns/taskColumnsSlice.ts @@ -48,6 +48,13 @@ const initialState: projectViewTaskListColumnsState = { width: 60, isVisible: false, }, + { + key: 'status', + name: 'status', + columnHeader: 'status', + width: 120, + isVisible: true, + }, { key: 'members', name: 'members', @@ -69,13 +76,6 @@ const initialState: projectViewTaskListColumnsState = { width: 150, isVisible: false, }, - { - key: 'status', - name: 'status', - columnHeader: 'status', - width: 120, - isVisible: true, - }, { key: 'priority', name: 'priority', diff --git a/worklenz-frontend/src/pages/projects/project-view-1/task-list/task-list-columns/task-list-columns.tsx b/worklenz-frontend/src/pages/projects/project-view-1/task-list/task-list-columns/task-list-columns.tsx index bb550337..773b683a 100644 --- a/worklenz-frontend/src/pages/projects/project-view-1/task-list/task-list-columns/task-list-columns.tsx +++ b/worklenz-frontend/src/pages/projects/project-view-1/task-list/task-list-columns/task-list-columns.tsx @@ -111,6 +111,24 @@ export const createColumns = ({ ), }), + columnHelper.accessor('status', { + header: 'Status', + id: COLUMN_KEYS.STATUS, + size: 120, + enablePinning: false, + cell: ({ row }) => ( + { + console.log('Status changed:', statusId); + }} + /> + ), + }), + columnHelper.accessor('names', { header: 'Assignees', id: COLUMN_KEYS.ASSIGNEES, @@ -163,24 +181,6 @@ export const createColumns = ({ cell: ({ row }) => , }), - columnHelper.accessor('status', { - header: 'Status', - id: COLUMN_KEYS.STATUS, - size: 120, - enablePinning: false, - cell: ({ row }) => ( - { - console.log('Status changed:', statusId); - }} - /> - ), - }), - columnHelper.accessor('labels', { header: 'Labels', id: COLUMN_KEYS.LABELS, diff --git a/worklenz-frontend/src/pages/projects/project-view-1/taskList/taskListTable/columns/columnList.ts b/worklenz-frontend/src/pages/projects/project-view-1/taskList/taskListTable/columns/columnList.ts index b7575d4e..576e3cf1 100644 --- a/worklenz-frontend/src/pages/projects/project-view-1/taskList/taskListTable/columns/columnList.ts +++ b/worklenz-frontend/src/pages/projects/project-view-1/taskList/taskListTable/columns/columnList.ts @@ -22,6 +22,11 @@ export const columnList: CustomTableColumnsType[] = [ columnHeader: 'progress', width: 60, }, + { + key: 'status', + columnHeader: 'status', + width: 120, + }, { key: 'members', columnHeader: 'members', @@ -37,11 +42,6 @@ export const columnList: CustomTableColumnsType[] = [ columnHeader: phaseHeader, width: 150, }, - { - key: 'status', - columnHeader: 'status', - width: 120, - }, { key: 'priority', columnHeader: 'priority',