diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 0867151c..d568a3dd 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -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 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(); + }); + + 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; diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 8d10b14b..061af512 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -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( diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 03d8e977..8640da45 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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; } };