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 (
+
+
+
+ );
+};
+
+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 {