Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ const BodyModeSelector = ({
>
<div className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
{humanizeRequestBodyMode(currentMode)}
{' '}
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
</div>
</MenuDropdown>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
Expand All @@ -11,6 +11,7 @@ import StyledWrapper from './StyledWrapper';
import { IconEdit, IconX, IconFileText } from '@tabler/icons';
import Button from 'ui/Button/index';
import ActionIcon from 'ui/ActionIcon/index';
import { usePersistedContainerScroll } from 'hooks/usePersistedState/usePersistedContainerScroll';

const Docs = ({ collection }) => {
const dispatch = useDispatch();
Expand All @@ -19,6 +20,18 @@ const Docs = ({ collection }) => {
const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');
const preferences = useSelector((state) => state.app.preferences);

// StyledWrapper has overflow-y: auto — use null selector.
// Preview mode: hook tracks wrapper scroll. Edit mode: CodeEditor's onScroll/initialScroll.
const wrapperRef = useRef(null);
const storageKey = usePersistedContainerScroll(wrapperRef, null, `collection-docs-scroll-${collection.uid}`, !isEditing);

const readScroll = () => {
try {
const raw = localStorage.getItem(storageKey);
return raw !== null ? JSON.parse(raw) : 0;
} catch { return 0; }
};

const toggleViewMode = () => {
setIsEditing((prev) => !prev);
};
Expand Down Expand Up @@ -48,7 +61,7 @@ const Docs = ({ collection }) => {
};

return (
<StyledWrapper className="h-full w-full relative flex flex-col">
<StyledWrapper className="h-full w-full relative flex flex-col" ref={wrapperRef}>
<div className="flex flex-row w-full justify-between items-center mb-4">
<div className="text-lg font-medium flex items-center gap-2">
<IconFileText size={20} strokeWidth={1.5} />
Expand Down Expand Up @@ -81,9 +94,11 @@ const Docs = ({ collection }) => {
mode="application/text"
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
initialScroll={readScroll()}
onScroll={(editor) => localStorage.setItem(storageKey, JSON.stringify(editor.doc.scrollTop))}
/>
) : (
<div className="h-full overflow-auto pl-1">
<div className="pl-1">
<div className="h-[1px] min-h-[500px]">
{
docs?.length > 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useRef } from 'react';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
Expand All @@ -13,6 +13,7 @@ import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
import BulkEditor from 'components/BulkEditor/index';
import Button from 'ui/Button';
import { headerNameRegex, headerValueRegex } from 'utils/common/regex';
import { usePersistedContainerScroll } from 'hooks/usePersistedState/usePersistedContainerScroll';

const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);

Expand All @@ -25,6 +26,8 @@ const Headers = ({ collection }) => {
? get(collection, 'draft.root.request.headers', [])
: get(collection, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const wrapperRef = useRef(null);
usePersistedContainerScroll(wrapperRef, '.collection-settings-content', `collection-headers-scroll-${collection.uid}`);

// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
Expand Down Expand Up @@ -120,7 +123,7 @@ const Headers = ({ collection }) => {
}

return (
<StyledWrapper className="h-full w-full">
<StyledWrapper className="h-full w-full" ref={wrapperRef}>
<div className="text-xs mb-4 text-muted">
Add request headers that will be sent with every request in this collection.
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import StatusDot from 'components/StatusDot';
import { flattenItems, isItemARequest } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import Button from 'ui/Button';
import { usePersistedEditorScroll } from 'hooks/usePersistedState/usePersistedEditorScroll';

const Script = ({ collection }) => {
const dispatch = useDispatch();
Expand All @@ -38,13 +39,20 @@ const Script = ({ collection }) => {
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);

// Refresh CodeMirror when tab becomes visible
const preReqScroll = usePersistedEditorScroll(preRequestEditorRef, `collection-pre-req-scroll-${collection.uid}`);
const postResScroll = usePersistedEditorScroll(postResponseEditorRef, `collection-post-res-scroll-${collection.uid}`);

// Refresh CodeMirror when tab becomes visible and restore scroll position.
// CodeMirror's scrollTo() is silently ignored when the editor is inside a display:none container
// (TabsContent hides inactive tabs via display:none). After refresh() recalculates layout, we re-apply scrollTo().
useEffect(() => {
const timer = setTimeout(() => {
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
preRequestEditorRef.current.editor.refresh();
preRequestEditorRef.current.editor.scrollTo(null, preReqScroll);
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
postResponseEditorRef.current.editor.refresh();
postResponseEditorRef.current.editor.scrollTo(null, postResScroll);
}
}, 0);

Expand Down Expand Up @@ -111,6 +119,7 @@ const Script = ({ collection }) => {
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'bru']}
initialScroll={preReqScroll}
/>
</TabsContent>

Expand All @@ -126,6 +135,7 @@ const Script = ({ collection }) => {
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
initialScroll={postResScroll}
/>
</TabsContent>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
Expand All @@ -7,13 +7,16 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
import Button from 'ui/Button';
import { usePersistedEditorScroll } from 'hooks/usePersistedState/usePersistedEditorScroll';

const Tests = ({ collection }) => {
const dispatch = useDispatch();
const testsEditorRef = useRef(null);
const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', '');

const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const testsScroll = usePersistedEditorScroll(testsEditorRef, `collection-tests-scroll-${collection.uid}`);

const onEdit = (value) => {
dispatch(
Expand All @@ -30,6 +33,7 @@ const Tests = ({ collection }) => {
<StyledWrapper className="w-full flex flex-col h-full">
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>
<CodeEditor
ref={testsEditorRef}
collection={collection}
value={tests || ''}
theme={displayedTheme}
Expand All @@ -39,6 +43,7 @@ const Tests = ({ collection }) => {
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
initialScroll={testsScroll}
/>

<div className="mt-6">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import React from 'react';
import React, { useRef } from 'react';
import get from 'lodash/get';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import Button from 'ui/Button';
import { usePersistedContainerScroll } from 'hooks/usePersistedState/usePersistedContainerScroll';

const Vars = ({ collection }) => {
const dispatch = useDispatch();
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));

const wrapperRef = useRef(null);
usePersistedContainerScroll(wrapperRef, '.collection-settings-content', `collection-vars-scroll-${collection.uid}`);

return (
<StyledWrapper className="w-full flex flex-col">
<StyledWrapper className="w-full flex flex-col" ref={wrapperRef}>
<div className="flex-1">
<div className="mb-3 title text-xs">Pre Request</div>
<VarsTable collection={collection} vars={requestVars} varType="request" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const CollectionSettings = ({ collection }) => {
{protobufConfig.protoFiles && protobufConfig.protoFiles.length > 0 && <StatusDot />}
</div>
</div>
<section className="mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
<section className="collection-settings-content mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
</StyledWrapper>
);
};
Expand Down
21 changes: 19 additions & 2 deletions packages/bruno-app/src/components/Documentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import find from 'lodash/find';
import { updateRequestDocs } from 'providers/ReduxStore/slices/collections';
import { updateDocsEditing } from 'providers/ReduxStore/slices/tabs';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
import { usePersistedContainerScroll } from 'hooks/usePersistedState/usePersistedContainerScroll';

const Documentation = ({ item, collection }) => {
const dispatch = useDispatch();
Expand All @@ -21,6 +22,20 @@ const Documentation = ({ item, collection }) => {
const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');
const preferences = useSelector((state) => state.app.preferences);

// Scroll persistence for both edit (CodeMirror) and preview (Markdown) modes using one shared key.
// Preview mode: hook tracks .flex-boundary scroll (enabled only when not editing).
// Edit mode: CodeEditor's onScroll/initialScroll props write/read the same localStorage key.
const wrapperRef = useRef(null);
// Pass null selector — wrapperRef itself is the scrollable container (has overflow-y: auto)
const storageKey = usePersistedContainerScroll(wrapperRef, null, `request-docs-scroll-${item.uid}`, !isEditing);

const readScroll = () => {
try {
const raw = localStorage.getItem(storageKey);
return raw !== null ? JSON.parse(raw) || 0 : 0;
} catch { return 0; }
};
Comment on lines +25 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preview → Edit can mount the editor with a stale scroll.

readScroll() runs during the render that mounts CodeEditor, but usePersistedContainerScroll() only flushes the latest preview scrollTop in its effect cleanup. On a quick scroll-then-toggle, that cleanup happens after this render, so edit mode restores the previous position instead of the one the user just left.

Also applies to: 75-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/Documentation/index.js` around lines 25 -
37, readScroll can return a stale value because usePersistedContainerScroll only
updates localStorage during effect cleanup; fix by reading the live DOM scroll
when available: inside readScroll, if wrapperRef.current exists use
wrapperRef.current.scrollTop (coerce to number) as the source of truth,
otherwise fallback to reading/parsing localStorage via the existing logic;
mention wrapperRef, readScroll, usePersistedContainerScroll and CodeEditor so
the change is applied where the editor mounts to avoid restoring a stale
position.


const toggleViewMode = () => {
dispatch(updateDocsEditing({ uid: activeTabUid, docsEditing: !isEditing }));
};
Expand All @@ -42,7 +57,7 @@ const Documentation = ({ item, collection }) => {
}

return (
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative" ref={wrapperRef}>
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>
Expand All @@ -57,6 +72,8 @@ const Documentation = ({ item, collection }) => {
onEdit={onEdit}
onSave={onSave}
mode="application/text"
initialScroll={readScroll()}
onScroll={(editor) => localStorage.setItem(storageKey, JSON.stringify(editor.doc.scrollTop))}
/>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const StyledWrapper = styled.div`
}

.table-container {
overflow: auto;
border-radius: ${(props) => props.theme.border.radius.base};
border: solid 1px ${(props) => props.theme.border.border0};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateFolderDocs } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import Button from 'ui/Button';
import StyledWrapper from './StyledWrapper';
import { usePersistedContainerScroll } from 'hooks/usePersistedState/usePersistedContainerScroll';

const Documentation = ({ collection, folder }) => {
const dispatch = useDispatch();
Expand All @@ -17,6 +18,19 @@ const Documentation = ({ collection, folder }) => {
const [isEditing, setIsEditing] = useState(false);
const docs = folder.draft ? get(folder, 'draft.docs', '') : get(folder, 'root.docs', '');

// Scroll persistence for both edit (CodeMirror) and preview (Markdown) modes using one shared key.
// Preview mode: hook tracks .folder-settings-content scroll (enabled only when not editing).
// Edit mode: CodeEditor's onScroll/initialScroll props write/read the same localStorage key.
const wrapperRef = useRef(null);
const storageKey = usePersistedContainerScroll(wrapperRef, '.folder-settings-content', `folder-docs-scroll-${folder.uid}`, !isEditing);

const readScroll = () => {
try {
const raw = localStorage.getItem(storageKey);
return raw !== null ? JSON.parse(raw) || 0 : 0;
} catch { return 0; }
};
Comment on lines +21 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Folder docs preview has the same stale-scroll race.

This path also reads from localStorage during the render that switches into edit mode, while the preview scroll hook persists its latest position in cleanup. If the user scrolls and immediately clicks Edit, the editor can reopen at the older saved offset.

Also applies to: 72-73

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/FolderSettings/Documentation/index.js`
around lines 21 - 32, The render-time read of localStorage in readScroll (used
when switching into edit mode) causes a stale-scroll race; move the read out of
render and instead obtain the initial scroll value when edit mode is entered
(e.g., in a useEffect that runs when isEditing becomes true) so the editor uses
the most recent persisted position from usePersistedContainerScroll's cleanup.
Locate usePersistedContainerScroll, wrapperRef and storageKey and change
readScroll so it is invoked lazily in an effect (or via a ref populated by the
preview hook’s cleanup callback) rather than during render; apply the same
change for the similar code around the other instance noted (lines 72-73).


const toggleViewMode = () => {
setIsEditing((prev) => !prev);
};
Expand All @@ -38,7 +52,7 @@ const Documentation = ({ collection, folder }) => {
}

return (
<StyledWrapper className="w-full relative flex flex-col">
<StyledWrapper className="w-full relative flex flex-col" ref={wrapperRef}>
<div className="editing-mode flex justify-between items-center flex-shrink-0" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>
Expand All @@ -55,6 +69,8 @@ const Documentation = ({ collection, folder }) => {
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
mode="application/text"
initialScroll={readScroll()}
onScroll={(editor) => localStorage.setItem(storageKey, JSON.stringify(editor.doc.scrollTop))}
/>
</div>
<div className="mt-6 flex-shrink-0">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from 'styled-components';

const Wrapper = styled.div`
const StyledWrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
Expand Down Expand Up @@ -53,4 +53,4 @@ const Wrapper = styled.div`
}
`;

export default Wrapper;
export default StyledWrapper;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useRef } from 'react';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
Expand All @@ -13,6 +13,7 @@ import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
import BulkEditor from 'components/BulkEditor/index';
import Button from 'ui/Button';
import { headerNameRegex, headerValueRegex } from 'utils/common/regex';
import { usePersistedContainerScroll } from 'hooks/usePersistedState/usePersistedContainerScroll';

const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);

Expand All @@ -25,6 +26,8 @@ const Headers = ({ collection, folder }) => {
? get(folder, 'draft.request.headers', [])
: get(folder, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const wrapperRef = useRef(null);
usePersistedContainerScroll(wrapperRef, '.folder-settings-content', `folder-headers-scroll-${folder.uid}`);

// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
Expand Down Expand Up @@ -125,7 +128,7 @@ const Headers = ({ collection, folder }) => {
}

return (
<StyledWrapper className="w-full">
<StyledWrapper className="w-full" ref={wrapperRef}>
<div className="text-xs mb-4 text-muted">
Request headers that will be sent with every request inside this folder.
</div>
Expand Down
Loading
Loading