From 8fcd4d0d53f320ffcfeac51c054b326e65c64b20 Mon Sep 17 00:00:00 2001 From: shancds Date: Tue, 1 Jul 2025 09:42:47 +0530 Subject: [PATCH] feat(enhanced-kanban): integrate task assignee, label, and priority updates - Added actions to update task assignees, labels, and priority within the enhanced Kanban feature, enhancing task management capabilities. - Updated task drawer components to utilize new actions for real-time updates based on user interactions. - Improved state management for better handling of task properties across different views. --- .../task-drawer-assignee-selector.tsx | 13 +- .../task-drawer-labels/task-drawer-labels.tsx | 9 +- .../task-drawer-priority-selector.tsx | 3 +- .../enhanced-kanban/enhanced-kanban.slice.ts | 172 +++++++++++++++++- 4 files changed, 185 insertions(+), 12 deletions(-) diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-assignee-selector/task-drawer-assignee-selector.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-assignee-selector/task-drawer-assignee-selector.tsx index 0f11346b..cb8d349f 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-assignee-selector/task-drawer-assignee-selector.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-assignee-selector/task-drawer-assignee-selector.tsx @@ -26,6 +26,7 @@ import { setTaskAssignee } from '@/features/task-drawer/task-drawer.slice'; import useTabSearchParam from '@/hooks/useTabSearchParam'; import { updateTaskAssignees as updateBoardTaskAssignees } from '@/features/board/board-slice'; import { updateTaskAssignees as updateTasksListTaskAssignees } from '@/features/tasks/tasks.slice'; +import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice'; interface TaskDrawerAssigneeSelectorProps { task: ITaskViewModel; } @@ -88,12 +89,12 @@ const TaskDrawerAssigneeSelector = ({ task }: TaskDrawerAssigneeSelectorProps) = SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), (data: ITaskAssigneesUpdateResponse) => { dispatch(setTaskAssignee(data)); - // if (tab === 'tasks-list') { - // dispatch(updateTasksListTaskAssignees(data)); - // } - // if (tab === 'board') { - // dispatch(updateBoardTaskAssignees(data)); - // } + if (tab === 'tasks-list') { + dispatch(updateTasksListTaskAssignees(data)); + } + if (tab === 'board') { + dispatch(updateEnhancedKanbanTaskAssignees(data)); + } } ); } catch (error) { diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-labels/task-drawer-labels.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-labels/task-drawer-labels.tsx index ae004d0b..dc182c14 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-labels/task-drawer-labels.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-labels/task-drawer-labels.tsx @@ -28,6 +28,7 @@ import { useAppDispatch } from '@/hooks/useAppDispatch'; import { setTaskLabels } from '@/features/task-drawer/task-drawer.slice'; import { setLabels, updateTaskLabel } from '@/features/tasks/tasks.slice'; import { setBoardLabels, updateBoardTaskLabel } from '@/features/board/board-slice'; +import { updateEnhancedKanbanTaskLabels } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import { ILabelsChangeResponse } from '@/types/tasks/taskList.types'; import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types'; @@ -65,7 +66,7 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => { dispatch(updateTaskLabel(data)); } if (tab === 'board') { - dispatch(updateBoardTaskLabel(data)); + dispatch(updateEnhancedKanbanTaskLabels(data)); } } ); @@ -90,9 +91,9 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => { if (tab === 'tasks-list') { dispatch(updateTaskLabel(data)); } - if (tab === 'board') { - dispatch(updateBoardTaskLabel(data)); - } + if (tab === 'board') { + dispatch(updateEnhancedKanbanTaskLabels(data)); + } } ); }; diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-priority-selector/task-drawer-priority-selector.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-priority-selector/task-drawer-priority-selector.tsx index 400d9c3f..d1f95da6 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-priority-selector/task-drawer-priority-selector.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-priority-selector/task-drawer-priority-selector.tsx @@ -15,6 +15,7 @@ import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priorit import { setTaskPriority } from '@/features/task-drawer/task-drawer.slice'; import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice'; import { updateTaskPriority as updateTasksListTaskPriority } from '@/features/tasks/tasks.slice'; +import { updateEnhancedKanbanTaskPriority } from '@/features/enhanced-kanban/enhanced-kanban.slice'; type PriorityDropdownProps = { task: ITaskViewModel; @@ -48,7 +49,7 @@ const PriorityDropdown = ({ task }: PriorityDropdownProps) => { dispatch(updateTasksListTaskPriority(data)); } if (tab === 'board') { - dispatch(updateBoardTaskPriority(data)); + dispatch(updateEnhancedKanbanTaskPriority(data)); } } ); diff --git a/worklenz-frontend/src/features/enhanced-kanban/enhanced-kanban.slice.ts b/worklenz-frontend/src/features/enhanced-kanban/enhanced-kanban.slice.ts index d7eeaa92..90303e71 100644 --- a/worklenz-frontend/src/features/enhanced-kanban/enhanced-kanban.slice.ts +++ b/worklenz-frontend/src/features/enhanced-kanban/enhanced-kanban.slice.ts @@ -14,6 +14,10 @@ import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.ty import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types'; import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types'; import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service'; +import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response'; +import { ITaskAssignee } from '@/types/project/projectTasksViewModel.types'; +import { InlineMember } from '@/types/teamMembers/inlineMember.types'; +import { ILabelsChangeResponse } from '@/types/tasks/taskList.types'; export enum IGroupBy { STATUS = 'status', @@ -357,6 +361,53 @@ export const fetchEnhancedKanbanLabels = createAsyncThunk( } ); +// Helper functions for common operations (similar to board-slice.ts) +const findTaskInAllGroups = ( + taskGroups: ITaskListGroup[], + taskId: string +): { task: IProjectTask; group: ITaskListGroup; groupId: string } | null => { + for (const group of taskGroups) { + const task = group.tasks.find(t => t.id === taskId); + if (task) return { task, group, groupId: group.id }; + + // Check in subtasks + for (const parentTask of group.tasks) { + if (!parentTask.sub_tasks) continue; + const subtask = parentTask.sub_tasks.find(st => st.id === taskId); + if (subtask) return { task: subtask, group, groupId: group.id }; + } + } + return null; +}; + +const deleteTaskFromGroup = ( + taskGroups: ITaskListGroup[], + task: IProjectTask, + groupId: string, + index: number | null = null +): void => { + const group = taskGroups.find(g => g.id === groupId); + if (!group || !task.id) return; + + if (task.is_sub_task) { + const parentTask = group.tasks.find(t => t.id === task.parent_task_id); + if (parentTask) { + const subTaskIndex = parentTask.sub_tasks?.findIndex(t => t.id === task.id); + if (typeof subTaskIndex !== 'undefined' && subTaskIndex !== -1) { + parentTask.sub_tasks_count = Math.max((parentTask.sub_tasks_count || 0) - 1, 0); + parentTask.sub_tasks?.splice(subTaskIndex, 1); + } + } + } else { + const taskIndex = index ?? group.tasks.findIndex(t => t.id === task.id); + if (taskIndex !== -1) { + group.tasks.splice(taskIndex, 1); + } + } +}; + + + const enhancedKanbanSlice = createSlice({ name: 'enhancedKanbanReducer', initialState, @@ -497,7 +548,7 @@ const enhancedKanbanSlice = createSlice({ // Enhanced Kanban external status update (for use in task drawer dropdown) updateEnhancedKanbanTaskStatus: (state, action: PayloadAction) => { - const { id: task_id, status_id } = action.payload; + const { id: task_id, status_id, color_code, color_code_dark, complete_ratio, statusCategory } = action.payload; let oldGroupId: string | null = null; let foundTask: IProjectTask | null = null; // Find the task and its group @@ -510,6 +561,14 @@ const enhancedKanbanSlice = createSlice({ } } if (!foundTask) return; + + // Update the task properties + foundTask.status_color = color_code; + foundTask.status_color_dark = color_code_dark; + foundTask.complete_ratio = +complete_ratio; + foundTask.status = status_id; + foundTask.status_category = statusCategory; + // If grouped by status and the group changes, move the task if (state.groupBy === IGroupBy.STATUS && oldGroupId && oldGroupId !== status_id) { // Remove from old group @@ -531,6 +590,110 @@ const enhancedKanbanSlice = createSlice({ state.taskCache[task_id] = foundTask; }, + // Enhanced Kanban priority update (for use in task drawer dropdown) + updateEnhancedKanbanTaskPriority: (state, action: PayloadAction) => { + const { id, priority_id, color_code, color_code_dark } = action.payload; + + // Find the task in any group + const taskInfo = findTaskInAllGroups(state.taskGroups, id); + if (!taskInfo || !priority_id) return; + + const { task, groupId } = taskInfo; + + // Update the task properties + task.priority = priority_id; + task.priority_color = color_code; + task.priority_color_dark = color_code_dark; + + // If grouped by priority and not a subtask, move the task to the new priority group + if ( + state.groupBy === IGroupBy.PRIORITY && + !task.is_sub_task && + groupId !== priority_id + ) { + // Remove from current group + deleteTaskFromGroup(state.taskGroups, task, groupId); + + // Add to new priority group + const newGroup = state.taskGroups.find(g => g.id === priority_id); + if (newGroup) { + newGroup.tasks.unshift(task); + state.groupCache[priority_id] = newGroup; + } + } + + // Update cache + state.taskCache[id] = task; + }, + + // Enhanced Kanban assignee update (for use in task drawer dropdown) + updateEnhancedKanbanTaskAssignees: (state, action: PayloadAction) => { + const { id, assignees, names } = action.payload; + + // Find the task in any group + const taskInfo = findTaskInAllGroups(state.taskGroups, id); + if (!taskInfo) return; + + const { task } = taskInfo; + + // Update the task properties + task.assignees = assignees as ITaskAssignee[]; + task.names = names as InlineMember[]; + + // Update cache + state.taskCache[id] = task; + }, + + // Enhanced Kanban label update (for use in task drawer dropdown) + updateEnhancedKanbanTaskLabels: (state, action: PayloadAction) => { + const label = action.payload; + for (const group of state.taskGroups) { + // Find the task or its subtask + const task = + group.tasks.find(task => task.id === label.id) || + group.tasks + .flatMap(task => task.sub_tasks || []) + .find(subtask => subtask.id === label.id); + if (task) { + task.labels = label.labels || []; + task.all_labels = label.all_labels || []; + // Update cache + state.taskCache[label.id] = task; + break; + } + } + }, + + // Enhanced Kanban progress update (for use in task drawer and socket events) + updateEnhancedKanbanTaskProgress: ( + state, + action: PayloadAction<{ + id: string; + complete_ratio: number; + completed_count: number; + total_tasks_count: number; + parent_task: string; + }> + ) => { + const { id, complete_ratio, completed_count, total_tasks_count, parent_task } = action.payload; + + // Find the task in any group + const taskInfo = findTaskInAllGroups(state.taskGroups, parent_task || id); + + // Check if taskInfo exists before destructuring + if (!taskInfo) return; + + const { task } = taskInfo; + + // Update the task properties + task.complete_ratio = +complete_ratio; + task.completed_count = completed_count; + task.total_tasks_count = total_tasks_count; + + // Update cache + state.taskCache[parent_task || id] = task; + }, + updateTaskPriority: (state, action: PayloadAction) => { const { id: task_id, priority_id } = action.payload; @@ -608,6 +771,9 @@ const enhancedKanbanSlice = createSlice({ const group = state.taskGroups.find(g => g.id === sectionId); if (group) { group.tasks.push(task); + // Update cache + state.taskCache[task.id!] = task; + state.groupCache[sectionId] = group; } }, }, @@ -737,6 +903,10 @@ export const { reorderGroups, addTaskToGroup, updateEnhancedKanbanTaskStatus, + updateEnhancedKanbanTaskPriority, + updateEnhancedKanbanTaskAssignees, + updateEnhancedKanbanTaskLabels, + updateEnhancedKanbanTaskProgress, } = enhancedKanbanSlice.actions; export default enhancedKanbanSlice.reducer; \ No newline at end of file