From ba28c626fd72c893f19f43b508c46d99b045ec49 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 23 Jul 2026 16:31:41 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20SQL=20=E4=BF=9D=E5=AD=98=E4=BB=A3=E7=A0=81=E9=94=99?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复保存队列与组件挂载状态的声明位置 - 修正草稿同步、保存完成判断和结果关闭逻辑结构 - 恢复保存查询回归用例并补齐异步等待 --- .../QueryEditor.external-sql-save.test.tsx | 140 +++++++++--------- frontend/src/components/QueryEditor.tsx | 20 +-- 2 files changed, 80 insertions(+), 80 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 45b2a9ca..afef0ebe 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -7862,6 +7862,13 @@ describe('QueryEditor external SQL save', () => { createdAt: 100, }, ]; + storeState.saveQuery.mockImplementationOnce(async (savedQuery: SavedQuery) => { + storeState.savedQueries = storeState.savedQueries.map((item) => ( + item.id === savedQuery.id ? savedQuery : item + )); + storeSubscribers.forEach((subscriber) => subscriber()); + return savedQuery; + }); let renderer!: ReactTestRenderer; await act(async () => { @@ -7875,7 +7882,7 @@ describe('QueryEditor external SQL save', () => { expect(getQueryTabDraft('tab-1')).toBe('select 3;'); await act(async () => { - findButton(renderer!, '保存').props.onClick(); + await findButton(renderer!, '保存').props.onClick(); }); expect(backendApp.WriteSQLFile).not.toHaveBeenCalled(); @@ -7887,75 +7894,6 @@ describe('QueryEditor external SQL save', () => { dbName: 'main', createdAt: 100, })); - }); - - it('keeps untitled fallback when the new query tab title is localized', async () => { - setCurrentLanguage('en-US'); - - let renderer!: ReactTestRenderer; - await act(async () => { - renderer = create(); - }); - - editorState.value = 'select 8;'; - - await act(async () => { - findButton(renderer!, 'Save').props.onClick(); - }); - - expect(storeState.saveQuery).toHaveBeenCalledWith(expect.objectContaining({ - id: 'saved-1', - name: 'Untitled query', - sql: 'select 8;', - connectionId: 'conn-1', - dbName: 'main', - })); - }); - - it('keeps untitled fallback after a language switch when the tab title came from another locale', async () => { - setCurrentLanguage('ja-JP'); - - let renderer!: ReactTestRenderer; - await act(async () => { - renderer = create(); - }); - - editorState.value = 'select 10;'; - - await act(async () => { - findButton(renderer!, '保存').props.onClick(); - }); - - expect(storeState.saveQuery).toHaveBeenCalledWith(expect.objectContaining({ - id: 'saved-1', - storeState.saveQuery.mockImplementationOnce(async (savedQuery: SavedQuery) => { - storeState.savedQueries = storeState.savedQueries.map((item) => ( - item.id === savedQuery.id ? savedQuery : item - )); - storeSubscribers.forEach((subscriber) => subscriber()); - return savedQuery; - }); - name: '無題のクエリ', - sql: 'select 10;', - connectionId: 'conn-1', - dbName: 'main', - })); - }); - - it('keeps untitled fallback for database-scoped new query titles after a language switch', async () => { - setCurrentLanguage('ja-JP'); - - let renderer!: ReactTestRenderer; - await act(async () => { - renderer = create(); - }); - - editorState.value = 'select 11;'; - - await act(async () => { - await findButton(renderer!, '保存').props.onClick(); - }); - expect(getQueryTabDraft('tab-1')).toBe(''); }); @@ -8165,6 +8103,68 @@ describe('QueryEditor external SQL save', () => { }); expect(getQueryTabDraft('tab-1')).toBe('select 3;'); + }); + + it('keeps untitled fallback when the new query tab title is localized', async () => { + setCurrentLanguage('en-US'); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + editorState.value = 'select 8;'; + + await act(async () => { + findButton(renderer!, 'Save').props.onClick(); + }); + + expect(storeState.saveQuery).toHaveBeenCalledWith(expect.objectContaining({ + id: 'saved-1', + name: 'Untitled query', + sql: 'select 8;', + connectionId: 'conn-1', + dbName: 'main', + })); + }); + + it('keeps untitled fallback after a language switch when the tab title came from another locale', async () => { + setCurrentLanguage('ja-JP'); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + editorState.value = 'select 10;'; + + await act(async () => { + findButton(renderer!, '保存').props.onClick(); + }); + + expect(storeState.saveQuery).toHaveBeenCalledWith(expect.objectContaining({ + id: 'saved-1', + name: '無題のクエリ', + sql: 'select 10;', + connectionId: 'conn-1', + dbName: 'main', + })); + }); + + it('keeps untitled fallback for database-scoped new query titles after a language switch', async () => { + setCurrentLanguage('ja-JP'); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + editorState.value = 'select 11;'; + + await act(async () => { + findButton(renderer!, '保存').props.onClick(); + }); + expect(storeState.saveQuery).toHaveBeenCalledWith(expect.objectContaining({ id: 'saved-1', name: '無題のクエリ', diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 52167f71..2ea8528b 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -1346,12 +1346,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const toggleQueryResultsPanelActionRef = useRef(null); const lastExternalQueryRef = useRef(getTabQueryValue(tab)); const lastLocalQueryRef = useRef(query); + const saveOperationQueueRef = useRef>(Promise.resolve()); + const queryEditorMountedRef = useRef(true); const imeCompositionFallbackRef = useRef<{ editor: any; valueBefore: string; selectionBefore: any; - const saveOperationQueueRef = useRef>(Promise.resolve()); - const queryEditorMountedRef = useRef(true); positionBefore: { lineNumber: number; column: number } | null; committedText: string; } | null>(null); @@ -1869,10 +1869,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc return savedQueries.find((item) => item.id === tabId) || null; }, [savedQueries, tab.id, tab.savedQueryId]); - const syncQueryDraft = useCallback((nextQuery: string) => { - const next = String(nextQuery ?? ''); - lastLocalQueryRef.current = next; - persistQueryTabDraftSnapshot(draftSnapshotTab, next, { useEffect(() => { queryEditorMountedRef.current = true; return () => { @@ -1889,6 +1885,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc return queued; }, []); + const syncQueryDraft = useCallback((nextQuery: string) => { + const next = String(nextQuery ?? ''); + lastLocalQueryRef.current = next; + persistQueryTabDraftSnapshot(draftSnapshotTab, next, { connectionId: currentConnectionIdRef.current, dbName: currentDbRef.current, }); @@ -8238,6 +8238,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const persistQuery = async (payload: { id: string; name: string; createdAt?: number }): Promise => { const sql = getCurrentQuery(); + lastLocalQueryRef.current = sql; const saved = { id: payload.id, name: payload.name, @@ -8284,7 +8285,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const openSaveQueryModal = (mode: 'save' | 'rename') => { setSaveModalMode(mode); - lastLocalQueryRef.current = sql; saveForm.setFieldsValue({ name: currentSavedQuery?.name || resolveDefaultQueryName() }); setIsSaveModalOpen(true); }; @@ -8665,6 +8665,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc name: String(values.name || '').trim() || translate('query_editor.save_modal.unnamed'), createdAt: existed?.createdAt, }); + if (!applied) { + return; + } message.success(translate( saveModalMode === 'rename' ? 'query_editor.message.renamed' : 'query_editor.message.saved' )); @@ -8688,9 +8691,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc currentResultSets, activeResultKeyRef.current, isV2Ui, - if (!applied) { - return; - } ); const nextResultSets = currentResultSets.filter(result => result.key !== key); const nextActiveKey = currentActiveKey && currentActiveKey !== key