import { useEffect, useRef, useState } from "react"; import { DependencyDialog } from "../DependencyDialog"; import { buildObjectDdl, dropCrudParams, initialCrudParams, type CrudItem, type ObjectKind, } from "../../../lib/objectCrud"; import { getObjectDependencies } from "../../../lib/commands"; import type { DependencyInfo } from "../../../lib/types"; import { useDbViewerStore } from "../../../stores/dbViewerStore"; interface Props { connectionId: string; objectType: ObjectKind; item: CrudItem; onRefresh: () => void; /** Controlled open state (e.g. driven by a row's right-click). */ open?: boolean; onOpenChange?: (open: boolean) => void; /** Extra non-CRUD actions appended below the divider (Copy DDL, Dependencies…). */ extraItems?: { id: string; label: string; danger?: boolean; onClick: () => void }[]; } export const DROP_TITLE: Record = { sequence: "sequence", enum: "type", view: "view", extension: "extension", index: "index", constraint: "constraint", function: "function", procedure: "procedure", trigger: "trigger", table: "table", role: "role", }; export function ObjectContextMenu({ connectionId, objectType, item, onRefresh, open, onOpenChange, extraItems, }: Props) { const [internalOpen, setInternalOpen] = useState(false); const [deps, setDeps] = useState(null); const addChange = useDbViewerStore((s) => s.addChange); const menuRef = useRef(null); const isOpen = open ?? internalOpen; const setOpen = (v: boolean) => { if (onOpenChange) onOpenChange(v); else setInternalOpen(v); }; // Track the current open state in a ref so the document-level outside-click // handler (registered once) only closes a menu that is actually open. Without // this, closed instances would fire onOpenChange(false) on every mousedown // and clobber the shared open key in the controlled (row right-click) case. const openRef = useRef(isOpen); openRef.current = isOpen; useEffect(() => { const h = (e: MouseEvent) => { if ( openRef.current && menuRef.current && !menuRef.current.contains(e.target as Node) ) { setOpen(false); } }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const kind = objectType; const openFormTab = (mode: "create" | "edit") => { const title = `${mode === "create" ? "Create" : "Edit"} ${kind}`; const description = `${mode === "create" ? "Create" : "Edit"} ${ item.name || kind }`; useDbViewerStore.getState().openFormTab({ kind, schema: mode === "create" ? useDbViewerStore.getState().currentSchema ?? item.schema ?? "public" : item.schema, name: mode === "edit" ? item.name : "", title, description, mode, params: initialCrudParams(kind, item, mode), }); setOpen(false); }; const startDrop = async () => { setOpen(false); let d: DependencyInfo[] = []; try { d = await getObjectDependencies(connectionId, item.schema, kind, item.name); } catch { d = []; } setDeps(d); }; const confirmDrop = async () => { const sqls = await buildObjectDdl( connectionId, kind, dropCrudParams(kind, item), ); sqls.forEach((sql) => addChange({ type: "ddl", sql, description: `Drop ${DROP_TITLE[kind]} ${item.name}`, }), ); setDeps(null); onRefresh(); }; return (
{isOpen && (
{extraItems && extraItems.length > 0 && ( <>
{extraItems.map((it) => ( ))} )}
)} setDeps(null)} />
); }