v0.7.6: PostgreSQL object management, keychain toggle, tabbed Objects workspace (#12)

* [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,90 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { ObjectContextMenu } from "./ObjectContextMenu";
import * as objectCrud from "../../../lib/objectCrud";
import { useDbViewerStore } from "../../../stores/dbViewerStore";
vi.mock("../../../lib/objectCrud", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../lib/objectCrud")>();
return {
...actual,
buildObjectDdl: vi.fn(),
getAvailableExtensions: vi.fn(),
};
});
describe("ObjectContextMenu", () => {
beforeEach(() => {
vi.clearAllMocks();
useDbViewerStore.getState().reset();
});
it("Edit on a sequence opens an objectForm tab with prefilled edit params", async () => {
render(
<ObjectContextMenu
connectionId="c1"
objectType="sequence"
item={{ schema: "public", name: "s" }}
onRefresh={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByText("Edit…"));
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
expect(st.tabs[0].tabType).toBe("objectForm");
expect(st.tabs[0].form?.mode).toBe("edit");
expect(st.tabs[0].form?.kind).toBe("sequence");
expect(st.tabs[0].form?.params?.name).toBe("s");
expect(st.tabs[0].form?.params?.schema).toBe("public");
});
it("Create on a sequence opens an objectForm tab in create mode", async () => {
render(
<ObjectContextMenu
connectionId="c1"
objectType="sequence"
item={{ schema: "public", name: "s" }}
onRefresh={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByText("Create…"));
const st = useDbViewerStore.getState();
expect(st.tabs).toHaveLength(1);
expect(st.tabs[0].tabType).toBe("objectForm");
expect(st.tabs[0].form?.mode).toBe("create");
expect(st.tabs[0].form?.kind).toBe("sequence");
expect(st.tabs[0].form?.params?.name).toBe("");
});
it("Drop fetches dependencies then stages the drop", async () => {
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
'DROP SEQUENCE "public"."s"',
]);
const addChange = vi.spyOn(useDbViewerStore.getState(), "addChange");
render(
<ObjectContextMenu
connectionId="c1"
objectType="sequence"
item={{ schema: "public", name: "s" }}
onRefresh={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByText("Drop…"));
// DependencyDialog with no deps → Proceed is enabled
expect(
await screen.findByRole("button", { name: /proceed/i }),
).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /proceed/i }));
await waitFor(() =>
expect(addChange).toHaveBeenCalledWith(
expect.objectContaining({
type: "ddl",
sql: 'DROP SEQUENCE "public"."s"',
}),
),
);
});
});