From 8fcd4d0d53f320ffcfeac51c054b326e65c64b20 Mon Sep 17 00:00:00 2001 From: shancds Date: Tue, 1 Jul 2025 09:42:47 +0530 Subject: [PATCH 1/3] 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 From c048085c8a4993b68f7caf3de49286b07c3b29df Mon Sep 17 00:00:00 2001 From: shancds Date: Tue, 1 Jul 2025 09:58:49 +0530 Subject: [PATCH 2/3] feat(enhanced-kanban): enhance task progress and name updates in task drawer - Integrated updates for task progress and name within the enhanced Kanban feature, allowing for real-time synchronization based on the selected tab (tasks-list or board). - Added new actions to handle task progress updates specifically for the enhanced Kanban view. - Updated task drawer components to dispatch the appropriate actions based on the current tab, improving user experience and state management. --- .../task-drawer-progress.tsx | 32 +++++++++++++------ .../task-drawer-header/task-drawer-header.tsx | 14 ++++++-- .../enhanced-kanban/enhanced-kanban.slice.ts | 19 +++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx index df1ce2ea..f6b025d4 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-progress/task-drawer-progress.tsx @@ -14,6 +14,7 @@ import { setTaskStatus } from '@/features/task-drawer/task-drawer.slice'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { updateBoardTaskStatus } from '@/features/board/board-slice'; import { updateTaskProgress, updateTaskStatus } from '@/features/tasks/tasks.slice'; +import { updateEnhancedKanbanTaskStatus, updateEnhancedKanbanTaskProgress } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import useTabSearchParam from '@/hooks/useTabSearchParam'; interface TaskDrawerProgressProps { @@ -102,14 +103,27 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => { ); socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: any) => { - dispatch( - updateTaskProgress({ - taskId: task.id, - progress: data.complete_ratio, - totalTasksCount: data.total_tasks_count, - completedCount: data.completed_count, - }) - ); + if (tab === 'tasks-list') { + dispatch( + updateTaskProgress({ + taskId: task.id, + progress: data.complete_ratio, + totalTasksCount: data.total_tasks_count, + completedCount: data.completed_count, + }) + ); + } + if (tab === 'board') { + dispatch( + updateEnhancedKanbanTaskProgress({ + id: task.id, + complete_ratio: data.complete_ratio, + completed_count: data.completed_count, + total_tasks_count: data.total_tasks_count, + parent_task: task.parent_task_id || null, + }) + ); + } }); if (task.id) { @@ -185,7 +199,7 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => { dispatch(updateTaskStatus(data)); } if (tab === 'board') { - dispatch(updateBoardTaskStatus(data)); + dispatch(updateEnhancedKanbanTaskStatus(data)); } if (data.parent_task) socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), data.parent_task); diff --git a/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx b/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx index 0bc322f3..15cd2cca 100644 --- a/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx +++ b/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx @@ -16,6 +16,9 @@ import { SocketEvents } from '@/shared/socket-events'; import useTaskDrawerUrlSync from '@/hooks/useTaskDrawerUrlSync'; import { deleteTask } from '@/features/tasks/tasks.slice'; import { deleteBoardTask, updateTaskName } from '@/features/board/board-slice'; +import { updateEnhancedKanbanTaskName } from '@/features/enhanced-kanban/enhanced-kanban.slice'; +import useTabSearchParam from '@/hooks/useTabSearchParam'; +import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; type TaskDrawerHeaderProps = { inputRef: React.RefObject; @@ -26,6 +29,7 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => { const dispatch = useAppDispatch(); const { socket, connected } = useSocket(); const { clearTaskFromUrl } = useTaskDrawerUrlSync(); + const { tab } = useTabSearchParam(); const isDeleting = useRef(false); const [isEditing, setIsEditing] = useState(false); @@ -84,7 +88,13 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => { const handleReceivedTaskNameChange = (data: { id: string; parent_task: string; name: string }) => { if (data.id === selectedTaskId) { - dispatch(updateTaskName({ task: data })); + const taskData = { ...data, manual_progress: false } as IProjectTask; + dispatch(updateTaskName({ task: taskData })); + + // Also update enhanced kanban if on board tab + if (tab === 'board') { + dispatch(updateEnhancedKanbanTaskName({ task: taskData })); + } } }; @@ -152,7 +162,7 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => { 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 90303e71..f4974f0f 100644 --- a/worklenz-frontend/src/features/enhanced-kanban/enhanced-kanban.slice.ts +++ b/worklenz-frontend/src/features/enhanced-kanban/enhanced-kanban.slice.ts @@ -694,6 +694,24 @@ const enhancedKanbanSlice = createSlice({ state.taskCache[parent_task || id] = task; }, + // Enhanced Kanban task name update (for use in task drawer header) + updateEnhancedKanbanTaskName: ( + state, + action: PayloadAction<{ + task: IProjectTask; + }> + ) => { + const { task } = action.payload; + + // Find the task and update it + const result = findTaskInAllGroups(state.taskGroups, task.id || ''); + if (result) { + result.task.name = task.name; + // Update cache + state.taskCache[task.id!] = result.task; + } + }, + updateTaskPriority: (state, action: PayloadAction) => { const { id: task_id, priority_id } = action.payload; @@ -907,6 +925,7 @@ export const { updateEnhancedKanbanTaskAssignees, updateEnhancedKanbanTaskLabels, updateEnhancedKanbanTaskProgress, + updateEnhancedKanbanTaskName, } = enhancedKanbanSlice.actions; export default enhancedKanbanSlice.reducer; \ No newline at end of file From e5ff036d811755f90ef6de9bae670ce09655566d Mon Sep 17 00:00:00 2001 From: shancds Date: Tue, 1 Jul 2025 10:18:56 +0530 Subject: [PATCH 3/3] feat(task-status): enhance task status change handling to reset manual progress - Added logic to reset manual_progress to FALSE when a task transitions from "done" to "todo" or "doing", allowing for accurate progress recalculation based on subtasks. - Improved logging for task status changes to provide better insights into task management actions. - Ensured parent task progress updates are triggered appropriately for subtasks during status changes. --- .../commands/on-task-status-change.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/worklenz-backend/src/socket.io/commands/on-task-status-change.ts b/worklenz-backend/src/socket.io/commands/on-task-status-change.ts index 0d003b59..e59e6b59 100644 --- a/worklenz-backend/src/socket.io/commands/on-task-status-change.ts +++ b/worklenz-backend/src/socket.io/commands/on-task-status-change.ts @@ -58,10 +58,10 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?: FROM tasks WHERE id = $1 `, [body.task_id]); - + const currentProgress = progressResult.rows[0]?.progress_value; const isManualProgress = progressResult.rows[0]?.manual_progress; - + // Only update if not already 100% if (currentProgress !== 100) { // Update progress to 100% @@ -70,9 +70,9 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?: SET progress_value = 100, manual_progress = TRUE WHERE id = $1 `, [body.task_id]); - + log(`Task ${body.task_id} moved to done status - progress automatically set to 100%`, null); - + // Log the progress change to activity logs await logProgressChange({ task_id: body.task_id, @@ -80,7 +80,7 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?: new_value: "100", socket }); - + // If this is a subtask, update parent task progress if (body.parent_task) { setTimeout(() => { @@ -88,6 +88,23 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?: }, 100); } } + } else { + // Task is moving from "done" to "todo" or "doing" - reset manual_progress to FALSE + // so progress can be recalculated based on subtasks + await db.query(` + UPDATE tasks + SET manual_progress = FALSE + WHERE id = $1 + `, [body.task_id]); + + log(`Task ${body.task_id} moved from done status - manual_progress reset to FALSE`, null); + + // If this is a subtask, update parent task progress + if (body.parent_task) { + setTimeout(() => { + socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), body.parent_task); + }, 100); + } } const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);