diff --git a/worklenz-backend/src/socket.io/commands/on-task-sort-order-change.ts b/worklenz-backend/src/socket.io/commands/on-task-sort-order-change.ts index 450551fb..83b4a70e 100644 --- a/worklenz-backend/src/socket.io/commands/on-task-sort-order-change.ts +++ b/worklenz-backend/src/socket.io/commands/on-task-sort-order-change.ts @@ -73,7 +73,7 @@ const onTaskSortOrderChange = async (io: Server, socket: Socket, data: ChangeReq // PERFORMANCE OPTIMIZATION: Use cached dependency check if available const cacheKey = `${project_id}-${userId}-${team_id}`; const cachedDependency = dependencyCache.get(cacheKey); - + let hasAccess = false; if (cachedDependency && (Date.now() - cachedDependency.timestamp) < CACHE_TTL) { hasAccess = cachedDependency.result; @@ -82,16 +82,16 @@ const onTaskSortOrderChange = async (io: Server, socket: Socket, data: ChangeReq const dependencyResult = await dbPool.query(` SELECT EXISTS( SELECT 1 FROM project_members pm - INNER JOIN projects p ON p.id = pm.project_id - WHERE pm.project_id = $1 - AND pm.user_id = $2 - AND p.team_id = $3 - AND pm.is_active = true + INNER JOIN projects p ON p.id = pm.project_id + INNER JOIN team_members tm ON pm.team_member_id = tm.id +WHERE pm.project_id = $1 + AND tm.user_id = $2 + AND p.team_id = $3 ) as has_access `, [project_id, userId, team_id]); - + hasAccess = dependencyResult.rows[0]?.has_access || false; - + // Cache the result dependencyCache.set(cacheKey, { result: hasAccess, timestamp: Date.now() }); } @@ -152,8 +152,8 @@ const onTaskSortOrderChange = async (io: Server, socket: Socket, data: ChangeReq }); // Send success response - socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { - success: true, + socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { + success: true, task_id: task.id, from_group, to_group, @@ -162,8 +162,8 @@ const onTaskSortOrderChange = async (io: Server, socket: Socket, data: ChangeReq } catch (error) { log_error(error); - socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { - error: "Internal server error" + socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { + error: "Internal server error" }); } }; diff --git a/worklenz-frontend/src/components/AssigneeSelector.tsx b/worklenz-frontend/src/components/AssigneeSelector.tsx index 177e0e73..50cabbab 100644 --- a/worklenz-frontend/src/components/AssigneeSelector.tsx +++ b/worklenz-frontend/src/components/AssigneeSelector.tsx @@ -14,6 +14,7 @@ import { sortTeamMembers } from '@/utils/sort-team-members'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice'; import { updateTask } from '@/features/task-management/task-management.slice'; +import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice'; interface AssigneeSelectorProps { task: IProjectTask; @@ -177,6 +178,12 @@ const AssigneeSelector: React.FC = ({ // Emit socket event - the socket handler will update Redux with proper types socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body)); + socket?.once( + SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), + (data: any) => { + dispatch(updateEnhancedKanbanTaskAssignees(data)); + } + ); // Remove from pending changes after a short delay (optimistic) setTimeout(() => { @@ -226,6 +233,7 @@ const AssigneeSelector: React.FC = ({ {isOpen && createPortal(
e.stopPropagation()} className={` fixed z-9999 w-72 rounded-md shadow-lg border ${isDarkMode @@ -284,12 +292,14 @@ const AssigneeSelector: React.FC = ({ }} >
- handleMemberToggle(member.id || '', checked)} - disabled={member.pending_invitation || pendingChanges.has(member.id || '')} - isDarkMode={isDarkMode} - /> + e.stopPropagation()}> + handleMemberToggle(member.id || '', checked)} + disabled={member.pending_invitation || pendingChanges.has(member.id || '')} + isDarkMode={isDarkMode} + /> + {pendingChanges.has(member.id || '') && (
import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters')); @@ -68,7 +65,8 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl performanceMetrics } = useSelector((state: RootState) => state.enhancedKanbanReducer); const { socket } = useSocket(); - const { teamId } = useAppSelector((state: RootState) => state.auth); + const authService = useAuthService(); + const teamId = authService.getCurrentSession()?.team_id; const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy); const project = useAppSelector((state: RootState) => state.projectReducer.project); const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer); diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx index e4f84142..7fcf062b 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx @@ -13,7 +13,6 @@ import { useAppDispatch } from '@/hooks/useAppDispatch'; import { setShowTaskDrawer, setSelectedTaskId } from '@/features/task-drawer/task-drawer.slice'; import PrioritySection from '../board/taskCard/priority-section/priority-section'; import Typography from 'antd/es/typography'; -import CustomAvatarGroup from '../board/custom-avatar-group'; import CustomDueDatePicker from '../board/custom-due-date-picker'; import { themeWiseColor } from '@/utils/themeWiseColor'; import { ForkOutlined } from '@ant-design/icons'; @@ -29,6 +28,8 @@ import BoardSubTaskCard from '@/pages/projects/projectView/board/board-section/b import BoardCreateSubtaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-create-sub-task-card'; import { useTranslation } from 'react-i18next'; import EnhancedKanbanCreateSubtaskCard from './EnhancedKanbanCreateSubtaskCard'; +import LazyAssigneeSelectorWrapper from '@/components/task-management/lazy-assignee-selector'; +import AvatarGroup from '@/components/AvatarGroup'; interface EnhancedKanbanTaskCardProps { task: IProjectTask; @@ -179,8 +180,15 @@ const EnhancedKanbanTaskCard: React.FC = React.memo marginBlock: 8, }} > - {task && } - + + + + diff --git a/worklenz-frontend/src/components/enhanced-kanban/README.md b/worklenz-frontend/src/components/enhanced-kanban/README.md deleted file mode 100644 index f2ef2575..00000000 --- a/worklenz-frontend/src/components/enhanced-kanban/README.md +++ /dev/null @@ -1,189 +0,0 @@ -# Enhanced Kanban Board - Performance Optimizations - -## Overview - -The Enhanced Kanban Board is designed to handle **much much** tasks efficiently through multiple performance optimization strategies. - -## Performance Features - -### 🚀 **Virtualization** -- **Automatic Activation**: Virtualization kicks in when groups have >50 tasks -- **React Window**: Uses `react-window` for efficient rendering of large lists -- **Overscan**: Renders 5 extra items for smooth scrolling -- **Dynamic Height**: Adjusts container height based on task count - -### 📊 **Performance Monitoring** -- **Real-time Metrics**: Tracks total tasks, largest group, average group size -- **Visual Indicators**: Shows performance status (Excellent/Good/Warning/Critical) -- **Virtualization Status**: Indicates when virtualization is active -- **Performance Tips**: Provides optimization suggestions for large datasets - -### 🎯 **Smart Rendering** -- **Memoization**: All components use React.memo for optimal re-rendering -- **Conditional Rendering**: Drop indicators only render when needed -- **Lazy Loading**: Components load only when required -- **CSS Containment**: Uses `contain: layout style paint` for performance - -### 💾 **Caching Strategy** -- **Task Cache**: Stores individual tasks for quick access -- **Group Cache**: Caches group data to prevent recalculation -- **Redux Optimization**: Optimistic updates with rollback capability -- **Memory Management**: Automatic cache cleanup - -## Performance Thresholds - -| Task Count | Performance Level | Features Enabled | -|------------|-------------------|------------------| -| 0-50 | Excellent | Standard rendering | -| 51-100 | Good | Virtualization | -| 101-500 | Good | Virtualization + Monitoring | -| 501-1000 | Warning | All optimizations | -| 1000+ | Critical | All optimizations + Tips | - -## Key Components - -### VirtualizedTaskList -```typescript -// Handles large task lists efficiently - -``` - -### PerformanceMonitor -```typescript -// Shows performance metrics for large datasets - -// Only appears when totalTasks > 100 -``` - -### EnhancedKanbanGroup -```typescript -// Automatically switches between standard and virtualized rendering -const shouldVirtualize = group.tasks.length > VIRTUALIZATION_THRESHOLD; -``` - -## Performance Optimizations - -### 1. **React Optimization** -- `React.memo()` on all components -- `useMemo()` for expensive calculations -- `useCallback()` for event handlers -- Conditional rendering to avoid unnecessary work - -### 2. **CSS Performance** -```css -/* Performance optimizations */ -.enhanced-kanban-group-tasks.large-list { - contain: layout style paint; - will-change: transform; -} -``` - -### 3. **Drag and Drop Optimization** -- Enhanced collision detection -- Optimized sensor configuration -- Minimal re-renders during drag operations -- Efficient drop target identification - -### 4. **Memory Management** -- Automatic cache cleanup -- Efficient data structures -- Minimal object creation -- Proper cleanup in useEffect - -## Usage Examples - -### Large Dataset Handling -```typescript -// The board automatically optimizes for large datasets -const largeProject = { - taskGroups: [ - { id: '1', name: 'To Do', tasks: [/* 200 tasks */] }, - { id: '2', name: 'In Progress', tasks: [/* 150 tasks */] }, - { id: '3', name: 'Done', tasks: [/* 300 tasks */] } - ] -}; -// Total: 650 tasks - virtualization automatically enabled -``` - -### Performance Monitoring -```typescript -// Performance metrics are automatically tracked -const metrics = { - totalTasks: 650, - largestGroupSize: 300, - averageGroupSize: 217, - virtualizationEnabled: true -}; -``` - -## Best Practices - -### For Large Projects -1. **Use Filters**: Reduce visible tasks with search/filters -2. **Group Strategically**: Choose grouping that distributes tasks evenly -3. **Monitor Performance**: Watch the performance monitor for insights -4. **Consider Pagination**: For extremely large datasets (>2000 tasks) - -### For Optimal Performance -1. **Keep Groups Balanced**: Avoid single groups with 1000+ tasks -2. **Use Meaningful Grouping**: Group by status, priority, or assignee -3. **Regular Cleanup**: Archive completed tasks regularly -4. **Monitor Metrics**: Use the performance monitor to track trends - -## Technical Details - -### Virtualization Implementation -- **Item Height**: Fixed at 80px for consistency -- **Overscan**: 5 items for smooth scrolling -- **Dynamic Height**: Scales with content (200px - 600px) -- **Responsive**: Adapts to screen size - -### Memory Usage -- **Task Cache**: ~1KB per task -- **Group Cache**: ~2KB per group -- **Virtualization**: Only renders visible items -- **Cleanup**: Automatic garbage collection - -### Rendering Performance -- **60fps**: Maintained even with 1000+ tasks -- **Smooth Scrolling**: Optimized for large lists -- **Drag and Drop**: Responsive even with large datasets -- **Updates**: Optimistic updates for immediate feedback - -## Troubleshooting - -### Performance Issues -1. **Check Task Count**: Monitor the performance metrics -2. **Enable Virtualization**: Ensure groups with >50 tasks use virtualization -3. **Use Filters**: Reduce visible tasks with search/filters -4. **Group Optimization**: Consider different grouping strategies - -### Memory Issues -1. **Clear Cache**: Use the clear cache action if needed -2. **Archive Tasks**: Move completed tasks to archived status -3. **Monitor Usage**: Watch browser memory usage -4. **Refresh**: Reload the page if memory usage is high - -## Future Enhancements - -### Planned Optimizations -- **Infinite Scrolling**: Load tasks on demand -- **Web Workers**: Move heavy calculations to background threads -- **IndexedDB**: Client-side caching for offline support -- **Service Workers**: Background sync for updates - -### Advanced Features -- **Predictive Loading**: Pre-load likely-to-be-viewed tasks -- **Smart Caching**: AI-powered cache optimization -- **Performance Analytics**: Detailed performance insights -- **Auto-optimization**: Automatic performance tuning - ---- - -**The Enhanced Kanban Board is designed to handle projects of any size efficiently, from small teams to enterprise-scale operations with thousands of tasks.** \ No newline at end of file diff --git a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts index bdf9480b..204d1b3f 100644 --- a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts +++ b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts @@ -176,8 +176,6 @@ export const useTaskSocketHandlers = () => { (response: ITaskListStatusChangeResponse) => { if (!response) return; - console.log('🔄 Status change received:', response); - if (response.completed_deps === false) { alertService.error( 'Task is not completed', @@ -198,13 +196,6 @@ export const useTaskSocketHandlers = () => { const groups = state.taskManagement.groups; const currentTask = state.taskManagement.entities[response.id]; - console.log('🔍 Status change debug:', { - hasGroups: !!groups, - groupsLength: groups?.length || 0, - hasCurrentTask: !!currentTask, - statusId: response.status_id, - currentGrouping: state.taskManagement.grouping - }); if (groups && groups.length > 0 && currentTask && response.status_id) { // Find current group containing the task @@ -215,11 +206,6 @@ export const useTaskSocketHandlers = () => { const targetGroup = groups.find(group => group.id === response.status_id); if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { - console.log('🔄 Moving task between groups:', { - taskId: response.id, - fromGroup: currentGroup.title, - toGroup: targetGroup.title - }); // Determine the new status value based on status category let newStatusValue: 'todo' | 'doing' | 'done' = 'todo'; @@ -244,8 +230,6 @@ export const useTaskSocketHandlers = () => { } })); } else if (!currentGroup || !targetGroup) { - // Log the issue but don't refetch - real-time updates should handle this - console.log('🔄 Groups not found, but avoiding refetch to prevent data thrashing'); // Remove unnecessary refetch that causes data thrashing // if (projectId) { // dispatch(fetchTasksV3(projectId)); @@ -305,8 +289,6 @@ export const useTaskSocketHandlers = () => { (response: ITaskListPriorityChangeResponse) => { if (!response) return; - console.log('🎯 Priority change received:', response); - // Update the old task slice (for backward compatibility) dispatch(updateTaskPriority(response)); dispatch(setTaskPriority(response)); @@ -332,14 +314,6 @@ export const useTaskSocketHandlers = () => { } } - console.log('🔧 Updating task priority:', { - taskId: response.id, - oldPriority: currentTask.priority, - newPriority: newPriorityValue, - priorityId: response.priority_id, - currentGrouping: state.taskManagement.grouping - }); - // Update the task entity dispatch(updateTask({ id: response.id, @@ -363,12 +337,6 @@ export const useTaskSocketHandlers = () => { ); if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { - console.log('🔄 Moving task between priority groups:', { - taskId: response.id, - fromGroup: currentGroup.title, - toGroup: targetGroup.title, - newPriority: newPriorityValue - }); dispatch(moveTaskBetweenGroups({ taskId: response.id, @@ -439,8 +407,6 @@ export const useTaskSocketHandlers = () => { (data: ITaskPhaseChangeResponse) => { if (!data) return; - console.log('🎯 Phase change received:', data); - // Update the old task slice (for backward compatibility) dispatch(updateTaskPhase(data)); dispatch(deselectAll()); @@ -466,14 +432,6 @@ export const useTaskSocketHandlers = () => { newPhaseValue = ''; } - console.log('🔧 Updating task phase:', { - taskId: taskId, - oldPhase: currentTask.phase, - newPhase: newPhaseValue, - phaseId: data.id, - currentGrouping: state.taskManagement.grouping - }); - // Update the task entity dispatch(updateTask({ id: taskId, @@ -507,12 +465,6 @@ export const useTaskSocketHandlers = () => { } if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { - console.log('🔄 Moving task between phase groups:', { - taskId: taskId, - fromGroup: currentGroup.title, - toGroup: targetGroup.title, - newPhase: newPhaseValue || 'No Phase' - }); dispatch(moveTaskBetweenGroups({ taskId: taskId, @@ -705,7 +657,7 @@ export const useTaskSocketHandlers = () => { // This event only provides assignee IDs, so we update what we can // The full assignee data will come from QUICK_ASSIGNEES_UPDATE - console.log('🔄 Task assignees change (limited data):', data); + // console.log('🔄 Task assignees change (limited data):', data); }, [] );