From 04732fa3d13fa7928e24572ce98af10448ec5fb9 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Tue, 5 May 2026 22:28:05 +0530 Subject: [PATCH] feat(api-spec): drag-to-resize split pane with persisted width (#7866) * Add drag-resize split pane for API Spec viewer Introduce a drag-to-resize split pane for the API Spec viewer and persist left pane width. Adds a new useDragResize hook to manage dragging state and clamping, plus UI: dragbar styles, a loading state for the Swagger preview (onComplete + loader), and memoization of the Swagger renderer. Wire up persisted widths via Redux: add updateApiSpecPanelLeftPaneWidth (apiSpec slice) and updateApiSpecTabLeftPaneWidth (tabs slice), and propagate leftPaneWidth / onLeftPaneWidthChange through ApiSpecPanel, OpenAPISpecTab, RequestTabPanel and SpecViewer. Misc: pass tab uid into OpenAPISpecTab and add .gstack/ to .gitignore. * Refactor SpecViewer and OpenAPISpecTab for improved loading and state management - Updated SpecViewer to enhance loading state handling for Swagger content, ensuring a smoother user experience by preventing flashes of unrendered content. - Refactored OpenAPISpecTab to streamline environment context management, optimizing the loading process for OpenAPI specifications. - Simplified the useDragResize hook by removing unnecessary references and improving the handling of drag events, ensuring better performance and responsiveness during resizing actions. * Enhance useDragResize hook to clamp width seed and improve test coverage - Updated the useDragResize hook to clamp the width seed value, ensuring it stays within defined bounds during drag events. - Added a new test case to verify that an out-of-bounds width seed is correctly clamped and persisted on immediate mouseup, enhancing the robustness of the drag-resize functionality. * Remove .gstack/ from .gitignore Delete the .gstack/ ignore entry and normalize the packages/bruno-converters/dist entry in .gitignore (deduplicated). No code changes; just tidy up ignore rules. --- .gitignore | 2 +- .../ApiSpecPanel/Renderers/Swagger/index.js | 7 +- .../src/components/ApiSpecPanel/SpecViewer.js | 126 ++++++++---- .../components/ApiSpecPanel/StyledWrapper.js | 29 +++ .../src/components/ApiSpecPanel/index.js | 17 +- .../src/components/OpenAPISpecTab/index.js | 43 +++- .../src/components/RequestTabPanel/index.js | 2 +- .../src/hooks/useDragResize/index.js | 108 ++++++++++ .../src/hooks/useDragResize/index.spec.js | 193 ++++++++++++++++++ .../providers/ReduxStore/slices/apiSpec.js | 9 +- .../src/providers/ReduxStore/slices/tabs.js | 8 + 11 files changed, 488 insertions(+), 56 deletions(-) create mode 100644 packages/bruno-app/src/hooks/useDragResize/index.js create mode 100644 packages/bruno-app/src/hooks/useDragResize/index.spec.js diff --git a/.gitignore b/.gitignore index 9ebdddea1..8dfcfb9e8 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,4 @@ AGENTS.md packages/bruno-filestore/dist packages/bruno-requests/dist packages/bruno-schema-types/dist -packages/bruno-converters/dist +packages/bruno-converters/dist \ No newline at end of file diff --git a/packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/index.js b/packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/index.js index d37b6b63c..8f276faf2 100644 --- a/packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/index.js +++ b/packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/index.js @@ -1,14 +1,15 @@ +import { memo } from 'react'; import SwaggerUI from 'swagger-ui-react'; import StyledWrapper from './StyledWrapper'; -const Swagger = ({ spec }) => { +const Swagger = ({ spec, onComplete }) => { return (
- +
); }; -export default Swagger; +export default memo(Swagger); diff --git a/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js b/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js index 97d14c65e..ae1a9cdeb 100644 --- a/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js +++ b/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js @@ -1,26 +1,31 @@ -import React, { useState, useEffect, Suspense } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useSelector } from 'react-redux'; -import { IconDeviceFloppy } from '@tabler/icons'; +import { IconDeviceFloppy, IconLoader2 } from '@tabler/icons'; import CodeEditor from './FileEditor/CodeEditor/index'; import Swagger from './Renderers/Swagger'; +import { useDragResize } from 'hooks/useDragResize'; + +const MIN_LEFT_PANE_WIDTH = 300; +const MIN_RIGHT_PANE_WIDTH = 450; /** * Shared split-pane spec viewer: CodeEditor (left) + Swagger preview (right). * * Props: - * - content (string) The spec content (YAML/JSON string) - * - readOnly (boolean) If true, editor is not editable and save icon is hidden - * - onSave (function) Called with current editor content on save (editable mode only) + * - content (string) The spec content (YAML/JSON string) + * - readOnly (boolean) If true, editor is not editable and save icon is hidden + * - onSave (fn) Called with current editor content on save (editable mode only) + * - leftPaneWidth (number|null) Persisted left pane width in px; null = use 50/50 default + * - onLeftPaneWidthChange (fn) Persist the new width (called on mouseup / double-click / resize-clamp) */ -const SpecViewer = ({ content, readOnly, onSave }) => { +const SpecViewer = ({ content, readOnly, onSave, leftPaneWidth, onLeftPaneWidthChange }) => { const { displayedTheme, theme } = useTheme(); const preferences = useSelector((state) => state.app.preferences); const [editorContent, setEditorContent] = useState(content); - // Sync editor when saved content changes from outside (e.g. after save completes) useEffect(() => { setEditorContent(content); }, [content]); @@ -31,38 +36,85 @@ const SpecViewer = ({ content, readOnly, onSave }) => { if (onSave) onSave(editorContent); }; + const mainSectionRef = useRef(null); + const { dragging, dragWidth, dragbarProps } = useDragResize({ + containerRef: mainSectionRef, + width: leftPaneWidth, + onWidthChange: onLeftPaneWidthChange, + minLeft: MIN_LEFT_PANE_WIDTH, + minRight: MIN_RIGHT_PANE_WIDTH + }); + + const effectiveWidth = dragging ? dragWidth : leftPaneWidth; + const leftPaneStyle = effectiveWidth != null + ? { width: `${effectiveWidth}px`, flexShrink: 0 } + : { flex: '1 1 50%', minWidth: 0 }; + + const [swaggerReady, setSwaggerReady] = useState(false); + + useEffect(() => { + setSwaggerReady(false); + }, [content]); + + const handleSwaggerComplete = useCallback(() => { + // Double rAF: wait for one full paint cycle so Swagger is actually on screen + // before hiding the loader — avoids a flash of unrendered content. + requestAnimationFrame(() => { + requestAnimationFrame(() => setSwaggerReady(true)); + }); + }, []); + return ( -
-
-
-
- setEditorContent(val)} - onSave={readOnly ? undefined : handleSave} - mode="yaml" - font={get(preferences, 'font.codeFont', 'default')} - /> - {!readOnly && onSave && ( - - )} +
+
+ setEditorContent(val)} + onSave={readOnly ? undefined : handleSave} + mode="yaml" + font={get(preferences, 'font.codeFont', 'default')} + /> + {!readOnly && onSave && ( + + )} +
+
+
+
+
+
+ +
+ {!swaggerReady && ( +
+
+ + Generating preview… +
-
-
- - - -
+ )}
); diff --git a/packages/bruno-app/src/components/ApiSpecPanel/StyledWrapper.js b/packages/bruno-app/src/components/ApiSpecPanel/StyledWrapper.js index f50f0e054..853582907 100644 --- a/packages/bruno-app/src/components/ApiSpecPanel/StyledWrapper.js +++ b/packages/bruno-app/src/components/ApiSpecPanel/StyledWrapper.js @@ -17,6 +17,35 @@ const StyledWrapper = styled.div` .react-tooltip { z-index: 10; } + + section.main.dragging { + cursor: col-resize; + user-select: none; + } + + div.dragbar-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 10px; + min-width: 10px; + padding: 0; + cursor: col-resize; + background: transparent; + position: relative; + flex-shrink: 0; + + div.dragbar-handle { + display: flex; + height: 100%; + width: 1px; + border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border}; + } + + &:hover div.dragbar-handle { + border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder}; + } + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ApiSpecPanel/index.js b/packages/bruno-app/src/components/ApiSpecPanel/index.js index 35f574000..57311bfde 100644 --- a/packages/bruno-app/src/components/ApiSpecPanel/index.js +++ b/packages/bruno-app/src/components/ApiSpecPanel/index.js @@ -1,11 +1,11 @@ -import React, { forwardRef, useRef } from 'react'; +import React, { forwardRef, useRef, useCallback } from 'react'; import find from 'lodash/find'; import { useSelector, useDispatch } from 'react-redux'; import { IconFileCode, IconDots } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import SpecViewer from './SpecViewer'; import Dropdown from 'components/Dropdown'; -import { openApiSpec, saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec'; +import { openApiSpec, saveApiSpecToFile, updateApiSpecPanelLeftPaneWidth } from 'providers/ReduxStore/slices/apiSpec'; import { useState } from 'react'; import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec'; import toast from 'react-hot-toast'; @@ -21,7 +21,16 @@ const ApiSpecPanel = () => { const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid); - const { filename, pathname, raw, uid } = apiSpec || {}; + const { filename, pathname, raw, uid, leftPaneWidth } = apiSpec || {}; + + const handleLeftPaneWidthChange = useCallback( + (w) => { + if (!uid) return; + dispatch(updateApiSpecPanelLeftPaneWidth({ uid, leftPaneWidth: w })); + }, + [dispatch, uid] + ); + if (!uid) { return
API Spec not found!
; } @@ -79,6 +88,8 @@ const ApiSpecPanel = () => { dispatch(saveApiSpecToFile({ uid, content }))} + leftPaneWidth={leftPaneWidth ?? null} + onLeftPaneWidthChange={handleLeftPaneWidthChange} /> ); diff --git a/packages/bruno-app/src/components/OpenAPISpecTab/index.js b/packages/bruno-app/src/components/OpenAPISpecTab/index.js index 8a67c9514..f9323b121 100644 --- a/packages/bruno-app/src/components/OpenAPISpecTab/index.js +++ b/packages/bruno-app/src/components/OpenAPISpecTab/index.js @@ -1,8 +1,11 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import find from 'lodash/find'; import { IconLoader2, IconCloud } from '@tabler/icons'; import fastJsonFormat from 'fast-json-format'; import SpecViewer from 'components/ApiSpecPanel/SpecViewer'; import StyledWrapper from 'components/ApiSpecPanel/StyledWrapper'; +import { updateApiSpecTabLeftPaneWidth } from 'providers/ReduxStore/slices/tabs'; /** * Pretty-print JSON content for readable display. YAML content is returned as-is. @@ -17,7 +20,17 @@ const prettyPrintSpec = (content) => { } }; -const OpenAPISpecTab = ({ collection }) => { +const OpenAPISpecTab = ({ collection, tabUid }) => { + const dispatch = useDispatch(); + const leftPaneWidth = useSelector((state) => { + const tab = find(state.tabs.tabs, (t) => t.uid === tabUid); + return tab?.apiSpecLeftPaneWidth ?? null; + }); + const handleLeftPaneWidthChange = useCallback( + (w) => dispatch(updateApiSpecTabLeftPaneWidth({ uid: tabUid, apiSpecLeftPaneWidth: w })), + [dispatch, tabUid] + ); + const [specContent, setSpecContent] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -26,6 +39,16 @@ const OpenAPISpecTab = ({ collection }) => { const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0]; const sourceUrl = openApiSyncConfig?.sourceUrl; + // Latest env context for loadSpec's remote-fetch fallback. Kept out of + // loadSpec's deps so toggling a variable doesn't refire the spec load. + const envContextRef = useRef({}); + envContextRef.current = { + activeEnvironmentUid: collection?.activeEnvironmentUid, + environments: collection?.environments, + runtimeVariables: collection?.runtimeVariables, + globalEnvironmentVariables: collection?.globalEnvironmentVariables + }; + const loadSpec = useCallback(async () => { setIsLoading(true); setError(null); @@ -42,12 +65,7 @@ const OpenAPISpecTab = ({ collection }) => { collectionUid: collection.uid, collectionPath: collection.pathname, sourceUrl, - environmentContext: { - activeEnvironmentUid: collection.activeEnvironmentUid, - environments: collection.environments, - runtimeVariables: collection.runtimeVariables, - globalEnvironmentVariables: collection.globalEnvironmentVariables - } + environmentContext: envContextRef.current }); if (fetchResult.content) { setSpecContent(prettyPrintSpec(fetchResult.content)); @@ -64,7 +82,7 @@ const OpenAPISpecTab = ({ collection }) => { } finally { setIsLoading(false); } - }, [collection?.pathname, collection?.uid, collection?.activeEnvironmentUid, collection?.environments, collection?.runtimeVariables, collection?.globalEnvironmentVariables, sourceUrl]); + }, [collection?.pathname, collection?.uid, sourceUrl]); useEffect(() => { if (collection?.pathname) { @@ -97,7 +115,12 @@ const OpenAPISpecTab = ({ collection }) => { Showing spec file from {sourceUrl}.
)} - + ); }; diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index d8eaa4515..138f20dfa 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -376,7 +376,7 @@ const RequestTabPanel = () => { } if (focusedTab.type === 'openapi-spec') { - return ; + return ; } if (!item || !item.uid) { diff --git a/packages/bruno-app/src/hooks/useDragResize/index.js b/packages/bruno-app/src/hooks/useDragResize/index.js new file mode 100644 index 000000000..842c631e3 --- /dev/null +++ b/packages/bruno-app/src/hooks/useDragResize/index.js @@ -0,0 +1,108 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +/** + * Drag-to-resize behavior for a horizontal split pane. + * + * Owns the transient drag state (cursor tracking, draft width, dragging flag) + * and clamps to [minLeft, container.width - minRight]. The persisted width and + * its setter are owned by the caller — pass them in as `width` / `onWidthChange` + * and the hook treats it as a controlled value. + * + * Render pattern in the consumer: + * const effectiveWidth = dragging ? dragWidth : width; + */ +export function useDragResize({ containerRef, width, onWidthChange, minLeft, minRight }) { + // Mirror the live drag width in a ref so handleMouseUp can read the final + // value without taking dragWidth as a dep (would re-create the handler on + // every mousemove and re-run the listener-attach effect). + const dragWidthRef = useRef(null); + const [dragging, setDragging] = useState(false); + const [dragWidth, setDragWidth] = useState(null); + + const clamp = useCallback( + (w) => { + const rect = containerRef.current?.getBoundingClientRect(); + if (!rect || rect.width === 0) return w; + return Math.max(minLeft, Math.min(w, rect.width - minRight)); + }, + [containerRef, minLeft, minRight] + ); + + const handleMouseMove = useCallback( + (e) => { + const rect = containerRef.current?.getBoundingClientRect(); + if (!rect) return; + e.preventDefault(); + const clamped = clamp(e.clientX - rect.left); + dragWidthRef.current = clamped; + setDragWidth(clamped); + }, + [containerRef, clamp] + ); + + const handleMouseUp = useCallback( + (e) => { + e.preventDefault(); + const finalWidth = dragWidthRef.current; + dragWidthRef.current = null; + setDragging(false); + setDragWidth(null); + if (finalWidth != null && onWidthChange) { + onWidthChange(finalWidth); + } + }, + [onWidthChange] + ); + + const onMouseDown = useCallback( + (e) => { + e.preventDefault(); + const rect = containerRef.current?.getBoundingClientRect(); + const seed = width != null ? width : rect ? rect.width / 2 : null; + const seedClamped = seed != null ? clamp(seed) : null; + dragWidthRef.current = seedClamped; + setDragWidth(seedClamped); + setDragging(true); + }, + [containerRef, width, clamp] + ); + + const onDoubleClick = useCallback( + (e) => { + e.preventDefault(); + if (onWidthChange) onWidthChange(null); + }, + [onWidthChange] + ); + + useEffect(() => { + if (!dragging) return; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [dragging, handleMouseMove, handleMouseUp]); + + // Re-clamp the persisted width when the container resizes (e.g. window or + // parent pane shrinks). Only dispatches if the clamped value differs. + // widthRef avoids tearing down the observer on every width change — the + // observer reads the latest width through the ref instead. + const widthRef = useRef(width); + widthRef.current = width; + const hasWidth = width != null; + useEffect(() => { + if (!hasWidth || !containerRef.current) return; + const ro = new ResizeObserver(() => { + const clamped = clamp(widthRef.current); + if (clamped !== widthRef.current && onWidthChange) { + onWidthChange(clamped); + } + }); + ro.observe(containerRef.current); + return () => ro.disconnect(); + }, [hasWidth, clamp, onWidthChange, containerRef]); + + return { dragging, dragWidth, dragbarProps: { onMouseDown, onDoubleClick } }; +} diff --git a/packages/bruno-app/src/hooks/useDragResize/index.spec.js b/packages/bruno-app/src/hooks/useDragResize/index.spec.js new file mode 100644 index 000000000..dffdbebe4 --- /dev/null +++ b/packages/bruno-app/src/hooks/useDragResize/index.spec.js @@ -0,0 +1,193 @@ +const { describe, it, expect, beforeEach, afterEach, jest } = require('@jest/globals'); +import { renderHook, act } from '@testing-library/react'; +import { useRef } from 'react'; +import { useDragResize } from './index'; + +const CONTAINER_WIDTH = 1000; +const MIN_LEFT = 200; +const MIN_RIGHT = 300; + +const makeContainer = (width = CONTAINER_WIDTH) => { + const el = document.createElement('div'); + el.getBoundingClientRect = jest.fn(() => ({ + left: 0, + top: 0, + width, + height: 600, + right: width, + bottom: 600, + x: 0, + y: 0 + })); + return el; +}; + +const renderDragResize = ({ width, onWidthChange = jest.fn(), container } = {}) => { + const containerEl = container ?? makeContainer(); + const result = renderHook( + ({ width, onWidthChange }) => { + const containerRef = useRef(containerEl); + return useDragResize({ + containerRef, + width, + onWidthChange, + minLeft: MIN_LEFT, + minRight: MIN_RIGHT + }); + }, + { initialProps: { width, onWidthChange } } + ); + return { ...result, containerEl, onWidthChange }; +}; + +const fireMouse = (type, clientX) => { + act(() => { + document.dispatchEvent(new MouseEvent(type, { clientX, bubbles: true })); + }); +}; + +describe('useDragResize', () => { + let observers; + + beforeEach(() => { + observers = []; + global.ResizeObserver = jest.fn().mockImplementation((callback) => { + const instance = { + callback, + observe: jest.fn(), + disconnect: jest.fn() + }; + observers.push(instance); + return instance; + }); + }); + + afterEach(() => { + delete global.ResizeObserver; + }); + + it('returns false dragging and null dragWidth on initial render', () => { + const { result } = renderDragResize({ width: 500 }); + expect(result.current.dragging).toBe(false); + expect(result.current.dragWidth).toBe(null); + }); + + it('onMouseDown seeds dragWidth from the width prop and flips dragging on', () => { + const { result } = renderDragResize({ width: 500 }); + + act(() => { + result.current.dragbarProps.onMouseDown({ preventDefault: jest.fn() }); + }); + + expect(result.current.dragging).toBe(true); + expect(result.current.dragWidth).toBe(500); + }); + + it('onMouseDown seeds dragWidth to half the container when width is null', () => { + const { result } = renderDragResize({ width: null }); + + act(() => { + result.current.dragbarProps.onMouseDown({ preventDefault: jest.fn() }); + }); + + expect(result.current.dragWidth).toBe(CONTAINER_WIDTH / 2); + }); + + it('onMouseDown clamps an out-of-bounds width seed; immediate mouseup commits the clamped value', () => { + // Persisted width is past the right bound (max = 1000 - 300 = 700). + // Without clamping the seed, an immediate mouseup would persist 800. + const { result, onWidthChange } = renderDragResize({ width: 800 }); + + act(() => { + result.current.dragbarProps.onMouseDown({ preventDefault: jest.fn() }); + }); + + expect(result.current.dragWidth).toBe(CONTAINER_WIDTH - MIN_RIGHT); + + fireMouse('mouseup', 800); + + expect(onWidthChange).toHaveBeenCalledTimes(1); + expect(onWidthChange).toHaveBeenCalledWith(CONTAINER_WIDTH - MIN_RIGHT); + }); + + it('mousemove during drag updates dragWidth clamped to [minLeft, containerWidth - minRight]', () => { + const { result } = renderDragResize({ width: 500 }); + + act(() => { + result.current.dragbarProps.onMouseDown({ preventDefault: jest.fn() }); + }); + + // Within bounds + fireMouse('mousemove', 600); + expect(result.current.dragWidth).toBe(600); + + // Below minLeft → clamps up + fireMouse('mousemove', 50); + expect(result.current.dragWidth).toBe(MIN_LEFT); + + // Above containerWidth - minRight → clamps down + fireMouse('mousemove', 950); + expect(result.current.dragWidth).toBe(CONTAINER_WIDTH - MIN_RIGHT); + }); + + it('mouseup commits the final width via onWidthChange and clears drag state', () => { + const { result, onWidthChange } = renderDragResize({ width: 500 }); + + act(() => { + result.current.dragbarProps.onMouseDown({ preventDefault: jest.fn() }); + }); + fireMouse('mousemove', 650); + fireMouse('mouseup', 650); + + expect(onWidthChange).toHaveBeenCalledTimes(1); + expect(onWidthChange).toHaveBeenCalledWith(650); + expect(result.current.dragging).toBe(false); + expect(result.current.dragWidth).toBe(null); + }); + + it('onDoubleClick calls onWidthChange(null) to reset', () => { + const { result, onWidthChange } = renderDragResize({ width: 500 }); + + act(() => { + result.current.dragbarProps.onDoubleClick({ preventDefault: jest.fn() }); + }); + + expect(onWidthChange).toHaveBeenCalledWith(null); + }); + + it('mousemove without an active drag is a no-op', () => { + const { result } = renderDragResize({ width: 500 }); + + fireMouse('mousemove', 600); + + expect(result.current.dragging).toBe(false); + expect(result.current.dragWidth).toBe(null); + }); + + it('ResizeObserver re-clamps the persisted width when the container shrinks', () => { + const containerEl = makeContainer(1000); + const onWidthChange = jest.fn(); + renderDragResize({ width: 800, onWidthChange, container: containerEl }); + + // 800 fits within 1000 - 300 (minRight) = 700? No: 800 > 700, so already + // out of bounds — but the effect only re-clamps on observed resize, so + // shrink the container and trigger the observer manually. + containerEl.getBoundingClientRect = jest.fn(() => ({ + left: 0, + top: 0, + width: 600, + height: 600, + right: 600, + bottom: 600, + x: 0, + y: 0 + })); + + act(() => { + observers[0].callback(); + }); + + // 800 clamped to 600 - 300 = 300 + expect(onWidthChange).toHaveBeenCalledWith(300); + }); +}); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/apiSpec.js b/packages/bruno-app/src/providers/ReduxStore/slices/apiSpec.js index 6c951c8a2..5a8dcea39 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/apiSpec.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/apiSpec.js @@ -59,6 +59,13 @@ export const apiSpecSlice = createSlice({ setActiveApiSpecUid: (state, action) => { state.activeApiSpecUid = action.payload.uid; }, + updateApiSpecPanelLeftPaneWidth: (state, action) => { + const { uid, leftPaneWidth } = action.payload; + const apiSpec = findApiSpecByUid(state.apiSpecs, uid); + if (apiSpec) { + apiSpec.leftPaneWidth = leftPaneWidth; + } + }, removeApiSpec: (state, action) => { const { uid } = action.payload; let apiSpecIndex = state.apiSpecs.findIndex((c) => c.uid == uid); @@ -70,7 +77,7 @@ export const apiSpecSlice = createSlice({ } }); -export const { apiSpecAddFileEvent, apiSpecChangeFileEvent, saveApiSpec, removeApiSpec, setActiveApiSpecUid } = apiSpecSlice.actions; +export const { apiSpecAddFileEvent, apiSpecChangeFileEvent, saveApiSpec, removeApiSpec, setActiveApiSpecUid, updateApiSpecPanelLeftPaneWidth } = apiSpecSlice.actions; export default apiSpecSlice.reducer; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index af50e3bc1..3b713120a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -159,6 +159,13 @@ export const tabsSlice = createSlice({ tab.requestPaneHeight = action.payload.requestPaneHeight; } }, + updateApiSpecTabLeftPaneWidth: (state, action) => { + const tab = find(state.tabs, (t) => t.uid === action.payload.uid); + + if (tab) { + tab.apiSpecLeftPaneWidth = action.payload.apiSpecLeftPaneWidth; + } + }, updateRequestPaneTab: (state, action) => { const tab = find(state.tabs, (t) => t.uid === action.payload.uid); @@ -416,6 +423,7 @@ export const { switchTab, updateRequestPaneTabWidth, updateRequestPaneTabHeight, + updateApiSpecTabLeftPaneWidth, updateRequestPaneTab, updateResponsePaneTab, updateResponseFormat,