import { useState, useEffect, useMemo } from 'react'; import { useForm, useFieldArray } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AlertCircle, Plus, X, Link, MoveRight } from 'lucide-react'; import { Alert, AlertDescription, Button, Input } from '@/components'; import { tableService } from '@/features/database/services/table.service'; import { TableFormColumnSchema, TableFormForeignKeySchema, tableFormSchema, TableFormSchema, } from '@/features/database/schema'; import { useToast } from '@/lib/hooks/useToast'; import { TableFormColumn } from './TableFormColumn'; import { ForeignKeyPopover } from './ForeignKeyPopover'; import { ColumnType, TableSchema, UpdateTableSchemaRequest } from '@insforge/shared-schemas'; import { SYSTEM_FIELDS } from '../helpers'; const newColumn: TableFormColumnSchema = { columnName: '', type: ColumnType.STRING, isNullable: true, isUnique: false, defaultValue: '', isSystemColumn: false, isNewColumn: true, }; interface TableFormProps { open: boolean; onOpenChange: (open: boolean) => void; onSuccess?: (newTable?: string) => void; mode?: 'create' | 'edit'; editTable?: TableSchema; setFormIsDirty: (dirty: boolean) => void; } export function TableForm({ open, onOpenChange, onSuccess, mode = 'create', editTable, setFormIsDirty, }: TableFormProps) { const [error, setError] = useState(null); const [showForeignKeyDialog, setShowForeignKeyDialog] = useState(false); const [editingForeignKey, setEditingForeignKey] = useState(); const [foreignKeys, setForeignKeys] = useState([]); const [foreignKeysDirty, setForeignKeysDirty] = useState(false); const queryClient = useQueryClient(); const { showToast } = useToast(); const form = useForm({ resolver: zodResolver(tableFormSchema), defaultValues: { tableName: '', columns: mode === 'create' ? [ { columnName: 'id', type: ColumnType.UUID, defaultValue: 'gen_random_uuid()', isPrimaryKey: true, isNullable: false, isUnique: true, isSystemColumn: true, isNewColumn: false, }, { columnName: 'created_at', type: ColumnType.DATETIME, defaultValue: 'CURRENT_TIMESTAMP', isNullable: true, isUnique: false, isSystemColumn: true, isNewColumn: false, }, { columnName: 'updated_at', type: ColumnType.DATETIME, defaultValue: 'CURRENT_TIMESTAMP', isNullable: true, isUnique: false, isSystemColumn: true, isNewColumn: false, }, { ...newColumn, }, ] : [{ ...newColumn }], }, }); // Reset form when switching between modes or when editTable changes useEffect(() => { // Clear error when effect runs setError(null); if (open && mode === 'edit' && editTable) { form.reset({ tableName: editTable.tableName, columns: editTable.columns.map((col) => ({ columnName: col.columnName, type: col.type, isPrimaryKey: col.isPrimaryKey, isNullable: col.isNullable, isUnique: col.isUnique || false, defaultValue: col.defaultValue || '', originalName: col.columnName, // Track original name for rename detection isSystemColumn: SYSTEM_FIELDS.includes(col.columnName), isNewColumn: false, })), }); // Set foreign keys from editTable const existingForeignKeys = editTable.columns .filter((col) => !SYSTEM_FIELDS.includes(col.columnName) && col.foreignKey) .map((col) => ({ columnName: col.columnName, referenceTable: col.foreignKey?.referenceTable ?? '', referenceColumn: col.foreignKey?.referenceColumn ?? '', onDelete: col.foreignKey?.onDelete || 'NO ACTION', onUpdate: col.foreignKey?.onUpdate || 'NO ACTION', })); setForeignKeys(existingForeignKeys); } else { form.reset({ tableName: '', columns: [ { columnName: 'id', type: ColumnType.UUID, defaultValue: 'gen_random_uuid()', isPrimaryKey: true, isNullable: false, isUnique: true, isSystemColumn: true, isNewColumn: false, }, { columnName: 'created_at', type: ColumnType.DATETIME, defaultValue: 'CURRENT_TIMESTAMP', isNullable: true, isUnique: false, isSystemColumn: true, isNewColumn: false, }, { columnName: 'updated_at', type: ColumnType.DATETIME, defaultValue: 'CURRENT_TIMESTAMP', isNullable: true, isUnique: false, isSystemColumn: true, isNewColumn: false, }, { ...newColumn }, ], }); setForeignKeys([]); } }, [mode, editTable, form, open]); useEffect(() => { setFormIsDirty(form.formState.isDirty); }, [form.formState.isDirty, setFormIsDirty]); const { fields, append, remove } = useFieldArray({ control: form.control, name: 'columns', }); const sortedFields = useMemo(() => { return [...fields].sort((a, b) => { // System fields come first if (a.isSystemColumn && !b.isSystemColumn) { return -1; } if (!a.isSystemColumn && b.isSystemColumn) { return 1; } // Within system fields, maintain the order: id, createdAt, updated_at if (a.isSystemColumn && b.isSystemColumn) { return SYSTEM_FIELDS.indexOf(a.columnName) - SYSTEM_FIELDS.indexOf(b.columnName); } // Keep original order for non-system fields return 0; }); }, [fields]); const createTableMutation = useMutation({ mutationFn: (data: TableFormSchema) => { const columns = data.columns.map((col) => { // Find foreign key for this field if it exists const foreignKey = foreignKeys.find((fk) => fk.columnName === col.columnName); return { columnName: col.columnName, type: col.type, isNullable: col.isNullable, isUnique: col.isUnique, defaultValue: col.defaultValue, // Embed foreign key information directly in the column ...(foreignKey && { foreignKey: { referenceTable: foreignKey.referenceTable, referenceColumn: foreignKey.referenceColumn, onDelete: foreignKey.onDelete, onUpdate: foreignKey.onUpdate, }, }), }; }); return tableService.createTable(data.tableName, columns); }, onSuccess: (data) => { void queryClient.invalidateQueries({ queryKey: ['database-metadata'] }); void queryClient.invalidateQueries({ queryKey: ['tables'] }); void queryClient.invalidateQueries({ queryKey: ['metadata'] }); showToast('Table created successfully!', 'success'); form.reset(); setError(null); setForeignKeys([]); setForeignKeysDirty(false); onSuccess?.(data.tableName); }, onError: (err) => { const errorMessage = err.message || 'Failed to create table'; setError(errorMessage); showToast(errorMessage, 'error'); }, }); const updateTableMutation = useMutation({ mutationFn: (data: TableFormSchema) => { if (!editTable) { return Promise.resolve(); } // Compare fields to determine what operations to perform const addColumns: UpdateTableSchemaRequest['addColumns'] = []; const dropColumns: UpdateTableSchemaRequest['dropColumns'] = []; const updateColumns: UpdateTableSchemaRequest['updateColumns'] = []; const addForeignKeys: UpdateTableSchemaRequest['addForeignKeys'] = []; const dropForeignKeys: UpdateTableSchemaRequest['dropForeignKeys'] = []; // Filter out system columns from existing fields for comparison const existingUserColumns = editTable.columns.filter( (col) => !SYSTEM_FIELDS.includes(col.columnName) ); // Track which original columns we've seen const processedOriginalColumns = new Set(); // Process each field data.columns.forEach((col) => { if (col.originalName) { // This field existed before processedOriginalColumns.add(col.originalName); const newDefaultValue = col.defaultValue || undefined; const originalDefaultValue = editTable.columns.find( (_col) => _col.columnName === col.originalName )?.defaultValue; // Check if it was renamed if (col.originalName !== col.columnName) { updateColumns.push({ columnName: col.originalName, defaultValue: newDefaultValue !== originalDefaultValue ? (newDefaultValue ?? '') : undefined, newColumnName: col.columnName, }); } else if (newDefaultValue !== originalDefaultValue) { updateColumns.push({ columnName: col.columnName, defaultValue: newDefaultValue ?? '', }); } } else { // This is a new field (added via Add Field button) const { ...fieldData } = col; addColumns.push({ ...fieldData, defaultValue: fieldData.defaultValue || undefined, }); } }); // Find dropped columns existingUserColumns.forEach((col) => { if (!processedOriginalColumns.has(col.columnName)) { dropColumns.push(col.columnName); } }); // Handle foreign keys // Get existing foreign keys from editTable const existingForeignKeys = existingUserColumns .filter((col) => col.foreignKey) .map((col) => ({ columnName: col.columnName, ...col.foreignKey, })); // Compare with new foreign keys foreignKeys.forEach((fk) => { const existingFK = existingForeignKeys.find((efk) => efk.columnName === fk.columnName); if (!existingFK) { addForeignKeys.push({ columnName: fk.columnName, foreignKey: { referenceTable: fk.referenceTable, referenceColumn: fk.referenceColumn, onDelete: fk.onDelete, onUpdate: fk.onUpdate, }, }); } }); // Check for dropped foreign keys existingForeignKeys.forEach((efk) => { const stillExists = foreignKeys.find((fk) => fk.columnName === efk.columnName); if (!stillExists) { // This foreign key was removed dropForeignKeys.push(efk.columnName); } }); const operations: UpdateTableSchemaRequest = { addColumns, dropColumns, updateColumns, addForeignKeys, dropForeignKeys, }; if (data.tableName !== editTable.tableName) { operations.renameTable = { newTableName: data.tableName }; } return tableService.updateTableSchema(editTable.tableName, operations); }, onSuccess: (_, data) => { void queryClient.invalidateQueries({ queryKey: ['database-metadata'] }); void queryClient.invalidateQueries({ queryKey: ['tables'] }); void queryClient.invalidateQueries({ queryKey: ['metadata'] }); // Invalidate all table data queries for this table (with all parameter combinations) void queryClient.invalidateQueries({ queryKey: ['table', editTable?.tableName] }); // Invalidate the separate table schema query used by AddRecordSheet void queryClient.invalidateQueries({ queryKey: ['table-schema', editTable?.tableName] }); showToast(`Table "${data.tableName}" updated successfully!`, 'success'); form.reset(); setError(null); setForeignKeys([]); onSuccess?.(); }, onError: (err) => { // Invalidate queries to ensure we have fresh data after failed request void queryClient.invalidateQueries({ queryKey: ['table', editTable?.tableName] }); void queryClient.invalidateQueries({ queryKey: ['table-schema', editTable?.tableName] }); const errorMessage = err.message || 'Failed to update table'; setError(errorMessage); showToast(errorMessage, 'error'); }, }); const handleSubmit = form.handleSubmit((data) => { const userColumns = data.columns.filter((col) => !col.isSystemColumn); if (!userColumns.length) { const msg = mode === 'create' ? 'Please add at least one user-defined column to create a table.' : 'Please ensure the table has at least one user-defined column.'; setError(msg); showToast(msg, 'error'); return; } if (mode === 'edit') { updateTableMutation.mutate(data); } else { createTableMutation.mutate(data); } }); const addField = () => { append({ ...newColumn }); }; const handleAddForeignKey = (fk: TableFormForeignKeySchema) => { if (editingForeignKey) { // Update existing foreign key setForeignKeys( foreignKeys.map((existingFk) => existingFk.columnName === editingForeignKey ? { ...fk } : existingFk ) ); setEditingForeignKey(undefined); setForeignKeysDirty(true); } else { // Add new foreign key setForeignKeys([ ...foreignKeys, { ...fk, }, ]); } setForeignKeysDirty(true); }; const handleRemoveForeignKey = (columnName?: string) => { setForeignKeys(foreignKeys.filter((fk) => fk.columnName !== columnName)); setForeignKeysDirty(true); }; if (!open) { return null; } return (
{/* Content area with slate background */}
{/* Title Bar */}

{mode === 'edit' ? 'Edit Table' : 'Create New Table'}

void handleSubmit()} className="flex flex-col gap-6"> {/* Table Name */}
{form.formState.errors.tableName && (

{form.formState.errors.tableName.message}

)}
{/* Columns Section */}
{/* Columns Header */}

Columns

{/* Columns Table */}
{/* Table Headers */}
Name
Type
Default Value
Nullable
Unique
{/* Columns */} {sortedFields.map((field) => { const originalIndex = fields.findIndex((f) => f.id === field.id); return ( remove(originalIndex)} isSystemColumn={field.isSystemColumn} isNewColumn={field.isNewColumn} /> ); })}
{/* Add Column Button */}
{/* Foreign Keys Section */}

Foreign Keys

Create a relationship between this table and another table

{/* Existing foreign keys */} {foreignKeys.length > 0 && (
{foreignKeys.map((fk) => (
{fk.columnName} {fk.referenceTable}.{fk.referenceColumn}
On Update: {fk.onUpdate}
On Delete: {fk.onDelete}
{/* */}
))}
)} {/* Add Foreign Key Button */}
{/* Foreign Key Popover */} { setShowForeignKeyDialog(open); if (!open) { setEditingForeignKey(undefined); } }} onAddForeignKey={handleAddForeignKey} initialValue={ editingForeignKey ? foreignKeys.find((fk) => fk.columnName === editingForeignKey) : undefined } />
{error && ( {error} )}
{/* Footer */}
); }