import * as plugins from '../plugins.js'; import { apiService } from '../services/index.js'; import { themeStyles } from '../styles/index.js'; const { html, css, cssManager, customElement, property, state, DeesElement } = plugins; const { DeesContextmenu } = plugins.deesCatalog; @customElement('tsview-mongo-documents') export class TsviewMongoDocuments extends DeesElement { @property({ type: String }) public accessor databaseName: string = ''; @property({ type: String }) public accessor collectionName: string = ''; @state() private accessor documents: Record[] = []; @state() private accessor total: number = 0; @state() private accessor page: number = 1; @state() private accessor pageSize: number = 50; @state() private accessor loading: boolean = false; @state() private accessor filterText: string = ''; @state() private accessor selectedId: string = ''; public static styles = [ cssManager.defaultStyles, themeStyles, css` :host { display: block; height: 100%; overflow: hidden; } .documents-container { display: flex; flex-direction: column; height: 100%; } .filter-bar { display: flex; gap: 12px; padding: 12px; border-bottom: 1px solid #333; } .filter-input { flex: 1; padding: 8px 12px; background: rgba(0, 0, 0, 0.3); border: 1px solid #444; border-radius: 6px; color: #fff; font-size: 13px; font-family: monospace; } .filter-input:focus { outline: none; border-color: #404040; } .filter-input::placeholder { color: #666; } .filter-btn { padding: 8px 16px; background: rgba(255, 255, 255, 0.1); border: 1px solid #404040; color: #e0e0e0; border-radius: 6px; cursor: pointer; font-size: 13px; } .filter-btn:hover { background: rgba(255, 255, 255, 0.15); } .documents-list { flex: 1; overflow-y: auto; padding: 8px; } .document-row { padding: 10px 12px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; background: rgba(0, 0, 0, 0.2); transition: background 0.1s; } .document-row:hover { background: rgba(255, 255, 255, 0.05); } .document-row.selected { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15); } .document-id { font-size: 12px; color: #e0e0e0; font-family: monospace; margin-bottom: 4px; } .document-preview { font-size: 12px; color: #888; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } .pagination { display: flex; align-items: center; justify-content: space-between; padding: 12px; border-top: 1px solid #333; font-size: 13px; color: #888; } .pagination-info { display: flex; align-items: center; gap: 8px; } .pagination-controls { display: flex; gap: 4px; } .page-btn { padding: 6px 12px; background: transparent; border: 1px solid #444; color: #888; border-radius: 4px; cursor: pointer; font-size: 12px; } .page-btn:hover:not(:disabled) { border-color: #666; color: #aaa; } .page-btn:disabled { opacity: 0.5; cursor: not-allowed; } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 200px; color: #666; } .loading-state { display: flex; align-items: center; justify-content: center; height: 200px; color: #888; } .actions-bar { display: flex; justify-content: flex-end; padding: 8px 12px; border-bottom: 1px solid #333; } .action-btn { padding: 6px 12px; background: rgba(34, 197, 94, 0.2); border: 1px solid #22c55e; color: #4ade80; border-radius: 4px; cursor: pointer; font-size: 12px; } .action-btn:hover { background: rgba(34, 197, 94, 0.3); } `, ]; async connectedCallback() { super.connectedCallback(); await this.loadDocuments(); } updated(changedProperties: Map) { if (changedProperties.has('databaseName') || changedProperties.has('collectionName')) { this.page = 1; this.loadDocuments(); } } private async loadDocuments() { if (!this.databaseName || !this.collectionName) return; this.loading = true; try { let filter = {}; if (this.filterText.trim()) { try { filter = JSON.parse(this.filterText); } catch { // Invalid JSON, ignore filter } } const result = await apiService.findDocuments( this.databaseName, this.collectionName, { filter, skip: (this.page - 1) * this.pageSize, limit: this.pageSize, } ); this.documents = result.documents; this.total = result.total; } catch (err) { console.error('Error loading documents:', err); this.documents = []; this.total = 0; } this.loading = false; } /** * Public method to refresh documents (called by parent on change events) */ public async refresh() { await this.loadDocuments(); } private handleFilterInput(e: Event) { this.filterText = (e.target as HTMLInputElement).value; } private handleFilterSubmit() { this.page = 1; this.loadDocuments(); } private handleKeyPress(e: KeyboardEvent) { if (e.key === 'Enter') { this.handleFilterSubmit(); } } private selectDocument(doc: Record) { const id = (doc._id as string) || ''; this.selectedId = id; this.dispatchEvent( new CustomEvent('document-selected', { detail: { documentId: id, document: doc }, bubbles: true, composed: true, }) ); } private goToPage(pageNum: number) { this.page = pageNum; this.loadDocuments(); } private getDocumentPreview(doc: Record): string { const preview: Record = {}; const keys = Object.keys(doc).filter((k) => k !== '_id'); for (const key of keys.slice(0, 3)) { preview[key] = doc[key]; } return JSON.stringify(preview); } private get totalPages(): number { return Math.ceil(this.total / this.pageSize); } private async handleInsertNew() { const newDoc = { // Default empty document createdAt: new Date().toISOString(), }; try { const insertedId = await apiService.insertDocument( this.databaseName, this.collectionName, newDoc ); await this.loadDocuments(); this.selectedId = insertedId; this.dispatchEvent( new CustomEvent('document-selected', { detail: { documentId: insertedId }, bubbles: true, composed: true, }) ); } catch (err) { console.error('Error inserting document:', err); } } private handleDocumentContextMenu(event: MouseEvent, doc: Record) { event.preventDefault(); const docId = doc._id as string; DeesContextmenu.openContextMenuWithOptions(event, [ { name: 'View/Edit', iconName: 'lucide:edit', action: async () => { this.selectDocument(doc); }, }, { name: 'Copy as JSON', iconName: 'lucide:copy', action: async () => { await navigator.clipboard.writeText(JSON.stringify(doc, null, 2)); }, }, { name: 'Duplicate', iconName: 'lucide:copyPlus', action: async () => { const { _id, ...docWithoutId } = doc; const newDoc = { ...docWithoutId, createdAt: new Date().toISOString() }; try { const insertedId = await apiService.insertDocument( this.databaseName, this.collectionName, newDoc ); await this.loadDocuments(); this.selectedId = insertedId; this.dispatchEvent( new CustomEvent('document-selected', { detail: { documentId: insertedId }, bubbles: true, composed: true, }) ); } catch (err) { console.error('Error duplicating document:', err); } }, }, { divider: true }, { name: 'Delete', iconName: 'lucide:trash2', action: async () => { if (confirm(`Delete document "${docId}"?`)) { const result = await apiService.deleteDocument( this.databaseName, this.collectionName, docId ); if (result.success) { await this.loadDocuments(); if (this.selectedId === docId) { this.selectedId = ''; } } } }, }, ]); } render() { const startRecord = (this.page - 1) * this.pageSize + 1; const endRecord = Math.min(this.page * this.pageSize, this.total); return html`
${this.loading ? html`
Loading...
` : this.documents.length === 0 ? html`
No documents found
` : this.documents.map( (doc) => html`
this.selectDocument(doc)} @contextmenu=${(e: MouseEvent) => this.handleDocumentContextMenu(e, doc)} >
_id: ${doc._id}
${this.getDocumentPreview(doc)}
` )}
${this.total > 0 ? html` ` : ''}
`; } }