Files
worklenz/worklenz-frontend/src/components/task-management/task-row-utils.ts
chamikaJ 14c5c148b9 refactor(task-management): optimize task management components with performance enhancements
- 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.
2025-06-30 11:02:41 +05:30

349 lines
8.9 KiB
TypeScript

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: 25, // Updated to match main virtualization threshold
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<string, string>();
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<string, number[]> = 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<string, { average: number; count: number; latest: number }> {
const result: Record<string, { average: number; count: number; latest: number }> = {};
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 = <T extends (...args: any[]) => any>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: NodeJS.Timeout | null = null;
let lastExecTime = 0;
return (...args: Parameters<T>) => {
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 = <T extends (...args: any[]) => any>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
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';
};