🐛 fix(sidebar): 单击打开数据库对象节点

- 支持视图、函数、触发器等对象单击直接打开定义 tab
- 覆盖查询日志打开后 CK/SQLite 查询结果仍切回数据结果

Fixes #595
This commit is contained in:
Syngnat
2026-06-27 10:51:50 +08:00
parent 2d73bcc6de
commit 23d7511f55
3 changed files with 154 additions and 0 deletions

View File

@@ -1121,6 +1121,78 @@ describe('QueryEditor external SQL save', () => {
renderer.unmount();
});
it.each(['sqlite', 'clickhouse'])(
'activates the data result tab for %s after the sql log tab was open',
async (dbType) => {
storeState.appearance.uiVersion = 'v2';
storeState.connections[0].config.type = dbType;
storeState.sqlLogs = [{
id: 'log-1',
timestamp: Date.now(),
sql: 'select old',
status: 'success',
duration: 12,
}];
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'id', key: 'PRI' }],
});
backendApp.DBGetIndexes.mockResolvedValue({ success: true, data: [] });
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{
columns: ['id', 'name'],
rows: [{ id: 1, name: 'alpha' }],
statementIndex: 1,
}],
});
const windowListeners: Record<string, ((event?: any) => void)[]> = {};
vi.stubGlobal('window', {
addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
windowListeners[type] ||= [];
windowListeners[type].push(listener);
}),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
requestAnimationFrame: vi.fn((callback: FrameRequestCallback) => {
callback(0);
return 1;
}),
cancelAnimationFrame: vi.fn(),
innerHeight: 900,
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({
query: 'SELECT * FROM users',
})} />);
});
const openEvent = new CustomEvent('gonavi:show-sql-execution-log', { detail: { mode: 'open' } });
await act(async () => {
windowListeners['gonavi:show-sql-execution-log']?.forEach((listener) => listener(openEvent));
});
expect(textContent(renderer.toJSON())).toContain('SQL 执行日志');
dataGridState.latestProps = null;
await act(async () => {
await findButton(renderer, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(textContent(renderer.toJSON())).toContain('结果 1');
expect(dataGridState.latestProps?.columnNames).toEqual(['id', 'name']);
expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ id: 1, name: 'alpha' });
renderer.unmount();
},
);
it('keeps query result panel visibility isolated per tab', async () => {
storeState.appearance.uiVersion = 'v2';
storeState.queryOptions.showQueryResultsPanel = false;

View File

@@ -665,6 +665,26 @@ describe('Sidebar locate toolbar', () => {
expect(commandSearchRunSource).toContain("tabId: String(node.key || '')");
});
it('opens view routine and trigger nodes from single-click selection', () => {
const source = readSidebarSource();
const openObjectSource = source.slice(
source.indexOf('const openSidebarObjectNode ='),
source.indexOf('const onSelect ='),
);
const onSelectSource = source.slice(
source.indexOf('const onSelect ='),
source.indexOf('const onExpand ='),
);
expect(openObjectSource).toContain("node.type === 'view' || node.type === 'materialized-view'");
expect(openObjectSource).toContain("node.type === 'db-trigger'");
expect(openObjectSource).toContain("node.type === 'routine'");
expect(openObjectSource).toContain("type: 'table'");
expect(openObjectSource).toContain("type: 'trigger'");
expect(openObjectSource).toContain("type: 'routine-def'");
expect(onSelectSource).toContain('openSidebarObjectNode(info.node)');
});
it('wires external SQL directory file actions to dedicated Wails APIs', () => {
const source = readSidebarSource();
const loadTablesSource = source.slice(

View File

@@ -1451,6 +1451,66 @@ const Sidebar: React.FC<{
});
};
const openSidebarObjectNode = (node: any): boolean => {
if (node.type === 'view' || node.type === 'materialized-view') {
const { viewName, dbName, id, schemaName } = node.dataRef;
addTab({
id: node.key,
title: viewName,
type: 'table',
connectionId: id,
dbName,
tableName: viewName,
objectType: node.type === 'materialized-view' ? 'materialized-view' : 'view',
schemaName,
sidebarLocateKey: String(node.key || ''),
});
return true;
}
if (node.type === 'db-trigger') {
const { triggerName, triggerTableName, schemaName, dbName, id } = node.dataRef;
addTab({
id: `trigger-${node.key}`,
title: t('sidebar.tab.trigger', { name: triggerName }),
type: 'trigger',
connectionId: id,
dbName,
triggerName,
triggerTableName,
schemaName,
sidebarLocateKey: String(node.key || ''),
});
return true;
}
if (node.type === 'db-event') {
openEventDefinition(node);
return true;
}
if (node.type === 'routine') {
const { routineName, routineType, dbName, id } = node.dataRef;
const typeLabel = t(routineType === 'PROCEDURE' ? 'sidebar.object.procedure' : 'sidebar.object.function');
addTab({
id: `routine-def-${node.key}`,
title: t('sidebar.tab.routine_definition', { type: typeLabel, name: routineName }),
type: 'routine-def',
connectionId: id,
dbName,
routineName,
routineType
});
return true;
}
if (node.type === 'sequence') {
openSequenceDefinition(node);
return true;
}
if (node.type === 'package') {
openPackageDefinition(node);
return true;
}
return false;
};
const onSelect = (keys: React.Key[], info: any) => {
if (isV2Ui && info?.node?.type === 'v2-table-section') {
return;
@@ -1514,6 +1574,8 @@ const Sidebar: React.FC<{
schemaName,
} as any);
}, 250);
} else if (openSidebarObjectNode(info.node)) {
return;
}
};