feat(query-editor): 支持还原 SQL 美化前内容

- 美化 SQL 前保存最近一次原始内容快照
- 在格式设置菜单提供还原上次美化入口
- 持久化查询标签页的美化恢复快照
- 补充编辑器与状态恢复回归测试
This commit is contained in:
Syngnat
2026-06-16 07:10:04 +08:00
parent 682017ba96
commit 23f95d7dc8
5 changed files with 140 additions and 3 deletions

View File

@@ -1527,6 +1527,43 @@ describe('QueryEditor external SQL save', () => {
}),
]),
);
expect(storeState.updateQueryTabDraft).toHaveBeenCalledWith('tab-1', {
formatRestoreSnapshot: {
query: 'select * from users where id=1',
createdAt: expect.any(Number),
},
});
});
it('restores the last pre-beautify SQL snapshot after reopening a query tab', async () => {
let renderer!: ReactTestRenderer;
const originalSql = 'select * from users where id=1';
await act(async () => {
renderer = create(
<QueryEditor
tab={createTab({
query: 'SELECT\n *\nFROM\n users\nWHERE\n id = 1',
formatRestoreSnapshot: {
query: originalSql,
createdAt: 123,
},
})}
/>,
);
});
const restoreButton = findButton(renderer, '还原上次美化');
await act(async () => {
await restoreButton.props.onClick();
});
expect(editorState.value).toBe(originalSql);
expect(storeState.updateQueryTabDraft).toHaveBeenCalledWith('tab-1', {
query: originalSql,
formatRestoreSnapshot: undefined,
});
expect(messageApi.success).toHaveBeenCalledWith('已还原到美化前 SQL');
});
it('formats postgres window-function SQL with cast syntax through Monaco edits', async () => {

View File

@@ -3948,12 +3948,22 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
? connectionsRef.current.find(c => c.id === tabConnectionId)
: undefined);
const formatterLanguage = resolveQueryEditorFormatterLanguage(conn);
const formatted = format(getCurrentQuery(), { language: formatterLanguage, keywordCase: sqlFormatOptions.keywordCase });
const sourceSql = getCurrentQuery();
const formatted = format(sourceSql, { language: formatterLanguage, keywordCase: sqlFormatOptions.keywordCase });
if (sourceSql === formatted) {
return;
}
updateQueryTabDraft(tab.id, {
formatRestoreSnapshot: {
query: sourceSql,
createdAt: Date.now(),
},
});
const editor = editorRef.current;
const monaco = monacoRef.current;
const model = editor?.getModel?.();
if (editor && monaco && model) {
const currentValue = String(model.getValue?.() || '');
const currentValue = String(model.getValue?.() || sourceSql);
if (currentValue === formatted) {
return;
}
@@ -3977,6 +3987,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
};
const handleRestoreLastFormat = () => {
const previousQuery = tab.formatRestoreSnapshot?.query;
if (!previousQuery) {
void message.info('没有可还原的美化前 SQL');
return;
}
syncQueryToEditor(previousQuery);
updateQueryTabDraft(tab.id, {
query: previousQuery,
formatRestoreSnapshot: undefined,
});
refreshObjectDecorations();
void message.success('已还原到美化前 SQL');
};
const handleAIAction = (action: 'generate' | 'explain' | 'optimize' | 'schema') => {
const editor = editorRef.current;
const selection = editor?.getModel()?.getValueInRange(editor.getSelection()) || '';
@@ -4013,6 +4038,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
onClick: () => setSqlFormatOptions({ keywordCase: 'lower' })
},
{ type: 'divider' },
{
key: 'restore-last-format',
label: '还原上次美化',
disabled: !tab.formatRestoreSnapshot?.query,
onClick: handleRestoreLastFormat,
},
{ type: 'divider' },
{
key: 'snippet-settings',
label: '代码片段管理...',

View File

@@ -879,6 +879,10 @@ describe('store appearance persistence', () => {
query: 'select * from orders where status = "paid";',
connectionId: 'conn-2',
dbName: 'reporting',
formatRestoreSnapshot: {
query: 'select * from orders where status="paid";',
createdAt: 123,
},
});
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
@@ -890,6 +894,10 @@ describe('store appearance persistence', () => {
connectionId: 'conn-2',
dbName: 'reporting',
query: 'select * from orders where status = "paid";',
formatRestoreSnapshot: {
query: 'select * from orders where status="paid";',
createdAt: 123,
},
}),
]);
expect(persisted.state.activeTabId).toBe('query-tab-1');
@@ -903,9 +911,19 @@ describe('store appearance persistence', () => {
connectionId: 'conn-2',
dbName: 'reporting',
query: 'select * from orders where status = "paid";',
formatRestoreSnapshot: {
query: 'select * from orders where status="paid";',
createdAt: 123,
},
}),
]);
expect(reloaded.useStore.getState().activeTabId).toBe('query-tab-1');
reloaded.useStore.getState().updateQueryTabDraft('query-tab-1', {
formatRestoreSnapshot: undefined,
});
expect(reloaded.useStore.getState().tabs[0].formatRestoreSnapshot).toBeUndefined();
});
it('updates activeContext when switching between tabs with different host or database', async () => {

View File

@@ -1279,7 +1279,12 @@ interface AppState {
draft: Partial<
Pick<
TabData,
"query" | "connectionId" | "dbName" | "title" | "resultPanelVisible"
| "query"
| "connectionId"
| "dbName"
| "title"
| "resultPanelVisible"
| "formatRestoreSnapshot"
>
>,
) => void;
@@ -1483,6 +1488,15 @@ const sanitizeQueryTabs = (value: unknown): TabData[] => {
const query = typeof raw.query === "string" ? raw.query.slice(0, MAX_PERSISTED_QUERY_LENGTH) : "";
const filePath = toTrimmedString(raw.filePath);
const savedQueryId = toTrimmedString(raw.savedQueryId);
const rawFormatRestoreSnapshot =
raw.formatRestoreSnapshot && typeof raw.formatRestoreSnapshot === "object"
? (raw.formatRestoreSnapshot as Record<string, unknown>)
: null;
const formatRestoreQuery =
typeof rawFormatRestoreSnapshot?.query === "string"
? rawFormatRestoreSnapshot.query.slice(0, MAX_PERSISTED_QUERY_LENGTH)
: "";
const formatRestoreCreatedAt = Number(rawFormatRestoreSnapshot?.createdAt);
if (!query.trim() && !filePath && !savedQueryId) return;
let id = toTrimmedString(raw.id, `query-${index + 1}`) || `query-${index + 1}`;
@@ -1505,6 +1519,14 @@ const sanitizeQueryTabs = (value: unknown): TabData[] => {
filePath: filePath || undefined,
savedQueryId: savedQueryId || undefined,
readOnly: raw.readOnly === true,
formatRestoreSnapshot: formatRestoreQuery
? {
query: formatRestoreQuery,
createdAt: Number.isFinite(formatRestoreCreatedAt)
? formatRestoreCreatedAt
: Date.now(),
}
: undefined,
});
});
@@ -2560,6 +2582,30 @@ export const useStore = create<AppState>()(
changed = true;
}
}
if (Object.prototype.hasOwnProperty.call(draft, "formatRestoreSnapshot")) {
const rawSnapshot = draft.formatRestoreSnapshot;
const nextSnapshot =
rawSnapshot && typeof rawSnapshot.query === "string"
? {
query: rawSnapshot.query.slice(0, MAX_PERSISTED_QUERY_LENGTH),
createdAt: Number.isFinite(Number(rawSnapshot.createdAt))
? Number(rawSnapshot.createdAt)
: Date.now(),
}
: undefined;
const currentSnapshot = nextTab.formatRestoreSnapshot;
if (
currentSnapshot?.query !== nextSnapshot?.query ||
currentSnapshot?.createdAt !== nextSnapshot?.createdAt
) {
if (nextSnapshot?.query) {
nextTab.formatRestoreSnapshot = nextSnapshot;
} else {
delete nextTab.formatRestoreSnapshot;
}
changed = true;
}
}
return nextTab;
});

View File

@@ -436,6 +436,10 @@ export interface TabData {
sidebarLocateKey?: string; // Precise sidebar tree key for locating an object node
savedQueryId?: string; // Saved query identity for quick-save behavior
objectType?: 'table' | 'view' | 'materialized-view'; // Table-like object type for shared viewers
formatRestoreSnapshot?: {
query: string;
createdAt: number;
}; // Last SQL content before beautify, for cross-session restore
}
export interface JVMAIPlanContext {