🐛 fix(query-editor): 限制快捷键仅执行选中SQL

Fixes #596
This commit is contained in:
Syngnat
2026-06-27 10:36:46 +08:00
parent bf51003c66
commit a24f4a2bc1
2 changed files with 132 additions and 5 deletions

View File

@@ -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> = {}): 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<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,
});
await act(async () => {
create(<QueryEditor tab={createTab({
dbName: 'main',
query: 'select 1;\nselect 2 as two;\nselect 3;',
})} />);
});
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<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,
});
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['two'], rows: [{ two: 2 }] }],
});
await act(async () => {
create(<QueryEditor tab={createTab({
dbName: 'main',
query: 'select 1;\nselect 2 as two;\nselect 3;',
})} />);
});
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';

View File

@@ -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(() => {