import { useEffect, useMemo, useState } from "react"; import type { ReactNode } from "react"; import { X, Link, Plus } from "lucide-react"; import { motion } from "motion/react"; import { useDbViewerStore } from "../../../stores/dbViewerStore"; import * as cmd from "../../../lib/commands"; import { buildObjectDdl } from "../../../lib/objectCrud"; import type { SchemaGraph } from "../../../lib/types"; // Panel selects: transparent, fill the available width (no surface bg like the grid). const panelSelect = "w-full bg-transparent font-heading text-xs text-text outline-none placeholder:text-text-muted cursor-pointer"; export interface FkColumn { name: string; data_type: string; } export interface FkStagedPair { localCol: string; refType: string; } export interface FkDefinition { columns: string[]; ref_schema: string; ref_table: string; ref_columns: string[]; on_delete?: string; on_update?: string; deferrable?: boolean; initially_deferred?: boolean; } interface FkPair { localCol: string; refCol: string; } interface Props { connectionId: string; schema: string; table: string; column: string | null; /** Columns of the table being edited (from the form grid — works even when the table isn't created yet). */ localColumns: FkColumn[]; mode: "create" | "edit"; onClose: () => void; /** Called after staging: pairs (for type auto-match); in create mode the FK definition is returned * so the form can inline it into the CREATE TABLE instead of a separate ALTER change. */ onStaged?: (result: { pairs: FkStagedPair[]; fk?: FkDefinition }) => void; } const FK_ACTIONS = [ "NO ACTION", "RESTRICT", "CASCADE", "SET NULL", "SET DEFAULT", ]; function Section({ label, children, flush = false, }: { label: string; children: ReactNode; flush?: boolean; }) { return (
{label}
{children}
); } export function FkPanel({ connectionId, schema, table, column, localColumns, mode, onClose, onStaged, }: Props) { const [graph, setGraph] = useState({ tables: [], relationships: [], }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [staging, setStaging] = useState(false); const [pairs, setPairs] = useState(() => column ? [{ localCol: column, refCol: "" }] : [{ localCol: "", refCol: "" }], ); const [refSchema, setRefSchema] = useState(schema); const [refTable, setRefTable] = useState(""); const [onDelete, setOnDelete] = useState("NO ACTION"); const [onUpdate, setOnUpdate] = useState("NO ACTION"); useEffect(() => { let active = true; setLoading(true); cmd.getSchemaGraph(connectionId, undefined) .then((g) => { if (!active) return; setGraph(g); setLoading(false); }) .catch((e) => { if (!active) return; setError(e instanceof Error ? e.message : String(e)); setLoading(false); }); return () => { active = false; }; }, [connectionId]); const localNames = useMemo( () => localColumns.map((c) => c.name), [localColumns], ); const schemas = useMemo( () => Array.from(new Set(graph.tables.map((t) => t.schema))).sort(), [graph], ); const tablesInSchema = useMemo( () => graph.tables .filter((t) => t.schema === refSchema) .sort((a, b) => a.name.localeCompare(b.name)), [graph, refSchema], ); const refTableInfo = useMemo( () => tablesInSchema.find((t) => t.name === refTable), [tablesInSchema, refTable], ); const refColumns = useMemo(() => { if (!refTableInfo) return []; return [...refTableInfo.columns].sort((a, b) => { if (a.is_pk && !b.is_pk) return -1; if (!a.is_pk && b.is_pk) return 1; return a.name.localeCompare(b.name); }); }, [refTableInfo]); // Defaults: keep selections valid as the graph loads / inputs change. useEffect(() => { if (schemas.length > 0 && !schemas.includes(refSchema)) { setRefSchema(schemas[0] ?? ""); } }, [schemas, refSchema]); useEffect(() => { if ( tablesInSchema.length > 0 && !tablesInSchema.some((t) => t.name === refTable) ) { setRefTable(tablesInSchema[0]?.name ?? ""); } else if (tablesInSchema.length === 0) { setRefTable(""); } }, [tablesInSchema, refTable]); useEffect(() => { if (localNames.length === 0) return; setPairs((prev) => prev.map((p) => localNames.includes(p.localCol) ? p : { ...p, localCol: localNames[0] ?? "" }, ), ); }, [localNames]); useEffect(() => { setPairs((prev) => prev.map((p) => p.refCol && refColumns.some((c) => c.name === p.refCol) ? p : { ...p, refCol: refColumns[0]?.name ?? "" }, ), ); }, [refColumns]); const setPair = (i: number, key: keyof FkPair, value: string) => setPairs((prev) => prev.map((p, j) => (j === i ? { ...p, [key]: value } : p)), ); const addPair = () => setPairs((prev) => [ ...prev, { localCol: localNames[0] ?? "", refCol: refColumns[0]?.name ?? "", }, ]); const removePair = (i: number) => setPairs((prev) => prev.length > 1 ? prev.filter((_, j) => j !== i) : prev, ); const typeMappings = pairs .map((p) => ({ localType: localColumns.find((c) => c.name === p.localCol)?.data_type ?? "", refType: refTableInfo?.columns.find((c) => c.name === p.refCol) ?.data_type ?? "", })) .filter((m) => m.localType !== "" || m.refType !== ""); const tableLabel = table.trim() === "" ? "unnamed table" : table; const addFk = async () => { const cols = pairs.map((p) => p.localCol); const refCols = pairs.map((p) => p.refCol); if ( cols.length === 0 || cols.some((c) => c === "") || refCols.some((c) => c === "") ) return; setStaging(true); setError(null); const refTypes: Record = {}; for (const c of refTableInfo?.columns ?? []) refTypes[c.name] = c.data_type; const pairsResult = pairs.map((p) => ({ localCol: p.localCol, refType: refTypes[p.refCol] ?? "", })); const fk: FkDefinition = { columns: cols, ref_schema: refSchema, ref_table: refTable, ref_columns: refCols, on_delete: onDelete, on_update: onUpdate, deferrable: false, initially_deferred: false, }; try { if (mode === "create") { // Inline the FK into the CREATE TABLE — no separate ALTER change. onStaged?.({ pairs: pairsResult, fk }); onClose(); return; } const sqls = await buildObjectDdl(connectionId, "constraint", { schema, table, name: "", action: { op: "foreign_key", columns: cols, ref_schema: refSchema, ref_table: refTable, ref_columns: refCols, on_delete: onDelete, on_update: onUpdate, }, }); sqls.forEach((sql, i) => useDbViewerStore.getState().addChange({ type: "ddl", sql, description: sqls.length > 1 ? `Add FK ${cols.join(", ")} → ${refSchema}.${refTable} (${i + 1}/${sqls.length})` : `Add FK ${cols.join(", ")} → ${refSchema}.${refTable}`, }), ); onStaged?.({ pairs: pairsResult }); onClose(); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setStaging(false); } }; return (
Foreign key
{table.trim() === "" && (

Enter a table name in the form first — a foreign key needs a named table.

)} {loading && (

Loading schema graph…

)} {!loading && graph.tables.length === 0 && (

No tables available for foreign key reference.

)} {!loading && graph.tables.length > 0 && ( <>
{refSchema && refTable && ( <>
{schema}.{tableLabel}
{refSchema}.{refTable || "—"}
{pairs.map((pair, i) => (
{pairs.length > 1 && ( )}
))}
{typeMappings.length > 0 && (
    {typeMappings.map((m, i) => (
  • {m.localType || "?"} → {" "} {m.refType || "?"}
  • ))}
)}
)} )} {error && (

{error}

)}
); }