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:
Abhishek S Lal
2025-12-23 16:38:24 +05:30
committed by GitHub
parent 4831434e37
commit f1961a8988
6 changed files with 151 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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