merge: catalogless schema tree fix
This commit is contained in:
commit
9cbca5fc3a
|
|
@ -105,6 +105,7 @@ import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType }
|
|||
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
|
||||
import { hexToRgba } from "@/lib/color";
|
||||
import { focusSidebarRenameInput, shouldPreventRenameCloseAutoFocus } from "@/lib/sidebarRenameFocus";
|
||||
import { hasTreeNodeDatabaseContext } from "@/lib/treeNodeContext";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
|
|
@ -250,6 +251,7 @@ function isGroupLabel(node: TreeNode): boolean {
|
|||
|
||||
function displayLabel(node: TreeNode): string {
|
||||
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
|
||||
if (node.label === "tree.defaultDatabase") return t(node.label);
|
||||
return isGroupLabel(node) ? t(node.label) : node.label;
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +310,7 @@ async function toggle() {
|
|||
const tabTitle = `${node.database}.${node.label}`;
|
||||
const tab = queryStore.createTab(node.connectionId, node.database, tabTitle, "mongo");
|
||||
queryStore.updateSql(tab, node.label);
|
||||
} else if (node.type === "database" && node.connectionId && node.database) {
|
||||
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (config?.db_type === "sqlserver") {
|
||||
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database);
|
||||
|
|
@ -317,17 +319,36 @@ async function toggle() {
|
|||
} else {
|
||||
await connectionStore.loadTables(node.connectionId, node.database);
|
||||
}
|
||||
} else if (node.type === "schema" && node.connectionId && node.database && node.schema) {
|
||||
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
|
||||
} else if ((node.type === "table" || node.type === "view") && node.connectionId && node.database) {
|
||||
} else if (
|
||||
(node.type === "table" || node.type === "view") &&
|
||||
node.connectionId &&
|
||||
hasTreeNodeDatabaseContext(node)
|
||||
) {
|
||||
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
|
||||
} else if (node.type === "group-columns" && node.connectionId && node.database && node.tableName) {
|
||||
} else if (
|
||||
node.type === "group-columns" &&
|
||||
node.connectionId &&
|
||||
hasTreeNodeDatabaseContext(node) &&
|
||||
node.tableName
|
||||
) {
|
||||
await connectionStore.loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id);
|
||||
} else if (node.type === "group-indexes" && node.connectionId && node.database && node.tableName) {
|
||||
} else if (
|
||||
node.type === "group-indexes" &&
|
||||
node.connectionId &&
|
||||
hasTreeNodeDatabaseContext(node) &&
|
||||
node.tableName
|
||||
) {
|
||||
await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id);
|
||||
} else if (node.type === "group-fkeys" && node.connectionId && node.database && node.tableName) {
|
||||
} 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);
|
||||
} else if (node.type === "group-triggers" && node.connectionId && node.database && node.tableName) {
|
||||
} 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);
|
||||
}
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
|
|
|
|||
|
|
@ -744,6 +744,7 @@ export default {
|
|||
},
|
||||
tree: {
|
||||
savedSql: "SQL Library",
|
||||
defaultDatabase: "Default DB",
|
||||
columns: "Columns",
|
||||
indexes: "Indexes",
|
||||
foreignKeys: "Foreign Keys",
|
||||
|
|
|
|||
|
|
@ -641,6 +641,7 @@ export default {
|
|||
},
|
||||
tree: {
|
||||
savedSql: "Biblioteca SQL",
|
||||
defaultDatabase: "Base predeterminada",
|
||||
columns: "Columnas",
|
||||
indexes: "Índices",
|
||||
foreignKeys: "Claves foráneas",
|
||||
|
|
|
|||
|
|
@ -725,6 +725,7 @@ export default {
|
|||
},
|
||||
tree: {
|
||||
savedSql: "SQL 库",
|
||||
defaultDatabase: "默认库",
|
||||
columns: "字段",
|
||||
indexes: "索引",
|
||||
foreignKeys: "外键",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,38 @@
|
|||
import type { DatabaseInfo, TreeNode } from "@/types/database";
|
||||
import { DEFAULT_DATABASE_TREE_LABEL } from "./treeNodeContext";
|
||||
|
||||
export function buildDatabaseTreeNodes(connectionId: string, databases: DatabaseInfo[]): TreeNode[] {
|
||||
return databases.map((db) => ({
|
||||
id: `${connectionId}:${db.name}`,
|
||||
label: db.name,
|
||||
type: "database" as const,
|
||||
connectionId,
|
||||
database: db.name,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
}));
|
||||
export function buildDatabaseTreeNodes(
|
||||
connectionId: string,
|
||||
databases: DatabaseInfo[],
|
||||
options: { includeDefaultWhenEmpty?: boolean } = {},
|
||||
): TreeNode[] {
|
||||
const nodes = databases.flatMap((db) => {
|
||||
const name = db.name.trim();
|
||||
if (!name) return [];
|
||||
return [
|
||||
{
|
||||
id: `${connectionId}:${name}`,
|
||||
label: name,
|
||||
type: "database" as const,
|
||||
connectionId,
|
||||
database: name,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
if (nodes.length > 0 || !options.includeDefaultWhenEmpty) return nodes;
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${connectionId}:`,
|
||||
label: DEFAULT_DATABASE_TREE_LABEL,
|
||||
type: "database" as const,
|
||||
connectionId,
|
||||
database: "",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import type { TreeNode } from "@/types/database";
|
||||
|
||||
export const DEFAULT_DATABASE_TREE_LABEL = "tree.defaultDatabase";
|
||||
|
||||
export function hasTreeNodeDatabaseContext(node: Pick<TreeNode, "database">): node is Pick<TreeNode, "database"> & {
|
||||
database: string;
|
||||
} {
|
||||
return node.database != null;
|
||||
}
|
||||
|
||||
function schemaCacheKey(...parts: string[]): string {
|
||||
return parts.map((part) => encodeURIComponent(part)).join(":");
|
||||
}
|
||||
|
||||
export function treeNodeSchemaCachePrefix(node: TreeNode): string | null {
|
||||
if (node.type === "connection" && node.connectionId) {
|
||||
return `${schemaCacheKey(node.connectionId)}:`;
|
||||
}
|
||||
if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
||||
return `${schemaCacheKey(node.connectionId, node.database)}:`;
|
||||
}
|
||||
if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
|
||||
return `${schemaCacheKey(node.connectionId, node.database, node.schema)}:`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeCataloglessDatabaseNodes(nodes: TreeNode[]): TreeNode[] {
|
||||
return nodes.map((node) => {
|
||||
const normalized =
|
||||
node.type === "database" && node.database === "" && !node.label.trim()
|
||||
? { ...node, label: DEFAULT_DATABASE_TREE_LABEL }
|
||||
: node;
|
||||
|
||||
return normalized.children
|
||||
? {
|
||||
...normalized,
|
||||
children: normalizeCataloglessDatabaseNodes(normalized.children),
|
||||
}
|
||||
: normalized;
|
||||
});
|
||||
}
|
||||
|
|
@ -32,6 +32,11 @@ import {
|
|||
expandCachedObjectBrowserNodes,
|
||||
objectGroupRefreshParentId,
|
||||
} from "@/lib/tableTree";
|
||||
import {
|
||||
hasTreeNodeDatabaseContext,
|
||||
normalizeCataloglessDatabaseNodes,
|
||||
treeNodeSchemaCachePrefix,
|
||||
} from "@/lib/treeNodeContext";
|
||||
import { decodeSchemaTreeCache, encodeSchemaTreeCache } from "@/lib/schemaTreeCache";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
|
||||
|
|
@ -355,7 +360,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const payload = await api.loadSchemaCache<unknown>(cacheKey).catch(() => null);
|
||||
const decoded = decodeSchemaTreeCache<TreeNode[]>(payload);
|
||||
if (!decoded) return { hit: false, isStale: false };
|
||||
const normalizedChildren = expandCachedObjectBrowserNodes(decoded.children);
|
||||
const normalizedChildren = normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children));
|
||||
setChildren(
|
||||
node,
|
||||
node.type === "connection" && node.connectionId
|
||||
|
|
@ -394,16 +399,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
}
|
||||
|
||||
function schemaCachePrefixForNode(node: TreeNode): string | null {
|
||||
if (node.type === "connection" && node.connectionId) {
|
||||
return `${schemaCacheKey(node.connectionId)}:`;
|
||||
}
|
||||
if (node.type === "database" && node.connectionId && node.database) {
|
||||
return `${schemaCacheKey(node.connectionId, node.database)}:`;
|
||||
}
|
||||
if (node.type === "schema" && node.connectionId && node.database && node.schema) {
|
||||
return `${schemaCacheKey(node.connectionId, node.database, node.schema)}:`;
|
||||
}
|
||||
return null;
|
||||
return treeNodeSchemaCachePrefix(node);
|
||||
}
|
||||
|
||||
async function clearPersistedTreeCacheForNode(node: TreeNode) {
|
||||
|
|
@ -692,7 +688,13 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
);
|
||||
const visibleNameSet = new Set(visibleNames);
|
||||
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
|
||||
const children = withSavedSqlRoot(connectionId, buildDatabaseTreeNodes(connectionId, visibleDatabases), node);
|
||||
const children = withSavedSqlRoot(
|
||||
connectionId,
|
||||
buildDatabaseTreeNodes(connectionId, visibleDatabases, {
|
||||
includeDefaultWhenEmpty: usesTreeSchemaMode(config?.db_type),
|
||||
}),
|
||||
node,
|
||||
);
|
||||
setChildren(node, children);
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
|
|
@ -1161,7 +1163,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
}
|
||||
} else if (node.type === "mongo-db" && node.connectionId && node.database) {
|
||||
await loadMongoCollections(node.connectionId, node.database);
|
||||
} else if (node.type === "database" && node.connectionId && node.database) {
|
||||
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
||||
const config = getConfig(node.connectionId);
|
||||
if (config?.db_type === "sqlserver") {
|
||||
await loadSqlServerDatabaseObjects(node.connectionId, node.database, options);
|
||||
|
|
@ -1170,17 +1172,36 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
} else {
|
||||
await loadTables(node.connectionId, node.database, undefined, options);
|
||||
}
|
||||
} else if (node.type === "schema" && node.connectionId && node.database && node.schema) {
|
||||
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
|
||||
await loadTables(node.connectionId, node.database, node.schema, options);
|
||||
} else if ((node.type === "table" || node.type === "view") && node.connectionId && node.database) {
|
||||
} else if (
|
||||
(node.type === "table" || node.type === "view") &&
|
||||
node.connectionId &&
|
||||
hasTreeNodeDatabaseContext(node)
|
||||
) {
|
||||
await loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
|
||||
} else if (node.type === "group-columns" && node.connectionId && node.database && node.tableName) {
|
||||
} else if (
|
||||
node.type === "group-columns" &&
|
||||
node.connectionId &&
|
||||
hasTreeNodeDatabaseContext(node) &&
|
||||
node.tableName
|
||||
) {
|
||||
await loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id);
|
||||
} else if (node.type === "group-indexes" && node.connectionId && node.database && node.tableName) {
|
||||
} else if (
|
||||
node.type === "group-indexes" &&
|
||||
node.connectionId &&
|
||||
hasTreeNodeDatabaseContext(node) &&
|
||||
node.tableName
|
||||
) {
|
||||
await loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id);
|
||||
} else if (node.type === "group-fkeys" && node.connectionId && node.database && node.tableName) {
|
||||
} else if (node.type === "group-fkeys" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
||||
await loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id);
|
||||
} else if (node.type === "group-triggers" && node.connectionId && node.database && node.tableName) {
|
||||
} else if (
|
||||
node.type === "group-triggers" &&
|
||||
node.connectionId &&
|
||||
hasTreeNodeDatabaseContext(node) &&
|
||||
node.tableName
|
||||
) {
|
||||
await loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id);
|
||||
} else if (
|
||||
node.type === "group-tables" ||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@ import test from "node:test";
|
|||
import { buildDatabaseTreeNodes } from "../../apps/desktop/src/lib/databaseTree.ts";
|
||||
|
||||
test("设置默认库后侧边栏数据库树仍保留全部数据库", () => {
|
||||
const nodes = buildDatabaseTreeNodes("conn-1", [
|
||||
{ name: "campaign_data" },
|
||||
{ name: "cms" },
|
||||
{ name: "mk_campaign" },
|
||||
]);
|
||||
const nodes = buildDatabaseTreeNodes("conn-1", [{ name: "campaign_data" }, { name: "cms" }, { name: "mk_campaign" }]);
|
||||
|
||||
assert.deepEqual(
|
||||
nodes.map((node) => node.database),
|
||||
|
|
@ -15,3 +11,27 @@ test("设置默认库后侧边栏数据库树仍保留全部数据库", () => {
|
|||
);
|
||||
assert.equal(nodes.find((node) => node.database === "mk_campaign")?.id, "conn-1:mk_campaign");
|
||||
});
|
||||
|
||||
test("catalogless database metadata gets a visible default node", () => {
|
||||
const nodes = buildDatabaseTreeNodes("conn-1", [{ name: " " }], { includeDefaultWhenEmpty: true });
|
||||
|
||||
assert.deepEqual(nodes, [
|
||||
{
|
||||
id: "conn-1:",
|
||||
label: "tree.defaultDatabase",
|
||||
type: "database",
|
||||
connectionId: "conn-1",
|
||||
database: "",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("tree schema mode can show a default node when no catalog is returned", () => {
|
||||
const nodes = buildDatabaseTreeNodes("conn-1", [], { includeDefaultWhenEmpty: true });
|
||||
|
||||
assert.equal(nodes.length, 1);
|
||||
assert.equal(nodes[0].database, "");
|
||||
assert.equal(nodes[0].label, "tree.defaultDatabase");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
hasTreeNodeDatabaseContext,
|
||||
normalizeCataloglessDatabaseNodes,
|
||||
treeNodeSchemaCachePrefix,
|
||||
} from "../../apps/desktop/src/lib/treeNodeContext.ts";
|
||||
import type { TreeNode } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
test("treats empty database string as a valid catalogless context", () => {
|
||||
assert.equal(hasTreeNodeDatabaseContext({ database: "" }), true);
|
||||
assert.equal(hasTreeNodeDatabaseContext({ database: "app" }), true);
|
||||
assert.equal(hasTreeNodeDatabaseContext({}), false);
|
||||
});
|
||||
|
||||
test("builds cache prefixes for catalogless database and schema nodes", () => {
|
||||
const databaseNode: TreeNode = {
|
||||
id: "conn:",
|
||||
label: "tree.defaultDatabase",
|
||||
type: "database",
|
||||
connectionId: "conn",
|
||||
database: "",
|
||||
};
|
||||
const schemaNode: TreeNode = {
|
||||
id: "conn::APP",
|
||||
label: "APP",
|
||||
type: "schema",
|
||||
connectionId: "conn",
|
||||
database: "",
|
||||
schema: "APP",
|
||||
};
|
||||
|
||||
assert.equal(treeNodeSchemaCachePrefix(databaseNode), "conn::");
|
||||
assert.equal(treeNodeSchemaCachePrefix(schemaNode), "conn::APP:");
|
||||
});
|
||||
|
||||
test("normalizes legacy blank cached catalogless database labels", () => {
|
||||
const nodes = normalizeCataloglessDatabaseNodes([
|
||||
{
|
||||
id: "conn:",
|
||||
label: "",
|
||||
type: "database",
|
||||
connectionId: "conn",
|
||||
database: "",
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(nodes[0].label, "tree.defaultDatabase");
|
||||
});
|
||||
Loading…
Reference in New Issue