From 23f95d7dc80fec5e6106ada95c6fdca1162ae894 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 16 Jun 2026 07:10:04 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(query-editor):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=BF=98=E5=8E=9F=20SQL=20=E7=BE=8E=E5=8C=96=E5=89=8D?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 美化 SQL 前保存最近一次原始内容快照 - 在格式设置菜单提供还原上次美化入口 - 持久化查询标签页的美化恢复快照 - 补充编辑器与状态恢复回归测试 --- .../QueryEditor.external-sql-save.test.tsx | 37 ++++++++++++++ frontend/src/components/QueryEditor.tsx | 36 +++++++++++++- frontend/src/store.test.ts | 18 +++++++ frontend/src/store.ts | 48 ++++++++++++++++++- frontend/src/types.ts | 4 ++ 5 files changed, 140 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index bb9f173..075d0ef 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -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( + , + ); + }); + + 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 () => { diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 0b8023a..c8e8306 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -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: '代码片段管理...', diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 4b1a3be..346e8b9 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -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 () => { diff --git a/frontend/src/store.ts b/frontend/src/store.ts index fefd976..b6b772b 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -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) + : 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()( 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; }); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 4fcd731..88927f6 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -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 {