feat(saved-query): 支持独立 SQL 文件与自定义存储目录

- 将已存查询内容从 JSON 拆分为独立 .sql 文件,并采用可读名称和数字后缀处理冲突
- 自动迁移旧版内联 SQL 与摘要文件名,保留外部编辑并在失败时回滚
- 支持选择、迁移、打开及恢复默认存储目录,补充数据根切换串行保护
- 新增已存查询右键在文件夹中打开,并同步处理查询重命名后的文件路径
- 补齐 Wails、浏览器模拟、六语言文案及后端和前端回归测试
This commit is contained in:
Syngnat
2026-07-24 11:29:04 +08:00
parent 4bc131264b
commit d3909c7dff
27 changed files with 2694 additions and 52 deletions

View File

@@ -128,7 +128,7 @@ describe('settings center layout', () => {
expect(appSource).toContain('OpenLogDirectory');
expect(appSource).toContain('SelectLogDirectory');
expect(appSource).toContain("const [selectedLogDirectoryPath, setSelectedLogDirectoryPath] = useState('');");
expect(appSource).toContain('const directorySettingsApplying = dataRootApplying || logDirectoryApplying;');
expect(appSource).toContain('const directorySettingsApplying = dataRootApplying || logDirectoryApplying || savedQueryDirectoryApplying;');
expect(appSource).toContain('const handleSelectLogDirectory = useCallback(async () => {');
expect(appSource).toContain('const handleApplyLogDirectory = useCallback(async (useDefaultPath = false) => {');
expect(appSource).toContain('const handleOpenLogDirectory = useCallback(async () => {');
@@ -153,6 +153,50 @@ describe('settings center layout', () => {
expect(appSource).not.toContain("handleOpenToolCenterPane('config', 'log-directory')");
});
it('keeps configurable saved query file storage inside the data-root detail page', () => {
const selectHandlerStart = appSource.indexOf('const handleSelectSavedQueryDirectory = useCallback(async () => {');
const selectHandlerEnd = appSource.indexOf('const handleApplySavedQueryDirectory = useCallback', selectHandlerStart);
const selectHandlerSource = appSource.slice(selectHandlerStart, selectHandlerEnd);
const rendererStart = appSource.indexOf('const renderSavedQueryDirectorySettings = (readOnly = false) => (');
const rendererEnd = appSource.indexOf('const renderLogDirectorySettings = () => {', rendererStart);
const rendererSource = appSource.slice(rendererStart, rendererEnd);
const embeddedDataRootStart = appSource.indexOf("if (activeSettingsCenterPane.key === 'data-root')");
const embeddedDataRootEnd = appSource.indexOf(
"if (activeSettingsCenterPane.key === 'security-update')",
embeddedDataRootStart,
);
const embeddedDataRootSource = appSource.slice(embeddedDataRootStart, embeddedDataRootEnd);
const standaloneDataRootStart = appSource.indexOf('{isDataRootModalOpen && (');
const standaloneDataRootEnd = appSource.indexOf(
'<ConnectionPackagePasswordModal',
standaloneDataRootStart,
);
const standaloneDataRootSource = appSource.slice(standaloneDataRootStart, standaloneDataRootEnd);
expect(appSource).toContain('ApplySavedQueryDirectory');
expect(appSource).toContain('OpenSavedQueryDirectory');
expect(appSource).toContain('SelectSavedQueryDirectory');
expect(appSource).toContain('GetSavedQueries');
expect(appSource).toContain("const [selectedSavedQueryDirectoryPath, setSelectedSavedQueryDirectoryPath] = useState('');");
expect(appSource).toContain("const [savedQueryDirectoryApplying, setSavedQueryDirectoryApplying] = useState(false);");
expect(appSource).toContain('const handleSelectSavedQueryDirectory = useCallback(async () => {');
expect(selectHandlerSource).toContain('if (data.cancelled === true) return;');
expect(selectHandlerSource).not.toContain('已取消');
expect(appSource).toContain('const handleApplySavedQueryDirectory = useCallback(async (useDefaultPath = false) => {');
expect(appSource).toContain('const handleOpenSavedQueryDirectory = useCallback(async () => {');
expect(rendererStart).toBeGreaterThan(-1);
expect(rendererEnd).toBeGreaterThan(rendererStart);
expect(rendererSource).toContain('data-saved-query-directory-settings="true"');
expect(rendererSource).toContain("t('app.data_root.saved_query_directory.title')");
expect(rendererSource).toContain('dataRootInfo?.defaultSavedQueryDirectory');
expect(rendererSource).toContain('{!readOnly && (');
expect(appSource).toContain('replaceSavedQueries(Array.isArray(queries) ? queries : []);');
expect(appSource).toContain('await reloadSavedQueryGroups();');
expect(embeddedDataRootSource).toContain('{renderSavedQueryDirectorySettings(true)}');
expect(embeddedDataRootSource).toContain('{renderSavedQueryDirectorySettings()}');
expect(standaloneDataRootSource).toContain('{renderSavedQueryDirectorySettings()}');
});
it('adds close and back-to-settings actions to settings center detail panes', () => {
expect(appSource).toContain('const handleBackFromSettingsCenterPane = useCallback(() => {');
expect(appSource).toContain('const handleCancelSettingsCenterPane = useCallback(() => {');

View File

@@ -222,7 +222,26 @@ import { useAppLogPanelResize } from './hooks/useAppLogPanelResize';
import { useAppSidebarResize } from './hooks/useAppSidebarResize';
import { useAppUtilityStyles } from './hooks/useAppUtilityStyles';
import { useWorkbenchTabs } from './hooks/useWorkbenchTabs';
import { ApplyDataRootDirectory, ApplyLogDirectory, CancelApplicationQuit, ForceQuitApplication, GetDataRootDirectoryInfo, GetSavedConnections, ListInstalledFontFamilies, OpenDataRootDirectory, OpenLogDirectory, SelectDataRootDirectory, SelectLogDirectory, SetApplicationBrandIcon, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
import {
ApplyDataRootDirectory,
ApplyLogDirectory,
ApplySavedQueryDirectory,
CancelApplicationQuit,
ForceQuitApplication,
GetDataRootDirectoryInfo,
GetSavedConnections,
GetSavedQueries,
ListInstalledFontFamilies,
OpenDataRootDirectory,
OpenLogDirectory,
OpenSavedQueryDirectory,
SelectDataRootDirectory,
SelectLogDirectory,
SelectSavedQueryDirectory,
SetApplicationBrandIcon,
SetMacNativeWindowControls,
SetWindowTranslucency,
} from '../wailsjs/go/app/App';
import { getAntdLocale } from './i18n/frameworkLocale';
import { useI18n } from './i18n/provider';
import './App.css';
@@ -3108,10 +3127,12 @@ function App() {
const [dataRootInfo, setDataRootInfo] = useState<any>(null);
const [selectedDataRootPath, setSelectedDataRootPath] = useState('');
const [selectedLogDirectoryPath, setSelectedLogDirectoryPath] = useState('');
const [selectedSavedQueryDirectoryPath, setSelectedSavedQueryDirectoryPath] = useState('');
const [dataRootLoading, setDataRootLoading] = useState(false);
const [dataRootApplying, setDataRootApplying] = useState(false);
const [logDirectoryApplying, setLogDirectoryApplying] = useState(false);
const directorySettingsApplying = dataRootApplying || logDirectoryApplying;
const [savedQueryDirectoryApplying, setSavedQueryDirectoryApplying] = useState(false);
const directorySettingsApplying = dataRootApplying || logDirectoryApplying || savedQueryDirectoryApplying;
const aiEntryPlacement = resolveAIEntryPlacement();
const legacyAiEdgeHandleAttachment = resolveLegacyAIEdgeHandleAttachment(aiPanelVisible);
@@ -3516,6 +3537,9 @@ function App() {
setDataRootInfo(data);
setSelectedDataRootPath(String(data.path || ''));
setSelectedLogDirectoryPath(String(data.logDirectory || data.defaultLogDirectory || ''));
setSelectedSavedQueryDirectoryPath(String(
data.savedQueryDirectory || data.defaultSavedQueryDirectory || '',
));
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown'));
void message.error(t('app.data_root.message.load_failed_with_error', { error: errMsg }));
@@ -3641,6 +3665,144 @@ function App() {
}
}, [t]);
const handleSelectSavedQueryDirectory = useCallback(async () => {
try {
const res = await SelectSavedQueryDirectory(
selectedSavedQueryDirectoryPath
|| dataRootInfo?.savedQueryDirectory
|| dataRootInfo?.defaultSavedQueryDirectory
|| '',
);
if (!res?.success) {
const data = (res?.data || {}) as any;
if (data.cancelled === true) return;
throw new Error(res?.message || t('common.unknown'));
}
const data = (res?.data || {}) as any;
setSelectedSavedQueryDirectoryPath(String(data.directory || ''));
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown'));
void message.error(t('app.data_root.saved_query_directory.message.select_failed_with_error', { error: errMsg }));
}
}, [
dataRootInfo?.defaultSavedQueryDirectory,
dataRootInfo?.savedQueryDirectory,
selectedSavedQueryDirectoryPath,
t,
]);
const handleApplySavedQueryDirectory = useCallback(async (useDefaultPath = false) => {
const nextPath = useDefaultPath
? String(dataRootInfo?.defaultSavedQueryDirectory || '')
: String(selectedSavedQueryDirectoryPath || '').trim();
if (!nextPath) {
void message.warning(t('app.data_root.saved_query_directory.message.select_valid_first'));
return;
}
setSavedQueryDirectoryApplying(true);
try {
const res = await ApplySavedQueryDirectory(nextPath);
if (!res?.success) {
throw new Error(res?.message || t('common.unknown'));
}
const data = (res?.data || {}) as any;
setDataRootInfo(data);
setSelectedSavedQueryDirectoryPath(String(
data.savedQueryDirectory || data.defaultSavedQueryDirectory || nextPath,
));
try {
const queries = await GetSavedQueries();
replaceSavedQueries(Array.isArray(queries) ? queries : []);
await reloadSavedQueryGroups();
} catch (refreshError) {
console.warn('Failed to refresh saved queries after changing their directory', refreshError);
}
void message.success(res?.message || t('app.data_root.saved_query_directory.message.updated'));
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown'));
void message.error(t('app.data_root.saved_query_directory.message.apply_failed_with_error', { error: errMsg }));
} finally {
setSavedQueryDirectoryApplying(false);
}
}, [
dataRootInfo?.defaultSavedQueryDirectory,
reloadSavedQueryGroups,
replaceSavedQueries,
selectedSavedQueryDirectoryPath,
t,
]);
const handleOpenSavedQueryDirectory = useCallback(async () => {
try {
const res = await OpenSavedQueryDirectory();
if (!res?.success) {
throw new Error(res?.message || t('common.unknown'));
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown'));
void message.error(t('app.data_root.saved_query_directory.message.open_failed_with_error', { error: errMsg }));
}
}, [t]);
const renderSavedQueryDirectorySettings = (readOnly = false) => (
<div style={utilityPanelStyle} data-saved-query-directory-settings="true">
<div style={{ fontWeight: 600 }}>{t('app.data_root.saved_query_directory.title')}</div>
<div style={{ ...utilityMutedTextStyle, marginTop: 6 }}>
{t('app.data_root.saved_query_directory.description')}
</div>
<div style={{ display: 'grid', gap: 10, marginTop: 14 }}>
<div>
<div style={{ marginBottom: 6, fontWeight: 500 }}>
{t('app.data_root.saved_query_directory.current_directory')}
</div>
<Input
readOnly
value={selectedSavedQueryDirectoryPath}
placeholder={t('app.data_root.saved_query_directory.placeholder')}
aria-label={t('app.data_root.saved_query_directory.title')}
/>
</div>
{!readOnly && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
<Button
icon={<FolderOpenOutlined />}
disabled={directorySettingsApplying}
onClick={() => void handleSelectSavedQueryDirectory()}
>
{t('app.data_root.action.select')}
</Button>
<Button onClick={() => void handleOpenSavedQueryDirectory()}>
{t('app.data_root.action.open_current')}
</Button>
<Button
disabled={directorySettingsApplying}
loading={savedQueryDirectoryApplying}
onClick={() => void handleApplySavedQueryDirectory(true)}
>
{t('app.data_root.action.restore_default_directory')}
</Button>
<Button
type="primary"
disabled={directorySettingsApplying}
loading={savedQueryDirectoryApplying}
onClick={() => void handleApplySavedQueryDirectory(false)}
>
{t('common.save')}
</Button>
</div>
)}
<div>
<div style={{ marginBottom: 6, fontWeight: 500 }}>
{t('app.data_root.saved_query_directory.default_directory')}
</div>
<div style={{ ...utilityMutedTextStyle, overflowWrap: 'anywhere' }}>
{dataRootInfo?.defaultSavedQueryDirectory || '-'}
</div>
</div>
</div>
</div>
);
const renderLogDirectorySettings = () => {
const editable = dataRootInfo?.logDirectoryEditable !== false;
const managedByEnvironment = dataRootInfo?.logDirectorySource === 'environment';
@@ -8044,6 +8206,7 @@ function App() {
</div>
</div>
</div>
{renderSavedQueryDirectorySettings(true)}
</div>
);
}
@@ -8142,6 +8305,7 @@ function App() {
{t('app.data_root.restart_hint')}
</div>
</div>
{renderSavedQueryDirectorySettings()}
{renderLogDirectorySettings()}
</div>
)}
@@ -8629,6 +8793,7 @@ function App() {
{t('app.data_root.restart_hint')}
</div>
</div>
{renderSavedQueryDirectorySettings()}
{renderLogDirectorySettings()}
</div>
)}

View File

@@ -1956,6 +1956,10 @@ describe('Sidebar locate toolbar', () => {
expect(renameHandlerSource).toContain("message.error(t('query_editor.save_modal.name_required'))");
expect(renameHandlerSource).toContain("message.warning(t('sidebar.message.saved_query_name_unchanged'))");
expect(renameHandlerSource).toContain("message.success(t('sidebar.message.saved_query_renamed'))");
expect(renameHandlerSource).toContain("typeof backendApp?.RenameSavedQuery === 'function'");
expect(renameHandlerSource).toContain('backendApp.RenameSavedQuery(renameSavedQueryTarget.id, nextName)');
expect(renameHandlerSource).toContain('latestState.replaceSavedQueries(latestState.savedQueries.map');
expect(renameHandlerSource).toContain('persisted = await saveQuery({');
expect(savedQueryMenuSource).toContain("key: 'rename-query'");
expect(savedQueryMenuSource).toContain("label: t('sidebar.menu.rename_query')");
expect(savedQueryMenuSource).toContain("label: t('sidebar.menu.open_query')");
@@ -1984,6 +1988,25 @@ describe('Sidebar locate toolbar', () => {
expect(renameModalSource).not.toContain("message: '请输入查询名称'");
});
it('reveals a saved query file from its context menu', () => {
const source = readSidebarSource();
const handlerStart = source.indexOf('const handleRevealSavedQueryInFolder = useCallback(async (query: SavedQuery) =>');
const handlerEnd = source.indexOf('const isSavedQueryUnmatched', handlerStart);
const savedQueryMenuStart = source.indexOf('// \u5df2\u5b58\u67e5\u8be2\u8282\u70b9\u7684\u53f3\u952e\u83dc\u5355');
const savedQueryMenuEnd = source.indexOf("if (node.type === 'external-sql-root') {", savedQueryMenuStart);
const handlerSource = source.slice(handlerStart, handlerEnd);
const savedQueryMenuSource = source.slice(savedQueryMenuStart, savedQueryMenuEnd);
expect(handlerStart).toBeGreaterThanOrEqual(0);
expect(handlerEnd).toBeGreaterThan(handlerStart);
expect(handlerSource).toContain('await RevealSavedQueryInFolder(query.id)');
expect(handlerSource).toContain("t('sidebar.message.saved_query_revealed')");
expect(handlerSource).toContain("t('sidebar.message.saved_query_reveal_failed'");
expect(savedQueryMenuSource).toContain("key: 'reveal-saved-query-in-folder'");
expect(savedQueryMenuSource).toContain("label: t('sidebar.menu.reveal_saved_query_in_folder')");
expect(savedQueryMenuSource).toContain('onClick: () => void handleRevealSavedQueryInFolder(q)');
});
it('renders the v2 table context menu with the redesigned table layout', () => {
const markup = renderToStaticMarkup(
<V2TableContextMenuView

View File

@@ -2298,6 +2298,7 @@ const Sidebar: React.FC<{
handleRenameView,
openRenameSavedQueryModal,
handleRenameSavedQuery,
handleRevealSavedQueryInFolder,
isSavedQueryUnmatched,
handleRebindSavedQuery,
openRoutineDefinition,
@@ -2688,6 +2689,7 @@ const Sidebar: React.FC<{
connections,
handleRebindSavedQuery,
openRenameSavedQueryModal,
handleRevealSavedQueryInFolder,
resolveSavedQueryDisplayName,
deleteQuery,
savedQueryGroups,

View File

@@ -199,6 +199,7 @@ export const buildSidebarLegacyNodeMenuItems = (
connections,
handleRebindSavedQuery,
openRenameSavedQueryModal,
handleRevealSavedQueryInFolder,
resolveSavedQueryDisplayName,
deleteQuery,
savedQueryGroups,
@@ -1160,6 +1161,12 @@ export const buildSidebarLegacyNodeMenuItems = (
});
}
},
{
key: 'reveal-saved-query-in-folder',
label: t('sidebar.menu.reveal_saved_query_in_folder'),
icon: <FolderOpenOutlined />,
onClick: () => void handleRevealSavedQueryInFolder(q),
},
...rebindMenuItems,
{
key: 'move-saved-query-to-group',

View File

@@ -41,6 +41,7 @@ import {
RenameDatabase,
RenameTable,
RenameView,
RevealSavedQueryInFolder,
} from '../../../wailsjs/go/app/App';
import { resolveSidebarNodeConnectionId, type SidebarTreeNode as TreeNode } from '../sidebarV2Utils';
@@ -1001,10 +1002,25 @@ export const useSidebarObjectActions = ({
return;
}
const persisted = await saveQuery({
...renameSavedQueryTarget,
name: nextName,
});
const backendApp = (window as any).go?.app?.App;
let persisted: SavedQuery;
if (typeof backendApp?.RenameSavedQuery === 'function') {
const renamed = await backendApp.RenameSavedQuery(renameSavedQueryTarget.id, nextName);
persisted = {
...renameSavedQueryTarget,
...(renamed || {}),
name: String(renamed?.name || nextName),
};
const latestState = useStore.getState();
latestState.replaceSavedQueries(latestState.savedQueries.map(query => (
query.id === persisted.id ? { ...query, ...persisted } : query
)));
} else {
persisted = await saveQuery({
...renameSavedQueryTarget,
name: nextName,
});
}
const updateSavedQueryNode = (list: TreeNode[]): TreeNode[] =>
list.map(node => {
if (node.type === 'saved-query' && node.dataRef?.id === renameSavedQueryTarget.id) {
@@ -1033,6 +1049,24 @@ export const useSidebarObjectActions = ({
}
};
const handleRevealSavedQueryInFolder = useCallback(async (query: SavedQuery) => {
if (!query?.id) return;
try {
const res = await RevealSavedQueryInFolder(query.id);
if (res?.success) {
message.success(res.message || t('sidebar.message.saved_query_revealed'));
return;
}
message.error(res?.message || t('sidebar.message.saved_query_reveal_failed', {
error: t('common.unknown'),
}));
} catch (error) {
message.error(t('sidebar.message.saved_query_reveal_failed', {
error: error instanceof Error ? error.message : String(error),
}));
}
}, []);
const isSavedQueryUnmatched = useCallback((query: SavedQuery): boolean => {
return query.bindingStatus === 'orphan' || !connectionIdSet.has(query.connectionId);
}, [connectionIdSet]);
@@ -1401,6 +1435,7 @@ export const useSidebarObjectActions = ({
handleRenameView,
openRenameSavedQueryModal,
handleRenameSavedQuery,
handleRevealSavedQueryInFolder,
isSavedQueryUnmatched,
handleRebindSavedQuery,
openRoutineDefinition,

View File

@@ -208,6 +208,50 @@ describe("i18n catalog", () => {
expect(catalogs["en-US"]["app.data_root.log_directory.restart_hint"]).toContain("gonavi.log");
});
it("keeps saved query directory copy complete across all catalogs", () => {
const savedQueryDirectoryKeys = [
"app.data_root.saved_query_directory.backend.dialog.select_directory",
"app.data_root.saved_query_directory.backend.error.desktop_only",
"app.data_root.saved_query_directory.backend.error.directory_unavailable",
"app.data_root.saved_query_directory.backend.error.migrate_failed",
"app.data_root.saved_query_directory.backend.error.open_directory_failed",
"app.data_root.saved_query_directory.backend.error.open_directory_unsupported",
"app.data_root.saved_query_directory.backend.error.query_file_unavailable",
"app.data_root.saved_query_directory.backend.error.query_id_required",
"app.data_root.saved_query_directory.backend.error.query_not_found",
"app.data_root.saved_query_directory.backend.error.reveal_failed",
"app.data_root.saved_query_directory.backend.error.reveal_unsupported",
"app.data_root.saved_query_directory.backend.error.save_failed",
"app.data_root.saved_query_directory.backend.message.opened",
"app.data_root.saved_query_directory.backend.message.revealed",
"app.data_root.saved_query_directory.backend.message.unchanged",
"app.data_root.saved_query_directory.backend.message.updated",
"app.data_root.saved_query_directory.current_directory",
"app.data_root.saved_query_directory.default_directory",
"app.data_root.saved_query_directory.description",
"app.data_root.saved_query_directory.message.apply_failed_with_error",
"app.data_root.saved_query_directory.message.open_failed_with_error",
"app.data_root.saved_query_directory.message.select_failed_with_error",
"app.data_root.saved_query_directory.message.select_valid_first",
"app.data_root.saved_query_directory.message.updated",
"app.data_root.saved_query_directory.placeholder",
"app.data_root.saved_query_directory.title",
] as const;
const base = catalogs["en-US"];
for (const language of SUPPORTED_LANGUAGES) {
for (const key of savedQueryDirectoryKeys) {
expect(catalogs[language]).toHaveProperty(key);
expect(catalogs[language][key]).toBeTruthy();
expect(getPlaceholders(catalogs[language][key])).toEqual(getPlaceholders(base[key]));
}
}
expect(catalogs["zh-CN"]["app.data_root.saved_query_directory.title"]).toBe("已存查询目录");
expect(catalogs["zh-CN"]["app.data_root.saved_query_directory.description"]).toContain(".sql");
expect(catalogs["en-US"]["app.data_root.saved_query_directory.description"]).toContain("independent .sql file");
});
it("includes App shell keys required by every supported language", () => {
const appShellKeys = [
"app.tools.title",

View File

@@ -76,7 +76,10 @@ const importMain = async () => {
ExportConnectionsPackage: (options?: { includeSecrets?: boolean; filePassword?: string }) => Promise<{ success: boolean; message?: string }>;
ApplyDataRootDirectory: (path: string) => Promise<{ success: boolean; message?: string; data?: { path?: string } }>;
ApplyLogDirectory: (path: string) => Promise<{ success: boolean; message?: string; data?: { logDirectory?: string; logDirectoryRestartRequired?: boolean } }>;
ApplySavedQueryDirectory: (path: string) => Promise<{ success: boolean; message?: string; data?: { savedQueryDirectory?: string; savedQueryDirectorySource?: string } }>;
SaveQuery: (input: { id?: string; name?: string; sql?: string }) => Promise<{ name: string; sql: string }>;
RenameSavedQuery: (id: string, name: string) => Promise<{ id: string; name: string; sql: string }>;
RevealSavedQueryInFolder: (id: string) => Promise<{ success: boolean; message?: string; data?: { path?: string } }>;
CheckForUpdates: () => Promise<{ success: boolean; data?: Record<string, unknown> }>;
CheckForUpdatesSilently: () => Promise<{ success: boolean; data?: Record<string, unknown> }>;
SetUpdateChannel: (channel: string) => Promise<{ success: boolean; data?: { channel?: string } }>;
@@ -261,6 +264,56 @@ describe('main browser mock', () => {
}));
});
it('keeps browser mock saved query directory state available for the data-root page', async () => {
vi.stubGlobal('navigator', {
languages: ['en-US'],
language: 'en-US',
});
const app = await importMain();
const { t } = await import('./i18n');
await expect(app!.ApplySavedQueryDirectory('C:/mock/custom-saved-queries')).resolves.toEqual(expect.objectContaining({
success: true,
message: t('app.data_root.saved_query_directory.message.updated'),
data: expect.objectContaining({
savedQueryDirectory: 'C:/mock/custom-saved-queries',
savedQueryDirectorySource: 'custom',
}),
}));
});
it('renames browser mock saved queries without replacing their SQL', async () => {
const app = await importMain();
const saved = await app!.SaveQuery({
id: 'browser-mock-rename-query',
name: 'Before',
sql: 'select 42',
});
await expect(app!.RenameSavedQuery('browser-mock-rename-query', 'After')).resolves.toEqual(expect.objectContaining({
id: 'browser-mock-rename-query',
name: 'After',
sql: saved.sql,
}));
});
it('reveals browser mock saved query files in their configured directory', async () => {
const app = await importMain();
await app!.SaveQuery({
id: 'browser-mock-reveal-query',
name: 'Reveal',
sql: 'select 7',
});
await expect(app!.RevealSavedQueryInFolder('browser-mock-reveal-query')).resolves.toEqual(expect.objectContaining({
success: true,
data: expect.objectContaining({
path: 'C:/mock/.gonavi/saved_queries/browser-mock-reveal-query.sql',
}),
}));
});
it('localizes browser mock MCP server test messages', async () => {
vi.stubGlobal('navigator', {
languages: ['en-US'],

View File

@@ -149,6 +149,9 @@ if (
logDirectorySource: 'default',
logDirectoryEditable: true,
logDirectoryRestartRequired: false,
savedQueryDirectory: 'C:/mock/.gonavi/saved_queries',
defaultSavedQueryDirectory: 'C:/mock/.gonavi/saved_queries',
savedQueryDirectorySource: 'default',
};
const upsertMockConnection = (view: any) => {
@@ -466,6 +469,29 @@ if (
GetSavedQueries: async () => cloneBrowserMockValue(mockSavedQueries),
GetSavedQueryGroups: async () => cloneBrowserMockValue(mockSavedQueryGroups),
SaveQuery: async (input: any) => saveMockQuery(input),
RenameSavedQuery: async (id: string, name: string) => {
const existing = mockSavedQueries.find((item) => item.id === id);
if (!existing) throw new Error('saved query not found');
return saveMockQuery({
...existing,
name: String(name || '').trim(),
});
},
RevealSavedQueryInFolder: async (id: string) => {
const existing = mockSavedQueries.find((item) => item.id === id);
if (!existing) {
return {
success: false,
message: t('app.data_root.saved_query_directory.backend.error.query_not_found', { id }),
};
}
const path = `${mockDataRootInfo.savedQueryDirectory}/${id}.sql`;
return {
success: true,
message: t('app.data_root.saved_query_directory.backend.message.revealed', { path }),
data: { path },
};
},
SaveSavedQueryGroup: async (input: any) => saveMockSavedQueryGroup(input),
ImportSavedQueries: async (payload: any) => {
const items = Array.isArray(payload) ? payload : payload?.queries;
@@ -551,6 +577,7 @@ if (
OpenDriverDownloadDirectory: async (path: string) => ({ success: true, data: { path } }),
OpenDataRootDirectory: async () => ({ success: true }),
OpenLogDirectory: async () => ({ success: true }),
OpenSavedQueryDirectory: async () => ({ success: true }),
SelectSQLDirectory: async (currentPath: string) => ({ success: false, message: currentPath ? '已取消' : '已取消' }),
ListSQLDirectory: async () => ({ success: true, data: [] }),
ReadSQLFile: async () => ({ success: false, message: '已取消' }),
@@ -674,6 +701,25 @@ if (
data: cloneBrowserMockValue(mockDataRootInfo),
};
},
SelectSavedQueryDirectory: async (currentPath: string) => ({
success: true,
data: { directory: currentPath || mockDataRootInfo.defaultSavedQueryDirectory },
}),
ApplySavedQueryDirectory: async (path: string) => {
const nextPath = String(path || mockDataRootInfo.defaultSavedQueryDirectory);
mockDataRootInfo = {
...mockDataRootInfo,
savedQueryDirectory: nextPath,
savedQueryDirectorySource: nextPath === mockDataRootInfo.defaultSavedQueryDirectory
? 'default'
: 'custom',
};
return {
success: true,
message: t('app.data_root.saved_query_directory.message.updated'),
data: cloneBrowserMockValue(mockDataRootInfo),
};
},
}
},
aiservice: {

View File

@@ -14,6 +14,8 @@ export function ApplyDataRootDirectory(arg1:string,arg2:boolean):Promise<connect
export function ApplyLogDirectory(arg1:string):Promise<connection.QueryResult>;
export function ApplySavedQueryDirectory(arg1:string):Promise<connection.QueryResult>;
export function BuildSQLAuditExport(arg1:sqlaudit.Filter,arg2:string):Promise<connection.QueryResult>;
export function CancelApplicationQuit():Promise<connection.QueryResult>;
@@ -290,6 +292,8 @@ export function OpenLogDirectory():Promise<connection.QueryResult>;
export function OpenSQLFile():Promise<connection.QueryResult>;
export function OpenSavedQueryDirectory():Promise<connection.QueryResult>;
export function PreviewChanges(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:connection.ChangeSet):Promise<connection.QueryResult>;
export function PreviewImportFile(arg1:string):Promise<connection.QueryResult>;
@@ -364,6 +368,8 @@ export function RenameSQLDirectory(arg1:string,arg2:string):Promise<connection.Q
export function RenameSQLFile(arg1:string,arg2:string):Promise<connection.QueryResult>;
export function RenameSavedQuery(arg1:string,arg2:string):Promise<connection.SavedQuery>;
export function RenameSchema(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise<connection.QueryResult>;
export function RenameTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise<connection.QueryResult>;
@@ -392,6 +398,8 @@ export function ResultDiffUploadChunk(arg1:resultdiff.UploadChunkRequest):Promis
export function RetrySecurityUpdateCurrentRound(arg1:app.RetrySecurityUpdateRequest):Promise<app.SecurityUpdateStatus>;
export function RevealSavedQueryInFolder(arg1:string):Promise<connection.QueryResult>;
export function SaveConnection(arg1:connection.SavedConnectionInput):Promise<connection.SavedConnectionView>;
export function SaveGlobalProxy(arg1:connection.SaveGlobalProxyInput):Promise<connection.GlobalProxyView>;
@@ -420,6 +428,8 @@ export function SelectSQLFileForExecution():Promise<connection.QueryResult>;
export function SelectSSHKeyFile(arg1:string):Promise<connection.QueryResult>;
export function SelectSavedQueryDirectory(arg1:string):Promise<connection.QueryResult>;
export function SetApplicationBrandIcon(arg1:string):Promise<connection.QueryResult>;
export function SetLanguage(arg1:string):Promise<void>;

View File

@@ -14,6 +14,10 @@ export function ApplyLogDirectory(arg1) {
return window['go']['app']['App']['ApplyLogDirectory'](arg1);
}
export function ApplySavedQueryDirectory(arg1) {
return window['go']['app']['App']['ApplySavedQueryDirectory'](arg1);
}
export function BuildSQLAuditExport(arg1, arg2) {
return window['go']['app']['App']['BuildSQLAuditExport'](arg1, arg2);
}
@@ -566,6 +570,10 @@ export function OpenSQLFile() {
return window['go']['app']['App']['OpenSQLFile']();
}
export function OpenSavedQueryDirectory() {
return window['go']['app']['App']['OpenSavedQueryDirectory']();
}
export function PreviewChanges(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['PreviewChanges'](arg1, arg2, arg3, arg4);
}
@@ -714,6 +722,10 @@ export function RenameSQLFile(arg1, arg2) {
return window['go']['app']['App']['RenameSQLFile'](arg1, arg2);
}
export function RenameSavedQuery(arg1, arg2) {
return window['go']['app']['App']['RenameSavedQuery'](arg1, arg2);
}
export function RenameSchema(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['RenameSchema'](arg1, arg2, arg3, arg4);
}
@@ -770,6 +782,10 @@ export function RetrySecurityUpdateCurrentRound(arg1) {
return window['go']['app']['App']['RetrySecurityUpdateCurrentRound'](arg1);
}
export function RevealSavedQueryInFolder(arg1) {
return window['go']['app']['App']['RevealSavedQueryInFolder'](arg1);
}
export function SaveConnection(arg1) {
return window['go']['app']['App']['SaveConnection'](arg1);
}
@@ -826,6 +842,10 @@ export function SelectSSHKeyFile(arg1) {
return window['go']['app']['App']['SelectSSHKeyFile'](arg1);
}
export function SelectSavedQueryDirectory(arg1) {
return window['go']['app']['App']['SelectSavedQueryDirectory'](arg1);
}
export function SetApplicationBrandIcon(arg1) {
return window['go']['app']['App']['SetApplicationBrandIcon'](arg1);
}