improve: tabs design (#6363)

* improve: tabs design

* fixes: tests
This commit is contained in:
naman-bruno
2025-12-09 21:35:27 +05:30
committed by GitHub
parent f6363389d0
commit cf4c896431
12 changed files with 459 additions and 298 deletions

View File

@@ -1,30 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: ${(props) => props.theme.tabs.marginRight};
color: var(--color-tab-inactive);
cursor: pointer;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
`;
export default StyledWrapper;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import find from 'lodash/find';
import get from 'lodash/get';
import classnames from 'classnames';
@@ -15,54 +15,86 @@ import Tests from 'components/RequestPane/Tests';
import { useTheme } from 'providers/Theme';
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import Documentation from 'components/Documentation/index';
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import Settings from 'components/RequestPane/Settings';
import RequestPaneTabs from 'components/RequestPane/RequestPaneTabs';
const MULTIPLE_CONTENT_TABS = new Set(['script', 'vars', 'auth', 'docs']);
const TAB_CONFIG = [
{ key: 'query', label: 'Query' },
{ key: 'variables', label: 'Variables' },
{ key: 'headers', label: 'Headers' },
{ key: 'auth', label: 'Auth' },
{ key: 'vars', label: 'Vars' },
{ key: 'script', label: 'Script' },
{ key: 'assert', label: 'Assert' },
{ key: 'tests', label: 'Tests' },
{ key: 'docs', label: 'Docs' },
{ key: 'settings', label: 'Settings' }
];
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const preferences = useSelector((state) => state.app.preferences);
const query = item.draft
? get(item, 'draft.request.body.graphql.query', '')
: get(item, 'request.body.graphql.query', '');
const variables = item.draft
? get(item, 'draft.request.body.graphql.variables')
: get(item, 'request.body.graphql.variables');
const { displayedTheme } = useTheme();
const [schema, setSchema] = useState(null);
const preferences = useSelector((state) => state.app.preferences);
const schemaActionsRef = useRef(null);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const requestPaneTab = focusedTab?.requestPaneTab;
useEffect(() => {
onSchemaLoad(schema);
}, [schema]);
}, [schema, onSchemaLoad]);
const onQueryChange = (value) => {
dispatch(
updateRequestGraphqlQuery({
query: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const onQueryChange = useCallback(
(value) => {
dispatch(
updateRequestGraphqlQuery({
query: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
},
[dispatch, item.uid, collection.uid]
);
const selectTab = (tab) => {
dispatch(
updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
})
);
};
const onRun = useCallback(
() => dispatch(sendRequest(item, collection.uid)),
[dispatch, item, collection.uid]
);
const getTabPanel = (tab) => {
switch (tab) {
case 'query': {
const onSave = useCallback(
() => dispatch(saveRequest(item.uid, collection.uid)),
[dispatch, item.uid, collection.uid]
);
const selectTab = useCallback(
(tabKey) => {
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));
},
[dispatch, item.uid]
);
const allTabs = useMemo(() => TAB_CONFIG.map(({ key, label }) => ({ key, label })), []);
const tabPanel = useMemo(() => {
switch (requestPaneTab) {
case 'query':
return (
<QueryEditor
collection={collection}
@@ -77,94 +109,55 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
fontSize={get(preferences, 'font.codeFontSize')}
/>
);
}
case 'variables': {
case 'variables':
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
}
case 'headers': {
case 'headers':
return <RequestHeaders item={item} collection={collection} />;
}
case 'auth': {
case 'auth':
return <Auth item={item} collection={collection} />;
}
case 'vars': {
case 'vars':
return <Vars item={item} collection={collection} />;
}
case 'assert': {
case 'assert':
return <Assertions item={item} collection={collection} />;
}
case 'script': {
case 'script':
return <Script item={item} collection={collection} />;
}
case 'tests': {
case 'tests':
return <Tests item={item} collection={collection} />;
}
case 'docs': {
case 'docs':
return <Documentation item={item} collection={collection} />;
}
case 'settings': {
case 'settings':
return <Settings item={item} collection={collection} />;
}
default: {
default:
return <div className="mt-4">404 | Not found</div>;
}
}
};
}, [requestPaneTab, item, collection, displayedTheme, schema, onSave, query, onRun, onQueryChange, handleGqlClickReference, preferences, variables]);
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {
return <div className="pb-4 px-4">An error occurred!</div>;
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === focusedTab.requestPaneTab
});
};
const isMultipleContentTab = MULTIPLE_CONTENT_TABS.has(requestPaneTab);
const rightContent = (
<div ref={schemaActionsRef}>
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
</div>
);
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
Query
</div>
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
Variables
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
Auth
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
Vars
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs
</div>
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
Settings
</div>
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
</div>
<section className="flex w-full mt-5 flex-1 relative">
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
<div className="flex flex-col h-full relative">
<RequestPaneTabs
tabs={allTabs}
activeTab={requestPaneTab}
onTabSelect={selectTab}
rightContent={rightContent}
rightContentRef={schemaActionsRef}
/>
<section className={classnames('flex w-full flex-1', { 'mt-5': !isMultipleContentTab })}>
<HeightBoundContainer>{tabPanel}</HeightBoundContainer>
</section>
</StyledWrapper>
</div>
);
};

View File

@@ -1,6 +1,7 @@
import React from 'react';
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
import classnames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { find, get } from 'lodash';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryParams from 'components/RequestPane/QueryParams';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
@@ -11,178 +12,144 @@ import Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import { useEffect } from 'react';
import StatusDot from 'components/StatusDot';
import Settings from 'components/RequestPane/Settings';
import Documentation from 'components/Documentation/index';
import StatusDot from 'components/StatusDot';
import RequestPaneTabs from 'components/RequestPane/RequestPaneTabs';
import HeightBoundContainer from 'ui/HeightBoundContainer';
const MULTIPLE_CONTENT_TABS = new Set(['params', 'script', 'vars', 'auth', 'docs']);
const TAB_CONFIG = [
{ key: 'params', label: 'Params' },
{ key: 'body', label: 'Body' },
{ key: 'headers', label: 'Headers' },
{ key: 'auth', label: 'Auth' },
{ key: 'vars', label: 'Vars' },
{ key: 'script', label: 'Script' },
{ key: 'assert', label: 'Assert' },
{ key: 'tests', label: 'Tests' },
{ key: 'docs', label: 'Docs' },
{ key: 'settings', label: 'Settings' }
];
const TAB_PANELS = {
params: QueryParams,
body: RequestBody,
headers: RequestHeaders,
auth: Auth,
vars: Vars,
assert: Assertions,
script: Script,
tests: Tests,
docs: Documentation,
settings: Settings
};
const HttpRequestPane = ({ item, collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const selectTab = (tab) => {
dispatch(
updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
})
);
};
const getTabPanel = (tab) => {
switch (tab) {
case 'params': {
return <QueryParams item={item} collection={collection} />;
}
case 'body': {
return <RequestBody item={item} collection={collection} />;
}
case 'headers': {
return <RequestHeaders item={item} collection={collection} />;
}
case 'auth': {
return <Auth item={item} collection={collection} />;
}
case 'vars': {
return <Vars item={item} collection={collection} />;
}
case 'assert': {
return <Assertions item={item} collection={collection} />;
}
case 'script': {
return <Script item={item} collection={collection} />;
}
case 'tests': {
return <Tests item={item} collection={collection} />;
}
case 'docs': {
return <Documentation item={item} collection={collection} />;
}
case 'settings': {
return <Settings item={item} collection={collection} />;
}
default: {
return <div className="mt-4">404 | Not found</div>;
}
}
};
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
const bodyModeRef = useRef(null);
const initialAutoSelectDone = useRef(false);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
const requestPaneTab = focusedTab?.requestPaneTab;
const getProperty = useCallback(
(key) => (item.draft ? get(item, `draft.${key}`, []) : get(item, key, [])),
[item.draft, item]
);
const params = getProperty('request.params');
const body = getProperty('request.body');
const headers = getProperty('request.headers');
const script = getProperty('request.script');
const assertions = getProperty('request.assertions');
const tests = getProperty('request.tests');
const docs = getProperty('request.docs');
const requestVars = getProperty('request.vars.req');
const responseVars = getProperty('request.vars.res');
const auth = getProperty('request.auth');
const tags = getProperty('tags');
const activeCounts = useMemo(() => ({
params: params.filter((p) => p.enabled).length,
headers: headers.filter((h) => h.enabled).length,
assertions: assertions.filter((a) => a.enabled).length,
vars: requestVars.filter((r) => r.enabled).length + responseVars.filter((r) => r.enabled).length
}), [params, headers, assertions, requestVars, responseVars]);
const selectTab = useCallback(
(tabKey) => {
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));
},
[dispatch, item.uid]
);
const indicators = useMemo(() => {
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
const hasTestError = item.testScriptErrorMessage;
return {
params: activeCounts.params > 0 ? <sup className="font-medium">{activeCounts.params}</sup> : null,
body: body.mode !== 'none' ? <StatusDot /> : null,
headers: activeCounts.headers > 0 ? <sup className="font-medium">{activeCounts.headers}</sup> : null,
auth: auth.mode !== 'none' ? <StatusDot /> : null,
vars: activeCounts.vars > 0 ? <sup className="font-medium">{activeCounts.vars}</sup> : null,
script: (script.req || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
assert: activeCounts.assertions > 0 ? <sup className="font-medium">{activeCounts.assertions}</sup> : null,
tests: tests?.length > 0 ? (hasTestError ? <StatusDot type="error" /> : <StatusDot />) : null,
docs: docs?.length > 0 ? <StatusDot /> : null,
settings: tags?.length > 0 ? <StatusDot /> : null
};
}, [activeCounts, body.mode, auth.mode, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
const allTabs = useMemo(
() => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: indicators[key] })),
[indicators]
);
const tabPanel = useMemo(() => {
const Component = TAB_PANELS[requestPaneTab];
return Component ? <Component item={item} collection={collection} /> : <div className="mt-4">404 | Not found</div>;
}, [requestPaneTab, item, collection]);
useEffect(() => {
if (!initialAutoSelectDone.current && activeCounts.params === 0 && body.mode !== 'none') {
selectTab('body');
}
initialAutoSelectDone.current = true;
}, [activeCounts.params, body.mode, selectTab]);
if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {
return <div className="pb-4 px-4">An error occurred!</div>;
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === focusedTab.requestPaneTab
});
};
const isMultipleContentTab = MULTIPLE_CONTENT_TABS.has(requestPaneTab);
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
// get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
const getPropertyFromDraftOrRequest = (propertyKey) =>
item.draft ? get(item, `draft.${propertyKey}`, []) : get(item, propertyKey, []);
const params = getPropertyFromDraftOrRequest('request.params');
const body = getPropertyFromDraftOrRequest('request.body');
const headers = getPropertyFromDraftOrRequest('request.headers');
const script = getPropertyFromDraftOrRequest('request.script');
const assertions = getPropertyFromDraftOrRequest('request.assertions');
const tests = getPropertyFromDraftOrRequest('request.tests');
const docs = getPropertyFromDraftOrRequest('request.docs');
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
const auth = getPropertyFromDraftOrRequest('request.auth');
const tags = getPropertyFromDraftOrRequest('tags');
const activeParamsLength = params.filter((param) => param.enabled).length;
const activeHeadersLength = headers.filter((header) => header.enabled).length;
const activeAssertionsLength = assertions.filter((assertion) => assertion.enabled).length;
const activeVarsLength
= requestVars.filter((request) => request.enabled).length
+ responseVars.filter((response) => response.enabled).length;
useEffect(() => {
if (activeParamsLength === 0 && body.mode !== 'none') {
selectTab('body');
}
}, []);
const rightContent = requestPaneTab === 'body' ? (
<div ref={bodyModeRef}>
<RequestBodyMode item={item} collection={collection} />
</div>
) : null;
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
Params
{activeParamsLength > 0 && <sup className="ml-1 font-medium">{activeParamsLength}</sup>}
</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Body
{body.mode !== 'none' && <StatusDot />}
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
Auth
{auth.mode !== 'none' && <StatusDot />}
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
Vars
{activeVarsLength > 0 && <sup className="ml-1 font-medium">{activeVarsLength}</sup>}
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
{(script.req || script.res) && (
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage
? <StatusDot type="error" />
: <StatusDot />
)}
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
{activeAssertionsLength > 0 && <sup className="ml-1 font-medium">{activeAssertionsLength}</sup>}
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
{tests && tests.length > 0 && (
item.testScriptErrorMessage
? <StatusDot type="error" />
: <StatusDot />
)}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs
{docs && docs.length > 0 && <StatusDot />}
</div>
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
Settings
{tags && tags.length > 0 && <StatusDot />}
</div>
{focusedTab.requestPaneTab === 'body' ? (
<div className="flex flex-grow justify-end items-center">
<RequestBodyMode item={item} collection={collection} />
</div>
) : null}
</div>
<section
className={classnames('flex w-full flex-1', {
'mt-5': !isMultipleContentTab
})}
>
<HeightBoundContainer>
{getTabPanel(focusedTab.requestPaneTab)}
</HeightBoundContainer>
<div className="flex flex-col h-full relative">
<RequestPaneTabs
tabs={allTabs}
activeTab={requestPaneTab}
onTabSelect={selectTab}
rightContent={rightContent}
rightContentRef={bodyModeRef}
delayedTabs={['body']}
/>
<section className={classnames('flex w-full flex-1', { 'mt-3': !isMultipleContentTab })}>
<HeightBoundContainer>{tabPanel}</HeightBoundContainer>
</section>
</StyledWrapper>
</div>
);
};

View File

@@ -2,6 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
font-size: ${(props) => props.theme.font.size.base};
min-width: 125px;
.body-mode-selector {
background: transparent;

View File

@@ -1,14 +1,28 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.tabs {
&.tabs {
div.more-tabs {
color: ${(props) => props.theme.text} !important;
background-color: ${(props) => props.theme.requestTabs.bg} !important;
&:hover {
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg} !important;
}
}
div.tab {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: ${(props) => props.theme.tabs.marginRight};
color: var(--color-tab-inactive);
cursor: pointer;
white-space: nowrap;
vertical-align: middle;
flex-shrink: 0;
&:focus,
&:active,
@@ -25,7 +39,15 @@ const StyledWrapper = styled.div`
}
.content-indicator {
color: ${(props) => props.theme.text}
color: ${(props) => props.theme.text};
}
sup {
display: inline-flex;
align-items: center;
line-height: 1;
vertical-align: baseline;
margin-left: 0;
}
}
}

