import { IconChevronDown, IconCircleX, IconMaximize, } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import type { DbAdminColumn } from "../../db-admin/types.js"; import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popover.js"; import { cn } from "../utils.js"; import { type EditorKind, formatCellValue, inferEnumValues, valueToEditString, parseEditValue, ParseError, cycleTriStateBoolean, formatJsonPretty, } from "./cell-format.js"; export interface EditableCellProps { column: DbAdminColumn; kind: EditorKind; value: unknown; /** Whether this cell holds a staged (uncommitted) edit. */ dirty?: boolean; /** Whether editing is allowed (false when the table has no PK). */ editable?: boolean; /** Whether this cell is the keyboard-focused/active cell in the grid. */ active?: boolean; /** True if the editor should open immediately (e.g. typing began). */ editing?: boolean; /** Commit a new value into the changeset. */ onCommit: (value: unknown) => void; /** Request entering edit mode. */ onStartEdit?: () => void; /** Request leaving edit mode without committing. */ onCancelEdit?: () => void; /** Move focus after Enter ("down") or Tab ("right"). */ onNavigate?: (dir: "up" | "down" | "left" | "right") => void; className?: string; } const NULL_TOKEN = ( NULL ); export function EditableCell({ column, kind, value, dirty, editable = true, active, editing, onCommit, onStartEdit, onCancelEdit, onNavigate, className, }: EditableCellProps) { const display = formatCellValue(value, kind); // Boolean cells toggle in place rather than opening a text editor. if (kind === "boolean") { return ( ); } const baseCell = cn( "relative h-full w-full px-2 py-1 text-xs truncate outline-none", "font-mono", active && "ring-1 ring-inset ring-ring", dirty && "bg-amber-500/10 ring-1 ring-inset ring-amber-500/50", editable && "cursor-text", className, ); if (editing && editable) { if (kind === "enum") { return ( ); } if (kind === "json") { return ( ); } return ( ); } return (
editable && onStartEdit?.()} onKeyDown={(e) => { if (!editable) return; if (e.key === "Enter" || e.key === "F2") { e.preventDefault(); onStartEdit?.(); } }} title={display.isNull ? "NULL" : display.text} >
{display.isNull ? NULL_TOKEN : display.text} {editable && (kind === "json" || kind === "text") && ( )}
); } // ─── Boolean (tri-state) ───────────────────────────────────────────────────── function BooleanCell({ value, dirty, editable, active, onCommit, onNavigate, className, }: { value: unknown; dirty?: boolean; editable: boolean; active?: boolean; onCommit: (v: unknown) => void; onNavigate?: (dir: "up" | "down" | "left" | "right") => void; className?: string; }) { const label = value === true ? "true" : value === false ? "false" : null; return (
editable && onCommit(cycleTriStateBoolean(value))} onKeyDown={(e) => { if (!editable) return; if (e.key === " " || e.key === "Enter") { e.preventDefault(); onCommit(cycleTriStateBoolean(value)); } else if (e.key === "ArrowDown") { e.preventDefault(); onNavigate?.("down"); } else if (e.key === "ArrowUp") { e.preventDefault(); onNavigate?.("up"); } }} > {label === null ? NULL_TOKEN : label}
); } // ─── Inline text / number / timestamp / uuid editor ────────────────────────── function InlineTextEditor({ kind, value, nullable, onCommit, onCancel, onNavigate, }: { kind: EditorKind; value: unknown; nullable: boolean; onCommit: (v: unknown) => void; onCancel?: () => void; onNavigate?: (dir: "up" | "down" | "left" | "right") => void; }) { const ref = useRef(null); const [text, setText] = useState(() => valueToEditString(value, kind)); const [error, setError] = useState(null); useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []); const commit = (nav?: "down" | "right") => { try { const parsed = parseEditValue(text, kind); setError(null); onCommit(parsed); if (nav) onNavigate?.(nav); } catch (err) { setError(err instanceof ParseError ? err.message : String(err)); } }; return (
setText(e.target.value)} onBlur={() => commit()} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); commit("down"); } else if (e.key === "Tab") { e.preventDefault(); commit("right"); } else if (e.key === "Escape") { e.preventDefault(); onCancel?.(); } }} className={cn( "h-full w-full bg-background px-2 py-1 text-xs font-mono outline-none", "ring-2 ring-inset ring-ring", error && "ring-destructive", )} /> {nullable && ( )} {error && (
{error}
)}
); } // ─── Enum (select) editor ──────────────────────────────────────────────────── function EnumEditor({ column, value, onCommit, onCancel, onNavigate, }: { column: DbAdminColumn; value: unknown; onCommit: (v: unknown) => void; onCancel?: () => void; onNavigate?: (dir: "up" | "down" | "left" | "right") => void; }) { const ref = useRef(null); const options = inferEnumValues(column) ?? []; useEffect(() => { ref.current?.focus(); }, []); return ( ); } // ─── JSON / long-text expanding editor ─────────────────────────────────────── function JsonEditor({ value, onCommit, onCancel, }: { value: unknown; onCommit: (v: unknown) => void; onCancel?: () => void; }) { const [open, setOpen] = useState(true); const [text, setText] = useState(() => formatJsonPretty(value)); const [error, setError] = useState(null); const commit = () => { if (text.trim() === "") { onCommit(null); setOpen(false); return; } try { const parsed = parseEditValue(text, "json"); setError(null); onCommit(parsed); setOpen(false); } catch (err) { setError(err instanceof ParseError ? err.message : String(err)); } }; return ( { if (!o) onCancel?.(); setOpen(o); }} > e.preventDefault()} >