diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index 41c40d8d..5eb5a53a 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -36,6 +36,8 @@ import { taskPropsEqual } from './task-row-utils'; import './task-row-optimized.css'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { setSelectedTaskId, setShowTaskDrawer, fetchTask } from '@/features/task-drawer/task-drawer.slice'; interface TaskRowProps { task: Task; @@ -184,6 +186,9 @@ const TaskRow: React.FC = React.memo(({ // PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible const { socket, connected } = useSocket(); + // Redux dispatch + const dispatch = useAppDispatch(); + // Edit task name state const [editTaskName, setEditTaskName] = useState(false); const [taskName, setTaskName] = useState(task.title || ''); @@ -345,6 +350,15 @@ const TaskRow: React.FC = React.memo(({ } }, [showAddSubtask]); + // Handle opening task drawer + const handleOpenTask = useCallback(() => { + if (!task.id) return; + dispatch(setSelectedTaskId(task.id)); + dispatch(setShowTaskDrawer(true)); + // Fetch task data + dispatch(fetchTask({ taskId: task.id, projectId })); + }, [task.id, projectId, dispatch]); + // Optimized date handling with better memoization const dateValues = useMemo(() => ({ start: task.startDate ? dayjs(task.startDate) : undefined, @@ -596,8 +610,8 @@ const TaskRow: React.FC = React.memo(({ {!editTaskName && (
{/* Subtasks count */} - {task.subtasks_count && task.subtasks_count > 0 && ( - + {(task as any).subtasks_count && (task as any).subtasks_count > 0 && ( +
= React.memo(({ handleToggleSubtasks?.(); }} > - {task.subtasks_count} + {(task as any).subtasks_count}
)} {/* Comments indicator */} - {task.comments_count && task.comments_count > 0 && ( - + {(task as any).comments_count && (task as any).comments_count > 0 && ( +
= React.memo(({ style={{ fontSize: '10px', border: '1px solid' }} > - {task.comments_count} + {(task as any).comments_count}
)} {/* Attachments indicator */} - {task.attachments_count && task.attachments_count > 0 && ( - + {(task as any).attachments_count && (task as any).attachments_count > 0 && ( +
= React.memo(({ style={{ fontSize: '10px', border: '1px solid' }} > - {task.attachments_count} + {(task as any).attachments_count}
)} @@ -661,7 +675,7 @@ const TaskRow: React.FC = React.memo(({ onClick={(e) => { e.preventDefault(); e.stopPropagation(); - // Handle opening task drawer + handleOpenTask(); }} className={`flex items-center gap-1 px-2 py-1 rounded border transition-all duration-200 text-xs font-medium ${ isDarkMode @@ -956,10 +970,9 @@ const TaskRow: React.FC = React.memo(({
diff --git a/worklenz-frontend/src/hooks/useTaskDrawerUrlSync.ts b/worklenz-frontend/src/hooks/useTaskDrawerUrlSync.ts index 4eb59771..52674d96 100644 --- a/worklenz-frontend/src/hooks/useTaskDrawerUrlSync.ts +++ b/worklenz-frontend/src/hooks/useTaskDrawerUrlSync.ts @@ -18,16 +18,17 @@ const useTaskDrawerUrlSync = () => { // Use a ref to track whether we're in the process of closing the drawer const isClosingDrawer = useRef(false); - // Use a ref to track if we've already processed the initial URL - const initialUrlProcessed = useRef(false); // Use a ref to track the last task ID we processed const lastProcessedTaskId = useRef(null); + // Use a ref to track if we should ignore URL changes (when programmatically updating) + const shouldIgnoreUrlChange = useRef(false); // Function to clear the task parameter from URL const clearTaskFromUrl = useCallback(() => { if (searchParams.has('task')) { // 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); @@ -36,40 +37,52 @@ const useTaskDrawerUrlSync = () => { // Update the URL without triggering a navigation setSearchParams(newParams, { replace: true }); - // Reset the flag after a short delay + // Reset the flags after a short delay setTimeout(() => { isClosingDrawer.current = false; - }, 200); + shouldIgnoreUrlChange.current = false; + }, 300); // Increased timeout to ensure proper cleanup } }, [searchParams, setSearchParams]); - // Check for task ID in URL when component mounts + // Check for task ID in URL when it changes useEffect(() => { - // Only process the URL once on initial mount - if (!initialUrlProcessed.current) { - const taskIdFromUrl = searchParams.get('task'); - if (taskIdFromUrl && !showTaskDrawer && projectId && !isClosingDrawer.current) { - lastProcessedTaskId.current = taskIdFromUrl; - dispatch(setSelectedTaskId(taskIdFromUrl)); - dispatch(setShowTaskDrawer(true)); - - // Fetch task data - dispatch(fetchTask({ taskId: taskIdFromUrl, projectId })); - } - initialUrlProcessed.current = true; + // 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) { + lastProcessedTaskId.current = taskIdFromUrl; + dispatch(setSelectedTaskId(taskIdFromUrl)); + dispatch(setShowTaskDrawer(true)); + + // Fetch task data + dispatch(fetchTask({ taskId: taskIdFromUrl, projectId })); } - }, [searchParams, dispatch, showTaskDrawer, projectId]); + }, [searchParams, showTaskDrawer, projectId, selectedTaskId, dispatch]); // Update URL when task drawer state changes useEffect(() => { - // Don't update URL if we're in the process of closing - if (isClosingDrawer.current) return; + // 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 @@ -78,12 +91,27 @@ const useTaskDrawerUrlSync = () => { // Update the URL without triggering a navigation setSearchParams(newParams, { replace: true }); - } else if (!showTaskDrawer && searchParams.has('task')) { - // Remove task ID from URL when drawer is closed + + // Reset the flag after a short delay + setTimeout(() => { + shouldIgnoreUrlChange.current = false; + }, 100); + } + }, [showTaskDrawer, selectedTaskId, searchParams, setSearchParams]); + + // Separate effect to handle URL clearing when drawer is closed + 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 clearTaskFromUrl(); lastProcessedTaskId.current = null; } - }, [showTaskDrawer, selectedTaskId, searchParams, setSearchParams, clearTaskFromUrl]); + }, [showTaskDrawer, searchParams, selectedTaskId, clearTaskFromUrl]); return { clearTaskFromUrl }; };