mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 00:42:47 +08:00
🐛 fix(workbench): 最近连接返回资源树
- 点击最近连接改为定位并选中侧栏连接或数据库 - 等待连接数据库加载完成后再恢复目标数据库选择 - 保留 Redis、Nacos 等非查询数据源的最近连接入口 - 外部 SQL 目录仍按原行为创建查询标签页 - 增加最近连接与侧栏定位事件回归测试
This commit is contained in:
@@ -155,12 +155,14 @@ import {
|
||||
resolveSidebarRuntimeDatabase,
|
||||
} from '../utils/sidebarMetadata';
|
||||
import {
|
||||
findSidebarNodePathByKey,
|
||||
findSidebarNodePathForLocate,
|
||||
normalizeSidebarLocateObjectRequest,
|
||||
normalizeSidebarLocateObjectRequestFromTab,
|
||||
resolveSidebarLocateTarget,
|
||||
type SidebarLocateTreeNodeLike,
|
||||
findSidebarNodePathByKey,
|
||||
findSidebarNodePathForLocate,
|
||||
SIDEBAR_LOCATE_CONNECTION_EVENT,
|
||||
normalizeSidebarLocateConnectionRequest,
|
||||
normalizeSidebarLocateObjectRequest,
|
||||
normalizeSidebarLocateObjectRequestFromTab,
|
||||
resolveSidebarLocateTarget,
|
||||
type SidebarLocateTreeNodeLike,
|
||||
} from '../utils/sidebarLocate';
|
||||
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../utils/connectionVisual';
|
||||
import {
|
||||
@@ -2990,10 +2992,53 @@ const Sidebar: React.FC<{
|
||||
expandConnectionFromRailRef.current = (connectionId: string) => {
|
||||
const conn = connections.find((item) => item.id === connectionId);
|
||||
if (conn) {
|
||||
selectConnectionFromRail(conn);
|
||||
void selectConnectionFromRail(conn);
|
||||
}
|
||||
};
|
||||
|
||||
const locateConnectionInSidebar = useCallback(async (detail: unknown) => {
|
||||
const request = normalizeSidebarLocateConnectionRequest(detail);
|
||||
if (!request) return;
|
||||
|
||||
const connection = connections.find((item) => item.id === request.connectionId);
|
||||
if (!connection) return;
|
||||
|
||||
onExpandSidebar?.();
|
||||
setSearchValue('');
|
||||
await selectConnectionFromRail(connection);
|
||||
|
||||
if (!request.dbName) {
|
||||
scrollSidebarTreeToKey(connection.id);
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForSidebarLoadKey(`dbs-${connection.id}`);
|
||||
const databaseNode = findTreeNodeByKeyRef.current(
|
||||
treeDataRef.current,
|
||||
`${connection.id}-${request.dbName}`,
|
||||
);
|
||||
if (!databaseNode) {
|
||||
scrollSidebarTreeToKey(connection.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const dbName = String(databaseNode.dataRef?.dbName || request.dbName).trim();
|
||||
setSelectedKeys([databaseNode.key]);
|
||||
selectedNodesRef.current = [databaseNode];
|
||||
setActiveContext({ connectionId: connection.id, dbName });
|
||||
scrollSidebarTreeToKey(databaseNode.key);
|
||||
}, [connections, findTreeNodeByKeyRef, onExpandSidebar, scrollSidebarTreeToKey, selectConnectionFromRail, selectedNodesRef, setActiveContext, setSearchValue, setSelectedKeys, treeDataRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleLocateSidebarConnection = (event: Event) => {
|
||||
void locateConnectionInSidebar((event as CustomEvent).detail);
|
||||
};
|
||||
window.addEventListener(SIDEBAR_LOCATE_CONNECTION_EVENT, handleLocateSidebarConnection as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener(SIDEBAR_LOCATE_CONNECTION_EVENT, handleLocateSidebarConnection as EventListener);
|
||||
};
|
||||
}, [locateConnectionInSidebar]);
|
||||
|
||||
const getNodeMenuItems = (node: any): MenuProps['items'] => buildSidebarLegacyNodeMenuItems(node, {
|
||||
addTab,
|
||||
getMetadataDialect,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
buildPinnedTableShortcuts,
|
||||
buildRecentConnectionShortcuts,
|
||||
buildRecentSQLFileShortcuts,
|
||||
dispatchRecentConnectionShortcut,
|
||||
RecentConnectionShortcutItem,
|
||||
} from './TabManager';
|
||||
import type { ExternalSQLDirectory, SavedConnection } from '../types';
|
||||
@@ -55,7 +56,7 @@ describe('recent workbench shortcuts', () => {
|
||||
expect(customizedMarkup).not.toContain('anticon-database');
|
||||
});
|
||||
|
||||
it('only offers connections that can open the SQL query editor', () => {
|
||||
it('keeps recent connections even when they do not support the SQL query editor', () => {
|
||||
const shortcuts = buildRecentConnectionShortcuts([
|
||||
connection('redis-1', 'redis'),
|
||||
connection('mysql-1', 'mysql'),
|
||||
@@ -65,6 +66,10 @@ describe('recent workbench shortcuts', () => {
|
||||
]);
|
||||
|
||||
expect(shortcuts).toEqual([
|
||||
expect.objectContaining({
|
||||
connection: expect.objectContaining({ id: 'redis-1' }),
|
||||
dbName: '0',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
connection: expect.objectContaining({ id: 'mysql-1' }),
|
||||
dbName: 'orders',
|
||||
@@ -72,6 +77,20 @@ describe('recent workbench shortcuts', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispatches a sidebar navigation request instead of creating a query tab', () => {
|
||||
const eventTarget = { dispatchEvent: vi.fn() };
|
||||
const mysqlConnection = connection('mysql-1', 'mysql');
|
||||
|
||||
expect(dispatchRecentConnectionShortcut({
|
||||
connection: mysqlConnection,
|
||||
dbName: 'orders',
|
||||
}, eventTarget)).toBe(true);
|
||||
|
||||
const event = eventTarget.dispatchEvent.mock.calls[0]?.[0] as CustomEvent;
|
||||
expect(event.type).toBe('gonavi:locate-sidebar-connection');
|
||||
expect(event.detail).toEqual({ connectionId: 'mysql-1', dbName: 'orders' });
|
||||
});
|
||||
|
||||
it('only exposes valid pinned tables whose connection still exists', () => {
|
||||
const shortcuts = buildPinnedTableShortcuts([
|
||||
connection('mysql-1', 'mysql'),
|
||||
|
||||
@@ -56,6 +56,7 @@ import { createSidebarResizeAwareFrameScheduler } from '../utils/sidebarResizeLi
|
||||
import { QUERY_TAB_RENAME_REQUEST_EVENT } from '../utils/queryTabTitle';
|
||||
import { getDbIcon } from './DatabaseIcons';
|
||||
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../utils/connectionVisual';
|
||||
import { dispatchSidebarLocateConnection } from '../utils/sidebarLocate';
|
||||
|
||||
const getTabKindLabel = (tab: TabData): string => {
|
||||
if (tab.type === 'query') return t('tab_manager.kind_badge.query');
|
||||
@@ -124,6 +125,14 @@ export type RecentConnectionShortcut = {
|
||||
dbName?: string;
|
||||
};
|
||||
|
||||
export const dispatchRecentConnectionShortcut = (
|
||||
shortcut: Pick<RecentConnectionShortcut, 'connection' | 'dbName'>,
|
||||
eventTarget?: Pick<Window, 'dispatchEvent'> | null,
|
||||
): boolean => dispatchSidebarLocateConnection({
|
||||
connectionId: shortcut.connection.id,
|
||||
...(shortcut.dbName ? { dbName: shortcut.dbName } : {}),
|
||||
}, eventTarget);
|
||||
|
||||
export type PinnedTableShortcut = {
|
||||
connection: SavedConnection;
|
||||
dbName: string;
|
||||
@@ -143,10 +152,7 @@ export const buildRecentConnectionShortcuts = (
|
||||
connections: SavedConnection[],
|
||||
recentTargets: RecentConnectionTarget[],
|
||||
): RecentConnectionShortcut[] => {
|
||||
const queryCapableConnections = connections.filter((connection) =>
|
||||
getDataSourceCapabilities(connection.config).supportsQueryEditor,
|
||||
);
|
||||
const connectionById = new Map(queryCapableConnections.map((connection) => [connection.id, connection]));
|
||||
const connectionById = new Map(connections.map((connection) => [connection.id, connection]));
|
||||
const seen = new Set<string>();
|
||||
const seenConnectionIds = new Set<string>();
|
||||
const result: RecentConnectionShortcut[] = [];
|
||||
@@ -166,7 +172,7 @@ export const buildRecentConnectionShortcuts = (
|
||||
append(connection, target.dbName);
|
||||
}
|
||||
});
|
||||
queryCapableConnections.forEach((connection) => {
|
||||
connections.forEach((connection) => {
|
||||
if (!seenConnectionIds.has(connection.id)) {
|
||||
append(connection);
|
||||
}
|
||||
@@ -1461,6 +1467,10 @@ const TabManager: React.FC<TabManagerProps> = React.memo<TabManagerProps>(({ onF
|
||||
};
|
||||
|
||||
const handleOpenRecentConnection = useCallback((shortcut: RecentConnectionShortcut) => {
|
||||
dispatchRecentConnectionShortcut(shortcut);
|
||||
}, []);
|
||||
|
||||
const handleCreateQueryForConnection = useCallback((shortcut: Pick<RecentConnectionShortcut, 'connection' | 'dbName'>) => {
|
||||
addTab({
|
||||
id: buildWorkbenchQueryTabId(),
|
||||
title: t('query.new'),
|
||||
@@ -1722,7 +1732,7 @@ const TabManager: React.FC<TabManagerProps> = React.memo<TabManagerProps>(({ onF
|
||||
key={shortcut.directory.id}
|
||||
type="button"
|
||||
className="gn-v2-empty-recent-item"
|
||||
onClick={() => handleOpenRecentConnection(shortcut)}
|
||||
onClick={() => handleCreateQueryForConnection(shortcut)}
|
||||
>
|
||||
<FolderOpenOutlined />
|
||||
<span>
|
||||
|
||||
@@ -45,7 +45,7 @@ export const useSidebarCommandSearchRunner = ({
|
||||
treeDataRef,
|
||||
v2CommandActiveIndex,
|
||||
}: UseSidebarCommandSearchRunnerArgs) => {
|
||||
const selectConnectionFromRail = useCallback((conn: SavedConnection) => {
|
||||
const selectConnectionFromRail = useCallback((conn: SavedConnection): Promise<void> => {
|
||||
const key = conn.id;
|
||||
const connectionNode = findTreeNodeByKeyRef.current(treeDataRef.current, key);
|
||||
setSelectedKeys([key]);
|
||||
@@ -57,7 +57,7 @@ export const useSidebarCommandSearchRunner = ({
|
||||
dataRef: conn,
|
||||
type: 'connection',
|
||||
};
|
||||
void loadDatabases(targetNode);
|
||||
return loadDatabases(targetNode);
|
||||
}, [findTreeNodeByKeyRef, loadDatabases, mergeExpandedTreeKeys, selectedNodesRef, setActiveContext, setSelectedKeys, treeDataRef]);
|
||||
|
||||
const runCommandSearchItem = useCallback((item?: V2CommandSearchItem) => {
|
||||
@@ -82,7 +82,7 @@ export const useSidebarCommandSearchRunner = ({
|
||||
const node = item.node;
|
||||
const dataRef = node.dataRef || {};
|
||||
if (node.type === 'connection') {
|
||||
selectConnectionFromRail(dataRef as SavedConnection);
|
||||
void selectConnectionFromRail(dataRef as SavedConnection);
|
||||
return;
|
||||
}
|
||||
if (node.type === 'database') {
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
dispatchSidebarLocateConnection,
|
||||
findSidebarNodePathByKey,
|
||||
findSidebarNodePathForLocate,
|
||||
normalizeSidebarLocateConnectionRequest,
|
||||
normalizeSidebarLocateObjectRequest,
|
||||
normalizeSidebarLocateObjectRequestFromTab,
|
||||
resolveSidebarLocateTarget,
|
||||
} from './sidebarLocate';
|
||||
|
||||
describe('sidebarLocate', () => {
|
||||
it('normalizes and dispatches a connection navigation request', () => {
|
||||
expect(normalizeSidebarLocateConnectionRequest({
|
||||
connectionId: ' conn-1 ',
|
||||
dbName: ' orders ',
|
||||
})).toEqual({ connectionId: 'conn-1', dbName: 'orders' });
|
||||
expect(normalizeSidebarLocateConnectionRequest({ connectionId: ' ' })).toBeNull();
|
||||
|
||||
const eventTarget = { dispatchEvent: vi.fn() };
|
||||
expect(dispatchSidebarLocateConnection({
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'orders',
|
||||
}, eventTarget)).toBe(true);
|
||||
const event = eventTarget.dispatchEvent.mock.calls[0]?.[0] as CustomEvent;
|
||||
expect(event.type).toBe('gonavi:locate-sidebar-connection');
|
||||
expect(event.detail).toEqual({ connectionId: 'conn-1', dbName: 'orders' });
|
||||
});
|
||||
|
||||
it('normalizes a table locate request and builds the direct tree path', () => {
|
||||
const request = normalizeSidebarLocateObjectRequest({
|
||||
tabId: 'conn-1-main-users',
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { splitQualifiedNameLast } from './qualifiedName';
|
||||
|
||||
export const SIDEBAR_LOCATE_CONNECTION_EVENT = 'gonavi:locate-sidebar-connection';
|
||||
|
||||
export type SidebarLocateObjectGroup = 'tables' | 'views' | 'materializedViews' | 'triggers' | 'routines' | 'sequences' | 'packages' | 'externalSqlFiles';
|
||||
export type SidebarLocateDatabaseObjectGroup = Exclude<SidebarLocateObjectGroup, 'externalSqlFiles'>;
|
||||
|
||||
export interface SidebarLocateConnectionRequest {
|
||||
connectionId: string;
|
||||
dbName?: string;
|
||||
}
|
||||
|
||||
export interface SidebarLocateDatabaseObjectRequest {
|
||||
tabId?: string;
|
||||
connectionId: string;
|
||||
@@ -70,6 +77,30 @@ const normalizeLocateName = (value: string): string => toTrimmedString(value).to
|
||||
|
||||
const normalizeExternalSQLLocatePath = (value: unknown): string => toTrimmedString(value).replace(/\\/g, '/');
|
||||
|
||||
export const normalizeSidebarLocateConnectionRequest = (detail: unknown): SidebarLocateConnectionRequest | null => {
|
||||
const raw = (detail || {}) as Record<string, unknown>;
|
||||
const connectionId = toTrimmedString(raw.connectionId);
|
||||
if (!connectionId) return null;
|
||||
const dbName = toTrimmedString(raw.dbName);
|
||||
return {
|
||||
connectionId,
|
||||
...(dbName ? { dbName } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const dispatchSidebarLocateConnection = (
|
||||
detail: unknown,
|
||||
eventTarget?: Pick<Window, 'dispatchEvent'> | null,
|
||||
): boolean => {
|
||||
const request = normalizeSidebarLocateConnectionRequest(detail);
|
||||
const target = eventTarget ?? (typeof window === 'undefined' ? null : window);
|
||||
if (!request || !target || typeof CustomEvent !== 'function') return false;
|
||||
target.dispatchEvent(new CustomEvent<SidebarLocateConnectionRequest>(SIDEBAR_LOCATE_CONNECTION_EVENT, {
|
||||
detail: request,
|
||||
}));
|
||||
return true;
|
||||
};
|
||||
|
||||
export const splitSidebarQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
|
||||
const raw = toTrimmedString(qualifiedName);
|
||||
if (!raw) return { schemaName: '', objectName: '' };
|
||||
|
||||
Reference in New Issue
Block a user