- Update consistent Delete Confirmation Modal

This commit is contained in:
sean-eskerium
2025-08-21 01:25:15 -04:00
parent 97a280461a
commit a194ec9a74
9 changed files with 275 additions and 129 deletions

View File

@@ -5,7 +5,7 @@ import { ToolTestingPanel } from './ToolTestingPanel';
import { Button } from '../ui/Button';
import { mcpClientService, MCPClient, MCPClientConfig } from '../../services/mcpClientService';
import { useToast } from '../../contexts/ToastContext';
import { DeleteConfirmModal } from '../../pages/ProjectPage';
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
// Client interface (keeping for backward compatibility)
export interface Client {
@@ -710,18 +710,31 @@ const EditClientDrawer: React.FC<EditClientDrawerProps> = ({ client, isOpen, onC
}
};
const handleDelete = async () => {
if (confirm(`Are you sure you want to delete "${client.name}"?`)) {
try {
await mcpClientService.deleteClient(client.id);
onClose();
// Trigger a reload of the clients list
window.location.reload();
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to delete client');
}
const handleDelete = () => {
setClientToDelete(client);
setShowDeleteConfirm(true);
};
const confirmDeleteClient = async () => {
if (!clientToDelete) return;
try {
await mcpClientService.deleteClient(clientToDelete.id);
onClose();
// Trigger a reload of the clients list
window.location.reload();
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to delete client');
} finally {
setShowDeleteConfirm(false);
setClientToDelete(null);
}
};
const cancelDeleteClient = () => {
setShowDeleteConfirm(false);
setClientToDelete(null);
};
if (!isOpen) return null;
@@ -853,6 +866,16 @@ const EditClientDrawer: React.FC<EditClientDrawerProps> = ({ client, isOpen, onC
</div>
</form>
</div>
{/* Delete Confirmation Modal */}
{showDeleteConfirm && clientToDelete && (
<DeleteConfirmModal
itemName={clientToDelete.name}
onConfirm={confirmDeleteClient}
onCancel={cancelDeleteClient}
type="client"
/>
)}
</div>
);
};

View File

@@ -14,6 +14,7 @@ import { MilkdownEditor } from './MilkdownEditor';
import { VersionHistoryModal } from './VersionHistoryModal';
import { PRPViewer } from '../prp';
import { DocumentCard, NewDocumentCard } from './DocumentCard';
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
@@ -514,6 +515,10 @@ export const DocsTab = ({
// Document state
const [documents, setDocuments] = useState<ProjectDoc[]>([]);
const [selectedDocument, setSelectedDocument] = useState<ProjectDoc | null>(null);
// Delete confirmation modal state
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [documentToDelete, setDocumentToDelete] = useState<{ id: string; title: string } | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [loading, setLoading] = useState(false);
@@ -575,7 +580,14 @@ export const DocsTab = ({
document_type: doc.document_type || 'document'
}));
setDocuments(projectDocuments);
// Merge with existing documents, preserving any temporary documents
setDocuments(prev => {
// Keep any temporary documents (ones with temp- prefix)
const tempDocs = prev.filter(doc => doc.id.startsWith('temp-'));
// Merge temporary docs with loaded docs
return [...projectDocuments, ...tempDocs];
});
// Auto-select first document if available and no document is currently selected
if (projectDocuments.length > 0 && !selectedDocument) {
@@ -598,6 +610,26 @@ export const DocsTab = ({
const template = DOCUMENT_TEMPLATES[templateKey as keyof typeof DOCUMENT_TEMPLATES];
if (!template) return;
// Create a temporary document for optimistic update
const tempDocument: ProjectDoc = {
id: `temp-${Date.now()}`,
title: template.name,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
content: template.content,
document_type: template.document_type
};
// Optimistically add the document to the UI immediately
console.log('[DocsTab] Adding temporary document:', tempDocument);
setDocuments(prev => {
const updated = [...prev, tempDocument];
console.log('[DocsTab] Documents after optimistic add:', updated);
return updated;
});
setSelectedDocument(tempDocument);
setShowTemplateModal(false);
try {
setIsSaving(true);
@@ -608,15 +640,22 @@ export const DocsTab = ({
document_type: template.document_type
});
// Add to documents list
setDocuments(prev => [...prev, newDocument]);
// Replace temporary document with the real one
setDocuments(prev => prev.map(doc =>
doc.id === tempDocument.id ? newDocument : doc
));
setSelectedDocument(newDocument);
console.log('Document created successfully via API:', newDocument);
showToast('Document created successfully', 'success');
setShowTemplateModal(false);
} catch (error) {
console.error('Failed to create document:', error);
// Remove the temporary document on error
setDocuments(prev => prev.filter(doc => doc.id !== tempDocument.id));
setSelectedDocument(null);
setShowTemplateModal(true); // Re-open the modal
showToast(
error instanceof Error ? error.message : 'Failed to create document',
'error'
@@ -783,6 +822,34 @@ export const DocsTab = ({
}
};
// Delete confirmation handlers
const confirmDeleteDocument = async () => {
if (!documentToDelete || !project?.id) return;
try {
// Call API to delete from database first
await projectService.deleteDocument(project.id, documentToDelete.id);
// Then remove from local state
setDocuments(prev => prev.filter(d => d.id !== documentToDelete.id));
if (selectedDocument?.id === documentToDelete.id) {
setSelectedDocument(documents.find(d => d.id !== documentToDelete.id) || null);
}
showToast('Document deleted', 'success');
} catch (error) {
console.error('Failed to delete document:', error);
showToast('Failed to delete document', 'error');
} finally {
setShowDeleteConfirm(false);
setDocumentToDelete(null);
}
};
const cancelDeleteDocument = () => {
setShowDeleteConfirm(false);
setDocumentToDelete(null);
};
const handleProgressComplete = (data: CrawlProgressData) => {
console.log('Crawl completed:', data);
setProgressItems(prev => prev.filter(item => item.progressId !== data.progressId));
@@ -935,22 +1002,11 @@ export const DocsTab = ({
document={doc}
isActive={selectedDocument?.id === doc.id}
onSelect={setSelectedDocument}
onDelete={async (docId) => {
if (!project?.id) return;
try {
// Call API to delete from database first
await projectService.deleteDocument(project.id, docId);
// Then remove from local state
setDocuments(prev => prev.filter(d => d.id !== docId));
if (selectedDocument?.id === docId) {
setSelectedDocument(documents.find(d => d.id !== docId) || null);
}
showToast('Document deleted', 'success');
} catch (error) {
console.error('Failed to delete document:', error);
showToast('Failed to delete document', 'error');
onDelete={(docId) => {
const doc = documents.find(d => d.id === docId);
if (doc) {
setDocumentToDelete({ id: docId, title: doc.title });
setShowDeleteConfirm(true);
}
}}
isDarkMode={isDarkMode}
@@ -1099,6 +1155,16 @@ export const DocsTab = ({
}}
/>
)}
{/* Delete Confirmation Modal */}
{showDeleteConfirm && documentToDelete && (
<DeleteConfirmModal
itemName={documentToDelete.title}
onConfirm={confirmDeleteDocument}
onCancel={cancelDeleteDocument}
type="document"
/>
)}
</div>
);
};

View File

@@ -117,9 +117,7 @@ export const DocumentCard: React.FC<DocumentCardProps> = ({
<button
onClick={(e) => {
e.stopPropagation();
if (confirm(`Delete "${document.title}"?`)) {
onDelete(document.id);
}
onDelete(document.id);
}}
className="absolute top-2 right-2 p-1 rounded-md bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-400 transition-colors"
aria-label={`Delete ${document.title}`}

View File

@@ -1,7 +1,7 @@
import React, { useRef, useState, useCallback } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { useToast } from '../../contexts/ToastContext';
import { DeleteConfirmModal } from '../../pages/ProjectPage';
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
import { CheckSquare, Square, Trash2, ArrowRight } from 'lucide-react';
import { projectService } from '../../services/projectService';
import { Task } from './TaskTableView'; // Import Task interface

View File

@@ -2,7 +2,7 @@ import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus } from 'lucide-react';
import { useToast } from '../../contexts/ToastContext';
import { DeleteConfirmModal } from '../../pages/ProjectPage';
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
import { projectService } from '../../services/projectService';
import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
import { DraggableTaskCard } from './DraggableTaskCard';

View File

@@ -176,7 +176,9 @@ export const TasksTab = ({
// Check if this is an echo of a local update
const localUpdateTime = localUpdates[updatedTask.id];
if (localUpdateTime && Date.now() - localUpdateTime < 2000) {
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);
// Clean up the local update marker after the echo protection window
setTimeout(() => {
@@ -185,9 +187,10 @@ export const TasksTab = ({
delete newUpdates[updatedTask.id];
return newUpdates;
});
}, 2000);
}, 5000);
return;
}
console.log('[Socket] Not an echo, applying update for task:', updatedTask.id);
// Skip updates while modal is open for the same task to prevent conflicts
if (isModalOpen && editingTask?.id === updatedTask.id) {
@@ -553,18 +556,26 @@ export const TasksTab = ({
console.log(`[TasksTab] Moving task ${movingTask.title} from ${oldStatus} to ${newStatus} with order ${newOrder}`);
// OPTIMISTIC UPDATE: Update UI immediately
console.log(`[TasksTab] Applying optimistic move for task ${taskId} to ${newStatus}`);
setTasks(prev => {
const updated = prev.map(task => task.id === taskId ? updatedTask : task);
console.log(`[TasksTab] Tasks after optimistic move:`, updated);
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
console.log(`[TasksTab] Optimistically updated UI for task ${taskId}`);
// Mark this update as local to prevent echo when socket update arrives
setLocalUpdates(prev => ({
...prev,
[taskId]: Date.now()
}));
const updateTime = Date.now();
console.log(`[TasksTab] Marking update as local for task ${taskId} at time ${updateTime}`);
setLocalUpdates(prev => {
const newUpdates = {
...prev,
[taskId]: updateTime
};
console.log('[TasksTab] LocalUpdates state:', newUpdates);
return newUpdates;
});
try {
// Then update the backend
@@ -698,19 +709,30 @@ export const TasksTab = ({
const originalTask = tasks.find(t => t.id === taskId);
// Optimistically update the UI immediately
setTasks(prevTasks =>
prevTasks.map(task =>
console.log(`[TasksTab] Applying optimistic update for task ${taskId}`, updates);
setTasks(prevTasks => {
const updated = prevTasks.map(task =>
task.id === taskId
? { ...task, ...updates }
: task
)
);
);
console.log(`[TasksTab] Tasks after optimistic update:`, updated);
// Notify parent of the optimistic update
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
// Mark this update as local to prevent echo when socket update arrives
setLocalUpdates(prev => ({
...prev,
[taskId]: Date.now()
}));
const updateTime = Date.now();
console.log(`[TasksTab] Marking update as local for task ${taskId} at time ${updateTime}`);
setLocalUpdates(prev => {
const newUpdates = {
...prev,
[taskId]: updateTime
};
console.log('[TasksTab] LocalUpdates state:', newUpdates);
return newUpdates;
});
try {
const updateData: Partial<UpdateTaskRequest> = {};

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { Trash2 } from 'lucide-react';
export interface DeleteConfirmModalProps {
itemName: string;
onConfirm: () => void;
onCancel: () => void;
type: 'project' | 'task' | 'client' | 'document' | 'knowledge-items' | 'feature' | 'data';
}
export const DeleteConfirmModal: React.FC<DeleteConfirmModalProps> = ({ itemName, onConfirm, onCancel, type }) => {
const getTitle = () => {
switch (type) {
case 'project': return 'Delete Project';
case 'task': return 'Delete Task';
case 'client': return 'Delete MCP Client';
case 'document': return 'Delete Document';
case 'knowledge-items': return 'Delete Knowledge Items';
case 'feature': return 'Delete Feature';
case 'data': return 'Delete Data';
}
};
const getMessage = () => {
switch (type) {
case 'project': return `Are you sure you want to delete the "${itemName}" project? This will also delete all associated tasks and documents and cannot be undone.`;
case 'task': return `Are you sure you want to delete the "${itemName}" task? This action cannot be undone.`;
case 'client': return `Are you sure you want to delete the "${itemName}" client? This will permanently remove its configuration and cannot be undone.`;
case 'document': return `Are you sure you want to delete the "${itemName}" document? This action cannot be undone.`;
case 'knowledge-items': return `Are you sure you want to delete ${itemName}? This will permanently remove the selected items from your knowledge base and cannot be undone.`;
case 'feature': return `Are you sure you want to delete the "${itemName}" feature? This action cannot be undone.`;
case 'data': return `Are you sure you want to delete this data? This action cannot be undone.`;
}
};
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="relative p-6 rounded-md backdrop-blur-md w-full max-w-md
bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30
border border-gray-200 dark:border-zinc-800/50
shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-[2px]
before:rounded-t-[4px] before:bg-red-500
before:shadow-[0_0_10px_2px_rgba(239,68,68,0.4)] dark:before:shadow-[0_0_20px_5px_rgba(239,68,68,0.7)]">
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<Trash2 className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
{getTitle()}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
This action cannot be undone
</p>
</div>
</div>
<p className="text-gray-700 dark:text-gray-300 mb-6">
{getMessage()}
</p>
<div className="flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors shadow-lg shadow-red-600/20"
>
Delete
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -18,6 +18,7 @@ import { KnowledgeTable } from '../components/knowledge-base/KnowledgeTable';
import { KnowledgeItemCard } from '../components/knowledge-base/KnowledgeItemCard';
import { GroupedKnowledgeItemCard } from '../components/knowledge-base/GroupedKnowledgeItemCard';
import { KnowledgeGridSkeleton, KnowledgeTableSkeleton } from '../components/knowledge-base/KnowledgeItemSkeleton';
import { DeleteConfirmModal } from '../components/ui/DeleteConfirmModal';
import { GroupCreationModal } from '../components/knowledge-base/GroupCreationModal';
const extractDomain = (url: string): string => {
@@ -70,6 +71,10 @@ export const KnowledgeBasePage = () => {
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
// Delete confirmation modal state
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [itemsToDelete, setItemsToDelete] = useState<{ count: number; items: Set<string> } | null>(null);
const { showToast } = useToast();
// Single consolidated loading function - only loads data, no filtering
@@ -360,32 +365,43 @@ export const KnowledgeBasePage = () => {
if (selectedItems.size === 0) return;
const count = selectedItems.size;
const confirmed = window.confirm(`Are you sure you want to delete ${count} selected item${count > 1 ? 's' : ''}?`);
if (!confirmed) return;
setItemsToDelete({ count, items: new Set(selectedItems) });
setShowDeleteConfirm(true);
};
const confirmDeleteItems = async () => {
if (!itemsToDelete) return;
try {
// Delete each selected item
const deletePromises = Array.from(selectedItems).map(itemId =>
const deletePromises = Array.from(itemsToDelete.items).map(itemId =>
knowledgeBaseService.deleteKnowledgeItem(itemId)
);
await Promise.all(deletePromises);
// Remove deleted items from state
setKnowledgeItems(prev => prev.filter(item => !selectedItems.has(item.id)));
setKnowledgeItems(prev => prev.filter(item => !itemsToDelete.items.has(item.id)));
// Clear selection
setSelectedItems(new Set());
setIsSelectionMode(false);
showToast(`Successfully deleted ${count} item${count > 1 ? 's' : ''}`, 'success');
showToast(`Successfully deleted ${itemsToDelete.count} item${itemsToDelete.count > 1 ? 's' : ''}`, 'success');
} catch (error) {
console.error('Failed to delete selected items:', error);
showToast('Failed to delete some items', 'error');
} finally {
setShowDeleteConfirm(false);
setItemsToDelete(null);
}
};
const cancelDeleteItems = () => {
setShowDeleteConfirm(false);
setItemsToDelete(null);
};
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -1194,6 +1210,16 @@ export const KnowledgeBasePage = () => {
}}
/>
)}
{/* Delete Confirmation Modal */}
{showDeleteConfirm && itemsToDelete && (
<DeleteConfirmModal
itemName={`${itemsToDelete.count} selected item${itemsToDelete.count > 1 ? 's' : ''}`}
onConfirm={confirmDeleteItems}
onCancel={cancelDeleteItems}
type="knowledge-items"
/>
)}
</div>;
};

View File

@@ -10,6 +10,7 @@ import { TasksTab } from '../components/project-tasks/TasksTab';
import { Button } from '../components/ui/Button';
import { ChevronRight, ShoppingCart, Code, Briefcase, Layers, Plus, X, AlertCircle, Loader2, Heart, BarChart3, Trash2, Pin, ListTodo, Activity, CheckCircle2, Clipboard } from 'lucide-react';
import { copyToClipboard } from '../utils/clipboard';
import { DeleteConfirmModal } from '../components/ui/DeleteConfirmModal';
// Import our service layer and types
import { projectService } from '../services/projectService';
@@ -1062,76 +1063,3 @@ export function ProjectPage({
);
}
// Reusable Delete Confirmation Modal Component
export interface DeleteConfirmModalProps {
itemName: string;
onConfirm: () => void;
onCancel: () => void;
type: 'project' | 'task' | 'client';
}
export const DeleteConfirmModal: React.FC<DeleteConfirmModalProps> = ({ itemName, onConfirm, onCancel, type }) => {
const getTitle = () => {
switch (type) {
case 'project': return 'Delete Project';
case 'task': return 'Delete Task';
case 'client': return 'Delete MCP Client';
}
};
const getMessage = () => {
switch (type) {
case 'project': return `Are you sure you want to delete the "${itemName}" project? This will also delete all associated tasks and documents and cannot be undone.`;
case 'task': return `Are you sure you want to delete the "${itemName}" task? This action cannot be undone.`;
case 'client': return `Are you sure you want to delete the "${itemName}" client? This will permanently remove its configuration and cannot be undone.`;
}
};
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="relative p-6 rounded-md backdrop-blur-md w-full max-w-md
bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30
border border-gray-200 dark:border-zinc-800/50
shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-[2px]
before:rounded-t-[4px] before:bg-red-500
before:shadow-[0_0_10px_2px_rgba(239,68,68,0.4)] dark:before:shadow-[0_0_20px_5px_rgba(239,68,68,0.7)]">
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<Trash2 className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
{getTitle()}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
This action cannot be undone
</p>
</div>
</div>
<p className="text-gray-700 dark:text-gray-300 mb-6">
{getMessage()}
</p>
<div className="flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors shadow-lg shadow-red-600/25 hover:shadow-red-700/25"
>
Delete
</button>
</div>
</div>
</div>
</div>
);
};