diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 5377d615..6ffb9731 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -1842,6 +1842,7 @@ describe('QueryEditor external SQL save', () => { initialViewMode: 'fields', initialViewModeRequestId: expect.any(String), objectType: 'table', + returnToTabId: 'tab-1', }); expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'gonavi:locate-sidebar-object', @@ -1889,6 +1890,7 @@ describe('QueryEditor external SQL save', () => { initialViewMode: 'fields', initialViewModeRequestId: expect.any(String), objectType: 'table', + returnToTabId: 'tab-1', })); expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'gonavi:locate-sidebar-object', @@ -1958,6 +1960,7 @@ describe('QueryEditor external SQL save', () => { connectionId: 'conn-1', dbName: 'main', queryMode: 'object-edit', + returnToTabId: 'tab-1', query: expect.stringContaining('CREATE OR REPLACE PROCEDURE reporting.refresh_stats()'), })); expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ @@ -4297,6 +4300,7 @@ describe('QueryEditor external SQL save', () => { connectionId: 'conn-1', dbName: 'main', queryMode: 'object-edit', + returnToTabId: 'tab-1', query: expect.stringContaining('CREATE OR REPLACE VIEW reporting.active_users AS'), })); expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ @@ -4371,6 +4375,7 @@ describe('QueryEditor external SQL save', () => { connectionId: 'conn-1', dbName: 'main', queryMode: 'object-edit', + returnToTabId: 'tab-1', query: expect.stringContaining('CREATE TRIGGER audit.users_bi'), })); expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ @@ -4380,6 +4385,7 @@ describe('QueryEditor external SQL save', () => { connectionId: 'conn-1', dbName: 'main', queryMode: 'object-edit', + returnToTabId: 'tab-1', query: expect.stringContaining('CREATE OR REPLACE PROCEDURE reporting.refresh_stats()'), })); expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 9573eb12..28786c94 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -774,6 +774,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const addTab = useStore(state => state.addTab); const setActiveContext = useStore(state => state.setActiveContext); const updateQueryTabDraft = useStore(state => state.updateQueryTabDraft); + const activeTabId = useStore(state => state.activeTabId); const savedQueries = useStore(state => state.savedQueries); const currentConnectionIdRef = useRef(currentConnectionId); const currentDbRef = useRef(currentDb); @@ -2115,8 +2116,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc dbName: targetDbName, query: editSql, queryMode: 'object-edit', + returnToTabId: activeTabId || undefined, }); - }, [addTab]); + }, [activeTabId, addTab]); const openDefinitionObjectEditTab = useCallback(async ( navigationTarget: Extract, @@ -2204,8 +2206,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc objectLabel, ), queryMode: 'object-edit', + returnToTabId: activeTabId || undefined, }); - }, [addTab]); + }, [activeTabId, addTab]); const openTriggerObjectEditTab = useCallback(async ( navigationTarget: Extract, @@ -2240,8 +2243,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc dbName: targetDbName, query: buildEditableTriggerSql(targetTriggerName, latestDefinition, { translate }), queryMode: 'object-edit', + returnToTabId: activeTabId || undefined, }); - }, [addTab]); + }, [activeTabId, addTab]); // Setup Autocomplete and Editor const handleEditorDidMount: OnMount = (editor, monaco) => { @@ -2603,6 +2607,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc initialViewMode: 'fields', initialViewModeRequestId: String(Date.now()), objectType: 'table', + returnToTabId: activeTabId || undefined, }); return; } diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 4d47ca57..437cb8ec 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -1312,6 +1312,93 @@ describe('store appearance persistence', () => { }); }); + it('returns to the source tab after closing an object edit tab opened from a hyperlink', async () => { + const { useStore } = await importStore(); + + useStore.getState().addTab({ + id: 'query-source', + title: '查询 1', + type: 'query', + connectionId: 'conn-1', + dbName: 'sys', + query: 'select * from users;', + }); + useStore.getState().addTab({ + id: 'query-other-1', + title: '查询 2', + type: 'query', + connectionId: 'conn-1', + dbName: 'sys', + query: 'select 2;', + }); + useStore.getState().addTab({ + id: 'query-other-2', + title: '查询 3', + type: 'query', + connectionId: 'conn-1', + dbName: 'sys', + query: 'select 3;', + }); + useStore.getState().setActiveTab('query-source'); + useStore.getState().addTab({ + id: 'query-edit-object', + title: '修改对象', + type: 'query', + connectionId: 'conn-1', + dbName: 'sys', + query: 'CREATE OR REPLACE VIEW users_view AS SELECT * FROM users;', + queryMode: 'object-edit', + returnToTabId: 'query-source', + }); + + expect(useStore.getState().activeTabId).toBe('query-edit-object'); + + useStore.getState().closeTab('query-edit-object'); + + expect(useStore.getState().activeTabId).toBe('query-source'); + expect(useStore.getState().activeContext).toEqual({ + connectionId: 'conn-1', + dbName: 'sys', + }); + }); + + it('keeps the existing close fallback when the object edit source tab is gone', async () => { + const { useStore } = await importStore(); + + useStore.getState().addTab({ + id: 'query-source', + title: '查询 1', + type: 'query', + connectionId: 'conn-1', + dbName: 'sys', + query: 'select 1;', + }); + useStore.getState().addTab({ + id: 'query-other', + title: '查询 2', + type: 'query', + connectionId: 'conn-1', + dbName: 'sys', + query: 'select 2;', + }); + useStore.getState().addTab({ + id: 'query-edit-object', + title: '修改对象', + type: 'query', + connectionId: 'conn-1', + dbName: 'sys', + query: 'CREATE OR REPLACE VIEW users_view AS SELECT 1;', + queryMode: 'object-edit', + returnToTabId: 'query-source', + }); + useStore.getState().closeTab('query-source'); + useStore.getState().setActiveTab('query-edit-object'); + + useStore.getState().closeTab('query-edit-object'); + + expect(useStore.getState().activeTabId).toBe('query-other'); + }); + it('reuses the same table-export tab for the same connection and table identity', async () => { const { useStore } = await importStore(); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 38ce15b0..22df3d87 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1748,6 +1748,17 @@ const sanitizeActiveTabId = (activeTabId: unknown, tabs: TabData[]): string | nu return tabs[0]?.id || null; }; +const resolveCloseTabActiveTabId = ( + closedTab: TabData | undefined, + newTabs: TabData[], +): string | null => { + const returnToTabId = toTrimmedString(closedTab?.returnToTabId); + if (returnToTabId && newTabs.some((tab) => tab.id === returnToTabId)) { + return returnToTabId; + } + return newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null; +}; + const resolveActiveContextFromTab = ( tab: TabData | null | undefined, ): { connectionId: string; dbName: string } | null => { @@ -2918,11 +2929,11 @@ export const useStore = create()( closeTab: (id) => set((state) => { + const closedTab = state.tabs.find((t) => t.id === id); const newTabs = state.tabs.filter((t) => t.id !== id); let newActiveId = state.activeTabId; if (state.activeTabId === id) { - newActiveId = - newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null; + newActiveId = resolveCloseTabActiveTabId(closedTab, newTabs); } return { tabs: newTabs, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index e7d30957..6fef4876 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -468,6 +468,7 @@ export interface TabData { query?: string; resultPanelVisible?: boolean; queryMode?: "standard" | "object-edit"; + returnToTabId?: string; filePath?: string; initialTab?: string; initialViewMode?: "table" | "json" | "text" | "fields" | "ddl" | "er" | "sqlLog";