diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/StyledWrapper.js
deleted file mode 100644
index c8e3c482a..000000000
--- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/StyledWrapper.js
+++ /dev/null
@@ -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;
diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
index da48bb34a..6fac333e5 100644
--- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
@@ -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 (
);
- }
- case 'variables': {
+ case 'variables':
return ;
- }
- case 'headers': {
+ case 'headers':
return ;
- }
- case 'auth': {
+ case 'auth':
return ;
- }
- case 'vars': {
+ case 'vars':
return ;
- }
- case 'assert': {
+ case 'assert':
return ;
- }
- case 'script': {
+ case 'script':
return ;
- }
- case 'tests': {
+ case 'tests':
return ;
- }
- case 'docs': {
+ case 'docs':
return ;
- }
- case 'settings': {
+ case 'settings':
return ;
- }
- default: {
+ default:
return
404 | Not found
;
- }
}
- };
+ }, [requestPaneTab, item, collection, displayedTheme, schema, onSave, query, onRun, onQueryChange, handleGqlClickReference, preferences, variables]);
- if (!activeTabUid) {
- return Something went wrong
;
- }
-
- const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
- if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
+ if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {
return An error occurred!
;
}
- const getTabClassname = (tabName) => {
- return classnames(`tab select-none ${tabName}`, {
- active: tabName === focusedTab.requestPaneTab
- });
- };
+ const isMultipleContentTab = MULTIPLE_CONTENT_TABS.has(requestPaneTab);
+
+ const rightContent = (
+
+
+
+ );
return (
-
-
-
selectTab('query')}>
- Query
-
-
selectTab('variables')}>
- Variables
-
-
selectTab('headers')}>
- Headers
-
-
selectTab('auth')}>
- Auth
-
-
selectTab('vars')}>
- Vars
-
-
selectTab('script')}>
- Script
-
-
selectTab('assert')}>
- Assert
-
-
selectTab('tests')}>
- Tests
-
-
selectTab('docs')}>
- Docs
-
-
selectTab('settings')}>
- Settings
-
-
-
-
- {getTabPanel(focusedTab.requestPaneTab)}
+
+
+
+
-
+
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
index c83065a29..fd9da22b3 100644
--- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
@@ -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 ;
- }
- case 'body': {
- return ;
- }
- case 'headers': {
- return ;
- }
- case 'auth': {
- return ;
- }
- case 'vars': {
- return ;
- }
- case 'assert': {
- return ;
- }
- case 'script': {
- return ;
- }
- case 'tests': {
- return ;
- }
- case 'docs': {
- return ;
- }
- case 'settings': {
- return ;
- }
- default: {
- return 404 | Not found
;
- }
- }
- };
-
- if (!activeTabUid) {
- return Something went wrong
;
- }
+ 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 ? {activeCounts.params} : null,
+ body: body.mode !== 'none' ? : null,
+ headers: activeCounts.headers > 0 ? {activeCounts.headers} : null,
+ auth: auth.mode !== 'none' ? : null,
+ vars: activeCounts.vars > 0 ? {activeCounts.vars} : null,
+ script: (script.req || script.res) ? (hasScriptError ? : ) : null,
+ assert: activeCounts.assertions > 0 ? {activeCounts.assertions} : null,
+ tests: tests?.length > 0 ? (hasTestError ? : ) : null,
+ docs: docs?.length > 0 ? : null,
+ settings: tags?.length > 0 ? : 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 ? : 404 | Not found
;
+ }, [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 An error occurred!
;
}
- 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' ? (
+
+
+
+ ) : null;
return (
-
-
-
selectTab('params')}>
- Params
- {activeParamsLength > 0 && {activeParamsLength}}
-
-
selectTab('body')}>
- Body
- {body.mode !== 'none' && }
-
-
selectTab('headers')}>
- Headers
- {activeHeadersLength > 0 && {activeHeadersLength}}
-
-
selectTab('auth')}>
- Auth
- {auth.mode !== 'none' && }
-
-
selectTab('vars')}>
- Vars
- {activeVarsLength > 0 && {activeVarsLength}}
-
-
selectTab('script')}>
- Script
- {(script.req || script.res) && (
- item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage
- ?
- :
- )}
-
-
selectTab('assert')}>
- Assert
- {activeAssertionsLength > 0 && {activeAssertionsLength}}
-
-
selectTab('tests')}>
- Tests
- {tests && tests.length > 0 && (
- item.testScriptErrorMessage
- ?
- :
- )}
-
-
selectTab('docs')}>
- Docs
- {docs && docs.length > 0 && }
-
-
selectTab('settings')}>
- Settings
- {tags && tags.length > 0 && }
-
- {focusedTab.requestPaneTab === 'body' ? (
-
-
-
- ) : null}
-
-
-
- {getTabPanel(focusedTab.requestPaneTab)}
-
+
+
+
+
-
+
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/StyledWrapper.js
index 983ab3cf7..86079e3d7 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/StyledWrapper.js
@@ -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;
diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/RequestPaneTabs/StyledWrapper.js
similarity index 53%
rename from packages/bruno-app/src/components/RequestPane/HttpRequestPane/StyledWrapper.js
rename to packages/bruno-app/src/components/RequestPane/RequestPaneTabs/StyledWrapper.js
index 44afba38d..ef1b84e02 100644
--- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestPaneTabs/StyledWrapper.js
@@ -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;
}
}
}
diff --git a/packages/bruno-app/src/components/RequestPane/RequestPaneTabs/index.js b/packages/bruno-app/src/components/RequestPane/RequestPaneTabs/index.js
new file mode 100644
index 000000000..c01364c29
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/RequestPaneTabs/index.js
@@ -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 (
+ handleTabSelect(tab.key)}
+ >
+
+ {tab.label}
+ {tab.indicator}
+
+
+ );
+ }
+
+ return (
+ handleTabSelect(tab.key)}
+ ref={(el) => el && (tabRefsMap.current[tab.key] = el)}
+ >
+ {tab.label}
+ {tab.indicator}
+
+ );
+ },
+ [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 (
+
+
+ {tabs.map((tab) => (
+
el && (tabRefsMap.current[tab.key] = el)}
+ >
+ {tab.label}
+ {tab.indicator}
+
+ ))}
+
+
+ {visibleTabs.map((tab) => renderTab(tab))}
+
+ {overflowTabs.length > 0 && (
+
+
+
+ )}
+ placement="bottom-start"
+ onCreate={(instance) => (dropdownTippyRef.current = instance)}
+ >
+ {overflowTabs.map((tab) => renderTab(tab, true))}
+
+ )}
+
+ {rightContent && (
+
+ {rightContent}
+
+ )}
+
+ );
+};
+
+export default RequestPaneTabs;
diff --git a/packages/bruno-app/src/pages/Bruno/StyledWrapper.js b/packages/bruno-app/src/pages/Bruno/StyledWrapper.js
index 403aacaac..9ce1816eb 100644
--- a/packages/bruno-app/src/pages/Bruno/StyledWrapper.js
+++ b/packages/bruno-app/src/pages/Bruno/StyledWrapper.js
@@ -15,6 +15,7 @@ const Wrapper = styled.div`
section.request-pane,
section.response-pane {
+ overflow: hidden;
}
}
diff --git a/tests/collection/moving-requests/tag-persistence.spec.ts b/tests/collection/moving-requests/tag-persistence.spec.ts
index cb1e52f15..cb971261d 100644
--- a/tests/collection/moving-requests/tag-persistence.spec.ts
+++ b/tests/collection/moving-requests/tag-persistence.spec.ts
@@ -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();
});
diff --git a/tests/request/settings/max-redirects.spec.ts b/tests/request/settings/max-redirects.spec.ts
index 1b6b856c7..9b10145e1 100644
--- a/tests/request/settings/max-redirects.spec.ts
+++ b/tests/request/settings/max-redirects.spec.ts
@@ -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"]');
diff --git a/tests/request/settings/no-redirects.spec.ts b/tests/request/settings/no-redirects.spec.ts
index eb3792f40..db9fc644e 100644
--- a/tests/request/settings/no-redirects.spec.ts
+++ b/tests/request/settings/no-redirects.spec.ts
@@ -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"]');
diff --git a/tests/request/settings/timeout.spec.ts b/tests/request/settings/timeout.spec.ts
index b71871216..b2eee32e9 100644
--- a/tests/request/settings/timeout.spec.ts
+++ b/tests/request/settings/timeout.spec.ts
@@ -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"]');
diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts
index 3efabdf6f..59bf55c0a 100644
--- a/tests/utils/page/actions.ts
+++ b/tests/utils/page/actions.ts
@@ -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 => {
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 };