From f9858fbd4bcbe839aa257ba9232be7f0e1b02cce Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Sun, 18 May 2025 20:58:20 +0530 Subject: [PATCH] refactor(task-list): enhance performance with useMemo and useCallback - Introduced useMemo to optimize loading state and empty state calculations. - Added useMemo for socket event handler functions to prevent unnecessary re-renders. - Refactored data fetching logic to improve initial data load handling. - Improved drag-and-drop functionality with memoized handlers for better performance. --- .../taskList/project-view-task-list.tsx | 84 ++-- .../task-group-wrapper/task-group-wrapper.tsx | 395 +++++++++--------- 2 files changed, 250 insertions(+), 229 deletions(-) diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx index fcd4931a..410644fb 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import Flex from 'antd/es/flex'; import Skeleton from 'antd/es/skeleton'; import { useSearchParams } from 'react-router-dom'; @@ -17,8 +17,8 @@ const ProjectViewTaskList = () => { const dispatch = useAppDispatch(); const { projectView } = useTabSearchParam(); const [searchParams, setSearchParams] = useSearchParams(); - // Add local loading state to immediately show skeleton const [isLoading, setIsLoading] = useState(true); + const [initialLoadComplete, setInitialLoadComplete] = useState(false); const { projectId } = useAppSelector(state => state.projectReducer); const { taskGroups, loadingGroups, groupBy, archived, fields, search } = useAppSelector( @@ -30,47 +30,73 @@ const ProjectViewTaskList = () => { const { loadingPhases } = useAppSelector(state => state.phaseReducer); const { loadingColumns } = useAppSelector(state => state.taskReducer); + // Memoize the loading state calculation - ignoring task list filter loading + const isLoadingState = useMemo(() => + loadingGroups || loadingPhases || loadingStatusCategories, + [loadingGroups, loadingPhases, loadingStatusCategories] + ); + + // Memoize the empty state check + const isEmptyState = useMemo(() => + taskGroups && taskGroups.length === 0 && !isLoadingState, + [taskGroups, isLoadingState] + ); + + // Handle view type changes useEffect(() => { - // Set default view to list if projectView is not list or board if (projectView !== 'list' && projectView !== 'board') { - searchParams.set('tab', 'tasks-list'); - searchParams.set('pinned_tab', 'tasks-list'); - setSearchParams(searchParams); + const newParams = new URLSearchParams(searchParams); + newParams.set('tab', 'tasks-list'); + newParams.set('pinned_tab', 'tasks-list'); + setSearchParams(newParams); } - }, [projectView, searchParams, setSearchParams]); + }, [projectView, setSearchParams]); + // Update loading state useEffect(() => { - // Set loading state based on all loading conditions - setIsLoading(loadingGroups || loadingColumns || loadingPhases || loadingStatusCategories); - }, [loadingGroups, loadingColumns, loadingPhases, loadingStatusCategories]); + setIsLoading(isLoadingState); + }, [isLoadingState]); + // Fetch initial data only once useEffect(() => { - const loadData = async () => { - if (projectId && groupBy) { - const promises = []; - - if (!loadingColumns) promises.push(dispatch(fetchTaskListColumns(projectId))); - if (!loadingPhases) promises.push(dispatch(fetchPhasesByProjectId(projectId))); - if (!loadingGroups && projectView === 'list') { - promises.push(dispatch(fetchTaskGroups(projectId))); - } - if (!statusCategories.length) { - promises.push(dispatch(fetchStatusesCategories())); - } - - // Wait for all data to load - await Promise.all(promises); + const fetchInitialData = async () => { + if (!projectId || !groupBy || initialLoadComplete) return; + + try { + await Promise.all([ + dispatch(fetchTaskListColumns(projectId)), + dispatch(fetchPhasesByProjectId(projectId)), + dispatch(fetchStatusesCategories()) + ]); + setInitialLoadComplete(true); + } catch (error) { + console.error('Error fetching initial data:', error); } }; - - loadData(); - }, [dispatch, projectId, groupBy, fields, search, archived]); + + fetchInitialData(); + }, [projectId, groupBy, dispatch, initialLoadComplete]); + + // Fetch task groups + useEffect(() => { + const fetchTasks = async () => { + if (!projectId || !groupBy || projectView !== 'list' || !initialLoadComplete) return; + + try { + await dispatch(fetchTaskGroups(projectId)); + } catch (error) { + console.error('Error fetching task groups:', error); + } + }; + + fetchTasks(); + }, [projectId, groupBy, projectView, dispatch, fields, search, archived, initialLoadComplete]); return ( - {(taskGroups && taskGroups.length === 0 && !isLoading) ? ( + {isEmptyState ? ( ) : ( diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-group-wrapper/task-group-wrapper.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-group-wrapper/task-group-wrapper.tsx index f619f20a..6a0e9374 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-group-wrapper/task-group-wrapper.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-group-wrapper/task-group-wrapper.tsx @@ -2,7 +2,7 @@ import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useSocket } from '@/socket/socketContext'; import { useAuthService } from '@/hooks/useAuth'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import Flex from 'antd/es/flex'; import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; @@ -87,16 +87,22 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { const loadingAssignees = useAppSelector(state => state.taskReducer.loadingAssignees); const { projectId } = useAppSelector(state => state.projectReducer); - const sensors = useSensors( - useSensor(PointerSensor, { + // Move useSensors to top level and memoize its configuration + const sensorConfig = useMemo( + () => ({ activationConstraint: { distance: 8 }, - }) + }), + [] ); + const pointerSensor = useSensor(PointerSensor, sensorConfig); + const sensors = useSensors(pointerSensor); + useEffect(() => { setGroups(taskGroups); }, [taskGroups]); + // Memoize resetTaskRowStyles to prevent unnecessary re-renders const resetTaskRowStyles = useCallback(() => { document.querySelectorAll('.task-row').forEach(row => { row.style.transition = 'transform 0.2s ease, opacity 0.2s ease'; @@ -106,21 +112,18 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { }); }, []); - // Socket handler for assignee updates - useEffect(() => { - if (!socket) return; - - const handleAssigneesUpdate = (data: ITaskAssigneesUpdateResponse) => { + // Memoize socket event handlers + const handleAssigneesUpdate = useCallback( + (data: ITaskAssigneesUpdateResponse) => { if (!data) return; - const updatedAssignees = data.assignees.map(assignee => ({ + const updatedAssignees = data.assignees?.map(assignee => ({ ...assignee, selected: true, - })); + })) || []; - // Find the group that contains the task or its subtasks - const groupId = groups.find(group => - group.tasks.some( + const groupId = groups?.find(group => + group.tasks?.some( task => task.id === data.id || (task.sub_tasks && task.sub_tasks.some(subtask => subtask.id === data.id)) @@ -136,47 +139,41 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { }) ); - dispatch(setTaskAssignee(data)); + dispatch( + setTaskAssignee({ + ...data, + manual_progress: false, + } as IProjectTask) + ); if (currentSession?.team_id && !loadingAssignees) { dispatch(fetchTaskAssignees(currentSession.team_id)); } } - }; + }, + [groups, dispatch, currentSession?.team_id, loadingAssignees] + ); - socket.on(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), handleAssigneesUpdate); - return () => { - socket.off(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), handleAssigneesUpdate); - }; - }, [socket, currentSession?.team_id, loadingAssignees, groups, dispatch]); - - // Socket handler for label updates - useEffect(() => { - if (!socket) return; - - const handleLabelsChange = async (labels: ILabelsChangeResponse) => { + // Memoize socket event handlers + const handleLabelsChange = useCallback( + async (labels: ILabelsChangeResponse) => { + if (!labels) return; + await Promise.all([ dispatch(updateTaskLabel(labels)), dispatch(setTaskLabels(labels)), dispatch(fetchLabels()), projectId && dispatch(fetchLabelsByProject(projectId)), ]); - }; + }, + [dispatch, projectId] + ); - socket.on(SocketEvents.TASK_LABELS_CHANGE.toString(), handleLabelsChange); - socket.on(SocketEvents.CREATE_LABEL.toString(), handleLabelsChange); + // Memoize socket event handlers + const handleTaskStatusChange = useCallback( + (response: ITaskListStatusChangeResponse) => { + if (!response) return; - return () => { - socket.off(SocketEvents.TASK_LABELS_CHANGE.toString(), handleLabelsChange); - socket.off(SocketEvents.CREATE_LABEL.toString(), handleLabelsChange); - }; - }, [socket, dispatch, projectId]); - - // Socket handler for status updates - useEffect(() => { - if (!socket) return; - - const handleTaskStatusChange = (response: ITaskListStatusChangeResponse) => { if (response.completed_deps === false) { alertService.error( 'Task is not completed', @@ -186,11 +183,14 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { } dispatch(updateTaskStatus(response)); - // dispatch(setTaskStatus(response)); dispatch(deselectAll()); - }; + }, + [dispatch] + ); - const handleTaskProgress = (data: { + // Memoize socket event handlers + const handleTaskProgress = useCallback( + (data: { id: string; status: string; complete_ratio: number; @@ -198,6 +198,8 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { total_tasks_count: number; parent_task: string; }) => { + if (!data) return; + dispatch( updateTaskProgress({ taskId: data.parent_task || data.id, @@ -206,190 +208,150 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { completedCount: data.completed_count, }) ); - }; + }, + [dispatch] + ); - socket.on(SocketEvents.TASK_STATUS_CHANGE.toString(), handleTaskStatusChange); - socket.on(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress); + // Memoize socket event handlers + const handlePriorityChange = useCallback( + (response: ITaskListPriorityChangeResponse) => { + if (!response) return; - return () => { - socket.off(SocketEvents.TASK_STATUS_CHANGE.toString(), handleTaskStatusChange); - socket.off(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress); - }; - }, [socket, dispatch]); - - // Socket handler for priority updates - useEffect(() => { - if (!socket) return; - - const handlePriorityChange = (response: ITaskListPriorityChangeResponse) => { dispatch(updateTaskPriority(response)); dispatch(setTaskPriority(response)); dispatch(deselectAll()); - }; + }, + [dispatch] + ); - socket.on(SocketEvents.TASK_PRIORITY_CHANGE.toString(), handlePriorityChange); - - return () => { - socket.off(SocketEvents.TASK_PRIORITY_CHANGE.toString(), handlePriorityChange); - }; - }, [socket, dispatch]); - - // Socket handler for due date updates - useEffect(() => { - if (!socket) return; - - const handleEndDateChange = (task: { + // Memoize socket event handlers + const handleEndDateChange = useCallback( + (task: { id: string; parent_task: string | null; end_date: string; }) => { - dispatch(updateTaskEndDate({ task })); - dispatch(setTaskEndDate(task)); - }; + if (!task) return; - socket.on(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleEndDateChange); + const taskWithProgress = { + ...task, + manual_progress: false, + } as IProjectTask; - return () => { - socket.off(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleEndDateChange); - }; - }, [socket, dispatch]); + dispatch(updateTaskEndDate({ task: taskWithProgress })); + dispatch(setTaskEndDate(taskWithProgress)); + }, + [dispatch] + ); - // Socket handler for task name updates - useEffect(() => { - if (!socket) return; + // Memoize socket event handlers + const handleTaskNameChange = useCallback( + (data: { id: string; parent_task: string; name: string }) => { + if (!data) return; - const handleTaskNameChange = (data: { id: string; parent_task: string; name: string }) => { dispatch(updateTaskName(data)); - }; + }, + [dispatch] + ); - socket.on(SocketEvents.TASK_NAME_CHANGE.toString(), handleTaskNameChange); + // Memoize socket event handlers + const handlePhaseChange = useCallback( + (data: ITaskPhaseChangeResponse) => { + if (!data) return; - return () => { - socket.off(SocketEvents.TASK_NAME_CHANGE.toString(), handleTaskNameChange); - }; - }, [socket, dispatch]); - - // Socket handler for phase updates - useEffect(() => { - if (!socket) return; - - const handlePhaseChange = (data: ITaskPhaseChangeResponse) => { dispatch(updateTaskPhase(data)); dispatch(deselectAll()); - }; + }, + [dispatch] + ); - socket.on(SocketEvents.TASK_PHASE_CHANGE.toString(), handlePhaseChange); - - return () => { - socket.off(SocketEvents.TASK_PHASE_CHANGE.toString(), handlePhaseChange); - }; - }, [socket, dispatch]); - - // Socket handler for start date updates - useEffect(() => { - if (!socket) return; - - const handleStartDateChange = (task: { + // Memoize socket event handlers + const handleStartDateChange = useCallback( + (task: { id: string; parent_task: string | null; start_date: string; }) => { - dispatch(updateTaskStartDate({ task })); - dispatch(setStartDate(task)); - }; + if (!task) return; - socket.on(SocketEvents.TASK_START_DATE_CHANGE.toString(), handleStartDateChange); + const taskWithProgress = { + ...task, + manual_progress: false, + } as IProjectTask; - return () => { - socket.off(SocketEvents.TASK_START_DATE_CHANGE.toString(), handleStartDateChange); - }; - }, [socket, dispatch]); + dispatch(updateTaskStartDate({ task: taskWithProgress })); + dispatch(setStartDate(taskWithProgress)); + }, + [dispatch] + ); - // Socket handler for task subscribers updates - useEffect(() => { - if (!socket) return; + // Memoize socket event handlers + const handleTaskSubscribersChange = useCallback( + (data: InlineMember[]) => { + if (!data) return; - const handleTaskSubscribersChange = (data: InlineMember[]) => { dispatch(setTaskSubscribers(data)); - }; + }, + [dispatch] + ); - socket.on(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), handleTaskSubscribersChange); - - return () => { - socket.off(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), handleTaskSubscribersChange); - }; - }, [socket, dispatch]); - - // Socket handler for task estimation updates - useEffect(() => { - if (!socket) return; - - const handleEstimationChange = (task: { + // Memoize socket event handlers + const handleEstimationChange = useCallback( + (task: { id: string; parent_task: string | null; estimation: number; }) => { - dispatch(updateTaskEstimation({ task })); - }; + if (!task) return; - socket.on(SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), handleEstimationChange); + const taskWithProgress = { + ...task, + manual_progress: false, + } as IProjectTask; - return () => { - socket.off(SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), handleEstimationChange); - }; - }, [socket, dispatch]); + dispatch(updateTaskEstimation({ task: taskWithProgress })); + }, + [dispatch] + ); - // Socket handler for task description updates - useEffect(() => { - if (!socket) return; - - const handleTaskDescriptionChange = (data: { + // Memoize socket event handlers + const handleTaskDescriptionChange = useCallback( + (data: { id: string; parent_task: string; description: string; }) => { + if (!data) return; + dispatch(updateTaskDescription(data)); - }; + }, + [dispatch] + ); - socket.on(SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), handleTaskDescriptionChange); - - return () => { - socket.off(SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), handleTaskDescriptionChange); - }; - }, [socket, dispatch]); - - // Socket handler for new task creation - useEffect(() => { - if (!socket) return; - - const handleNewTaskReceived = (data: IProjectTask) => { + // Memoize socket event handlers + const handleNewTaskReceived = useCallback( + (data: IProjectTask) => { if (!data) return; if (data.parent_task_id) { dispatch(updateSubTasks(data)); } - }; + }, + [dispatch] + ); - socket.on(SocketEvents.QUICK_TASK.toString(), handleNewTaskReceived); - - return () => { - socket.off(SocketEvents.QUICK_TASK.toString(), handleNewTaskReceived); - }; - }, [socket, dispatch]); - - // Socket handler for task progress updates - useEffect(() => { - if (!socket) return; - - const handleTaskProgressUpdated = (data: { + // Memoize socket event handlers + const handleTaskProgressUpdated = useCallback( + (data: { task_id: string; progress_value?: number; weight?: number; }) => { + if (!data || !taskGroups) return; + if (data.progress_value !== undefined) { - // Find the task in the task groups and update its progress for (const group of taskGroups) { - const task = group.tasks.find(task => task.id === data.task_id); + const task = group.tasks?.find(task => task.id === data.task_id); if (task) { dispatch( updateTaskProgress({ @@ -403,25 +365,76 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { } } } + }, + [dispatch, taskGroups] + ); + + // Set up socket event listeners + useEffect(() => { + if (!socket) return; + + const eventHandlers = { + [SocketEvents.QUICK_ASSIGNEES_UPDATE.toString()]: handleAssigneesUpdate, + [SocketEvents.TASK_LABELS_CHANGE.toString()]: handleLabelsChange, + [SocketEvents.CREATE_LABEL.toString()]: handleLabelsChange, + [SocketEvents.TASK_STATUS_CHANGE.toString()]: handleTaskStatusChange, + [SocketEvents.GET_TASK_PROGRESS.toString()]: handleTaskProgress, + [SocketEvents.TASK_PRIORITY_CHANGE.toString()]: handlePriorityChange, + [SocketEvents.TASK_END_DATE_CHANGE.toString()]: handleEndDateChange, + [SocketEvents.TASK_NAME_CHANGE.toString()]: handleTaskNameChange, + [SocketEvents.TASK_PHASE_CHANGE.toString()]: handlePhaseChange, + [SocketEvents.TASK_START_DATE_CHANGE.toString()]: handleStartDateChange, + [SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString()]: handleTaskSubscribersChange, + [SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString()]: handleEstimationChange, + [SocketEvents.TASK_DESCRIPTION_CHANGE.toString()]: handleTaskDescriptionChange, + [SocketEvents.QUICK_TASK.toString()]: handleNewTaskReceived, + [SocketEvents.TASK_PROGRESS_UPDATED.toString()]: handleTaskProgressUpdated, }; - socket.on(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleTaskProgressUpdated); + // Register all event handlers + Object.entries(eventHandlers).forEach(([event, handler]) => { + if (handler) { + socket.on(event, handler); + } + }); + // Cleanup function return () => { - socket.off(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleTaskProgressUpdated); + Object.entries(eventHandlers).forEach(([event, handler]) => { + if (handler) { + socket.off(event, handler); + } + }); }; - }, [socket, dispatch, taskGroups]); + }, [ + socket, + handleAssigneesUpdate, + handleLabelsChange, + handleTaskStatusChange, + handleTaskProgress, + handlePriorityChange, + handleEndDateChange, + handleTaskNameChange, + handlePhaseChange, + handleStartDateChange, + handleTaskSubscribersChange, + handleEstimationChange, + handleTaskDescriptionChange, + handleNewTaskReceived, + handleTaskProgressUpdated, + ]); + // Memoize drag handlers const handleDragStart = useCallback(({ active }: DragStartEvent) => { setActiveId(active.id as string); - // Add smooth transition to the dragged item const draggedElement = document.querySelector(`[data-id="${active.id}"]`); if (draggedElement) { (draggedElement as HTMLElement).style.transition = 'transform 0.2s ease'; } }, []); + // Memoize drag handlers const handleDragEnd = useCallback( async ({ active, over }: DragEndEvent) => { setActiveId(null); @@ -440,10 +453,8 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { const fromIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId); if (fromIndex === -1) return; - // Create a deep clone of the task to avoid reference issues const task = JSON.parse(JSON.stringify(sourceGroup.tasks[fromIndex])); - // Check if task dependencies allow the move if (activeGroupId !== overGroupId) { const canContinue = await checkTaskDependencyStatus(task.id, overGroupId); if (!canContinue) { @@ -455,7 +466,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { return; } - // Update task properties based on target group switch (groupBy) { case IGroupBy.STATUS: task.status = overGroupId; @@ -468,35 +478,29 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { task.priority_color_dark = targetGroup.color_code_dark; break; case IGroupBy.PHASE: - // Check if ALPHA_CHANNEL is already added const baseColor = targetGroup.color_code.endsWith(ALPHA_CHANNEL) - ? targetGroup.color_code.slice(0, -ALPHA_CHANNEL.length) // Remove ALPHA_CHANNEL - : targetGroup.color_code; // Use as is if not present + ? targetGroup.color_code.slice(0, -ALPHA_CHANNEL.length) + : targetGroup.color_code; task.phase_id = overGroupId; - task.phase_color = baseColor; // Set the cleaned color + task.phase_color = baseColor; break; } } const isTargetGroupEmpty = targetGroup.tasks.length === 0; - - // Calculate toIndex - for empty groups, always add at index 0 const toIndex = isTargetGroupEmpty ? 0 : overTaskId ? targetGroup.tasks.findIndex(t => t.id === overTaskId) : targetGroup.tasks.length; - // Calculate toPos similar to Angular implementation const toPos = isTargetGroupEmpty ? -1 : targetGroup.tasks[toIndex]?.sort_order || targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order || -1; - // Update Redux state if (activeGroupId === overGroupId) { - // Same group - move within array const updatedTasks = [...sourceGroup.tasks]; updatedTasks.splice(fromIndex, 1); updatedTasks.splice(toIndex, 0, task); @@ -514,7 +518,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { }, }); } else { - // Different groups - transfer between arrays const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex); const updatedTargetTasks = [...targetGroup.tasks]; @@ -540,7 +543,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { }); } - // Emit socket event socket?.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { project_id: projectId, from_index: sourceGroup.tasks[fromIndex].sort_order, @@ -549,13 +551,11 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { from_group: sourceGroup.id, to_group: targetGroup.id, group_by: groupBy, - task: sourceGroup.tasks[fromIndex], // Send original task to maintain references + task: sourceGroup.tasks[fromIndex], team_id: currentSession?.team_id, }); - // Reset styles setTimeout(resetTaskRowStyles, 0); - trackMixpanelEvent(evt_project_task_list_drag_and_move); }, [ @@ -570,6 +570,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { ] ); + // Memoize drag handlers const handleDragOver = useCallback( ({ active, over }: DragEndEvent) => { if (!over) return; @@ -589,12 +590,9 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { if (fromIndex === -1 || toIndex === -1) return; - // Create a deep clone of the task to avoid reference issues const task = JSON.parse(JSON.stringify(sourceGroup.tasks[fromIndex])); - // Update Redux state if (activeGroupId === overGroupId) { - // Same group - move within array const updatedTasks = [...sourceGroup.tasks]; updatedTasks.splice(fromIndex, 1); updatedTasks.splice(toIndex, 0, task); @@ -612,10 +610,8 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { }, }); } else { - // Different groups - transfer between arrays const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex); const updatedTargetTasks = [...targetGroup.tasks]; - updatedTargetTasks.splice(toIndex, 0, task); dispatch({ @@ -663,7 +659,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => { // Handle animation cleanup after drag ends useIsomorphicLayoutEffect(() => { if (activeId === null) { - // Final cleanup after React updates DOM const timeoutId = setTimeout(resetTaskRowStyles, 50); return () => clearTimeout(timeoutId); }