mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 22:18:33 +00:00
797 lines
28 KiB
JavaScript
797 lines
28 KiB
JavaScript
import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';
|
|
import get from 'lodash/get';
|
|
import { makeTabPermanent, syncTabUid } 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);
|
|
}
|
|
|
|
useEffect(() => {
|
|
const isRequestType = tab.type === 'request'
|
|
|| tab.type === 'http-request'
|
|
|| tab.type === 'graphql-request'
|
|
|| tab.type === 'grpc-request'
|
|
|| tab.type === 'ws-request';
|
|
|
|
if (!isRequestType || !tab.pathname || !item?.uid || tab.uid === item.uid) {
|
|
return;
|
|
}
|
|
|
|
dispatch(syncTabUid({ oldUid: tab.uid, newUid: item.uid }));
|
|
}, [dispatch, item?.uid, tab.pathname, tab.type, tab.uid]);
|
|
|
|
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;
|
|
if (environmentUid?.startsWith('dotenv:')) {
|
|
window.dispatchEvent(new Event('dotenv-save'));
|
|
} else {
|
|
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;
|