Skip to content

Commit b73bf9d

Browse files
authored
feat(app): scroll to and highlight error line on script error (usebruno#8183)
1 parent 9d8c0fd commit b73bf9d

13 files changed

Lines changed: 485 additions & 1 deletion

File tree

packages/bruno-app/src/components/CodeEditor/StyledWrapper.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,32 @@ const StyledWrapper = styled.div`
165165
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
166166
}
167167
168+
@keyframes cm-error-line-flash {
169+
0%, 60% {
170+
background-color: ${(props) => props.theme.status.danger.background};
171+
}
172+
100% {
173+
background-color: transparent;
174+
}
175+
}
176+
177+
.CodeMirror .cm-error-line-flash {
178+
background-color: transparent;
179+
animation: cm-error-line-flash 3s ease-in-out;
180+
}
181+
182+
.CodeMirror .cm-error-line-flash-gutter {
183+
color: ${(props) => props.theme.colors.text.danger} !important;
184+
font-weight: 600;
185+
}
186+
187+
@media (prefers-reduced-motion: reduce) {
188+
.CodeMirror .cm-error-line-flash {
189+
animation: none;
190+
background-color: ${(props) => props.theme.status.danger.background};
191+
}
192+
}
193+
168194
.cm-search-match {
169195
background: rgba(255, 193, 7, 0.25);
170196
}

packages/bruno-app/src/components/CollectionSettings/Script/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { flattenItems, isItemARequest } from 'utils/collections';
1414
import StyledWrapper from './StyledWrapper';
1515
import Button from 'ui/Button';
1616
import { usePersistedState } from 'hooks/usePersistedState';
17+
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
1718

1819
const Script = ({ collection }) => {
1920
const dispatch = useDispatch();
@@ -60,6 +61,20 @@ const Script = ({ collection }) => {
6061
return () => clearTimeout(timer);
6162
}, [activeTab]);
6263

64+
useFocusErrorLine({
65+
uid: collection.uid,
66+
editorRef: preRequestEditorRef,
67+
scriptPhase: 'pre-request',
68+
isVisible: activeTab === 'pre-request'
69+
});
70+
71+
useFocusErrorLine({
72+
uid: collection.uid,
73+
editorRef: postResponseEditorRef,
74+
scriptPhase: 'post-response',
75+
isVisible: activeTab === 'post-response'
76+
});
77+
6378
const onRequestScriptEdit = (value) => {
6479
dispatch(
6580
updateCollectionRequestScript({

packages/bruno-app/src/components/CollectionSettings/Tests/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useTheme } from 'providers/Theme';
99
import StyledWrapper from './StyledWrapper';
1010
import Button from 'ui/Button';
1111
import { usePersistedState } from 'hooks/usePersistedState';
12+
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
1213

1314
const Tests = ({ collection }) => {
1415
const dispatch = useDispatch();
@@ -30,6 +31,12 @@ const Tests = ({ collection }) => {
3031

3132
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
3233

34+
useFocusErrorLine({
35+
uid: collection.uid,
36+
editorRef: testsEditorRef,
37+
scriptPhase: 'test'
38+
});
39+
3340
return (
3441
<StyledWrapper className="w-full flex flex-col h-full">
3542
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>

packages/bruno-app/src/components/FolderSettings/Script/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { flattenItems, isItemARequest } from 'utils/collections';
1414
import StyledWrapper from './StyledWrapper';
1515
import Button from 'ui/Button';
1616
import { usePersistedState } from 'hooks/usePersistedState';
17+
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
1718

1819
const Script = ({ collection, folder }) => {
1920
const dispatch = useDispatch();
@@ -63,6 +64,20 @@ const Script = ({ collection, folder }) => {
6364
return () => clearTimeout(timer);
6465
}, [activeTab]);
6566

67+
useFocusErrorLine({
68+
uid: folder.uid,
69+
editorRef: preRequestEditorRef,
70+
scriptPhase: 'pre-request',
71+
isVisible: activeTab === 'pre-request'
72+
});
73+
74+
useFocusErrorLine({
75+
uid: folder.uid,
76+
editorRef: postResponseEditorRef,
77+
scriptPhase: 'post-response',
78+
isVisible: activeTab === 'post-response'
79+
});
80+
6681
const onRequestScriptEdit = (value) => {
6782
dispatch(
6883
updateFolderRequestScript({

packages/bruno-app/src/components/FolderSettings/Tests/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useTheme } from 'providers/Theme';
99
import StyledWrapper from './StyledWrapper';
1010
import Button from 'ui/Button';
1111
import { usePersistedState } from 'hooks/usePersistedState';
12+
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
1213

1314
const Tests = ({ collection, folder }) => {
1415
const dispatch = useDispatch();
@@ -31,6 +32,12 @@ const Tests = ({ collection, folder }) => {
3132

3233
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
3334

35+
useFocusErrorLine({
36+
uid: folder.uid,
37+
editorRef: testsEditorRef,
38+
scriptPhase: 'test'
39+
});
40+
3441
return (
3542
<StyledWrapper className="w-full flex flex-col h-full">
3643
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>

packages/bruno-app/src/components/RequestPane/Script/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useTheme } from 'providers/Theme';
1212
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
1313
import StatusDot from 'components/StatusDot';
1414
import { usePersistedState } from 'hooks/usePersistedState';
15+
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
1516

1617
const Script = ({ item, collection }) => {
1718
const dispatch = useDispatch();
@@ -57,6 +58,20 @@ const Script = ({ item, collection }) => {
5758
return () => clearTimeout(timer);
5859
}, [activeTab]);
5960

61+
useFocusErrorLine({
62+
uid: item.uid,
63+
editorRef: preRequestEditorRef,
64+
scriptPhase: 'pre-request',
65+
isVisible: activeTab === 'pre-request'
66+
});
67+
68+
useFocusErrorLine({
69+
uid: item.uid,
70+
editorRef: postResponseEditorRef,
71+
scriptPhase: 'post-response',
72+
isVisible: activeTab === 'post-response'
73+
});
74+
6075
const onRequestScriptEdit = (value) => {
6176
dispatch(
6277
updateRequestScript({

packages/bruno-app/src/components/RequestPane/Tests/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
88
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
99
import { useTheme } from 'providers/Theme';
1010
import { usePersistedState } from 'hooks/usePersistedState';
11+
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
1112

1213
const Tests = ({ item, collection }) => {
1314
const dispatch = useDispatch();
@@ -31,6 +32,12 @@ const Tests = ({ item, collection }) => {
3132
const onRun = () => dispatch(sendRequest(item, collection.uid));
3233
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
3334

35+
useFocusErrorLine({
36+
uid: item.uid,
37+
editorRef: testsEditorRef,
38+
scriptPhase: 'test'
39+
});
40+
3441
const requestContext = useMemo(() => buildRequestContextFromItem(item), [item]);
3542

3643
return (

packages/bruno-app/src/components/ResponsePane/ScriptError/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ErrorBanner from 'ui/ErrorBanner';
55
import CodeSnippet from 'components/CodeSnippet';
66
import { getTreePathFromCollectionToItem } from 'utils/collections';
77
import { normalizePath } from 'utils/common/path';
8-
import { addTab, updateRequestPaneTab, updateScriptPaneTab } from 'providers/ReduxStore/slices/tabs';
8+
import { addTab, updateRequestPaneTab, updateScriptPaneTab, setFocusErrorLine } from 'providers/ReduxStore/slices/tabs';
99
import { updateSettingsSelectedTab, updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
1010
import StyledWrapper from './StyledWrapper';
1111

@@ -114,18 +114,28 @@ const ScriptErrorCard = ({ title, message, errorContext, item, collection, scrip
114114
const collectionSettingsTab = scriptPhase === 'test' ? 'tests' : 'script';
115115
const folderSettingsTab = scriptPhase === 'test' ? 'test' : 'script';
116116

117+
const errorLine = errorContext?.errorLine;
118+
const focusPayload = (uid) =>
119+
typeof errorLine === 'number'
120+
? { uid, scriptPhase, line: errorLine, requestedAt: Date.now() }
121+
: null;
122+
117123
if (sourceInfo.sourceType === 'collection') {
118124
dispatch(addTab({ uid: collection.uid, collectionUid: collection.uid, type: 'collection-settings' }));
119125
dispatch(updateSettingsSelectedTab({ collectionUid: collection.uid, tab: collectionSettingsTab }));
120126
if (collectionSettingsTab === 'script') {
121127
dispatch(updateScriptPaneTab({ uid: collection.uid, scriptPaneTab: scriptPhase }));
122128
}
129+
const payload = focusPayload(collection.uid);
130+
if (payload) dispatch(setFocusErrorLine(payload));
123131
} else if (sourceInfo.sourceType === 'folder' && sourceInfo.sourceUid) {
124132
dispatch(addTab({ uid: sourceInfo.sourceUid, collectionUid: collection.uid, type: 'folder-settings' }));
125133
dispatch(updatedFolderSettingsSelectedTab({ collectionUid: collection.uid, folderUid: sourceInfo.sourceUid, tab: folderSettingsTab }));
126134
if (folderSettingsTab === 'script') {
127135
dispatch(updateScriptPaneTab({ uid: sourceInfo.sourceUid, scriptPaneTab: scriptPhase }));
128136
}
137+
const payload = focusPayload(sourceInfo.sourceUid);
138+
if (payload) dispatch(setFocusErrorLine(payload));
129139
} else if (sourceInfo.sourceType === 'request') {
130140
dispatch(addTab({ uid: item.uid, collectionUid: collection.uid, type: 'request' }));
131141
if (scriptPhase === 'test') {
@@ -134,6 +144,8 @@ const ScriptErrorCard = ({ title, message, errorContext, item, collection, scrip
134144
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: 'script' }));
135145
dispatch(updateScriptPaneTab({ uid: item.uid, scriptPaneTab: scriptPhase }));
136146
}
147+
const payload = focusPayload(item.uid);
148+
if (payload) dispatch(setFocusErrorLine(payload));
137149
}
138150
};
139151

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useEffect, useRef } from 'react';
2+
import find from 'lodash/find';
3+
import { useDispatch, useSelector } from 'react-redux';
4+
import { clearFocusErrorLine } from 'providers/ReduxStore/slices/tabs';
5+
import { focusErrorLine } from 'utils/codemirror/focusErrorLine';
6+
7+
/**
8+
* Subscribes a CodeMirror-hosting component to the tab's `focusErrorLine` signal.
9+
* When the signal targets this host's `scriptPhase`, scrolls the editor to the
10+
* line and flashes a red highlight that fades over ~3s. Re-firing for the same
11+
* line is handled via the `requestedAt` token.
12+
*
13+
* @param {object} params
14+
* @param {string} params.uid Tab uid (request/folder/collection uid)
15+
* @param {React.RefObject} params.editorRef Ref to a CodeEditor component (exposes `.editor`)
16+
* @param {string} params.scriptPhase 'pre-request' | 'post-response' | 'test'
17+
* @param {boolean} [params.isVisible=true] Whether this editor's tab is currently shown
18+
*/
19+
export const useFocusErrorLine = ({ uid, editorRef, scriptPhase, isVisible = true }) => {
20+
const dispatch = useDispatch();
21+
const focusErrorLineState = useSelector((state) => {
22+
const tab = find(state.tabs.tabs, (t) => t.uid === uid);
23+
return tab?.focusErrorLine || null;
24+
});
25+
26+
const disposeRef = useRef(null);
27+
28+
useEffect(() => {
29+
if (!focusErrorLineState || !isVisible) return;
30+
31+
if (focusErrorLineState.scriptPhase !== scriptPhase) return;
32+
33+
const timer = setTimeout(() => {
34+
const editor = editorRef.current?.editor;
35+
if (!editor) return;
36+
37+
if (disposeRef.current) {
38+
disposeRef.current();
39+
disposeRef.current = null;
40+
}
41+
42+
disposeRef.current = focusErrorLine(editor, focusErrorLineState.line);
43+
dispatch(clearFocusErrorLine({ uid }));
44+
}, 0);
45+
46+
return () => clearTimeout(timer);
47+
}, [focusErrorLineState?.requestedAt, focusErrorLineState?.line, focusErrorLineState?.scriptPhase, isVisible, scriptPhase, uid]);
48+
49+
useEffect(() => {
50+
return () => {
51+
if (disposeRef.current) {
52+
disposeRef.current();
53+
disposeRef.current = null;
54+
}
55+
};
56+
}, []);
57+
};
58+
59+
export default useFocusErrorLine;

packages/bruno-app/src/providers/ReduxStore/slices/tabs.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,24 @@ export const tabsSlice = createSlice({
280280
tab.scriptPaneTab = action.payload.scriptPaneTab;
281281
}
282282
},
283+
setFocusErrorLine: (state, action) => {
284+
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
285+
286+
if (tab) {
287+
tab.focusErrorLine = {
288+
scriptPhase: action.payload.scriptPhase,
289+
line: action.payload.line,
290+
requestedAt: action.payload.requestedAt
291+
};
292+
}
293+
},
294+
clearFocusErrorLine: (state, action) => {
295+
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
296+
297+
if (tab) {
298+
tab.focusErrorLine = null;
299+
}
300+
},
283301
updateQueryBuilderOpen: (state, action) => {
284302
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
285303

@@ -519,6 +537,8 @@ export const {
519537
updateGqlDocsOpen,
520538
updateTableColumnWidths,
521539
updateScriptPaneTab,
540+
setFocusErrorLine,
541+
clearFocusErrorLine,
522542
closeTabs,
523543
closeAllCollectionTabs,
524544
makeTabPermanent,

0 commit comments

Comments
 (0)