From 84ecbe0cd6f53f4d942e439151b3e6d2075b885e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 21 Feb 2025 13:16:09 -0800 Subject: [PATCH] Better querystring import (https://feedback.yaak.app/p/url-pasted-params-parsed-incorrectly) --- src-web/components/HttpRequestPane.tsx | 35 ++++++++---- src-web/components/UrlBar.tsx | 2 +- src-web/components/WebsocketRequestPane.tsx | 31 +++++++++-- src-web/components/core/Editor/Editor.tsx | 6 +-- src-web/components/core/Input.tsx | 2 +- src-web/hooks/useImportQuerystring.ts | 59 --------------------- src-web/lib/prepareImportQuerystring.ts | 25 +++++++++ 7 files changed, 82 insertions(+), 78 deletions(-) delete mode 100644 src-web/hooks/useImportQuerystring.ts create mode 100644 src-web/lib/prepareImportQuerystring.ts diff --git a/src-web/components/HttpRequestPane.tsx b/src-web/components/HttpRequestPane.tsx index 9739c5c9..f8a201dc 100644 --- a/src-web/components/HttpRequestPane.tsx +++ b/src-web/components/HttpRequestPane.tsx @@ -12,7 +12,6 @@ import { grpcRequestsAtom } from '../hooks/useGrpcRequests'; import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication'; import { httpRequestsAtom } from '../hooks/useHttpRequests'; import { useImportCurl } from '../hooks/useImportCurl'; -import { useImportQuerystring } from '../hooks/useImportQuerystring'; import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; @@ -20,7 +19,6 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { deepEqualAtom } from '../lib/atoms'; import { languageFromContentType } from '../lib/contentType'; -import { resolvedModelName } from '../lib/resolvedModelName'; import { generateId } from '../lib/generateId'; import { BODY_TYPE_BINARY, @@ -32,6 +30,8 @@ import { BODY_TYPE_OTHER, BODY_TYPE_XML, } from '../lib/model_util'; +import { prepareImportQuerystring } from '../lib/prepareImportQuerystring'; +import { resolvedModelName } from '../lib/resolvedModelName'; import { showToast } from '../lib/toast'; import { BinaryFileEditor } from './BinaryFileEditor'; import { CountBadge } from './core/CountBadge'; @@ -83,7 +83,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: const [activeTabs, setActiveTabs] = useAtom(tabsAtom); const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState(0); const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); - const [{ urlKey }] = useRequestEditor(); + const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor(); const contentType = useContentTypeFromHeaders(activeRequest.headers); const authentication = useHttpAuthenticationSummaries(); @@ -273,7 +273,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null); const { updateKey } = useRequestUpdateKey(activeRequestId); const { mutate: importCurl } = useImportCurl(); - const { mutate: importQuerystring } = useImportQuerystring(activeRequestId); const handleBodyChange = useCallback( (body: HttpRequest['body']) => updateRequest({ id: activeRequestId, update: { body } }), @@ -314,17 +313,35 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: ); const handlePaste = useCallback( - (text: string) => { + (e: ClipboardEvent, text: string) => { if (text.startsWith('curl ')) { importCurl({ overwriteRequestId: activeRequestId, command: text }); } else { - // Only import query if pasted text contains entire querystring - importQuerystring(text); + const data = prepareImportQuerystring(text); + if (data != null) { + e.preventDefault(); // Prevent input onChange + + updateRequest({ id: activeRequestId, update: data }); + focusParamsTab(); + + // Wait for request to update, then refresh the UI + // TODO: Somehow make this deterministic + setTimeout(() => { + forceUrlRefresh(); + forceParamsRefresh(); + }, 100); + } } }, - [activeRequestId, importCurl, importQuerystring], + [ + activeRequestId, + focusParamsTab, + forceParamsRefresh, + forceUrlRefresh, + importCurl, + updateRequest, + ], ); - const handleSend = useCallback( () => sendRequest(activeRequest.id ?? null), [activeRequest.id, sendRequest], diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index 50203aa0..3f96d5da 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -18,7 +18,7 @@ type Props = Pick & { onSend: () => void; onUrlChange: (url: string) => void; onPaste?: (v: string) => void; - onPasteOverwrite?: (v: string) => void; + onPasteOverwrite?: InputProps['onPasteOverwrite']; onCancel: () => void; submitIcon?: IconProps['icon'] | null; onMethodChange?: (method: string) => void; diff --git a/src-web/components/WebsocketRequestPane.tsx b/src-web/components/WebsocketRequestPane.tsx index 36ec4168..7b9c5e86 100644 --- a/src-web/components/WebsocketRequestPane.tsx +++ b/src-web/components/WebsocketRequestPane.tsx @@ -12,17 +12,18 @@ import { getActiveEnvironment } from '../hooks/useActiveEnvironment'; import { activeRequestIdAtom } from '../hooks/useActiveRequestId'; import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication'; -import { useImportQuerystring } from '../hooks/useImportQuerystring'; import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor'; import { requestsAtom } from '../hooks/useRequests'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; +import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useLatestWebsocketConnection } from '../hooks/useWebsocketConnections'; import { trackEvent } from '../lib/analytics'; import { deepEqualAtom } from '../lib/atoms'; import { languageFromContentType } from '../lib/contentType'; -import { resolvedModelName } from '../lib/resolvedModelName'; import { generateId } from '../lib/generateId'; +import { prepareImportQuerystring } from '../lib/prepareImportQuerystring'; +import { resolvedModelName } from '../lib/resolvedModelName'; import { CountBadge } from './core/CountBadge'; import { Editor } from './core/Editor/Editor'; import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; @@ -66,7 +67,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque const activeRequestId = activeRequest.id; const [activeTabs, setActiveTabs] = useAtom(tabsAtom); const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); - const [{ urlKey }] = useRequestEditor(); + const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor(); const authentication = useHttpAuthenticationSummaries(); const { urlParameterPairs, urlParametersKey } = useMemo(() => { @@ -151,8 +152,8 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque const { activeResponse } = usePinnedHttpResponse(activeRequestId); const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null); + const { mutate: updateRequest } = useUpdateAnyHttpRequest(); const { updateKey } = useRequestUpdateKey(activeRequestId); - const { mutate: importQuerystring } = useImportQuerystring(activeRequestId); const connection = useLatestWebsocketConnection(activeRequestId); const activeTab = activeTabs?.[activeRequestId]; @@ -212,6 +213,26 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque [activeRequest], ); + const handlePaste = useCallback( + (e: ClipboardEvent, text: string) => { + const data = prepareImportQuerystring(text); + if (data != null) { + e.preventDefault(); // Prevent input onChange + + updateRequest({ id: activeRequestId, update: data }); + focusParamsTab(); + + // Wait for request to update, then refresh the UI + // TODO: Somehow make this deterministic + setTimeout(() => { + forceUrlRefresh(); + forceParamsRefresh(); + }, 100); + } + }, + [activeRequestId, focusParamsTab, forceParamsRefresh, forceUrlRefresh, updateRequest], + ); + const messageLanguage = languageFromContentType(null, activeRequest.message); const isLoading = connection !== null && connection.state !== 'closed'; @@ -242,7 +263,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque ) } placeholder="wss://example.com" - onPasteOverwrite={importQuerystring} + onPasteOverwrite={handlePaste} autocomplete={autocomplete} onSend={isLoading ? handleSend : handleConnect} onCancel={cancelResponse} diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 3d9e6994..55f809f7 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -71,7 +71,7 @@ export interface EditorProps { useTemplating?: boolean; onChange?: (value: string) => void; onPaste?: (value: string) => void; - onPasteOverwrite?: (value: string) => void; + onPasteOverwrite?: (e: ClipboardEvent, value: string) => void; onFocus?: () => void; onBlur?: () => void; onKeyDown?: (e: KeyboardEvent) => void; @@ -173,7 +173,7 @@ export const Editor = forwardRef(function E }, [onPaste]); // Use ref so we can update the handler without re-initializing the editor - const handlePasteOverwrite = useRef(onPaste); + const handlePasteOverwrite = useRef(onPasteOverwrite); useEffect(() => { handlePasteOverwrite.current = onPasteOverwrite; }, [onPasteOverwrite]); @@ -606,7 +606,7 @@ function getExtensions({ const textData = e.clipboardData?.getData('text/plain') ?? ''; onPaste.current?.(textData); if (v.state.selection.main.from === 0 && v.state.selection.main.to === v.state.doc.length) { - onPasteOverwrite.current?.(textData); + onPasteOverwrite.current?.(e, textData); } }, }), diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index ddc01153..fc92560d 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -35,7 +35,7 @@ export type InputProps = Pick< onFocus?: () => void; onBlur?: () => void; onPaste?: (value: string) => void; - onPasteOverwrite?: (value: string) => void; + onPasteOverwrite?: EditorProps['onPasteOverwrite']; defaultValue?: string; leftSlot?: ReactNode; rightSlot?: ReactNode; diff --git a/src-web/hooks/useImportQuerystring.ts b/src-web/hooks/useImportQuerystring.ts deleted file mode 100644 index 4bac4802..00000000 --- a/src-web/hooks/useImportQuerystring.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { HttpUrlParameter } from '@yaakapp-internal/models'; -import { generateId } from '../lib/generateId'; -import { pluralize } from '../lib/pluralize'; -import { showToast } from '../lib/toast'; -import { useFastMutation } from './useFastMutation'; -import { getHttpRequest } from './useHttpRequests'; -import { useRequestEditor } from './useRequestEditor'; -import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest'; - -export function useImportQuerystring(requestId: string) { - const updateRequest = useUpdateAnyHttpRequest(); - const [, { focusParamsTab, forceParamsRefresh, forceUrlRefresh }] = useRequestEditor(); - - return useFastMutation({ - mutationKey: ['import_querystring'], - mutationFn: async (url: string) => { - const split = url.split(/\?(.*)/s); - const baseUrl = split[0] ?? ''; - const querystring = split[1] ?? ''; - if (!querystring) return; - - const request = getHttpRequest(requestId); - if (request == null) return; - - const parsedParams = Array.from(new URLSearchParams(querystring).entries()); - const urlParameters: HttpUrlParameter[] = parsedParams.map(([name, value]) => ({ - name, - value, - enabled: true, - id: generateId(), - })); - - await updateRequest.mutateAsync({ - id: requestId, - update: { - url: baseUrl ?? '', - urlParameters, - }, - }); - - if (urlParameters.length > 0) { - showToast({ - id: 'querystring-imported', - color: 'info', - message: `Extracted ${urlParameters.length} ${pluralize('parameter', urlParameters.length)} from URL`, - }); - } - - focusParamsTab(); - - // Wait for request to update, then refresh the UI - // TODO: Somehow make this deterministic - setTimeout(() => { - forceUrlRefresh(); - forceParamsRefresh(); - }, 100); - }, - }); -} diff --git a/src-web/lib/prepareImportQuerystring.ts b/src-web/lib/prepareImportQuerystring.ts new file mode 100644 index 00000000..06aa70f3 --- /dev/null +++ b/src-web/lib/prepareImportQuerystring.ts @@ -0,0 +1,25 @@ +import type { HttpUrlParameter } from '@yaakapp-internal/models'; +import { generateId } from './generateId'; + +export function prepareImportQuerystring( + url: string, +): { url: string; urlParameters: HttpUrlParameter[] } | null { + const split = url.split(/\?(.*)/s); + const baseUrl = split[0] ?? ''; + const querystring = split[1] ?? ''; + + // No querystring in url + if (!querystring) { + return null; + } + + const parsedParams = Array.from(new URLSearchParams(querystring).entries()); + const urlParameters: HttpUrlParameter[] = parsedParams.map(([name, value]) => ({ + name, + value, + enabled: true, + id: generateId(), + })); + + return { url: baseUrl, urlParameters }; +}