feat(query-editor): 支持自定义新建查询默认 SQL

- 在 appearance 持久化设置中新增 newQuerySqlTemplate,支持默认回退与空白模板
- 新建空白查询标签页按用户模板初始化,不影响外部 SQL、保存查询和只读标签
- 在工具中心补充模板编辑与恢复默认入口,并完善 store、编辑器和 i18n 回归测试
This commit is contained in:
Syngnat
2026-07-01 09:44:22 +08:00
parent e135956eba
commit d94335dc62
14 changed files with 175 additions and 5 deletions

View File

@@ -49,7 +49,10 @@ const storeState = vi.hoisted(() => ({
saveQuery: vi.fn(),
theme: 'light',
languagePreference: 'zh-CN' as 'zh-CN' | 'en-US',
appearance: { uiVersion: 'legacy' as 'legacy' | 'v2' },
appearance: {
uiVersion: 'legacy' as 'legacy' | 'v2',
newQuerySqlTemplate: null as string | null,
},
sqlFormatOptions: { keywordCase: 'upper' as const },
setSqlFormatOptions: vi.fn(),
queryOptions: {
@@ -705,6 +708,7 @@ describe('QueryEditor external SQL save', () => {
storeState.aiPanelVisible = false;
storeState.setAIPanelVisible.mockReset();
storeState.appearance.uiVersion = 'legacy';
storeState.appearance.newQuerySqlTemplate = null;
storeState.queryOptions = {
maxRows: 5000,
showColumnComment: true,
@@ -833,6 +837,26 @@ describe('QueryEditor external SQL save', () => {
expect(editorState.value).toBe('SELECT * FROM ');
});
it('uses the customized new query template for a fresh blank query tab', async () => {
storeState.appearance.newQuerySqlTemplate = 'SELECT id,\n name\nFROM users;';
await act(async () => {
create(<QueryEditor tab={createTab({ query: '' })} />);
});
expect(editorState.value).toBe('SELECT id,\n name\nFROM users;');
});
it('allows a blank new query template when the default content is cleared', async () => {
storeState.appearance.newQuerySqlTemplate = '';
await act(async () => {
create(<QueryEditor tab={createTab({ query: '' })} />);
});
expect(editorState.value).toBe('');
});
it('keeps the query results panel hidden by default on first entry', async () => {
storeState.appearance.uiVersion = 'v2';

View File

@@ -124,6 +124,7 @@ import {
queryCompletionMetadataRowsBySpecs,
readSidebarSqlDropText,
matchLeadingSelectTableReference,
resolveNewQueryDefaultTemplate,
resolveEventTargetNode,
resolveNextResultSetIndex,
resolveOracleExactCaseTableReference,
@@ -759,7 +760,11 @@ const resetSharedQueryEditorMetadata = () => {
const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isActive = true }) => {
const [query, setQuery] = useState(getInitialEditorQuery(tab));
const appearance = useStore(state => state.appearance);
const [query, setQuery] = useState(() => getInitialEditorQuery(
tab,
resolveNewQueryDefaultTemplate(appearance.newQuerySqlTemplate),
));
const isExternalSQLFileTab = Boolean(String(tab.filePath || '').trim());
const isObjectEditQueryTab = tab.type === 'query' && tab.queryMode === 'object-edit';
@@ -860,7 +865,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const theme = useStore(state => state.theme);
const languagePreference = useStore((state) => state.languagePreference);
void languagePreference;
const appearance = useStore(state => state.appearance);
const darkMode = theme === 'dark';
const isV2Ui = appearance.uiVersion === 'v2';
const sqlFormatOptions = useStore(state => state.sqlFormatOptions);

View File

@@ -682,11 +682,25 @@ export const splitCompletionSchemaAndTable = (qualified: string): { schema: stri
export const DEFAULT_QUERY_TEMPLATE = 'SELECT * FROM ';
export const resolveNewQueryDefaultTemplate = (
template: string | null | undefined,
): string => {
if (template === null || template === undefined) {
return DEFAULT_QUERY_TEMPLATE;
}
return String(template)
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
};
export const getTabQueryValue = (tab: TabData): string => (
typeof tab.query === 'string' ? tab.query : ''
);
export const getInitialEditorQuery = (tab: TabData): string => {
export const getInitialEditorQuery = (
tab: TabData,
defaultQueryTemplate: string | null | undefined = DEFAULT_QUERY_TEMPLATE,
): string => {
if (hasQueryTabDraft(tab.id)) {
return getQueryTabDraft(tab.id);
}
@@ -694,7 +708,7 @@ export const getInitialEditorQuery = (tab: TabData): string => {
if (tabQuery || tab.filePath || tab.savedQueryId || tab.readOnly) {
return tabQuery;
}
return DEFAULT_QUERY_TEMPLATE;
return resolveNewQueryDefaultTemplate(defaultQueryTemplate);
};
export const resolveNextResultSetIndex = (sets: Array<{ key?: string }>): number => {