diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx index 1b25d90..5009a20 100644 --- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx +++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx @@ -537,6 +537,20 @@ const getLastInjectedPrompt = (): string => { return event?.detail?.prompt; }; +const createRunShortcutEvent = () => { + const isMacRuntime = /(Mac|iPhone|iPad|iPod)/i.test(`${navigator.platform || ''} ${navigator.userAgent || ''}`); + return { + ctrlKey: !isMacRuntime, + metaKey: isMacRuntime, + altKey: false, + shiftKey: false, + key: 'Enter', + target: null, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + }; +}; + const createTab = (overrides: Partial = {}): TabData => ({ id: 'tab-1', title: 'query.sql', @@ -1610,6 +1624,103 @@ describe('QueryEditor external SQL save', () => { expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3'); }); + it('does not run SQL from the run shortcut when nothing is selected', async () => { + storeState.shortcutOptions.runQuery.mac = { enabled: true, combo: 'Meta+Enter' }; + storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+Enter' }; + 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, + }); + + await act(async () => { + create(); + }); + editorState.position = { lineNumber: 2, column: 8 }; + editorState.selection = null; + backendApp.DBQueryMulti.mockClear(); + + const event = createRunShortcutEvent(); + await act(async () => { + windowListeners.keydown?.forEach((listener) => listener(event)); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(backendApp.DBQueryMulti).not.toHaveBeenCalled(); + expect(messageApi.info).toHaveBeenCalledWith('没有可选择的 SQL 语句。'); + }); + + it('runs selected SQL from the run shortcut', async () => { + storeState.shortcutOptions.runQuery.mac = { enabled: true, combo: 'Meta+Enter' }; + storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+Enter' }; + 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, + }); + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ columns: ['two'], rows: [{ two: 2 }] }], + }); + + await act(async () => { + create(); + }); + editorState.position = { lineNumber: 1, column: 4 }; + editorState.selection = { + startLineNumber: 2, + startColumn: 1, + endLineNumber: 2, + endColumn: 'select 2 as two'.length + 1, + }; + + const event = createRunShortcutEvent(); + await act(async () => { + windowListeners.keydown?.forEach((listener) => listener(event)); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as two'), 'query-1'); + expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 1'); + expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3'); + }); + it('renders V2 empty state copy for the active non-Chinese language', async () => { storeState.appearance.uiVersion = 'v2'; storeState.languagePreference = 'en-US'; diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index aa79321..0553521 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -2573,7 +2573,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc label: buildQueryEditorMonacoActionLabel('app.shortcuts.action.runQuery.label'), keybindings: [keyBinding.keyMod | keyBinding.keyCode], run: () => { - window.dispatchEvent(new CustomEvent('gonavi:run-active-query')); + window.dispatchEvent(new CustomEvent('gonavi:run-active-query', { + detail: { requireSelection: true }, + })); }, }); } @@ -4629,6 +4631,14 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc } }; + const handleRunSelectedShortcut = async () => { + if (!getSelectedSQL().trim()) { + message.info(translate('query_editor.message.no_selectable_sql')); + return; + } + await handleRun(); + }; + const handleCancel = async () => { if (!currentQueryIdRef.current) { message.warning(translate('query_editor.message.cancel_no_running')); @@ -4710,7 +4720,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc } event.preventDefault(); event.stopPropagation(); - void handleRun(); + void handleRunSelectedShortcut(); }; window.addEventListener('keydown', handleRunShortcut, true); @@ -4758,7 +4768,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc label: buildQueryEditorMonacoActionLabel('app.shortcuts.action.runQuery.label'), keybindings: [keyBinding.keyMod | keyBinding.keyCode], run: () => { - window.dispatchEvent(new CustomEvent('gonavi:run-active-query')); + window.dispatchEvent(new CustomEvent('gonavi:run-active-query', { + detail: { requireSelection: true }, + })); }, }); } @@ -4882,10 +4894,14 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }, [languagePreference, toggleQueryResultsPanelShortcutBinding, toggleResultPanelVisibility]); useEffect(() => { - const handleRunActiveQuery = () => { + const handleRunActiveQuery = (event: Event) => { if (!isActive) { return; } + if ((event as CustomEvent<{ requireSelection?: boolean }>).detail?.requireSelection) { + void handleRunSelectedShortcut(); + return; + } void handleRun(); }; @@ -4893,7 +4909,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc return () => { window.removeEventListener('gonavi:run-active-query', handleRunActiveQuery as EventListener); }; - }, [isActive, handleRun]); + }, [isActive, handleRun, handleRunSelectedShortcut]); // 监听由 TabManager 分发的专用注入事件 useEffect(() => {