diff --git a/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts b/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts index d8a92747..31651cee 100644 --- a/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts +++ b/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts @@ -49,22 +49,77 @@ export function useProjectFeatures(projectId: string | undefined) { }); } -// Create project mutation +// Create project mutation with optimistic updates export function useCreateProject() { const queryClient = useQueryClient(); const { showToast } = useToast(); return useMutation({ mutationFn: (projectData: CreateProjectRequest) => projectService.createProject(projectData), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); - showToast("Project created successfully!", "success"); + onMutate: async (newProjectData) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ queryKey: projectKeys.lists() }); + + // Snapshot the previous value + const previousProjects = queryClient.getQueryData(projectKeys.lists()); + + // Create optimistic project with temporary ID + const optimisticProject: Project = { + id: `temp-${Date.now()}`, // Temporary ID until real one comes back + title: newProjectData.title, + description: newProjectData.description, + github_repo: newProjectData.github_repo, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + prd: undefined, + features: [], + data: undefined, + docs: [], + pinned: false, + }; + + // Optimistically add the new project + queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => { + if (!old) return [optimisticProject]; + // Add new project at the beginning of the list + return [optimisticProject, ...old]; + }); + + return { previousProjects }; }, - onError: (error, variables) => { + onError: (error, variables, context) => { const errorMessage = error instanceof Error ? error.message : String(error); console.error("Failed to create project:", error, { variables }); + + // Rollback on error + if (context?.previousProjects) { + queryClient.setQueryData(projectKeys.lists(), context.previousProjects); + } + showToast(`Failed to create project: ${errorMessage}`, "error"); }, + onSuccess: (response) => { + // Extract the actual project from the response + const newProject = response.project; + + // Replace optimistic project with real one from server + queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => { + if (!old) return [newProject]; + // Replace temp project with real one + return old.map(project => + project.id.startsWith('temp-') ? newProject : project + ).filter((project, index, self) => + // Remove any duplicates just in case + index === self.findIndex(p => p.id === project.id) + ); + }); + + showToast("Project created successfully!", "success"); + }, + onSettled: () => { + // Always refetch to ensure consistency after operation completes + queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); + }, }); } diff --git a/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts b/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts index e44ee25e..8507bc37 100644 --- a/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts +++ b/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts @@ -23,25 +23,69 @@ export function useProjectTasks(projectId: string | undefined, enabled = true) { }); } -// Create task mutation +// Create task mutation with optimistic updates export function useCreateTask() { const queryClient = useQueryClient(); const { showToast } = useToast(); return useMutation({ mutationFn: (taskData: CreateTaskRequest) => taskService.createTask(taskData), - onSuccess: (_data, variables) => { - // Invalidate tasks for the project - queryClient.invalidateQueries({ - queryKey: taskKeys.all(variables.project_id), + onMutate: async (newTaskData) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ queryKey: taskKeys.all(newTaskData.project_id) }); + + // Snapshot the previous value + const previousTasks = queryClient.getQueryData(taskKeys.all(newTaskData.project_id)); + + // Create optimistic task with temporary ID + const optimisticTask: Task = { + id: `temp-${Date.now()}`, // Temporary ID until real one comes back + ...newTaskData, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + deleted_at: null, + subtasks: [], + // Ensure all required fields have defaults + task_order: newTaskData.task_order ?? 100, + status: newTaskData.status ?? "todo", + assignee: newTaskData.assignee ?? "User", + } as Task; + + // Optimistically add the new task + queryClient.setQueryData(taskKeys.all(newTaskData.project_id), (old: Task[] | undefined) => { + if (!old) return [optimisticTask]; + return [...old, optimisticTask]; + }); + + return { previousTasks }; + }, + onError: (error, variables, context) => { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("Failed to create task:", error, { variables }); + // Rollback on error + if (context?.previousTasks) { + queryClient.setQueryData(taskKeys.all(variables.project_id), context.previousTasks); + } + showToast(`Failed to create task: ${errorMessage}`, "error"); + }, + onSuccess: (data, variables) => { + // Replace optimistic task with real one from server + queryClient.setQueryData(taskKeys.all(variables.project_id), (old: Task[] | undefined) => { + if (!old) return [data]; + // Remove temp task and add real one + return old.map(task => + task.id.startsWith('temp-') ? data : task + ).filter((task, index, self) => + // Remove any duplicates just in case + index === self.findIndex(t => t.id === task.id) + ); }); queryClient.invalidateQueries({ queryKey: taskKeys.counts() }); showToast("Task created successfully", "success"); }, - onError: (error, variables) => { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error("Failed to create task:", error, { variables }); - showToast(`Failed to create task: ${errorMessage}`, "error"); + onSettled: (_data, _error, variables) => { + // Always refetch to ensure consistency after operation completes + queryClient.invalidateQueries({ queryKey: taskKeys.all(variables.project_id) }); }, }); }