mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 00:42:47 +08:00
✨ feat(saved-query): 支持独立 SQL 文件与自定义存储目录
- 将已存查询内容从 JSON 拆分为独立 .sql 文件,并采用可读名称和数字后缀处理冲突 - 自动迁移旧版内联 SQL 与摘要文件名,保留外部编辑并在失败时回滚 - 支持选择、迁移、打开及恢复默认存储目录,补充数据根切换串行保护 - 新增已存查询右键在文件夹中打开,并同步处理查询重命名后的文件路径 - 补齐 Wails、浏览器模拟、六语言文案及后端和前端回归测试
This commit is contained in:
@@ -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(() => {');
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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: {
|
||||
|
||||
10
frontend/wailsjs/go/app/App.d.ts
vendored
10
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user