feat: snapshot issues with global tabs (#7942)

* chore: fix for sidebar state

* fix: global and sidebar state sync

* fix: re-priroritise how tab uid is synced
This commit is contained in:
Sid
2026-05-07 19:03:02 +05:30
committed by GitHub
parent f8bf1460bd
commit 415b75decb
9 changed files with 290 additions and 14 deletions

View File

@@ -64,7 +64,7 @@ const RequestTabPanel = () => {
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
const isRequestTab = focusedTab && ['http-request', 'grpc-request', 'ws-request', 'graphql-request'].includes(focusedTab.type);
const isRequestTab = focusedTab && ['request', 'http-request', 'grpc-request', 'ws-request', 'graphql-request'].includes(focusedTab.type);
useKeybinding('sendRequest', (e) => {
e?.preventDefault?.();
e?.stopPropagation?.();

View File

@@ -45,7 +45,11 @@ import CollectionItemIcon from './CollectionItemIcon';
import ExampleItem from './ExampleItem';
import ExampleIcon from 'components/Icons/ExampleIcon';
import { scrollToTheActiveTab } from 'utils/tabs';
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
import {
getTabUidForItem as getTabUidForItemSelector,
isTabForItemActive as isTabForItemActiveSelector,
isTabForItemPresent as isTabForItemPresentSelector
} from 'src/selectors/tab';
import { isEqual } from 'lodash';
import { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest';
import { calculateDraggedItemNewPathname, getInitialExampleName, findParentItemInCollection } from 'utils/collections/index';
@@ -60,12 +64,21 @@ import useKeybinding from 'hooks/useKeybinding';
const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
const { dropdownContainerRef } = useSidebarAccordion();
const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });
const selectorInput = {
itemUid: item.uid,
itemPathname: item.pathname,
collectionUid
};
const _isTabForItemActiveSelector = isTabForItemActiveSelector(selectorInput);
const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual);
const _isTabForItemPresentSelector = isTabForItemPresentSelector({ itemUid: item.uid });
const _isTabForItemPresentSelector = isTabForItemPresentSelector(selectorInput);
const isTabForItemPresent = useSelector(_isTabForItemPresentSelector, isEqual);
const _tabUidForItemSelector = getTabUidForItemSelector(selectorInput);
const tabUidForItem = useSelector(_tabUidForItemSelector, isEqual);
const isSidebarDragging = useSelector((state) => state.app.isDragging);
const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));
const { hasCopiedItems } = useSelector((state) => state.app.clipboard);
@@ -242,7 +255,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
if (isTabForItemPresent) {
dispatch(
focusTab({
uid: item.uid
uid: tabUidForItem || item.uid
})
);
return;
@@ -468,7 +481,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
}
const handleDoubleClick = (event) => {
dispatch(makeTabPermanent({ uid: item.uid }));
dispatch(makeTabPermanent({ uid: tabUidForItem || item.uid }));
};
// Sort items by their "seq" property.
@@ -547,7 +560,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const viewFolderSettings = () => {
if (isItemAFolder(item)) {
if (isTabForItemPresent) {
dispatch(focusTab({ uid: item.uid }));
dispatch(focusTab({ uid: tabUidForItem || item.uid }));
return;
}
dispatch(

View File

@@ -18,6 +18,28 @@ const tabTypeAlreadyExists = (tabs, collectionUid, type) => {
return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type);
};
const findTabByPathname = (tabs, { collectionUid, pathname, type, exampleName }) => {
if (!pathname || !collectionUid || !type) {
return null;
}
return find(tabs, (tab) => {
if (tab.collectionUid !== collectionUid) {
return false;
}
if (tab.pathname !== pathname || tab.type !== type) {
return false;
}
if (type === 'response-example') {
return tab.exampleName === exampleName;
}
return true;
});
};
export const tabsSlice = createSlice({
name: 'tabs',
initialState,
@@ -43,6 +65,12 @@ export const tabsSlice = createSlice({
return;
}
const existingPathnameTab = findTabByPathname(state.tabs, { collectionUid, pathname, type, exampleName });
if (existingPathnameTab) {
state.activeTabUid = existingPathnameTab.uid;
return;
}
if (nonReplaceableTabTypes.includes(type)) {
const existingTab = tabTypeAlreadyExists(state.tabs, collectionUid, type);
if (existingTab) {

View File

@@ -1,9 +1,55 @@
import { createSelector } from '@reduxjs/toolkit';
export const isTabForItemActive = ({ itemUid }) => createSelector([
(state) => state.tabs?.activeTabUid
], (activeTabUid) => activeTabUid === itemUid);
export const isTabForItemPresent = ({ itemUid }) => createSelector([
export const getTabUidForItem = ({ itemUid, itemPathname, collectionUid }) => createSelector([
(state) => state.tabs.tabs
], (tabs) => tabs.some((tab) => tab.uid === itemUid));
], (tabs) => {
const tabByUid = tabs.find((tab) => tab.uid === itemUid && (!collectionUid || tab.collectionUid === collectionUid));
if (tabByUid) {
return tabByUid.uid;
}
if (!itemPathname) {
return null;
}
const tabByPathname = tabs.find((tab) => tab.pathname === itemPathname && (!collectionUid || tab.collectionUid === collectionUid));
return tabByPathname?.uid || null;
});
export const isTabForItemActive = ({ itemUid, itemPathname, collectionUid }) => createSelector([
(state) => state.tabs?.activeTabUid,
(state) => state.tabs.tabs
], (activeTabUid, tabs) => {
if (!activeTabUid) {
return false;
}
const activeTab = tabs.find((tab) => tab.uid === activeTabUid);
if (!activeTab) {
return false;
}
if (collectionUid && activeTab.collectionUid !== collectionUid) {
return false;
}
if (activeTabUid === itemUid) {
return true;
}
if (!itemPathname) {
return false;
}
return activeTab.pathname === itemPathname;
});
export const isTabForItemPresent = ({ itemUid, itemPathname, collectionUid }) => createSelector([
(state) => state.tabs.tabs
], (tabs) => tabs.some((tab) => {
if (collectionUid && tab.collectionUid !== collectionUid) {
return false;
}
return tab.uid === itemUid || (itemPathname && tab.pathname === itemPathname);
}));

View File

@@ -11,6 +11,10 @@ const SINGLETON_TAB_TYPES = new Set([
'collection-settings',
'collection-overview',
'environment-settings',
'global-environment-settings',
'preferences',
'workspaceOverview',
'workspaceEnvironments',
'openapi-sync',
'openapi-spec'
]);
@@ -449,6 +453,12 @@ export const deserializeTab = (snapshotTab, collection) => {
scriptPaneTab: null
};
const isCollectionScopedSingleton = type === 'preferences'
|| type === 'environment-settings'
|| type === 'global-environment-settings';
const needsTypeBasedFallback = accessor === 'type' || (accessor === 'pathname' && !pathname && isCollectionScopedSingleton);
if (accessor === 'pathname' && pathname) {
const item = findItemInCollectionByPathname(collection, pathname);
tab.uid = item?.uid || pathname;
@@ -461,7 +471,7 @@ export const deserializeTab = (snapshotTab, collection) => {
tab.uid = example?.uid || `${pathname}::${exampleName}`;
tab.itemUid = item?.uid || pathname;
tab.exampleName = exampleName;
} else if (accessor === 'type') {
} else if (needsTypeBasedFallback) {
const collectionUidFromSnapshot = typeof snapshotTab.collection === 'string' && snapshotTab.collection.length > 0
? snapshotTab.collection
: (typeof snapshotTab.collectionUid === 'string' && snapshotTab.collectionUid.length > 0
@@ -470,6 +480,12 @@ export const deserializeTab = (snapshotTab, collection) => {
if (type === 'collection-settings') {
tab.uid = collectionUidFromSnapshot || collection.uid;
} else if (type === 'preferences') {
tab.uid = `${collection.uid}-preferences`;
} else if (type === 'environment-settings') {
tab.uid = `${collection.uid}-environment-settings`;
} else if (type === 'global-environment-settings') {
tab.uid = `${collection.uid}-global-environment-settings`;
} else if (NON_REPLACEABLE_SINGLETON_TAB_TYPES.has(type)) {
tab.uid = uuid();
} else {

View File

@@ -246,6 +246,39 @@ describe('deserializeTab', () => {
expect(secondTab.uid).not.toBe('variables');
expect(firstTab.uid).not.toBe(secondTab.uid);
});
it('restores preferences uid scoped to collection uid', () => {
const snapshotTab = {
type: 'preferences',
accessor: 'type',
permanent: true
};
const tab = deserializeTab(snapshotTab, collection);
expect(tab.uid).toBe('collection-uid-preferences');
});
it('restores global environment settings uid scoped to collection uid', () => {
const snapshotTab = {
type: 'global-environment-settings',
accessor: 'type',
permanent: true
};
const tab = deserializeTab(snapshotTab, collection);
expect(tab.uid).toBe('collection-uid-global-environment-settings');
});
it('falls back to type-based uid restore for collection-scoped singleton tabs missing pathname', () => {
const snapshotTab = {
type: 'preferences',
accessor: 'pathname',
permanent: true
};
const tab = deserializeTab(snapshotTab, collection);
expect(tab.uid).toBe('collection-uid-preferences');
});
});
describe('hydrateCollectionTabs', () => {