View File

@@ -0,0 +1,177 @@
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import classnames from 'classnames';
import Dropdown from 'components/Dropdown';
import { IconDots } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const DROPDOWN_WIDTH = 60;
const CALCULATION_DELAY_DEFAULT = 20;
const CALCULATION_DELAY_EXTENDED = 150;
const RequestPaneTabs = ({
tabs,
activeTab,
onTabSelect,
rightContent,
rightContentRef,
delayedTabs = []
}) => {
const [visibleTabs, setVisibleTabs] = useState([]);
const [overflowTabs, setOverflowTabs] = useState([]);
const tabsContainerRef = useRef(null);
const tabRefsMap = useRef({});
const dropdownTippyRef = useRef(null);
const handleTabSelect = useCallback(
(tabKey) => {
onTabSelect(tabKey);
dropdownTippyRef.current?.hide();
},
[onTabSelect]
);
const calculateTabVisibility = useCallback(() => {
const container = tabsContainerRef.current;
if (!container || !tabs.length) return;
const containerWidth = container.offsetWidth;
const rightContentWidth = rightContentRef?.current
? rightContentRef.current.offsetWidth + 20
: 0;
const availableWidth = containerWidth - rightContentWidth - DROPDOWN_WIDTH;
const visible = [];
const overflow = [];
let currentWidth = 0;
for (const tab of tabs) {
const tabElement = tabRefsMap.current[tab.key];
const tabWidth = tabElement ? tabElement.offsetWidth + 20 : 100;
if (currentWidth + tabWidth <= availableWidth && !overflow.length) {
visible.push(tab);
currentWidth += tabWidth;
} else {
overflow.push(tab);
}
}
if (!visible.some((t) => t.key === activeTab) && overflow.length) {
const activeTabIndex = overflow.findIndex((t) => t.key === activeTab);
if (activeTabIndex !== -1) {
const [activeTabItem] = overflow.splice(activeTabIndex, 1);
const lastVisible = visible.pop();
if (lastVisible) overflow.unshift(lastVisible);
visible.push(activeTabItem);
}
}
setVisibleTabs(visible);
setOverflowTabs(overflow);
}, [tabs, activeTab, rightContentRef]);
const renderTab = useCallback(
(tab, isInDropdown = false) => {
const isActive = tab.key === activeTab;
if (isInDropdown) {
return (
<div
key={tab.key}
className={classnames('dropdown-item', { active: isActive })}
role="tab"
onClick={() => handleTabSelect(tab.key)}
>
<span className="flex items-center gap-1">
{tab.label}
{tab.indicator}
</span>
</div>
);
}
return (
<div
key={tab.key}
className={classnames('tab select-none', tab.key, { active: isActive })}
role="tab"
onClick={() => handleTabSelect(tab.key)}
ref={(el) => el && (tabRefsMap.current[tab.key] = el)}
>
{tab.label}
{tab.indicator}
</div>
);
},
[activeTab, handleTabSelect]
);
useEffect(() => {
const delay = delayedTabs.includes(activeTab) ? CALCULATION_DELAY_EXTENDED : CALCULATION_DELAY_DEFAULT;
const timeoutId = setTimeout(() => requestAnimationFrame(calculateTabVisibility), delay);
return () => clearTimeout(timeoutId);
}, [calculateTabVisibility, activeTab, delayedTabs]);
useEffect(() => {
let frameId = null;
const observer = new ResizeObserver(() => {
if (frameId) cancelAnimationFrame(frameId);
frameId = requestAnimationFrame(calculateTabVisibility);
});
if (tabsContainerRef.current) observer.observe(tabsContainerRef.current);
if (rightContentRef?.current) observer.observe(rightContentRef.current);
return () => {
if (frameId) cancelAnimationFrame(frameId);
observer.disconnect();
};
}, [calculateTabVisibility, rightContentRef]);
const hiddenStyle = useMemo(
() => ({ visibility: 'hidden', position: 'absolute', display: 'flex', pointerEvents: 'none' }),
[]
);
return (
<StyledWrapper ref={tabsContainerRef} className="flex items-center tabs" role="tablist">
<div style={hiddenStyle}>
{tabs.map((tab) => (
<div
key={tab.key}
className={classnames('tab select-none', tab.key, { active: tab.key === activeTab })}
ref={(el) => el && (tabRefsMap.current[tab.key] = el)}
>
{tab.label}
{tab.indicator}
</div>
))}
</div>
{visibleTabs.map((tab) => renderTab(tab))}
{overflowTabs.length > 0 && (
<Dropdown
icon={(
<div className="tab more-tabs select-none flex items-center cursor-pointer rounded-md" style={{ padding: '2px 8px' }}>
<IconDots size={18} />
</div>
)}
placement="bottom-start"
onCreate={(instance) => (dropdownTippyRef.current = instance)}
>
<div style={{ minWidth: '150px' }}>{overflowTabs.map((tab) => renderTab(tab, true))}</div>
</Dropdown>
)}
{rightContent && (
<div className="flex flex-grow justify-end items-center">
{rightContent}
</div>
)}
</StyledWrapper>
);
};
export default RequestPaneTabs;

