expand sub tasks
This commit is contained in:
@@ -11,7 +11,7 @@ const useDragCursor = (isDragging: boolean) => {
|
||||
|
||||
// Save the original cursor style
|
||||
const originalCursor = document.body.style.cursor;
|
||||
|
||||
|
||||
// Apply grabbing cursor to the entire document when dragging
|
||||
document.body.style.cursor = 'grabbing';
|
||||
|
||||
@@ -22,4 +22,4 @@ const useDragCursor = (isDragging: boolean) => {
|
||||
}, [isDragging]);
|
||||
};
|
||||
|
||||
export default useDragCursor;
|
||||
export default useDragCursor;
|
||||
|
||||
@@ -2,10 +2,7 @@ import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
|
||||
import {
|
||||
fetchLabelsByProject,
|
||||
fetchTaskAssignees,
|
||||
} from '@/features/tasks/tasks.slice';
|
||||
import { fetchLabelsByProject, fetchTaskAssignees } from '@/features/tasks/tasks.slice';
|
||||
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||
|
||||
/**
|
||||
@@ -14,10 +11,10 @@ import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||
*/
|
||||
export const useFilterDataLoader = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
// Memoize the priorities selector to prevent unnecessary re-renders
|
||||
const priorities = useAppSelector(state => state.priorityReducer.priorities);
|
||||
|
||||
|
||||
// Memoize the projectId selector to prevent unnecessary re-renders
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
|
||||
@@ -38,14 +35,16 @@ export const useFilterDataLoader = () => {
|
||||
}
|
||||
|
||||
// Load team members for member filters
|
||||
dispatch(getTeamMembers({
|
||||
index: 0,
|
||||
size: 100,
|
||||
field: null,
|
||||
order: null,
|
||||
search: null,
|
||||
all: true
|
||||
}));
|
||||
dispatch(
|
||||
getTeamMembers({
|
||||
index: 0,
|
||||
size: 100,
|
||||
field: null,
|
||||
order: null,
|
||||
search: null,
|
||||
all: true,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error loading filter data:', error);
|
||||
// Don't throw - filter loading errors shouldn't break the main UI
|
||||
@@ -57,11 +56,11 @@ export const useFilterDataLoader = () => {
|
||||
// Use setTimeout to ensure this runs after the main component render
|
||||
// This prevents filter loading from blocking the initial render
|
||||
const timeoutId = setTimeout(loadFilterData, 0);
|
||||
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [loadFilterData]);
|
||||
|
||||
return {
|
||||
loadFilterData,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,12 +5,14 @@ const useIsProjectManager = () => {
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const { project: currentProject } = useAppSelector(state => state.projectReducer);
|
||||
const { project: drawerProject } = useAppSelector(state => state.projectDrawerReducer);
|
||||
|
||||
|
||||
// Check if user is project manager for either the current project or drawer project
|
||||
const isManagerOfCurrentProject = currentSession?.team_member_id === currentProject?.project_manager?.id;
|
||||
const isManagerOfDrawerProject = currentSession?.team_member_id === drawerProject?.project_manager?.id;
|
||||
const isManagerOfCurrentProject =
|
||||
currentSession?.team_member_id === currentProject?.project_manager?.id;
|
||||
const isManagerOfDrawerProject =
|
||||
currentSession?.team_member_id === drawerProject?.project_manager?.id;
|
||||
|
||||
return isManagerOfCurrentProject || isManagerOfDrawerProject;
|
||||
};
|
||||
|
||||
export default useIsProjectManager;
|
||||
export default useIsProjectManager;
|
||||
|
||||
@@ -3,4 +3,4 @@ import { useLayoutEffect, useEffect } from 'react';
|
||||
// Use useLayoutEffect in browser environments and useEffect in SSR environments
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
export default useIsomorphicLayoutEffect;
|
||||
export default useIsomorphicLayoutEffect;
|
||||
|
||||
@@ -15,29 +15,32 @@ export const usePerformanceOptimization = () => {
|
||||
lastRenderTimeRef.current = now;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[${componentName}] Render #${renderCountRef.current}, Time since last: ${timeSinceLastRender.toFixed(2)}ms`);
|
||||
|
||||
if (timeSinceLastRender < 16) { // Less than 60fps
|
||||
console.log(
|
||||
`[${componentName}] Render #${renderCountRef.current}, Time since last: ${timeSinceLastRender.toFixed(2)}ms`
|
||||
);
|
||||
|
||||
if (timeSinceLastRender < 16) {
|
||||
// Less than 60fps
|
||||
console.warn(`[${componentName}] Potential over-rendering detected`);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Debounced callback creator
|
||||
const createDebouncedCallback = useCallback(<T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
delay: number = 300
|
||||
) => {
|
||||
return debounce(callback, delay);
|
||||
}, []);
|
||||
const createDebouncedCallback = useCallback(
|
||||
<T extends (...args: any[]) => any>(callback: T, delay: number = 300) => {
|
||||
return debounce(callback, delay);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Throttled callback creator
|
||||
const createThrottledCallback = useCallback(<T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
delay: number = 100
|
||||
) => {
|
||||
return throttle(callback, delay);
|
||||
}, []);
|
||||
const createThrottledCallback = useCallback(
|
||||
<T extends (...args: any[]) => any>(callback: T, delay: number = 100) => {
|
||||
return throttle(callback, delay);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
trackRender,
|
||||
@@ -73,11 +76,11 @@ export const useOptimizedEventHandlers = <T extends Record<string, (...args: any
|
||||
) => {
|
||||
return useMemo(() => {
|
||||
const optimizedHandlers = {} as any;
|
||||
|
||||
|
||||
Object.entries(handlers).forEach(([key, handler]) => {
|
||||
optimizedHandlers[key] = useCallback(handler, [handler]);
|
||||
});
|
||||
|
||||
|
||||
return optimizedHandlers as T;
|
||||
}, [handlers]);
|
||||
};
|
||||
@@ -90,11 +93,8 @@ export const useVirtualScrolling = (
|
||||
) => {
|
||||
const visibleRange = useMemo(() => {
|
||||
const startIndex = Math.floor(window.scrollY / itemHeight);
|
||||
const endIndex = Math.min(
|
||||
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
|
||||
itemCount
|
||||
);
|
||||
|
||||
const endIndex = Math.min(startIndex + Math.ceil(containerHeight / itemHeight) + 1, itemCount);
|
||||
|
||||
return { startIndex: Math.max(0, startIndex), endIndex };
|
||||
}, [itemCount, itemHeight, containerHeight]);
|
||||
|
||||
@@ -106,22 +106,25 @@ export const useLazyLoading = (threshold: number = 0.1) => {
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const targetRef = useCallback((node: HTMLElement | null) => {
|
||||
if (observerRef.current) observerRef.current.disconnect();
|
||||
|
||||
if (node) {
|
||||
observerRef.current = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observerRef.current?.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold }
|
||||
);
|
||||
observerRef.current.observe(node);
|
||||
}
|
||||
}, [threshold]);
|
||||
const targetRef = useCallback(
|
||||
(node: HTMLElement | null) => {
|
||||
if (observerRef.current) observerRef.current.disconnect();
|
||||
|
||||
if (node) {
|
||||
observerRef.current = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observerRef.current?.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold }
|
||||
);
|
||||
observerRef.current.observe(node);
|
||||
}
|
||||
},
|
||||
[threshold]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -133,10 +136,7 @@ export const useLazyLoading = (threshold: number = 0.1) => {
|
||||
};
|
||||
|
||||
// Memory optimization for large datasets
|
||||
export const useMemoryOptimization = <T>(
|
||||
data: T[],
|
||||
maxCacheSize: number = 1000
|
||||
) => {
|
||||
export const useMemoryOptimization = <T>(data: T[], maxCacheSize: number = 1000) => {
|
||||
const cacheRef = useRef(new Map<string, T>());
|
||||
|
||||
const optimizedData = useMemo(() => {
|
||||
@@ -147,7 +147,7 @@ export const useMemoryOptimization = <T>(
|
||||
// Keep only the most recently accessed items
|
||||
const cache = cacheRef.current;
|
||||
const recentData = data.slice(0, maxCacheSize);
|
||||
|
||||
|
||||
// Clear old cache entries
|
||||
cache.clear();
|
||||
recentData.forEach((item, index) => {
|
||||
@@ -160,4 +160,4 @@ export const useMemoryOptimization = <T>(
|
||||
return optimizedData;
|
||||
};
|
||||
|
||||
export default usePerformanceOptimization;
|
||||
export default usePerformanceOptimization;
|
||||
|
||||
@@ -4,8 +4,8 @@ const useTabSearchParam = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const tab = searchParams.get('tab');
|
||||
const projectView = tab === 'tasks-list' ? 'list' : 'kanban';
|
||||
|
||||
|
||||
return { tab, projectView };
|
||||
};
|
||||
|
||||
export default useTabSearchParam;
|
||||
export default useTabSearchParam;
|
||||
|
||||
@@ -2,7 +2,11 @@ import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
import { useAppDispatch } from './useAppDispatch';
|
||||
import { fetchTask, setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
||||
import {
|
||||
fetchTask,
|
||||
setSelectedTaskId,
|
||||
setShowTaskDrawer,
|
||||
} from '@/features/task-drawer/task-drawer.slice';
|
||||
|
||||
/**
|
||||
* A custom hook that synchronizes the task drawer state with the URL.
|
||||
@@ -15,7 +19,7 @@ const useTaskDrawerUrlSync = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { showTaskDrawer, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
|
||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||
|
||||
|
||||
// Use a ref to track whether we're in the process of closing the drawer
|
||||
const isClosingDrawer = useRef(false);
|
||||
// Use a ref to track the last task ID we processed
|
||||
@@ -29,14 +33,14 @@ const useTaskDrawerUrlSync = () => {
|
||||
// Set the flag to indicate we're closing the drawer
|
||||
isClosingDrawer.current = true;
|
||||
shouldIgnoreUrlChange.current = true;
|
||||
|
||||
|
||||
// Create a new URLSearchParams object to avoid modifying the current one
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.delete('task');
|
||||
|
||||
|
||||
// Update the URL without triggering a navigation
|
||||
setSearchParams(newParams, { replace: true });
|
||||
|
||||
|
||||
// Reset the flags after a short delay
|
||||
setTimeout(() => {
|
||||
isClosingDrawer.current = false;
|
||||
@@ -49,24 +53,26 @@ const useTaskDrawerUrlSync = () => {
|
||||
useEffect(() => {
|
||||
// Skip if we're programmatically updating the URL or closing the drawer
|
||||
if (shouldIgnoreUrlChange.current || isClosingDrawer.current) return;
|
||||
|
||||
|
||||
const taskIdFromUrl = searchParams.get('task');
|
||||
|
||||
|
||||
// Only process URL changes if:
|
||||
// 1. There's a task ID in the URL
|
||||
// 2. The drawer is not currently open
|
||||
// 3. We have a project ID
|
||||
// 4. It's a different task ID than what we last processed
|
||||
// 5. The selected task ID is different from URL (to avoid reopening same task)
|
||||
if (taskIdFromUrl &&
|
||||
!showTaskDrawer &&
|
||||
projectId &&
|
||||
taskIdFromUrl !== lastProcessedTaskId.current &&
|
||||
taskIdFromUrl !== selectedTaskId) {
|
||||
if (
|
||||
taskIdFromUrl &&
|
||||
!showTaskDrawer &&
|
||||
projectId &&
|
||||
taskIdFromUrl !== lastProcessedTaskId.current &&
|
||||
taskIdFromUrl !== selectedTaskId
|
||||
) {
|
||||
lastProcessedTaskId.current = taskIdFromUrl;
|
||||
dispatch(setSelectedTaskId(taskIdFromUrl));
|
||||
dispatch(setShowTaskDrawer(true));
|
||||
|
||||
|
||||
// Fetch task data
|
||||
dispatch(fetchTask({ taskId: taskIdFromUrl, projectId }));
|
||||
}
|
||||
@@ -76,22 +82,22 @@ const useTaskDrawerUrlSync = () => {
|
||||
useEffect(() => {
|
||||
// Don't update URL if we're in the process of closing or ignoring changes
|
||||
if (isClosingDrawer.current || shouldIgnoreUrlChange.current) return;
|
||||
|
||||
|
||||
if (showTaskDrawer && selectedTaskId) {
|
||||
// Don't update if it's the same task ID we already processed
|
||||
if (lastProcessedTaskId.current === selectedTaskId) return;
|
||||
|
||||
|
||||
// Add task ID to URL when drawer is opened
|
||||
shouldIgnoreUrlChange.current = true;
|
||||
lastProcessedTaskId.current = selectedTaskId;
|
||||
|
||||
|
||||
// Create a new URLSearchParams object to avoid modifying the current one
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('task', selectedTaskId);
|
||||
|
||||
|
||||
// Update the URL without triggering a navigation
|
||||
setSearchParams(newParams, { replace: true });
|
||||
|
||||
|
||||
// Reset the flag after a short delay
|
||||
setTimeout(() => {
|
||||
shouldIgnoreUrlChange.current = false;
|
||||
@@ -103,11 +109,14 @@ const useTaskDrawerUrlSync = () => {
|
||||
useEffect(() => {
|
||||
// Only clear URL when drawer is closed and we have a task in URL
|
||||
// Also ensure we're not in the middle of processing other URL changes
|
||||
if (!showTaskDrawer &&
|
||||
searchParams.has('task') &&
|
||||
!isClosingDrawer.current &&
|
||||
!shouldIgnoreUrlChange.current &&
|
||||
!selectedTaskId) { // Only clear if selectedTaskId is also null/cleared
|
||||
if (
|
||||
!showTaskDrawer &&
|
||||
searchParams.has('task') &&
|
||||
!isClosingDrawer.current &&
|
||||
!shouldIgnoreUrlChange.current &&
|
||||
!selectedTaskId
|
||||
) {
|
||||
// Only clear if selectedTaskId is also null/cleared
|
||||
clearTaskFromUrl();
|
||||
lastProcessedTaskId.current = null;
|
||||
}
|
||||
@@ -116,4 +125,4 @@ const useTaskDrawerUrlSync = () => {
|
||||
return { clearTaskFromUrl };
|
||||
};
|
||||
|
||||
export default useTaskDrawerUrlSync;
|
||||
export default useTaskDrawerUrlSync;
|
||||
|
||||
@@ -34,13 +34,13 @@ import {
|
||||
updateTaskProgress,
|
||||
} from '@/features/tasks/tasks.slice';
|
||||
import {
|
||||
addTask,
|
||||
addTask,
|
||||
addTaskToGroup,
|
||||
updateTask,
|
||||
updateTask,
|
||||
moveTaskToGroup,
|
||||
moveTaskBetweenGroups,
|
||||
selectCurrentGroupingV3,
|
||||
fetchTasksV3
|
||||
fetchTasksV3,
|
||||
} from '@/features/task-management/task-management.slice';
|
||||
import {
|
||||
updateEnhancedKanbanSubtask,
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
updateEnhancedKanbanTaskProgress,
|
||||
updateEnhancedKanbanTaskName,
|
||||
updateEnhancedKanbanTaskEndDate,
|
||||
updateEnhancedKanbanTaskStartDate
|
||||
updateEnhancedKanbanTaskStartDate,
|
||||
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
||||
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
|
||||
@@ -71,7 +71,7 @@ export const useTaskSocketHandlers = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { socket } = useSocket();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
|
||||
|
||||
const { loadingAssignees, taskGroups } = useAppSelector((state: any) => state.taskReducer);
|
||||
const { projectId } = useAppSelector((state: any) => state.projectReducer);
|
||||
const currentGroupingV3 = useAppSelector(selectCurrentGroupingV3);
|
||||
@@ -81,21 +81,24 @@ export const useTaskSocketHandlers = () => {
|
||||
(data: ITaskAssigneesUpdateResponse) => {
|
||||
if (!data) return;
|
||||
|
||||
const updatedAssignees = data.assignees?.map(assignee => ({
|
||||
...assignee,
|
||||
selected: true,
|
||||
})) || [];
|
||||
const updatedAssignees =
|
||||
data.assignees?.map(assignee => ({
|
||||
...assignee,
|
||||
selected: true,
|
||||
})) || [];
|
||||
|
||||
// REAL-TIME UPDATES: Update the task-management slice for immediate UI updates
|
||||
if (data.id) {
|
||||
dispatch(updateTask({
|
||||
id: data.id,
|
||||
changes: {
|
||||
assignees: data.assignees?.map(a => a.team_member_id) || [],
|
||||
assignee_names: data.names || [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}));
|
||||
dispatch(
|
||||
updateTask({
|
||||
id: data.id,
|
||||
changes: {
|
||||
assignees: data.assignees?.map(a => a.team_member_id) || [],
|
||||
assignee_names: data.names || [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Update the old task slice (for backward compatibility)
|
||||
@@ -103,7 +106,8 @@ export const useTaskSocketHandlers = () => {
|
||||
group.tasks?.some(
|
||||
(task: IProjectTask) =>
|
||||
task.id === data.id ||
|
||||
(task.sub_tasks && task.sub_tasks.some((subtask: IProjectTask) => subtask.id === data.id))
|
||||
(task.sub_tasks &&
|
||||
task.sub_tasks.some((subtask: IProjectTask) => subtask.id === data.id))
|
||||
)
|
||||
)?.id;
|
||||
|
||||
@@ -122,7 +126,7 @@ export const useTaskSocketHandlers = () => {
|
||||
manual_progress: false,
|
||||
} as IProjectTask)
|
||||
);
|
||||
|
||||
|
||||
// Update enhanced kanban slice
|
||||
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
||||
|
||||
@@ -138,24 +142,27 @@ export const useTaskSocketHandlers = () => {
|
||||
const handleLabelsChange = useCallback(
|
||||
async (labels: ILabelsChangeResponse) => {
|
||||
if (!labels) return;
|
||||
|
||||
|
||||
// REAL-TIME UPDATES: Update the task-management slice for immediate UI updates
|
||||
if (labels.id) {
|
||||
dispatch(updateTask({
|
||||
id: labels.id,
|
||||
changes: {
|
||||
labels: labels.labels?.map(l => ({
|
||||
id: l.id || '',
|
||||
name: l.name || '',
|
||||
color: l.color_code || '#1890ff',
|
||||
end: l.end,
|
||||
names: l.names
|
||||
})) || [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}));
|
||||
dispatch(
|
||||
updateTask({
|
||||
id: labels.id,
|
||||
changes: {
|
||||
labels:
|
||||
labels.labels?.map(l => ({
|
||||
id: l.id || '',
|
||||
name: l.name || '',
|
||||
color: l.color_code || '#1890ff',
|
||||
end: l.end,
|
||||
names: l.names,
|
||||
})) || [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Update the old task slice and other related slices (for backward compatibility)
|
||||
// Only update existing data, don't refetch from server
|
||||
await Promise.all([
|
||||
@@ -165,7 +172,7 @@ export const useTaskSocketHandlers = () => {
|
||||
// dispatch(fetchLabels()),
|
||||
// projectId && dispatch(fetchLabelsByProject(projectId)),
|
||||
]);
|
||||
|
||||
|
||||
// Update enhanced kanban slice
|
||||
dispatch(updateEnhancedKanbanTaskLabels(labels));
|
||||
},
|
||||
@@ -187,7 +194,7 @@ export const useTaskSocketHandlers = () => {
|
||||
// Update the old task slice (for backward compatibility)
|
||||
dispatch(updateTaskStatus(response));
|
||||
dispatch(deselectAll());
|
||||
|
||||
|
||||
// Update enhanced kanban slice
|
||||
dispatch(updateEnhancedKanbanTaskStatus(response));
|
||||
|
||||
@@ -195,18 +202,16 @@ export const useTaskSocketHandlers = () => {
|
||||
const state = store.getState();
|
||||
const groups = state.taskManagement.groups;
|
||||
const currentTask = state.taskManagement.entities[response.id];
|
||||
|
||||
|
||||
|
||||
if (groups && groups.length > 0 && currentTask && response.status_id) {
|
||||
// Find current group containing the task
|
||||
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
||||
|
||||
|
||||
// Find target group based on new status ID
|
||||
// The status_id from response is the UUID of the new status
|
||||
const targetGroup = groups.find(group => group.id === response.status_id);
|
||||
|
||||
|
||||
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||
|
||||
// Determine the new status value based on status category
|
||||
let newStatusValue: 'todo' | 'doing' | 'done' = 'todo';
|
||||
if (response.statusCategory) {
|
||||
@@ -218,17 +223,19 @@ export const useTaskSocketHandlers = () => {
|
||||
newStatusValue = 'todo';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Use the new action to move task between groups
|
||||
dispatch(moveTaskBetweenGroups({
|
||||
taskId: response.id,
|
||||
fromGroupId: currentGroup.id,
|
||||
toGroupId: targetGroup.id,
|
||||
taskUpdate: {
|
||||
status: newStatusValue,
|
||||
progress: response.complete_ratio || currentTask.progress,
|
||||
}
|
||||
}));
|
||||
dispatch(
|
||||
moveTaskBetweenGroups({
|
||||
taskId: response.id,
|
||||
fromGroupId: currentGroup.id,
|
||||
toGroupId: targetGroup.id,
|
||||
taskUpdate: {
|
||||
status: newStatusValue,
|
||||
progress: response.complete_ratio || currentTask.progress,
|
||||
},
|
||||
})
|
||||
);
|
||||
} else if (!currentGroup || !targetGroup) {
|
||||
// Remove unnecessary refetch that causes data thrashing
|
||||
// if (projectId) {
|
||||
@@ -264,23 +271,27 @@ export const useTaskSocketHandlers = () => {
|
||||
// For the task management slice, update task progress
|
||||
const taskId = data.parent_task || data.id;
|
||||
if (taskId) {
|
||||
dispatch(updateTask({
|
||||
id: taskId,
|
||||
changes: {
|
||||
progress: data.complete_ratio,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}));
|
||||
dispatch(
|
||||
updateTask({
|
||||
id: taskId,
|
||||
changes: {
|
||||
progress: data.complete_ratio,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Update enhanced kanban slice
|
||||
dispatch(updateEnhancedKanbanTaskProgress({
|
||||
id: data.id,
|
||||
complete_ratio: data.complete_ratio,
|
||||
completed_count: data.completed_count,
|
||||
total_tasks_count: data.total_tasks_count,
|
||||
parent_task: data.parent_task,
|
||||
}));
|
||||
dispatch(
|
||||
updateEnhancedKanbanTaskProgress({
|
||||
id: data.id,
|
||||
complete_ratio: data.complete_ratio,
|
||||
completed_count: data.completed_count,
|
||||
total_tasks_count: data.total_tasks_count,
|
||||
parent_task: data.parent_task,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@@ -293,19 +304,19 @@ export const useTaskSocketHandlers = () => {
|
||||
dispatch(updateTaskPriority(response));
|
||||
dispatch(setTaskPriority(response));
|
||||
dispatch(deselectAll());
|
||||
|
||||
|
||||
// Update enhanced kanban slice
|
||||
dispatch(updateEnhancedKanbanTaskPriority(response));
|
||||
|
||||
// For the task management slice, always update the task entity first
|
||||
const state = store.getState();
|
||||
const currentTask = state.taskManagement.entities[response.id];
|
||||
|
||||
|
||||
if (currentTask) {
|
||||
// Get priority list to map priority_id to priority name
|
||||
const priorityList = state.priorityReducer?.priorities || [];
|
||||
const priority = priorityList.find(p => p.id === response.priority_id);
|
||||
|
||||
|
||||
let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium';
|
||||
if (priority?.name) {
|
||||
const priorityName = priority.name.toLowerCase();
|
||||
@@ -315,37 +326,40 @@ export const useTaskSocketHandlers = () => {
|
||||
}
|
||||
|
||||
// Update the task entity
|
||||
dispatch(updateTask({
|
||||
id: response.id,
|
||||
changes: {
|
||||
priority: newPriorityValue,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}));
|
||||
dispatch(
|
||||
updateTask({
|
||||
id: response.id,
|
||||
changes: {
|
||||
priority: newPriorityValue,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Handle group movement ONLY if grouping by priority
|
||||
const groups = state.taskManagement.groups;
|
||||
const currentGrouping = state.taskManagement.grouping;
|
||||
|
||||
|
||||
if (groups && groups.length > 0 && currentGrouping === 'priority') {
|
||||
// Find current group containing the task
|
||||
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
||||
|
||||
|
||||
// Find target group based on new priority value
|
||||
const targetGroup = groups.find(group =>
|
||||
group.groupValue.toLowerCase() === newPriorityValue.toLowerCase()
|
||||
const targetGroup = groups.find(
|
||||
group => group.groupValue.toLowerCase() === newPriorityValue.toLowerCase()
|
||||
);
|
||||
|
||||
|
||||
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||
|
||||
dispatch(moveTaskBetweenGroups({
|
||||
taskId: response.id,
|
||||
fromGroupId: currentGroup.id,
|
||||
toGroupId: targetGroup.id,
|
||||
taskUpdate: {
|
||||
priority: newPriorityValue,
|
||||
}
|
||||
}));
|
||||
dispatch(
|
||||
moveTaskBetweenGroups({
|
||||
taskId: response.id,
|
||||
fromGroupId: currentGroup.id,
|
||||
toGroupId: targetGroup.id,
|
||||
taskUpdate: {
|
||||
priority: newPriorityValue,
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log('🔧 No group movement needed for priority change');
|
||||
}
|
||||
@@ -358,11 +372,7 @@ export const useTaskSocketHandlers = () => {
|
||||
);
|
||||
|
||||
const handleEndDateChange = useCallback(
|
||||
(task: {
|
||||
id: string;
|
||||
parent_task: string | null;
|
||||
end_date: string;
|
||||
}) => {
|
||||
(task: { id: string; parent_task: string | null; end_date: string }) => {
|
||||
if (!task) return;
|
||||
|
||||
const taskWithProgress = {
|
||||
@@ -372,7 +382,7 @@ export const useTaskSocketHandlers = () => {
|
||||
|
||||
dispatch(updateTaskEndDate({ task: taskWithProgress }));
|
||||
dispatch(setTaskEndDate(taskWithProgress));
|
||||
|
||||
|
||||
// Update enhanced kanban slice
|
||||
dispatch(updateEnhancedKanbanTaskEndDate({ task: taskWithProgress }));
|
||||
},
|
||||
@@ -382,21 +392,23 @@ export const useTaskSocketHandlers = () => {
|
||||
const handleTaskNameChange = useCallback(
|
||||
(data: { id: string; parent_task: string; name: string }) => {
|
||||
if (!data) return;
|
||||
|
||||
|
||||
// Update the old task slice (for backward compatibility)
|
||||
dispatch(updateTaskName(data));
|
||||
|
||||
// For the task management slice, update task name
|
||||
if (data.id) {
|
||||
dispatch(updateTask({
|
||||
id: data.id,
|
||||
changes: {
|
||||
title: data.name,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}));
|
||||
dispatch(
|
||||
updateTask({
|
||||
id: data.id,
|
||||
changes: {
|
||||
title: data.name,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Update enhanced kanban slice
|
||||
dispatch(updateEnhancedKanbanTaskName({ task: data }));
|
||||
},
|
||||
@@ -406,7 +418,7 @@ export const useTaskSocketHandlers = () => {
|
||||
const handlePhaseChange = useCallback(
|
||||
(data: ITaskPhaseChangeResponse) => {
|
||||
if (!data) return;
|
||||
|
||||
|
||||
// Update the old task slice (for backward compatibility)
|
||||
dispatch(updateTaskPhase(data));
|
||||
dispatch(deselectAll());
|
||||
@@ -414,72 +426,78 @@ export const useTaskSocketHandlers = () => {
|
||||
// For the task management slice, always update the task entity first
|
||||
const state = store.getState();
|
||||
const taskId = data.task_id;
|
||||
|
||||
|
||||
if (taskId) {
|
||||
const currentTask = state.taskManagement.entities[taskId];
|
||||
|
||||
|
||||
if (currentTask) {
|
||||
// Get phase list to map phase_id to phase name
|
||||
const phaseList = state.phaseReducer?.phaseList || [];
|
||||
let newPhaseValue = '';
|
||||
|
||||
if (data.id) {
|
||||
// data.id is the phase_id
|
||||
const phase = phaseList.find(p => p.id === data.id);
|
||||
newPhaseValue = phase?.name || '';
|
||||
} else {
|
||||
// No phase selected (cleared)
|
||||
newPhaseValue = '';
|
||||
}
|
||||
// Get phase list to map phase_id to phase name
|
||||
const phaseList = state.phaseReducer?.phaseList || [];
|
||||
let newPhaseValue = '';
|
||||
|
||||
// Update the task entity
|
||||
dispatch(updateTask({
|
||||
id: taskId,
|
||||
changes: {
|
||||
phase: newPhaseValue,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}));
|
||||
|
||||
// Handle group movement ONLY if grouping by phase
|
||||
const groups = state.taskManagement.groups;
|
||||
const currentGrouping = state.taskManagement.grouping;
|
||||
|
||||
if (groups && groups.length > 0 && currentGrouping === 'phase') {
|
||||
// Find current group containing the task
|
||||
const currentGroup = groups.find(group => group.taskIds.includes(taskId));
|
||||
|
||||
// Find target group based on new phase value
|
||||
let targetGroup: any = null;
|
||||
|
||||
if (newPhaseValue) {
|
||||
// Find group by phase name
|
||||
targetGroup = groups.find(group =>
|
||||
group.groupValue === newPhaseValue || group.title === newPhaseValue
|
||||
);
|
||||
if (data.id) {
|
||||
// data.id is the phase_id
|
||||
const phase = phaseList.find(p => p.id === data.id);
|
||||
newPhaseValue = phase?.name || '';
|
||||
} else {
|
||||
// Find "No Phase" or similar group
|
||||
targetGroup = groups.find(group =>
|
||||
group.groupValue === '' || group.title.toLowerCase().includes('no phase') || group.title.toLowerCase().includes('unassigned')
|
||||
);
|
||||
// No phase selected (cleared)
|
||||
newPhaseValue = '';
|
||||
}
|
||||
|
||||
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||
|
||||
dispatch(moveTaskBetweenGroups({
|
||||
taskId: taskId,
|
||||
fromGroupId: currentGroup.id,
|
||||
toGroupId: targetGroup.id,
|
||||
taskUpdate: {
|
||||
|
||||
// Update the task entity
|
||||
dispatch(
|
||||
updateTask({
|
||||
id: taskId,
|
||||
changes: {
|
||||
phase: newPhaseValue,
|
||||
}
|
||||
}));
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Handle group movement ONLY if grouping by phase
|
||||
const groups = state.taskManagement.groups;
|
||||
const currentGrouping = state.taskManagement.grouping;
|
||||
|
||||
if (groups && groups.length > 0 && currentGrouping === 'phase') {
|
||||
// Find current group containing the task
|
||||
const currentGroup = groups.find(group => group.taskIds.includes(taskId));
|
||||
|
||||
// Find target group based on new phase value
|
||||
let targetGroup: any = null;
|
||||
|
||||
if (newPhaseValue) {
|
||||
// Find group by phase name
|
||||
targetGroup = groups.find(
|
||||
group => group.groupValue === newPhaseValue || group.title === newPhaseValue
|
||||
);
|
||||
} else {
|
||||
// Find "No Phase" or similar group
|
||||
targetGroup = groups.find(
|
||||
group =>
|
||||
group.groupValue === '' ||
|
||||
group.title.toLowerCase().includes('no phase') ||
|
||||
group.title.toLowerCase().includes('unassigned')
|
||||
);
|
||||
}
|
||||
|
||||
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||
dispatch(
|
||||
moveTaskBetweenGroups({
|
||||
taskId: taskId,
|
||||
fromGroupId: currentGroup.id,
|
||||
toGroupId: targetGroup.id,
|
||||
taskUpdate: {
|
||||
phase: newPhaseValue,
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log('🔧 No group movement needed for phase change');
|
||||
}
|
||||
} else {
|
||||
console.log('🔧 No group movement needed for phase change');
|
||||
console.log('🔧 Not grouped by phase, skipping group movement');
|
||||
}
|
||||
} else {
|
||||
console.log('🔧 Not grouped by phase, skipping group movement');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -487,11 +505,7 @@ export const useTaskSocketHandlers = () => {
|
||||
);
|
||||
|
||||
const handleStartDateChange = useCallback(
|
||||
(task: {
|
||||
id: string;
|
||||
parent_task: string | null;
|
||||
start_date: string;
|
||||
}) => {
|
||||
(task: { id: string; parent_task: string | null; start_date: string }) => {
|
||||
if (!task) return;
|
||||
|
||||
const taskWithProgress = {
|
||||
@@ -514,11 +528,7 @@ export const useTaskSocketHandlers = () => {
|
||||
);
|
||||
|
||||
const handleEstimationChange = useCallback(
|
||||
(task: {
|
||||
id: string;
|
||||
parent_task: string | null;
|
||||
estimation: number;
|
||||
}) => {
|
||||
(task: { id: string; parent_task: string | null; estimation: number }) => {
|
||||
if (!task) return;
|
||||
|
||||
const taskWithProgress = {
|
||||
@@ -532,11 +542,7 @@ export const useTaskSocketHandlers = () => {
|
||||
);
|
||||
|
||||
const handleTaskDescriptionChange = useCallback(
|
||||
(data: {
|
||||
id: string;
|
||||
parent_task: string;
|
||||
description: string;
|
||||
}) => {
|
||||
(data: { id: string; parent_task: string; description: string }) => {
|
||||
if (!data) return;
|
||||
dispatch(updateTaskDescription(data));
|
||||
},
|
||||
@@ -548,14 +554,59 @@ export const useTaskSocketHandlers = () => {
|
||||
if (!data) return;
|
||||
if (data.parent_task_id) {
|
||||
// Handle subtask creation
|
||||
dispatch(updateSubTasks(data));
|
||||
|
||||
const subtask: Task = {
|
||||
id: data.id || '',
|
||||
task_key: data.task_key || '',
|
||||
title: data.name || '',
|
||||
description: data.description || '',
|
||||
status: (data.status_category?.is_todo
|
||||
? 'todo'
|
||||
: data.status_category?.is_doing
|
||||
? 'doing'
|
||||
: data.status_category?.is_done
|
||||
? 'done'
|
||||
: 'todo') as 'todo' | 'doing' | 'done',
|
||||
priority: (data.priority_value === 3
|
||||
? 'critical'
|
||||
: data.priority_value === 2
|
||||
? 'high'
|
||||
: data.priority_value === 1
|
||||
? 'medium'
|
||||
: 'low') as 'critical' | 'high' | 'medium' | 'low',
|
||||
phase: data.phase_name || 'Development',
|
||||
progress: data.complete_ratio || 0,
|
||||
assignees: data.assignees?.map(a => a.team_member_id) || [],
|
||||
assignee_names: data.names || [],
|
||||
labels:
|
||||
data.labels?.map(l => ({
|
||||
id: l.id || '',
|
||||
name: l.name || '',
|
||||
color: l.color_code || '#1890ff',
|
||||
end: l.end,
|
||||
names: l.names,
|
||||
})) || [],
|
||||
dueDate: data.end_date,
|
||||
timeTracking: {
|
||||
estimated: (data.total_hours || 0) + (data.total_minutes || 0) / 60,
|
||||
logged: (data.time_spent?.hours || 0) + (data.time_spent?.minutes || 0) / 60,
|
||||
},
|
||||
customFields: {},
|
||||
createdAt: data.created_at || new Date().toISOString(),
|
||||
updatedAt: data.updated_at || new Date().toISOString(),
|
||||
order: data.sort_order || 0,
|
||||
parent_task_id: data.parent_task_id,
|
||||
is_sub_task: true,
|
||||
};
|
||||
dispatch(addSubtaskToParent({ subtask, parentTaskId: data.parent_task_id }));
|
||||
|
||||
// Also update enhanced kanban slice for subtask creation
|
||||
dispatch(updateEnhancedKanbanSubtask({
|
||||
sectionId: '',
|
||||
subtask: data,
|
||||
mode: 'add'
|
||||
}));
|
||||
dispatch(
|
||||
updateEnhancedKanbanSubtask({
|
||||
sectionId: '',
|
||||
subtask: data,
|
||||
mode: 'add',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Handle regular task creation - transform to Task format and add
|
||||
const task = {
|
||||
@@ -563,41 +614,50 @@ export const useTaskSocketHandlers = () => {
|
||||
task_key: data.task_key || '',
|
||||
title: data.name || '',
|
||||
description: data.description || '',
|
||||
status: (data.status_category?.is_todo ? 'todo' :
|
||||
data.status_category?.is_doing ? 'doing' :
|
||||
data.status_category?.is_done ? 'done' : 'todo') as 'todo' | 'doing' | 'done',
|
||||
priority: (data.priority_value === 3 ? 'critical' :
|
||||
data.priority_value === 2 ? 'high' :
|
||||
data.priority_value === 1 ? 'medium' : 'low') as 'critical' | 'high' | 'medium' | 'low',
|
||||
status: (data.status_category?.is_todo
|
||||
? 'todo'
|
||||
: data.status_category?.is_doing
|
||||
? 'doing'
|
||||
: data.status_category?.is_done
|
||||
? 'done'
|
||||
: 'todo') as 'todo' | 'doing' | 'done',
|
||||
priority: (data.priority_value === 3
|
||||
? 'critical'
|
||||
: data.priority_value === 2
|
||||
? 'high'
|
||||
: data.priority_value === 1
|
||||
? 'medium'
|
||||
: 'low') as 'critical' | 'high' | 'medium' | 'low',
|
||||
phase: data.phase_name || 'Development',
|
||||
progress: data.complete_ratio || 0,
|
||||
assignees: data.assignees?.map(a => a.team_member_id) || [],
|
||||
assignee_names: data.names || [],
|
||||
labels: data.labels?.map(l => ({
|
||||
id: l.id || '',
|
||||
name: l.name || '',
|
||||
color: l.color_code || '#1890ff',
|
||||
end: l.end,
|
||||
names: l.names
|
||||
})) || [],
|
||||
labels:
|
||||
data.labels?.map(l => ({
|
||||
id: l.id || '',
|
||||
name: l.name || '',
|
||||
color: l.color_code || '#1890ff',
|
||||
end: l.end,
|
||||
names: l.names,
|
||||
})) || [],
|
||||
dueDate: data.end_date,
|
||||
timeTracking: {
|
||||
estimated: (data.total_hours || 0) + ((data.total_minutes || 0) / 60),
|
||||
logged: ((data.time_spent?.hours || 0) + ((data.time_spent?.minutes || 0) / 60)),
|
||||
estimated: (data.total_hours || 0) + (data.total_minutes || 0) / 60,
|
||||
logged: (data.time_spent?.hours || 0) + (data.time_spent?.minutes || 0) / 60,
|
||||
},
|
||||
customFields: {},
|
||||
createdAt: data.created_at || new Date().toISOString(),
|
||||
updatedAt: data.updated_at || new Date().toISOString(),
|
||||
order: data.sort_order || 0,
|
||||
};
|
||||
|
||||
|
||||
// Extract the group UUID from the backend response based on current grouping
|
||||
let groupId: string | undefined;
|
||||
|
||||
|
||||
// Select the correct UUID based on current grouping
|
||||
// If currentGroupingV3 is null, default to 'status' since that's the most common grouping
|
||||
const grouping = currentGroupingV3 || 'status';
|
||||
|
||||
|
||||
if (grouping === 'status') {
|
||||
// For status grouping, use status field (which contains the status UUID)
|
||||
groupId = data.status;
|
||||
@@ -608,26 +668,24 @@ export const useTaskSocketHandlers = () => {
|
||||
// For phase grouping, use phase_id
|
||||
groupId = data.phase_id;
|
||||
}
|
||||
|
||||
|
||||
// Use addTaskToGroup with the actual group UUID
|
||||
dispatch(addTaskToGroup({ task, groupId }));
|
||||
|
||||
|
||||
// Also update enhanced kanban slice for regular task creation
|
||||
dispatch(addEnhancedKanbanTaskToGroup({
|
||||
sectionId: groupId || '',
|
||||
task: data
|
||||
}));
|
||||
dispatch(
|
||||
addEnhancedKanbanTaskToGroup({
|
||||
sectionId: groupId || '',
|
||||
task: data,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTaskProgressUpdated = useCallback(
|
||||
(data: {
|
||||
task_id: string;
|
||||
progress_value?: number;
|
||||
weight?: number;
|
||||
}) => {
|
||||
(data: { task_id: string; progress_value?: number; weight?: number }) => {
|
||||
if (!data || !taskGroups) return;
|
||||
|
||||
if (data.progress_value !== undefined) {
|
||||
@@ -651,16 +709,13 @@ export const useTaskSocketHandlers = () => {
|
||||
);
|
||||
|
||||
// Handler for TASK_ASSIGNEES_CHANGE (fallback event with limited data)
|
||||
const handleTaskAssigneesChange = useCallback(
|
||||
(data: { assigneeIds: string[] }) => {
|
||||
if (!data || !data.assigneeIds) return;
|
||||
|
||||
// This event only provides assignee IDs, so we update what we can
|
||||
// The full assignee data will come from QUICK_ASSIGNEES_UPDATE
|
||||
// console.log('🔄 Task assignees change (limited data):', data);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleTaskAssigneesChange = useCallback((data: { assigneeIds: string[] }) => {
|
||||
if (!data || !data.assigneeIds) return;
|
||||
|
||||
// This event only provides assignee IDs, so we update what we can
|
||||
// The full assignee data will come from QUICK_ASSIGNEES_UPDATE
|
||||
// console.log('🔄 Task assignees change (limited data):', data);
|
||||
}, []);
|
||||
|
||||
// Register socket event listeners
|
||||
useEffect(() => {
|
||||
@@ -678,9 +733,18 @@ export const useTaskSocketHandlers = () => {
|
||||
{ event: SocketEvents.TASK_NAME_CHANGE.toString(), handler: handleTaskNameChange },
|
||||
{ event: SocketEvents.TASK_PHASE_CHANGE.toString(), handler: handlePhaseChange },
|
||||
{ event: SocketEvents.TASK_START_DATE_CHANGE.toString(), handler: handleStartDateChange },
|
||||
{ event: SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), handler: handleTaskSubscribersChange },
|
||||
{ event: SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), handler: handleEstimationChange },
|
||||
{ event: SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), handler: handleTaskDescriptionChange },
|
||||
{
|
||||
event: SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(),
|
||||
handler: handleTaskSubscribersChange,
|
||||
},
|
||||
{
|
||||
event: SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(),
|
||||
handler: handleEstimationChange,
|
||||
},
|
||||
{
|
||||
event: SocketEvents.TASK_DESCRIPTION_CHANGE.toString(),
|
||||
handler: handleTaskDescriptionChange,
|
||||
},
|
||||
{ event: SocketEvents.QUICK_TASK.toString(), handler: handleNewTaskReceived },
|
||||
{ event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgressUpdated },
|
||||
];
|
||||
@@ -714,4 +778,4 @@ export const useTaskSocketHandlers = () => {
|
||||
handleNewTaskReceived,
|
||||
handleTaskProgressUpdated,
|
||||
]);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,14 +33,12 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
|
||||
const clearTimerInterval = useCallback(() => {
|
||||
if (intervalRef.current) {
|
||||
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [taskId]);
|
||||
|
||||
const resetTimer = useCallback(() => {
|
||||
|
||||
clearTimerInterval();
|
||||
setTimeString(DEFAULT_TIME_LEFT);
|
||||
setLocalStarted(false);
|
||||
@@ -48,15 +46,11 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
|
||||
// Timer management effect
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
if (started && localStarted && reduxStartTime) {
|
||||
|
||||
clearTimerInterval();
|
||||
timerTick();
|
||||
intervalRef.current = setInterval(timerTick, 1000);
|
||||
} else {
|
||||
|
||||
clearTimerInterval();
|
||||
setTimeString(DEFAULT_TIME_LEFT);
|
||||
if (started !== localStarted) {
|
||||
@@ -65,7 +59,6 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
clearTimerInterval();
|
||||
};
|
||||
}, [reduxStartTime, started, localStarted, timerTick, clearTimerInterval, taskId]);
|
||||
@@ -73,11 +66,9 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
// Initialize timer only on first mount if Redux is unset
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && initialStartTime && reduxStartTime === undefined) {
|
||||
|
||||
dispatch(updateTaskTimeTracking({ taskId, timeTracking: initialStartTime }));
|
||||
setLocalStarted(true);
|
||||
} else if (reduxStartTime && !localStarted) {
|
||||
|
||||
setLocalStarted(true);
|
||||
}
|
||||
hasInitialized.current = true; // Mark as initialized
|
||||
@@ -87,7 +78,7 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
if (started || !taskId) return;
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
dispatch(updateTaskTimeTracking({ taskId, timeTracking: now }));
|
||||
setLocalStarted(true);
|
||||
socket?.emit(SocketEvents.TASK_TIMER_START.toString(), JSON.stringify({ task_id: taskId }));
|
||||
@@ -98,7 +89,7 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
|
||||
const handleStopTimer = useCallback(() => {
|
||||
if (!taskId) return;
|
||||
|
||||
|
||||
resetTimer();
|
||||
socket?.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({ task_id: taskId }));
|
||||
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null }));
|
||||
@@ -112,7 +103,6 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
try {
|
||||
const { task_id } = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
if (task_id === taskId) {
|
||||
|
||||
resetTimer();
|
||||
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null }));
|
||||
}
|
||||
@@ -126,7 +116,7 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
if (task_id === taskId && start_time) {
|
||||
const time = typeof start_time === 'number' ? start_time : parseInt(start_time);
|
||||
|
||||
|
||||
dispatch(updateTaskTimeTracking({ taskId, timeTracking: time }));
|
||||
setLocalStarted(true);
|
||||
}
|
||||
@@ -150,4 +140,4 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
handleStartTimer,
|
||||
handleStopTimer,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -25,16 +25,16 @@ export const useTranslationPreloader = (
|
||||
const loadTranslations = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
// Ensure translations are loaded
|
||||
await ensureTranslationsLoaded(namespaces);
|
||||
|
||||
|
||||
// Wait for i18next to be ready
|
||||
if (!ready) {
|
||||
// If i18next is not ready, wait a bit and check again
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
|
||||
if (isMounted) {
|
||||
setIsLoaded(true);
|
||||
setIsLoading(false);
|
||||
@@ -74,4 +74,4 @@ export const useBulkActionTranslations = () => {
|
||||
*/
|
||||
export const useTaskManagementTranslations = () => {
|
||||
return useTranslationPreloader(['task-management', 'tasks/task-table-bulk-actions']);
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user