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

* [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:
2026-08-06 22:56:49 +08:00
committed by GitHub
parent 8d8ed78202
commit 0c74d77e75
86 changed files with 7094 additions and 1165 deletions
@@ -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>
);
}