import { IconKey, IconX, IconRefresh, IconCircleX } from "@tabler/icons-react"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import type { DbAdminColumn, DbAdminTableSchema, } from "../../db-admin/types.js"; import { cn } from "../utils.js"; import { inferEditorKind, inferEnumValues, valueToEditString, parseEditValue, ParseError, type EditorKind, } from "./cell-format.js"; export type RowSidePanelMode = "insert" | "edit"; export interface RowSidePanelProps { schema: DbAdminTableSchema; mode: RowSidePanelMode; /** For edit mode: the original row values. */ row?: Record; /** For edit mode: staged overrides already in the changeset. */ staged?: Record; onClose: () => void; /** * Persist into the changeset. For insert mode `values` is the full new-row * object; for edit mode it is only the changed columns. */ onSave: (values: Record) => void; } type FieldState = { /** Raw text in the input. */ text: string; /** Whether the field is explicitly set to NULL. */ isNull: boolean; /** Parse error, if any. */ error?: string | null; /** Whether the user has touched this field (insert) — drives "blank = skip". */ touched: boolean; }; function initialFieldState( _col: DbAdminColumn, kind: EditorKind, value: unknown, isInsert: boolean, ): FieldState { const isNull = value === null || value === undefined; return { text: isNull ? "" : valueToEditString(value, kind), isNull: isInsert ? false : isNull, touched: false, }; } export function RowSidePanel({ schema, mode, row, staged, onClose, onSave, }: RowSidePanelProps) { const isInsert = mode === "insert"; const [fields, setFields] = useState>(() => { const init: Record = {}; for (const col of schema.columns) { const kind = inferEditorKind(col); const original = row?.[col.name]; const value = staged && Object.prototype.hasOwnProperty.call(staged, col.name) ? staged[col.name] : original; init[col.name] = initialFieldState(col, kind, value, isInsert); } return init; }); // Close on Escape. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const update = (name: string, patch: Partial) => setFields((prev) => ({ ...prev, [name]: { ...prev[name], ...patch, touched: true }, })); const save = () => { const out: Record = {}; let hadError = false; const nextFields = { ...fields }; for (const col of schema.columns) { const kind = inferEditorKind(col); const fs = fields[col.name]; const isGenerated = col.pk && col.autoIncrement; if (fs.isNull) { // Explicit NULL. if (!isInsert || fs.touched) out[col.name] = null; continue; } // Insert: blank, untouched, auto/pk/default columns are left out entirely. if (isInsert && fs.text === "" && !fs.touched) { continue; } if (isInsert && fs.text === "" && (isGenerated || col.defaultValue)) { continue; } // Edit: only include columns the user touched. if (!isInsert && !fs.touched) continue; try { const parsed = parseEditValue(fs.text, kind, { allowEmptyString: kind === "text", }); out[col.name] = parsed; } catch (err) { hadError = true; nextFields[col.name] = { ...fs, error: err instanceof ParseError ? err.message : String(err), }; } } if (hadError) { setFields(nextFields); return; } onSave(out); onClose(); }; const panel = (

{isInsert ? "Insert row" : "Edit row"}

{schema.name}

{schema.columns.map((col) => ( update(col.name, patch)} /> ))}
); if (typeof document === "undefined") return null; return createPortal(panel, document.body); } function RowField({ column, state, isInsert: _isInsert, onChange, }: { column: DbAdminColumn; state: FieldState; isInsert: boolean; onChange: (patch: Partial) => void; }) { const kind = inferEditorKind(column); const enumValues = inferEnumValues(column); const isGenerated = column.pk && column.autoIncrement; return (
{column.pk && } {column.type} {!column.nullable && ( required )} {column.nullable && ( )}
{state.isNull ? (
NULL
) : enumValues ? ( ) : kind === "boolean" ? ( ) : kind === "json" ? (