From 2c00ecbf396c6768d088a6f0cb2c61a0084cc41d Mon Sep 17 00:00:00 2001 From: CELin Date: Thu, 12 Mar 2026 11:22:06 +0100 Subject: [PATCH 1/5] feat: Add filter type to support date filtering in the data explorer. --- backend/models/data_documents.py | 1 + backend/services/data_documents_service.py | 17 +++++++++++- backend/test_datetime.py | 10 +++++++ frontend/pages/DataExplorerPage.tsx | 32 ++++++++++++++-------- frontend/services/dbService.ts | 4 +-- 5 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 backend/test_datetime.py diff --git a/backend/models/data_documents.py b/backend/models/data_documents.py index d9a2f18..00fa6db 100644 --- a/backend/models/data_documents.py +++ b/backend/models/data_documents.py @@ -6,6 +6,7 @@ class DataDocumentsFilter(BaseModel): key: str value: Any = "" # Can be str, int, etc. depending on the field type operator: Optional[str] = "equals" # 'equals', 'exists', 'not_exists' + type: Optional[str] = "string" class DataDocumentsRequest(BaseModel): diff --git a/backend/services/data_documents_service.py b/backend/services/data_documents_service.py index 8983251..3165447 100644 --- a/backend/services/data_documents_service.py +++ b/backend/services/data_documents_service.py @@ -102,6 +102,7 @@ def get_query_for_filter(f: dict): key = f.get("key") value = f.get("value") operator = f.get("operator", "equals") + val_type = f.get("type", "string") if not key: return {} @@ -127,7 +128,21 @@ def get_query_for_filter(f: dict): return {"$or": or_clauses} return {} else: - if key == "_id": + if val_type == "date" and isinstance(value, str): + try: + dt_str = value + if len(dt_str) == 10: + dt_str += "T00:00:00" + if ( + "Z" not in dt_str + and "+" not in dt_str[-6:] + and "-" not in dt_str[-6:] + ): + dt_str += "Z" + query_val = datetime.fromisoformat(dt_str.replace("Z", "+00:00")) + except Exception: + query_val = value + elif key == "_id": try: query_val = ObjectId(value) except Exception: diff --git a/backend/test_datetime.py b/backend/test_datetime.py new file mode 100644 index 0000000..b417b24 --- /dev/null +++ b/backend/test_datetime.py @@ -0,0 +1,10 @@ +from datetime import datetime + +dt_str = "2026-03-12T10:00" +if len(dt_str) == 10: + dt_str += "T00:00:00" +if "Z" not in dt_str and "+" not in dt_str[-6:] and "-" not in dt_str[-6:]: + dt_str += "Z" + +print(dt_str) +print(datetime.fromisoformat(dt_str.replace("Z", "+00:00"))) diff --git a/frontend/pages/DataExplorerPage.tsx b/frontend/pages/DataExplorerPage.tsx index 3854544..b9ffdf6 100644 --- a/frontend/pages/DataExplorerPage.tsx +++ b/frontend/pages/DataExplorerPage.tsx @@ -187,15 +187,16 @@ const DataExplorerPage: React.FC = ({ value: string; isCustom: boolean; operator?: string; + type?: 'string' | 'number' | 'boolean' | 'date'; } - const [filters, setFilters] = useState([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals' }]); + const [filters, setFilters] = useState([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); const [debouncedFilters, setDebouncedFilters] = useState(filters); const [schemaTree, setSchemaTree] = useState([]); const [isFetchingSchema, setIsFetchingSchema] = useState(false); const [currentCollectionInfo, setCurrentCollectionInfo] = useState(null); const addFilter = () => { - setFilters(prev => [...prev, { id: Math.random().toString(36).substring(7), key: 'all', value: '', isCustom: false, operator: 'equals' }]); + setFilters(prev => [...prev, { id: Math.random().toString(36).substring(7), key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); }; const removeFilter = (id: string) => { @@ -371,8 +372,8 @@ const DataExplorerPage: React.FC = ({ setTotalPages(1); setTotalDocuments(0); setPageInput('1'); - setFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals' }]); - setDebouncedFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals' }]); + setFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); + setDebouncedFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); setSchemaTree([]); setIsFetchingSchema(false); setSelectedDocument(null); @@ -451,7 +452,8 @@ const DataExplorerPage: React.FC = ({ const activeFilters = debouncedFilters.map(f => ({ key: f.key, value: getCoercedFilterValue(f.value), - operator: f.operator || 'equals' + operator: f.operator || 'equals', + type: f.type || 'string' })); const response = await getDocuments(selectedCollection, currentResource, currentPage, 20, undefined, activeFilters); setDocuments(response.documents); @@ -589,8 +591,8 @@ const DataExplorerPage: React.FC = ({ if (selectedCollection === collectionName) return; setSelectedCollection(collectionName); setCurrentPage(1); - setFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals' }]); - setDebouncedFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals' }]); + setFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); + setDebouncedFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); setSelectedDocument(null); setBreadcrumbs([]); await fetchSchemaForCollection(collectionName); @@ -1257,7 +1259,7 @@ const DataExplorerPage: React.FC = ({
- {filters.map((f, i) => ( + {filters.map((f) => (
{filters.length > 1 && ( +
+ + +
diff --git a/frontend/services/dbService.ts b/frontend/services/dbService.ts index f6f9aa1..429f557 100644 --- a/frontend/services/dbService.ts +++ b/frontend/services/dbService.ts @@ -431,6 +431,53 @@ export const getDocuments = async ( return response.json(); }; +export const getDocumentsQueryCode = async ( + collectionName: string, + resource: SelectedResource, + filter?: { key: string, value: any, operator?: string, type?: string }, + filters?: { key: string, value: any, operator?: string, type?: string }[] +): Promise => { + if (!USE_MSAL_AUTH) { + return Promise.resolve("db['" + collectionName + "'].find({\n // Exporting is only fully supported dynamically in remote mode.\n})"); + } + + const accessToken = await getAuthenticatedToken(); + + const bodyQuery: any = { + account_id: resource.accountId, + database_name: resource.databaseName, + collection_name: collectionName, + page: 1, + limit: 1, + }; + + if (filter && ((filter.value !== '' && filter.value !== null && filter.value !== undefined) || filter.operator === 'exists' || filter.operator === 'not_exists')) { + bodyQuery.filter = filter; + } + + if (filters && filters.length > 0) { + bodyQuery.filters = filters.filter(f => (f.value !== '' && f.value !== null && f.value !== undefined) || f.operator === 'exists' || f.operator === 'not_exists'); + } + + const response = await fetch(`${API_BASE_URL}/data/documents/query_code`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + body: JSON.stringify(bodyQuery), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const errorMessage = errorData.detail || errorData.message || `Failed to generate query code. Status: ${response.status}`; + throw new Error(errorMessage); + } + + const result = await response.json(); + return result.query_code; +}; + /** * Finds a single document by its ID, searching across all provided collections. * @param documentId The string representation of the document's ObjectId. From 2ef949379fdbfb7c634d3e96214bf5984346df43 Mon Sep 17 00:00:00 2001 From: CELin Date: Fri, 13 Mar 2026 12:36:09 +0100 Subject: [PATCH 3/5] feat: generalize image preview component to support both base64 and URL images. --- frontend/components/JsonDisplay.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/components/JsonDisplay.tsx b/frontend/components/JsonDisplay.tsx index 42a3ac3..66e0298 100644 --- a/frontend/components/JsonDisplay.tsx +++ b/frontend/components/JsonDisplay.tsx @@ -201,10 +201,10 @@ const ObjectIdDisplay: React.FC<{ ); }; -const Base64ImagePreview: React.FC<{ - base64String: string; +const ImagePreview: React.FC<{ + imageUrl: string; searchRegex: RegExp | null; -}> = ({ base64String, searchRegex }) => { +}> = ({ imageUrl, searchRegex }) => { const [isExpanded, setIsExpanded] = useState(false); const [previewPos, setPreviewPos] = useState<{ x: number; y: number } | null>(null); const iconRef = useRef(null); @@ -248,7 +248,7 @@ const Base64ImagePreview: React.FC<{ title="Click to collapse back to icon" > " - {searchRegex ? highlightText(base64String, searchRegex) : base64String}" + {searchRegex ? highlightText(imageUrl, searchRegex) : imageUrl}" ); @@ -281,7 +281,7 @@ const Base64ImagePreview: React.FC<{ }} > Preview @@ -475,8 +475,11 @@ const JsonNode: React.FC<{ ); } - if (nodeValue.startsWith("data:image/png;base64,")) { - return ; + const isBase64Image = nodeValue.startsWith("data:image/"); + const isUrlImage = /^https?:\/\/.+\.(png|jpe?g|gif|svg|webp)(\?.*)?$/i.test(nodeValue); + + if (isBase64Image || isUrlImage) { + return ; } return " From cc164e6f408ca7c3925a37aad93c059caf2fb62e Mon Sep 17 00:00:00 2001 From: CELin Date: Thu, 19 Mar 2026 10:53:44 +0100 Subject: [PATCH 4/5] feat: Implement save conflict detection and resolution with a diff viewer for document edits. --- frontend/components/DocumentDetailView.tsx | 46 +- frontend/components/SaveConflictDialog.tsx | 73 +++ frontend/pages/DataExplorerPage.tsx | 554 +++++++++++---------- 3 files changed, 405 insertions(+), 268 deletions(-) create mode 100644 frontend/components/SaveConflictDialog.tsx diff --git a/frontend/components/DocumentDetailView.tsx b/frontend/components/DocumentDetailView.tsx index e9be38d..70e4855 100644 --- a/frontend/components/DocumentDetailView.tsx +++ b/frontend/components/DocumentDetailView.tsx @@ -3,6 +3,8 @@ import { useTheme } from '../contexts/ThemeContext'; import { Button, CircularProgress } from '@mui/material'; import MonacoEditor from '@monaco-editor/react'; import { updateDocument, getSingleDocument } from '../services/dbService'; +import { isEqual, omit } from 'lodash'; +import SaveConflictDialog from './SaveConflictDialog'; interface DocumentEditViewProps { @@ -26,17 +28,47 @@ const DocumentEditView = forwardRef( const [feedback, setFeedback] = useState<{ type: 'success' | 'error'; message: string } | null>(null); const { theme } = useTheme(); const [isSaving, setIsSaving] = useState(false); + const [isConflictDialogOpen, setIsConflictDialogOpen] = useState(false); + const [conflictServerDocStr, setConflictServerDocStr] = useState(''); useImperativeHandle(ref, () => ({ getCurrentValue: () => jsonValue, setCurrentValue: (val: string) => setJsonValue(val) })); - const handleSave = async () => { + const handleSave = async (forceSave = false) => { setIsSaving(true); try { const parsed = JSON.parse(jsonValue); if (!accountId || !databaseName || !collection || !docId) throw new Error('Missing DB info'); + + if (!forceSave) { + // Fetch the latest document from DB + const refreshed = await getSingleDocument(accountId, databaseName, collection, docId); + + // Compare with the original document prop + const ignoredKeys = ['_id', 'datetime_creation', 'datetime_last_modified']; + const oldWithoutIgnored = omit(document, ignoredKeys); + const newWithoutIgnored = omit(refreshed, ignoredKeys); + + if (!isEqual(oldWithoutIgnored, newWithoutIgnored)) { + // Sync ignored fields to match user's expected view without highlighting them + const displayServerDoc = { ...refreshed }; + ignoredKeys.forEach(key => { + if (key in parsed) { + displayServerDoc[key] = parsed[key]; + } else { + delete displayServerDoc[key]; + } + }); + + setConflictServerDocStr(JSON.stringify(displayServerDoc, null, 2)); + setIsConflictDialogOpen(true); + setIsSaving(false); + return; + } + } + await updateDocument(accountId, databaseName, collection, docId, parsed); // Fetch the latest document after update const refreshed = await getSingleDocument(accountId, databaseName, collection, docId); @@ -56,7 +88,7 @@ const DocumentEditView = forwardRef(

Edit Document

-
@@ -96,6 +128,16 @@ const DocumentEditView = forwardRef( {feedback.message}
)} + setIsConflictDialogOpen(false)} + onOverwrite={() => { + setIsConflictDialogOpen(false); + handleSave(true); + }} + />
); }); diff --git a/frontend/components/SaveConflictDialog.tsx b/frontend/components/SaveConflictDialog.tsx new file mode 100644 index 0000000..466f9fc --- /dev/null +++ b/frontend/components/SaveConflictDialog.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import ReactDiffViewer from 'react-diff-viewer-continued'; +import { useTheme } from '../contexts/ThemeContext'; +import { WarningIcon } from './icons/material-icons-imports'; + +interface SaveConflictDialogProps { + open: boolean; + serverValue: string; // The newly fetched data from server + localValue: string; // The user's current edited value + onClose: () => void; + onOverwrite: () => void; +} + +const SaveConflictDialog: React.FC = ({ open, serverValue, localValue, onClose, onOverwrite }) => { + const { theme } = useTheme(); + + if (!open) return null; + + return ( +
+
+
+

+ Save Conflict Detected +

+

+ The document on the server has been modified since you started editing. + If you save now, you will overwrite the server's changes with your own. +

+
+ +
+ +
+ +
+ + +
+
+
+ ); +}; + +export default SaveConflictDialog; diff --git a/frontend/pages/DataExplorerPage.tsx b/frontend/pages/DataExplorerPage.tsx index b415292..92f26c5 100644 --- a/frontend/pages/DataExplorerPage.tsx +++ b/frontend/pages/DataExplorerPage.tsx @@ -145,6 +145,14 @@ const DeleteDocumentDialog: React.FC<{ ); }; +export interface OpenDocument { + id: string; + doc: Record; + collectionName: string; + editMode: boolean; + breadcrumbs: BreadcrumbItem[]; +} + const DataExplorerPage: React.FC = ({ resource, dbInfo, @@ -208,8 +216,7 @@ const DataExplorerPage: React.FC = ({ }; // --- Editor State --- - const [selectedDocument, setSelectedDocument] = useState | null>(null); - const [editMode, setEditMode] = useState(false); + const [openDocuments, setOpenDocuments] = useState([]); // --- Create Document Dialog State --- const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); @@ -221,15 +228,16 @@ const DataExplorerPage: React.FC = ({ const [isDeleting, setIsDeleting] = React.useState(false); // --- Document History Dialog State --- - const [isHistoryDialogOpen, setIsHistoryDialogOpen] = React.useState(false); + const [historyTargetDoc, setHistoryTargetDoc] = React.useState<{ docId: string, collectionName: string } | null>(null); // --- Diff Overwrite Dialog State --- const [isDiffOverwriteDialogOpen, setIsDiffOverwriteDialogOpen] = useState(false); const [diffIncomingDocument, setDiffIncomingDocument] = useState | null>(null); const [diffCurrentEditedText, setDiffCurrentEditedText] = useState(''); + const [diffTargetDocId, setDiffTargetDocId] = useState(null); // --- Editor Ref --- - const editorRef = useRef(null); + const editorRefs = useRef>({}); // Helper to infer schema for new document (mimic CollectionActionPanel logic) const getInitialDocFromSchema = useCallback(() => { @@ -304,7 +312,13 @@ const DataExplorerPage: React.FC = ({ const { addDocument } = await import('../services/dbService'); const newDoc = await addDocument(selectedCollection, currentResource, doc); setDocuments(prev => [newDoc, ...prev]); - setSelectedDocument(newDoc); + setOpenDocuments(prev => [{ + id: Math.random().toString(36).substring(7), + doc: newDoc, + collectionName: selectedCollection, + editMode: false, + breadcrumbs: [] + }, ...prev]); setIsCreateDialogOpen(false); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to create document.'); @@ -323,9 +337,7 @@ const DataExplorerPage: React.FC = ({ const docId = getDocId(deleteTargetDoc); await deleteDocument(selectedCollection, currentResource, docId); setDocuments(prev => prev.filter(doc => getDocId(doc) !== docId)); - if (selectedDocument && getDocId(selectedDocument) === docId) { - setSelectedDocument(null); - } + setOpenDocuments(prev => prev.filter(od => getDocId(od.doc) !== docId)); setIsDeleteDialogOpen(false); setDeleteTargetDoc(null); } catch (e) { @@ -333,14 +345,14 @@ const DataExplorerPage: React.FC = ({ } finally { setIsDeleting(false); } - }, [selectedCollection, deleteTargetDoc, currentResource, setDocuments, selectedDocument]); + }, [selectedCollection, deleteTargetDoc, currentResource, setDocuments]); // --- Sorting State --- const [collectionSortKey, setCollectionSortKey] = useState<'name' | 'count'>('name'); const [collectionSortOrder, setCollectionSortOrder] = useState<'asc' | 'desc'>('asc'); // --- Navigation State --- - const [breadcrumbs, setBreadcrumbs] = useState([]); + // global breadcrumbs removed // --- Pinned Documents State --- const [pinnedDocuments, setPinnedDocuments] = useState([]); @@ -379,8 +391,7 @@ const DataExplorerPage: React.FC = ({ setDebouncedFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); setSchemaTree([]); setIsFetchingSchema(false); - setSelectedDocument(null); - setBreadcrumbs([]); + setOpenDocuments([]); // Do not reset pinned documents here, as they should persist across DB/collection changes. }, []); @@ -389,7 +400,7 @@ const DataExplorerPage: React.FC = ({ const handler = setTimeout(() => { setDebouncedFilters(filters); setCurrentPage(1); // Reset to page 1 on new search - setSelectedDocument(null); // Clear selection on new search + setOpenDocuments([]); // Clear selection on new search }, 300); return () => clearTimeout(handler); }, [filters]); @@ -474,9 +485,8 @@ const DataExplorerPage: React.FC = ({ }, [selectedCollection, currentResource, currentPage, debouncedFilters]); useEffect(() => { - if (breadcrumbs.length > 0 && selectedDocument) return; fetchDocuments(); - }, [fetchDocuments, breadcrumbs.length, selectedDocument]); + }, [fetchDocuments]); const fetchSchemaForCollection = useCallback(async (collectionName: string) => { if (!collectionName || !currentResource) return; @@ -520,8 +530,14 @@ const DataExplorerPage: React.FC = ({ await fetchSchemaForCollection(result.collectionName); // Set the found document - console.log('Setting selected document:', result.document); - setSelectedDocument(result.document); + console.log('Setting open document:', result.document); + setOpenDocuments([{ + id: Math.random().toString(36).substring(7), + doc: result.document, + collectionName: result.collectionName, + editMode: false, + breadcrumbs: [] + }]); } catch (error) { console.error('Failed to load initial document:', error); @@ -596,16 +612,13 @@ const DataExplorerPage: React.FC = ({ setCurrentPage(1); setFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); setDebouncedFilters([{ id: 'default', key: 'all', value: '', isCustom: false, operator: 'equals', type: 'string' }]); - setSelectedDocument(null); - setBreadcrumbs([]); + setOpenDocuments([]); await fetchSchemaForCollection(collectionName); }, [fetchSchemaForCollection, selectedCollection]); const handleRefresh = useCallback(() => { if (!selectedCollection) return; - setSelectedDocument(null); setError(null); - setBreadcrumbs([]); fetchSchemaForCollection(selectedCollection); fetchDocuments(); }, [selectedCollection, fetchSchemaForCollection, fetchDocuments]); @@ -613,7 +626,6 @@ const DataExplorerPage: React.FC = ({ const handlePageChange = (newPage: number) => { if (newPage < 1 || newPage > totalPages || newPage === currentPage) return; setCurrentPage(newPage); - setSelectedDocument(null); } const handlePageInputSubmit = (e: React.KeyboardEvent | React.FocusEvent) => { @@ -646,8 +658,8 @@ const DataExplorerPage: React.FC = ({ }); }, [currentDb, collectionSortKey, collectionSortOrder]); - const handleObjectIdClick = useCallback(async (objectId: string, keyContext?: string, openInNewTab?: boolean) => { - if (!selectedDocument || !selectedCollection || !currentDb) return; + const handleObjectIdClick = useCallback(async (openDocId: string | null, objectId: string, keyContext?: string, openInNewTab?: boolean) => { + if (!currentDb) return; if (openInNewTab) { // Generate URL for new tab - let backend find which collection contains the document @@ -662,15 +674,31 @@ const DataExplorerPage: React.FC = ({ setIsLoading(true); setError(null); - const currentBreadcrumb: BreadcrumbItem = { collectionName: selectedCollection, document: selectedDocument }; + const openDoc = openDocId ? openDocuments.find(d => d.id === openDocId) : null; + const currentBreadcrumb: BreadcrumbItem | null = openDoc ? { collectionName: openDoc.collectionName, document: openDoc.doc } : null; try { const collectionNames = currentDb.collections.map(c => c.name); const result = await findDocumentById(objectId, currentResource, collectionNames, keyContext); - setBreadcrumbs(prev => [...prev, currentBreadcrumb]); - setSelectedCollection(result.collectionName); - setSelectedDocument(result.document); + if (openDocId && openDoc) { + setOpenDocuments(prev => prev.map(od => + od.id === openDocId + ? { ...od, collectionName: result.collectionName, doc: result.document, breadcrumbs: [...od.breadcrumbs, currentBreadcrumb!] } + : od + )); + } else { + setOpenDocuments(prev => { + if (prev.some(od => getDocId(od.doc) === getDocId(result.document))) return prev; + return [...prev, { + id: Math.random().toString(36).substring(7), + doc: result.document, + collectionName: result.collectionName, + editMode: false, + breadcrumbs: [] + }]; + }); + } await fetchSchemaForCollection(result.collectionName); } catch (e) { if (e instanceof Error) setError(e.message); @@ -678,17 +706,22 @@ const DataExplorerPage: React.FC = ({ } finally { setIsLoading(false); } - }, [selectedDocument, selectedCollection, currentResource, currentDb, currentAccount.id, fetchSchemaForCollection]); + }, [openDocuments, currentResource, currentDb, currentAccount.id, fetchSchemaForCollection]); - const handleBreadcrumbClick = useCallback(async (index: number) => { - const targetState = breadcrumbs[index]; - const newBreadcrumbs = breadcrumbs.slice(0, index); + const handleBreadcrumbClick = useCallback(async (openDocId: string, index: number) => { + const openDoc = openDocuments.find(d => d.id === openDocId); + if (!openDoc) return; - setBreadcrumbs(newBreadcrumbs); - setSelectedCollection(targetState.collectionName); - setSelectedDocument(targetState.document); + const targetState = openDoc.breadcrumbs[index]; + const newBreadcrumbs = openDoc.breadcrumbs.slice(0, index); + + setOpenDocuments(prev => prev.map(od => + od.id === openDocId + ? { ...od, collectionName: targetState.collectionName, doc: targetState.document, breadcrumbs: newBreadcrumbs } + : od + )); await fetchSchemaForCollection(targetState.collectionName); - }, [breadcrumbs, fetchSchemaForCollection]); + }, [openDocuments, fetchSchemaForCollection]); const handleClearDocCache = useCallback(async () => { setCacheClearStatus('loading'); @@ -751,7 +784,7 @@ const DataExplorerPage: React.FC = ({
    {documents.map((doc) => { const docId = getDocId(doc); - const isSelected = selectedDocument && getDocId(selectedDocument) === docId; + const isSelected = openDocuments.some(od => getDocId(od.doc) === docId); const isPinned = isDocumentPinned(doc); return ( @@ -760,7 +793,33 @@ const DataExplorerPage: React.FC = ({ className={`flex items-center justify-between transition-colors group ${isSelected ? 'bg-blue-100 dark:bg-blue-900/50' : 'hover:bg-slate-100 dark:hover:bg-slate-700/50'}`} >
    - + handleObjectIdClick(null, objId, keyCtx, openNewTab)} />
    ))} @@ -1182,8 +1191,8 @@ const DataExplorerPage: React.FC = ({ {/* Main Content Area */} -
    - +
    + {/* Column 1: Collections */}
    @@ -1406,174 +1415,185 @@ const DataExplorerPage: React.FC = ({ {/* Column 3: Document Editor */} -
    -
    -
    -
    -

    Editor

    - {selectedDocument && ( -
    - ID: - {getDocId(selectedDocument)} - - - {!editMode && ( - <> - +
    + {openDocuments.length === 0 ? ( +
    +

    Select a document to open it here.

    +
    + ) : ( + openDocuments.map((openDoc) => ( +
    +
    +
    +
    +

    {openDoc.collectionName}

    +
    + {getDocId(openDoc.doc)} - - + {!openDoc.editMode && ( + <> + + + + + + )} + {openDoc.editMode && ( + + )} +
    +
    + +
    + + {/* Breadcrumbs */} + {openDoc.breadcrumbs.length > 0 && ( +
    + {openDoc.breadcrumbs.map((crumb, i) => ( + + + + + ))} + + {openDoc.collectionName} / {getDocId(openDoc.doc)} + +
    + )} + + {/* Content */} +
    + {!openDoc.editMode && ( +
    + handleObjectIdClick(openDoc.id, objId, keyCtx, openNewTab)} /> +
    )} - {editMode && ( - + {openDoc.editMode && ( + { + if (el) editorRefs.current[openDoc.id] = el; + else delete editorRefs.current[openDoc.id]; + }} + accountId={currentResource.accountId} + databaseName={currentResource.databaseName} + document={openDoc.doc} + collection={openDoc.collectionName} + docId={getDocId(openDoc.doc)} + loading={isLoading} + onCancel={() => handleEditCancel(openDoc.id)} + onSave={() => handleEditSave(openDoc.id)} + /> )}
    - )} -
    - {selectedDocument && !editMode && } -
    - - {/* Breadcrumbs */} - {breadcrumbs.length > 0 && selectedDocument && ( -
    - {breadcrumbs.map((crumb, i) => ( - - - - - ))} - - {selectedCollection} / {getDocId(selectedDocument)} - +
    - )} - {renderEditorPanel()} -
    + )) + )}
    @@ -1599,11 +1619,11 @@ const DataExplorerPage: React.FC = ({ /> {/* Document History Dialog */} setIsHistoryDialogOpen(false)} + onClose={() => setHistoryTargetDoc(null)} /> {/* Diff Overwrite Dialog */} = ({ newValue={displayDiffNewValue} onClose={() => setIsDiffOverwriteDialogOpen(false)} onOverwrite={() => { - if (diffIncomingDocument) { - setSelectedDocument(diffIncomingDocument); + if (diffIncomingDocument && diffTargetDocId) { + setOpenDocuments(prev => prev.map(od => od.id === diffTargetDocId ? { ...od, doc: diffIncomingDocument } : od)); setPinnedDocuments(prev => prev.map(p => getDocId(p.doc) === getDocId(diffIncomingDocument) ? { ...p, doc: diffIncomingDocument } : p )); - if (editMode && editorRef.current) { - editorRef.current.setCurrentValue(JSON.stringify(diffIncomingDocument, null, 2)); + const od = openDocuments.find(d => d.id === diffTargetDocId); + if (od?.editMode && editorRefs.current[diffTargetDocId]) { + editorRefs.current[diffTargetDocId]!.setCurrentValue(JSON.stringify(diffIncomingDocument, null, 2)); } } setIsDiffOverwriteDialogOpen(false); + setDiffTargetDocId(null); }} />