Fix response viewer stream scrolling

This commit is contained in:
Gregory Schier
2025-10-24 14:39:25 -07:00
parent bbf85c953d
commit cf9882b5b9
5 changed files with 46 additions and 37 deletions

View File

@@ -1,3 +1,4 @@
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace'; import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey'; import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey';

View File

@@ -151,7 +151,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}, [allEnvironmentVariables, autocompleteVariables]); }, [allEnvironmentVariables, autocompleteVariables]);
// Track a local key for updates. If the default value is changed when the input is not in focus, // Track a local key for updates. If the default value is changed when the input is not in focus,
// regenerate this to force the field to update. // regenerate this to force the field to update.
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey(); const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey('initial');
const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`; const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`;
if (settings && wrapLines === undefined) { if (settings && wrapLines === undefined) {
@@ -352,17 +352,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[], [],
); );
// Force input to update when receiving change and not in focus
useLayoutEffect(() => {
const currDoc = cm.current?.view.state.doc.toString() || '';
const nextDoc = defaultValue || '';
const notFocused = !cm.current?.view.hasFocus;
const hasChanged = currDoc !== nextDoc;
if (notFocused && hasChanged) {
regenerateFocusedUpdateKey();
}
}, [defaultValue, regenerateFocusedUpdateKey]);
const [, { focusParamValue }] = useRequestEditor(); const [, { focusParamValue }] = useRequestEditor();
const onClickPathParameter = useCallback( const onClickPathParameter = useCallback(
async (name: string) => { async (name: string) => {
@@ -487,33 +476,24 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
// For read-only mode, update content when `defaultValue` changes // For read-only mode, update content when `defaultValue` changes
useEffect( useEffect(
function updateReadOnlyEditor() { function updateReadOnlyEditor() {
if (!readOnly || cm.current?.view == null || defaultValue == null) return; if (readOnly && cm.current?.view != null) {
updateContents(cm.current.view, defaultValue || '');
// Replace codemirror contents
const currentDoc = cm.current.view.state.doc.toString();
if (defaultValue.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
cm.current.view.dispatch({
changes: cm.current.view.state.changes({
from: currentDoc.length,
insert: defaultValue.slice(currentDoc.length),
}),
});
} else {
// If we're replacing everything, reset the entire content
cm.current.view.dispatch({
changes: cm.current.view.state.changes({
from: 0,
to: currentDoc.length,
insert: defaultValue,
}),
});
} }
}, },
[defaultValue, readOnly], [defaultValue, readOnly],
); );
// Force input to update when receiving change and not in focus
useLayoutEffect(
function updateNonFocusedEditor() {
const notFocused = !cm.current?.view.hasFocus;
if (notFocused && cm.current != null) {
updateContents(cm.current.view, defaultValue || '');
}
},
[defaultValue, readOnly, regenerateFocusedUpdateKey],
);
// Add bg classes to actions, so they appear over the text // Add bg classes to actions, so they appear over the text
const decoratedActions = useMemo(() => { const decoratedActions = useMemo(() => {
const results = []; const results = [];
@@ -720,3 +700,30 @@ function getCachedEditorState(doc: string, stateKey: string | null) {
function computeFullStateKey(stateKey: string): string { function computeFullStateKey(stateKey: string): string {
return `editor.${stateKey}`; return `editor.${stateKey}`;
} }
function updateContents(view: EditorView, text: string) {
// Replace codemirror contents
const currentDoc = view.state.doc.toString();
if (currentDoc === text) {
return;
} else if (text.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
view.dispatch({
changes: view.state.changes({
from: currentDoc.length,
insert: text.slice(currentDoc.length),
}),
});
} else {
// If we're replacing everything, reset the entire content
view.dispatch({
changes: view.state.changes({
from: 0,
to: currentDoc.length,
insert: text,
}),
});
}
}

View File

@@ -132,7 +132,7 @@ export function TextViewer({ language, text, response, requestId, pretty, classN
language={language} language={language}
actions={actions} actions={actions}
extraExtensions={extraExtensions} extraExtensions={extraExtensions}
stateKey={null} stateKey={'response.body.' + response.id}
/> />
); );
} }

View File

@@ -1,8 +1,8 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { generateId } from '../lib/generateId'; import { generateId } from '../lib/generateId';
export function useRandomKey() { export function useRandomKey(initialValue?: string) {
const [value, setValue] = useState<string>(generateId()); const [value, setValue] = useState<string>(initialValue ?? generateId());
const regenerate = useCallback(() => setValue(generateId()), []); const regenerate = useCallback(() => setValue(generateId()), []);
return [value, regenerate] as const; return [value, regenerate] as const;
} }

View File

@@ -10,6 +10,7 @@ export function useResponseBodyText({
filter: string | null; filter: string | null;
}) { }) {
return useQuery({ return useQuery({
placeholderData: (prev) => prev, // Keep previous data on refetch
queryKey: [ queryKey: [
'response_body_text', 'response_body_text',
response.id, response.id,