mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-09 16:17:58 -05:00
- Update consistent Delete Confirmation Modal
This commit is contained in:
@@ -5,7 +5,7 @@ import { ToolTestingPanel } from './ToolTestingPanel';
|
|||||||
import { Button } from '../ui/Button';
|
import { Button } from '../ui/Button';
|
||||||
import { mcpClientService, MCPClient, MCPClientConfig } from '../../services/mcpClientService';
|
import { mcpClientService, MCPClient, MCPClientConfig } from '../../services/mcpClientService';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { DeleteConfirmModal } from '../../pages/ProjectPage';
|
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
|
||||||
|
|
||||||
// Client interface (keeping for backward compatibility)
|
// Client interface (keeping for backward compatibility)
|
||||||
export interface Client {
|
export interface Client {
|
||||||
@@ -710,17 +710,30 @@ const EditClientDrawer: React.FC<EditClientDrawerProps> = ({ client, isOpen, onC
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = () => {
|
||||||
if (confirm(`Are you sure you want to delete "${client.name}"?`)) {
|
setClientToDelete(client);
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDeleteClient = async () => {
|
||||||
|
if (!clientToDelete) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mcpClientService.deleteClient(client.id);
|
await mcpClientService.deleteClient(clientToDelete.id);
|
||||||
onClose();
|
onClose();
|
||||||
// Trigger a reload of the clients list
|
// Trigger a reload of the clients list
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error instanceof Error ? error.message : 'Failed to delete client');
|
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;
|
if (!isOpen) return null;
|
||||||
@@ -853,6 +866,16 @@ const EditClientDrawer: React.FC<EditClientDrawerProps> = ({ client, isOpen, onC
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
{showDeleteConfirm && clientToDelete && (
|
||||||
|
<DeleteConfirmModal
|
||||||
|
itemName={clientToDelete.name}
|
||||||
|
onConfirm={confirmDeleteClient}
|
||||||
|
onCancel={cancelDeleteClient}
|
||||||
|
type="client"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -14,6 +14,7 @@ import { MilkdownEditor } from './MilkdownEditor';
|
|||||||
import { VersionHistoryModal } from './VersionHistoryModal';
|
import { VersionHistoryModal } from './VersionHistoryModal';
|
||||||
import { PRPViewer } from '../prp';
|
import { PRPViewer } from '../prp';
|
||||||
import { DocumentCard, NewDocumentCard } from './DocumentCard';
|
import { DocumentCard, NewDocumentCard } from './DocumentCard';
|
||||||
|
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -514,6 +515,10 @@ export const DocsTab = ({
|
|||||||
// Document state
|
// Document state
|
||||||
const [documents, setDocuments] = useState<ProjectDoc[]>([]);
|
const [documents, setDocuments] = useState<ProjectDoc[]>([]);
|
||||||
const [selectedDocument, setSelectedDocument] = useState<ProjectDoc | null>(null);
|
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 [isEditing, setIsEditing] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -575,7 +580,14 @@ export const DocsTab = ({
|
|||||||
document_type: doc.document_type || 'document'
|
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
|
// Auto-select first document if available and no document is currently selected
|
||||||
if (projectDocuments.length > 0 && !selectedDocument) {
|
if (projectDocuments.length > 0 && !selectedDocument) {
|
||||||
@@ -598,6 +610,26 @@ export const DocsTab = ({
|
|||||||
const template = DOCUMENT_TEMPLATES[templateKey as keyof typeof DOCUMENT_TEMPLATES];
|
const template = DOCUMENT_TEMPLATES[templateKey as keyof typeof DOCUMENT_TEMPLATES];
|
||||||
if (!template) return;
|
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 {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
@@ -608,15 +640,22 @@ export const DocsTab = ({
|
|||||||
document_type: template.document_type
|
document_type: template.document_type
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to documents list
|
// Replace temporary document with the real one
|
||||||
setDocuments(prev => [...prev, newDocument]);
|
setDocuments(prev => prev.map(doc =>
|
||||||
|
doc.id === tempDocument.id ? newDocument : doc
|
||||||
|
));
|
||||||
setSelectedDocument(newDocument);
|
setSelectedDocument(newDocument);
|
||||||
|
|
||||||
console.log('Document created successfully via API:', newDocument);
|
console.log('Document created successfully via API:', newDocument);
|
||||||
showToast('Document created successfully', 'success');
|
showToast('Document created successfully', 'success');
|
||||||
setShowTemplateModal(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create document:', 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(
|
showToast(
|
||||||
error instanceof Error ? error.message : 'Failed to create document',
|
error instanceof Error ? error.message : 'Failed to create document',
|
||||||
'error'
|
'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) => {
|
const handleProgressComplete = (data: CrawlProgressData) => {
|
||||||
console.log('Crawl completed:', data);
|
console.log('Crawl completed:', data);
|
||||||
setProgressItems(prev => prev.filter(item => item.progressId !== data.progressId));
|
setProgressItems(prev => prev.filter(item => item.progressId !== data.progressId));
|
||||||
@@ -935,22 +1002,11 @@ export const DocsTab = ({
|
|||||||
document={doc}
|
document={doc}
|
||||||
isActive={selectedDocument?.id === doc.id}
|
isActive={selectedDocument?.id === doc.id}
|
||||||
onSelect={setSelectedDocument}
|
onSelect={setSelectedDocument}
|
||||||
onDelete={async (docId) => {
|
onDelete={(docId) => {
|
||||||
if (!project?.id) return;
|
const doc = documents.find(d => d.id === docId);
|
||||||
|
if (doc) {
|
||||||
try {
|
setDocumentToDelete({ id: docId, title: doc.title });
|
||||||
// Call API to delete from database first
|
setShowDeleteConfirm(true);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isDarkMode={isDarkMode}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -117,9 +117,7 @@ export const DocumentCard: React.FC<DocumentCardProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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"
|
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}`}
|
aria-label={`Delete ${document.title}`}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useRef, useState, useCallback } from 'react';
|
import React, { useRef, useState, useCallback } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { DeleteConfirmModal } from '../../pages/ProjectPage';
|
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
|
||||||
import { CheckSquare, Square, Trash2, ArrowRight } from 'lucide-react';
|
import { CheckSquare, Square, Trash2, ArrowRight } from 'lucide-react';
|
||||||
import { projectService } from '../../services/projectService';
|
import { projectService } from '../../services/projectService';
|
||||||
import { Task } from './TaskTableView'; // Import Task interface
|
import { Task } from './TaskTableView'; // Import Task interface
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus } from 'lucide-react';
|
import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus } from 'lucide-react';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { DeleteConfirmModal } from '../../pages/ProjectPage';
|
import { DeleteConfirmModal } from '../ui/DeleteConfirmModal';
|
||||||
import { projectService } from '../../services/projectService';
|
import { projectService } from '../../services/projectService';
|
||||||
import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
|
import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
|
||||||
import { DraggableTaskCard } from './DraggableTaskCard';
|
import { DraggableTaskCard } from './DraggableTaskCard';
|
||||||
|
|||||||
@@ -176,7 +176,9 @@ 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];
|
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);
|
console.log('[Socket] Skipping echo update for locally updated task:', updatedTask.id);
|
||||||
// Clean up the local update marker after the echo protection window
|
// Clean up the local update marker after the echo protection window
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -185,9 +187,10 @@ export const TasksTab = ({
|
|||||||
delete newUpdates[updatedTask.id];
|
delete newUpdates[updatedTask.id];
|
||||||
return newUpdates;
|
return newUpdates;
|
||||||
});
|
});
|
||||||
}, 2000);
|
}, 5000);
|
||||||
return;
|
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
|
// Skip updates while modal is open for the same task to prevent conflicts
|
||||||
if (isModalOpen && editingTask?.id === updatedTask.id) {
|
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}`);
|
console.log(`[TasksTab] Moving task ${movingTask.title} from ${oldStatus} to ${newStatus} with order ${newOrder}`);
|
||||||
|
|
||||||
// OPTIMISTIC UPDATE: Update UI immediately
|
// OPTIMISTIC UPDATE: Update UI immediately
|
||||||
|
console.log(`[TasksTab] Applying optimistic move for task ${taskId} to ${newStatus}`);
|
||||||
setTasks(prev => {
|
setTasks(prev => {
|
||||||
const updated = prev.map(task => task.id === taskId ? updatedTask : task);
|
const updated = prev.map(task => task.id === taskId ? updatedTask : task);
|
||||||
|
console.log(`[TasksTab] Tasks after optimistic move:`, updated);
|
||||||
setTimeout(() => onTasksChange(updated), 0);
|
setTimeout(() => onTasksChange(updated), 0);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
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 this update as local to prevent echo when socket update arrives
|
||||||
setLocalUpdates(prev => ({
|
const updateTime = Date.now();
|
||||||
|
console.log(`[TasksTab] Marking update as local for task ${taskId} at time ${updateTime}`);
|
||||||
|
setLocalUpdates(prev => {
|
||||||
|
const newUpdates = {
|
||||||
...prev,
|
...prev,
|
||||||
[taskId]: Date.now()
|
[taskId]: updateTime
|
||||||
}));
|
};
|
||||||
|
console.log('[TasksTab] LocalUpdates state:', newUpdates);
|
||||||
|
return newUpdates;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Then update the backend
|
// Then update the backend
|
||||||
@@ -698,19 +709,30 @@ export const TasksTab = ({
|
|||||||
const originalTask = tasks.find(t => t.id === taskId);
|
const originalTask = tasks.find(t => t.id === taskId);
|
||||||
|
|
||||||
// Optimistically update the UI immediately
|
// Optimistically update the UI immediately
|
||||||
setTasks(prevTasks =>
|
console.log(`[TasksTab] Applying optimistic update for task ${taskId}`, updates);
|
||||||
prevTasks.map(task =>
|
setTasks(prevTasks => {
|
||||||
|
const updated = prevTasks.map(task =>
|
||||||
task.id === taskId
|
task.id === taskId
|
||||||
? { ...task, ...updates }
|
? { ...task, ...updates }
|
||||||
: task
|
: 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
|
// Mark this update as local to prevent echo when socket update arrives
|
||||||
setLocalUpdates(prev => ({
|
const updateTime = Date.now();
|
||||||
|
console.log(`[TasksTab] Marking update as local for task ${taskId} at time ${updateTime}`);
|
||||||
|
setLocalUpdates(prev => {
|
||||||
|
const newUpdates = {
|
||||||
...prev,
|
...prev,
|
||||||
[taskId]: Date.now()
|
[taskId]: updateTime
|
||||||
}));
|
};
|
||||||
|
console.log('[TasksTab] LocalUpdates state:', newUpdates);
|
||||||
|
return newUpdates;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateData: Partial<UpdateTaskRequest> = {};
|
const updateData: Partial<UpdateTaskRequest> = {};
|
||||||
|
|||||||
83
archon-ui-main/src/components/ui/DeleteConfirmModal.tsx
Normal file
83
archon-ui-main/src/components/ui/DeleteConfirmModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ import { KnowledgeTable } from '../components/knowledge-base/KnowledgeTable';
|
|||||||
import { KnowledgeItemCard } from '../components/knowledge-base/KnowledgeItemCard';
|
import { KnowledgeItemCard } from '../components/knowledge-base/KnowledgeItemCard';
|
||||||
import { GroupedKnowledgeItemCard } from '../components/knowledge-base/GroupedKnowledgeItemCard';
|
import { GroupedKnowledgeItemCard } from '../components/knowledge-base/GroupedKnowledgeItemCard';
|
||||||
import { KnowledgeGridSkeleton, KnowledgeTableSkeleton } from '../components/knowledge-base/KnowledgeItemSkeleton';
|
import { KnowledgeGridSkeleton, KnowledgeTableSkeleton } from '../components/knowledge-base/KnowledgeItemSkeleton';
|
||||||
|
import { DeleteConfirmModal } from '../components/ui/DeleteConfirmModal';
|
||||||
import { GroupCreationModal } from '../components/knowledge-base/GroupCreationModal';
|
import { GroupCreationModal } from '../components/knowledge-base/GroupCreationModal';
|
||||||
|
|
||||||
const extractDomain = (url: string): string => {
|
const extractDomain = (url: string): string => {
|
||||||
@@ -70,6 +71,10 @@ export const KnowledgeBasePage = () => {
|
|||||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||||
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
|
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();
|
const { showToast } = useToast();
|
||||||
|
|
||||||
// Single consolidated loading function - only loads data, no filtering
|
// Single consolidated loading function - only loads data, no filtering
|
||||||
@@ -360,32 +365,43 @@ export const KnowledgeBasePage = () => {
|
|||||||
if (selectedItems.size === 0) return;
|
if (selectedItems.size === 0) return;
|
||||||
|
|
||||||
const count = selectedItems.size;
|
const count = selectedItems.size;
|
||||||
const confirmed = window.confirm(`Are you sure you want to delete ${count} selected item${count > 1 ? 's' : ''}?`);
|
setItemsToDelete({ count, items: new Set(selectedItems) });
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
if (!confirmed) return;
|
const confirmDeleteItems = async () => {
|
||||||
|
if (!itemsToDelete) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete each selected item
|
// Delete each selected item
|
||||||
const deletePromises = Array.from(selectedItems).map(itemId =>
|
const deletePromises = Array.from(itemsToDelete.items).map(itemId =>
|
||||||
knowledgeBaseService.deleteKnowledgeItem(itemId)
|
knowledgeBaseService.deleteKnowledgeItem(itemId)
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(deletePromises);
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
// Remove deleted items from state
|
// 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
|
// Clear selection
|
||||||
setSelectedItems(new Set());
|
setSelectedItems(new Set());
|
||||||
setIsSelectionMode(false);
|
setIsSelectionMode(false);
|
||||||
|
|
||||||
showToast(`Successfully deleted ${count} item${count > 1 ? 's' : ''}`, 'success');
|
showToast(`Successfully deleted ${itemsToDelete.count} item${itemsToDelete.count > 1 ? 's' : ''}`, 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete selected items:', error);
|
console.error('Failed to delete selected items:', error);
|
||||||
showToast('Failed to delete some items', 'error');
|
showToast('Failed to delete some items', 'error');
|
||||||
|
} finally {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setItemsToDelete(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelDeleteItems = () => {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setItemsToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
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>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TasksTab } from '../components/project-tasks/TasksTab';
|
|||||||
import { Button } from '../components/ui/Button';
|
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 { 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 { copyToClipboard } from '../utils/clipboard';
|
||||||
|
import { DeleteConfirmModal } from '../components/ui/DeleteConfirmModal';
|
||||||
|
|
||||||
// Import our service layer and types
|
// Import our service layer and types
|
||||||
import { projectService } from '../services/projectService';
|
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user