@catalystlabs/awm
Version:
Appwrite Migration Tool - Schema management and code generation for Appwrite databases
361 lines (332 loc) • 10.3 kB
JavaScript
'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>
</>
);
}