import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Plus, Upload } from 'lucide-react'; import PencilIcon from '@/assets/icons/pencil.svg?react'; import RefreshIcon from '@/assets/icons/refresh.svg?react'; import { useTables } from '@/features/database/hooks/useTables'; import { useRecords } from '@/features/database/hooks/useRecords'; import { TableSidebar } from '@/features/database/components/TableSidebar'; import { RecordFormDialog } from '@/features/database/components/RecordFormDialog'; import { TableForm } from '@/features/database/components/TableForm'; import { TablesEmptyState } from '@/features/database/components/TablesEmptyState'; import { TemplatePreview } from '@/features/database/components/TemplatePreview'; import { DATABASE_TEMPLATES, DatabaseTemplate } from '@/features/database/templates'; import { Alert, AlertDescription, Button, ConfirmDialog, ConnectCTA, EmptyState, SearchInput, SelectionClearButton, DeleteActionButton, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components'; import { useConfirm } from '@/lib/hooks/useConfirm'; import { useToast } from '@/lib/hooks/useToast'; import { DatabaseDataGrid } from '@/features/database/components/DatabaseDataGrid'; import { SortColumn } from 'react-data-grid'; import { convertValueForColumn } from '@/lib/utils/utils'; import { DataUpdatePayload, DataUpdateResourceType, ServerEvents, SocketMessage, useSocket, } from '@/lib/contexts/SocketContext'; import { useCSVImport } from '@/features/database/hooks/useCSVImport'; const PAGE_SIZE = 50; export default function TablesPage() { // Load selected table from localStorage on mount const [selectedTable, setSelectedTable] = useState(() => { return localStorage.getItem('selectedTable'); }); const [pendingTableSelection, setPendingTableSelection] = useState(); const [showRecordForm, setShowRecordForm] = useState(false); const [isTableFormDirty, setIsTableFormDirty] = useState(false); const [showTableForm, setShowTableForm] = useState(false); const [editingTable, setEditingTable] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [selectedRows, setSelectedRows] = useState>(new Set()); const [sortColumns, setSortColumns] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [isSorting, setIsSorting] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [previewingTemplate, setPreviewingTemplate] = useState(null); const { confirm, confirmDialogProps } = useConfirm(); const { showToast } = useToast(); const queryClient = useQueryClient(); const fileInputRef = useRef(null); const { tables, isLoadingTables, tablesError, deleteTable, useTableSchema, refetchTables } = useTables(); const recordsHook = useRecords(selectedTable || ''); const { socket, isConnected } = useSocket(); const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file && selectedTable) { importCSV(file); } // Reset file input to allow re-uploading the same file if (event.target) { event.target.value = ''; } }; // Persist selected table to localStorage when it changes useEffect(() => { if (selectedTable) { localStorage.setItem('selectedTable', selectedTable); } else { localStorage.removeItem('selectedTable'); } }, [selectedTable]); // Reset page when search query or selected table changes useEffect(() => { setCurrentPage(1); }, [searchQuery, selectedTable]); // Clear selected rows when table changes useEffect(() => { setSelectedRows(new Set()); }, [selectedTable]); // Safe sort columns change handler const handleSortColumnsChange = useCallback( (newSortColumns: SortColumn[]) => { try { setIsSorting(true); setSortColumns(newSortColumns); // isSorting will be reset when the query completes } catch { // Clear sorting on error setSortColumns([]); setIsSorting(false); showToast('Sorting failed. Please try a different sort option.', 'error'); } }, [showToast] ); // Fetch schema for selected table const { data: schemaData } = useTableSchema(selectedTable || '', !!selectedTable); // Fetch schema for editing table const { data: editingTableSchema } = useTableSchema(editingTable || '', !!editingTable); const primaryKeyColumn = useMemo(() => { return schemaData?.columns.find((col) => col.isPrimaryKey)?.columnName; }, [schemaData]); const { mutate: importCSV, isPending: isImporting, reset: resetImport, } = useCSVImport(selectedTable || '', { onSuccess: (data) => { if (data.success) { showToast(data.message || 'Import successful!', 'success'); void refetchTableData(); } else { // This case handles validation errors returned with a 200 OK but success: false const errorMessage = data.message || 'CSV import failed due to validation errors. Please check the file.'; showToast(errorMessage, 'error'); } resetImport(); }, onError: (error: Error) => { // This handles 400/500 errors from the API client const message = error?.message || 'An unexpected error occurred during import. Please try again.'; showToast(message, 'error'); resetImport(); }, }); // Fetch table records using the hook const offset = (currentPage - 1) * PAGE_SIZE; const { data: recordsData, isLoading: isLoadingRecords, error: recordsError, refetch: refetchRecords, } = recordsHook.useTableRecords( PAGE_SIZE, offset, searchQuery, sortColumns, !!selectedTable && !!schemaData ); // Combine schema and records data const tableData = selectedTable && schemaData && recordsData ? { name: selectedTable, schema: schemaData, records: recordsData.records, totalRecords: recordsData.pagination.total ?? schemaData.recordCount, } : null; const isLoadingTable = isLoadingRecords; const tableError = recordsError; const refetchTableData = refetchRecords; useEffect(() => { if (!socket || !isConnected) { return; } const handleDataUpdate = (message: SocketMessage) => { if (message.payload?.resource === DataUpdateResourceType.DATABASE) { // Invalidate all tables queries void queryClient.invalidateQueries({ queryKey: ['tables'] }); void queryClient.invalidateQueries({ queryKey: ['records', selectedTable] }); } if (message.payload?.resource === DataUpdateResourceType.RECORDS) { // Invalidate records queries for the updated table const data = message.payload.data as { tableName?: string }; const updatedTableName = data?.tableName; // Only invalidate if this is the currently selected table if (updatedTableName && updatedTableName === selectedTable) { void queryClient.invalidateQueries({ queryKey: ['records', selectedTable] }); } } }; socket.on(ServerEvents.DATA_UPDATE, handleDataUpdate); return () => { socket.off(ServerEvents.DATA_UPDATE, handleDataUpdate); }; }, [socket, isConnected, queryClient, selectedTable]); // Reset sorting flag when loading completes useEffect(() => { if (!isLoadingTable && isSorting) { setIsSorting(false); } }, [isLoadingTable, isSorting]); // Auto-select first table (excluding system tables) useEffect(() => { if (!isLoadingTables && tables) { if (pendingTableSelection && tables.includes(pendingTableSelection)) { setSelectedTable(pendingTableSelection); setPendingTableSelection(undefined); return; } if (selectedTable && !tables.includes(selectedTable)) { setSelectedTable(null); return; } if (!selectedTable && tables.length && !showTableForm && !pendingTableSelection) { setSelectedTable(tables[0]); } } }, [tables, pendingTableSelection, selectedTable, showTableForm, isLoadingTables]); const handleRefresh = async () => { setIsRefreshing(true); try { // Reset all state setSelectedRows(new Set()); setSortColumns([]); setSearchQuery(''); setIsSorting(false); // Refresh current table data (if table is selected) if (selectedTable) { await refetchTableData(); } await refetchTables(); } finally { setIsRefreshing(false); } }; const handleTableFormClose = async (): Promise => { if (isTableFormDirty) { const confirmOptions = { title: 'Unsaved Changes', description: `You have unsaved changes. Do you want to discard the changes and exit the form?`, confirmText: 'Discard', destructive: true, }; const shouldDiscard = await confirm(confirmOptions); if (shouldDiscard) { setShowTableForm(false); setEditingTable(null); return true; } else { return false; } } else { setShowTableForm(false); return true; } }; const handleSelectTable = (tableName: string) => { if (showTableForm) { void handleTableFormClose().then((discarded) => { if (discarded) { setSelectedTable(tableName); } }); } else { setSelectedTable(tableName); } }; const handleCreateTable = () => { setSelectedTable(null); setEditingTable(null); setShowTableForm(true); }; const handleEditTable = (tableName: string) => { setSelectedTable(tableName); setEditingTable(tableName); setShowTableForm(true); }; const handleDeleteTable = async (tableName: string) => { const confirmOptions = { title: 'Delete Table', description: `Are you sure you want to delete the table "${tableName}"? This will permanently delete all records in this table. This action cannot be undone.`, confirmText: 'Delete', destructive: true, }; const shouldDelete = await confirm(confirmOptions); if (shouldDelete) { // Update selectedTable BEFORE deleting to prevent queries on deleted table if (selectedTable === tableName) { setSelectedTable(null); } deleteTable(tableName); } }; const handleTemplateClick = (template: DatabaseTemplate) => { setPreviewingTemplate(template); }; const handleCancelPreview = () => { setPreviewingTemplate(null); }; // Handle record update const handleRecordUpdate = async (rowId: string, columnKey: string, newValue: string) => { if (!selectedTable) { return; } try { // Find column schema to determine the correct type conversion const columnSchema = tableData?.schema?.columns?.find((col) => col.columnName === columnKey); if (columnSchema) { // Convert value based on column type using utility function const conversionResult = convertValueForColumn(columnSchema.type, newValue); if (!conversionResult.success) { showToast(conversionResult.error || 'Invalid value', 'error'); return; } const updates = { [columnKey]: conversionResult.value }; await recordsHook.updateRecord({ pkColumn: primaryKeyColumn || 'id', pkValue: rowId, data: updates, }); } } catch (error) { showToast('Failed to update record', 'error'); throw error; } }; // Handle bulk delete const handleBulkDelete = async (ids: string[]) => { if (!selectedTable || !ids.length) { return; } const shouldDelete = await confirm({ title: `Delete ${ids.length} ${ids.length === 1 ? 'Record' : 'Records'}`, description: `Are you sure you want to delete ${ids.length} ${ids.length === 1 ? 'record' : 'records'}? This action cannot be undone.`, confirmText: 'Delete', destructive: true, }); if (shouldDelete) { await recordsHook.deleteRecords({ pkColumn: primaryKeyColumn || 'id', pkValues: ids }); // Query invalidation is handled by the mutation, no manual refetch needed setSelectedRows(new Set()); } }; const error = tablesError || tableError; // Calculate pagination const totalPages = Math.ceil((tableData?.totalRecords || 0) / PAGE_SIZE); // Show empty state when there are no tables and not loading const showEmptyState = !isLoadingTables && tables?.length === 0 && !showTableForm; // Show template preview - takes full width without sidebar if (previewingTemplate) { return ; } return (
{/* Secondary Sidebar - Table List */} void handleDeleteTable(tableName)} /> {/* Main Content Area */}
{showTableForm ? ( // Show TableForm replacing entire main content area { if (!open) { void handleTableFormClose(); } }} mode={editingTable ? 'edit' : 'create'} editTable={editingTable ? editingTableSchema : undefined} setFormIsDirty={setIsTableFormDirty} onSuccess={(newTableName?: string) => { void refetchTables(); void refetchTableData(); setShowTableForm(false); setPendingTableSelection(newTableName); }} /> ) : ( // Show normal content with header <> {/* Sticky Header Section */} {selectedTable && (
{/* Page Header with Breadcrumb */}
{/* Separator */}
{/* Action buttons group */}

Edit Table

{isRefreshing ? 'Refreshing...' : 'Refresh'}

{/* Search Bar and Actions - only show when table is selected */} {selectedTable && (
{selectedRows.size > 0 ? (
setSelectedRows(new Set())} /> { void handleBulkDelete(Array.from(selectedRows))} /> }
) : ( )}
{selectedRows.size === 0 && ( <> {/* Import CSV Button */} {/* Add Record Button */} )}
)}
)} {/* Content - Full height without padding for table to fill */}
{error && ( {String(error)} )} {showEmptyState ? ( ) : !selectedTable ? (
) : ( String(row[primaryKeyColumn || 'id'])} selectedRows={selectedRows} onSelectedRowsChange={setSelectedRows} sortColumns={sortColumns} onSortColumnsChange={handleSortColumnsChange} onCellEdit={handleRecordUpdate} onJumpToTable={setSelectedTable} currentPage={currentPage} totalPages={totalPages} pageSize={PAGE_SIZE} totalRecords={tableData?.totalRecords || 0} onPageChange={setCurrentPage} emptyState={
{searchQuery ? 'No records match your search criteria' : 'No records found'}.{' '}
} /> )}
)}
{/* Add Record Form */} {selectedTable && schemaData && ( // In the RecordForm onSuccess callback )} {/* Confirm Dialog */}
); }