mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-12 16:02:48 +08:00
✨ feat(query-editor): 新增编辑器内搜索入口
- 在查询编辑器工具栏新增搜索入口并直连 Monaco 查找\n- 接管 Ctrl/Cmd+F,优先打开编辑器内搜索而不是浏览器默认查找\n- 补充多语言文案与静态测试覆盖\n\nFixes #539
This commit is contained in:
@@ -35,6 +35,12 @@ describe('QueryEditor i18n source guards', () => {
|
||||
expect(queryEditorSource).not.toContain('翻页未返回结果集');
|
||||
});
|
||||
|
||||
it('routes editor search through a localized Monaco action', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.action.find_in_editor');
|
||||
expect(queryEditorSource).toContain('gonavi:find-active-query');
|
||||
expect(queryEditorSource).toContain("editor.getAction?.('actions.find')");
|
||||
});
|
||||
|
||||
it('uses a localized wrapper for save query failures', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.message.save_query_failed');
|
||||
expect(queryEditorSource).not.toContain('保存查询失败: ');
|
||||
|
||||
@@ -731,6 +731,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const runQueryActionRef = useRef<any>(null);
|
||||
const selectCurrentStatementActionRef = useRef<any>(null);
|
||||
const saveQueryActionRef = useRef<any>(null);
|
||||
const findInEditorActionRef = useRef<any>(null);
|
||||
const formatSqlActionRef = useRef<any>(null);
|
||||
const aiContextMenuActionDisposablesRef = useRef<any[]>([]);
|
||||
const toggleQueryResultsPanelActionRef = useRef<any>(null);
|
||||
@@ -880,6 +881,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
() => resolveShortcutBinding(shortcutOptions, 'toggleQueryResultsPanel', activeShortcutPlatform),
|
||||
[activeShortcutPlatform, shortcutOptions],
|
||||
);
|
||||
const findInEditorShortcutCombo = useMemo(
|
||||
() => activeShortcutPlatform === 'mac' ? 'Meta+F' : 'Ctrl+F',
|
||||
[activeShortcutPlatform],
|
||||
);
|
||||
const primaryShortcutModifierLabel = useMemo(
|
||||
() => getShortcutPrimaryModifierDisplayLabel(activeShortcutPlatform),
|
||||
[activeShortcutPlatform],
|
||||
@@ -898,6 +903,25 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return nextVisible;
|
||||
});
|
||||
}, [tab.id, updateQueryTabDraft]);
|
||||
const handleOpenEditorFind = useCallback(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.focus?.();
|
||||
try {
|
||||
const findAction = editor.getAction?.('actions.find');
|
||||
if (findAction?.run) {
|
||||
void findAction.run();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to Monaco's built-in command id if the action lookup fails.
|
||||
}
|
||||
|
||||
editor.trigger?.('keyboard', 'actions.find', null);
|
||||
}, []);
|
||||
const handleShowSqlExecutionLog = useCallback((mode: 'open' | 'toggle' = 'toggle') => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
@@ -2732,6 +2756,20 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
}
|
||||
|
||||
const findInEditorKeyBinding = comboToMonacoKeyBinding(
|
||||
findInEditorShortcutCombo, monaco.KeyMod, monaco.KeyCode
|
||||
);
|
||||
if (findInEditorKeyBinding) {
|
||||
findInEditorActionRef.current = editor.addAction({
|
||||
id: 'gonavi.findInEditor',
|
||||
label: buildQueryEditorMonacoActionLabel('query_editor.action.find_in_editor'),
|
||||
keybindings: [findInEditorKeyBinding.keyMod | findInEditorKeyBinding.keyCode],
|
||||
run: () => {
|
||||
window.dispatchEvent(new CustomEvent('gonavi:find-active-query'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const formatBinding = formatSqlShortcutBinding;
|
||||
if (formatBinding?.enabled && formatBinding.combo) {
|
||||
const keyBinding = comboToMonacoKeyBinding(
|
||||
@@ -4998,6 +5036,40 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
};
|
||||
}, [languagePreference, saveQueryShortcutBinding]);
|
||||
|
||||
useEffect(() => {
|
||||
if (findInEditorActionRef.current) {
|
||||
findInEditorActionRef.current.dispose();
|
||||
findInEditorActionRef.current = null;
|
||||
}
|
||||
|
||||
const editor = editorRef.current;
|
||||
const monaco = monacoRef.current;
|
||||
if (!editor || !monaco) return;
|
||||
|
||||
const keyBinding = comboToMonacoKeyBinding(
|
||||
findInEditorShortcutCombo,
|
||||
monaco.KeyMod,
|
||||
monaco.KeyCode,
|
||||
);
|
||||
if (keyBinding) {
|
||||
findInEditorActionRef.current = editor.addAction({
|
||||
id: 'gonavi.findInEditor',
|
||||
label: buildQueryEditorMonacoActionLabel('query_editor.action.find_in_editor'),
|
||||
keybindings: [keyBinding.keyMod | keyBinding.keyCode],
|
||||
run: () => {
|
||||
window.dispatchEvent(new CustomEvent('gonavi:find-active-query'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (findInEditorActionRef.current) {
|
||||
findInEditorActionRef.current.dispose();
|
||||
findInEditorActionRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [findInEditorShortcutCombo, languagePreference]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formatSqlActionRef.current) {
|
||||
formatSqlActionRef.current.dispose();
|
||||
@@ -5095,6 +5167,20 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
};
|
||||
}, [isActive, handleRun, handleRunSelectedShortcut]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFindActiveQuery = () => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
handleOpenEditorFind();
|
||||
};
|
||||
|
||||
window.addEventListener('gonavi:find-active-query', handleFindActiveQuery as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('gonavi:find-active-query', handleFindActiveQuery as EventListener);
|
||||
};
|
||||
}, [handleOpenEditorFind, isActive]);
|
||||
|
||||
// 监听由 TabManager 分发的专用注入事件
|
||||
useEffect(() => {
|
||||
const handleInsertSql = (e: any) => {
|
||||
@@ -5339,6 +5425,38 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleFindShortcut = (event: KeyboardEvent) => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
if (!isShortcutMatch(event, findInEditorShortcutCombo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = editorRef.current;
|
||||
const targetNode = resolveEventTargetNode(event.target);
|
||||
const editorHasFocus = !!editor?.hasTextFocus?.();
|
||||
const inEditorPane = !!(targetNode && editorPaneRef.current?.contains(targetNode));
|
||||
const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode));
|
||||
if (isEditableElement(event.target) && !inEditorPane) {
|
||||
return;
|
||||
}
|
||||
if (!editorHasFocus && !inEditorPane && !inQueryEditor && !isDocumentLevelShortcutTarget(targetNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleOpenEditorFind();
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleFindShortcut, true);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleFindShortcut, true);
|
||||
};
|
||||
}, [findInEditorShortcutCombo, handleOpenEditorFind, isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
const binding = saveQueryShortcutBinding;
|
||||
if (!binding?.enabled || !binding.combo) {
|
||||
@@ -5605,6 +5723,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
onRun={handleRun}
|
||||
onCancel={handleCancel}
|
||||
onQuickSave={handleQuickSave}
|
||||
onFindInEditor={handleOpenEditorFind}
|
||||
onFormat={handleFormat}
|
||||
onToggleResultPanelVisibility={toggleResultPanelVisibility}
|
||||
onAIAction={handleAIAction}
|
||||
|
||||
@@ -42,6 +42,8 @@ const requiredKeys = [
|
||||
'query_editor.action.save',
|
||||
'query_editor.action.save_with_shortcut',
|
||||
'query_editor.action.more',
|
||||
'query_editor.action.find_in_editor',
|
||||
'query_editor.action.find_in_editor_with_shortcut',
|
||||
'query_editor.action.format',
|
||||
'query_editor.action.format_sql',
|
||||
'query_editor.action.format_sql_with_shortcut',
|
||||
|
||||
@@ -80,6 +80,19 @@ describe('QueryEditorToolbar layout', () => {
|
||||
expect(transactionDelayCss).toContain('flex: 0 0 104px !important;');
|
||||
});
|
||||
|
||||
it('keeps editor search action grouped with formatting actions', () => {
|
||||
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
|
||||
const formatActionPairStart = toolbarSource.indexOf('SearchOutlined');
|
||||
const formatActionPairEnd = toolbarSource.indexOf('!isV2Ui && (');
|
||||
const formatActionPairSource = toolbarSource.slice(formatActionPairStart, formatActionPairEnd);
|
||||
|
||||
expect(toolbarSource).toContain('SearchOutlined');
|
||||
expect(formatActionPairSource).toContain('query_editor.action.find_in_editor');
|
||||
expect(formatActionPairSource).toContain('query_editor.action.find_in_editor_with_shortcut');
|
||||
expect(formatActionPairSource).toContain('onClick={onFindInEditor}');
|
||||
expect(formatActionPairSource).toContain('FormatPainterOutlined');
|
||||
});
|
||||
|
||||
it('shows delayed full-name tooltips for truncated connection and database selectors', () => {
|
||||
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
|
||||
const css = readV2ThemeCss();
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FormatPainterOutlined,
|
||||
PlayCircleOutlined,
|
||||
RobotOutlined,
|
||||
SearchOutlined,
|
||||
SaveOutlined,
|
||||
SettingOutlined,
|
||||
StopOutlined,
|
||||
@@ -51,6 +52,7 @@ type QueryEditorToolbarProps = {
|
||||
onRun: () => void;
|
||||
onCancel: () => void;
|
||||
onQuickSave: () => void;
|
||||
onFindInEditor: () => void;
|
||||
onFormat: () => void;
|
||||
onToggleResultPanelVisibility: () => void;
|
||||
onAIAction: (action: "generate" | "explain" | "optimize" | "schema") => void;
|
||||
@@ -112,6 +114,7 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
onRun,
|
||||
onCancel,
|
||||
onQuickSave,
|
||||
onFindInEditor,
|
||||
onFormat,
|
||||
onToggleResultPanelVisibility,
|
||||
onAIAction,
|
||||
@@ -161,6 +164,17 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
),
|
||||
})
|
||||
: t("query_editor.action.format_sql");
|
||||
const findInEditorShortcutCombo =
|
||||
activeShortcutPlatform === "mac" ? "Meta+F" : "Ctrl+F";
|
||||
const findInEditorTitle = t(
|
||||
"query_editor.action.find_in_editor_with_shortcut",
|
||||
{
|
||||
shortcut: getShortcutDisplayLabel(
|
||||
findInEditorShortcutCombo,
|
||||
activeShortcutPlatform,
|
||||
),
|
||||
},
|
||||
);
|
||||
const aiMenuItems: MenuProps["items"] = [
|
||||
{
|
||||
key: "ai-generate",
|
||||
@@ -371,6 +385,11 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-action-pair" : undefined}
|
||||
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
||||
>
|
||||
<Tooltip title={findInEditorTitle}>
|
||||
<Button icon={<SearchOutlined />} onClick={onFindInEditor}>
|
||||
{t("query_editor.action.find_in_editor")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={formatSqlTitle}>
|
||||
<Button icon={<FormatPainterOutlined />} onClick={onFormat}>
|
||||
{t("query_editor.action.format")}
|
||||
|
||||
@@ -5940,6 +5940,8 @@
|
||||
"query_editor.action.ai_schema_analysis": "Schema-Analyse",
|
||||
"query_editor.action.export_sql_file": "SQL-Datei exportieren",
|
||||
"query_editor.action.format": "Formatieren",
|
||||
"query_editor.action.find_in_editor": "Suchen",
|
||||
"query_editor.action.find_in_editor_with_shortcut": "Im Editor suchen ({{shortcut}})",
|
||||
"query_editor.action.format_sql": "SQL formatieren",
|
||||
"query_editor.action.format_sql_with_shortcut": "SQL formatieren ({{shortcut}})",
|
||||
"query_editor.action.hide_results_panel": "Ergebnisbereich ausblenden",
|
||||
|
||||
@@ -5940,6 +5940,8 @@
|
||||
"query_editor.action.ai_schema_analysis": "Schema analysis",
|
||||
"query_editor.action.export_sql_file": "Export SQL file",
|
||||
"query_editor.action.format": "Format",
|
||||
"query_editor.action.find_in_editor": "Find",
|
||||
"query_editor.action.find_in_editor_with_shortcut": "Find in editor ({{shortcut}})",
|
||||
"query_editor.action.format_sql": "Format SQL",
|
||||
"query_editor.action.format_sql_with_shortcut": "Format SQL ({{shortcut}})",
|
||||
"query_editor.action.hide_results_panel": "Hide results panel",
|
||||
|
||||
@@ -5940,6 +5940,8 @@
|
||||
"query_editor.action.ai_schema_analysis": "スキーマ分析",
|
||||
"query_editor.action.export_sql_file": "SQL ファイルをエクスポート",
|
||||
"query_editor.action.format": "整形",
|
||||
"query_editor.action.find_in_editor": "検索",
|
||||
"query_editor.action.find_in_editor_with_shortcut": "エディター内を検索({{shortcut}})",
|
||||
"query_editor.action.format_sql": "SQL を整形",
|
||||
"query_editor.action.format_sql_with_shortcut": "SQL を整形({{shortcut}})",
|
||||
"query_editor.action.hide_results_panel": "結果エリアを非表示",
|
||||
|
||||
@@ -5940,6 +5940,8 @@
|
||||
"query_editor.action.ai_schema_analysis": "Анализ схемы",
|
||||
"query_editor.action.export_sql_file": "Экспортировать SQL-файл",
|
||||
"query_editor.action.format": "Форматировать",
|
||||
"query_editor.action.find_in_editor": "Поиск",
|
||||
"query_editor.action.find_in_editor_with_shortcut": "Поиск в редакторе ({{shortcut}})",
|
||||
"query_editor.action.format_sql": "Форматировать SQL",
|
||||
"query_editor.action.format_sql_with_shortcut": "Форматировать SQL ({{shortcut}})",
|
||||
"query_editor.action.hide_results_panel": "Скрыть область результатов",
|
||||
|
||||
@@ -5940,6 +5940,8 @@
|
||||
"query_editor.action.ai_schema_analysis": "Schema 分析",
|
||||
"query_editor.action.export_sql_file": "导出 SQL 文件",
|
||||
"query_editor.action.format": "美化",
|
||||
"query_editor.action.find_in_editor": "搜索",
|
||||
"query_editor.action.find_in_editor_with_shortcut": "编辑器内搜索({{shortcut}})",
|
||||
"query_editor.action.format_sql": "美化 SQL",
|
||||
"query_editor.action.format_sql_with_shortcut": "美化 SQL({{shortcut}})",
|
||||
"query_editor.action.hide_results_panel": "隐藏结果区",
|
||||
|
||||
@@ -5940,6 +5940,8 @@
|
||||
"query_editor.action.ai_schema_analysis": "結構描述分析",
|
||||
"query_editor.action.export_sql_file": "匯出 SQL 檔案",
|
||||
"query_editor.action.format": "格式化",
|
||||
"query_editor.action.find_in_editor": "搜尋",
|
||||
"query_editor.action.find_in_editor_with_shortcut": "編輯器內搜尋({{shortcut}})",
|
||||
"query_editor.action.format_sql": "格式化 SQL",
|
||||
"query_editor.action.format_sql_with_shortcut": "格式化 SQL({{shortcut}})",
|
||||
"query_editor.action.hide_results_panel": "隱藏結果區",
|
||||
|
||||
Reference in New Issue
Block a user