From a0b08614f0bfb87c3d7b029f5aa374ee8bb7538b Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 3 Jun 2024 13:49:51 -0700 Subject: [PATCH] Preserve JSON/XPath filter (Closes #22) --- src-web/components/core/Editor/Editor.tsx | 5 +- src-web/components/core/FormattedError.tsx | 2 +- .../components/responseViewers/TextViewer.tsx | 53 +++++++++++++------ src-web/hooks/useFilterResponse.ts | 2 +- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index bfb889f2..61110b7b 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -195,7 +195,7 @@ export const Editor = forwardRef(function E placeholderCompartment.current.of( placeholderExt(placeholderElFromText(placeholder ?? '')), ), - wrapLinesCompartment.current.of([]), + wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []), ...getExtensions({ container, readOnly, @@ -357,8 +357,7 @@ function getExtensions({ blur: () => { onBlur.current?.(); }, - keydown: (e, cm) => { - console.log('KEY DOWN', e, cm); + keydown: (e) => { onKeyDown.current?.(e); }, paste: (e) => { diff --git a/src-web/components/core/FormattedError.tsx b/src-web/components/core/FormattedError.tsx index 87a159db..8539b4a3 100644 --- a/src-web/components/core/FormattedError.tsx +++ b/src-web/components/core/FormattedError.tsx @@ -9,7 +9,7 @@ export function FormattedError({ children }: Props) { return (
diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx
index 241faa1d..a396c3e5 100644
--- a/src-web/components/responseViewers/TextViewer.tsx
+++ b/src-web/components/responseViewers/TextViewer.tsx
@@ -1,11 +1,11 @@
 import classNames from 'classnames';
 import type { ReactNode } from 'react';
 import { useCallback, useMemo } from 'react';
+import { createGlobalState } from 'react-use';
 import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders';
-import { useDebouncedState } from '../../hooks/useDebouncedState';
+import { useDebouncedValue } from '../../hooks/useDebouncedValue';
 import { useFilterResponse } from '../../hooks/useFilterResponse';
 import { useResponseBodyText } from '../../hooks/useResponseBodyText';
-import { useToggle } from '../../hooks/useToggle';
 import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
 import type { HttpResponse } from '../../lib/models';
 import { Editor } from '../core/Editor';
@@ -21,25 +21,42 @@ interface Props {
   className?: string;
 }
 
+const useFilterText = createGlobalState>({});
+
 export function TextViewer({ response, pretty, className }: Props) {
-  const [isSearching, toggleIsSearching] = useToggle();
-  const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState('', 400);
+  const [filterTextMap, setFilterTextMap] = useFilterText();
+  const filterText = filterTextMap[response.id] ?? null;
+  const debouncedFilterText = useDebouncedValue(filterText, 300);
+  const setFilterText = useCallback(
+    (v: string | null) => {
+      setFilterTextMap((m) => ({ ...m, [response.id]: v }));
+    },
+    [setFilterTextMap, response],
+  );
 
   const contentType = useContentTypeFromHeaders(response.headers);
   const rawBody = useResponseBodyText(response) ?? '';
+  const isSearching = filterText != null;
   const formattedBody =
     pretty && contentType?.includes('json')
       ? tryFormatJson(rawBody)
       : pretty && contentType?.includes('xml')
       ? tryFormatXml(rawBody)
       : rawBody;
-  const filteredResponse = useFilterResponse({ filter: filterText, responseId: response.id });
 
-  const body = filteredResponse ?? formattedBody;
-  const clearSearch = useCallback(() => {
-    toggleIsSearching();
-    setFilterText('');
-  }, [setFilterText, toggleIsSearching]);
+  const filteredResponse = useFilterResponse({
+    filter: debouncedFilterText ?? '',
+    responseId: response.id,
+  });
+
+  const body = isSearching && filterText?.length > 0 ? filteredResponse : formattedBody;
+  const toggleSearch = useCallback(() => {
+    if (isSearching) {
+      setFilterText(null);
+    } else {
+      setFilterText('');
+    }
+  }, [isSearching, setFilterText]);
 
   const isJson = contentType?.includes('json');
   const isXml = contentType?.includes('xml') || contentType?.includes('html');
@@ -54,16 +71,17 @@ export function TextViewer({ response, pretty, className }: Props) {
       result.push(
         
e.key === 'Escape' && clearSearch()} - onChange={setDebouncedFilterText} + onKeyDown={(e) => e.key === 'Escape' && toggleSearch()} + onChange={setFilterText} />
, ); @@ -75,13 +93,16 @@ export function TextViewer({ response, pretty, className }: Props) { size="sm" icon={isSearching ? 'x' : 'filter'} title={isSearching ? 'Close filter' : 'Filter response'} - onClick={clearSearch} - className={classNames(isSearching && '!opacity-100')} + onClick={toggleSearch} + className={classNames( + 'bg-background border !border-background-highlight', + isSearching && '!opacity-100', + )} />, ); return result; - }, [canFilter, clearSearch, filterText, isJson, isSearching, setDebouncedFilterText]); + }, [canFilter, filterText, isJson, isSearching, setFilterText, toggleSearch]); return (