fix(tree): preserve object group metadata routing
This commit is contained in:
parent
01ecb6020c
commit
7b14ef3b48
|
|
@ -65,6 +65,7 @@ import * as api from "@/lib/backend/api";
|
|||
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { canTreeNodePin, canTreeNodeShowExpander } from "@/lib/sidebar/sidebarTreeItemLayout";
|
||||
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
|
||||
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
||||
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
|
||||
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
|
||||
import {
|
||||
|
|
@ -513,6 +514,11 @@ async function toggle() {
|
|||
}
|
||||
|
||||
try {
|
||||
if (await loadSidebarObjectGroup(node, connectionStore)) {
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "connection" && node.connectionId) {
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (config?.db_type === "redis") {
|
||||
|
|
@ -623,10 +629,6 @@ async function toggle() {
|
|||
await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
||||
} else if (node.type === "group-fkeys" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
||||
await connectionStore.loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
||||
} else if (node.type === "group-triggers" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
||||
await connectionStore.loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
||||
} else if (databaseObjectGroup) {
|
||||
await connectionStore.loadObjectGroupChildren(node);
|
||||
}
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
||||
import type { ConnectionConfig, ObjectInfo, TreeNode } from "@/types/database";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
function mysqlConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "mysql-1",
|
||||
name: "MySQL",
|
||||
db_type: "mysql",
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
username: "root",
|
||||
password: "",
|
||||
database: "app",
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function objectGroup(type: "group-triggers" | "group-types", id: string): TreeNode {
|
||||
return {
|
||||
id,
|
||||
label: type === "group-triggers" ? "tree.triggers" : "tree.types",
|
||||
type,
|
||||
connectionId: "mysql-1",
|
||||
database: "app",
|
||||
schema: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function createStore({ listObjects, listTriggers }: { listObjects: ReturnType<typeof vi.fn>; listTriggers: ReturnType<typeof vi.fn> }) {
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listObjects,
|
||||
listTriggers,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = mysqlConnection();
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
useSettingsStore().desktopSettings.sidebar_table_page_size = 10;
|
||||
return { connection, store };
|
||||
}
|
||||
|
||||
describe("sidebar object-group routing", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("uses listObjects for schema-level trigger and type groups, including empty results", async () => {
|
||||
const listObjects = vi.fn<() => Promise<ObjectInfo[]>>().mockResolvedValue([]);
|
||||
const listTriggers = vi.fn<() => Promise<never[]>>().mockResolvedValue([]);
|
||||
const { connection, store } = await createStore({ listObjects, listTriggers });
|
||||
const triggerGroup = objectGroup("group-triggers", `${connection.id}:app:app:__triggers`);
|
||||
const typeGroup = objectGroup("group-types", `${connection.id}:app:app:__types`);
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [triggerGroup, typeGroup] }];
|
||||
const storedTriggerGroup = store.treeNodes[0].children![0];
|
||||
const storedTypeGroup = store.treeNodes[0].children![1];
|
||||
|
||||
await loadSidebarObjectGroup(storedTriggerGroup, store);
|
||||
await loadSidebarObjectGroup(storedTypeGroup, store);
|
||||
|
||||
expect(listObjects).toHaveBeenNthCalledWith(1, connection.id, "app", "app", ["TRIGGER"], undefined, 11, 0);
|
||||
expect(listObjects).toHaveBeenNthCalledWith(2, connection.id, "app", "app", ["TYPE", "TYPE_BODY"], undefined, 11, 0);
|
||||
expect(listTriggers).not.toHaveBeenCalled();
|
||||
expect(storedTriggerGroup).toMatchObject({ isExpanded: true, isLoading: false, children: [] });
|
||||
expect(storedTypeGroup).toMatchObject({ isExpanded: true, isLoading: false, children: [] });
|
||||
});
|
||||
|
||||
it("keeps table-level trigger groups on listTriggers", async () => {
|
||||
const listObjects = vi.fn<() => Promise<ObjectInfo[]>>().mockResolvedValue([]);
|
||||
const listTriggers = vi.fn<() => Promise<never[]>>().mockResolvedValue([]);
|
||||
const { connection, store } = await createStore({ listObjects, listTriggers });
|
||||
const tableTriggerGroup: TreeNode = {
|
||||
...objectGroup("group-triggers", `${connection.id}:app:app:orders:__triggers`),
|
||||
tableName: "orders",
|
||||
};
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [tableTriggerGroup] }];
|
||||
const storedTriggerGroup = store.treeNodes[0].children![0];
|
||||
|
||||
await loadSidebarObjectGroup(storedTriggerGroup, store);
|
||||
|
||||
expect(listTriggers).toHaveBeenCalledWith(connection.id, "app", "app", "orders", undefined);
|
||||
expect(listObjects).not.toHaveBeenCalled();
|
||||
expect(storedTriggerGroup).toMatchObject({ isExpanded: true, isLoading: false, children: [] });
|
||||
});
|
||||
|
||||
it("propagates rejected schema-level metadata while clearing the loading state", async () => {
|
||||
const listObjects = vi.fn<() => Promise<ObjectInfo[]>>().mockRejectedValue(new Error("metadata access denied"));
|
||||
const listTriggers = vi.fn<() => Promise<never[]>>().mockResolvedValue([]);
|
||||
const { connection, store } = await createStore({ listObjects, listTriggers });
|
||||
const triggerGroup = objectGroup("group-triggers", `${connection.id}:app:app:__triggers`);
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [triggerGroup] }];
|
||||
const storedTriggerGroup = store.treeNodes[0].children![0];
|
||||
|
||||
await expect(loadSidebarObjectGroup(storedTriggerGroup, store)).rejects.toThrow("metadata access denied");
|
||||
|
||||
expect(listObjects).toHaveBeenCalledWith(connection.id, "app", "app", ["TRIGGER"], undefined, 11, 0);
|
||||
expect(listTriggers).not.toHaveBeenCalled();
|
||||
expect(storedTriggerGroup).toMatchObject({ isExpanded: false, isLoading: false, children: [] });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import type { TreeNode } from "@/types/database";
|
||||
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
|
||||
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
|
||||
|
||||
export interface SidebarObjectGroupLoaders {
|
||||
loadTriggers(connectionId: string, database: string, table: string, schema?: string, nodeId?: string, catalog?: string): Promise<void>;
|
||||
loadObjectGroupChildren(node: TreeNode): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads object-group metadata from the same path used by the sidebar toggle.
|
||||
* Table trigger groups are special because they are scoped to one table;
|
||||
* schema-level trigger groups are regular database object groups.
|
||||
*/
|
||||
export async function loadSidebarObjectGroup(node: TreeNode, loaders: SidebarObjectGroupLoaders): Promise<boolean> {
|
||||
if (node.type === "group-triggers" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
||||
await loaders.loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!objectTypesForGroupNode(node.type)) return false;
|
||||
await loaders.loadObjectGroupChildren(node);
|
||||
return true;
|
||||
}
|
||||
Loading…
Reference in New Issue