fix: 修复侧边栏定位表节点异常及连接配置测试字段缺失

* Fix: 修复侧边栏定位表节点异常

* Fix: 修复连接配置测试字段缺失

---------

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg 2026-06-26 13:59:29 +08:00 committed by GitHub
parent 37d0dd7ad7
commit dbfe284ca9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 141 additions and 6 deletions

View File

@ -400,6 +400,12 @@ const pendingRenameGroupId = ref<string | null>(null);
const highlightedNodeId = ref<string | null>(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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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<boolean> {
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,

View File

@ -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", () => {