mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-07 15:18:14 -05:00
Fixing optimistic updates when switching tabs
This commit is contained in:
@@ -640,10 +640,10 @@ export const DocsTab = ({
|
|||||||
document_type: template.document_type
|
document_type: template.document_type
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace temporary document with the real one
|
// Force refresh to get the real document from server
|
||||||
setDocuments(prev => prev.map(doc =>
|
await loadProjectDocuments();
|
||||||
doc.id === tempDocument.id ? newDocument : doc
|
|
||||||
));
|
// Select the newly created document
|
||||||
setSelectedDocument(newDocument);
|
setSelectedDocument(newDocument);
|
||||||
|
|
||||||
console.log('Document created successfully via API:', newDocument);
|
console.log('Document created successfully via API:', newDocument);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { Table, LayoutGrid, Plus, Wifi, WifiOff, List, Trash2 } from 'lucide-react';
|
import { Table, LayoutGrid, Plus, Wifi, WifiOff, List, Trash2 } from 'lucide-react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
@@ -8,6 +8,7 @@ import { getGlobalOperationTracker } from '../../utils/operationTracker';
|
|||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
|
|
||||||
import { useTaskSocket } from '../../hooks/useTaskSocket';
|
import { useTaskSocket } from '../../hooks/useTaskSocket';
|
||||||
|
import { useOptimisticUpdates } from '../../hooks/useOptimisticUpdates';
|
||||||
import type { CreateTaskRequest, UpdateTaskRequest, DatabaseTaskStatus } from '../../types/project';
|
import type { CreateTaskRequest, UpdateTaskRequest, DatabaseTaskStatus } from '../../types/project';
|
||||||
import { WebSocketState } from '../../services/socketIOService';
|
import { WebSocketState } from '../../services/socketIOService';
|
||||||
import { TaskTableView, Task } from './TaskTableView';
|
import { TaskTableView, Task } from './TaskTableView';
|
||||||
@@ -85,16 +86,26 @@ export const TasksTab = ({
|
|||||||
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
|
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
// Track local updates to prevent echo from WebSocket
|
// Use optimistic updates hook for proper echo suppression
|
||||||
const [localUpdates, setLocalUpdates] = useState<Record<string, number>>({});
|
const { addPendingUpdate, isPendingUpdate, removePendingUpdate } = useOptimisticUpdates<Task>();
|
||||||
|
|
||||||
// Track recently deleted tasks to prevent race conditions
|
// Track recently deleted tasks to prevent race conditions
|
||||||
const [recentlyDeletedIds, setRecentlyDeletedIds] = useState<Set<string>>(new Set());
|
const [recentlyDeletedIds, setRecentlyDeletedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Initialize tasks
|
// Track the project ID to detect when we switch projects
|
||||||
|
const lastProjectId = useRef(projectId);
|
||||||
|
|
||||||
|
// Initialize tasks when component mounts or project changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTasks(initialTasks);
|
// If project changed, always reinitialize
|
||||||
}, [initialTasks]);
|
if (lastProjectId.current !== projectId) {
|
||||||
|
setTasks(initialTasks);
|
||||||
|
lastProjectId.current = projectId;
|
||||||
|
} else if (tasks.length === 0 && initialTasks.length > 0) {
|
||||||
|
// Only initialize if we have no tasks but received initial tasks
|
||||||
|
setTasks(initialTasks);
|
||||||
|
}
|
||||||
|
}, [initialTasks, projectId]);
|
||||||
|
|
||||||
// Load project features on component mount
|
// Load project features on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -113,19 +124,8 @@ export const TasksTab = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is an echo of a local update
|
// Check if this is an echo of a local update
|
||||||
const localUpdateTime = localUpdates[updatedTask.id];
|
if (isPendingUpdate(updatedTask.id, mappedTask)) {
|
||||||
console.log(`[Socket] Checking for echo - Task ${updatedTask.id}, localUpdateTime: ${localUpdateTime}, current time: ${Date.now()}, diff: ${localUpdateTime ? Date.now() - localUpdateTime : 'N/A'}`);
|
|
||||||
|
|
||||||
if (localUpdateTime && Date.now() - localUpdateTime < 5000) { // Increased window to 5 seconds
|
|
||||||
console.log('[Socket] Skipping echo update for locally updated task:', updatedTask.id);
|
console.log('[Socket] Skipping echo update for locally updated task:', updatedTask.id);
|
||||||
// Clean up the local update marker after the echo protection window
|
|
||||||
setTimeout(() => {
|
|
||||||
setLocalUpdates(prev => {
|
|
||||||
const newUpdates = { ...prev };
|
|
||||||
delete newUpdates[updatedTask.id];
|
|
||||||
return newUpdates;
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[Socket] Not an echo, applying update for task:', updatedTask.id);
|
console.log('[Socket] Not an echo, applying update for task:', updatedTask.id);
|
||||||
@@ -155,7 +155,7 @@ export const TasksTab = ({
|
|||||||
setTimeout(() => onTasksChange(updated), 0);
|
setTimeout(() => onTasksChange(updated), 0);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds, localUpdates]);
|
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds, isPendingUpdate]);
|
||||||
|
|
||||||
const handleTaskCreated = useCallback((message: any) => {
|
const handleTaskCreated = useCallback((message: any) => {
|
||||||
const newTask = message.data || message;
|
const newTask = message.data || message;
|
||||||
@@ -286,6 +286,30 @@ export const TasksTab = ({
|
|||||||
setEditingTask(task);
|
setEditingTask(task);
|
||||||
|
|
||||||
setIsSavingTask(true);
|
setIsSavingTask(true);
|
||||||
|
|
||||||
|
// Store original task for rollback
|
||||||
|
const originalTask = task.id ? tasks.find(t => t.id === task.id) : null;
|
||||||
|
|
||||||
|
// OPTIMISTIC UPDATE: Update UI immediately for existing tasks
|
||||||
|
if (task.id) {
|
||||||
|
setTasks(prev => {
|
||||||
|
const updated = prev.map(t =>
|
||||||
|
t.id === task.id ? task : t
|
||||||
|
);
|
||||||
|
// Notify parent of the change
|
||||||
|
onTasksChange(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as pending update to prevent echo
|
||||||
|
addPendingUpdate({
|
||||||
|
id: task.id,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
data: task,
|
||||||
|
operation: 'update'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let parentTaskId = task.id;
|
let parentTaskId = task.id;
|
||||||
|
|
||||||
@@ -323,6 +347,22 @@ export const TasksTab = ({
|
|||||||
closeModal();
|
closeModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save task:', error);
|
console.error('Failed to save task:', error);
|
||||||
|
|
||||||
|
// Rollback optimistic update on error
|
||||||
|
if (task.id && originalTask) {
|
||||||
|
setTasks(prev => {
|
||||||
|
const updated = prev.map(t =>
|
||||||
|
t.id === task.id ? originalTask : t
|
||||||
|
);
|
||||||
|
// Notify parent of the rollback
|
||||||
|
onTasksChange(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear pending update tracking
|
||||||
|
removePendingUpdate(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
alert(`Failed to save task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
alert(`Failed to save task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSavingTask(false);
|
setIsSavingTask(false);
|
||||||
@@ -503,17 +543,17 @@ export const TasksTab = ({
|
|||||||
});
|
});
|
||||||
console.log(`[TasksTab] Optimistically updated UI for task ${taskId}`);
|
console.log(`[TasksTab] Optimistically updated UI for task ${taskId}`);
|
||||||
|
|
||||||
// Mark this update as local to prevent echo when socket update arrives
|
// Mark as pending update to prevent echo when socket update arrives
|
||||||
const updateTime = Date.now();
|
const taskToUpdate = tasks.find(t => t.id === taskId);
|
||||||
console.log(`[TasksTab] Marking update as local for task ${taskId} at time ${updateTime}`);
|
if (taskToUpdate) {
|
||||||
setLocalUpdates(prev => {
|
const updatedTask = { ...taskToUpdate, status: newStatus, task_order: newOrder };
|
||||||
const newUpdates = {
|
addPendingUpdate({
|
||||||
...prev,
|
id: taskId,
|
||||||
[taskId]: updateTime
|
timestamp: Date.now(),
|
||||||
};
|
data: updatedTask,
|
||||||
console.log('[TasksTab] LocalUpdates state:', newUpdates);
|
operation: 'update'
|
||||||
return newUpdates;
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Then update the backend
|
// Then update the backend
|
||||||
@@ -535,12 +575,8 @@ export const TasksTab = ({
|
|||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear the local update marker
|
// Clear the pending update marker
|
||||||
setLocalUpdates(prev => {
|
removePendingUpdate(taskId);
|
||||||
const newUpdates = { ...prev };
|
|
||||||
delete newUpdates[taskId];
|
|
||||||
return newUpdates;
|
|
||||||
});
|
|
||||||
|
|
||||||
alert(`Failed to move task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
alert(`Failed to move task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
@@ -613,10 +649,25 @@ export const TasksTab = ({
|
|||||||
|
|
||||||
// Inline task creation function
|
// Inline task creation function
|
||||||
const createTaskInline = async (newTask: Omit<Task, 'id'>) => {
|
const createTaskInline = async (newTask: Omit<Task, 'id'>) => {
|
||||||
|
// Create temporary task with a temp ID for optimistic update
|
||||||
|
const tempId = `temp-${Date.now()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Auto-assign next order number if not provided
|
// Auto-assign next order number if not provided
|
||||||
const nextOrder = newTask.task_order || getNextOrderForStatus(newTask.status);
|
const nextOrder = newTask.task_order || getNextOrderForStatus(newTask.status);
|
||||||
|
|
||||||
|
const tempTask: Task = {
|
||||||
|
...newTask,
|
||||||
|
id: tempId,
|
||||||
|
task_order: nextOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
// OPTIMISTIC UPDATE: Add to UI immediately
|
||||||
|
setTasks(prev => [...prev, tempTask]);
|
||||||
|
|
||||||
|
// Notify parent component of the change
|
||||||
|
onTasksChange([...tasks, tempTask]);
|
||||||
|
|
||||||
const createData: CreateTaskRequest = {
|
const createData: CreateTaskRequest = {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
title: newTask.title,
|
title: newTask.title,
|
||||||
@@ -628,13 +679,30 @@ export const TasksTab = ({
|
|||||||
...(newTask.featureColor && { featureColor: newTask.featureColor })
|
...(newTask.featureColor && { featureColor: newTask.featureColor })
|
||||||
};
|
};
|
||||||
|
|
||||||
await projectService.createTask(createData);
|
const createdTask = await projectService.createTask(createData);
|
||||||
|
|
||||||
// Don't reload tasks - let socket updates handle synchronization
|
// Replace temp task with real one
|
||||||
console.log('[TasksTab] Task creation sent to backend, waiting for socket update');
|
setTasks(prev => {
|
||||||
|
const updated = prev.map(t =>
|
||||||
|
t.id === tempId ? mapDatabaseTaskToUITask(createdTask) : t
|
||||||
|
);
|
||||||
|
// Notify parent of the update
|
||||||
|
onTasksChange(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[TasksTab] Task created successfully with optimistic update');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create task:', error);
|
console.error('Failed to create task:', error);
|
||||||
|
|
||||||
|
// Rollback: Remove temp task on error
|
||||||
|
setTasks(prev => {
|
||||||
|
const updated = prev.filter(t => t.id !== tempId);
|
||||||
|
onTasksChange(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -660,17 +728,17 @@ export const TasksTab = ({
|
|||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mark this update as local to prevent echo when socket update arrives
|
// Mark as pending update to prevent echo when socket update arrives
|
||||||
const updateTime = Date.now();
|
const taskToUpdate = tasks.find(t => t.id === taskId);
|
||||||
console.log(`[TasksTab] Marking update as local for task ${taskId} at time ${updateTime}`);
|
if (taskToUpdate) {
|
||||||
setLocalUpdates(prev => {
|
const updatedTask = { ...taskToUpdate, ...updates };
|
||||||
const newUpdates = {
|
addPendingUpdate({
|
||||||
...prev,
|
id: taskId,
|
||||||
[taskId]: updateTime
|
timestamp: Date.now(),
|
||||||
};
|
data: updatedTask,
|
||||||
console.log('[TasksTab] LocalUpdates state:', newUpdates);
|
operation: 'update'
|
||||||
return newUpdates;
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateData: Partial<UpdateTaskRequest> = {};
|
const updateData: Partial<UpdateTaskRequest> = {};
|
||||||
@@ -703,12 +771,8 @@ export const TasksTab = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the local update marker
|
// Clear the pending update marker
|
||||||
setLocalUpdates(prev => {
|
removePendingUpdate(taskId);
|
||||||
const newUpdates = { ...prev };
|
|
||||||
delete newUpdates[taskId];
|
|
||||||
return newUpdates;
|
|
||||||
});
|
|
||||||
|
|
||||||
alert(`Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
alert(`Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user