From d7632e29a658d1bc9d2542c8c9245da29f989d50 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 14 Jun 2026 18:49:22 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BA=8B=E5=8A=A1=E5=B7=A5=E5=85=B7=E6=A0=8F=E6=8E=92?= =?UTF-8?q?=E7=89=88=E5=B9=B6=E4=BF=AE=E5=A4=8D=20Wails=20=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E9=A1=B5=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将手动事务提交回滚按钮并入主工具栏,移除重复入口和未提交文案 - 调整 v2 查询工具栏顺序,收起结果区入口并统一更多、AI、设置交互 - 修正 Wails dev 地址为 127.0.0.1,避免 index.html not found --- .../QueryEditor.external-sql-save.test.tsx | 59 +++- frontend/src/components/QueryEditor.tsx | 1 - .../components/QueryEditorResultsPanel.tsx | 7 +- .../src/components/QueryEditorToolbar.tsx | 272 +++++++++++++----- .../QueryEditorTransactionToolbar.tsx | 10 +- frontend/src/v2-theme.css | 23 +- wails.json | 2 +- 7 files changed, 275 insertions(+), 99 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 080276af..99963ff9 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -417,6 +417,9 @@ const textContent = (node: any): string => const findButton = (renderer: ReactTestRenderer, text: string) => renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0]; +const findButtons = (renderer: ReactTestRenderer, text: string) => + renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text)); + const findExactButton = (renderer: ReactTestRenderer, text: string) => renderer.root.findAll((node) => node.type === 'button' && textContent(node) === text)[0]; @@ -2448,7 +2451,8 @@ describe('QueryEditor external SQL save', () => { 'query-1', ); expect(backendApp.DBQueryMulti).not.toHaveBeenCalled(); - expect(textContent(renderer!.root)).toContain('未提交'); + expect(textContent(renderer!.root)).not.toContain('未提交'); + expect(textContent(renderer!.root)).toContain('提交'); expect(textContent(renderer!.root)).toContain('影响行数:2'); await act(async () => { @@ -2494,7 +2498,8 @@ describe('QueryEditor external SQL save', () => { 'query-1', ); expect(backendApp.DBQueryMulti).not.toHaveBeenCalled(); - expect(textContent(renderer!.root)).toContain('未提交'); + expect(textContent(renderer!.root)).not.toContain('未提交'); + expect(textContent(renderer!.root)).toContain('提交'); await act(async () => { await findButton(renderer!, '提交').props.onClick(); @@ -2544,7 +2549,7 @@ describe('QueryEditor external SQL save', () => { expect.stringContaining('DELETE FROM users'), 'query-1', ); - expect(textContent(renderer!.root)).toContain('未提交'); + expect(textContent(renderer!.root)).not.toContain('未提交'); expect(textContent(renderer!.root)).toContain('提交 (2)'); expect(storeState.sqlEditorPendingTransactions['tab-1']).toMatchObject({ id: 'tx-multi-dml', @@ -2583,6 +2588,35 @@ describe('QueryEditor external SQL save', () => { expect(backendApp.DBQueryMultiTransactional).not.toHaveBeenCalled(); }); + it('keeps manual SQL transaction actions inline in the top toolbar without duplicating them in result tabs', async () => { + backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({ + success: true, + transactionId: 'tx-toolbar-inline', + transactionPending: true, + data: [ + { columns: ['affectedRows'], rows: [{ affectedRows: 1 }], statementIndex: 1 }, + ], + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + await findButton(renderer!, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const pageText = textContent(renderer!.root); + expect(pageText).not.toContain('未提交'); + expect(findButtons(renderer!, '提交')).toHaveLength(1); + expect(findButtons(renderer!, '回滚')).toHaveLength(1); + }); + it('adds pagination to limited query results and reloads the selected page only', async () => { const firstPageRows = Array.from({ length: 500 }, (_item, index) => ({ id: index + 1 })); const secondPageRows = Array.from({ length: 500 }, (_item, index) => ({ id: index + 501 })); @@ -2673,7 +2707,7 @@ describe('QueryEditor external SQL save', () => { 'query-1', ); expect(backendApp.DBQueryMulti).not.toHaveBeenCalled(); - expect(textContent(renderer!.root)).toContain('未提交'); + expect(textContent(renderer!.root)).not.toContain('未提交'); }); it('auto commits SQL editor DML transactions after the configured delay', async () => { @@ -3980,6 +4014,7 @@ describe('QueryEditor external SQL save', () => { it('keeps the v2 query editor toolbar grouped and compact', () => { const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8'); const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8'); + const resultsPanelSource = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8'); const transactionSettingsSource = readFileSync(new URL('./QueryEditorTransactionSettings.tsx', import.meta.url), 'utf8'); const transactionToolbarSource = readFileSync(new URL('./QueryEditorTransactionToolbar.tsx', import.meta.url), 'utf8'); const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8'); @@ -4002,12 +4037,23 @@ describe('QueryEditor external SQL save', () => { expect(transactionSettingsSource).toContain("label: '3s'"); expect(source).toContain('QueryEditorTransactionToolbar'); expect(transactionToolbarSource).toContain("className={isV2Ui ? 'gn-v2-query-transaction-toolbar' : undefined}"); - expect(transactionToolbarSource).toContain("'未提交'"); + expect(transactionToolbarSource).toContain(": null;"); expect(transactionToolbarSource).toContain('gn-v2-query-transaction-commit-button'); expect(transactionToolbarSource).toContain('gn-v2-toolbar-kbd'); expect(transactionToolbarSource).toContain("'自动提交中'"); expect(transactionToolbarSource).toContain('onFinish'); + expect(toolbarSource).toContain('{isV2Ui && pendingTransactionToolbar}'); + expect(toolbarSource).not.toContain('gn-v2-query-toolbar-transaction-row'); + expect(resultsPanelSource).not.toContain('transactionToolbar?: React.ReactNode;'); expect(toolbarSource).toContain('gn-v2-query-toolbar-action-group'); + expect(toolbarSource).toContain('gn-v2-query-toolbar-action-pair'); + expect(toolbarSource).toContain('const aiMenuItems'); + expect(toolbarSource).toContain('key: "toggle-result-panel"'); + expect(toolbarSource).toContain('{!isV2Ui && ('); + expect(toolbarSource).toContain('trigger={["click"]}'); + expect(toolbarSource.indexOf('onClick={onQuickSave}')).toBeLessThan(toolbarSource.indexOf('menu={{ items: aiMenuItems }}')); + expect(toolbarSource.indexOf('menu={{ items: aiMenuItems }}')).toBeLessThan(toolbarSource.indexOf('menu={{ items: moreMenuItems }}')); + expect(toolbarSource.indexOf('menu={{ items: moreMenuItems }}')).toBeLessThan(toolbarSource.indexOf('icon={}')); expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 78 }}'); expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 68 }}'); expect(toolbarSource).toContain('style={isV2Ui ? undefined : { width: 200 }}'); @@ -4023,6 +4069,8 @@ describe('QueryEditor external SQL save', () => { expect(css).toContain('line-height: 30px !important;'); expect(css).toContain('display: inline-flex !important;'); expect(css).toContain('gap: 6px;'); + expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-action-pair'); + expect(css).toContain('gap: 8px;'); expect(css).toContain('margin-left: 0 !important;'); expect(css).toContain('max-width: 760px;'); expect(css).toContain('width: 140px !important;'); @@ -4030,6 +4078,7 @@ describe('QueryEditor external SQL save', () => { expect(css).toContain('width: 132px !important;'); expect(css).toContain('width: 34px !important;'); expect(css).toContain('@media (max-width: 900px)'); + expect(css).not.toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-row {'); const queryToolbarMainCss = css.slice(css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar-main {'), css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar-selects {')); expect(queryToolbarMainCss).not.toContain('margin-left: auto;'); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 528233e5..73856966 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -5279,7 +5279,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc currentDb={currentDb} currentConnectionId={currentConnectionId} toggleShortcutLabel={toggleQueryResultsPanelShortcutLabel} - transactionToolbar={sqlEditorTransactionToolbar} onActiveResultKeyChange={setActiveResultKey} onHide={() => updateResultPanelVisibility(false)} onCloseResult={handleCloseResult} diff --git a/frontend/src/components/QueryEditorResultsPanel.tsx b/frontend/src/components/QueryEditorResultsPanel.tsx index 45e151ec..914c5ded 100644 --- a/frontend/src/components/QueryEditorResultsPanel.tsx +++ b/frontend/src/components/QueryEditorResultsPanel.tsx @@ -35,7 +35,6 @@ interface QueryEditorResultsPanelProps { currentDb: string; currentConnectionId: string; toggleShortcutLabel: string; - transactionToolbar?: React.ReactNode; onActiveResultKeyChange: (key: string) => void; onHide: () => void; onCloseResult: (key: string) => void; @@ -61,7 +60,6 @@ const QueryEditorResultsPanel: React.FC = ({ currentDb, currentConnectionId, toggleShortcutLabel, - transactionToolbar, onActiveResultKeyChange, onHide, onCloseResult, @@ -138,12 +136,11 @@ const QueryEditorResultsPanel: React.FC = ({ /> ); - const tabsExtraContent = transactionToolbar || !activeResultUsesDataGrid + const tabsExtraContent = !activeResultUsesDataGrid ? { right: (
- {transactionToolbar} - {!activeResultUsesDataGrid ? tabsHideButton : null} + {tabsHideButton}
), } diff --git a/frontend/src/components/QueryEditorToolbar.tsx b/frontend/src/components/QueryEditorToolbar.tsx index 723c5d37..d910d56b 100644 --- a/frontend/src/components/QueryEditorToolbar.tsx +++ b/frontend/src/components/QueryEditorToolbar.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Button, Dropdown, Select, Space, Tooltip, type MenuProps } from 'antd'; +import React from "react"; +import { Button, Dropdown, Select, Space, Tooltip, type MenuProps } from "antd"; import { EyeInvisibleOutlined, EyeOutlined, @@ -9,11 +9,17 @@ import { SaveOutlined, SettingOutlined, StopOutlined, -} from '@ant-design/icons'; +} from "@ant-design/icons"; -import type { SavedConnection } from '../types'; -import { getShortcutDisplayLabel, type ShortcutPlatform, type ShortcutPlatformBinding } from '../utils/shortcuts'; -import QueryEditorTransactionSettings, { type SqlEditorCommitMode } from './QueryEditorTransactionSettings'; +import type { SavedConnection } from "../types"; +import { + getShortcutDisplayLabel, + type ShortcutPlatform, + type ShortcutPlatformBinding, +} from "../utils/shortcuts"; +import QueryEditorTransactionSettings, { + type SqlEditorCommitMode, +} from "./QueryEditorTransactionSettings"; type QueryEditorToolbarProps = { isV2Ui: boolean; @@ -31,8 +37,8 @@ type QueryEditorToolbarProps = { activeShortcutPlatform: ShortcutPlatform; isResultPanelVisible: boolean; loading: boolean; - saveMoreMenuItems: MenuProps['items']; - formatSettingsMenu: MenuProps['items']; + saveMoreMenuItems: MenuProps["items"]; + formatSettingsMenu: MenuProps["items"]; onConnectionChange: (connectionId: string) => void; onDatabaseChange: (dbName: string) => void; onMaxRowsChange: (maxRows: number) => void; @@ -44,7 +50,7 @@ type QueryEditorToolbarProps = { onQuickSave: () => void; onFormat: () => void; onToggleResultPanelVisibility: () => void; - onAIAction: (action: 'generate' | 'explain' | 'optimize' | 'schema') => void; + onAIAction: (action: "generate" | "explain" | "optimize" | "schema") => void; }; const QueryEditorToolbar: React.FC = ({ @@ -78,41 +84,112 @@ const QueryEditorToolbar: React.FC = ({ onToggleResultPanelVisibility, onAIAction, }) => { + const baseMoreMenuItems = saveMoreMenuItems ?? []; + const toggleResultPanelTitle = + toggleQueryResultsPanelShortcutBinding.enabled && + toggleQueryResultsPanelShortcutBinding.combo + ? `${isResultPanelVisible ? "隐藏结果区" : "显示结果区"}(${getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform)})` + : isResultPanelVisible + ? "隐藏结果区" + : "显示结果区"; + const aiMenuItems: MenuProps["items"] = [ + { + key: "ai-generate", + label: "生成 SQL", + icon: , + onClick: () => onAIAction("generate"), + }, + { + key: "ai-explain", + label: "解释 SQL", + icon: , + onClick: () => onAIAction("explain"), + }, + { + key: "ai-optimize", + label: "优化 SQL", + icon: , + onClick: () => onAIAction("optimize"), + }, + { type: "divider" as const }, + { + key: "ai-schema", + label: "Schema 分析", + icon: , + onClick: () => onAIAction("schema"), + }, + ]; + const moreMenuItems: MenuProps["items"] = isV2Ui + ? [ + ...baseMoreMenuItems, + ...(baseMoreMenuItems.length > 0 ? [{ type: "divider" as const }] : []), + { + key: "toggle-result-panel", + label: toggleResultPanelTitle, + icon: isResultPanelVisible ? ( + + ) : ( + + ), + onClick: onToggleResultPanelVisibility, + }, + ] + : baseMoreMenuItems; const selects = (
({ label: db, value: db }))} + options={dbList.map((db) => ({ label: db, value: db }))} showSearch />