mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-24 02:39:17 -05:00
feat: add optimistic updates for task and project creation
- Implement optimistic updates for useCreateTask mutation - Tasks now appear instantly with temporary ID - Replaced with real task from server on success - Rollback on error with proper error handling - Implement optimistic updates for useCreateProject mutation - Projects appear immediately in the list - Temporary ID replaced with real one on success - Proper rollback on failure - Both mutations follow existing patterns from update/delete operations - Provides instant visual feedback improving perceived performance - Eliminates 2-3 second delay before items appear in UI
This commit is contained in:
@@ -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() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user