Editor settings, SSH/SSL runtime, data import + table-menu loose ends (#7)

* feat: editor settings model + typed clamped defaults (Task 1)

* feat: carry SSH config + ssh_password to backend DbConfig (Task 2)

* feat: Change enum bulk/drop/empty + type-specific change payload builder (Task 3)

* feat: csv parser + shared export util (Task 4)

* feat: rustls TLS connector factory with modes + client auth (Task 5)

* feat: real SSH tunnel manager (testable backend) + pool eviction hook (Task 6)

* feat: table DDL fetch (sqlite + pg_dump arg builder) (Task 7)

* feat: keychain SSH secrets + connection delete purge + tunnel lifecycle (Task 8)

* feat: real SSH tunnel + TLS connect path for postgres/mysql (Task 9)

* feat: execute_change bulk/drop/empty + get_table_ddl command (Task 10)

* feat: fetch SSH secrets into dbConnect + save on connection form (Task 11)

* feat: Editor settings tab UI (Task 12)

* feat: QueryEditor applies editor settings live (Task 13)

* feat: ImportDialog with CSV/JSON preview + column mapping (Task 14)

* feat: table-menu export/empty/delete/import + queue labels + payload builder (Task 15)

* fix: error sanitization, encrypted-key guard, row-indexed import errors, caps (Task 16)

* docs: mark Editor Settings, SSH/SSL runtime, Data Import, table-menu loose ends shipped (Task 17)

* feat: auto-refresh schema tree after schema-modifying SQL (query + queue drop)

* fix: use theme-consistent red classes for danger menu items (text-error was undefined)

* feat: changes queue as tab-bar popover + amber pending border

* feat: redesign changes popover (visual/SQL toggle, cards, footer actions, Cmd+S)

* refactor: drop per-card status label from changes popover cards

* feat: green completion indicator on committed cards + auto-close tabs of dropped tables

* docs: changes queue popover UX + auto schema refresh statuses
This commit is contained in:
2026-08-02 20:38:23 +08:00
committed by GitHub
parent e32fe7967c
commit e0c0db8352
68 changed files with 3885 additions and 553 deletions
@@ -1,7 +1,11 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TableOverflowMenu } from "./TableOverflowMenu";
import { useDbViewerStore } from "../../stores/dbViewerStore";
import { useUiStore } from "../../stores/uiStore";
import * as commands from "../../lib/commands";
import * as exportData from "../../lib/exportData";
describe("TableOverflowMenu", () => {
beforeEach(() => {
@@ -10,6 +14,9 @@ describe("TableOverflowMenu", () => {
configurable: true,
writable: true,
});
useDbViewerStore.getState().reset();
useUiStore.setState({ activeConnectionId: "c1" });
vi.resetAllMocks();
});
it("renders menu trigger button", () => {
@@ -34,4 +41,44 @@ describe("TableOverflowMenu", () => {
await user.click(screen.getByText("Open in new tab"));
expect(onOpenTab).toHaveBeenCalledWith("public", "users", true);
});
it("Copy table schema calls getTableDdl and writes clipboard", async () => {
vi.spyOn(commands, "getTableDdl").mockResolvedValue("CREATE TABLE t (id int)");
const writeText = (navigator.clipboard as any).writeText as ReturnType<typeof vi.fn>;
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} />);
fireEvent.click(screen.getByLabelText(/table options/i));
fireEvent.click(screen.getByText(/copy table schema/i));
await waitFor(() => expect(writeText).toHaveBeenCalledWith("CREATE TABLE t (id int)"));
});
it("Empty Table opens confirm then stages an empty_table change", async () => {
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} />);
fireEvent.click(screen.getByLabelText(/table options/i));
fireEvent.click(screen.getByText(/empty table/i));
fireEvent.click(screen.getByRole("button", { name: /empty table/i }));
await waitFor(() => {
const q = useDbViewerStore.getState().changesQueue;
expect(q[q.length - 1]).toEqual(expect.objectContaining({ type: "empty_table", schema: "public", table: "t" }));
});
});
it("Delete Table opens confirm then stages a drop_table change", async () => {
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} />);
fireEvent.click(screen.getByLabelText(/table options/i));
fireEvent.click(screen.getByText(/delete table/i));
fireEvent.click(screen.getByRole("button", { name: /delete table/i }));
await waitFor(() => {
const q = useDbViewerStore.getState().changesQueue;
expect(q[q.length - 1]).toEqual(expect.objectContaining({ type: "drop_table", schema: "public", table: "t" }));
});
});
it("Export data calls exportData when rows and columns are provided", async () => {
const spy = vi.spyOn(exportData, "exportData");
const columns = [{ name: "id", data_type: "integer", is_nullable: false, is_pk: true, is_fk: false, fk_ref: null, default_value: null }];
render(<TableOverflowMenu schema="public" table="t" onOpenTab={() => "tab-1"} columns={columns} rows={[[1]]} />);
fireEvent.click(screen.getByLabelText(/table options/i));
fireEvent.click(screen.getByText(/export data \(csv\)/i));
await waitFor(() => expect(spy).toHaveBeenCalledWith([[1]], columns, "csv", "public.t"));
});
});