onNavigateToTask?.(parentTask.id)}
+ onClick={() => onNavigateToTask?.(displayParentTask.id)}
className="shadow-sm"
/>
diff --git a/frontend/src/hooks/useAttemptCreation.ts b/frontend/src/hooks/useAttemptCreation.ts
index 364b55ea..7bacf3ed 100644
--- a/frontend/src/hooks/useAttemptCreation.ts
+++ b/frontend/src/hooks/useAttemptCreation.ts
@@ -1,13 +1,14 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { useNavigate, useParams } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
import { attemptsApi } from '@/lib/api';
+import { useTaskViewManager } from '@/hooks/useTaskViewManager';
import type { TaskAttempt } from 'shared/types';
import type { ExecutorProfileId } from 'shared/types';
export function useAttemptCreation(taskId: string) {
const queryClient = useQueryClient();
- const navigate = useNavigate();
const { projectId } = useParams<{ projectId: string }>();
+ const { navigateToAttempt } = useTaskViewManager();
const mutation = useMutation({
mutationFn: ({
@@ -31,10 +32,7 @@ export function useAttemptCreation(taskId: string) {
// Navigate to new attempt (triggers polling switch)
if (projectId) {
- navigate(
- `/projects/${projectId}/tasks/${taskId}/attempts/${newAttempt.id}`,
- { replace: true }
- );
+ navigateToAttempt(projectId, taskId, newAttempt.id);
}
},
});
diff --git a/frontend/src/hooks/useTaskMutations.ts b/frontend/src/hooks/useTaskMutations.ts
index bef69d1a..1dc60549 100644
--- a/frontend/src/hooks/useTaskMutations.ts
+++ b/frontend/src/hooks/useTaskMutations.ts
@@ -1,6 +1,6 @@
-import { useNavigate } from 'react-router-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { tasksApi } from '@/lib/api';
+import { useTaskViewManager } from '@/hooks/useTaskViewManager';
import type {
CreateTask,
CreateAndStartTaskRequest,
@@ -10,8 +10,8 @@ import type {
} from 'shared/types';
export function useTaskMutations(projectId?: string) {
- const navigate = useNavigate();
const queryClient = useQueryClient();
+ const { navigateToTask } = useTaskViewManager();
const invalidateQueries = (taskId?: string) => {
queryClient.invalidateQueries({ queryKey: ['tasks', projectId] });
@@ -24,9 +24,9 @@ export function useTaskMutations(projectId?: string) {
mutationFn: (data: CreateTask) => tasksApi.create(data),
onSuccess: (createdTask: Task) => {
invalidateQueries();
- navigate(`/projects/${projectId}/tasks/${createdTask.id}`, {
- replace: true,
- });
+ if (projectId) {
+ navigateToTask(projectId, createdTask.id);
+ }
},
onError: (err) => {
console.error('Failed to create task:', err);
@@ -38,9 +38,9 @@ export function useTaskMutations(projectId?: string) {
tasksApi.createAndStart(data),
onSuccess: (createdTask: TaskWithAttemptStatus) => {
invalidateQueries();
- navigate(`/projects/${projectId}/tasks/${createdTask.id}`, {
- replace: true,
- });
+ if (projectId) {
+ navigateToTask(projectId, createdTask.id);
+ }
},
onError: (err) => {
console.error('Failed to create and start task:', err);
diff --git a/frontend/src/hooks/useTaskViewManager.ts b/frontend/src/hooks/useTaskViewManager.ts
new file mode 100644
index 00000000..44c0131d
--- /dev/null
+++ b/frontend/src/hooks/useTaskViewManager.ts
@@ -0,0 +1,89 @@
+import { useCallback } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+
+interface NavigateOptions {
+ attemptId?: string;
+ fullscreen?: boolean;
+ replace?: boolean;
+ state?: unknown;
+}
+
+/**
+ * Centralised hook for task routing and fullscreen controls
+ * Exposes navigation helpers alongside fullscreen state/toggles
+ */
+export function useTaskViewManager() {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const isFullscreen = location.pathname.endsWith('/full');
+
+ const toggleFullscreen = useCallback(
+ (fullscreen: boolean) => {
+ const currentPath = location.pathname;
+ let targetPath: string;
+
+ if (fullscreen) {
+ targetPath = currentPath.endsWith('/full')
+ ? currentPath
+ : `${currentPath}/full`;
+ } else {
+ targetPath = currentPath.endsWith('/full')
+ ? currentPath.slice(0, -5)
+ : currentPath;
+ }
+
+ navigate(targetPath, { replace: true });
+ },
+ [location.pathname, navigate]
+ );
+
+ const buildTaskUrl = useCallback(
+ (projectId: string, taskId: string, options?: NavigateOptions) => {
+ const baseUrl = `/projects/${projectId}/tasks/${taskId}`;
+ const attemptUrl = options?.attemptId
+ ? `/attempts/${options.attemptId}`
+ : '';
+ const fullscreenSuffix =
+ (options?.fullscreen ?? isFullscreen) ? '/full' : '';
+
+ return `${baseUrl}${attemptUrl}${fullscreenSuffix}`;
+ },
+ [isFullscreen]
+ );
+
+ const navigateToTask = useCallback(
+ (projectId: string, taskId: string, options?: NavigateOptions) => {
+ const targetUrl = buildTaskUrl(projectId, taskId, options);
+
+ navigate(targetUrl, {
+ replace: options?.replace ?? true,
+ state: options?.state,
+ });
+ },
+ [buildTaskUrl, navigate]
+ );
+
+ const navigateToAttempt = useCallback(
+ (
+ projectId: string,
+ taskId: string,
+ attemptId: string,
+ options?: Omit
+ ) => {
+ navigateToTask(projectId, taskId, {
+ ...options,
+ attemptId,
+ });
+ },
+ [navigateToTask]
+ );
+
+ return {
+ isFullscreen,
+ toggleFullscreen,
+ buildTaskUrl,
+ navigateToTask,
+ navigateToAttempt,
+ };
+}
diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx
index daaa3009..3c9ccd1a 100644
--- a/frontend/src/pages/project-tasks.tsx
+++ b/frontend/src/pages/project-tasks.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState, useMemo } from 'react';
-import { useNavigate, useParams, useLocation } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { AlertTriangle, Plus } from 'lucide-react';
@@ -9,6 +9,7 @@ import { openTaskForm } from '@/lib/openTaskForm';
import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts';
import { useSearch } from '@/contexts/search-context';
import { useQuery } from '@tanstack/react-query';
+import { useTaskViewManager } from '@/hooks/useTaskViewManager';
import {
getKanbanSectionClasses,
@@ -32,7 +33,6 @@ export function ProjectTasks() {
attemptId?: string;
}>();
const navigate = useNavigate();
- const location = useLocation();
const [project, setProject] = useState(null);
const [error, setError] = useState(null);
@@ -60,8 +60,9 @@ export function ProjectTasks() {
const [selectedTask, setSelectedTask] = useState(null);
const [isPanelOpen, setIsPanelOpen] = useState(false);
- // Fullscreen state from pathname
- const isFullscreen = location.pathname.endsWith('/full');
+ // Fullscreen state using custom hook
+ const { isFullscreen, navigateToTask, navigateToAttempt } =
+ useTaskViewManager();
// Attempts fetching (only when task is selected)
const { data: attempts = [] } = useQuery({
@@ -86,14 +87,13 @@ export function ProjectTasks() {
(attempt: TaskAttempt | null) => {
if (!selectedTask) return;
- const baseUrl = `/projects/${projectId}/tasks/${selectedTask.id}`;
- const attemptUrl = attempt ? `/attempts/${attempt.id}` : '';
- const fullSuffix = isFullscreen ? '/full' : '';
- const fullUrl = `${baseUrl}${attemptUrl}${fullSuffix}`;
-
- navigate(fullUrl, { replace: true });
+ if (attempt) {
+ navigateToAttempt(projectId!, selectedTask.id, attempt.id);
+ } else {
+ navigateToTask(projectId!, selectedTask.id);
+ }
},
- [navigate, projectId, selectedTask, isFullscreen]
+ [navigateToTask, navigateToAttempt, projectId, selectedTask]
);
// Stream tasks for this project
@@ -178,16 +178,14 @@ export function ProjectTasks() {
);
const handleViewTaskDetails = useCallback(
- (task: Task, attemptIdToShow?: string) => {
- // setSelectedTask(task);
- // setIsPanelOpen(true);
- // Update URL to include task ID and optionally attempt ID
- const targetUrl = attemptIdToShow
- ? `/projects/${projectId}/tasks/${task.id}/attempts/${attemptIdToShow}`
- : `/projects/${projectId}/tasks/${task.id}`;
- navigate(targetUrl, { replace: true });
+ (task: Task, attemptIdToShow?: string, fullscreen?: boolean) => {
+ if (attemptIdToShow) {
+ navigateToAttempt(projectId!, task.id, attemptIdToShow, { fullscreen });
+ } else {
+ navigateToTask(projectId!, task.id, { fullscreen });
+ }
},
- [projectId, navigate]
+ [projectId, navigateToTask, navigateToAttempt]
);
const handleDragEnd = useCallback(
@@ -312,22 +310,14 @@ export function ProjectTasks() {
onNavigateToTask={(taskId) => {
const task = tasksById[taskId];
if (task) {
- handleViewTaskDetails(task);
+ handleViewTaskDetails(task, undefined, true);
}
}}
isFullScreen={isFullscreen}
- setFullScreen={
- selectedAttempt
- ? (fullscreen) => {
- const baseUrl = `/projects/${projectId}/tasks/${selectedTask!.id}/attempts/${selectedAttempt.id}`;
- const fullUrl = fullscreen ? `${baseUrl}/full` : baseUrl;
- navigate(fullUrl, { replace: true });
- }
- : undefined
- }
selectedAttempt={selectedAttempt}
attempts={attempts}
setSelectedAttempt={setSelectedAttempt}
+ tasksById={tasksById}
/>
)}