From 4c4a860c7604c21abe272e4bd7410d95cce0f917 Mon Sep 17 00:00:00 2001 From: shancds Date: Wed, 18 Jun 2025 17:11:39 +0530 Subject: [PATCH] feat(board): enhance task and subtask management in board components - Updated boardSlice to allow updating task assignees and names for both main tasks and subtasks. - Improved BoardSubTaskCard to include context menu options for assigning tasks, deleting subtasks, and handling errors. - Refactored BoardViewTaskCard to integrate dropdown menus for better task interaction and organization. - Enhanced user experience by adding loading states and error handling for task actions. --- .../src/features/board/board-slice.ts | 22 ++- .../board-sub-task-card.tsx | 175 +++++++++++++++--- .../board-task-card/board-view-task-card.tsx | 166 +++++++++-------- 3 files changed, 249 insertions(+), 114 deletions(-) diff --git a/worklenz-frontend/src/features/board/board-slice.ts b/worklenz-frontend/src/features/board/board-slice.ts index a25262dc..f62ecdab 100644 --- a/worklenz-frontend/src/features/board/board-slice.ts +++ b/worklenz-frontend/src/features/board/board-slice.ts @@ -459,10 +459,24 @@ const boardSlice = createSlice({ const { body, sectionId, taskId } = action.payload; const section = state.taskGroups.find(sec => sec.id === sectionId); if (section) { - const task = section.tasks.find(task => task.id === taskId); - if (task) { - task.assignees = body.assignees; - task.names = body.names; + // First try to find the task in main tasks + const mainTask = section.tasks.find(task => task.id === taskId); + if (mainTask) { + mainTask.assignees = body.assignees; + mainTask.names = body.names; + return; + } + + // If not found in main tasks, look in subtasks + for (const parentTask of section.tasks) { + if (!parentTask.sub_tasks) continue; + + const subtask = parentTask.sub_tasks.find(st => st.id === taskId); + if (subtask) { + subtask.assignees = body.assignees; + subtask.names = body.names; + return; + } } } }, diff --git a/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card.tsx b/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card.tsx index 1c5549c5..7c669fae 100644 --- a/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card.tsx @@ -1,11 +1,25 @@ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import dayjs, { Dayjs } from 'dayjs'; -import { Col, Flex, Typography, List } from 'antd'; +import { Col, Flex, Typography, List, Dropdown, MenuProps, Popconfirm } from 'antd'; +import { UserAddOutlined, DeleteOutlined, ExclamationCircleFilled, InboxOutlined } from '@ant-design/icons'; import CustomAvatarGroup from '@/components/board/custom-avatar-group'; import CustomDueDatePicker from '@/components/board/custom-due-date-picker'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice'; +import { useTranslation } from 'react-i18next'; +import { colors } from '@/styles/colors'; +import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service'; +import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; +import { + evt_project_task_list_context_menu_assign_me, + evt_project_task_list_context_menu_delete, + evt_project_task_list_context_menu_archive, +} from '@/shared/worklenz-analytics-events'; +import logger from '@/utils/errorLogger'; +import { useAppSelector } from '@/hooks/useAppSelector'; +import { deleteBoardTask, updateBoardTaskAssignee } from '@features/board/board-slice'; +import { IBulkAssignRequest } from '@/types/tasks/bulk-action-bar.types'; interface IBoardSubTaskCardProps { subtask: IProjectTask; @@ -14,48 +28,153 @@ interface IBoardSubTaskCardProps { const BoardSubTaskCard = ({ subtask, sectionId }: IBoardSubTaskCardProps) => { const dispatch = useAppDispatch(); + const { t } = useTranslation('kanban-board'); + const { trackMixpanelEvent } = useMixpanelTracking(); + const projectId = useAppSelector(state => state.projectReducer.projectId); + const [updatingAssignToMe, setUpdatingAssignToMe] = useState(false); const [subtaskDueDate, setSubtaskDueDate] = useState( subtask?.end_date ? dayjs(subtask?.end_date) : null ); const handleCardClick = (e: React.MouseEvent, id: string) => { - // Prevent the event from propagating to parent elements e.stopPropagation(); - - // Add a small delay to ensure it's a click and not the start of a drag const clickTimeout = setTimeout(() => { dispatch(setSelectedTaskId(id)); dispatch(setShowTaskDrawer(true)); }, 50); - return () => clearTimeout(clickTimeout); }; - return ( - handleCardClick(e, subtask.id || '')} - > - - { + if (!projectId || !subtask.id || updatingAssignToMe) return; + + try { + setUpdatingAssignToMe(true); + const body: IBulkAssignRequest = { + tasks: [subtask.id], + project_id: projectId, + }; + const res = await taskListBulkActionsApiService.assignToMe(body); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_context_menu_assign_me); + dispatch( + updateBoardTaskAssignee({ + body: res.body, + sectionId, + taskId: subtask.id, + }) + ); + } + } catch (error) { + logger.error('Error assigning task to me:', error); + } finally { + setUpdatingAssignToMe(false); + } + }, [projectId, subtask.id, updatingAssignToMe, dispatch, trackMixpanelEvent, sectionId]); + + // const handleArchive = async () => { + // if (!projectId || !subtask.id) return; + + // try { + // const res = await taskListBulkActionsApiService.archiveTasks( + // { + // tasks: [subtask.id], + // project_id: projectId, + // }, + // false + // ); + + // if (res.done) { + // trackMixpanelEvent(evt_project_task_list_context_menu_archive); + // dispatch(deleteBoardTask({ sectionId, taskId: subtask.id })); + // } + // } catch (error) { + // logger.error('Error archiving subtask:', error); + // } + // }; + + const handleDelete = async () => { + if (!projectId || !subtask.id) return; + + try { + const res = await taskListBulkActionsApiService.deleteTasks({ tasks: [subtask.id] }, projectId); + if (res.done) { + trackMixpanelEvent(evt_project_task_list_context_menu_delete); + dispatch(deleteBoardTask({ sectionId, taskId: subtask.id })); + } + } catch (error) { + logger.error('Error deleting subtask:', error); + } + }; + + const items: MenuProps['items'] = [ + { + label: ( + + +   + {t('assignToMe')} + + ), + key: '1', + onClick: () => handleAssignToMe(), + disabled: updatingAssignToMe, + }, + // { + // label: ( + // + // + //   + // {t('archive')} + // + // ), + // key: '2', + // onClick: () => handleArchive(), + // }, + { + label: ( + } + okText={t('deleteConfirmationOk')} + cancelText={t('deleteConfirmationCancel')} + onConfirm={() => handleDelete()} > - {subtask.name} - - + +   + {t('delete')} + + ), + key: '3', + }, + ]; - - + return ( + + handleCardClick(e, subtask.id || '')} + > + + + {subtask.name} + + - - - + + + + + + ); }; diff --git a/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-task-card/board-view-task-card.tsx b/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-task-card/board-view-task-card.tsx index d2022b40..b256fbfa 100644 --- a/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-task-card/board-view-task-card.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/board/board-section/board-task-card/board-view-task-card.tsx @@ -256,50 +256,49 @@ const BoardViewTaskCard = ({ task, sectionId }: IBoardViewTaskCardProps) => { }, [task.labels, themeMode]); return ( - - handleCardClick(e, task.id || '')} - data-id={task.id} - data-dragging={isDragging ? "true" : "false"} - > - {/* Labels and Progress */} - - - {renderLabels} + + + {/* Task Card */} + handleCardClick(e, task.id || '')}> + {/* Labels and Progress */} + + + {renderLabels} + + + + = 100 ? 9 : 7} /> + + + + {/* Action Icons */} + + + {task.name} + - - - = 100 ? 9 : 7} /> - - - - {/* Action Icons */} - - - {task.name} - - - - - { - - {isSubTaskShow && ( - - - - {task.sub_tasks_loading && ( - - - - )} - - {!task.sub_tasks_loading && task?.sub_tasks && - task?.sub_tasks.map((subtask: any) => ( - - ))} - - {showNewSubtaskCard && ( - - )} - - - - )} + + {/* Subtask Section */} + + {isSubTaskShow && ( + + + + {task.sub_tasks_loading && ( + + + + )} + + {!task.sub_tasks_loading && task?.sub_tasks && + task?.sub_tasks.map((subtask: any) => ( + + ))} + + {showNewSubtaskCard && ( + + )} + + + + )} - + + ); };