feat: implement vertical layout for response pane and enhance drag (#4957)

This commit is contained in:
lohit
2025-06-24 19:22:05 +05:30
committed by GitHub
23 changed files with 477 additions and 64 deletions

View File

@@ -6,6 +6,7 @@
"baseUrl": "./",
"paths": {
"assets/*": ["src/assets/*"],
"ui/*": ["src/ui/*"],
"components/*": ["src/components/*"],
"hooks/*": ["src/hooks/*"],
"themes/*": ["src/themes/*"],

View File

@@ -3,6 +3,7 @@ import get from 'lodash/get';
import { useSelector, useDispatch } from 'react-redux';
import { savePreferences } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
const Font = ({ close }) => {
const dispatch = useDispatch();
@@ -31,7 +32,10 @@ const Font = ({ close }) => {
}
})
).then(() => {
toast.success('Preferences saved successfully')
close();
}).catch(() => {
toast.error('Failed to save preferences')
});
};

View File

@@ -80,9 +80,9 @@ const General = ({ close }) => {
storeCookies: newPreferences.storeCookies,
sendCookies: newPreferences.sendCookies
}
})
)
}))
.then(() => {
toast.success('Preferences saved successfully')
close();
})
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));

View File

@@ -84,7 +84,10 @@ const ProxySettings = ({ close }) => {
proxy: validatedProxy
})
).then(() => {
toast.success('Preferences saved successfully')
close();
}).catch(() => {
toast.error('Failed to save preferences')
});
})
.catch((error) => {

View File

@@ -18,8 +18,9 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import StyledWrapper from './StyledWrapper';
import Documentation from 'components/Documentation/index';
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
import HeightBoundContainer from 'ui/HeightBoundContainer';
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
@@ -66,7 +67,6 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
collection={collection}
theme={displayedTheme}
schema={schema}
width={leftPaneWidth}
onSave={onSave}
value={query}
onRun={onRun}
@@ -154,7 +154,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
</div>
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
</div>
<section className="flex w-full mt-5 flex-1 relative">{getTabPanel(focusedTab.requestPaneTab)}</section>
<section className="flex w-full mt-5 flex-1 relative">
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
</section>
</StyledWrapper>
);
};

View File

@@ -15,6 +15,7 @@ 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';
const ContentIndicator = () => {
@@ -33,7 +34,7 @@ const ErrorIndicator = () => {
);
};
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const HttpRequestPane = ({ item, collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
@@ -180,7 +181,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
'mt-5': !isMultipleContentTab
})}
>
{getTabPanel(focusedTab.requestPaneTab)}
<HeightBoundContainer>
{getTabPanel(focusedTab.requestPaneTab)}
</HeightBoundContainer>
</section>
</StyledWrapper>
);

View File

