🐛 fix(sidebar): 修正树节点加载与展开时序

This commit is contained in:
Syngnat
2026-07-02 09:40:09 +08:00
parent 0428a30071
commit e466e87fcc
3 changed files with 129 additions and 6 deletions

View File

@@ -24,11 +24,13 @@ import Sidebar, {
resolveSidebarDropTargetMetricsFromDomEvent,
resolveSidebarDropInsertBefore,
resolveSidebarNodeConnectionId,
resolveSidebarSwitcherLoadKey,
resolveV2ActiveConnectionId,
resolveV2ObjectGroupTitle,
isSidebarTablePinned,
SQLFileExecutionProgressContent,
resolveSidebarTableNameForCopy,
shouldKeepSidebarSwitcherCollapsedWhileLoading,
shouldClearSidebarActiveContextOnEmptySelect,
shouldSkipSidebarLoadOnExpandWhileDragging,
shouldSkipSidebarSelectWhileDragging,
@@ -319,6 +321,45 @@ describe('Sidebar locate toolbar', () => {
expect(shouldLoadSidebarNodeOnExpand({ type: 'object-group', children: [] })).toBe(false);
});
it('keeps sidebar switchers collapsed while lazy loading is still pending', () => {
const connectionNode = {
key: 'conn-1',
data: {
key: 'conn-1',
title: '开发240',
type: 'connection' as const,
dataRef: { id: 'conn-1' },
},
expanded: true,
};
const databaseNode = {
key: 'conn-1-main',
data: {
key: 'conn-1-main',
title: 'main',
type: 'database' as const,
dataRef: { id: 'conn-1', dbName: 'main' },
},
expanded: true,
};
expect(resolveSidebarSwitcherLoadKey(connectionNode)).toBe('dbs-conn-1');
expect(resolveSidebarSwitcherLoadKey(databaseNode)).toBe('tables-conn-1-main');
expect(shouldKeepSidebarSwitcherCollapsedWhileLoading(connectionNode, new Set(['dbs-conn-1']))).toBe(true);
expect(shouldKeepSidebarSwitcherCollapsedWhileLoading(databaseNode, new Set(['tables-conn-1-main']))).toBe(true);
expect(shouldKeepSidebarSwitcherCollapsedWhileLoading({
key: 'table-users',
data: {
key: 'table-users',
title: 'users',
type: 'table' as const,
dataRef: { id: 'conn-1', dbName: 'main', tableName: 'users' },
},
loading: true,
}, new Set())).toBe(true);
expect(shouldKeepSidebarSwitcherCollapsedWhileLoading(databaseNode, new Set())).toBe(false);
});
it('wires tree expand and double-click expansion to lazy loading', () => {
const source = readSidebarSource();
@@ -328,6 +369,9 @@ describe('Sidebar locate toolbar', () => {
expect(source).toContain('clearTreeNodeChildrenByKeys(keysToClear);');
expect(source).toContain('if (!shouldSkipSidebarLoadOnExpandWhileDragging(isTreeDragging, info))');
expect(source).toContain('if (shouldLoadSidebarNodeOnExpand(node))');
expect(source).toContain('const keepCollapsed = shouldKeepSidebarSwitcherCollapsedWhileLoading(node, loadingNodesRef.current);');
expect(source).toContain('return <CaretDownFilled rotate={keepCollapsed ? -90 : undefined} />;');
expect(source).toContain('switcherIcon={renderSidebarSwitcherIcon}');
});
it('uses the appearance preference to switch table double-click to the embedded object designer', () => {
@@ -1133,6 +1177,13 @@ describe('Sidebar locate toolbar', () => {
expect(source).toContain("export type SidebarConnectionState = 'loading' | 'success' | 'error';");
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));");
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [key as string]: 'loading' }));");
expect(treeLoaderSource).toContain('let shouldMarkConnectionSuccess = false;');
expect(treeLoaderSource).toContain('let shouldMarkDatabaseSuccess = false;');
expect(treeLoaderSource).toContain('loadingNodesRef.current.delete(loadKey);');
expect(treeLoaderSource).toContain('if (shouldMarkConnectionSuccess) {');
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));");
expect(treeLoaderSource).toContain('if (shouldMarkDatabaseSuccess) {');
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));");
expect(titleRenderSource).toContain("let status: 'loading' | 'success' | 'error' | 'default' = 'default';");
expect(titleRenderSource).toContain("if (connectionStates[node.key] === 'loading') status = 'loading';");
expect(v2ContextMenuSource).toContain("const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();");

