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,159 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, afterEach } from "vitest";
import { ObjectFormTab } from "./ObjectFormTab";
import * as objectCrud from "../../../lib/objectCrud";
import {
useDbViewerStore,
type ViewerTab,
} from "../../../stores/dbViewerStore";
vi.mock("../../../lib/objectCrud", () => ({
buildObjectDdl: vi.fn(),
}));
vi.mock("../../editor/SqlEditorField", () => ({
SqlEditorField: ({ value, readOnly }: { value: string; readOnly?: boolean }) => (
<textarea data-testid="sql-editor" readOnly={readOnly} value={value} onChange={() => {}} />
),
}));
describe("ObjectFormTab", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
useDbViewerStore.getState().reset();
});
const connectionId = "c1";
const baseTab: ViewerTab = {
id: "form-tab-1",
schema: "public",
table: "Create sequence",
page: 1,
pageSize: 50,
loading: false,
error: null,
data: null,
filterRules: [],
sortRules: [],
hiddenColumns: [],
smartSortApplied: false,
tabType: "objectForm",
objectType: null,
form: {
kind: "sequence",
title: "Create sequence",
description: "Create my_seq",
mode: "create",
params: { schema: "public", name: "my_seq", action: { op: "create" } },
},
};
it("renders the form for a sequence kind in Visual view", () => {
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
]);
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
expect(screen.getByPlaceholderText("Sequence name")).toBeInTheDocument();
});
it("toggles to SQL and shows the generated SQL, then back to Visual", async () => {
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
]);
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
fireEvent.click(screen.getByRole("button", { name: "SQL" }));
const ed = await screen.findByTestId("sql-editor");
expect(ed).toHaveValue('CREATE SEQUENCE "public"."my_seq" START WITH 1;');
expect(ed).toHaveProperty("readOnly", true);
fireEvent.click(screen.getByRole("button", { name: "Visual" }));
expect(screen.getByPlaceholderText("Sequence name")).toBeInTheDocument();
});
it("stages one ddl change and closes the tab", async () => {
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
]);
useDbViewerStore.setState({
tabs: [baseTab],
activeTabId: baseTab.id,
});
const addChange = vi.spyOn(useDbViewerStore.getState(), "addChange");
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
fireEvent.click(screen.getByRole("button", { name: /stage/i }));
await waitFor(() =>
expect(addChange).toHaveBeenCalledWith(
expect.objectContaining({
type: "ddl",
sql: 'CREATE SEQUENCE "public"."my_seq" START WITH 1;',
description: "Create my_seq",
}),
),
);
expect(useDbViewerStore.getState().tabs).toHaveLength(0);
});
it("stages multiple statements with (n/total) descriptions", async () => {
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([
"DROP SEQUENCE \"public\".\"my_seq\";",
"CREATE SEQUENCE \"public\".\"my_seq\" START WITH 1;",
]);
useDbViewerStore.setState({
tabs: [baseTab],
activeTabId: baseTab.id,
});
const addChange = vi.spyOn(useDbViewerStore.getState(), "addChange");
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
fireEvent.click(screen.getByRole("button", { name: /stage/i }));
await waitFor(() => {
expect(addChange).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ description: "Create my_seq (1/2)" }),
);
expect(addChange).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ description: "Create my_seq (2/2)" }),
);
});
expect(useDbViewerStore.getState().tabs).toHaveLength(0);
});
it("shows a builder error and disables Stage", async () => {
vi.mocked(objectCrud.buildObjectDdl).mockRejectedValueOnce(
"Name is required",
);
render(<ObjectFormTab connectionId={connectionId} tab={baseTab} />);
await waitFor(() =>
expect(screen.getByText(/Name is required/)).toBeInTheDocument(),
);
expect(screen.getByRole("button", { name: /stage/i })).toBeDisabled();
});
it("renders only the uppercase kicker, not a duplicate title heading", () => {
vi.mocked(objectCrud.buildObjectDdl).mockResolvedValue([]);
render(
<ObjectFormTab
connectionId={connectionId}
tab={{
...baseTab,
table: "Create Function",
form: {
...baseTab.form!,
kind: "function",
title: "Create Function",
},
}}
/>,
);
// Kicker: lowercase DOM text uppercased by CSS
expect(screen.getByText("create function")).toBeInTheDocument();
expect(screen.queryByText("Create Function")).toBeNull();
});
});