@@ -114,7 +114,7 @@ const QueryParams = ({ item, collection }) => {
};
return (
<StyledWrapper className="w-full flex flex-col absolute">
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Query</div>
<Table

View File

@@ -3,9 +3,13 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
&.dragging {
cursor: col-resize;
&.vertical-layout {
cursor: row-resize;
}
}
div.drag-request {
div.dragbar-wrapper {
display: flex;
align-items: center;
justify-content: center;
@@ -15,18 +19,39 @@ const StyledWrapper = styled.div`
cursor: col-resize;
background: transparent;
div.drag-request-border {
div.dragbar-handle {
display: flex;
height: 100%;
width: 1px;
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
}
&:hover div.drag-request-border {
&:hover div.dragbar-handle {
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
}
}
&.vertical-layout {
div.dragbar-wrapper {
width: 100%;
height: 10px;
cursor: row-resize;
div.dragbar-handle {
width: 100%;
height: 1px;
border-left: none;
border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
margin-top: 0.5rem;
}
&:hover div.dragbar-handle {
border-left: none;
border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
}
}
}
div.graphql-docs-explorer-container {
background: white;
outline: none;

View File

@@ -29,7 +29,8 @@ import FolderNotFound from './FolderNotFound';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
const DEFAULT_PADDING = 5;
const MIN_TOP_PANE_HEIGHT = 150;
const MIN_BOTTOM_PANE_HEIGHT = 150;
const RequestTabPanel = () => {
if (typeof window == 'undefined') {
@@ -41,6 +42,8 @@ const RequestTabPanel = () => {
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
const _collections = useSelector((state) => state.collections.collections);
const preferences = useSelector((state) => state.app.preferences);
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
// merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
let collections = produce(_collections, (draft) => {
@@ -64,13 +67,15 @@ const RequestTabPanel = () => {
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
const [leftPaneWidth, setLeftPaneWidth] = useState(
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
); // 2.2 is intentional to make both panes appear to be of equal width
const [topPaneHeight, setTopPaneHeight] = useState(focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT);
const [dragging, setDragging] = useState(false);
const dragOffset = useRef({ x: 0, y: 0 });
// Not a recommended pattern here to have the child component
// make a callback to set state, but treating this as an exception
const docExplorerRef = useRef(null);
const mainSectionRef = useRef(null);
const [schema, setSchema] = useState(null);
const [showGqlDocs, setShowGqlDocs] = useState(false);
const onSchemaLoad = (schema) => setSchema(schema);
@@ -85,43 +90,72 @@ const RequestTabPanel = () => {
};
useEffect(() => {
const leftPaneWidth = (screenWidth - asideWidth) / 2.2;
setLeftPaneWidth(leftPaneWidth);
}, [screenWidth]);
useEffect(() => {
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
}, [screenWidth, asideWidth, leftPaneWidth]);
// Initialize vertical heights when switching to vertical layout
if (mainSectionRef.current) {
const mainRect = mainSectionRef.current.getBoundingClientRect();
if (isVerticalLayout) {
const initialHeight = mainRect.height / 2;
setTopPaneHeight(initialHeight);
// In vertical mode, set leftPaneWidth to full container width
setLeftPaneWidth(mainRect.width);
} else {
// In horizontal mode, set to roughly half width
setLeftPaneWidth((screenWidth - asideWidth) / 2.2);
}
}
}, [isVerticalLayout, screenWidth, asideWidth]);
const handleMouseMove = (e) => {
if (dragging) {
if (dragging && mainSectionRef.current) {
e.preventDefault();
let leftPaneXPosition = e.clientX + 2;
if (
leftPaneXPosition < asideWidth + DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH ||
leftPaneXPosition > screenWidth - MIN_RIGHT_PANE_WIDTH
) {
return;
const mainRect = mainSectionRef.current.getBoundingClientRect();
if (isVerticalLayout) {
const newHeight = e.clientY - mainRect.top - dragOffset.current.y;
if (newHeight < MIN_TOP_PANE_HEIGHT || newHeight > mainRect.height - MIN_BOTTOM_PANE_HEIGHT) {
return;
}
setTopPaneHeight(newHeight);
} else {
const newWidth = e.clientX - mainRect.left - dragOffset.current.x;
if (newWidth < MIN_LEFT_PANE_WIDTH || newWidth > mainRect.width - MIN_RIGHT_PANE_WIDTH) {
return;
}
setLeftPaneWidth(newWidth);
}
setLeftPaneWidth(leftPaneXPosition - asideWidth);
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
}
};
const handleMouseUp = (e) => {
if (dragging) {
if (dragging && mainSectionRef.current) {
e.preventDefault();
setDragging(false);
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
})
);
if (!isVerticalLayout) {
const mainRect = mainSectionRef.current.getBoundingClientRect();
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - mainRect.left
})
);
}
}
};
const handleDragbarMouseDown = (e) => {
e.preventDefault();
setDragging(true);
if (isVerticalLayout) {
const dragBar = e.currentTarget;
const dragBarRect = dragBar.getBoundingClientRect();
dragOffset.current.y = e.clientY - dragBarRect.top;
} else {
const dragBar = e.currentTarget;
const dragBarRect = dragBar.getBoundingClientRect();
dragOffset.current.x = e.clientX - dragBarRect.left;
}
};
useEffect(() => {
@@ -132,7 +166,7 @@ const RequestTabPanel = () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [dragging, asideWidth]);
}, [dragging]);
if (!activeTabUid) {
return <Welcome />;
@@ -197,15 +231,19 @@ const RequestTabPanel = () => {
};
return (
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${isVerticalLayout ? 'vertical-layout' : ''}`}>
<div className="pt-4 pb-3 px-4">
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
</div>
<section className="main flex flex-grow pb-4 relative">
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative`}>
<section className="request-pane">
<div
className="px-4 h-full"
style={{
style={isVerticalLayout ? {
height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
width: '100%'
} : {
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
}}
>
@@ -213,7 +251,6 @@ const RequestTabPanel = () => {
<GraphQLRequestPane
item={item}
collection={collection}
leftPaneWidth={leftPaneWidth}
onSchemaLoad={onSchemaLoad}
toggleDocs={toggleDocs}
handleGqlClickReference={handleGqlClickReference}
@@ -221,17 +258,17 @@ const RequestTabPanel = () => {
) : null}
{item.type === 'http-request' ? (
<HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} />
<HttpRequestPane item={item} collection={collection} />
) : null}
</div>
</section>
<div className="drag-request" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
<div className="dragbar-wrapper" onMouseDown={handleDragbarMouseDown}>
<div className="dragbar-handle" />
</div>
<section className="response-pane flex-grow">
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
<ResponsePane item={item} collection={collection} response={item.response} />
</section>
</section>

View File

@@ -22,6 +22,15 @@ const StyledWrapper = styled.div`
animation: rotateCounterClockwise 1s linear infinite;
}
}
// spinner and request time content looks better centered vertically in vertical layout
// while in horizontal layout, it looks better when the content is aligned to the top
&.vertical-layout {
div.overlay {
justify-content: center;
padding: 1rem;
}
}
`;
export default StyledWrapper;

View File

@@ -1,19 +1,21 @@
import React from 'react';
import { IconRefresh } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { cancelRequest } from 'providers/ReduxStore/slices/collections/actions';
import StopWatch from '../../StopWatch';
import StyledWrapper from './StyledWrapper';
const ResponseLoadingOverlay = ({ item, collection }) => {
const dispatch = useDispatch();
const preferences = useSelector((state) => state.app.preferences);
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
const handleCancelRequest = () => {
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
};
return (
<StyledWrapper className="w-full">
<StyledWrapper className={`w-full ${isVerticalLayout ? 'vertical-layout' : ''}`}>
<div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>

View File

@@ -1,12 +1,19 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
padding-top: 20%;
width: 100%;
.send-icon {
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
}
&.vertical-layout {
padding: 1rem;
justify-content: center;
}
`;
export default StyledWrapper;

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { IconSend } from '@tabler/icons';
import { useSelector } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import { isMacOS } from 'utils/common/platform';
@@ -8,9 +9,11 @@ const Placeholder = () => {
const sendRequestShortcut = isMac ? 'Cmd + Enter' : 'Ctrl + Enter';
const newRequestShortcut = isMac ? 'Cmd + B' : 'Ctrl + B';
const editEnvironmentShortcut = isMac ? 'Cmd + E' : 'Ctrl + E';
const preferences = useSelector((state) => state.app.preferences);
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
return (
<StyledWrapper>
<StyledWrapper className={`${isVerticalLayout ? 'vertical-layout' : ''}`}>
<div className="send-icon flex justify-center" style={{ fontSize: 200 }}>
<IconSend size={150} strokeWidth={1} />
</div>

View File

@@ -74,7 +74,7 @@ const formatErrorMessage = (error) => {
return error;
};
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
const QueryResult = ({ item, collection, data, dataBuffer, disableRunEventListener, headers, error }) => {
const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
const [filter, setFilter] = useState(null);
@@ -164,7 +164,6 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
return (
<StyledWrapper
className="w-full h-full relative flex"
style={{ maxWidth: width }}
queryFilterEnabled={queryFilterEnabled}
>
<div className="flex justify-end gap-2 text-xs" role="tablist">

View File

@@ -0,0 +1,15 @@
import styled from 'styled-components';
const Wrapper = styled.div`
button {
display: flex;
align-items: center;
padding: 0.25rem;
background: transparent;
border: none;
cursor: pointer;
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { savePreferences } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
const IconDockToBottom = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
fill="none"
>
<path stroke="none" fill="none" d="M0 0h24v24H0z" />
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
<path d="M4 15l16 0" />
<path
fill="currentColor"
d="M 5.5135136,19.111502 C 5.2542477,18.995986 5.0221761,18.756859 4.8928709,18.47199 4.7922381,18.250288 4.7788524,18.078909 4.7777079,16.997543 l -0.0013,-1.223586 H 12 19.223587 v 1.22675 c 0,1.194609 -0.0039,1.234605 -0.149369,1.526503 -0.09333,0.187285 -0.240773,0.363095 -0.392978,0.46858 l -0.243606,0.168829 -6.373606,0.0129 c -5.2129418,0.0105 -6.4058225,-0.0015 -6.5505114,-0.06597 z"
/>
</svg>
);
};
const IconDockToRight = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
fill="none"
>
<path fill="none" stroke="none" d="M 0,24 V 0 h 24 v 24 z" />
<path d="m 4,20 m 2,0 A 2,2 0 0 1 4,18 V 6 A 2,2 0 0 1 6,4 h 12 a 2,2 0 0 1 2,2 v 12 a 2,2 0 0 1 -2,2 z" />
<path d="M 15,20 V 4" />
<path
fill="currentColor"
stroke="currentColor"
d="m 19.111502,18.486486 c -0.115516,0.259266 -0.354643,0.491338 -0.639512,0.620643 -0.221702,0.100633 -0.393081,0.114019 -1.474447,0.115163 l -1.223586,0.0013 V 12 4.7764125 h 1.22675 c 1.194609,0 1.234605,0.0039 1.526503,0.14937 0.187285,0.09333 0.363095,0.2407725 0.46858,0.3929775 l 0.168829,0.243606 0.0129,6.373606 c 0.0105,5.212942 -0.0015,6.405822 -0.06597,6.550511 z"
/>
</svg>
);
};
const ResponseLayoutToggle = () => {
const dispatch = useDispatch();
const preferences = useSelector((state) => state.app.preferences);
const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';
const toggleOrientation = () => {
const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
const updatedPreferences = {
...preferences,
layout: {
...preferences.layout,
responsePaneOrientation: newOrientation
}
};
dispatch(savePreferences(updatedPreferences));
};
return (
<StyledWrapper className="ml-2 flex items-center">
<button
onClick={toggleOrientation}
title={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
>
{orientation === 'horizontal' ? (
<IconDockToBottom />
) : (
<IconDockToRight />
)}
</button>
</StyledWrapper>
);
};
export default ResponseLayoutToggle;

View File

@@ -0,0 +1,173 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, screen, fireEvent} from '@testing-library/react';
import { Provider } from 'react-redux';
import { ThemeProvider } from 'providers/Theme';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import ResponseLayoutToggle from './index';
const mockSavePreferences = jest.fn((payload) => ({ type: 'app/savePreferences', payload }));
// Mock the savePreferences action
jest.mock('providers/ReduxStore/slices/app', () => ({
savePreferences: (payload) => mockSavePreferences(payload)
}));
// Mock localStorage
const mockLocalStorage = {
getItem: jest.fn(() => 'dark'),
setItem: jest.fn(),
removeItem: jest.fn()
};
// Mock matchMedia
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
})),
});
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage
});
});
beforeEach(() => {
mockSavePreferences.mockClear();
});
const initialState = {
app: {
preferences: {
layout: {
responsePaneOrientation: 'horizontal'
}
}
}
};
const createTestStore = (initialState) => {
const appSlice = createSlice({
name: 'app',
initialState: initialState.app,
reducers: {
savePreferences: (state, action) => {
state.preferences = action.payload;
}
}
});
return configureStore({
reducer: { app: appSlice.reducer }
});
};
const renderWithProviders = (component, customState = initialState) => {
const store = createTestStore(customState);
return {
store,
...render(
<Provider store={store}>
<ThemeProvider>
{component}
</ThemeProvider>
</Provider>
)
};
};
describe('ResponseLayoutToggle', () => {
describe('Initial Render', () => {
it('should render with horizontal orientation by default', () => {
renderWithProviders(<ResponseLayoutToggle />);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
});
it('should render with vertical orientation when specified', () => {
const customState = {
app: {
preferences: {
layout: {
responsePaneOrientation: 'vertical'
}
}
}
};
renderWithProviders(<ResponseLayoutToggle />, customState);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
});
});
describe('Interaction', () => {
it('should switch to vertical layout when clicked in horizontal mode', () => {
const { store } = renderWithProviders(<ResponseLayoutToggle />);
const button = screen.getByRole('button');
// Initial state check
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
fireEvent.click(button);
// Check if action was called
expect(mockSavePreferences).toHaveBeenCalledWith({
layout: {
responsePaneOrientation: 'vertical'
}
});
// Manually update store to simulate state change
store.dispatch(mockSavePreferences({
layout: {
responsePaneOrientation: 'vertical'
}
}));
// Check if button title was updated
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
});
it('should switch to horizontal layout when clicked in vertical mode', () => {
const customState = {
app: {
preferences: {
layout: {
responsePaneOrientation: 'vertical'
}
}
}
};
const { store } = renderWithProviders(<ResponseLayoutToggle />, customState);
const button = screen.getByRole('button');
// Initial state check
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
fireEvent.click(button);
// Check if action was called
expect(mockSavePreferences).toHaveBeenCalledWith({
layout: {
responsePaneOrientation: 'horizontal'
}
});
// Manually update store to simulate state change
store.dispatch(mockSavePreferences({
layout: {
responsePaneOrientation: 'horizontal'
}
}));
// Check if button title was updated
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
});
});
});

View File

@@ -20,8 +20,10 @@ import ResponseSave from 'src/components/ResponsePane/ResponseSave';
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
import SkippedRequest from './SkippedRequest';
import ClearTimeline from './ClearTimeline/index';
import ResponseLayoutToggle from './ResponseLayoutToggle';
import HeightBoundContainer from 'ui/HeightBoundContainer';
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const ResponsePane = ({ item, collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
@@ -57,7 +59,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
<QueryResult
item={item}
collection={collection}
width={rightPaneWidth}
data={response.data}
dataBuffer={response.dataBuffer}
headers={response.headers}
@@ -70,7 +71,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
return <ResponseHeaders headers={response.headers} />;
}
case 'timeline': {
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
return <Timeline collection={collection} item={item} />;
}
case 'tests': {
return <TestResults
@@ -105,9 +106,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
if (!item.response && !requestTimeline?.length) {
return (
<StyledWrapper className="flex h-full relative">
<HeightBoundContainer>
<Placeholder />
</StyledWrapper>
</HeightBoundContainer>
);
}
@@ -159,6 +160,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
onClick={() => setShowScriptErrorCard(true)}
/>
)}
<ResponseLayoutToggle />
{focusedTab?.responsePaneTab === "timeline" ? (
<ClearTimeline item={item} collection={collection} />
) : (item?.response && !item?.response?.error) ? (
@@ -193,7 +195,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
<Timeline
collection={collection}
item={item}
width={rightPaneWidth}
/>
) : null
) : (

View File

@@ -1,6 +1,5 @@
import { createSlice } from '@reduxjs/toolkit';
import filter from 'lodash/filter';
import toast from 'react-hot-toast';
const initialState = {
isDragging: false,
@@ -103,14 +102,9 @@ export const savePreferences = (preferences) => (dispatch, getState) => {
ipcRenderer
.invoke('renderer:save-preferences', preferences)
.then(() => toast.success('Preferences saved successfully'))
.then(() => dispatch(updatePreferences(preferences)))
.then(resolve)
.catch((err) => {
toast.error('An error occurred while saving preferences');
console.error(err);
reject(err);
});
.catch(reject);
});
};

View File

@@ -1,3 +1,4 @@
import React from 'react';
import themes from 'themes/index';
import useLocalStorage from 'hooks/useLocalStorage/index';

View File

@@ -0,0 +1,25 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
/* Primary container - establishes flex context */
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
/* Flex shrink container - allows content to be constrained */
.height-constraint {
display: flex;
flex: 1 1 0;
min-height: 0;
}
/* Grid container - enforces boundaries */
.grid-boundary {
width: 100%;
display: grid;
overflow-y: auto;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const HeightBoundContainer = ({children}) => {
return (
<StyledWrapper>
<div className="height-constraint">
<div className="grid-boundary">
{children}
</div>
</div>
</StyledWrapper>
);
};
export default HeightBoundContainer;

View File

@@ -37,6 +37,9 @@ const defaultPreferences = {
password: ''
},
bypassProxy: ''
},
layout: {
responsePaneOrientation: 'horizontal'
}
};
@@ -69,6 +72,9 @@ const preferencesSchema = Yup.object().shape({
password: Yup.string().max(1024)
}).optional(),
bypassProxy: Yup.string().optional().max(1024)
}),
layout: Yup.object({
responsePaneOrientation: Yup.string().oneOf(['horizontal', 'vertical'])
})
});
@@ -149,6 +155,9 @@ const preferencesUtil = {
shouldSendCookies: () => {
return get(getPreferences(), 'request.sendCookies', true);
},
getResponsePaneOrientation: () => {
return get(getPreferences(), 'layout.responsePaneOrientation', 'horizontal');
},
getSystemProxyEnvVariables: () => {
const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
return {