diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index cd6253ab..f98d2c7f 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -1,3 +1,4 @@ +import { activeRequestAtom } from '../hooks/useActiveRequest'; import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey'; diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index d56b2624..68a258ab 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -151,7 +151,7 @@ export const Editor = forwardRef(function E }, [allEnvironmentVariables, autocompleteVariables]); // 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. - const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey(); + const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey('initial'); const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`; if (settings && wrapLines === undefined) { @@ -352,17 +352,6 @@ export const Editor = forwardRef(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 onClickPathParameter = useCallback( async (name: string) => { @@ -487,33 +476,24 @@ export const Editor = forwardRef(function E // For read-only mode, update content when `defaultValue` changes useEffect( function updateReadOnlyEditor() { - if (!readOnly || cm.current?.view == null || defaultValue == null) return; - - // 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, - }), - }); + if (readOnly && cm.current?.view != null) { + updateContents(cm.current.view, defaultValue || ''); } }, [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 const decoratedActions = useMemo(() => { const results = []; @@ -720,3 +700,30 @@ function getCachedEditorState(doc: string, stateKey: string | null) { function computeFullStateKey(stateKey: string): string { 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, + }), + }); + } +} diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx index ea918b23..58608ebc 100644 --- a/src-web/components/responseViewers/TextViewer.tsx +++ b/src-web/components/responseViewers/TextViewer.tsx @@ -132,7 +132,7 @@ export function TextViewer({ language, text, response, requestId, pretty, classN language={language} actions={actions} extraExtensions={extraExtensions} - stateKey={null} + stateKey={'response.body.' + response.id} /> ); } diff --git a/src-web/hooks/useRandomKey.ts b/src-web/hooks/useRandomKey.ts index a1495827..12d8a2b5 100644 --- a/src-web/hooks/useRandomKey.ts +++ b/src-web/hooks/useRandomKey.ts @@ -1,8 +1,8 @@ import { useCallback, useState } from 'react'; import { generateId } from '../lib/generateId'; -export function useRandomKey() { - const [value, setValue] = useState(generateId()); +export function useRandomKey(initialValue?: string) { + const [value, setValue] = useState(initialValue ?? generateId()); const regenerate = useCallback(() => setValue(generateId()), []); return [value, regenerate] as const; } diff --git a/src-web/hooks/useResponseBodyText.ts b/src-web/hooks/useResponseBodyText.ts index 613e0caf..795ab6e5 100644 --- a/src-web/hooks/useResponseBodyText.ts +++ b/src-web/hooks/useResponseBodyText.ts @@ -10,6 +10,7 @@ export function useResponseBodyText({ filter: string | null; }) { return useQuery({ + placeholderData: (prev) => prev, // Keep previous data on refetch queryKey: [ 'response_body_text', response.id,