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.
This commit is contained in:
Abhishek S Lal
2026-05-05 22:28:05 +05:30
committed by GitHub
parent 69417adcbf
commit 04732fa3d1
11 changed files with 488 additions and 56 deletions

2
.gitignore vendored
View File

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

View File

@@ -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 (
<StyledWrapper>
<div className="swagger-root w-full">
<SwaggerUI spec={spec} />
<SwaggerUI spec={spec} onComplete={onComplete} />
</div>
</StyledWrapper>
);
};
export default Swagger;
export default memo(Swagger);

View File

@@ -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 (
<section className="main flex flex-grow pl-4 relative">
<div className="w-full grid grid-cols-2">
<div className="col-span-1">
<div className="flex flex-grow relative">
<CodeEditor
theme={displayedTheme}
value={readOnly ? content : editorContent}
readOnly={readOnly ? 'nocursor' : false}
onEdit={readOnly ? undefined : (val) => setEditorContent(val)}
onSave={readOnly ? undefined : handleSave}
mode="yaml"
font={get(preferences, 'font.codeFont', 'default')}
/>
{!readOnly && onSave && (
<IconDeviceFloppy
onClick={handleSave}
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={22}
className={`absolute right-0 top-0 m-4 ${
hasChanges ? 'cursor-pointer opacity-100' : 'cursor-default opacity-50'
}`}
/>
)}
<section
ref={mainSectionRef}
className={`main flex flex-grow pl-4 relative ${dragging ? 'dragging' : ''}`}
>
<div
className="api-spec-left-pane flex flex-grow relative h-full"
style={leftPaneStyle}
>
<CodeEditor
theme={displayedTheme}
value={readOnly ? content : editorContent}
readOnly={readOnly ? 'nocursor' : false}
onEdit={readOnly ? undefined : (val) => setEditorContent(val)}
onSave={readOnly ? undefined : handleSave}
mode="yaml"
font={get(preferences, 'font.codeFont', 'default')}
/>
{!readOnly && onSave && (
<IconDeviceFloppy
onClick={handleSave}
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={22}
className={`absolute right-0 top-0 m-4 ${
hasChanges ? 'cursor-pointer opacity-100' : 'cursor-default opacity-50'
}`}
/>
)}
</div>
<div className="dragbar-wrapper" {...dragbarProps}>
<div className="dragbar-handle" />
</div>
<div
className="api-spec-right-pane relative"
style={{ flex: '1 1 50%', minWidth: 0 }}
>
<div style={{ visibility: swaggerReady ? 'visible' : 'hidden', height: '100%' }}>
<Swagger spec={content} onComplete={handleSwaggerComplete} />
</div>
{!swaggerReady && (
<div
className="absolute inset-0 flex items-center justify-center gap-2"
style={{ background: theme.bg }}
>
<div className="flex items-center justify-center gap-2 opacity-70">
<IconLoader2 size={20} className="animate-spin" />
<span>Generating preview</span>
</div>
</div>
</div>
<div className="col-span-1">
<Suspense fallback="">
<Swagger spec={content} />
</Suspense>
</div>
)}
</div>
</section>
);

View File

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

View File

@@ -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 <div className="p-4 opacity-50">API Spec not found!</div>;
}
@@ -79,6 +88,8 @@ const ApiSpecPanel = () => {
<SpecViewer
content={raw}
onSave={(content) => dispatch(saveApiSpecToFile({ uid, content }))}
leftPaneWidth={leftPaneWidth ?? null}
onLeftPaneWidthChange={handleLeftPaneWidthChange}
/>
</StyledWrapper>
);

View File

@@ -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 }) => {
<span>Showing spec file from {sourceUrl}.</span>
</div>
)}
<SpecViewer content={specContent} readOnly />
<SpecViewer
content={specContent}
readOnly
leftPaneWidth={leftPaneWidth}
onLeftPaneWidthChange={handleLeftPaneWidthChange}
/>
</StyledWrapper>
);
};

View File

@@ -376,7 +376,7 @@ const RequestTabPanel = () => {
}
if (focusedTab.type === 'openapi-spec') {
return <OpenAPISpecTab collection={collection} />;
return <OpenAPISpecTab collection={collection} tabUid={focusedTab.uid} />;
}
if (!item || !item.uid) {

View File

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

View File

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

View File

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

View File

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