From a4237a6f1715cd5ae05f516dd5c5ac917e911d31 Mon Sep 17 00:00:00 2001 From: shancds Date: Wed, 18 Jun 2025 09:45:37 +0530 Subject: [PATCH 1/5] refactor(project-view-board): update collision detection strategy for drag-and-drop - Replaced `closestCorners` with `collisionDetectionStrategy` to enhance drag-and-drop functionality. - Aims to improve performance and accuracy during task movement on the project board. --- .../src/pages/projects/projectView/board/project-view-board.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx b/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx index 3f886223..b9fa1edb 100644 --- a/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx @@ -541,7 +541,7 @@ const ProjectViewBoard = () => { Date: Wed, 18 Jun 2025 12:18:58 +0530 Subject: [PATCH 2/5] feat(project-view-board): implement task priority change handling - Added a new function to handle task priority changes via socket events. - Integrated priority change logic into the drag-and-drop functionality for improved task management. - Cleaned up unused imports and improved code organization for better readability. --- .../projectView/board/project-view-board.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx b/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx index b9fa1edb..a110732b 100644 --- a/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; +import { useEffect, useState, useRef, useCallback } from 'react'; import { useAppSelector } from '@/hooks/useAppSelector'; import TaskListFilters from '../taskList/task-list-filters/task-list-filters'; import { Flex, Skeleton } from 'antd'; @@ -22,14 +22,10 @@ import { TouchSensor, useSensor, useSensors, - MeasuringStrategy, getFirstCollision, pointerWithin, rectIntersection, UniqueIdentifier, - DragOverlayProps, - DragOverlay as DragOverlayType, - closestCorners, } from '@dnd-kit/core'; import BoardViewTaskCard from './board-section/board-task-card/board-view-task-card'; import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice'; @@ -45,7 +41,8 @@ import { statusApiService } from '@/api/taskAttributes/status/status.api.service import logger from '@/utils/errorLogger'; import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'; import { debounce } from 'lodash'; - +import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types'; +import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice'; interface DroppableContainer { id: UniqueIdentifier; data: { @@ -272,6 +269,21 @@ const ProjectViewBoard = () => { } }; + const handlePriorityChange = (taskId: string, priorityId: string) => { + if (!taskId || !priorityId || !socket) return; + + const payload = { + task_id: taskId, + priority_id: priorityId, + team_id: currentSession?.team_id, + }; + + socket.emit(SocketEvents.TASK_PRIORITY_CHANGE.toString(), JSON.stringify(payload)); + socket.once(SocketEvents.TASK_PRIORITY_CHANGE.toString(), (data: ITaskListPriorityChangeResponse) => { + dispatch(updateBoardTaskPriority(data)); + }); + }; + const handleDragEnd = async (event: DragEndEvent) => { isDraggingRef.current = false; const { active, over } = event; @@ -316,6 +328,7 @@ const ProjectViewBoard = () => { originalSourceGroupIdRef.current = null; // Reset the ref return; } + if (targetGroupId !== sourceGroupId) { const canContinue = await checkTaskDependencyStatus(task.id, targetGroupId); if (!canContinue) { @@ -375,8 +388,6 @@ const ProjectViewBoard = () => { team_id: currentSession?.team_id }; - // logger.error('Emitting socket event with payload (task not found in source):', body); - // Emit socket event if (socket) { socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body); @@ -389,6 +400,11 @@ const ProjectViewBoard = () => { socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id); } }); + + // Handle priority change if groupBy is priority + if (groupBy === IGroupBy.PRIORITY) { + handlePriorityChange(task.id, targetGroupId); + } } // Track analytics event From 39e8add1033ad8839c41e157627790c2cbee50bc Mon Sep 17 00:00:00 2001 From: shancds Date: Wed, 18 Jun 2025 12:56:24 +0530 Subject: [PATCH 3/5] feat(filters): enhance labels and members filter dropdowns - Added useEffect to fetch labels when the component mounts or projectId changes in LabelsFilterDropdown. - Improved members filter logic to only sync board members when the board task assignees are empty. - Cleaned up redundant checks and optimized dependencies in the members filter dropdown. --- .../filter-dropdowns/labels-filter-dropdown.tsx | 12 ++++++++---- .../filter-dropdowns/members-filter-dropdown.tsx | 6 +++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/labels-filter-dropdown.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/labels-filter-dropdown.tsx index 4c2744cc..dc63e9be 100644 --- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/labels-filter-dropdown.tsx +++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/labels-filter-dropdown.tsx @@ -11,7 +11,7 @@ import List from 'antd/es/list'; import Space from 'antd/es/space'; import { useSearchParams } from 'react-router-dom'; -import { useMemo, useRef, useState } from 'react'; +import { useMemo, useRef, useState, useEffect } from 'react'; import { useAppSelector } from '@/hooks/useAppSelector'; import { colors } from '@/styles/colors'; import { useTranslation } from 'react-i18next'; @@ -36,6 +36,13 @@ const LabelsFilterDropdown = () => { const tab = searchParams.get('tab'); const projectView = tab === 'tasks-list' ? 'list' : 'kanban'; + // Fetch labels when component mounts or projectId changes + useEffect(() => { + if (projectId) { + dispatch(fetchLabelsByProject(projectId)); + } + }, [dispatch, projectId]); + const filteredLabelData = useMemo(() => { if (projectView === 'list') { return labels.filter(label => label.name?.toLowerCase().includes(searchQuery.toLowerCase())); @@ -81,9 +88,6 @@ const LabelsFilterDropdown = () => { setTimeout(() => { labelInputRef.current?.focus(); }, 0); - if (projectView === 'kanban') { - dispatch(setBoardLabels(labels)); - } } }; diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx index c9b2d307..85f3a2df 100644 --- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx +++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx @@ -76,7 +76,6 @@ const MembersFilterDropdown = () => { const handleSelectedFiltersCount = useCallback(async (memberId: string | undefined, checked: boolean) => { if (!memberId || !projectId) return; - if (!memberId || !projectId) return; const updateMembers = async (members: Member[], setAction: any, fetchAction: any) => { const updatedMembers = members.map(member => @@ -142,11 +141,12 @@ const MembersFilterDropdown = () => { const handleMembersDropdownOpen = useCallback((open: boolean) => { if (open) { setTimeout(() => membersInputRef.current?.focus(), 0); - if (taskAssignees.length) { + // Only sync the members if board members are empty + if (projectView === 'kanban' && boardTaskAssignees.length === 0 && taskAssignees.length > 0) { dispatch(setBoardMembers(taskAssignees)); } } - }, [dispatch, taskAssignees]); + }, [dispatch, taskAssignees, boardTaskAssignees, projectView]); const buttonStyle = { backgroundColor: selectedCount > 0 From 193288013e4632e1ad67b7e0d25944e8522e278a Mon Sep 17 00:00:00 2001 From: shancds Date: Wed, 18 Jun 2025 15:31:00 +0530 Subject: [PATCH 4/5] refactor(priority-section): remove task name display from PrioritySection - Eliminated the task name display from the PrioritySection component for a cleaner layout. - Updated BoardViewTaskCard to include task name alongside the PrioritySection for improved organization. --- .../taskCard/priority-section/priority-section.tsx | 6 ------ .../board-task-card/board-view-task-card.tsx | 12 ++++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx b/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx index 3deceb7a..bf12b447 100644 --- a/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx +++ b/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx @@ -53,12 +53,6 @@ const PrioritySection = ({ task }: PrioritySectionProps) => { return ( {priorityIcon} - - {task.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 8faa1fe5..d2022b40 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 @@ -287,9 +287,17 @@ const BoardViewTaskCard = ({ task, sectionId }: IBoardViewTaskCardProps) => { = 100 ? 9 : 7} /> + + {/* Action Icons */} + + + {task.name} + + - {/* Action Icons */} - Date: Wed, 18 Jun 2025 17:11:39 +0530 Subject: [PATCH 5/5] 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 && ( + + )} + + + + )} - + + ); };