UNPKG

@catalystlabs/awm

Version:

Appwrite Migration Tool - Schema management and code generation for Appwrite databases

361 lines (332 loc) 10.3 kB
'use client'; import { useState } from 'react'; import { Table, Button, IconButton, Tag, Modal, Message, toaster, Input, Form, Loader } from 'rsuite'; import { Trash2, Edit, Eye } from 'lucide-react'; import { formatDate, truncate } from '@/lib/utils'; const { Column, HeaderCell, Cell } = Table; export default function DocumentTable({ collection, documents, onRefresh }) { const [selectedDoc, setSelectedDoc] = useState(null); const [showViewModal, setShowViewModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [loading, setLoading] = useState(false); const [formValue, setFormValue] = useState({}); function handleViewDocument(doc) { setSelectedDoc(doc); setShowViewModal(true); } function handleEditDocument(doc) { setSelectedDoc(doc); // Extract only editable fields (exclude system fields) const editableData = {}; collection.attributes.forEach(attr => { if (attr.type !== 'relationship') { editableData[attr.key] = doc[attr.key]; } }); setFormValue(editableData); setShowEditModal(true); } async function handleUpdateDocument() { try { setLoading(true); const response = await fetch( `/api/collections/${collection.$id}/documents/${selectedDoc.$id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: formValue }) } ); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to update document'); } toaster.push( <Message showIcon type="success" closable> Document updated successfully </Message>, { placement: 'topEnd' } ); setShowEditModal(false); onRefresh(); } catch (error) { toaster.push( <Message showIcon type="error" closable> {error.message} </Message>, { placement: 'topEnd' } ); } finally { setLoading(false); } } async function handleDeleteDocument() { try { setLoading(true); const response = await fetch( `/api/collections/${collection.$id}/documents/${selectedDoc.$id}`, { method: 'DELETE' } ); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to delete document'); } toaster.push( <Message showIcon type="success" closable> Document deleted successfully </Message>, { placement: 'topEnd' } ); setShowDeleteModal(false); onRefresh(); } catch (error) { toaster.push( <Message showIcon type="error" closable> {error.message} </Message>, { placement: 'topEnd' } ); } finally { setLoading(false); } } function renderCellValue(value) { if (value === null || value === undefined) { return <span style={{ color: '#ccc', fontStyle: 'italic' }}>null</span>; } if (typeof value === 'boolean') { return value ? '✓ true' : '✗ false'; } if (typeof value === 'object' && !Array.isArray(value)) { return <Tag color="cyan">Object</Tag>; } if (Array.isArray(value)) { return <Tag color="orange">Array[{value.length}]</Tag>; } return truncate(String(value), 50); } const visibleAttributes = collection.attributes .filter(attr => attr.type !== 'relationship') .slice(0, 5); if (documents.length === 0) { return ( <div className="empty-state"> <div style={{ fontSize: '48px', marginBottom: '16px' }}>📭</div> <h4>No documents yet</h4> <p style={{ color: '#999', marginTop: '8px' }}> Create your first document to get started </p> </div> ); } return ( <> <Table height={500} data={documents} onRowClick={handleViewDocument} hover style={{ cursor: 'pointer' }} loading={loading} > <Column width={200} fixed> <HeaderCell>Document ID</HeaderCell> <Cell> {rowData => ( <code style={{ fontSize: '12px' }}>{truncate(rowData.$id, 20)}</code> )} </Cell> </Column> {visibleAttributes.map(attr => ( <Column key={attr.key} width={200} flexGrow={1}> <HeaderCell>{attr.key}</HeaderCell> <Cell> {rowData => renderCellValue(rowData[attr.key])} </Cell> </Column> ))} <Column width={150}> <HeaderCell>Created</HeaderCell> <Cell> {rowData => ( <span style={{ fontSize: '12px', color: '#666' }}> {formatDate(rowData.$createdAt)} </span> )} </Cell> </Column> <Column width={150} fixed="right"> <HeaderCell>Actions</HeaderCell> <Cell> {rowData => ( <div style={{ display: 'flex', gap: '4px' }}> <IconButton icon={<Eye size={16} />} appearance="subtle" size="xs" onClick={(e) => { e.stopPropagation(); handleViewDocument(rowData); }} /> <IconButton icon={<Edit size={16} />} appearance="subtle" size="xs" onClick={(e) => { e.stopPropagation(); handleEditDocument(rowData); }} /> <IconButton icon={<Trash2 size={16} />} appearance="subtle" size="xs" color="red" onClick={(e) => { e.stopPropagation(); setSelectedDoc(rowData); setShowDeleteModal(true); }} /> </div> )} </Cell> </Column> </Table> {/* View Document Modal */} <Modal size="lg" open={showViewModal} onClose={() => setShowViewModal(false)} > <Modal.Header> <Modal.Title>Document Details</Modal.Title> </Modal.Header> <Modal.Body> {selectedDoc && ( <div> <pre style={{ background: '#f5f5f5', padding: '16px', borderRadius: '4px', overflow: 'auto', maxHeight: '500px', fontSize: '12px', fontFamily: 'monospace' }}> {JSON.stringify(selectedDoc, null, 2)} </pre> </div> )} </Modal.Body> <Modal.Footer> <Button onClick={() => setShowViewModal(false)} appearance="subtle"> Close </Button> <Button appearance="primary" onClick={() => { setShowViewModal(false); handleEditDocument(selectedDoc); }} > Edit Document </Button> </Modal.Footer> </Modal> {/* Edit Document Modal */} <Modal size="md" open={showEditModal} onClose={() => setShowEditModal(false)} > <Modal.Header> <Modal.Title>Edit Document</Modal.Title> </Modal.Header> <Modal.Body> <Form fluid formValue={formValue} onChange={setFormValue} > {collection.attributes .filter(attr => attr.type !== 'relationship') .map(attr => ( <Form.Group key={attr.key}> <Form.ControlLabel> {attr.key} {attr.required && <span style={{ color: 'red' }}> *</span>} </Form.ControlLabel> <Form.Control name={attr.key} type={attr.type === 'integer' || attr.type === 'float' ? 'number' : 'text'} placeholder={`Enter ${attr.key}`} /> <Form.HelpText>{attr.type}</Form.HelpText> </Form.Group> ))} </Form> </Modal.Body> <Modal.Footer> <Button onClick={() => setShowEditModal(false)} appearance="subtle"> Cancel </Button> <Button appearance="primary" onClick={handleUpdateDocument} loading={loading} disabled={loading} > {loading ? 'Saving...' : 'Save Changes'} </Button> </Modal.Footer> </Modal> {/* Delete Confirmation Modal */} <Modal size="xs" open={showDeleteModal} onClose={() => setShowDeleteModal(false)} > <Modal.Header> <Modal.Title>Delete Document</Modal.Title> </Modal.Header> <Modal.Body> <p>Are you sure you want to delete this document?</p> {selectedDoc && ( <code style={{ display: 'block', marginTop: '8px', padding: '8px', background: '#f5f5f5', borderRadius: '4px', fontSize: '12px' }}> {selectedDoc.$id} </code> )} <p style={{ marginTop: '8px', color: '#c00' }}> <strong>This action cannot be undone.</strong> </p> </Modal.Body> <Modal.Footer> <Button onClick={() => setShowDeleteModal(false)} appearance="subtle"> Cancel </Button> <Button appearance="primary" color="red" onClick={handleDeleteDocument} loading={loading} disabled={loading} > {loading ? 'Deleting...' : 'Delete'} </Button> </Modal.Footer> </Modal> </> ); }