refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
@@ -0,0 +1,630 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { useDbViewerStore } from "./dbViewerStore";
|
||||
import type { QueryResult, TableInfo, RoleInfo } from "../lib/types";
|
||||
import * as commands from "../lib/commands";
|
||||
|
||||
vi.mock("../lib/commands", () => ({
|
||||
getDatabases: vi.fn(),
|
||||
getSchemas: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
useDbViewerStore.getState().reset();
|
||||
});
|
||||
|
||||
describe("dbViewerStore", () => {
|
||||
it("starts empty", () => {
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.tabs).toEqual([]);
|
||||
expect(state.activeTabId).toBeNull();
|
||||
expect(state.changesQueue).toEqual([]);
|
||||
expect(state.databases).toEqual([]);
|
||||
expect(state.schemas).toEqual([]);
|
||||
expect(state.tables).toEqual([]);
|
||||
expect(state.currentDatabase).toBeNull();
|
||||
expect(state.currentSchema).toBeNull();
|
||||
expect(state.objectSearchOpen).toBe(false);
|
||||
expect(state.selectedObjectType).toBeNull();
|
||||
expect(state.schemaTreeLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("object search open + selected object type setters", () => {
|
||||
const { setObjectSearchOpen, setSelectedObjectType } =
|
||||
useDbViewerStore.getState();
|
||||
setObjectSearchOpen(true);
|
||||
expect(useDbViewerStore.getState().objectSearchOpen).toBe(true);
|
||||
setSelectedObjectType("functions");
|
||||
expect(useDbViewerStore.getState().selectedObjectType).toBe("functions");
|
||||
setSelectedObjectType(null);
|
||||
expect(useDbViewerStore.getState().selectedObjectType).toBeNull();
|
||||
});
|
||||
|
||||
it("requestedView setter stores then clears", () => {
|
||||
const { setRequestedView } = useDbViewerStore.getState();
|
||||
setRequestedView("objects");
|
||||
expect(useDbViewerStore.getState().requestedView).toBe("objects");
|
||||
setRequestedView(null);
|
||||
expect(useDbViewerStore.getState().requestedView).toBeNull();
|
||||
});
|
||||
|
||||
it("openTab adds a new tab", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.tabs).toHaveLength(1);
|
||||
const tab = state.tabs[0];
|
||||
expect(tab.schema).toBe("public");
|
||||
expect(tab.table).toBe("users");
|
||||
expect(tab.page).toBe(1);
|
||||
expect(state.activeTabId).toBe(tab.id);
|
||||
});
|
||||
|
||||
it("openTab does not duplicate", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
store.openTab("public", "users");
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.tabs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("openTab forceNew creates duplicate", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
store.openTab("public", "users");
|
||||
const tabsAfterTwo = useDbViewerStore.getState().tabs;
|
||||
expect(tabsAfterTwo).toHaveLength(1);
|
||||
|
||||
store.openTab("public", "users", true);
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.tabs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("closeTab removes tab and switches activeTabId", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const tab1Id = useDbViewerStore.getState().tabs[0].id;
|
||||
store.openTab("public", "posts", true);
|
||||
|
||||
store.closeTab(tab1Id);
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.tabs).toHaveLength(1);
|
||||
expect(state.activeTabId).toBe(state.tabs[0].id);
|
||||
});
|
||||
|
||||
it("closeTabsForTable closes matching table tabs and keeps others", () => {
|
||||
useDbViewerStore.getState().openTab("public", "users");
|
||||
useDbViewerStore.getState().openTab("public", "posts", true);
|
||||
useDbViewerStore.getState().closeTabsForTable("public", "users");
|
||||
const tabs = useDbViewerStore.getState().tabs;
|
||||
expect(tabs).toHaveLength(1);
|
||||
expect(tabs[0].table).toBe("posts");
|
||||
});
|
||||
|
||||
it("closeTabsForTable fixes the active tab when it is closed", () => {
|
||||
useDbViewerStore.getState().openTab("public", "users");
|
||||
const usersId = useDbViewerStore.getState().tabs[0].id;
|
||||
useDbViewerStore.getState().openTab("public", "posts", true);
|
||||
useDbViewerStore.getState().setActiveTab(usersId);
|
||||
useDbViewerStore.getState().closeTabsForTable("public", "users");
|
||||
const st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(1);
|
||||
expect(st.activeTabId).toBe(st.tabs[0].id);
|
||||
});
|
||||
|
||||
it("reorderTab moves a tab and keeps the active tab by id", () => {
|
||||
useDbViewerStore.getState().openTab("public", "users");
|
||||
useDbViewerStore.getState().openTab("public", "posts", true);
|
||||
useDbViewerStore.getState().openTab("public", "comments", true);
|
||||
const before = useDbViewerStore.getState().tabs;
|
||||
const activeId = useDbViewerStore.getState().activeTabId;
|
||||
const activeTable = before.find((t) => t.id === activeId)!.table;
|
||||
|
||||
useDbViewerStore.getState().reorderTab(0, 2);
|
||||
const after = useDbViewerStore.getState().tabs;
|
||||
expect(after.map((t) => t.table)).toEqual(["posts", "comments", "users"]);
|
||||
// Active tab must follow the moved item (tracked by id, not index).
|
||||
expect(useDbViewerStore.getState().activeTabId).toBe(activeId);
|
||||
expect(after.find((t) => t.id === activeId)!.table).toBe(activeTable);
|
||||
});
|
||||
|
||||
it("reorderTab ignores out-of-range and no-op moves", () => {
|
||||
useDbViewerStore.getState().openTab("public", "users");
|
||||
useDbViewerStore.getState().openTab("public", "posts", true);
|
||||
const reorderTab = useDbViewerStore.getState().reorderTab;
|
||||
reorderTab(0, 0);
|
||||
expect(useDbViewerStore.getState().tabs.map((t) => t.table)).toEqual([
|
||||
"users",
|
||||
"posts",
|
||||
]);
|
||||
reorderTab(-1, 1);
|
||||
reorderTab(0, 5);
|
||||
expect(useDbViewerStore.getState().tabs.map((t) => t.table)).toEqual([
|
||||
"users",
|
||||
"posts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("setPage updates pagination", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const tabId = useDbViewerStore.getState().tabs[0].id;
|
||||
|
||||
store.setPage(tabId, 3);
|
||||
const tab = useDbViewerStore.getState().tabs[0];
|
||||
expect(tab.page).toBe(3);
|
||||
});
|
||||
|
||||
it("setTabData updates tab data", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const tabId = useDbViewerStore.getState().tabs[0].id;
|
||||
|
||||
// First set loading to true to verify it gets cleared
|
||||
store.setTabLoading(tabId, true);
|
||||
|
||||
const mockData: QueryResult = {
|
||||
columns: [{name:"id",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false},{name:"name",data_type:"text",is_pk:false,is_fk:false,is_nullable:false,default_value:null,fk_ref:null,editable:true,is_generated:false}],
|
||||
rows: [[1, "Alice"]],
|
||||
total_rows: 1, page: 1, page_size: 50,
|
||||
};
|
||||
store.setTabData(tabId, mockData);
|
||||
|
||||
const tab = useDbViewerStore.getState().tabs[0];
|
||||
expect(tab.data).toEqual(mockData);
|
||||
expect(tab.loading).toBe(false);
|
||||
expect(tab.error).toBeNull();
|
||||
});
|
||||
|
||||
it("setTabError sets error", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const tabId = useDbViewerStore.getState().tabs[0].id;
|
||||
|
||||
store.setTabError(tabId, "Something went wrong");
|
||||
const tab = useDbViewerStore.getState().tabs[0];
|
||||
expect(tab.error).toBe("Something went wrong");
|
||||
expect(tab.loading).toBe(false);
|
||||
});
|
||||
|
||||
it("addChange appends to queue", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
|
||||
|
||||
const queue = useDbViewerStore.getState().changesQueue;
|
||||
expect(queue).toHaveLength(1);
|
||||
expect(queue[0].status).toBe("pending");
|
||||
expect(queue[0].id).toBeTruthy();
|
||||
expect(queue[0].createdAt).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("addChange stages a bulk_insert with columns+rows", () => {
|
||||
useDbViewerStore.getState().addChange({
|
||||
type: "bulk_insert", schema: "public", table: "t",
|
||||
columns: ["a", "b"], rows: [[1, 2]], description: "Import",
|
||||
} as any);
|
||||
const q = useDbViewerStore.getState().changesQueue;
|
||||
expect(q[q.length - 1].type).toBe("bulk_insert");
|
||||
expect((q[q.length - 1] as any).columns).toEqual(["a", "b"]);
|
||||
expect((q[q.length - 1] as any).rows).toEqual([[1, 2]]);
|
||||
});
|
||||
|
||||
it("cancelChange marks change as cancelled", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
|
||||
const changeId = useDbViewerStore.getState().changesQueue[0].id;
|
||||
|
||||
store.cancelChange(changeId);
|
||||
const change = useDbViewerStore.getState().changesQueue[0];
|
||||
expect(change.status).toBe("cancelled");
|
||||
});
|
||||
|
||||
it("markChangeCommitted updates status", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
|
||||
const changeId = useDbViewerStore.getState().changesQueue[0].id;
|
||||
|
||||
store.markChangeCommitted(changeId);
|
||||
const change = useDbViewerStore.getState().changesQueue[0];
|
||||
expect(change.status).toBe("committed");
|
||||
});
|
||||
|
||||
it("markChangeFailed updates status and records error", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
|
||||
const changeId = useDbViewerStore.getState().changesQueue[0].id;
|
||||
|
||||
store.markChangeFailed(changeId, "Constraint violation");
|
||||
const change = useDbViewerStore.getState().changesQueue[0];
|
||||
expect(change.status).toBe("failed");
|
||||
expect(change.error).toBe("Constraint violation");
|
||||
});
|
||||
|
||||
it("removeChange drops the change from the queue", () => {
|
||||
useDbViewerStore.getState().addChange({ type: "insert", schema: "public", table: "t", newData: { a: 1 }, description: "x" } as any);
|
||||
const id = useDbViewerStore.getState().changesQueue[0].id;
|
||||
useDbViewerStore.getState().removeChange(id);
|
||||
expect(useDbViewerStore.getState().changesQueue).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("clearChanges empties the queue", () => {
|
||||
useDbViewerStore.getState().addChange({ type: "insert", schema: "public", table: "t", newData: { a: 1 }, description: "x" } as any);
|
||||
useDbViewerStore.getState().addChange({ type: "delete", schema: "public", table: "t", primaryKey: { id: 1 }, description: "y" } as any);
|
||||
useDbViewerStore.getState().clearChanges();
|
||||
expect(useDbViewerStore.getState().changesQueue).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("setSchemaTreeLoading toggles the loading flag", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
expect(useDbViewerStore.getState().schemaTreeLoading).toBe(false);
|
||||
store.setSchemaTreeLoading(true);
|
||||
expect(useDbViewerStore.getState().schemaTreeLoading).toBe(true);
|
||||
store.setSchemaTreeLoading(false);
|
||||
expect(useDbViewerStore.getState().schemaTreeLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("reset clears all state", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
store.addChange({ type: "insert", sql: "INSERT INTO users (id) VALUES (1)" });
|
||||
store.populate(["mydb"], ["public"], [{ name: "users", schema: "public", table_type: "TABLE" }]);
|
||||
|
||||
store.reset();
|
||||
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.tabs).toEqual([]);
|
||||
expect(state.activeTabId).toBeNull();
|
||||
expect(state.changesQueue).toEqual([]);
|
||||
expect(state.databases).toEqual([]);
|
||||
expect(state.schemas).toEqual([]);
|
||||
expect(state.tables).toEqual([]);
|
||||
expect(state.currentDatabase).toBeNull();
|
||||
expect(state.currentSchema).toBeNull();
|
||||
expect(state.schemaTreeLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("populate sets databases, schemas, tables", () => {
|
||||
const tables: TableInfo[] = [
|
||||
{ name: "users", schema: "public", table_type: "TABLE" },
|
||||
{ name: "posts", schema: "public", table_type: "TABLE" },
|
||||
];
|
||||
const store = useDbViewerStore.getState();
|
||||
store.populate(["mydb", "testdb"], ["public", "private"], tables);
|
||||
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.databases).toEqual(["mydb", "testdb"]);
|
||||
expect(state.schemas).toEqual(["public", "private"]);
|
||||
expect(state.tables).toEqual(tables);
|
||||
});
|
||||
|
||||
it("stageCellEdit appends an update QueueItem with primaryKey + old/new data", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const tabId = useDbViewerStore.getState().activeTabId!;
|
||||
store.stageCellEdit({
|
||||
tabId,
|
||||
schema: "public",
|
||||
table: "users",
|
||||
primaryKey: { id: 1 },
|
||||
oldData: { name: "Alice" },
|
||||
newData: { name: "Alicia" },
|
||||
description: "Edit users.name",
|
||||
});
|
||||
const q = useDbViewerStore.getState().changesQueue;
|
||||
expect(q).toHaveLength(1);
|
||||
expect(q[0].type).toBe("update");
|
||||
expect(q[0].primaryKey).toEqual({ id: 1 });
|
||||
expect(q[0].oldData).toEqual({ name: "Alice" });
|
||||
expect(q[0].newData).toEqual({ name: "Alicia" });
|
||||
});
|
||||
|
||||
it("re-staging the same cell replaces the pending entry (keeps original oldData)", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const tabId = useDbViewerStore.getState().activeTabId!;
|
||||
store.stageCellEdit({
|
||||
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
|
||||
oldData: { name: "Alice" }, newData: { name: "Alicia" },
|
||||
description: "Edit users.name",
|
||||
});
|
||||
store.stageCellEdit({
|
||||
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
|
||||
oldData: { name: "Alice" }, newData: { name: "Alicia 2" },
|
||||
description: "Edit users.name",
|
||||
});
|
||||
const q = useDbViewerStore.getState().changesQueue;
|
||||
expect(q).toHaveLength(1);
|
||||
expect(q[0].newData).toEqual({ name: "Alicia 2" });
|
||||
expect(q[0].oldData).toEqual({ name: "Alice" }); // original DB value preserved
|
||||
});
|
||||
|
||||
it("staging a different cell appends a second entry", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openTab("public", "users");
|
||||
const tabId = useDbViewerStore.getState().activeTabId!;
|
||||
store.stageCellEdit({
|
||||
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
|
||||
oldData: { name: "Alice" }, newData: { name: "Alicia" },
|
||||
});
|
||||
store.stageCellEdit({
|
||||
tabId, schema: "public", table: "users", primaryKey: { id: 1 },
|
||||
oldData: { age: 30 }, newData: { age: 31 },
|
||||
});
|
||||
expect(useDbViewerStore.getState().changesQueue).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("setIndexes / setConstraints update store slices", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.setIndexes([{ name: "idx", schema: "public", table: "t", definition: "", is_unique: true, method: "btree", columns: ["id"], size_bytes: 1, tablespace: null }]);
|
||||
store.setConstraints([{ name: "ck", schema: "public", table: "t", contype: "CHECK", definition: "", deferrable: false, validated: true, columns: ["x"] }]);
|
||||
expect(useDbViewerStore.getState().indexes).toHaveLength(1);
|
||||
expect(useDbViewerStore.getState().constraints).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("reset clears indexes and constraints", () => {
|
||||
useDbViewerStore.getState().setIndexes([{ name: "x", schema: "s", table: "t", definition: "", is_unique: false, method: "btree", columns: [], size_bytes: null, tablespace: null }]);
|
||||
useDbViewerStore.getState().reset();
|
||||
expect(useDbViewerStore.getState().indexes).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshTree", () => {
|
||||
it("fetches databases/schemas/tables and populates", async () => {
|
||||
vi.mocked(commands.getDatabases).mockResolvedValue(["mydb"]);
|
||||
vi.mocked(commands.getSchemas).mockResolvedValue(["public"]);
|
||||
vi.mocked(commands.getTables).mockResolvedValue([
|
||||
{ name: "users", schema: "public", table_type: "TABLE" },
|
||||
] as any);
|
||||
useDbViewerStore.setState({ currentSchema: "public" });
|
||||
await useDbViewerStore.getState().refreshTree("c1");
|
||||
expect(useDbViewerStore.getState().databases).toEqual(["mydb"]);
|
||||
expect(useDbViewerStore.getState().schemas).toEqual(["public"]);
|
||||
expect(useDbViewerStore.getState().tables).toHaveLength(1);
|
||||
expect(commands.getTables).toHaveBeenCalledWith("c1", "public");
|
||||
});
|
||||
it("falls back to no schema when currentSchema is null", async () => {
|
||||
vi.mocked(commands.getDatabases).mockResolvedValue([] as any);
|
||||
vi.mocked(commands.getSchemas).mockResolvedValue([] as any);
|
||||
vi.mocked(commands.getTables).mockResolvedValue([] as any);
|
||||
useDbViewerStore.setState({ currentSchema: null });
|
||||
await useDbViewerStore.getState().refreshTree("c1");
|
||||
expect(commands.getTables).toHaveBeenCalledWith("c1", undefined);
|
||||
});
|
||||
it("swallows fetch errors", async () => {
|
||||
vi.mocked(commands.getDatabases).mockResolvedValue([] as any);
|
||||
vi.mocked(commands.getSchemas).mockRejectedValue(new Error("boom"));
|
||||
await expect(
|
||||
useDbViewerStore.getState().refreshTree("c1"),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("tabType discriminator", () => {
|
||||
beforeEach(() => {
|
||||
useDbViewerStore.getState().reset();
|
||||
useDbViewerStore.setState({
|
||||
schemas: ["public"],
|
||||
currentSchema: "public",
|
||||
currentDatabase: "mydb",
|
||||
});
|
||||
});
|
||||
|
||||
it("openQueryTab creates a query-type tab with empty query", () => {
|
||||
useDbViewerStore.getState().openQueryTab();
|
||||
const state = useDbViewerStore.getState();
|
||||
expect(state.tabs).toHaveLength(1);
|
||||
const tab = state.tabs[0];
|
||||
expect(tab.tabType).toBe("query");
|
||||
expect(tab.query).toBe("");
|
||||
expect(tab.schema).toBe("public");
|
||||
expect(tab.table).toBe("Query");
|
||||
expect(state.activeTabId).toBe(tab.id);
|
||||
});
|
||||
|
||||
it("creates sequential query tabs with unique IDs", () => {
|
||||
const store = useDbViewerStore.getState();
|
||||
store.openQueryTab();
|
||||
store.openQueryTab();
|
||||
store.openQueryTab();
|
||||
const tabs = useDbViewerStore.getState().tabs;
|
||||
expect(tabs).toHaveLength(3);
|
||||
const ids = tabs.map((t) => t.id);
|
||||
const uniqueIds = new Set(ids);
|
||||
expect(uniqueIds.size).toBe(3);
|
||||
tabs.forEach((t) => expect(t.tabType).toBe("query"));
|
||||
});
|
||||
|
||||
it("openTab creates table-type tabs by default (backward compat)", () => {
|
||||
useDbViewerStore.getState().openTab("public", "users");
|
||||
const tab = useDbViewerStore.getState().tabs[0];
|
||||
expect(tab.tabType).toBe("table");
|
||||
});
|
||||
});
|
||||
describe("openObjectTab", () => {
|
||||
beforeEach(() => useDbViewerStore.getState().reset());
|
||||
|
||||
it("opens an object tab carrying the per-type identity + item", () => {
|
||||
useDbViewerStore.getState().openObjectTab("functions", "public", "add", { name: "add", schema: "public" });
|
||||
const st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(1);
|
||||
expect(st.tabs[0]).toMatchObject({ tabType: "object", objectType: "functions", schema: "public", table: "add" });
|
||||
expect(st.tabs[0].objectItem).toEqual({ name: "add", schema: "public" });
|
||||
expect(st.activeTabId).toBe(st.tabs[0].id);
|
||||
});
|
||||
|
||||
it("dedups: re-clicking the same object focuses the existing tab", () => {
|
||||
useDbViewerStore.getState().openObjectTab("enums", "public", "role", { name: "role" });
|
||||
const first = useDbViewerStore.getState().tabs[0];
|
||||
useDbViewerStore.getState().openObjectTab("enums", "public", "role", { name: "role" });
|
||||
const st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(1);
|
||||
expect(st.activeTabId).toBe(first.id);
|
||||
});
|
||||
|
||||
it("does not collide with a table tab of the same name", () => {
|
||||
useDbViewerStore.getState().openTab("public", "users");
|
||||
useDbViewerStore.getState().openObjectTab("functions", "public", "users", { name: "users" });
|
||||
const st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(2);
|
||||
expect(st.tabs.find((t) => t.tabType === "object")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openFormTab", () => {
|
||||
beforeEach(() => useDbViewerStore.getState().reset());
|
||||
|
||||
const createOpts = (name: string, title = "Create sequence") => ({
|
||||
kind: "sequence" as const,
|
||||
schema: "public",
|
||||
name,
|
||||
title,
|
||||
description: `Create ${name}`,
|
||||
mode: "create" as const,
|
||||
params: { schema: "public", name, action: { op: "create" } },
|
||||
});
|
||||
|
||||
it("opens a create form tab carrying kind/params/title/description/mode", () => {
|
||||
useDbViewerStore.getState().openFormTab(createOpts("my_seq"));
|
||||
const st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(1);
|
||||
const tab = st.tabs[0];
|
||||
expect(tab.tabType).toBe("objectForm");
|
||||
// table holds the display title, used as the TabBar label
|
||||
expect(tab.table).toBe("Create sequence");
|
||||
expect(tab.schema).toBe("public");
|
||||
expect(tab.objectType).toBeNull();
|
||||
expect(tab.form).toBeDefined();
|
||||
expect(tab.form!.kind).toBe("sequence");
|
||||
expect(tab.form!.mode).toBe("create");
|
||||
expect(tab.form!.title).toBe("Create sequence");
|
||||
expect(tab.form!.description).toBe("Create my_seq");
|
||||
expect(tab.form!.params).toEqual({
|
||||
schema: "public",
|
||||
name: "my_seq",
|
||||
action: { op: "create" },
|
||||
});
|
||||
expect(st.activeTabId).toBe(tab.id);
|
||||
});
|
||||
|
||||
it("dedups create tabs per kind regardless of schema/name and does not clobber params", () => {
|
||||
useDbViewerStore.getState().openFormTab(createOpts("first_seq"));
|
||||
const first = useDbViewerStore.getState().tabs[0];
|
||||
useDbViewerStore.getState().openFormTab(createOpts("second_seq"));
|
||||
const st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(1);
|
||||
expect(st.activeTabId).toBe(first.id);
|
||||
// the existing tab keeps its original params (name not clobbered)
|
||||
expect(st.tabs[0].form!.params.name).toBe("first_seq");
|
||||
});
|
||||
|
||||
it("dedups edit tabs by kind+schema+name but a create tab of the same kind coexists", () => {
|
||||
const editOpts = {
|
||||
kind: "enum" as const,
|
||||
schema: "public",
|
||||
name: "role",
|
||||
title: "Edit role",
|
||||
description: "Edit public.role",
|
||||
mode: "edit" as const,
|
||||
params: { schema: "public", name: "role", action: { op: "update" } },
|
||||
};
|
||||
useDbViewerStore.getState().openFormTab(editOpts);
|
||||
const first = useDbViewerStore.getState().tabs[0];
|
||||
useDbViewerStore.getState().openFormTab(editOpts);
|
||||
let st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(1);
|
||||
expect(st.activeTabId).toBe(first.id);
|
||||
|
||||
// A create tab of the same kind is a distinct tab.
|
||||
useDbViewerStore.getState().openFormTab(createOpts("new_enum", "Create enum"));
|
||||
st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(2);
|
||||
expect(st.tabs.every((t) => t.tabType === "objectForm")).toBe(true);
|
||||
expect(st.tabs.some((t) => t.form!.mode === "edit")).toBe(true);
|
||||
expect(st.tabs.some((t) => t.form!.mode === "create")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not collide with a table tab (distinct types)", () => {
|
||||
useDbViewerStore.getState().openTab("public", "users");
|
||||
useDbViewerStore.getState().openFormTab(createOpts("users_id_seq"));
|
||||
const st = useDbViewerStore.getState();
|
||||
expect(st.tabs).toHaveLength(2);
|
||||
expect(st.tabs[0].tabType).toBe("table");
|
||||
expect(st.tabs[1].tabType).toBe("objectForm");
|
||||
});
|
||||
|
||||
it("resolves an empty schema to the current schema", () => {
|
||||
useDbViewerStore.setState({ currentSchema: "utils" });
|
||||
useDbViewerStore
|
||||
.getState()
|
||||
.openFormTab({ ...createOpts("my_seq"), schema: "" });
|
||||
const tab = useDbViewerStore.getState().tabs[0];
|
||||
expect(tab.schema).toBe("utils");
|
||||
// params stay untouched — the form owns them
|
||||
expect(tab.form!.params.schema).toBe("public");
|
||||
});
|
||||
|
||||
it("falls back to public when no schema and no currentSchema", () => {
|
||||
useDbViewerStore
|
||||
.getState()
|
||||
.openFormTab({ ...createOpts("my_seq"), schema: "" });
|
||||
const tab = useDbViewerStore.getState().tabs[0];
|
||||
expect(tab.schema).toBe("public");
|
||||
});
|
||||
|
||||
it("keeps an explicit schema even when currentSchema is set", () => {
|
||||
useDbViewerStore.setState({ currentSchema: "utils" });
|
||||
useDbViewerStore
|
||||
.getState()
|
||||
.openFormTab({ ...createOpts("my_seq"), schema: "public" });
|
||||
const tab = useDbViewerStore.getState().tabs[0];
|
||||
expect(tab.schema).toBe("public");
|
||||
});
|
||||
|
||||
it("updateFormTabParams updates params without touching title/description/mode/kind", () => {
|
||||
useDbViewerStore.getState().openFormTab(createOpts("my_seq"));
|
||||
const tabId = useDbViewerStore.getState().tabs[0].id;
|
||||
const before = useDbViewerStore.getState().tabs[0].form;
|
||||
expect(before).toBeDefined();
|
||||
|
||||
useDbViewerStore
|
||||
.getState()
|
||||
.updateFormTabParams(tabId, { schema: "other", name: "renamed", action: { op: "create" } });
|
||||
|
||||
const after = useDbViewerStore.getState().tabs[0].form;
|
||||
expect(after?.params).toEqual({ schema: "other", name: "renamed", action: { op: "create" } });
|
||||
expect(after?.title).toBe(before?.title);
|
||||
expect(after?.description).toBe(before?.description);
|
||||
expect(after?.mode).toBe(before?.mode);
|
||||
expect(after?.kind).toBe(before?.kind);
|
||||
});
|
||||
});
|
||||
|
||||
describe("roles slice", () => {
|
||||
it("setRoles stores roles", () => {
|
||||
useDbViewerStore.getState().reset();
|
||||
const r: RoleInfo = {
|
||||
name: "app", superuser: false, inherit: true, create_db: false, create_role: false,
|
||||
can_login: true, replication: false, bypass_rls: false, connection_limit: -1,
|
||||
valid_until: null, memberships: [],
|
||||
};
|
||||
useDbViewerStore.getState().setRoles([r]);
|
||||
expect(useDbViewerStore.getState().roles).toEqual([r]);
|
||||
});
|
||||
|
||||
it("openFormTab accepts kind table and role", () => {
|
||||
useDbViewerStore.getState().reset();
|
||||
useDbViewerStore.getState().openFormTab({
|
||||
kind: "table", schema: "public", name: "", title: "Create Table",
|
||||
description: "Create Table", mode: "create", params: { schema: "public", name: "", action: { op: "create", columns: [] } },
|
||||
});
|
||||
const t = useDbViewerStore.getState().tabs[0];
|
||||
expect(t.tabType).toBe("objectForm");
|
||||
expect(t.form?.kind).toBe("table");
|
||||
// dedup: second create of same kind focuses existing
|
||||
useDbViewerStore.getState().openFormTab({
|
||||
kind: "table", schema: "public", name: "", title: "Create Table",
|
||||
description: "Create Table", mode: "create", params: { schema: "public", name: "", action: { op: "create", columns: [] } },
|
||||
});
|
||||
expect(useDbViewerStore.getState().tabs.filter((x) => x.form?.kind === "table").length).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user