🐛 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, resolveSidebarDropTargetMetricsFromDomEvent,
resolveSidebarDropInsertBefore, resolveSidebarDropInsertBefore,
resolveSidebarNodeConnectionId, resolveSidebarNodeConnectionId,
resolveSidebarSwitcherLoadKey,
resolveV2ActiveConnectionId, resolveV2ActiveConnectionId,
resolveV2ObjectGroupTitle, resolveV2ObjectGroupTitle,
isSidebarTablePinned, isSidebarTablePinned,
SQLFileExecutionProgressContent, SQLFileExecutionProgressContent,
resolveSidebarTableNameForCopy, resolveSidebarTableNameForCopy,
shouldKeepSidebarSwitcherCollapsedWhileLoading,
shouldClearSidebarActiveContextOnEmptySelect, shouldClearSidebarActiveContextOnEmptySelect,
shouldSkipSidebarLoadOnExpandWhileDragging, shouldSkipSidebarLoadOnExpandWhileDragging,
shouldSkipSidebarSelectWhileDragging, shouldSkipSidebarSelectWhileDragging,
@@ -319,6 +321,45 @@ describe('Sidebar locate toolbar', () => {
expect(shouldLoadSidebarNodeOnExpand({ type: 'object-group', children: [] })).toBe(false); 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', () => { it('wires tree expand and double-click expansion to lazy loading', () => {
const source = readSidebarSource(); const source = readSidebarSource();
@@ -328,6 +369,9 @@ describe('Sidebar locate toolbar', () => {
expect(source).toContain('clearTreeNodeChildrenByKeys(keysToClear);'); expect(source).toContain('clearTreeNodeChildrenByKeys(keysToClear);');
expect(source).toContain('if (!shouldSkipSidebarLoadOnExpandWhileDragging(isTreeDragging, info))'); expect(source).toContain('if (!shouldSkipSidebarLoadOnExpandWhileDragging(isTreeDragging, info))');
expect(source).toContain('if (shouldLoadSidebarNodeOnExpand(node))'); 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', () => { 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(source).toContain("export type SidebarConnectionState = 'loading' | 'success' | 'error';");
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));"); expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));");
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [key as string]: '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("let status: 'loading' | 'success' | 'error' | 'default' = 'default';");
expect(titleRenderSource).toContain("if (connectionStates[node.key] === 'loading') status = 'loading';"); expect(titleRenderSource).toContain("if (connectionStates[node.key] === 'loading') status = 'loading';");
expect(v2ContextMenuSource).toContain("const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();"); 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 { createPortal } from 'react-dom';
import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Popover, Tooltip } from 'antd'; import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Popover, Tooltip } from 'antd';
import { import {
CaretDownFilled,
DatabaseOutlined, DatabaseOutlined,
TableOutlined, TableOutlined,
ConsoleSqlOutlined, ConsoleSqlOutlined,
@@ -209,6 +210,55 @@ export {
}; };
export type { V2CommandSearchItem, V2RailConnectionGroup } from './sidebarV2Utils'; 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 { Search } = Input;
const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50; const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50;
const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160; 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) => { const buildRuntimeConfig = (conn: any, overrideDatabase?: string, clearDatabase: boolean = false) => {
@@ -2949,6 +3007,7 @@ const Sidebar: React.FC<{
onDoubleClick={onDoubleClick} onDoubleClick={onDoubleClick}
onSelect={onSelect} onSelect={onSelect}
titleRender={titleRender} titleRender={titleRender}
switcherIcon={renderSidebarSwitcherIcon}
expandedKeys={expandedKeys} expandedKeys={expandedKeys}
onExpand={onExpand} onExpand={onExpand}
loadedKeys={loadedKeys} loadedKeys={loadedKeys}

View File

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