From dbfe284ca963bc83f85c08c1a66246ef0bbe9968 Mon Sep 17 00:00:00 2001 From: zipg Date: Fri, 26 Jun 2026 13:59:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=AE=9A=E4=BD=8D=E8=A1=A8=E8=8A=82=E7=82=B9=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=8F=8A=E8=BF=9E=E6=8E=A5=E9=85=8D=E7=BD=AE=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=AD=97=E6=AE=B5=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: 修复侧边栏定位表节点异常 * Fix: 修复连接配置测试字段缺失 --------- Co-authored-by: staff --- .../src/components/sidebar/ConnectionTree.vue | 16 +++ .../desktop/src/lib/queryCursorTableTarget.ts | 2 +- .../desktop/src/lib/sidebarActiveTabTarget.ts | 6 +- apps/desktop/src/stores/connectionStore.ts | 122 +++++++++++++++++- .../app-tests/sidebarActiveTabTarget.test.ts | 1 + 5 files changed, 141 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 352cf7f88..777c6a0bc 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -400,6 +400,12 @@ const pendingRenameGroupId = ref(null); const highlightedNodeId = ref(null); let highlightTimer: number | undefined; +function topOcclusionHeightForSidebarNode(nodeId: string): number { + const sticky = stickyNode.value; + if (!useVirtualTree.value || !sticky || sticky.id === nodeId) return 0; + return SIDEBAR_TREE_ROW_HEIGHT; +} + async function scrollToSidebarNode(nodeId: string) { await nextTick(); @@ -411,6 +417,7 @@ async function scrollToSidebarNode(nodeId: string) { index, currentScrollTop: scroller.scrollTop, viewportHeight: scroller.clientHeight, + topOcclusionHeight: topOcclusionHeightForSidebarNode(nodeId), }); if (nextScrollTop !== scroller.scrollTop) { scroller.scrollTop = nextScrollTop; @@ -486,6 +493,12 @@ async function locateActiveTabInSidebar() { nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null; } + if (!nodePath && cursorCandidate) { + await store.loadTableForLocate(cursorCandidate); + target = resolveLoadedLocateTarget(initialTarget, cursorCandidate); + nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null; + } + if (!nodePath && cursorCandidate && fallbackTarget) { await ensureTreeLoadedForTarget(fallbackTarget); target = fallbackTarget; @@ -506,6 +519,8 @@ async function locateActiveTabInSidebar() { if (!match) return; store.selectedTreeNodeId = match.id; + store.selectedTreeNodeIds = [match.id]; + store.treeSelectionAnchorId = match.id; await nextTick(); window.clearTimeout(highlightTimer); @@ -694,6 +709,7 @@ async function selectActiveTabSidebarNode(options: { scroll: boolean }) { index, currentScrollTop: scroller.scrollTop, viewportHeight: scroller.clientHeight, + topOcclusionHeight: topOcclusionHeightForSidebarNode(match.id), }); if (nextScrollTop !== scroller.scrollTop) { scroller.scrollTop = nextScrollTop; diff --git a/apps/desktop/src/lib/queryCursorTableTarget.ts b/apps/desktop/src/lib/queryCursorTableTarget.ts index ad5a777d4..5f150c458 100644 --- a/apps/desktop/src/lib/queryCursorTableTarget.ts +++ b/apps/desktop/src/lib/queryCursorTableTarget.ts @@ -155,7 +155,7 @@ function sameIdentifier(left: string | undefined, right: string | undefined): bo } function nodeMatchesCandidate(node: TreeNode, candidate: QueryCursorTableCandidate): boolean { - if (node.type !== "table" && node.type !== "view") return false; + if (node.type !== "table" && node.type !== "view" && node.type !== "materialized_view") return false; if (node.connectionId !== candidate.connectionId) return false; if (!sameIdentifier(node.database, candidate.database)) return false; if (candidate.schema && !sameIdentifier(node.schema, candidate.schema)) return false; diff --git a/apps/desktop/src/lib/sidebarActiveTabTarget.ts b/apps/desktop/src/lib/sidebarActiveTabTarget.ts index 4bd2b16ec..adf86a5a5 100644 --- a/apps/desktop/src/lib/sidebarActiveTabTarget.ts +++ b/apps/desktop/src/lib/sidebarActiveTabTarget.ts @@ -180,16 +180,16 @@ export function shouldScrollActiveSidebarSelection(options: { activeTabId: strin return options.activeTabId !== options.previousActiveTabId || (options.autoSelectEnabled && options.previousAutoSelectEnabled === false); } -export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; rowHeight?: number }): number { +export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; rowHeight?: number; topOcclusionHeight?: number }): number { const rowHeight = options.rowHeight ?? SIDEBAR_TREE_ROW_HEIGHT; if (options.index < 0 || options.viewportHeight <= 0) return options.currentScrollTop; const rowTop = options.index * rowHeight; const rowBottom = rowTop + rowHeight; - const viewportTop = options.currentScrollTop; + const viewportTop = options.currentScrollTop + (options.topOcclusionHeight ?? 0); const viewportBottom = options.currentScrollTop + options.viewportHeight; - if (rowTop < viewportTop) return rowTop; + if (rowTop < viewportTop) return Math.max(0, rowTop - (options.topOcclusionHeight ?? 0)); if (rowBottom > viewportBottom) return Math.max(0, rowBottom - options.viewportHeight); return options.currentScrollTop; } diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 638b6eef0..c31b6969f 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -39,6 +39,7 @@ import { mergeTableTreePageChildren, objectGroupRefreshParentId, objectTypesForGroupNode, + sortDatabaseObjectsByName, tablePartitionGroups, type DatabaseObjectTreeKind, } from "@/lib/tableTree"; @@ -71,6 +72,13 @@ function sidebarObjectGroupPageSize(): number { } type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip"; +interface LocateTableTarget { + connectionId: string; + database: string; + schema?: string; + tableName: string; +} + function nodeIdPart(value: string): string { return encodeURIComponent(value); } @@ -636,6 +644,44 @@ export const useConnectionStore = defineStore("connection", () => { })); } + function sameSidebarObjectName(left: string | undefined, right: string | undefined): boolean { + return (left || "").toLowerCase() === (right || "").toLowerCase(); + } + + function treeNodeObjectIdentity(node: TreeNode): string { + return `${node.type}\0${(node.schema || "").toLowerCase()}\0${node.label.toLowerCase()}`; + } + + function mergeLocatedTreeChildren(parent: TreeNode, currentChildren: TreeNode[], pageChildren: TreeNode[], connectionId: string, database: string): TreeNode[] { + const tableChildren = pageChildren.filter((child) => child.type === "table"); + const nonTableChildren = pageChildren.filter((child) => child.type !== "table"); + let merged = tableChildren.length ? mergeTableTreePageChildren(currentChildren, tableChildren, connectionId, database) : [...currentChildren]; + const existing = new Set(merged.map(treeNodeObjectIdentity)); + for (const child of nonTableChildren) { + const key = treeNodeObjectIdentity(child); + if (existing.has(key)) continue; + merged.push(child); + existing.add(key); + } + const config = parent.connectionId ? getConfig(parent.connectionId) : undefined; + return sortSidebarTreeChildrenForParent( + parent, + sortDatabaseObjectsByName(merged, (node) => node.label), + config?.db_type, + ); + } + + function findTreeNodes(nodes: TreeNode[], predicate: (node: TreeNode) => boolean): TreeNode[] { + const matches: TreeNode[] = []; + for (const node of nodes) { + if (predicate(node)) matches.push(node); + if (node.children) matches.push(...findTreeNodes(node.children, predicate)); + const hiddenOnlyChildren = node.hiddenChildren?.filter((child) => !(node.children || []).includes(child)); + if (hiddenOnlyChildren?.length) matches.push(...findTreeNodes(hiddenOnlyChildren, predicate)); + } + return matches; + } + async function loadPagedTableGroupChildren(options: { node: TreeNode; parentNodeId: string; @@ -644,11 +690,12 @@ export const useConnectionStore = defineStore("connection", () => { objectTypes: DatabaseObjectTreeKind[]; offset: number; pageSize: number; + searchFilter?: string; }): Promise<{ children: TreeNode[]; objectCount: number; hasMore: boolean; nextOffset: number }> { if (!options.node.connectionId || !options.node.database) { return { children: [], objectCount: 0, hasMore: false, nextOffset: options.offset }; } - const searchFilter = sidebarSearchQuery.value || undefined; + const searchFilter = options.searchFilter || sidebarSearchQuery.value || undefined; const fetchLimit = searchFilter ? options.pageSize : options.pageSize + 1; const tables = await api.listTables(options.node.connectionId, options.node.database, options.querySchema, searchFilter, fetchLimit, searchFilter ? undefined : options.offset, options.objectTypes); const hasMore = searchFilter ? false : tables.length > options.pageSize; @@ -679,8 +726,9 @@ export const useConnectionStore = defineStore("connection", () => { nonTableObjectTypes: DatabaseObjectTreeKind[]; offset: number; pageSize: number; + searchFilter?: string; }): Promise<{ children: TreeNode[]; objectCount: number; hasMore: boolean; nextOffset: number }> { - const searchFilter = sidebarSearchQuery.value || undefined; + const searchFilter = options.searchFilter || sidebarSearchQuery.value || undefined; const fetchLimit = searchFilter ? options.pageSize : options.pageSize + 1; const tables = await api.listTables(options.connectionId, options.database, options.querySchema, searchFilter, fetchLimit, searchFilter ? undefined : options.offset); const hasMore = searchFilter ? false : tables.length > options.pageSize; @@ -2052,6 +2100,75 @@ export const useConnectionStore = defineStore("connection", () => { } } + async function loadTableForLocate(target: LocateTableTarget): Promise { + const config = getConfig(target.connectionId); + if (!config) return false; + await ensureConnected(target.connectionId); + + const querySchema = connectionObjectTreeQuerySchema(config, target.database, target.schema); + const effectiveSchema = connectionObjectTreeNodeSchema(config, target.database, target.schema); + const pageSize = sidebarObjectGroupPageSize(); + const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple"; + let loaded = false; + + if (simpleObjectDisplay) { + const parentId = target.schema ? `${target.connectionId}:${target.database}:${target.schema}` : `${target.connectionId}:${target.database}`; + const parent = findNode(treeNodes.value, parentId); + if (!parent) return false; + const page = await loadPagedSimpleTableChildren({ + nodeId: parentId, + connectionId: target.connectionId, + database: target.database, + querySchema, + effectiveSchema, + nonTableObjectTypes: [], + offset: 0, + pageSize, + searchFilter: target.tableName, + }); + if (!page.children.length) return false; + const currentChildren = withoutLoadMoreNodes(parent.children); + const loadMoreNodes = (parent.children || []).filter((child) => child.type === "load-more"); + const mergedChildren = mergeLocatedTreeChildren(parent, currentChildren, page.children, target.connectionId, target.database); + setChildren(parent, [...mergedChildren, ...loadMoreNodes]); + parent.objectCount = Math.max(parent.objectCount ?? currentChildren.length, mergedChildren.length); + parent.isExpanded = true; + return true; + } + + const matchingGroups = findTreeNodes(treeNodes.value, (node) => { + return (node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views") && node.connectionId === target.connectionId && sameSidebarObjectName(node.database, target.database) && (!target.schema || sameSidebarObjectName(node.schema, target.schema)); + }); + + for (const group of matchingGroups) { + const objectTypes = objectTypesForGroupNode(group.type); + const parentNodeId = objectGroupRefreshParentId(group); + if (!objectTypes || !parentNodeId) continue; + + const page = await loadPagedTableGroupChildren({ + node: group, + parentNodeId, + querySchema, + effectiveSchema, + objectTypes, + offset: 0, + pageSize, + searchFilter: target.tableName, + }); + if (!page.children.length) continue; + + const currentChildren = withoutLoadMoreNodes(group.children); + const loadMoreNodes = (group.children || []).filter((child) => child.type === "load-more"); + const mergedChildren = mergeLocatedTreeChildren(group, currentChildren, page.children, target.connectionId, target.database); + setChildren(group, [...mergedChildren, ...loadMoreNodes]); + group.objectCount = Math.max(group.objectCount ?? currentChildren.length, mergedChildren.length); + group.isExpanded = true; + loaded = true; + } + + return loaded; + } + async function loadAllObjectGroupChildren(parent: TreeNode) { if (!parent.connectionId || !hasTreeNodeDatabaseContext(parent)) return; if (!objectTypesForGroupNode(parent.type)) return; @@ -3682,6 +3799,7 @@ export const useConnectionStore = defineStore("connection", () => { loadSqlServerLinkedServerCatalogs, loadSqlServerLinkedServerSchemas, loadTables, + loadTableForLocate, loadObjectGroupChildren, loadMoreObjectGroupChildren, loadAllObjectGroupChildren, diff --git a/packages/app-tests/sidebarActiveTabTarget.test.ts b/packages/app-tests/sidebarActiveTabTarget.test.ts index de9fb6aba..fd582aeeb 100644 --- a/packages/app-tests/sidebarActiveTabTarget.test.ts +++ b/packages/app-tests/sidebarActiveTabTarget.test.ts @@ -213,6 +213,7 @@ test("sidebar node scrolling keeps visible rows in place and reveals hidden rows assert.equal(scrollTopForSidebarNode({ index: 2, currentScrollTop: 0, viewportHeight: 140 }), 0); assert.equal(scrollTopForSidebarNode({ index: 20, currentScrollTop: 0, viewportHeight: 140 }), 448); assert.equal(scrollTopForSidebarNode({ index: 1, currentScrollTop: 280, viewportHeight: 140 }), 28); + assert.equal(scrollTopForSidebarNode({ index: 11, currentScrollTop: 300, viewportHeight: 140, topOcclusionHeight: 28 }), 280); }); test("active sidebar selection only scrolls on tab or setting changes", () => {