fix: 修复侧边栏定位表节点异常及连接配置测试字段缺失
* Fix: 修复侧边栏定位表节点异常 * Fix: 修复连接配置测试字段缺失 --------- Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
parent
37d0dd7ad7
commit
dbfe284ca9
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue