From f1961a89889413c970834c231536d3c3dee4926c Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Tue, 23 Dec 2025 16:38:24 +0530 Subject: [PATCH] refactor: update ResponsePane and QueryResultTypeSelector (#6490) * refactor: update ResponsePane and QueryResultTypeSelector for improved tab handling and styling - Adjusted the expanded width for right-side action buttons in ResponsePane. - Refactored view tab toggle logic to enhance clarity and functionality. - Introduced new styling for result view tabs and dropdown buttons. - Added icon support for format options in QueryResultTypeSelector, improving visual feedback. - Implemented dropdown state management to ensure proper interaction with active tabs. * refactor: remove console log from ResponsePane for cleaner code --- .../src/components/RequestTabPanel/index.js | 2 +- .../QueryResultTypeSelector/StyledWrapper.js | 10 +-- .../QueryResultTypeSelector/index.jsx | 67 +++++++++++++++---- .../components/ResponsePane/StyledWrapper.js | 27 ++++++++ .../src/components/ResponsePane/index.js | 32 +++++---- .../bruno-app/src/ui/ResponsiveTabs/index.js | 47 +++++++++++-- 6 files changed, 151 insertions(+), 34 deletions(-) diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 6b4d0b409..3062676bb 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -38,7 +38,7 @@ import EnvironmentSettings from 'components/Environments/EnvironmentSettings'; import GlobalEnvironmentSettings from 'components/Environments/GlobalEnvironmentSettings'; const MIN_LEFT_PANE_WIDTH = 300; -const MIN_RIGHT_PANE_WIDTH = 480; +const MIN_RIGHT_PANE_WIDTH = 490; const MIN_TOP_PANE_HEIGHT = 150; const MIN_BOTTOM_PANE_HEIGHT = 150; diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/StyledWrapper.js index 1270b5486..6a3b66566 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/StyledWrapper.js @@ -2,16 +2,14 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` .caret { - fill: currentColor; + color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret}; + fill: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret}; } .button-dropdown-button { color: ${(props) => props.theme.dropdown.primaryText}; border-color: ${(props) => props.theme.workspace.border}; - &:hover { - background-color: ${(props) => props.theme.dropdown.hoverBg}; - } } .dropdown-divider { @@ -24,6 +22,10 @@ const StyledWrapper = styled.div` color: ${(props) => props.theme.colors.text.yellow}; } + .icon-muted { + color: ${(props) => props.theme.colors.text.muted}; + } + .preview-response-tab-label { color: ${(props) => props.theme.colors.text.muted}; } diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/index.jsx b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/index.jsx index 7fff2c88c..4496c1fe4 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/index.jsx +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/index.jsx @@ -1,11 +1,22 @@ -import React, { forwardRef } from 'react'; -import { IconEye, IconCaretDown } from '@tabler/icons'; +import React, { forwardRef, useState } from 'react'; +import { IconEye, IconCaretDown, IconBraces, IconCode, IconFileCode, IconBrandJavascript, IconFileText, IconHexagons, IconBinaryTree } from '@tabler/icons'; import classnames from 'classnames'; import MenuDropdown from 'ui/MenuDropdown'; import ToggleSwitch from 'components/ToggleSwitch'; import StyledWrapper from './StyledWrapper'; -const ButtonIcon = forwardRef(({ disabled, className, style, prefix, selectedLabel, suffix, ...props }, ref) => { +// Icon mapping for format options +const FORMAT_ICONS = { + json: IconBraces, + html: IconCode, + xml: IconFileCode, + javascript: IconBrandJavascript, + raw: IconFileText, + hex: IconHexagons, + base64: IconBinaryTree +}; + +const ButtonIcon = forwardRef(({ disabled, className, style, prefix, selectedLabel, suffix, isActive, ...props }, ref) => { return ( ); }); @@ -34,8 +45,21 @@ const QueryResultTypeSelector = ({ formatValue, onFormatChange, onPreviewTabSelect, - selectedTab + selectedTab, + isActiveTab, + onTabSelect }) => { + const [dropdownOpen, setDropdownOpen] = useState(false); + + // Handle dropdown state change - only allow opening when active tab + const handleDropdownChange = (open) => { + if (!isActiveTab && open) { + // First click when not active - select this tab, don't open dropdown + onTabSelect?.(); + return; + } + setDropdownOpen(open); + }; // Find the selected item's label const findSelectedLabel = () => { if (formatValue != null) { @@ -47,10 +71,26 @@ const QueryResultTypeSelector = ({ const selectedLabel = findSelectedLabel(); - // Enhance items with onChange handler + // Get the icon for the currently selected format + const SelectedFormatIcon = FORMAT_ICONS[formatValue]; + + // Determine the prefix icon - eye icon when in preview mode, format icon otherwise + const getPrefixIcon = () => { + if (selectedTab === 'preview') { + return ; + } + if (SelectedFormatIcon) { + return ; + } + return null; + }; + + // Enhance items with onChange handler and icons const enhancedItems = formatOptions.map((item) => { + const IconComponent = FORMAT_ICONS[item.id]; return { ...item, + leftSection: IconComponent ? : null, onClick: () => { if (onFormatChange) { onFormatChange(item.id); @@ -67,7 +107,7 @@ const QueryResultTypeSelector = ({ handleToggle={(e) => { e.preventDefault(); // e.stopPropagation(); - onPreviewTabSelect(); + onPreviewTabSelect(selectedTab === 'preview' ? 'editor' : 'preview'); }} size="2xs" data-testid="preview-response-tab" @@ -77,7 +117,7 @@ const QueryResultTypeSelector = ({ ); return ( - + : null} + prefix={getPrefixIcon()} + isActive={isActiveTab} disabled={false} - className="h-[20px] text-[11px]" + className="h-[22px] text-[10px]" data-testid="format-response-tab" /> diff --git a/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js index da26a9a78..ed97f6a8b 100644 --- a/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js @@ -80,6 +80,33 @@ const StyledWrapper = styled.div` border-left: 1px solid ${(props) => props.theme.preferences.sidebar.border}; margin: 0 8px; } + + .result-view-tabs { + display: flex; + align-items: center; + gap: 2px; + padding: 3px; + border-radius: 8px; + + .button-dropdown-button { + border: 1px solid transparent !important; + background-color: transparent; + border-radius: 5px; + font-size: ${(props) => props.theme.font.size.sm}; + + &:hover { + border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.border} !important; + } + } + + .tab-active .button-dropdown-button { + border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.border} !important; + + &:hover { + border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder} !important; + } + } + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 3cb9af16f..725666045 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -26,7 +26,7 @@ import WSMessagesList from './WsResponsePane/WSMessagesList'; import ResponsiveTabs from 'ui/ResponsiveTabs'; // Width threshold for expanded right-side action buttons -const RIGHT_CONTENT_EXPANDED_WIDTH = 375; +const RIGHT_CONTENT_EXPANDED_WIDTH = 135; const ResponsePane = ({ item, collection }) => { const dispatch = useDispatch(); @@ -68,10 +68,9 @@ const ResponsePane = ({ item, collection }) => { dispatch(updateResponseFormat({ uid: item.uid, responseFormat: newFormat })); }, [dispatch, item.uid]); - const handleViewTabToggle = useCallback(() => { - const newViewTab = selectedViewTab === 'editor' ? 'preview' : 'editor'; + const handleViewTabChange = useCallback((newViewTab) => { dispatch(updateResponseViewTab({ uid: item.uid, responseViewTab: newViewTab })); - }, [dispatch, item.uid, selectedViewTab]); + }, [dispatch, item.uid]); const requestTimeline = ([...(collection.timeline || [])]).filter((obj) => { if (obj.itemUid === item.uid) return true; @@ -226,15 +225,24 @@ const ResponsePane = ({ item, collection }) => { onClick={() => setShowScriptErrorCard(true)} /> )} - {focusedTab?.responsePaneTab === 'response' ? ( + {focusedTab?.responsePaneTab === 'response' && item?.response ? ( <> - + {/* Result View Tabs (Visualizations + Response Format) */} +
+ + {/* Response Format */} + { + handleViewTabChange('editor'); + }} + /> +
) : null}
diff --git a/packages/bruno-app/src/ui/ResponsiveTabs/index.js b/packages/bruno-app/src/ui/ResponsiveTabs/index.js index b6b98d528..b44c7a0b2 100644 --- a/packages/bruno-app/src/ui/ResponsiveTabs/index.js +++ b/packages/bruno-app/src/ui/ResponsiveTabs/index.js @@ -8,6 +8,7 @@ const DROPDOWN_WIDTH = 60; const CALCULATION_DELAY_DEFAULT = 20; const CALCULATION_DELAY_EXTENDED = 150; const GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT = 80; +const EXPANDABLE_HYSTERESIS = 20; // Buffer to prevent flickering at boundary // Compare two tab arrays by their keys const areTabArraysEqual = (a, b) => { @@ -22,7 +23,8 @@ const ResponsiveTabs = ({ rightContent, rightContentRef, delayedTabs = [], - rightContentExpandedWidth // Optional: width of the right content when expanded(used when right content's elements are collapsible) + rightContentExpandedWidth, // Optional: width of the expandable element when expanded + expandableElementIndex = -1 // Optional: index of the expandable child element (-1 means last child) }) => { const [visibleTabs, setVisibleTabs] = useState([]); const [overflowTabs, setOverflowTabs] = useState([]); @@ -82,12 +84,47 @@ const ResponsiveTabs = ({ setOverflowTabs((prev) => (areTabArraysEqual(prev, overflow) ? prev : overflow)); // Only calculate expandibility if rightContentExpandedWidth is provided - if (rightContentExpandedWidth) { + if (rightContentExpandedWidth && rightContentRef?.current) { const leftContentWidth = currentWidth + (overflow.length ? DROPDOWN_WIDTH : 0); - const expandable = containerWidth - leftContentWidth - GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT > rightContentExpandedWidth; - setRightSideExpandable((prev) => (prev === expandable ? prev : expandable)); + + // Calculate total expanded width by summing children widths + // and replacing the expandable element's current width with its expanded width + const children = rightContentRef.current.children; + const childrenCount = children.length; + + if (childrenCount > 0) { + // Resolve the expandable element index (-1 means last child) + const targetIndex = expandableElementIndex < 0 ? childrenCount + expandableElementIndex : expandableElementIndex; + const validTargetIndex = Math.max(0, Math.min(targetIndex, childrenCount - 1)); + + let totalExpandedWidth = 0; + for (let i = 0; i < childrenCount; i++) { + if (i === validTargetIndex) { + // Use the expanded width for the expandable element + totalExpandedWidth += rightContentExpandedWidth; + } else { + // Use the current width for other elements + totalExpandedWidth += children[i].offsetWidth; + } + } + + const availableSpace = containerWidth - leftContentWidth - GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT; + + // Use hysteresis to prevent flickering at boundary + // When expanded: only collapse if significantly less space available + // When collapsed: expand when there's enough space + setRightSideExpandable((prev) => { + if (prev) { + // Currently expanded - only collapse if space drops below threshold minus hysteresis + return availableSpace > totalExpandedWidth - EXPANDABLE_HYSTERESIS; + } else { + // Currently collapsed - expand if there's enough space + return availableSpace > totalExpandedWidth; + } + }); + } } - }, [tabs, activeTab, rightContentRef, rightContentExpandedWidth]); + }, [tabs, activeTab, rightContentRef, rightContentExpandedWidth, expandableElementIndex]); // Recalculate on tab/activeTab changes useEffect(() => {