🐛 fix(tabs): 修复对象编辑页关闭后未回到来源标签

- 为 SQL 超链接打开的对象标签记录来源标签

- 关闭当前对象标签时优先回到来源标签

- 补充表对象与对象编辑标签关闭回退测试
This commit is contained in:
Syngnat
2026-06-28 20:03:15 +08:00
parent fee703e879
commit 730d4bb1ab
5 changed files with 115 additions and 5 deletions

View File

@@ -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({

View File

@@ -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<QueryEditorNavigationTarget, { type: 'view' | 'materialized-view' | 'sequence' | 'package' }>,
@@ -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<QueryEditorNavigationTarget, { type: 'trigger' }>,
@@ -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;
}

View File

@@ -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();

View File

@@ -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<AppState>()(
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,

View File

@@ -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";