mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
feat: Increase visibility of text in Request tabs (#6243)
* refactor(RequestTabs): update tab width calculation and improve styling * refactor: replace close icon implementation with GradientCloseButton and adjust styles * changes: design * fix: failing tests * fixes * fixes: coderabbit * fixes * fixes * gradient color fix --------- Co-authored-by: naman-bruno <naman@usebruno.com> Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
@@ -8,8 +8,7 @@ import ExampleIcon from 'components/Icons/ExampleIcon';
|
||||
import ConfirmRequestClose from '../RequestTab/ConfirmRequestClose';
|
||||
import RequestTabNotFound from '../RequestTab/RequestTabNotFound';
|
||||
import StyledWrapper from '../RequestTab/StyledWrapper';
|
||||
import CloseTabIcon from '../RequestTab/CloseTabIcon';
|
||||
import DraftTabIcon from '../RequestTab/DraftTabIcon';
|
||||
import GradientCloseButton from '../RequestTab/GradientCloseButton';
|
||||
|
||||
const ExampleTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -59,7 +58,7 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
if (!item || !example) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="flex items-center justify-between tab-container px-1"
|
||||
className="flex items-center justify-between tab-container px-3"
|
||||
onMouseUp={(e) => {
|
||||
if (e.button === 1) {
|
||||
e.preventDefault();
|
||||
@@ -75,7 +74,7 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-3">
|
||||
{showConfirmClose && (
|
||||
<ConfirmRequestClose
|
||||
item={item}
|
||||
@@ -116,13 +115,13 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ExampleIcon size={16} color="currentColor" className="mr-2 text-gray-500 flex-shrink-0" />
|
||||
<ExampleIcon size={14} color="currentColor" className="mr-1.5 text-gray-500 flex-shrink-0" />
|
||||
<span className="tab-name" title={example.name}>
|
||||
{example.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
<GradientCloseButton
|
||||
hasChanges={hasChanges}
|
||||
onClick={(e) => {
|
||||
if (!hasChanges) {
|
||||
return handleCloseClick(e);
|
||||
@@ -132,13 +131,7 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
e.preventDefault();
|
||||
setShowConfirmClose(true);
|
||||
}}
|
||||
>
|
||||
{!hasChanges ? (
|
||||
<CloseTabIcon />
|
||||
) : (
|
||||
<DraftTabIcon />
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div.attrs((props) => ({
|
||||
style: {
|
||||
'--gradient-color': props.theme.requestTabs.bg,
|
||||
'--gradient-color-active': props.theme.bg
|
||||
}
|
||||
}))`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
width: 44px;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding-right: 4px;
|
||||
z-index: 3;
|
||||
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--gradient-color) 40%
|
||||
);
|
||||
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
li.active & {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--gradient-color-active) 40%
|
||||
);
|
||||
}
|
||||
|
||||
li:hover &,
|
||||
&.has-changes {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.close-icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
|
||||
|
||||
.close-icon {
|
||||
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
color: ${(props) => props.theme.requestTabs.icon.color};
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: color 0.12s ease;
|
||||
}
|
||||
|
||||
.has-changes-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.draft-icon-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.close-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.has-changes:not(li:hover &) {
|
||||
.draft-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.close-icon-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover &.has-changes {
|
||||
.draft-icon-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.close-icon-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import CloseTabIcon from '../CloseTabIcon';
|
||||
import DraftTabIcon from '../DraftTabIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GradientCloseButton = ({ onClick, hasChanges = false }) => {
|
||||
return (
|
||||
<StyledWrapper className={`close-gradient ${hasChanges ? 'has-changes' : ''}`}>
|
||||
<div className="close-icon-container" onClick={onClick} data-testid="request-tab-close-icon">
|
||||
<span className="draft-icon-wrapper">
|
||||
<DraftTabIcon />
|
||||
</span>
|
||||
<span className="close-icon-wrapper">
|
||||
<CloseTabIcon />
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GradientCloseButton;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import DraftTabIcon from './DraftTabIcon';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {
|
||||
@@ -8,49 +7,49 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
return (
|
||||
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
</div>
|
||||
<>
|
||||
<IconSettings size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Collection</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'collection-overview': {
|
||||
return (
|
||||
<>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
<IconSettings size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Overview</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'security-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconShieldLock size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Security</span>
|
||||
<IconShieldLock size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Security</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'folder-settings': {
|
||||
return (
|
||||
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconFolder size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
|
||||
<span className="ml-1 leading-6 truncate">{tabName || 'Folder'}</span>
|
||||
</div>
|
||||
<>
|
||||
<IconFolder size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">{tabName || 'Folder'}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'variables': {
|
||||
return (
|
||||
<>
|
||||
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Variables</span>
|
||||
<IconVariable size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Variables</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'collection-runner': {
|
||||
return (
|
||||
<>
|
||||
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Runner</span>
|
||||
<IconRun size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Runner</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -59,10 +58,13 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type, tabName)}</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
{hasDraft ? <DraftTabIcon /> : <CloseTabIcon />}
|
||||
<div
|
||||
className="flex items-baseline tab-label"
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{getTabInfo(type, tabName)}
|
||||
</div>
|
||||
<GradientCloseButton hasChanges={hasDraft} onClick={(e) => handleCloseClick(e)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,43 +1,30 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.tab-label {
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-method {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close-icon-container {
|
||||
min-height: 20px;
|
||||
min-width: 24px;
|
||||
margin-left: 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
.close-icon {
|
||||
display: none;
|
||||
color: ${(props) => props.theme.requestTabs.icon.color};
|
||||
width: 8px;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover .close-icon {
|
||||
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
|
||||
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
|
||||
}
|
||||
|
||||
.has-changes-icon {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tab-method {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
}
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useRef, Fragment, useMemo } from 'react';
|
||||
import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest, saveCollectionRoot, saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -17,16 +17,17 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
|
||||
import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import DraftTabIcon from './DraftTabIcon';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
import { flattenItems } from 'utils/collections/index';
|
||||
import { closeWsConnection } from 'utils/network/index';
|
||||
import ExampleTab from '../ExampleTab';
|
||||
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
const tabNameRef = useRef(null);
|
||||
const lastOverflowStateRef = useRef(null);
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
|
||||
const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false);
|
||||
@@ -36,6 +37,48 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
const item = findItemInCollection(collection, 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]);
|
||||
|
||||
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();
|
||||
@@ -105,11 +148,12 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
const hasDraft = tab.type === 'collection-settings' && collection?.draft;
|
||||
const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft;
|
||||
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? 'italic' : ''}`}
|
||||
onMouseUp={handleMouseUp} // Add middle-click behavior here
|
||||
className={`flex items-center justify-between tab-container px-2 ${tab.preview ? 'italic' : ''}`}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
{showConfirmCollectionClose && tab.type === 'collection-settings' && (
|
||||
<ConfirmCollectionClose
|
||||
@@ -192,21 +236,6 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
);
|
||||
}
|
||||
|
||||
const getMethodText = useCallback((item) => {
|
||||
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]);
|
||||
|
||||
if (!item) {
|
||||
@@ -228,10 +257,36 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
}
|
||||
|
||||
const isWS = item.type === 'ws-request';
|
||||
const method = getMethodText(item);
|
||||
|
||||
useEffect(() => {
|
||||
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.name, method, setHasOverflow]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-2">
|
||||
{showConfirmClose && (
|
||||
<ConfirmRequestClose
|
||||
item={item}
|
||||
@@ -268,7 +323,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-baseline tab-label pl-2 ${tab.preview ? 'italic' : ''}`}
|
||||
className={`flex items-baseline tab-label ${tab.preview ? 'italic' : ''}`}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
|
||||
onMouseUp={(e) => {
|
||||
@@ -284,7 +339,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method) }}>
|
||||
{method}
|
||||
</span>
|
||||
<span className="ml-1 tab-name" title={item.name}>
|
||||
<span ref={tabNameRef} className="ml-1 tab-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
<RequestTabMenu
|
||||
@@ -297,25 +352,19 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
<GradientCloseButton
|
||||
hasChanges={hasChanges}
|
||||
onClick={(e) => {
|
||||
if (!hasChanges) {
|
||||
isWS && closeWsConnection(item.uid);
|
||||
return handleCloseClick(e);
|
||||
};
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowConfirmClose(true);
|
||||
}}
|
||||
>
|
||||
{!hasChanges ? (
|
||||
<CloseTabIcon />
|
||||
) : (
|
||||
<DraftTabIcon />
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -349,7 +398,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
}
|
||||
|
||||
dispatch(closeTabs({ tabUids: [tabUid] }));
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
function handleRevertChanges(event) {
|
||||
@@ -368,7 +417,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
}
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
function handleCloseOtherTabs(event) {
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tabs-scroll-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: clip;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: -10px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
padding: 0 2px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
overflow: scroll;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@@ -17,57 +48,128 @@ const Wrapper = styled.div`
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
max-width: 150px;
|
||||
border: 1px solid transparent;
|
||||
max-width: 180px;
|
||||
min-width: 80px;
|
||||
list-style: none;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
cursor: pointer;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
height: 38px;
|
||||
|
||||
margin-right: 6px;
|
||||
font-size: 0.8125rem;
|
||||
position: relative;
|
||||
margin-right: 3px;
|
||||
color: ${(props) => props.theme.requestTabs.color};
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 0;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.tab-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
border-color: transparent;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
|
||||
}
|
||||
|
||||
&:nth-last-child(1) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.has-overflow:not(:hover) .tab-name {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 24px),
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 24px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&.has-overflow:hover .tab-name {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 8px),
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 8px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.requestTabs.active.bg};
|
||||
}
|
||||
background: ${(props) => props.theme.bg || '#ffffff'};
|
||||
border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-bottom-color: ${(props) => props.theme.bg || '#ffffff'};
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-weight: 500;
|
||||
z-index: 2;
|
||||
margin-bottom: -2px;
|
||||
padding-bottom: 12px;
|
||||
|
||||
&.active {
|
||||
.close-icon-container .close-icon {
|
||||
display: block;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: -8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
border-bottom-right-radius: 8px;
|
||||
box-shadow: 2px 2px 0 0 ${(props) => props.theme.bg || '#ffffff'};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.close-icon-container .close-icon {
|
||||
display: block;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
right: -8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
border-bottom-left-radius: 8px;
|
||||
box-shadow: -2px 2px 0 0 ${(props) => props.theme.bg || '#ffffff'};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
}
|
||||
}
|
||||
|
||||
&.short-tab {
|
||||
vertical-align: bottom;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
max-width: 34px;
|
||||
padding: 3px 0px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
padding: 5px 0;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.requestTabs.shortTab.color};
|
||||
background-color: ${(props) => props.theme.requestTabs.shortTab.bg};
|
||||
position: relative;
|
||||
top: -1px;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
flex-shrink: 0;
|
||||
|
||||
> div {
|
||||
padding: 3px 4px;
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
transition: background-color 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
|
||||
> div.home-icon-container {
|
||||
@@ -81,19 +183,23 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 22px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> div {
|
||||
background-color: ${(props) => props.theme.requestTabs.shortTab.hoverBg};
|
||||
color: ${(props) => props.theme.requestTabs.shortTab.hoverColor};
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-chevrons ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
@@ -14,7 +14,10 @@ import DraggableTab from './DraggableTab';
|
||||
const RequestTabs = () => {
|
||||
const dispatch = useDispatch();
|
||||
const tabsRef = useRef();
|
||||
const scrollContainerRef = useRef();
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [tabOverflowStates, setTabOverflowStates] = useState({});
|
||||
const [showChevrons, setShowChevrons] = useState(false);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
@@ -22,10 +25,48 @@ const RequestTabs = () => {
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const screenWidth = useSelector((state) => state.app.screenWidth);
|
||||
|
||||
const createSetHasOverflow = useCallback((tabUid) => {
|
||||
return (hasOverflow) => {
|
||||
setTabOverflowStates((prev) => {
|
||||
if (prev[tabUid] === hasOverflow) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[tabUid]: hasOverflow
|
||||
};
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const activeCollection = find(collections, (c) => c.uid === activeTab?.collectionUid);
|
||||
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab?.collectionUid);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTabUid || !activeTab) return;
|
||||
|
||||
const checkOverflow = () => {
|
||||
if (tabsRef.current && scrollContainerRef.current) {
|
||||
const hasOverflow = tabsRef.current.scrollWidth > scrollContainerRef.current.clientWidth;
|
||||
setShowChevrons(hasOverflow);
|
||||
}
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
const resizeObserver = new ResizeObserver(checkOverflow);
|
||||
if (scrollContainerRef.current) {
|
||||
resizeObserver.observe(scrollContainerRef.current);
|
||||
}
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [activeTabUid, activeTab, collectionRequestTabs.length, screenWidth, leftSidebarWidth, sidebarCollapsed]);
|
||||
|
||||
const getTabClassname = (tab, index) => {
|
||||
return classnames('request-tab select-none', {
|
||||
'active': tab.uid === activeTabUid,
|
||||
'last-tab': tabs && tabs.length && index === tabs.length - 1
|
||||
'last-tab': tabs && tabs.length && index === tabs.length - 1,
|
||||
'has-overflow': tabOverflowStates[tab.uid]
|
||||
});
|
||||
};
|
||||
|
||||
@@ -43,31 +84,22 @@ const RequestTabs = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!activeTab) {
|
||||
return <StyledWrapper>Something went wrong!</StyledWrapper>;
|
||||
}
|
||||
|
||||
const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid);
|
||||
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid);
|
||||
|
||||
const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth;
|
||||
const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150;
|
||||
const tabsWidth = collectionRequestTabs.length * 150 + 34; // 34: (+)icon
|
||||
const showChevrons = maxTablistWidth < tabsWidth;
|
||||
|
||||
const leftSlide = () => {
|
||||
tabsRef.current.scrollBy({
|
||||
scrollContainerRef.current?.scrollBy({
|
||||
left: -120,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
// todo: bring new tab to focus if its not in focus
|
||||
// tabsRef.current.scrollLeft
|
||||
|
||||
const rightSlide = () => {
|
||||
tabsRef.current.scrollBy({
|
||||
scrollContainerRef.current?.scrollBy({
|
||||
left: 120,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
@@ -87,7 +119,7 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center pl-4">
|
||||
<div className="flex items-center pl-2">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
@@ -103,36 +135,40 @@ const RequestTabs = () => {
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<DraggableTab
|
||||
key={tab.uid}
|
||||
id={tab.uid}
|
||||
index={index}
|
||||
onMoveTab={(source, target) => {
|
||||
dispatch(reorderTabs({
|
||||
sourceUid: source,
|
||||
targetUid: target
|
||||
}));
|
||||
}}
|
||||
className={getTabClassname(tab, index)}
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabIndex={index}
|
||||
<div className="tabs-scroll-container" style={{ maxWidth: maxTablistWidth }} ref={scrollContainerRef}>
|
||||
<ul role="tablist" ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<DraggableTab
|
||||
key={tab.uid}
|
||||
tab={tab}
|
||||
collection={activeCollection}
|
||||
folderUid={tab.folderUid}
|
||||
/>
|
||||
</DraggableTab>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
id={tab.uid}
|
||||
index={index}
|
||||
onMoveTab={(source, target) => {
|
||||
dispatch(reorderTabs({
|
||||
sourceUid: source,
|
||||
targetUid: target
|
||||
}));
|
||||
}}
|
||||
className={getTabClassname(tab, index)}
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabIndex={index}
|
||||
key={tab.uid}
|
||||
tab={tab}
|
||||
collection={activeCollection}
|
||||
folderUid={tab.folderUid}
|
||||
hasOverflow={tabOverflowStates[tab.uid]}
|
||||
setHasOverflow={createSetHasOverflow(tab.uid)}
|
||||
/>
|
||||
</DraggableTab>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
|
||||
@@ -15,7 +15,7 @@ test.describe('Grpc Collection - Method Search Functionality', () => {
|
||||
|
||||
test.afterEach(async ({ pageWithUserData: page }) => {
|
||||
await test.step('Close the gRPC sayHello tab without saving changes', async () => {
|
||||
await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click();
|
||||
await page.getByRole('tab', { name: 'gRPC sayHello' }).getByTestId('request-tab-close-icon').click();
|
||||
await page.getByRole('button', { name: 'Don\'t Save' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,18 +59,19 @@ test.describe('Autosave', () => {
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.type('/users');
|
||||
|
||||
// Verify draft indicator appears
|
||||
// Wait for draft indicator to appear (change registered)
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
|
||||
// Verify draft indicator disappears after autosave
|
||||
// Wait for autosave to complete (draft indicator disappears)
|
||||
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('Verify changes persisted', async () => {
|
||||
// Close and reopen the request tab to verify persistence
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
|
||||
await requestTab.locator('.close-icon').click();
|
||||
await requestTab.hover();
|
||||
await requestTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// Reopen request
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
|
||||
@@ -107,6 +108,9 @@ test.describe('Autosave', () => {
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.type('/posts');
|
||||
|
||||
// Move mouse away from tab to ensure draft icon is visible (hover shows close icon)
|
||||
await page.mouse.move(0, 0);
|
||||
|
||||
// Verify draft indicator appears
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
@@ -149,6 +153,9 @@ test.describe('Autosave', () => {
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.type('/existing-draft');
|
||||
|
||||
// Move mouse away from tab to ensure draft icon is visible (hover shows close icon)
|
||||
await page.mouse.move(0, 0);
|
||||
|
||||
// Verify draft indicator appears
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
@@ -180,7 +187,8 @@ test.describe('Autosave', () => {
|
||||
await test.step('Verify changes persisted', async () => {
|
||||
// Close and reopen the request tab to verify persistence
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });
|
||||
await requestTab.locator('.close-icon').click();
|
||||
await requestTab.hover();
|
||||
await requestTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// Reopen request
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();
|
||||
|
||||
@@ -35,19 +35,22 @@ test.describe('manage protofile', () => {
|
||||
const protoFilesTable = page.getByTestId('protobuf-proto-files-table');
|
||||
await expect(protoFilesTable).toBeVisible();
|
||||
|
||||
// Wait for table data to load by checking for a known cell
|
||||
const file = page.getByRole('cell', { name: 'product.proto', exact: true });
|
||||
expect(file).toBeVisible();
|
||||
await expect(file).toBeVisible();
|
||||
|
||||
const filePath = page.getByRole('cell', { name: '../protos/services/product.proto' });
|
||||
expect(filePath).toBeVisible();
|
||||
await expect(filePath).toBeVisible();
|
||||
|
||||
// Check import paths table
|
||||
const importPathsTable = page.getByTestId('protobuf-import-paths-table');
|
||||
await expect(importPathsTable).toBeVisible();
|
||||
|
||||
// Wait for import paths table data to load
|
||||
const importPath = page.getByRole('cell', { name: '../protos/types', exact: true });
|
||||
await expect(importPath).toBeVisible();
|
||||
|
||||
// Wait for invalid file path cell to appear
|
||||
const invalidFilePath = page.getByRole('cell', { name: 'invalid-file-path.proto', exact: true });
|
||||
await expect(invalidFilePath).toBeVisible();
|
||||
|
||||
@@ -64,6 +67,7 @@ test.describe('manage protofile', () => {
|
||||
await expect(invalidProtoFilesMessage).toBeVisible();
|
||||
await expect(invalidImportPathsMessage).toBeVisible();
|
||||
|
||||
// Wait for collection path cells to appear
|
||||
await expect(collectionPathAsImportPath).toBeVisible();
|
||||
await expect(collectionPathName).toBeVisible();
|
||||
|
||||
@@ -102,7 +106,9 @@ test.describe('manage protofile', () => {
|
||||
const method = page.getByTestId('grpc-method-item').filter({ hasText: /^CreateOrderunary$/ }).first();
|
||||
await expect(method).toBeVisible();
|
||||
await method.click();
|
||||
await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click();
|
||||
const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });
|
||||
await requestTab.hover();
|
||||
await requestTab.getByTestId('request-tab-close-icon').click();
|
||||
await page.getByRole('button', { name: 'Don\'t Save' }).click();
|
||||
});
|
||||
|
||||
@@ -128,7 +134,9 @@ test.describe('manage protofile', () => {
|
||||
const methodsDropdown = page.getByTestId('grpc-methods-dropdown');
|
||||
await expect(methodsDropdown).not.toBeVisible();
|
||||
|
||||
await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click();
|
||||
const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });
|
||||
await requestTab.hover();
|
||||
await requestTab.getByTestId('request-tab-close-icon').click();
|
||||
await page.getByRole('button', { name: 'Don\'t Save' }).click();
|
||||
});
|
||||
|
||||
@@ -170,7 +178,9 @@ test.describe('manage protofile', () => {
|
||||
await method.click();
|
||||
|
||||
// Clean up
|
||||
await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click();
|
||||
const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });
|
||||
await requestTab.hover();
|
||||
await requestTab.getByTestId('request-tab-close-icon').click();
|
||||
await page.getByRole('button', { name: 'Don\'t Save' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user