View File

@@ -68,6 +68,7 @@ import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredVa
import { createPortal } from 'react-dom';
import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Popover, Tooltip } from 'antd';
import {
CaretDownFilled,
DatabaseOutlined,
TableOutlined,
ConsoleSqlOutlined,
@@ -209,6 +210,55 @@ export {
};
export type { V2CommandSearchItem, V2RailConnectionGroup } from './sidebarV2Utils';
type SidebarTreeSwitcherNodeLike = {
key?: React.Key;
data?: TreeNode;
isLeaf?: boolean;
loading?: boolean;
};
export const resolveSidebarSwitcherLoadKey = (node: SidebarTreeSwitcherNodeLike | null | undefined): string | null => {
const treeNode = node?.data;
const dataRef = treeNode?.dataRef;
if (!treeNode) {
return null;
}
if (treeNode.type === 'connection') {
const connectionId = String(dataRef?.id || treeNode.key || node?.key || '').trim();
return connectionId ? `dbs-${connectionId}` : null;
}
if (treeNode.type === 'database') {
const connectionId = String(dataRef?.id || '').trim();
const dbName = String(dataRef?.dbName || '').trim();
return connectionId && dbName ? `tables-${connectionId}-${dbName}` : null;
}
if (treeNode.type === 'jvm-mode' || treeNode.type === 'jvm-resource') {
const connectionId = String(dataRef?.id || '').trim();
const providerMode = String(dataRef?.providerMode || '').trim().toLowerCase();
const parentPath = treeNode.type === 'jvm-resource' ? String(dataRef?.resourcePath || '').trim() : '';
return connectionId && providerMode ? `jvm-resources-${connectionId}-${providerMode}-${parentPath}` : null;
}
return null;
};
export const shouldKeepSidebarSwitcherCollapsedWhileLoading = (
node: SidebarTreeSwitcherNodeLike | null | undefined,
loadingKeys: ReadonlySet<string>,
): boolean => {
if (!node || node.isLeaf) {
return false;
}
if (node.loading) {
return true;
}
const loadKey = resolveSidebarSwitcherLoadKey(node);
return !!loadKey && loadingKeys.has(loadKey);
};
const { Search } = Input;
const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50;
const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160;
@@ -1763,6 +1813,14 @@ const Sidebar: React.FC<{
}
}
};
const renderSidebarSwitcherIcon = useCallback((node: SidebarTreeSwitcherNodeLike) => {
if (node.isLeaf) {
return null;
}
const keepCollapsed = shouldKeepSidebarSwitcherCollapsedWhileLoading(node, loadingNodesRef.current);
return <CaretDownFilled rotate={keepCollapsed ? -90 : undefined} />;
}, []);
const buildRuntimeConfig = (conn: any, overrideDatabase?: string, clearDatabase: boolean = false) => {
@@ -2949,6 +3007,7 @@ const Sidebar: React.FC<{
onDoubleClick={onDoubleClick}
onSelect={onSelect}
titleRender={titleRender}
switcherIcon={renderSidebarSwitcherIcon}
expandedKeys={expandedKeys}
onExpand={onExpand}
loadedKeys={loadedKeys}

View File

@@ -215,12 +215,13 @@ export const useSidebarTreeLoaders = ({
console.warn('检查驱动代理更新状态失败', error);
}
};
const loadDatabases = async (node: any) => {
const loadDatabases = async (node: any) => {
const conn = node.dataRef as SavedConnection;
const loadKey = `dbs-${conn.id}`;
if (loadingNodesRef.current.has(loadKey)) return;
loadingNodesRef.current.add(loadKey);
setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));
let shouldMarkConnectionSuccess = false;
const config = {
...conn.config,
port: Number(conn.config.port),
@@ -234,7 +235,6 @@ export const useSidebarTreeLoaders = ({
try {
const res = await JVMProbeCapabilities(buildRuntimeConfig(conn) as any);
if (res.success) {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
const capabilities: JVMCapability[] = Array.isArray(res.data) ? res.data as JVMCapability[] : [];
const modeNodes: TreeNode[] = capabilities.map((capability) => ({
title: capability.displayLabel || capability.mode,
@@ -264,6 +264,7 @@ export const useSidebarTreeLoaders = ({
}));
const diagnosticNode = buildJVMDiagnosticTreeNodes(conn);
replaceTreeNodeChildren(node.key, [...monitoringNodes, ...modeNodes, ...diagnosticNode]);
shouldMarkConnectionSuccess = true;
} else {
const diagnosticNode = buildJVMDiagnosticTreeNodes(conn);
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
@@ -300,6 +301,9 @@ export const useSidebarTreeLoaders = ({
}
} finally {
loadingNodesRef.current.delete(loadKey);
if (shouldMarkConnectionSuccess) {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
}
}
return;
}
@@ -309,7 +313,6 @@ export const useSidebarTreeLoaders = ({
try {
const res = await (window as any).go.app.App.RedisGetDatabases(buildRpcConnectionConfig(config));
if (res.success) {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
const redisRows: any[] = Array.isArray(res.data) ? res.data : [];
const redisDbAliases = useStore.getState().appearance.redisDbAliases;
let dbs = redisRows.map((db: any) => {
@@ -333,6 +336,7 @@ export const useSidebarTreeLoaders = ({
dbs = dbs.filter(db => conn.includeRedisDatabases!.includes(db.dbIndex));
}
replaceTreeNodeChildren(node.key, dbs);
shouldMarkConnectionSuccess = true;
} else {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
message.error({ content: res.message, key: `conn-${conn.id}-dbs` });
@@ -345,6 +349,9 @@ export const useSidebarTreeLoaders = ({
});
} finally {
loadingNodesRef.current.delete(loadKey);
if (shouldMarkConnectionSuccess) {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
}
}
return;
}
@@ -352,7 +359,6 @@ export const useSidebarTreeLoaders = ({
try {
const res = await DBGetDatabases(buildRpcConnectionConfig(config) as any);
if (res.success) {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
const dbRows: any[] = Array.isArray(res.data) ? res.data : [];
let dbs = dbRows.map((row: any) => ({
title: row.Database || row.database,
@@ -375,6 +381,7 @@ export const useSidebarTreeLoaders = ({
setLoadedKeys(prev => prev.filter(k => k !== node.key));
message.warning({ content: t('sidebar.message.no_visible_databases'), key: `conn-${conn.id}-dbs` });
}
shouldMarkConnectionSuccess = true;
} else {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
setLoadedKeys(prev => prev.filter(k => k !== node.key));
@@ -389,6 +396,9 @@ export const useSidebarTreeLoaders = ({
});
} finally {
loadingNodesRef.current.delete(loadKey);
if (shouldMarkConnectionSuccess) {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
}
}
};
@@ -450,6 +460,7 @@ export const useSidebarTreeLoaders = ({
if (loadingNodesRef.current.has(loadKey)) return;
loadingNodesRef.current.add(loadKey);
setConnectionStates(prev => ({ ...prev, [key as string]: 'loading' }));
let shouldMarkDatabaseSuccess = false;
const dbQueries = savedQueries.filter(q => q.connectionId === conn.id && q.dbName === dbName);
const queriesNode: TreeNode = {
@@ -479,8 +490,6 @@ export const useSidebarTreeLoaders = ({
try {
const res = await DBGetTables(buildRpcConnectionConfig(config) as any, conn.dbName);
if (res.success) {
setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));
const tableRows: any[] = Array.isArray(res.data) ? res.data : [];
const tableStatusSql = buildSidebarTableStatusSQL(conn as SavedConnection, conn.dbName);
const tableStatsResult = tableStatusSql
@@ -959,6 +968,7 @@ export const useSidebarTreeLoaders = ({
replaceTreeNodeChildren(key, [queriesNode, ...groupedNodes]);
}
onDatabaseTreeLoaded?.(String(key));
shouldMarkDatabaseSuccess = true;
} else {
setConnectionStates(prev => ({ ...prev, [key as string]: 'error' }));
message.error({ content: res.message, key: `db-${key}-tables` });
@@ -971,6 +981,9 @@ export const useSidebarTreeLoaders = ({
});
} finally {
loadingNodesRef.current.delete(loadKey);
if (shouldMarkDatabaseSuccess) {
setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));
}
}
};