v0.7.6: PostgreSQL object management, keychain toggle, tabbed Objects workspace (#12)
Release / publish (, ubuntu-22.04) (push) Failing after 14m42s
Release / publish (, windows-latest) (push) Canceled after 0s
Release / publish (--target aarch64-apple-darwin, macos-latest) (push) Canceled after 0s
Release / publish (--target x86_64-apple-darwin, macos-15-intel) (push) Canceled after 0s
Release / publish (, ubuntu-22.04) (push) Failing after 14m42s
Release / publish (, windows-latest) (push) Canceled after 0s
Release / publish (--target aarch64-apple-darwin, macos-latest) (push) Canceled after 0s
Release / publish (--target x86_64-apple-darwin, macos-15-intel) (push) Canceled after 0s
* [P1-T1] Change::Ddl Rust variant + execute_change arm * [P1-T2] Frontend ddl change type + objectCrud capability * [P1-T3] use_keychain data model + migration v8 * [P2-T1] object_crud skeleton + validators + build_ddl dispatch * [P2-T2] Sequence builders * [P2-T3] Enum builders (no value removal) * [P2-T4] View / matview / extension builders * [P2-T5] Index + constraint builders * [P2-T6] Function / procedure + trigger builders * [P2-T7] build_object_ddl + get_available_extensions commands + wrappers * [P3-T1] conditional keychain + session passwords * [P3-T2] keychain-off password prompt on connect * [P3-T3] default-ON keychain opt-out + tooltip + modal conditional * [P3-T4] ddl queue card + after-commit refetch * [P4-T1] ObjectCrudDialog shell * [P4-T2] SequenceForm * [P4-T3] EnumForm with no-removal note * [P4-T4] ExtensionForm with available-extensions picker * [P4-T5] ViewForm (view + materialized view) * [P5-T1] IndexForm with column picker * [P5-T2] ConstraintForm (check/unique/pk/fk) + ColumnPicker * [P5-T3] FunctionForm (function + procedure) * [P5-T4] TriggerForm with trigger-function picker * [P5-T5] ObjectContextMenu + Explorer/TableOverflowMenu CRUD wiring * [P6-T1] object tab type + openObjectTab dedup * [P6-T2] extract ObjectDetail for object tabs * [P6-T3] Objects view two-pane sidebar + workspace * [P6-T4] object-tab content + per-type tab icons * [P7-T1] Version bump 0.7.5 -> 0.7.6 * [P7-T2] docs sync README/ROADMAP/AGENTS for v0.7.6 * [P7-T3] chore: Cargo.lock version sync 0.7.5 -> 0.7.6 * fix(ui): object tab icon stacks above name (preflight svg block) * fix(ui): optically center object tab icon with name * fix(ui): object tab icon matches query/table icon handling * [UI-POLISH-1] objectForm tab type + openFormTab store action * [UI-POLISH-2] ObjectFormTab + KindForm with Visual/SQL toggle * [UI-POLISH-3] route create/edit through form tabs; remove modal * docs: create/edit now open as form tabs (AGENTS sync) * [FB-1] follow app styling patterns + schema dropdown in forms * [FB-2] Monaco editor for function body + view definition * [FB-3] form tabs styled like viewers + in-cell editing * [FB-5] focus outline scoped to input area (label excluded) * [FB-6] no amber focus outline on Monaco body/definition rows * [FB-7] header dedupe + schema default + full edit prefill * [FB-8] SQL view in read-only Monaco editor * docs: roadmap — table create/edit + relationships (next) * docs: roadmap — Admin follow-up is 0.7.7 (next after 0.7.6)
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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 }[];
|
||||
}
|
||||
|
||||
const DROP_TITLE: Record<ObjectKind, string> = {
|
||||
sequence: "sequence",
|
||||
enum: "type",
|
||||
view: "view",
|
||||
extension: "extension",
|
||||
index: "index",
|
||||
constraint: "constraint",
|
||||
function: "function",
|
||||
procedure: "procedure",
|
||||
trigger: "trigger",
|
||||
};
|
||||
|
||||
export function ObjectContextMenu({
|
||||
connectionId,
|
||||
objectType,
|
||||
item,
|
||||
onRefresh,
|
||||
open,
|
||||
onOpenChange,
|
||||
extraItems,
|
||||
}: Props) {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [deps, setDeps] = useState<DependencyInfo[] | null>(null);
|
||||
const addChange = useDbViewerStore((s) => s.addChange);
|
||||
const menuRef = useRef<HTMLDivElement>(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 (
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
aria-label="actions"
|
||||
className="text-text-muted hover:text-text cursor-pointer"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-6 z-20 w-40 rounded-lg border border-border bg-surface py-1 text-sm text-text shadow-lg">
|
||||
<button
|
||||
onClick={() => openFormTab("create")}
|
||||
className="block w-full text-left px-3 py-1.5 hover:bg-border/30 cursor-pointer"
|
||||
>
|
||||
Create…
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openFormTab("edit")}
|
||||
className="block w-full text-left px-3 py-1.5 hover:bg-border/30 cursor-pointer"
|
||||
>
|
||||
Edit…
|
||||
</button>
|
||||
<button
|
||||
onClick={startDrop}
|
||||
className="block w-full text-left px-3 py-1.5 text-red-400 hover:bg-border/30 cursor-pointer"
|
||||
>
|
||||
Drop…
|
||||
</button>
|
||||
{extraItems && extraItems.length > 0 && (
|
||||
<>
|
||||
<div className="my-1 border-t border-border" />
|
||||
{extraItems.map((it) => (
|
||||
<button
|
||||
key={it.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
it.onClick();
|
||||
}}
|
||||
className={`block w-full text-left px-3 py-1.5 hover:bg-border/30 cursor-pointer ${
|
||||
it.danger ? "text-red-400" : ""
|
||||
}`}
|
||||
>
|
||||
{it.label}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DependencyDialog
|
||||
open={!!deps}
|
||||
deps={deps ?? []}
|
||||
onProceed={confirmDrop}
|
||||
onCancel={() => setDeps(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user