View File

@@ -15,6 +15,7 @@ const Wrapper = styled.div`
section.request-pane,
section.response-pane {
overflow: hidden;
}
}

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, createUntitledRequest } from '../../utils/page';
import { closeAllCollections, createUntitledRequest, selectRequestPaneTab } from '../../utils/page';
test.describe('Tag persistence', () => {
test.afterEach(async ({ page }) => {
@@ -62,7 +62,7 @@ test.describe('Tag persistence', () => {
// Click on the moved request (now first) to verify the tag persisted after the move
await untitledRequests.first().click();
await page.locator('.request-tab.active').waitFor({ state: 'visible' });
await page.getByRole('tab', { name: 'Settings' }).click();
await selectRequestPaneTab(page, 'Settings');
await page.waitForTimeout(200);
// Verify the tag is still present after the move
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
@@ -116,11 +116,11 @@ test.describe('Tag persistence', () => {
await page.waitForTimeout(200);
// Add a tag to the request
await page.getByRole('tab', { name: 'Settings' }).click();
await selectRequestPaneTab(page, 'Settings');
await page.waitForTimeout(200);
const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
await tagInput.fill('smoke');
await tagInput.press('Enter');
const tagInput2 = await page.getByTestId('tag-input').getByRole('textbox');
await tagInput2.fill('smoke');
await tagInput2.press('Enter');
await page.waitForTimeout(200);
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
await page.keyboard.press('Meta+s');
@@ -157,7 +157,7 @@ test.describe('Tag persistence', () => {
// Click on request-2 to verify the tag persisted after the move
await page.locator('.collection-item-name').filter({ hasText: 'request-2' }).click();
await page.locator('.request-tab.active').filter({ hasText: 'request-2' }).waitFor({ state: 'visible' });
await page.getByRole('tab', { name: 'Settings' }).click();
await selectRequestPaneTab(page, 'Settings');
await page.waitForTimeout(200);
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
});

View File

@@ -1,4 +1,5 @@
import { test, expect } from '../../../playwright';
import { selectRequestPaneTab } from '../../utils/page';
test.describe('Max Redirects Settings Tests', () => {
test('should configure and test max redirects settings', async ({
@@ -13,7 +14,7 @@ test.describe('Max Redirects Settings Tests', () => {
await page.getByRole('complementary').getByText('max-redirects').click();
// Go to Settings tab
await page.getByRole('tab', { name: 'Settings' }).click();
await selectRequestPaneTab(page, 'Settings');
// Test Max Redirects Settings
const maxRedirectsInput = page.locator('input[id="maxRedirects"]');

View File

@@ -1,4 +1,5 @@
import { test, expect } from '../../../playwright';
import { selectRequestPaneTab } from '../../utils/page';
test.describe('No Redirects Settings Tests', () => {
test('should configure and test no redirects settings', async ({
@@ -13,7 +14,7 @@ test.describe('No Redirects Settings Tests', () => {
await page.getByRole('complementary').getByText('no-redirects').click();
// Go to Settings tab
await page.getByRole('tab', { name: 'Settings' }).click();
await selectRequestPaneTab(page, 'Settings');
// Test No Redirects Settings
const maxRedirectsInput = page.locator('input[id="maxRedirects"]');

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections } from '../../utils/page';
import { closeAllCollections, selectRequestPaneTab } from '../../utils/page';
test.describe('Timeout Settings Tests', () => {
test('should configure and test timeout settings', async ({
@@ -13,7 +13,7 @@ test.describe('Timeout Settings Tests', () => {
await page.getByRole('complementary').getByText('timeout-test').click();
// Go to Settings tab
await page.getByRole('tab', { name: 'Settings' }).click();
await selectRequestPaneTab(page, 'Settings');
// Test Timeout Settings with custom value
const timeoutInput = page.locator('input[id="timeout"]');

View File

@@ -132,7 +132,7 @@ const createUntitledRequest = async (
// Add tag if provided
if (tag) {
await page.getByRole('tab', { name: 'Settings' }).click();
await selectRequestPaneTab(page, 'Settings');
await page.waitForTimeout(200);
const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
await tagInput.fill(tag);
@@ -563,6 +563,33 @@ const getResponseBody = async (page: Page): Promise<string> => {
return await page.locator('.response-pane').innerText();
};
const selectRequestPaneTab = async (page: Page, tabName: string) => {
await test.step(`Select request pane tab "${tabName}"`, async () => {
const visibleTab = page.locator('.tabs').getByRole('tab', { name: tabName });
const overflowButton = page.locator('.tabs .more-tabs');
// Check if tab is directly visible
if (await visibleTab.isVisible()) {
await visibleTab.click();
return;
}
// Check if there's an overflow dropdown
if (await overflowButton.isVisible()) {
await overflowButton.click();
// Wait for dropdown to appear and click the tab
const dropdownTab = page.locator('.tippy-content').getByRole('tab', { name: tabName });
await expect(dropdownTab).toBeVisible();
await dropdownTab.click();
return;
}
// If neither found, fail with a helpful message
throw new Error(`Tab "${tabName}" not found in visible tabs or overflow dropdown`);
});
};
/**
* Verify response contains specific text
* @param page - The page object
@@ -599,7 +626,8 @@ export {
openRequest,
openFolderRequest,
getResponseBody,
expectResponseContains
expectResponseContains,
selectRequestPaneTab
};
export type { SandboxMode, EnvironmentType, EnvironmentVariable, CreateCollectionOptions, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions };