diff --git a/packages/bruno-app/jsconfig.json b/packages/bruno-app/jsconfig.json index 867626852..a71bc3138 100644 --- a/packages/bruno-app/jsconfig.json +++ b/packages/bruno-app/jsconfig.json @@ -6,6 +6,7 @@ "baseUrl": "./", "paths": { "assets/*": ["src/assets/*"], + "ui/*": ["src/ui/*"], "components/*": ["src/components/*"], "hooks/*": ["src/hooks/*"], "themes/*": ["src/themes/*"], diff --git a/packages/bruno-app/src/components/Preferences/Display/Font/index.js b/packages/bruno-app/src/components/Preferences/Display/Font/index.js index 622ea0817..e6bbf9c3f 100644 --- a/packages/bruno-app/src/components/Preferences/Display/Font/index.js +++ b/packages/bruno-app/src/components/Preferences/Display/Font/index.js @@ -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') }); }; diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js index 929eae0ff..554dd0d72 100644 --- a/packages/bruno-app/src/components/Preferences/General/index.js +++ b/packages/bruno-app/src/components/Preferences/General/index.js @@ -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')); diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js index e7ac735c7..16695f60f 100644 --- a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js +++ b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js @@ -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) => { diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js index 07dcf1419..34558d928 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js @@ -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 -
{getTabPanel(focusedTab.requestPaneTab)}
+
+ {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 4c7e6029b..126613504 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -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)} + + {getTabPanel(focusedTab.requestPaneTab)} + ); diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 3f7f7ef01..8fe1cd00b 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -114,7 +114,7 @@ const QueryParams = ({ item, collection }) => { }; return ( - +
Query
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; diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 5f53a5e02..19e52ae0f 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -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 ; @@ -197,15 +231,19 @@ const RequestTabPanel = () => { }; return ( - +
-
+
@@ -213,7 +251,6 @@ const RequestTabPanel = () => { { ) : null} {item.type === 'http-request' ? ( - + ) : null}
-
-
+
+
- +
diff --git a/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js index 045a9dcc3..e60eb7024 100644 --- a/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/ResponsePane/Overlay/index.js b/packages/bruno-app/src/components/ResponsePane/Overlay/index.js index 8ede2d6ec..429c4889a 100644 --- a/packages/bruno-app/src/components/ResponsePane/Overlay/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Overlay/index.js @@ -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 ( - +
diff --git a/packages/bruno-app/src/components/ResponsePane/Placeholder/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/Placeholder/StyledWrapper.js index f6d7a09c5..369637b01 100644 --- a/packages/bruno-app/src/components/ResponsePane/Placeholder/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/Placeholder/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js index bca9e138a..4a315ca26 100644 --- a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js @@ -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 ( - +
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index 229a40072..31f1759cb 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -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 (
diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/StyledWrapper.js new file mode 100644 index 000000000..8cb5d4b43 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/StyledWrapper.js @@ -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; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js new file mode 100644 index 000000000..49299422b --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js @@ -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 ( + + + + + + + ); +}; + +const IconDockToRight = () => { + return ( + + + + + + + ); +}; + +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 ( + + + + ); +}; + +export default ResponseLayoutToggle; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.spec.js b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.spec.js new file mode 100644 index 000000000..0dd1c7b1a --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.spec.js @@ -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( + + + {component} + + + ) + }; +}; + +describe('ResponseLayoutToggle', () => { + describe('Initial Render', () => { + it('should render with horizontal orientation by default', () => { + renderWithProviders(); + 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(, 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(); + 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(, 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'); + }); + }); +}); diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 205a89c56..5d85ce086 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -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 }) => { { return ; } case 'timeline': { - return ; + return ; } case 'tests': { return { if (!item.response && !requestTimeline?.length) { return ( - + - + ); } @@ -159,6 +160,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { onClick={() => setShowScriptErrorCard(true)} /> )} + {focusedTab?.responsePaneTab === "timeline" ? ( ) : (item?.response && !item?.response?.error) ? ( @@ -193,7 +195,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { ) : null ) : ( diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index f19c51101..0fde3c8b2 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -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); }); }; diff --git a/packages/bruno-app/src/providers/Theme/index.js b/packages/bruno-app/src/providers/Theme/index.js index 44025197a..9b741872b 100644 --- a/packages/bruno-app/src/providers/Theme/index.js +++ b/packages/bruno-app/src/providers/Theme/index.js @@ -1,3 +1,4 @@ +import React from 'react'; import themes from 'themes/index'; import useLocalStorage from 'hooks/useLocalStorage/index'; diff --git a/packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js b/packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js new file mode 100644 index 000000000..8770381e7 --- /dev/null +++ b/packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/ui/HeightBoundContainer/index.js b/packages/bruno-app/src/ui/HeightBoundContainer/index.js new file mode 100644 index 000000000..be7b2727a --- /dev/null +++ b/packages/bruno-app/src/ui/HeightBoundContainer/index.js @@ -0,0 +1,16 @@ +import React from 'react'; +import StyledWrapper from './StyledWrapper'; + +const HeightBoundContainer = ({children}) => { + return ( + +
+
+ {children} +
+
+
+ ); +}; + +export default HeightBoundContainer; diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index 33d7a02f8..f5dac56d5 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -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 {