From 84f77940fd4569af3ea0fe0b0c4b02cd5e6bc0b3 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Fri, 27 Jun 2025 07:06:02 +0530 Subject: [PATCH 1/3] feat(task-management): add functionality to assign tasks to specific groups - Introduced `addTaskToGroup` action to allow tasks to be added to designated groups based on group IDs. - Enhanced task management slice to support group assignment for better organization and compatibility with V3 API. - Updated socket handlers to dispatch `addTaskToGroup` with appropriate group IDs extracted from backend responses. --- .../task-management/task-management.slice.ts | 39 +++++++++++++++++++ .../src/hooks/useTaskSocketHandlers.ts | 35 ++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/worklenz-frontend/src/features/task-management/task-management.slice.ts b/worklenz-frontend/src/features/task-management/task-management.slice.ts index d28a1b31..6ad52719 100644 --- a/worklenz-frontend/src/features/task-management/task-management.slice.ts +++ b/worklenz-frontend/src/features/task-management/task-management.slice.ts @@ -226,6 +226,44 @@ const taskManagementSlice = createSlice({ tasksAdapter.addOne(state, action.payload); }, + + addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId?: string }>) => { + const { task, groupId } = action.payload; + + // Add to entity adapter + tasksAdapter.addOne(state, task); + + // Add to groups array for V3 API compatibility + if (state.groups && state.groups.length > 0) { + console.log('🔍 Looking for group with ID:', groupId); + console.log('📋 Available groups:', state.groups.map(g => ({ id: g.id, title: g.title }))); + + // Find the target group using the provided UUID + const targetGroup = state.groups.find(group => { + // If a specific groupId (UUID) is provided, use it directly + if (groupId && group.id === groupId) { + return true; + } + + return false; + }); + + if (targetGroup) { + console.log('✅ Found target group:', targetGroup.title); + // Add task ID to the end of the group's taskIds array (newest last) + targetGroup.taskIds.push(task.id); + console.log('✅ Task added to group. New taskIds count:', targetGroup.taskIds.length); + + // Also add to the tasks array if it exists (for backward compatibility) + if ((targetGroup as any).tasks) { + (targetGroup as any).tasks.push(task); + } + } else { + console.warn('❌ No matching group found for groupId:', groupId); + } + } + }, + updateTask: (state, action: PayloadAction<{ id: string; changes: Partial }>) => { tasksAdapter.updateOne(state, { id: action.payload.id, @@ -392,6 +430,7 @@ const taskManagementSlice = createSlice({ export const { setTasks, addTask, + addTaskToGroup, updateTask, deleteTask, bulkUpdateTasks, diff --git a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts index d040b86f..80d99d23 100644 --- a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts +++ b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts @@ -35,6 +35,7 @@ import { } from '@/features/tasks/tasks.slice'; import { addTask, + addTaskToGroup, updateTask, moveTaskToGroup, selectCurrentGroupingV3, @@ -355,7 +356,39 @@ export const useTaskSocketHandlers = () => { order: data.sort_order || 0, }; - dispatch(addTask(task)); + // Extract the group UUID from the backend response based on current grouping + let groupId: string | undefined; + + console.log('🔍 Quick task received:', { + currentGrouping: currentGroupingV3, + status: data.status, + priority: data.priority, + phase_id: data.phase_id + }); + + // Select the correct UUID based on current grouping + // If currentGroupingV3 is null, default to 'status' since that's the most common grouping + const grouping = currentGroupingV3 || 'status'; + console.log('📊 Using grouping:', grouping); + + if (grouping === 'status') { + // For status grouping, use status field (which contains the status UUID) + groupId = data.status; + console.log('✅ Using status UUID:', groupId); + } else if (grouping === 'priority') { + // For priority grouping, use priority field (which contains the priority UUID) + groupId = data.priority; + console.log('✅ Using priority UUID:', groupId); + } else if (grouping === 'phase') { + // For phase grouping, use phase_id + groupId = data.phase_id; + console.log('✅ Using phase UUID:', groupId); + } + + console.log('📤 Dispatching addTaskToGroup with:', { taskId: task.id, groupId }); + + // Use addTaskToGroup with the actual group UUID + dispatch(addTaskToGroup({ task, groupId })); } }, [dispatch] From e73196a24983b7cd92ecdc7af7ca72a0e74efa8e Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Fri, 27 Jun 2025 07:06:14 +0530 Subject: [PATCH 2/3] feat(task-management): implement task movement between groups - Added `moveTaskBetweenGroups` action to facilitate moving tasks across different groups while maintaining state integrity. - Enhanced task management slice to support task updates during group transitions, including logging for better debugging. - Updated socket handlers to utilize the new action for moving tasks based on status, priority, and phase changes, improving task organization and user experience. --- .../components/task-management/task-group.tsx | 4 +- .../task-management/task-management.slice.ts | 62 +++++- .../src/hooks/useTaskSocketHandlers.ts | 197 +++++++++++++++--- 3 files changed, 225 insertions(+), 38 deletions(-) diff --git a/worklenz-frontend/src/components/task-management/task-group.tsx b/worklenz-frontend/src/components/task-management/task-group.tsx index 7df7af83..3f96b37d 100644 --- a/worklenz-frontend/src/components/task-management/task-group.tsx +++ b/worklenz-frontend/src/components/task-management/task-group.tsx @@ -308,7 +308,6 @@ const TaskGroup: React.FC = React.memo(({ No tasks in this group
+ + {/* Dropdown Menu */} + {isOpen && createPortal( +
+ {/* Phase Options */} +
+ {/* No Phase Option */} + + + {/* Phase Options */} + {phaseList.map((phase, index) => { + const isSelected = phase.name === task.phase; + + return ( + + ); + })} +
+
, + document.body + )} + + {/* CSS Animations */} + {isOpen && createPortal( + , + document.head + )} + + ); +}; + +export default TaskPhaseDropdown; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/task-priority-dropdown.tsx b/worklenz-frontend/src/components/task-management/task-priority-dropdown.tsx new file mode 100644 index 00000000..db157568 --- /dev/null +++ b/worklenz-frontend/src/components/task-management/task-priority-dropdown.tsx @@ -0,0 +1,257 @@ +import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import { useAppSelector } from '@/hooks/useAppSelector'; +import { useSocket } from '@/socket/socketContext'; +import { SocketEvents } from '@/shared/socket-events'; +import { Task } from '@/types/task-management.types'; +import { MinusOutlined, PauseOutlined, DoubleRightOutlined } from '@ant-design/icons'; + +interface TaskPriorityDropdownProps { + task: Task; + projectId: string; + isDarkMode?: boolean; +} + +const TaskPriorityDropdown: React.FC = ({ + task, + projectId, + isDarkMode = false +}) => { + const { socket, connected } = useSocket(); + const [isOpen, setIsOpen] = useState(false); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); + const buttonRef = useRef(null); + const dropdownRef = useRef(null); + + const priorityList = useAppSelector(state => state.priorityReducer.priorities); + + // Find current priority details + const currentPriority = useMemo(() => { + return priorityList.find(priority => + priority.name?.toLowerCase() === task.priority?.toLowerCase() || + priority.id === task.priority + ); + }, [priorityList, task.priority]); + + // Handle priority change + const handlePriorityChange = useCallback((priorityId: string, priorityName: string) => { + if (!task.id || !priorityId || !connected) return; + + console.log('🎯 Priority change initiated:', { taskId: task.id, priorityId, priorityName }); + + socket?.emit( + SocketEvents.TASK_PRIORITY_CHANGE.toString(), + JSON.stringify({ + task_id: task.id, + priority_id: priorityId, + team_id: projectId, // Using projectId as teamId + }) + ); + setIsOpen(false); + }, [task.id, connected, socket, projectId]); + + // Calculate dropdown position and handle outside clicks + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (buttonRef.current && buttonRef.current.contains(event.target as Node)) { + return; // Don't close if clicking the button + } + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen && buttonRef.current) { + // Calculate position + const rect = buttonRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + window.scrollY + 4, + left: rect.left + window.scrollX, + }); + + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + // Get priority color + const getPriorityColor = useCallback((priority: any) => { + if (isDarkMode) { + return priority?.color_code_dark || priority?.color_code || '#4b5563'; + } + return priority?.color_code || '#6b7280'; + }, [isDarkMode]); + + // Get priority icon + const getPriorityIcon = useCallback((priorityName: string) => { + const name = priorityName?.toLowerCase(); + switch (name) { + case 'low': + return ; + case 'medium': + return ; + case 'high': + return ; + default: + return ; + } + }, []); + + // Format priority name for display + const formatPriorityName = useCallback((name: string) => { + if (!name) return name; + return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); + }, []); + + if (!task.priority) return null; + + return ( + <> + {/* Priority Button - Simple text display like status */} + + + {/* Dropdown Menu */} + {isOpen && createPortal( +
+ {/* Priority Options */} +
+ {priorityList.map((priority, index) => { + const isSelected = priority.name?.toLowerCase() === task.priority?.toLowerCase() || priority.id === task.priority; + + return ( + + ); + })} +
+
, + document.body + )} + + {/* CSS Animations */} + {isOpen && createPortal( + , + document.head + )} + + ); +}; + +export default TaskPriorityDropdown; \ 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 32b80309..de8731b1 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -22,6 +22,8 @@ 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 TaskPriorityDropdown from './task-priority-dropdown'; +import TaskPhaseDropdown from './task-phase-dropdown'; import { formatDate as utilFormatDate, formatDateTime as utilFormatDateTime, @@ -419,6 +421,24 @@ const TaskRow: React.FC = React.memo(({ ); + case 'priority': + return ( +
+
+ {task.priority || 'Medium'} +
+
+ ); + + case 'phase': + return ( +
+
+ {task.phase || 'No Phase'} +
+
+ ); + default: // For non-essential columns, show placeholder during initial load return ( @@ -580,10 +600,14 @@ const TaskRow: React.FC = React.memo(({ case 'phase': return ( -
- - {task.phase || 'No Phase'} - +
+
+ +
); @@ -602,8 +626,14 @@ const TaskRow: React.FC = React.memo(({ case 'priority': return ( -
- +
+
+ +
); diff --git a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts index 8990c2ce..160fba41 100644 --- a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts +++ b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts @@ -240,65 +240,81 @@ 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)); dispatch(deselectAll()); - // For the task management slice, move task between groups if grouping by priority + // For the task management slice, always update the task entity first const state = store.getState(); - const groups = state.taskManagement.groups; const currentTask = state.taskManagement.entities[response.id]; - const currentGrouping = state.taskManagement.grouping; - if (groups && groups.length > 0 && currentTask && response.priority_id && currentGrouping === 'priority') { - // Find current group containing the task - const currentGroup = groups.find(group => group.taskIds.includes(response.id)); + if (currentTask) { + // Get priority list to map priority_id to priority name + const priorityList = state.priorityReducer?.priorities || []; + const priority = priorityList.find(p => p.id === response.priority_id); - // Find target group based on new priority ID - const targetGroup = groups.find(group => group.id === response.priority_id); - - if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { - console.log('🔄 Moving task between priority groups:', { - taskId: response.id, - fromGroup: currentGroup.title, - toGroup: targetGroup.title - }); - - // Determine priority value from target group - let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium'; - const priorityValue = targetGroup.groupValue.toLowerCase(); - if (['critical', 'high', 'medium', 'low'].includes(priorityValue)) { - newPriorityValue = priorityValue as 'critical' | 'high' | 'medium' | 'low'; - } - - dispatch(moveTaskBetweenGroups({ - taskId: response.id, - fromGroupId: currentGroup.id, - toGroupId: targetGroup.id, - taskUpdate: { - priority: newPriorityValue, - } - })); - } else if (!currentGroup || !targetGroup) { - // Fallback to refetch if groups not found - console.log('🔄 Priority groups not found, refetching tasks...'); - if (projectId) { - dispatch(fetchTasksV3(projectId)); + let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium'; + if (priority?.name) { + const priorityName = priority.name.toLowerCase(); + if (['critical', 'high', 'medium', 'low'].includes(priorityName)) { + newPriorityValue = priorityName as 'critical' | 'high' | 'medium' | 'low'; } } - } else if (currentGrouping !== 'priority') { - // If not grouping by priority, just update the task - if (currentTask) { - let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium'; - // We need to map priority_id to priority value - this might require additional logic - // For now, let's just update without changing groups - dispatch(updateTask({ - id: response.id, - changes: { - // priority: newPriorityValue, // We'd need to map priority_id to value - } - })); + + 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, + changes: { + priority: newPriorityValue, + updatedAt: new Date().toISOString(), + } + })); + + // Handle group movement ONLY if grouping by priority + const groups = state.taskManagement.groups; + const currentGrouping = state.taskManagement.grouping; + + if (groups && groups.length > 0 && currentGrouping === 'priority') { + // Find current group containing the task + const currentGroup = groups.find(group => group.taskIds.includes(response.id)); + + // Find target group based on new priority value + const targetGroup = groups.find(group => + group.groupValue.toLowerCase() === newPriorityValue.toLowerCase() + ); + + 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, + fromGroupId: currentGroup.id, + toGroupId: targetGroup.id, + taskUpdate: { + priority: newPriorityValue, + } + })); + } else { + console.log('🔧 No group movement needed for priority change'); + } + } else { + console.log('🔧 Not grouped by priority, skipping group movement'); } } }, @@ -349,61 +365,95 @@ 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()); - // For the task management slice, move task between groups if grouping by phase + // For the task management slice, always update the task entity first const state = store.getState(); - const groups = state.taskManagement.groups; - const currentTask = state.taskManagement.entities[data.task_id || data.id]; - const currentGrouping = state.taskManagement.grouping; - const taskId = data.task_id || data.id; + const taskId = data.task_id; - if (groups && groups.length > 0 && currentTask && taskId && currentGrouping === 'phase') { - // Find current group containing the task - const currentGroup = groups.find(group => group.taskIds.includes(taskId)); + if (taskId) { + const currentTask = state.taskManagement.entities[taskId]; - // For phase changes, we need to find the target group by phase name/value - // The response might not have a direct phase_id, so we'll look for the group by value - let targetGroup: any = null; + if (currentTask) { + // Get phase list to map phase_id to phase name + const phaseList = state.phaseReducer?.phaseList || []; + let newPhaseValue = ''; - // Try to find target group - this might need adjustment based on the actual response structure if (data.id) { - targetGroup = groups.find(group => group.id === data.id); + // data.id is the phase_id + const phase = phaseList.find(p => p.id === data.id); + newPhaseValue = phase?.name || ''; + } else { + // No phase selected (cleared) + newPhaseValue = ''; } - - if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { - console.log('🔄 Moving task between phase groups:', { - taskId: taskId, - fromGroup: currentGroup.title, - toGroup: targetGroup.title - }); - - dispatch(moveTaskBetweenGroups({ - taskId: taskId, - fromGroupId: currentGroup.id, - toGroupId: targetGroup.id, - taskUpdate: { - phase: targetGroup.groupValue, - } - })); - } else if (!currentGroup || !targetGroup) { - // Fallback to refetch if groups not found - console.log('🔄 Phase groups not found, refetching tasks...'); - if (projectId) { - dispatch(fetchTasksV3(projectId)); + + 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, + changes: { + phase: newPhaseValue, + updatedAt: new Date().toISOString(), } + })); + + // Handle group movement ONLY if grouping by phase + const groups = state.taskManagement.groups; + const currentGrouping = state.taskManagement.grouping; + + if (groups && groups.length > 0 && currentGrouping === 'phase') { + // Find current group containing the task + const currentGroup = groups.find(group => group.taskIds.includes(taskId)); + + // Find target group based on new phase value + let targetGroup: any = null; + + if (newPhaseValue) { + // Find group by phase name + targetGroup = groups.find(group => + group.groupValue === newPhaseValue || group.title === newPhaseValue + ); + } else { + // Find "No Phase" or similar group + targetGroup = groups.find(group => + group.groupValue === '' || group.title.toLowerCase().includes('no phase') || group.title.toLowerCase().includes('unassigned') + ); + } + + 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, + fromGroupId: currentGroup.id, + toGroupId: targetGroup.id, + taskUpdate: { + phase: newPhaseValue, + } + })); + } else { + console.log('🔧 No group movement needed for phase change'); + } + } else { + console.log('🔧 Not grouped by phase, skipping group movement'); } - } else if (currentGrouping !== 'phase') { - // If not grouping by phase, just update the task - if (currentTask && taskId) { - dispatch(updateTask({ - id: taskId, - changes: { - // phase: newPhaseValue, // We'd need to determine the phase value - } - })); } } },