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:
Sanjai Kumar
2025-12-06 18:42:57 +05:30
committed by GitHub
parent 42bef4ae1e
commit 3e5ae613f5
11 changed files with 508 additions and 190 deletions

View File

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

View File

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

View File

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

View File

@@ -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)} />
</>
);
};

View File

@@ -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;
}
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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