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.
This commit is contained in:
@@ -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: '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: 'DESCRIPTION', label: 'Description', showInDropdown: true, order: 3, category: 'basic' },
|
||||||
{ key: 'PROGRESS', label: 'Progress', showInDropdown: true, order: 4, category: 'basic' },
|
{ key: 'PROGRESS', label: 'Progress', showInDropdown: true, order: 4, category: 'basic' },
|
||||||
{ key: 'ASSIGNEES', label: 'Assignees', showInDropdown: true, order: 5, category: 'basic' },
|
{ key: 'STATUS', label: 'Status', showInDropdown: true, order: 5, category: 'basic' },
|
||||||
{ key: 'LABELS', label: 'Labels', showInDropdown: true, order: 6, category: 'basic' },
|
{ key: 'ASSIGNEES', label: 'Assignees', showInDropdown: true, order: 6, category: 'basic' },
|
||||||
{ key: 'PHASE', label: 'Phase', showInDropdown: true, order: 7, category: 'basic' },
|
{ key: 'LABELS', label: 'Labels', showInDropdown: true, order: 7, category: 'basic' },
|
||||||
{ key: 'STATUS', label: 'Status', showInDropdown: true, order: 8, category: 'basic' },
|
{ key: 'PHASE', label: 'Phase', showInDropdown: true, order: 8, category: 'basic' },
|
||||||
{ key: 'PRIORITY', label: 'Priority', showInDropdown: true, order: 9, 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: 'TIME_TRACKING', label: 'Time Tracking', showInDropdown: true, order: 10, category: 'time' },
|
||||||
{ key: 'ESTIMATION', label: 'Estimation', showInDropdown: true, order: 11, category: 'time' },
|
{ key: 'ESTIMATION', label: 'Estimation', showInDropdown: true, order: 11, category: 'time' },
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
const allScrollableColumns = [
|
const allScrollableColumns = [
|
||||||
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
||||||
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
{ 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: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
||||||
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
||||||
{ key: 'phase', label: 'Phase', width: 100, fieldKey: 'PHASE' },
|
{ 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: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
||||||
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
||||||
{ key: 'estimation', label: 'Estimation', width: 100, fieldKey: 'ESTIMATION' },
|
{ key: 'estimation', label: 'Estimation', width: 100, fieldKey: 'ESTIMATION' },
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<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';
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,13 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
|||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
||||||
|
|
||||||
|
// 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
|
// Find current status details
|
||||||
const currentStatus = useMemo(() => {
|
const currentStatus = useMemo(() => {
|
||||||
@@ -76,32 +83,39 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
|||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
// Get status color
|
// Get status color - enhanced dark mode support
|
||||||
const getStatusColor = useCallback((status: any) => {
|
const getStatusColor = useCallback((status: any) => {
|
||||||
if (isDarkMode) {
|
if (isDarkMode) {
|
||||||
return status?.color_code_dark || status?.color_code || '#6b7280';
|
return status?.color_code_dark || status?.color_code || '#4b5563';
|
||||||
}
|
}
|
||||||
return status?.color_code || '#6b7280';
|
return status?.color_code || '#6b7280';
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Status display name
|
// Status display name - format status names by replacing underscores with spaces
|
||||||
const getStatusDisplayName = useCallback((status: string) => {
|
const getStatusDisplayName = useCallback((status: string) => {
|
||||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
return status
|
||||||
|
.replace(/_/g, ' ') // Replace underscores with spaces
|
||||||
|
.replace(/\b\w/g, char => char.toUpperCase()); // Capitalize first letter of each word
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Format status name for display
|
||||||
|
const formatStatusName = useCallback((name: string) => {
|
||||||
|
if (!name) return name;
|
||||||
|
return name.replace(/_/g, ' '); // Replace underscores with spaces
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!task.status) return null;
|
if (!task.status) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Status Button */}
|
{/* Status Button - Rounded Pill Design */}
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log('Status dropdown clicked, current isOpen:', isOpen);
|
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`
|
||||||
@@ -109,11 +123,11 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
|||||||
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] justify-center
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: currentStatus ? getStatusColor(currentStatus) : (isDarkMode ? '#6b7280' : '#9ca3af'),
|
backgroundColor: currentStatus ? getStatusColor(currentStatus) : (isDarkMode ? '#4b5563' : '#9ca3af'),
|
||||||
color: 'white',
|
color: 'white',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{currentStatus?.name || getStatusDisplayName(task.status)}</span>
|
<span>{currentStatus ? formatStatusName(currentStatus.name || '') : getStatusDisplayName(task.status)}</span>
|
||||||
<svg
|
<svg
|
||||||
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -124,62 +138,110 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown Menu - Rendered in Portal */}
|
{/* Dropdown Menu - Redesigned */}
|
||||||
{isOpen && createPortal(
|
{isOpen && createPortal(
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className={`
|
className={`
|
||||||
fixed min-w-[120px] max-w-[180px]
|
fixed min-w-[160px] max-w-[220px]
|
||||||
rounded-lg shadow-xl border z-[9999]
|
rounded border backdrop-blur-sm z-[9999]
|
||||||
${isDarkMode
|
${isDarkMode
|
||||||
? 'bg-gray-800 border-gray-600'
|
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
||||||
: 'bg-white border-gray-200'
|
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
top: dropdownPosition.top,
|
top: dropdownPosition.top,
|
||||||
left: dropdownPosition.left,
|
left: dropdownPosition.left,
|
||||||
zIndex: 9999
|
zIndex: 9999,
|
||||||
|
animation: 'fadeInScale 0.15s ease-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="py-1">
|
{/* Status Options */}
|
||||||
{statusList.map((status) => (
|
<div className="py-1 max-h-64 overflow-y-auto">
|
||||||
<button
|
{statusList.map((status, index) => {
|
||||||
key={status.id}
|
const isSelected = status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status;
|
||||||
onClick={() => handleStatusChange(status.id!, status.name!)}
|
|
||||||
className={`
|
return (
|
||||||
w-full px-3 py-2 text-left text-xs font-medium flex items-center gap-2
|
<button
|
||||||
transition-colors duration-150 rounded-md mx-1
|
key={status.id}
|
||||||
${isDarkMode
|
onClick={() => handleStatusChange(status.id!, status.name!)}
|
||||||
? 'hover:bg-gray-700 text-gray-200'
|
className={`
|
||||||
: 'hover:bg-gray-50 text-gray-900'
|
w-full px-3 py-2.5 text-left text-xs font-medium flex items-center gap-3
|
||||||
}
|
transition-all duration-150 hover:scale-[1.02] active:scale-[0.98]
|
||||||
${(status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status)
|
${isDarkMode
|
||||||
? (isDarkMode ? 'bg-gray-700' : 'bg-gray-50')
|
? 'hover:bg-gray-700/80 text-gray-100'
|
||||||
: ''
|
: 'hover:bg-gray-50/70 text-gray-900'
|
||||||
}
|
}
|
||||||
`}
|
${isSelected
|
||||||
>
|
? (isDarkMode ? 'bg-gray-700/60 ring-1 ring-blue-400/40' : 'bg-blue-50/50 ring-1 ring-blue-200')
|
||||||
{/* Status Pill Preview */}
|
: ''
|
||||||
<div
|
}
|
||||||
className="px-2 py-0.5 rounded-full text-white text-xs min-w-[50px] text-center"
|
`}
|
||||||
style={{ backgroundColor: getStatusColor(status) }}
|
style={{
|
||||||
|
animationDelay: `${index * 30}ms`,
|
||||||
|
animation: 'slideInFromLeft 0.2s ease-out forwards',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{status.name}
|
{/* Status Color Indicator */}
|
||||||
</div>
|
<div
|
||||||
|
className={`w-3 h-3 rounded-full shadow-sm border-2 ${
|
||||||
{/* Current Status Indicator */}
|
isDarkMode ? 'border-gray-800/30' : 'border-white/20'
|
||||||
{(status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status) && (
|
}`}
|
||||||
<div className="ml-auto">
|
style={{ backgroundColor: getStatusColor(status) }}
|
||||||
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
|
/>
|
||||||
</div>
|
|
||||||
)}
|
{/* Status Name */}
|
||||||
</button>
|
<span className="flex-1 truncate">
|
||||||
))}
|
{formatStatusName(status.name || '')}
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
|
{/* Current Status Badge */}
|
||||||
|
{isSelected && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
|
||||||
|
<span className={`text-xs font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
|
||||||
|
Current
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* CSS Animations - Injected as style tag */}
|
||||||
|
{isOpen && createPortal(
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInFromLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>,
|
||||||
|
document.head
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,15 +83,15 @@ const VirtualizedTaskGroup: React.FC<VirtualizedTaskGroupProps> = React.memo(({
|
|||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
|
||||||
<span className="column-header-text">Progress</span>
|
<span className="column-header-text">Progress</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
||||||
|
<span className="column-header-text">Status</span>
|
||||||
|
</div>
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
||||||
<span className="column-header-text">Members</span>
|
<span className="column-header-text">Members</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '200px' }}>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '200px' }}>
|
||||||
<span className="column-header-text">Labels</span>
|
<span className="column-header-text">Labels</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
|
||||||
<span className="column-header-text">Status</span>
|
|
||||||
</div>
|
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
||||||
<span className="column-header-text">Priority</span>
|
<span className="column-header-text">Priority</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,10 +100,10 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
const allScrollableColumns = [
|
const allScrollableColumns = [
|
||||||
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
||||||
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
{ 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: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
||||||
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
||||||
{ key: 'phase', label: 'Phase', width: 100, fieldKey: 'PHASE' },
|
{ 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: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
||||||
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
||||||
{ key: 'estimation', label: 'Estimation', width: 100, fieldKey: 'ESTIMATION' },
|
{ key: 'estimation', label: 'Estimation', width: 100, fieldKey: 'ESTIMATION' },
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ const initialState: projectViewTaskListColumnsState = {
|
|||||||
width: 60,
|
width: 60,
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
name: 'status',
|
||||||
|
columnHeader: 'status',
|
||||||
|
width: 120,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'members',
|
key: 'members',
|
||||||
name: 'members',
|
name: 'members',
|
||||||
@@ -69,13 +76,6 @@ const initialState: projectViewTaskListColumnsState = {
|
|||||||
width: 150,
|
width: 150,
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
name: 'status',
|
|
||||||
columnHeader: 'status',
|
|
||||||
width: 120,
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'priority',
|
key: 'priority',
|
||||||
name: 'priority',
|
name: 'priority',
|
||||||
|
|||||||
@@ -111,6 +111,24 @@ export const createColumns = ({
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor('status', {
|
||||||
|
header: 'Status',
|
||||||
|
id: COLUMN_KEYS.STATUS,
|
||||||
|
size: 120,
|
||||||
|
enablePinning: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<StatusDropdown
|
||||||
|
key={`${row.original.id}-status`}
|
||||||
|
statusList={statuses}
|
||||||
|
task={row.original}
|
||||||
|
teamId={getCurrentSession()?.team_id || ''}
|
||||||
|
onChange={statusId => {
|
||||||
|
console.log('Status changed:', statusId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
columnHelper.accessor('names', {
|
columnHelper.accessor('names', {
|
||||||
header: 'Assignees',
|
header: 'Assignees',
|
||||||
id: COLUMN_KEYS.ASSIGNEES,
|
id: COLUMN_KEYS.ASSIGNEES,
|
||||||
@@ -163,24 +181,6 @@ export const createColumns = ({
|
|||||||
cell: ({ row }) => <TaskRowDueTime dueTime={row.original.due_time || ''} />,
|
cell: ({ row }) => <TaskRowDueTime dueTime={row.original.due_time || ''} />,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor('status', {
|
|
||||||
header: 'Status',
|
|
||||||
id: COLUMN_KEYS.STATUS,
|
|
||||||
size: 120,
|
|
||||||
enablePinning: false,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<StatusDropdown
|
|
||||||
key={`${row.original.id}-status`}
|
|
||||||
statusList={statuses}
|
|
||||||
task={row.original}
|
|
||||||
teamId={getCurrentSession()?.team_id || ''}
|
|
||||||
onChange={statusId => {
|
|
||||||
console.log('Status changed:', statusId);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
columnHelper.accessor('labels', {
|
columnHelper.accessor('labels', {
|
||||||
header: 'Labels',
|
header: 'Labels',
|
||||||
id: COLUMN_KEYS.LABELS,
|
id: COLUMN_KEYS.LABELS,
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export const columnList: CustomTableColumnsType[] = [
|
|||||||
columnHeader: 'progress',
|
columnHeader: 'progress',
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
columnHeader: 'status',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'members',
|
key: 'members',
|
||||||
columnHeader: 'members',
|
columnHeader: 'members',
|
||||||
@@ -37,11 +42,6 @@ export const columnList: CustomTableColumnsType[] = [
|
|||||||
columnHeader: phaseHeader,
|
columnHeader: phaseHeader,
|
||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
columnHeader: 'status',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'priority',
|
key: 'priority',
|
||||||
columnHeader: 'priority',
|
columnHeader: 'priority',
|
||||||
|
|||||||
Reference in New Issue
Block a user