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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import Flex from 'antd/es/flex';
|
import Flex from 'antd/es/flex';
|
||||||
import Skeleton from 'antd/es/skeleton';
|
import Skeleton from 'antd/es/skeleton';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
@@ -17,8 +17,8 @@ const ProjectViewTaskList = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { projectView } = useTabSearchParam();
|
const { projectView } = useTabSearchParam();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
// Add local loading state to immediately show skeleton
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||||
|
|
||||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||||
const { taskGroups, loadingGroups, groupBy, archived, fields, search } = useAppSelector(
|
const { taskGroups, loadingGroups, groupBy, archived, fields, search } = useAppSelector(
|
||||||
@@ -30,47 +30,73 @@ const ProjectViewTaskList = () => {
|
|||||||
const { loadingPhases } = useAppSelector(state => state.phaseReducer);
|
const { loadingPhases } = useAppSelector(state => state.phaseReducer);
|
||||||
const { loadingColumns } = useAppSelector(state => state.taskReducer);
|
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(() => {
|
useEffect(() => {
|
||||||
// Set default view to list if projectView is not list or board
|
|
||||||
if (projectView !== 'list' && projectView !== 'board') {
|
if (projectView !== 'list' && projectView !== 'board') {
|
||||||
searchParams.set('tab', 'tasks-list');
|
const newParams = new URLSearchParams(searchParams);
|
||||||
searchParams.set('pinned_tab', 'tasks-list');
|
newParams.set('tab', 'tasks-list');
|
||||||
setSearchParams(searchParams);
|
newParams.set('pinned_tab', 'tasks-list');
|
||||||
|
setSearchParams(newParams);
|
||||||
}
|
}
|
||||||
}, [projectView, searchParams, setSearchParams]);
|
}, [projectView, setSearchParams]);
|
||||||
|
|
||||||
|
// Update loading state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set loading state based on all loading conditions
|
setIsLoading(isLoadingState);
|
||||||
setIsLoading(loadingGroups || loadingColumns || loadingPhases || loadingStatusCategories);
|
}, [isLoadingState]);
|
||||||
}, [loadingGroups, loadingColumns, loadingPhases, loadingStatusCategories]);
|
|
||||||
|
|
||||||
|
// Fetch initial data only once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const fetchInitialData = async () => {
|
||||||
if (projectId && groupBy) {
|
if (!projectId || !groupBy || initialLoadComplete) return;
|
||||||
const promises = [];
|
|
||||||
|
try {
|
||||||
if (!loadingColumns) promises.push(dispatch(fetchTaskListColumns(projectId)));
|
await Promise.all([
|
||||||
if (!loadingPhases) promises.push(dispatch(fetchPhasesByProjectId(projectId)));
|
dispatch(fetchTaskListColumns(projectId)),
|
||||||
if (!loadingGroups && projectView === 'list') {
|
dispatch(fetchPhasesByProjectId(projectId)),
|
||||||
promises.push(dispatch(fetchTaskGroups(projectId)));
|
dispatch(fetchStatusesCategories())
|
||||||
}
|
]);
|
||||||
if (!statusCategories.length) {
|
setInitialLoadComplete(true);
|
||||||
promises.push(dispatch(fetchStatusesCategories()));
|
} catch (error) {
|
||||||
}
|
console.error('Error fetching initial data:', error);
|
||||||
|
|
||||||
// Wait for all data to load
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
fetchInitialData();
|
||||||
}, [dispatch, projectId, groupBy, fields, search, archived]);
|
}, [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 (
|
return (
|
||||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||||
<TaskListFilters position="list" />
|
<TaskListFilters position="list" />
|
||||||
|
|
||||||
{(taskGroups && taskGroups.length === 0 && !isLoading) ? (
|
{isEmptyState ? (
|
||||||
<Empty description="No tasks group found" />
|
<Empty description="No tasks group found" />
|
||||||
) : (
|
) : (
|
||||||
<Skeleton active loading={isLoading} className='mt-4 p-4'>
|
<Skeleton active loading={isLoading} className='mt-4 p-4'>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useAppSelector } from '@/hooks/useAppSelector';
|
|||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import Flex from 'antd/es/flex';
|
import Flex from 'antd/es/flex';
|
||||||
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
|
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
|
||||||
@@ -87,16 +87,22 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
const loadingAssignees = useAppSelector(state => state.taskReducer.loadingAssignees);
|
const loadingAssignees = useAppSelector(state => state.taskReducer.loadingAssignees);
|
||||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||||
|
|
||||||
const sensors = useSensors(
|
// Move useSensors to top level and memoize its configuration
|
||||||
useSensor(PointerSensor, {
|
const sensorConfig = useMemo(
|
||||||
|
() => ({
|
||||||
activationConstraint: { distance: 8 },
|
activationConstraint: { distance: 8 },
|
||||||
})
|
}),
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pointerSensor = useSensor(PointerSensor, sensorConfig);
|
||||||
|
const sensors = useSensors(pointerSensor);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGroups(taskGroups);
|
setGroups(taskGroups);
|
||||||
}, [taskGroups]);
|
}, [taskGroups]);
|
||||||
|
|
||||||
|
// Memoize resetTaskRowStyles to prevent unnecessary re-renders
|
||||||
const resetTaskRowStyles = useCallback(() => {
|
const resetTaskRowStyles = useCallback(() => {
|
||||||
document.querySelectorAll<HTMLElement>('.task-row').forEach(row => {
|
document.querySelectorAll<HTMLElement>('.task-row').forEach(row => {
|
||||||
row.style.transition = 'transform 0.2s ease, opacity 0.2s ease';
|
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
|
// Memoize socket event handlers
|
||||||
useEffect(() => {
|
const handleAssigneesUpdate = useCallback(
|
||||||
if (!socket) return;
|
(data: ITaskAssigneesUpdateResponse) => {
|
||||||
|
|
||||||
const handleAssigneesUpdate = (data: ITaskAssigneesUpdateResponse) => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const updatedAssignees = data.assignees.map(assignee => ({
|
const updatedAssignees = data.assignees?.map(assignee => ({
|
||||||
...assignee,
|
...assignee,
|
||||||
selected: true,
|
selected: true,
|
||||||
}));
|
})) || [];
|
||||||
|
|
||||||
// Find the group that contains the task or its subtasks
|
const groupId = groups?.find(group =>
|
||||||
const groupId = groups.find(group =>
|
group.tasks?.some(
|
||||||
group.tasks.some(
|
|
||||||
task =>
|
task =>
|
||||||
task.id === data.id ||
|
task.id === data.id ||
|
||||||
(task.sub_tasks && task.sub_tasks.some(subtask => subtask.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) {
|
if (currentSession?.team_id && !loadingAssignees) {
|
||||||
dispatch(fetchTaskAssignees(currentSession.team_id));
|
dispatch(fetchTaskAssignees(currentSession.team_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[groups, dispatch, currentSession?.team_id, loadingAssignees]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), handleAssigneesUpdate);
|
// Memoize socket event handlers
|
||||||
return () => {
|
const handleLabelsChange = useCallback(
|
||||||
socket.off(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), handleAssigneesUpdate);
|
async (labels: ILabelsChangeResponse) => {
|
||||||
};
|
if (!labels) return;
|
||||||
}, [socket, currentSession?.team_id, loadingAssignees, groups, dispatch]);
|
|
||||||
|
|
||||||
// Socket handler for label updates
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const handleLabelsChange = async (labels: ILabelsChangeResponse) => {
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
dispatch(updateTaskLabel(labels)),
|
dispatch(updateTaskLabel(labels)),
|
||||||
dispatch(setTaskLabels(labels)),
|
dispatch(setTaskLabels(labels)),
|
||||||
dispatch(fetchLabels()),
|
dispatch(fetchLabels()),
|
||||||
projectId && dispatch(fetchLabelsByProject(projectId)),
|
projectId && dispatch(fetchLabelsByProject(projectId)),
|
||||||
]);
|
]);
|
||||||
};
|
},
|
||||||
|
[dispatch, projectId]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_LABELS_CHANGE.toString(), handleLabelsChange);
|
// Memoize socket event handlers
|
||||||
socket.on(SocketEvents.CREATE_LABEL.toString(), handleLabelsChange);
|
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) {
|
if (response.completed_deps === false) {
|
||||||
alertService.error(
|
alertService.error(
|
||||||
'Task is not completed',
|
'Task is not completed',
|
||||||
@@ -186,11 +183,14 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(updateTaskStatus(response));
|
dispatch(updateTaskStatus(response));
|
||||||
// dispatch(setTaskStatus(response));
|
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const handleTaskProgress = (data: {
|
// Memoize socket event handlers
|
||||||
|
const handleTaskProgress = useCallback(
|
||||||
|
(data: {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
complete_ratio: number;
|
complete_ratio: number;
|
||||||
@@ -198,6 +198,8 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
total_tasks_count: number;
|
total_tasks_count: number;
|
||||||
parent_task: string;
|
parent_task: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateTaskProgress({
|
updateTaskProgress({
|
||||||
taskId: data.parent_task || data.id,
|
taskId: data.parent_task || data.id,
|
||||||
@@ -206,190 +208,150 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
completedCount: data.completed_count,
|
completedCount: data.completed_count,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_STATUS_CHANGE.toString(), handleTaskStatusChange);
|
// Memoize socket event handlers
|
||||||
socket.on(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress);
|
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(updateTaskPriority(response));
|
||||||
dispatch(setTaskPriority(response));
|
dispatch(setTaskPriority(response));
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_PRIORITY_CHANGE.toString(), handlePriorityChange);
|
// Memoize socket event handlers
|
||||||
|
const handleEndDateChange = useCallback(
|
||||||
return () => {
|
(task: {
|
||||||
socket.off(SocketEvents.TASK_PRIORITY_CHANGE.toString(), handlePriorityChange);
|
|
||||||
};
|
|
||||||
}, [socket, dispatch]);
|
|
||||||
|
|
||||||
// Socket handler for due date updates
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const handleEndDateChange = (task: {
|
|
||||||
id: string;
|
id: string;
|
||||||
parent_task: string | null;
|
parent_task: string | null;
|
||||||
end_date: string;
|
end_date: string;
|
||||||
}) => {
|
}) => {
|
||||||
dispatch(updateTaskEndDate({ task }));
|
if (!task) return;
|
||||||
dispatch(setTaskEndDate(task));
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleEndDateChange);
|
const taskWithProgress = {
|
||||||
|
...task,
|
||||||
|
manual_progress: false,
|
||||||
|
} as IProjectTask;
|
||||||
|
|
||||||
return () => {
|
dispatch(updateTaskEndDate({ task: taskWithProgress }));
|
||||||
socket.off(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleEndDateChange);
|
dispatch(setTaskEndDate(taskWithProgress));
|
||||||
};
|
},
|
||||||
}, [socket, dispatch]);
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
// Socket handler for task name updates
|
// Memoize socket event handlers
|
||||||
useEffect(() => {
|
const handleTaskNameChange = useCallback(
|
||||||
if (!socket) return;
|
(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(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(updateTaskPhase(data));
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_PHASE_CHANGE.toString(), handlePhaseChange);
|
// Memoize socket event handlers
|
||||||
|
const handleStartDateChange = useCallback(
|
||||||
return () => {
|
(task: {
|
||||||
socket.off(SocketEvents.TASK_PHASE_CHANGE.toString(), handlePhaseChange);
|
|
||||||
};
|
|
||||||
}, [socket, dispatch]);
|
|
||||||
|
|
||||||
// Socket handler for start date updates
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const handleStartDateChange = (task: {
|
|
||||||
id: string;
|
id: string;
|
||||||
parent_task: string | null;
|
parent_task: string | null;
|
||||||
start_date: string;
|
start_date: string;
|
||||||
}) => {
|
}) => {
|
||||||
dispatch(updateTaskStartDate({ task }));
|
if (!task) return;
|
||||||
dispatch(setStartDate(task));
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_START_DATE_CHANGE.toString(), handleStartDateChange);
|
const taskWithProgress = {
|
||||||
|
...task,
|
||||||
|
manual_progress: false,
|
||||||
|
} as IProjectTask;
|
||||||
|
|
||||||
return () => {
|
dispatch(updateTaskStartDate({ task: taskWithProgress }));
|
||||||
socket.off(SocketEvents.TASK_START_DATE_CHANGE.toString(), handleStartDateChange);
|
dispatch(setStartDate(taskWithProgress));
|
||||||
};
|
},
|
||||||
}, [socket, dispatch]);
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
// Socket handler for task subscribers updates
|
// Memoize socket event handlers
|
||||||
useEffect(() => {
|
const handleTaskSubscribersChange = useCallback(
|
||||||
if (!socket) return;
|
(data: InlineMember[]) => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
const handleTaskSubscribersChange = (data: InlineMember[]) => {
|
|
||||||
dispatch(setTaskSubscribers(data));
|
dispatch(setTaskSubscribers(data));
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), handleTaskSubscribersChange);
|
// Memoize socket event handlers
|
||||||
|
const handleEstimationChange = useCallback(
|
||||||
return () => {
|
(task: {
|
||||||
socket.off(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), handleTaskSubscribersChange);
|
|
||||||
};
|
|
||||||
}, [socket, dispatch]);
|
|
||||||
|
|
||||||
// Socket handler for task estimation updates
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const handleEstimationChange = (task: {
|
|
||||||
id: string;
|
id: string;
|
||||||
parent_task: string | null;
|
parent_task: string | null;
|
||||||
estimation: number;
|
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 () => {
|
dispatch(updateTaskEstimation({ task: taskWithProgress }));
|
||||||
socket.off(SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), handleEstimationChange);
|
},
|
||||||
};
|
[dispatch]
|
||||||
}, [socket, dispatch]);
|
);
|
||||||
|
|
||||||
// Socket handler for task description updates
|
// Memoize socket event handlers
|
||||||
useEffect(() => {
|
const handleTaskDescriptionChange = useCallback(
|
||||||
if (!socket) return;
|
(data: {
|
||||||
|
|
||||||
const handleTaskDescriptionChange = (data: {
|
|
||||||
id: string;
|
id: string;
|
||||||
parent_task: string;
|
parent_task: string;
|
||||||
description: string;
|
description: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
dispatch(updateTaskDescription(data));
|
dispatch(updateTaskDescription(data));
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), handleTaskDescriptionChange);
|
// Memoize socket event handlers
|
||||||
|
const handleNewTaskReceived = useCallback(
|
||||||
return () => {
|
(data: IProjectTask) => {
|
||||||
socket.off(SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), handleTaskDescriptionChange);
|
|
||||||
};
|
|
||||||
}, [socket, dispatch]);
|
|
||||||
|
|
||||||
// Socket handler for new task creation
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const handleNewTaskReceived = (data: IProjectTask) => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
if (data.parent_task_id) {
|
if (data.parent_task_id) {
|
||||||
dispatch(updateSubTasks(data));
|
dispatch(updateSubTasks(data));
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
socket.on(SocketEvents.QUICK_TASK.toString(), handleNewTaskReceived);
|
// Memoize socket event handlers
|
||||||
|
const handleTaskProgressUpdated = useCallback(
|
||||||
return () => {
|
(data: {
|
||||||
socket.off(SocketEvents.QUICK_TASK.toString(), handleNewTaskReceived);
|
|
||||||
};
|
|
||||||
}, [socket, dispatch]);
|
|
||||||
|
|
||||||
// Socket handler for task progress updates
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const handleTaskProgressUpdated = (data: {
|
|
||||||
task_id: string;
|
task_id: string;
|
||||||
progress_value?: number;
|
progress_value?: number;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!data || !taskGroups) return;
|
||||||
|
|
||||||
if (data.progress_value !== undefined) {
|
if (data.progress_value !== undefined) {
|
||||||
// Find the task in the task groups and update its progress
|
|
||||||
for (const group of taskGroups) {
|
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) {
|
if (task) {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateTaskProgress({
|
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 () => {
|
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) => {
|
const handleDragStart = useCallback(({ active }: DragStartEvent) => {
|
||||||
setActiveId(active.id as string);
|
setActiveId(active.id as string);
|
||||||
|
|
||||||
// Add smooth transition to the dragged item
|
|
||||||
const draggedElement = document.querySelector(`[data-id="${active.id}"]`);
|
const draggedElement = document.querySelector(`[data-id="${active.id}"]`);
|
||||||
if (draggedElement) {
|
if (draggedElement) {
|
||||||
(draggedElement as HTMLElement).style.transition = 'transform 0.2s ease';
|
(draggedElement as HTMLElement).style.transition = 'transform 0.2s ease';
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Memoize drag handlers
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
async ({ active, over }: DragEndEvent) => {
|
async ({ active, over }: DragEndEvent) => {
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
@@ -440,10 +453,8 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
const fromIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
|
const fromIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
|
||||||
if (fromIndex === -1) return;
|
if (fromIndex === -1) return;
|
||||||
|
|
||||||
// Create a deep clone of the task to avoid reference issues
|
|
||||||
const task = JSON.parse(JSON.stringify(sourceGroup.tasks[fromIndex]));
|
const task = JSON.parse(JSON.stringify(sourceGroup.tasks[fromIndex]));
|
||||||
|
|
||||||
// Check if task dependencies allow the move
|
|
||||||
if (activeGroupId !== overGroupId) {
|
if (activeGroupId !== overGroupId) {
|
||||||
const canContinue = await checkTaskDependencyStatus(task.id, overGroupId);
|
const canContinue = await checkTaskDependencyStatus(task.id, overGroupId);
|
||||||
if (!canContinue) {
|
if (!canContinue) {
|
||||||
@@ -455,7 +466,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update task properties based on target group
|
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case IGroupBy.STATUS:
|
case IGroupBy.STATUS:
|
||||||
task.status = overGroupId;
|
task.status = overGroupId;
|
||||||
@@ -468,35 +478,29 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
task.priority_color_dark = targetGroup.color_code_dark;
|
task.priority_color_dark = targetGroup.color_code_dark;
|
||||||
break;
|
break;
|
||||||
case IGroupBy.PHASE:
|
case IGroupBy.PHASE:
|
||||||
// Check if ALPHA_CHANNEL is already added
|
|
||||||
const baseColor = targetGroup.color_code.endsWith(ALPHA_CHANNEL)
|
const baseColor = targetGroup.color_code.endsWith(ALPHA_CHANNEL)
|
||||||
? targetGroup.color_code.slice(0, -ALPHA_CHANNEL.length) // Remove ALPHA_CHANNEL
|
? targetGroup.color_code.slice(0, -ALPHA_CHANNEL.length)
|
||||||
: targetGroup.color_code; // Use as is if not present
|
: targetGroup.color_code;
|
||||||
task.phase_id = overGroupId;
|
task.phase_id = overGroupId;
|
||||||
task.phase_color = baseColor; // Set the cleaned color
|
task.phase_color = baseColor;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTargetGroupEmpty = targetGroup.tasks.length === 0;
|
const isTargetGroupEmpty = targetGroup.tasks.length === 0;
|
||||||
|
|
||||||
// Calculate toIndex - for empty groups, always add at index 0
|
|
||||||
const toIndex = isTargetGroupEmpty
|
const toIndex = isTargetGroupEmpty
|
||||||
? 0
|
? 0
|
||||||
: overTaskId
|
: overTaskId
|
||||||
? targetGroup.tasks.findIndex(t => t.id === overTaskId)
|
? targetGroup.tasks.findIndex(t => t.id === overTaskId)
|
||||||
: targetGroup.tasks.length;
|
: targetGroup.tasks.length;
|
||||||
|
|
||||||
// Calculate toPos similar to Angular implementation
|
|
||||||
const toPos = isTargetGroupEmpty
|
const toPos = isTargetGroupEmpty
|
||||||
? -1
|
? -1
|
||||||
: targetGroup.tasks[toIndex]?.sort_order ||
|
: targetGroup.tasks[toIndex]?.sort_order ||
|
||||||
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
|
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
|
||||||
-1;
|
-1;
|
||||||
|
|
||||||
// Update Redux state
|
|
||||||
if (activeGroupId === overGroupId) {
|
if (activeGroupId === overGroupId) {
|
||||||
// Same group - move within array
|
|
||||||
const updatedTasks = [...sourceGroup.tasks];
|
const updatedTasks = [...sourceGroup.tasks];
|
||||||
updatedTasks.splice(fromIndex, 1);
|
updatedTasks.splice(fromIndex, 1);
|
||||||
updatedTasks.splice(toIndex, 0, task);
|
updatedTasks.splice(toIndex, 0, task);
|
||||||
@@ -514,7 +518,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Different groups - transfer between arrays
|
|
||||||
const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex);
|
const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex);
|
||||||
const updatedTargetTasks = [...targetGroup.tasks];
|
const updatedTargetTasks = [...targetGroup.tasks];
|
||||||
|
|
||||||
@@ -540,7 +543,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit socket event
|
|
||||||
socket?.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
socket?.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
from_index: sourceGroup.tasks[fromIndex].sort_order,
|
from_index: sourceGroup.tasks[fromIndex].sort_order,
|
||||||
@@ -549,13 +551,11 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
from_group: sourceGroup.id,
|
from_group: sourceGroup.id,
|
||||||
to_group: targetGroup.id,
|
to_group: targetGroup.id,
|
||||||
group_by: groupBy,
|
group_by: groupBy,
|
||||||
task: sourceGroup.tasks[fromIndex], // Send original task to maintain references
|
task: sourceGroup.tasks[fromIndex],
|
||||||
team_id: currentSession?.team_id,
|
team_id: currentSession?.team_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset styles
|
|
||||||
setTimeout(resetTaskRowStyles, 0);
|
setTimeout(resetTaskRowStyles, 0);
|
||||||
|
|
||||||
trackMixpanelEvent(evt_project_task_list_drag_and_move);
|
trackMixpanelEvent(evt_project_task_list_drag_and_move);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -570,6 +570,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Memoize drag handlers
|
||||||
const handleDragOver = useCallback(
|
const handleDragOver = useCallback(
|
||||||
({ active, over }: DragEndEvent) => {
|
({ active, over }: DragEndEvent) => {
|
||||||
if (!over) return;
|
if (!over) return;
|
||||||
@@ -589,12 +590,9 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
|
|
||||||
if (fromIndex === -1 || toIndex === -1) return;
|
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]));
|
const task = JSON.parse(JSON.stringify(sourceGroup.tasks[fromIndex]));
|
||||||
|
|
||||||
// Update Redux state
|
|
||||||
if (activeGroupId === overGroupId) {
|
if (activeGroupId === overGroupId) {
|
||||||
// Same group - move within array
|
|
||||||
const updatedTasks = [...sourceGroup.tasks];
|
const updatedTasks = [...sourceGroup.tasks];
|
||||||
updatedTasks.splice(fromIndex, 1);
|
updatedTasks.splice(fromIndex, 1);
|
||||||
updatedTasks.splice(toIndex, 0, task);
|
updatedTasks.splice(toIndex, 0, task);
|
||||||
@@ -612,10 +610,8 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Different groups - transfer between arrays
|
|
||||||
const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex);
|
const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex);
|
||||||
const updatedTargetTasks = [...targetGroup.tasks];
|
const updatedTargetTasks = [...targetGroup.tasks];
|
||||||
|
|
||||||
updatedTargetTasks.splice(toIndex, 0, task);
|
updatedTargetTasks.splice(toIndex, 0, task);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -663,7 +659,6 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
// Handle animation cleanup after drag ends
|
// Handle animation cleanup after drag ends
|
||||||
useIsomorphicLayoutEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
if (activeId === null) {
|
if (activeId === null) {
|
||||||
// Final cleanup after React updates DOM
|
|
||||||
const timeoutId = setTimeout(resetTaskRowStyles, 50);
|
const timeoutId = setTimeout(resetTaskRowStyles, 50);
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user