From ef634075aba826627f40716221bc803de3e5ceb0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 28 Apr 2026 13:26:55 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(external-sql):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=A4=96=E9=83=A8=20SQL=20=E6=96=87=E4=BB=B6=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E4=B8=8D=E5=86=99=E5=9B=9E=E6=BA=90=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 保存逻辑:外部 SQL 文件标签页携带 filePath,保存时写回原始磁盘文件 - 后端接口:新增 WriteSQLFile 能力,支持覆盖已有 SQL 文件并保留原文件权限 - 状态隔离:外部文件保存失败时不创建 savedQuery,避免写入 localStorage 副本 - 兼容行为:非文件标签页继续沿用原有 savedQuery 快速保存逻辑 - 文案优化:将数据库下入口改为“外部 SQL 目录”,减少与单文件打开入口的歧义 - 测试覆盖:补充前端保存分支、后端写文件边界和外部 SQL 目录文案测试 Refs #422 --- .../QueryEditor.external-sql-save.test.tsx | 279 ++++++++++++++++++ frontend/src/components/QueryEditor.tsx | 28 +- frontend/src/components/Sidebar.tsx | 3 + frontend/src/main.tsx | 1 + frontend/src/types.ts | 1 + frontend/src/utils/externalSqlTree.test.ts | 1 + frontend/src/utils/externalSqlTree.ts | 2 +- frontend/wailsjs/go/app/App.d.ts | 2 + frontend/wailsjs/go/app/App.js | 4 + internal/app/methods_file.go | 27 ++ .../app/methods_file_sql_directory_test.go | 34 +++ 11 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/QueryEditor.external-sql-save.test.tsx diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx new file mode 100644 index 0000000..711ea7c --- /dev/null +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -0,0 +1,279 @@ +import React from 'react'; +import { act, create, type ReactTestRenderer } from 'react-test-renderer'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { SavedQuery, TabData } from '../types'; +import QueryEditor from './QueryEditor'; + +const storeState = vi.hoisted(() => ({ + connections: [ + { + id: 'conn-1', + name: 'local', + config: { + type: 'mysql', + host: '127.0.0.1', + port: 3306, + user: 'root', + password: '', + database: 'main', + }, + }, + ], + addSqlLog: vi.fn(), + addTab: vi.fn(), + savedQueries: [] as SavedQuery[], + saveQuery: vi.fn(), + theme: 'light', + sqlFormatOptions: { keywordCase: 'upper' as const }, + setSqlFormatOptions: vi.fn(), + queryOptions: { maxRows: 5000 }, + setQueryOptions: vi.fn(), + shortcutOptions: { + runQuery: { enabled: false, combo: '' }, + }, + activeTabId: 'tab-1', + aiPanelVisible: false, + setAIPanelVisible: vi.fn(), +})); + +const backendApp = vi.hoisted(() => ({ + DBQueryWithCancel: vi.fn(), + DBQueryMulti: vi.fn(), + DBGetTables: vi.fn(), + DBGetAllColumns: vi.fn(), + DBGetDatabases: vi.fn(), + DBGetColumns: vi.fn(), + CancelQuery: vi.fn(), + GenerateQueryID: vi.fn(), + WriteSQLFile: vi.fn(), +})); + +const messageApi = vi.hoisted(() => ({ + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warning: vi.fn(), +})); + +const editorState = vi.hoisted(() => { + const state = { + value: '', + editor: null as any, + }; + state.editor = { + getValue: vi.fn(() => state.value), + setValue: vi.fn((value: string) => { + state.value = value; + }), + getModel: vi.fn(() => ({ + getValue: () => state.value, + setValue: (value: string) => { + state.value = value; + }, + getValueInRange: () => '', + getLineContent: () => '', + getWordUntilPosition: () => ({ startColumn: 1, endColumn: 1 }), + })), + getSelection: vi.fn(() => null), + addAction: vi.fn(), + onDidChangeModelContent: vi.fn(() => ({ dispose: vi.fn() })), + hasTextFocus: vi.fn(() => true), + }; + return state; +}); + +vi.mock('../store', () => { + const useStore = Object.assign( + (selector: (state: typeof storeState) => any) => selector(storeState), + { getState: () => storeState }, + ); + return { useStore }; +}); + +vi.mock('../../wailsjs/go/app/App', () => backendApp); + +vi.mock('../utils/autoFetchVisibility', () => ({ + useAutoFetchVisibility: () => false, +})); + +vi.mock('@monaco-editor/react', () => ({ + default: ({ defaultValue, onMount }: any) => { + React.useEffect(() => { + editorState.value = String(defaultValue || ''); + onMount?.(editorState.editor, { + editor: { setTheme: vi.fn() }, + languages: { + CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 }, + registerCompletionItemProvider: vi.fn(), + }, + }); + }, []); + return