mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
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
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
ref={ref}
|
||||
@@ -20,10 +31,10 @@ const ButtonIcon = forwardRef(({ disabled, className, style, prefix, selectedLab
|
||||
role="button"
|
||||
{...props}
|
||||
>
|
||||
{prefix && <span>{prefix}</span>}
|
||||
<span className="active">{selectedLabel}</span>
|
||||
{prefix && <span className={isActive ? 'active' : 'icon-muted'}>{prefix}</span>}
|
||||
<span>{selectedLabel}</span>
|
||||
{suffix && <span>{suffix}</span>}
|
||||
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
{isActive && <IconCaretDown className="caret ml-0.5" size={12} strokeWidth={2} />}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -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 <IconEye size={14} strokeWidth={2} />;
|
||||
}
|
||||
if (SelectedFormatIcon) {
|
||||
return <SelectedFormatIcon size={14} strokeWidth={1.5} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Enhance items with onChange handler and icons
|
||||
const enhancedItems = formatOptions.map((item) => {
|
||||
const IconComponent = FORMAT_ICONS[item.id];
|
||||
return {
|
||||
...item,
|
||||
leftSection: IconComponent ? <IconComponent size={14} strokeWidth={1.5} /> : 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 (
|
||||
<StyledWrapper>
|
||||
<StyledWrapper className={isActiveTab ? 'tab-active' : ''}>
|
||||
<MenuDropdown
|
||||
items={enhancedItems}
|
||||
header={header}
|
||||
@@ -85,12 +125,15 @@ const QueryResultTypeSelector = ({
|
||||
showTickMark={true}
|
||||
placement="bottom-end"
|
||||
data-testid="format-response-tab"
|
||||
opened={dropdownOpen}
|
||||
onChange={handleDropdownChange}
|
||||
>
|
||||
<ButtonIcon
|
||||
selectedLabel={selectedLabel}
|
||||
suffix={selectedTab === 'preview' ? <IconEye size={14} strokeWidth={2} className="active mr-[2px]" /> : null}
|
||||
prefix={getPrefixIcon()}
|
||||
isActive={isActiveTab}
|
||||
disabled={false}
|
||||
className="h-[20px] text-[11px]"
|
||||
className="h-[22px] text-[10px]"
|
||||
data-testid="format-response-tab"
|
||||
/>
|
||||
</MenuDropdown>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
<QueryResultTypeSelector
|
||||
formatOptions={previewFormatOptions}
|
||||
formatValue={selectedFormat}
|
||||
onFormatChange={handleFormatChange}
|
||||
onPreviewTabSelect={handleViewTabToggle}
|
||||
selectedTab={selectedViewTab}
|
||||
/>
|
||||
{/* Result View Tabs (Visualizations + Response Format) */}
|
||||
<div className="result-view-tabs">
|
||||
|
||||
{/* Response Format */}
|
||||
<QueryResultTypeSelector
|
||||
formatOptions={previewFormatOptions}
|
||||
formatValue={selectedFormat}
|
||||
onFormatChange={handleFormatChange}
|
||||
onPreviewTabSelect={handleViewTabChange}
|
||||
selectedTab={selectedViewTab}
|
||||
isActiveTab={selectedViewTab === 'editor' || selectedViewTab === 'preview'}
|
||||
onTabSelect={() => {
|
||||
handleViewTabChange('editor');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="flex items-center response-pane-status">
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user