feat: Persist response body scroll position across tabs (#3902)

This commit is contained in:
Coel Aspey
2025-08-25 12:51:34 +01:00
committed by GitHub
parent 54c41c861e
commit 325d03b92f
4 changed files with 84 additions and 33 deletions

View File

@@ -186,6 +186,8 @@ export default class CodeEditor extends React.Component {
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
editor.on('change', this._onEdit);
editor.on('scroll', this.onScroll);
editor.scrollTo(null, this.props.initialScroll);
this.addOverlay();
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
@@ -230,12 +232,18 @@ export default class CodeEditor extends React.Component {
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
if (this.props.initialScroll !== prevProps.initialScroll) {
this.editor.scrollTo(null, this.props.initialScroll);
}
this.ignoreChangeEvent = false;
}
componentWillUnmount() {
if (this.editor) {
this.editor.off('change', this._onEdit);
this.editor.off('scroll', this.onScroll);
this.editor = null;
}
@@ -271,6 +279,8 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('mode', 'brunovariables');
};
onScroll = (event) => this.props.onScroll?.(event);
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
import CodeEditor from 'components/CodeEditor/index';
import { get } from 'lodash';
import find from 'lodash/find';
import { useDispatch, useSelector } from 'react-redux';
import { updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { Document, Page } from 'react-pdf';
import 'pdfjs-dist/build/pdf.worker';
@@ -51,6 +53,10 @@ const QueryResultPreview = ({
displayedTheme
}) => {
const preferences = useSelector((state) => state.app.preferences);
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const dispatch = useDispatch();
const [numPages, setNumPages] = useState(null);
@@ -66,9 +72,19 @@ const QueryResultPreview = ({
if (disableRunEventListener) {
return;
}
dispatch(sendRequest(item, collection.uid));
};
const onScroll = (event) => {
dispatch(
updateResponsePaneScrollPosition({
uid: focusedTab.uid,
scrollY: event.doc.scrollTop
})
);
};
switch (previewTab?.mode) {
case 'preview-web': {
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent?.url || ''}">`);
@@ -111,8 +127,10 @@ const QueryResultPreview = ({
fontSize={get(preferences, 'font.codeFontSize')}
theme={displayedTheme}
onRun={onRun}
onScroll={onScroll}
value={formattedData}
mode={mode}
initialScroll={focusedTab.responsePaneScrollPosition || 0}
readOnly
/>
);

View File

@@ -45,7 +45,7 @@ import {
} from './index';
import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { closeAllCollectionTabs, updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
import { parsePathParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
@@ -258,6 +258,13 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const requestUid = uuid();
itemCopy.requestUid = requestUid;
await dispatch(
updateResponsePaneScrollPosition({
uid: state.tabs.activeTabUid,
scrollY: 0
})
);
await dispatch(
initRunRequestEvent({
requestUid,
@@ -1627,27 +1634,33 @@ export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
};
// todo: could be removed
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
export const loadRequestViaWorker =
({ collectionUid, pathname }) =>
(dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
// todo: could be removed
export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
export const loadRequest =
({ collectionUid, pathname }) =>
(dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
export const loadLargeRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
export const loadLargeRequest =
({ collectionUid, pathname }) =>
(dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
export const mountCollection =
({ collectionUid, collectionPathname, brunoConfig }) =>
@@ -1671,16 +1684,17 @@ export const showInFolder = (collectionPath) => () => {
});
};
export const updateRunnerConfiguration = (collectionUid, selectedRequestItems, requestItemsOrder, delay) => (dispatch) => {
dispatch(
_updateRunnerConfiguration({
collectionUid,
selectedRequestItems,
requestItemsOrder,
delay
})
);
};
export const updateRunnerConfiguration =
(collectionUid, selectedRequestItems, requestItemsOrder, delay) => (dispatch) => {
dispatch(
_updateRunnerConfiguration({
collectionUid,
selectedRequestItems,
requestItemsOrder,
delay
})
);
};
export const updateActiveConnectionsInStore = (activeConnectionIds) => (dispatch, getState) => {
dispatch(updateActiveConnections(activeConnectionIds));

View File

@@ -62,10 +62,10 @@ export const tabsSlice = createSlice({
? preview
: !nonReplaceableTabTypes.includes(type),
...(uid ? { folderUid: uid } : {})
}
};
state.activeTabUid = uid;
return
return;
}
state.tabs.push({
@@ -74,6 +74,7 @@ export const tabsSlice = createSlice({
requestPaneWidth: null,
requestPaneTab: requestPaneTab || defaultRequestPaneTab,
responsePaneTab: 'response',
responsePaneScrollPosition: null,
type: type || 'request',
...(uid ? { folderUid: uid } : {}),
preview: preview !== undefined
@@ -126,6 +127,13 @@ export const tabsSlice = createSlice({
tab.responsePaneTab = action.payload.responsePaneTab;
}
},
updateResponsePaneScrollPosition: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if (tab) {
tab.responsePaneScrollPosition = action.payload.scrollY;
}
},
closeTabs: (state, action) => {
const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);
const tabUids = action.payload.tabUids || [];
@@ -167,8 +175,8 @@ export const tabsSlice = createSlice({
const tab = find(state.tabs, (t) => t.uid === uid);
if (tab) {
tab.preview = false;
} else{
console.error("Tab not found!")
} else {
console.error('Tab not found!');
}
}
}
@@ -181,6 +189,7 @@ export const {
updateRequestPaneTabWidth,
updateRequestPaneTab,
updateResponsePaneTab,
updateResponsePaneScrollPosition,
closeTabs,
closeAllCollectionTabs,
makeTabPermanent