Files
bruno/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
Sid 20f4e4263a feat: ui state snapshots (#7794)
* feat(snapshot): add session snapshot persistence and restoration

- Add snapshot middleware to persist UI state (tabs, workspaces, environments)
- Add SnapshotManager service in electron for atomic snapshot storage
- Add accessor-based tab serialization using pathname for reliable restoration
- Add loading states for tabs while collections are mounting
- Add hydrateTabs to restore tabs from snapshots on app load
- Add devTools state persistence (console open/height/tab)

* fix(snapshot): preserve unloaded collections and fix async serialization

Make serializeSnapshot async to fetch existing snapshot before saving,
ensuring collections not currently loaded in Redux are preserved. Fix
activeTab serialization to pass collection object instead of just UID.

* refactor(snapshot): rewrite storage to map-based schema with granular IPC

Replace array-based snapshot storage with key-value maps keyed by pathname
for O(1) lookups. Separate tabs into their own top-level map, decoupled
from collections.

- Rewrite SnapshotManager with map-based schema and granular read/write
  methods (getWorkspace, getTabs, setCollection, removeWorkspace, etc.)
- Add 12 granular IPC handlers (renderer:snapshot:*) replacing 4 coarse ones
- Update middleware serialization to produce maps; remove activeCollectionUidCache
  in favor of lastActiveCollectionPathname on workspace objects
- Fix mountCollection passing collectionUid instead of collection to restoreTabs
- Preserve non-active workspace state from existing snapshot on save

* wip

* refactor: allow migration of old snapshot

* refactor: trim down redundancy

* fix: for workspace state

* feat: fix for finalised schema

* fix: schema cleanup

* chore: simplify

* chore: wait on hydration to finish

* chore: snapshot changes to schema

* chore: fix typo

* fix: switch schema for saving and restoring extras

* chore: correctness changes

* chore: add active in the schema check

* chore: fix lint

* chore: comments

* chore: make writes async

* fix: sorting and cross workspace shared collection fixes

* chore: add version

* fix: wait on hydration

* chore: fix backward compat for ui-snapshots

* chore: dead code removal

* Fix optional chaining in snapshot lookup

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-05-06 17:42:46 +05:30

779 lines
27 KiB
JavaScript

import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';
import get from 'lodash/get';
import { makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { saveRequest, saveCollectionRoot, saveFolderRoot, saveEnvironment, saveCollectionSettings, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import useKeybinding from 'hooks/useKeybinding';
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
import { clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { useTheme } from 'providers/Theme';
import { useDispatch, useSelector } from 'react-redux';
import { findItemInCollection, findItemInCollectionByPathname, hasRequestChanges, areItemsLoading } from 'utils/collections';
import ConfirmRequestClose from './ConfirmRequestClose';
import ConfirmCollectionClose from './ConfirmCollectionClose';
import ConfirmFolderClose from './ConfirmFolderClose';
import ConfirmCloseEnvironment from 'components/Environments/ConfirmCloseEnvironment';
import RequestTabNotFound from './RequestTabNotFound';
import RequestTabLoading from './RequestTabLoading';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
import MenuDropdown from 'ui/MenuDropdown';
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
import NewRequest from 'components/Sidebar/NewRequest/index';
import GradientCloseButton from './GradientCloseButton';
import { flattenItems } from 'utils/collections/index';
import { closeWsConnection } from 'utils/network/index';
import { getInvalidVariableNames } from 'utils/common/variables';
import ExampleTab from '../ExampleTab';
import toast from 'react-hot-toast';
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow, dropdownContainerRef }) => {
const dispatch = useDispatch();
const { theme } = useTheme();
const tabNameRef = useRef(null);
const tabLabelRef = useRef(null);
const lastOverflowStateRef = useRef(null);
const [showConfirmClose, setShowConfirmClose] = useState(false);
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false);
const [showConfirmEnvironmentClose, setShowConfirmEnvironmentClose] = useState(false);
const [showConfirmGlobalEnvironmentClose, setShowConfirmGlobalEnvironmentClose] = useState(false);
const menuDropdownRef = useRef();
let item = findItemInCollection(collection, tab.uid);
if (!item && tab.pathname) {
item = findItemInCollectionByPathname(collection, tab.pathname);
}
const method = useMemo(() => {
if (!item) return;
switch (item.type) {
case 'grpc-request':
return 'gRPC';
case 'ws-request':
return 'WS';
case 'graphql-request':
return 'GQL';
default:
return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
}
}, [item]);
const hasChanges = useMemo(() => hasRequestChanges(item), [item]);
const isItemsLoading = useMemo(() => {
return collection?.mountStatus === 'mounting' || areItemsLoading(collection);
}, [collection?.mountStatus, collection]);
const isWS = item?.type === 'ws-request';
useEffect(() => {
if (!item || !tabNameRef.current || !setHasOverflow) return;
const checkOverflow = () => {
if (tabNameRef.current && setHasOverflow) {
const hasOverflow = tabNameRef.current.scrollWidth > tabNameRef.current.clientWidth;
if (lastOverflowStateRef.current !== hasOverflow) {
lastOverflowStateRef.current = hasOverflow;
setHasOverflow(hasOverflow);
}
}
};
const timeoutId = setTimeout(checkOverflow, 0);
const resizeObserver = new ResizeObserver(() => {
checkOverflow();
});
if (tabNameRef.current) {
resizeObserver.observe(tabNameRef.current);
}
return () => {
clearTimeout(timeoutId);
resizeObserver.disconnect();
};
}, [item, item?.name, method, setHasOverflow]);
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
};
const handleRightClick = (event) => {
event.preventDefault();
event.stopPropagation();
menuDropdownRef.current?.show();
};
// Prevent the browser's autoscroll (triggered on middle-button mousedown)
const handleMouseDown = (e) => {
if (e.button === 1) {
e.preventDefault();
}
};
const handleMouseUp = (e) => {
if (e.button === 1) {
e.preventDefault();
e.stopPropagation();
// Close the tab
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
}
};
const getMethodColor = (method = '') => {
const colorMap = {
...theme.request.methods,
...theme.request
};
return colorMap[method.toLocaleLowerCase()];
};
const handleCloseCollectionSettings = (event) => {
if (!collection.draft) {
return handleCloseClick(event);
}
event.stopPropagation();
event.preventDefault();
setShowConfirmCollectionClose(true);
};
let folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (!folder && tab.type === 'folder-settings' && tab.pathname) {
folder = findItemInCollectionByPathname(collection, tab.pathname);
}
const handleCloseFolderSettings = (event) => {
if (!folder?.draft) {
return handleCloseClick(event);
}
event.stopPropagation();
event.preventDefault();
setShowConfirmFolderClose(true);
};
const specialTabs = [
'collection-overview',
'collection-settings',
'folder-settings',
'variables',
'collection-runner',
'environment-settings',
'global-environment-settings',
'preferences',
'workspaceOverview',
'workspaceEnvironments',
'openapi-sync',
'openapi-spec'
];
const hasDraft = tab.type === 'collection-settings' && collection?.draft;
const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft;
const hasEnvironmentDraft = tab.type === 'environment-settings' && collection?.environmentsDraft;
const globalEnvironmentDraft = useSelector((state) => state.globalEnvironments.globalEnvironmentDraft);
const hasGlobalEnvironmentDraft = tab.type === 'global-environment-settings' && globalEnvironmentDraft;
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isActive = tab.uid === activeTabUid;
// Close tab shortcut — draft-aware, only active for the focused tab
useKeybinding('closeTab', () => {
if (tab.type === 'request' || tab.type === 'grpc-request' || tab.type === 'ws-request' || tab.type === 'graphql-request') {
if (hasChanges) {
setShowConfirmClose(true);
} else {
if (item?.type === 'ws-request') {
closeWsConnection(item.uid);
}
dispatch(closeTabs({ tabUids: [tab.uid] }));
}
} else if (tab.type === 'collection-settings') {
if (collection?.draft) {
setShowConfirmCollectionClose(true);
} else {
dispatch(closeTabs({ tabUids: [tab.uid] }));
}
} else if (tab.type === 'folder-settings') {
if (folder?.draft) {
setShowConfirmFolderClose(true);
} else {
dispatch(closeTabs({ tabUids: [tab.uid] }));
}
} else if (tab.type === 'environment-settings') {
if (collection?.environmentsDraft) {
setShowConfirmEnvironmentClose(true);
} else {
dispatch(closeTabs({ tabUids: [tab.uid] }));
}
} else if (tab.type === 'global-environment-settings') {
if (globalEnvironmentDraft) {
setShowConfirmGlobalEnvironmentClose(true);
} else {
dispatch(closeTabs({ tabUids: [tab.uid] }));
}
} else {
dispatch(closeTabs({ tabUids: [tab.uid] }));
}
return false;
}, { enabled: isActive, deps: [isActive, tab, hasChanges, item, collection, folder, globalEnvironmentDraft] });
// Save shortcut — tab-type-aware, only active for the focused tab
useKeybinding('save', () => {
if (tab.type === 'environment-settings') {
if (collection?.environmentsDraft) {
const { environmentUid, variables } = collection.environmentsDraft;
if (environmentUid?.startsWith('dotenv:')) {
window.dispatchEvent(new Event('dotenv-save'));
} else {
dispatch(saveEnvironment(variables, environmentUid, collection.uid));
}
}
} else if (tab.type === 'global-environment-settings') {
if (globalEnvironmentDraft) {
const { environmentUid, variables } = globalEnvironmentDraft;
dispatch(saveGlobalEnvironment({ variables, environmentUid }));
}
} else if (tab.type === 'folder-settings') {
if (folder) {
dispatch(saveFolderRoot(collection.uid, folder.uid));
}
} else if (tab.type === 'collection-settings') {
dispatch(saveCollectionSettings(collection.uid));
} else if (item && item.uid) {
dispatch(saveRequest(tab.uid, tab.collectionUid));
}
return false;
}, { enabled: isActive, deps: [isActive, tab, item, collection, folder, globalEnvironmentDraft] });
const handleCloseEnvironmentSettings = (event) => {
if (!collection?.environmentsDraft) {
return handleCloseClick(event);
}
event.stopPropagation();
event.preventDefault();
setShowConfirmEnvironmentClose(true);
};
const handleCloseGlobalEnvironmentSettings = (event) => {
if (!globalEnvironmentDraft) {
return handleCloseClick(event);
}
event.stopPropagation();
event.preventDefault();
setShowConfirmGlobalEnvironmentClose(true);
};
if (specialTabs.includes(tab.type)) {
return (
<StyledWrapper
className={`flex items-center justify-between tab-container px-2 ${tab.preview ? 'italic' : ''}`}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
{showConfirmCollectionClose && tab.type === 'collection-settings' && (
<ConfirmCollectionClose
collection={collection}
onCancel={() => setShowConfirmCollectionClose(false)}
onCloseWithoutSave={() => {
dispatch(deleteCollectionDraft({
collectionUid: collection.uid
}));
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmCollectionClose(false);
}}
onSaveAndClose={() => {
dispatch(saveCollectionRoot(collection.uid))
.then(() => {
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmCollectionClose(false);
})
.catch((err) => {
console.log('err', err);
});
}}
/>
)}
{showConfirmFolderClose && tab.type === 'folder-settings' && (
<ConfirmFolderClose
folder={folder}
onCancel={() => setShowConfirmFolderClose(false)}
onCloseWithoutSave={() => {
dispatch(deleteFolderDraft({
collectionUid: collection.uid,
folderUid: folder.uid
}));
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmFolderClose(false);
}}
onSaveAndClose={() => {
dispatch(saveFolderRoot(collection.uid, folder.uid))
.then(() => {
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmFolderClose(false);
})
.catch((err) => {
console.log('err', err);
});
}}
/>
)}
{showConfirmEnvironmentClose && tab.type === 'environment-settings' && (
<ConfirmCloseEnvironment
isGlobal={false}
isDotEnv={collection.environmentsDraft?.environmentUid?.startsWith('dotenv:')}
onCancel={() => setShowConfirmEnvironmentClose(false)}
onCloseWithoutSave={() => {
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
dispatch(closeTabs({ tabUids: [tab.uid] }));
setShowConfirmEnvironmentClose(false);
}}
onSaveAndClose={() => {
const draft = collection.environmentsDraft;
if (draft?.environmentUid?.startsWith('dotenv:')) {
const onSuccess = () => {
cleanup();
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
dispatch(closeTabs({ tabUids: [tab.uid] }));
setShowConfirmEnvironmentClose(false);
};
const onFailed = () => {
cleanup();
setShowConfirmEnvironmentClose(false);
};
const cleanup = () => {
window.removeEventListener('dotenv-save-complete', onSuccess);
window.removeEventListener('dotenv-save-failed', onFailed);
};
window.addEventListener('dotenv-save-complete', onSuccess, { once: true });
window.addEventListener('dotenv-save-failed', onFailed, { once: true });
window.dispatchEvent(new Event('dotenv-save'));
} else if (draft?.environmentUid && draft?.variables) {
const invalidNames = getInvalidVariableNames(draft.variables);
if (invalidNames.length > 0) {
toast.error(`Invalid variable name(s): ${invalidNames.join(', ')}`);
return;
}
dispatch(saveEnvironment(draft.variables, draft.environmentUid, collection.uid))
.then(() => {
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
dispatch(closeTabs({ tabUids: [tab.uid] }));
setShowConfirmEnvironmentClose(false);
toast.success('Environment saved');
})
.catch((err) => {
console.log('err', err);
toast.error('Failed to save environment');
});
}
}}
/>
)}
{showConfirmGlobalEnvironmentClose && tab.type === 'global-environment-settings' && (
<ConfirmCloseEnvironment
isGlobal={true}
isDotEnv={globalEnvironmentDraft?.environmentUid?.startsWith('dotenv:')}
onCancel={() => setShowConfirmGlobalEnvironmentClose(false)}
onCloseWithoutSave={() => {
dispatch(clearGlobalEnvironmentDraft());
dispatch(closeTabs({ tabUids: [tab.uid] }));
setShowConfirmGlobalEnvironmentClose(false);
}}
onSaveAndClose={() => {
const draft = globalEnvironmentDraft;
if (draft?.environmentUid?.startsWith('dotenv:')) {
const onSuccess = () => {
cleanup();
dispatch(clearGlobalEnvironmentDraft());
dispatch(closeTabs({ tabUids: [tab.uid] }));
setShowConfirmGlobalEnvironmentClose(false);
};
const onFailed = () => {
cleanup();
setShowConfirmGlobalEnvironmentClose(false);
};
const cleanup = () => {
window.removeEventListener('dotenv-save-complete', onSuccess);
window.removeEventListener('dotenv-save-failed', onFailed);
};
window.addEventListener('dotenv-save-complete', onSuccess, { once: true });
window.addEventListener('dotenv-save-failed', onFailed, { once: true });
window.dispatchEvent(new Event('dotenv-save'));
} else if (draft?.environmentUid && draft?.variables) {
const invalidNames = getInvalidVariableNames(draft.variables);
if (invalidNames.length > 0) {
toast.error(`Invalid variable name(s): ${invalidNames.join(', ')}`);
return;
}
dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }))
.then(() => {
dispatch(clearGlobalEnvironmentDraft());
dispatch(closeTabs({ tabUids: [tab.uid] }));
setShowConfirmGlobalEnvironmentClose(false);
toast.success('Global environment saved');
})
.catch((err) => {
console.log('err', err);
toast.error('Failed to save global environment');
});
}
}}
/>
)}
{tab.type === 'folder-settings' && !folder ? (
tab.name && isItemsLoading
? <RequestTabLoading handleCloseClick={handleCloseClick} name={tab.name} />
: <RequestTabNotFound handleCloseClick={handleCloseClick} />
) : tab.type === 'folder-settings' ? (
<SpecialTab handleCloseClick={handleCloseFolderSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} hasDraft={hasFolderDraft} />
) : tab.type === 'collection-settings' ? (
<SpecialTab handleCloseClick={handleCloseCollectionSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={collection?.name} hasDraft={hasDraft} />
) : tab.type === 'environment-settings' ? (
<SpecialTab handleCloseClick={handleCloseEnvironmentSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} hasDraft={hasEnvironmentDraft} />
) : tab.type === 'global-environment-settings' ? (
<SpecialTab handleCloseClick={handleCloseGlobalEnvironmentSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} hasDraft={hasGlobalEnvironmentDraft} />
) : tab.type === 'workspaceOverview' ? (
<SpecialTab handleCloseClick={null} type={tab.type} />
) : tab.type === 'workspaceEnvironments' ? (
<SpecialTab handleCloseClick={null} type={tab.type} />
) : (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
)}
</StyledWrapper>
);
}
// Handle response-example tabs specially
if (tab.type === 'response-example') {
return (
<ExampleTab
tab={tab}
collection={collection}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
folderUid={folderUid}
/>
);
}
if (!item) {
const showLoading = tab.name && isItemsLoading;
return (
<StyledWrapper
className="flex items-center justify-between tab-container px-2"
onMouseDown={handleMouseDown}
onMouseUp={(e) => {
if (e.button === 1) {
e.preventDefault();
e.stopPropagation();
dispatch(closeTabs({ tabUids: [tab.uid] }));
}
}}
>
{showLoading ? (
<RequestTabLoading handleCloseClick={handleCloseClick} name={tab.name} />
) : (
<RequestTabNotFound handleCloseClick={handleCloseClick} />
)}
</StyledWrapper>
);
}
return (
<StyledWrapper className="flex items-center justify-between tab-container px-2">
{showConfirmClose && (
<ConfirmRequestClose
item={item}
onCancel={() => setShowConfirmClose(false)}
onCloseWithoutSave={() => {
isWS && closeWsConnection(item.uid);
dispatch(
deleteRequestDraft({
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
}}
onSaveAndClose={() => {
dispatch(saveRequest(item.uid, collection.uid))
.then(() => {
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
})
.catch((err) => {
console.log('err', err);
});
}}
/>
)}
<div
ref={tabLabelRef}
className={`flex items-baseline tab-label ${tab.preview ? 'italic' : ''}`}
onContextMenu={handleRightClick}
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
onMouseDown={handleMouseDown}
onMouseUp={(e) => {
if (!hasChanges) return handleMouseUp(e);
if (e.button === 1) {
e.stopPropagation();
e.preventDefault();
setShowConfirmClose(true);
}
}}
>
<span className="tab-method uppercase" style={{ color: getMethodColor(method) }}>
{method}
</span>
<span ref={tabNameRef} className="ml-1 tab-name" title={item.name}>
{item.name}
</span>
<RequestTabMenu
menuDropdownRef={menuDropdownRef}
tabLabelRef={tabLabelRef}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
collection={collection}
dispatch={dispatch}
dropdownContainerRef={dropdownContainerRef}
/>
</div>
<GradientCloseButton
hasChanges={hasChanges}
onClick={(e) => {
if (!hasChanges) {
isWS && closeWsConnection(item.uid);
return handleCloseClick(e);
}
e.stopPropagation();
e.preventDefault();
setShowConfirmClose(true);
}}
/>
</StyledWrapper>
);
};
function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, tabIndex, collection, dispatch, dropdownContainerRef }) {
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
// Returns the tab-label's position for dropdown positioning.
// Returns zero-sized rect if element isn't mounted yet (prevents Tippy errors).
const getTabLabelRect = () => {
if (!tabLabelRef.current) {
return { width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0 };
}
return tabLabelRef.current.getBoundingClientRect();
};
const totalTabs = collectionRequestTabs.length || 0;
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
const currentTabItem = findItemInCollection(collection, currentTabUid);
const currentTabHasChanges = useMemo(() => hasRequestChanges(currentTabItem), [currentTabItem]);
const hasLeftTabs = tabIndex !== 0;
const hasRightTabs = totalTabs > tabIndex + 1;
const hasOtherTabs = totalTabs > 1;
async function handleCloseTab(tabUid) {
if (!tabUid) {
return;
}
try {
const item = findItemInCollection(collection, tabUid);
// silently save unsaved changes before closing the tab
if (hasRequestChanges(item)) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}
dispatch(closeTabs({ tabUids: [tabUid] }));
} catch (err) { }
}
function handleRevertChanges() {
if (!currentTabUid) {
return;
}
try {
const item = findItemInCollection(collection, currentTabUid);
if (item.draft) {
dispatch(deleteRequestDraft({
itemUid: item.uid,
collectionUid: collection.uid
}));
}
} catch (err) { }
}
async function handleCloseMultipleTabs(tabs) {
const tabUidsToClose = [];
for (const tab of tabs) {
const item = findItemInCollection(collection, tab.uid);
if (item && hasRequestChanges(item)) {
try {
await dispatch(saveRequest(item.uid, collection.uid, true));
} catch (err) {
continue;
}
}
if (tab?.uid) {
tabUidsToClose.push(tab.uid);
}
}
if (tabUidsToClose.length > 0) {
dispatch(closeTabs({ tabUids: tabUidsToClose }));
}
}
async function handleCloseOtherTabs() {
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
await handleCloseMultipleTabs(otherTabs);
}
async function handleCloseTabsToTheLeft() {
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
await handleCloseMultipleTabs(leftTabs);
}
async function handleCloseTabsToTheRight() {
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
await handleCloseMultipleTabs(rightTabs);
}
function handleCloseSavedTabs() {
const items = flattenItems(collection?.items);
const savedTabs = items?.filter?.((item) => !hasRequestChanges(item));
const savedTabIds = savedTabs?.map((item) => item.uid) || [];
dispatch(closeTabs({ tabUids: savedTabIds }));
}
async function handleCloseAllTabs() {
await handleCloseMultipleTabs(collectionRequestTabs);
}
const menuItems = useMemo(() => [
{
id: 'new-request',
label: 'New Request',
onClick: () => setShowAddNewRequestModal(true)
},
{
id: 'clone-request',
label: 'Clone Request',
onClick: () => setShowCloneRequestModal(true)
},
{
id: 'revert-changes',
label: 'Revert Changes',
onClick: handleRevertChanges,
disabled: !currentTabItem?.draft
},
{
id: 'close',
label: 'Close',
onClick: () => handleCloseTab(currentTabUid)
},
{
id: 'close-others',
label: 'Close Others',
onClick: handleCloseOtherTabs,
disabled: !hasOtherTabs
},
{
id: 'close-left',
label: 'Close to the Left',
onClick: handleCloseTabsToTheLeft,
disabled: !hasLeftTabs
},
{
id: 'close-right',
label: 'Close to the Right',
onClick: handleCloseTabsToTheRight,
disabled: !hasRightTabs
},
{
id: 'close-saved',
label: 'Close Saved',
onClick: handleCloseSavedTabs
},
{
id: 'close-all',
label: 'Close All',
onClick: handleCloseAllTabs
}
], [currentTabUid, currentTabItem, hasOtherTabs, hasLeftTabs, hasRightTabs, collection, collectionRequestTabs, tabIndex, dispatch]);
const menuDropdown = (
<MenuDropdown
ref={menuDropdownRef}
items={menuItems}
placement="bottom-start"
appendTo={dropdownContainerRef?.current || document.body}
getReferenceClientRect={getTabLabelRect}
>
<span></span>
</MenuDropdown>
);
return (
<Fragment>
{showAddNewRequestModal && (
<NewRequest collectionUid={collection.uid} onClose={() => setShowAddNewRequestModal(false)} />
)}
{showCloneRequestModal && (
<CloneCollectionItem
item={currentTabItem}
collectionUid={collection.uid}
onClose={() => setShowCloneRequestModal(false)}
/>
)}
{menuDropdown}
</Fragment>
);
}
export default RequestTab;