mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-21 16:31:18 +02:00
Better querystring import (https://feedback.yaak.app/p/url-pasted-params-parsed-incorrectly)
This commit is contained in:
@@ -12,7 +12,6 @@ import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
|||||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||||
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||||
import { useImportCurl } from '../hooks/useImportCurl';
|
import { useImportCurl } from '../hooks/useImportCurl';
|
||||||
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
@@ -20,7 +19,6 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
|||||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
import { deepEqualAtom } from '../lib/atoms';
|
import { deepEqualAtom } from '../lib/atoms';
|
||||||
import { languageFromContentType } from '../lib/contentType';
|
import { languageFromContentType } from '../lib/contentType';
|
||||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
|
||||||
import { generateId } from '../lib/generateId';
|
import { generateId } from '../lib/generateId';
|
||||||
import {
|
import {
|
||||||
BODY_TYPE_BINARY,
|
BODY_TYPE_BINARY,
|
||||||
@@ -32,6 +30,8 @@ import {
|
|||||||
BODY_TYPE_OTHER,
|
BODY_TYPE_OTHER,
|
||||||
BODY_TYPE_XML,
|
BODY_TYPE_XML,
|
||||||
} from '../lib/model_util';
|
} from '../lib/model_util';
|
||||||
|
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
|
||||||
|
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||||
import { showToast } from '../lib/toast';
|
import { showToast } from '../lib/toast';
|
||||||
import { BinaryFileEditor } from './BinaryFileEditor';
|
import { BinaryFileEditor } from './BinaryFileEditor';
|
||||||
import { CountBadge } from './core/CountBadge';
|
import { CountBadge } from './core/CountBadge';
|
||||||
@@ -83,7 +83,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
||||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
const [{ urlKey }] = useRequestEditor();
|
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||||
const contentType = useContentTypeFromHeaders(activeRequest.headers);
|
const contentType = useContentTypeFromHeaders(activeRequest.headers);
|
||||||
const authentication = useHttpAuthenticationSummaries();
|
const authentication = useHttpAuthenticationSummaries();
|
||||||
|
|
||||||
@@ -273,7 +273,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
||||||
const { mutate: importCurl } = useImportCurl();
|
const { mutate: importCurl } = useImportCurl();
|
||||||
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
|
|
||||||
|
|
||||||
const handleBodyChange = useCallback(
|
const handleBodyChange = useCallback(
|
||||||
(body: HttpRequest['body']) => updateRequest({ id: activeRequestId, update: { body } }),
|
(body: HttpRequest['body']) => updateRequest({ id: activeRequestId, update: { body } }),
|
||||||
@@ -314,17 +313,35 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handlePaste = useCallback(
|
const handlePaste = useCallback(
|
||||||
(text: string) => {
|
(e: ClipboardEvent, text: string) => {
|
||||||
if (text.startsWith('curl ')) {
|
if (text.startsWith('curl ')) {
|
||||||
importCurl({ overwriteRequestId: activeRequestId, command: text });
|
importCurl({ overwriteRequestId: activeRequestId, command: text });
|
||||||
} else {
|
} else {
|
||||||
// Only import query if pasted text contains entire querystring
|
const data = prepareImportQuerystring(text);
|
||||||
importQuerystring(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(
|
const handleSend = useCallback(
|
||||||
() => sendRequest(activeRequest.id ?? null),
|
() => sendRequest(activeRequest.id ?? null),
|
||||||
[activeRequest.id, sendRequest],
|
[activeRequest.id, sendRequest],
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Props = Pick<HttpRequest, 'url'> & {
|
|||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
onUrlChange: (url: string) => void;
|
onUrlChange: (url: string) => void;
|
||||||
onPaste?: (v: string) => void;
|
onPaste?: (v: string) => void;
|
||||||
onPasteOverwrite?: (v: string) => void;
|
onPasteOverwrite?: InputProps['onPasteOverwrite'];
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
submitIcon?: IconProps['icon'] | null;
|
submitIcon?: IconProps['icon'] | null;
|
||||||
onMethodChange?: (method: string) => void;
|
onMethodChange?: (method: string) => void;
|
||||||
|
|||||||
@@ -12,17 +12,18 @@ import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
|
|||||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||||
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||||
import { requestsAtom } from '../hooks/useRequests';
|
import { requestsAtom } from '../hooks/useRequests';
|
||||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
import { useLatestWebsocketConnection } from '../hooks/useWebsocketConnections';
|
import { useLatestWebsocketConnection } from '../hooks/useWebsocketConnections';
|
||||||
import { trackEvent } from '../lib/analytics';
|
import { trackEvent } from '../lib/analytics';
|
||||||
import { deepEqualAtom } from '../lib/atoms';
|
import { deepEqualAtom } from '../lib/atoms';
|
||||||
import { languageFromContentType } from '../lib/contentType';
|
import { languageFromContentType } from '../lib/contentType';
|
||||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
|
||||||
import { generateId } from '../lib/generateId';
|
import { generateId } from '../lib/generateId';
|
||||||
|
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
|
||||||
|
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||||
import { CountBadge } from './core/CountBadge';
|
import { CountBadge } from './core/CountBadge';
|
||||||
import { Editor } from './core/Editor/Editor';
|
import { Editor } from './core/Editor/Editor';
|
||||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||||
@@ -66,7 +67,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
const activeRequestId = activeRequest.id;
|
const activeRequestId = activeRequest.id;
|
||||||
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
||||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
const [{ urlKey }] = useRequestEditor();
|
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||||
const authentication = useHttpAuthenticationSummaries();
|
const authentication = useHttpAuthenticationSummaries();
|
||||||
|
|
||||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||||
@@ -151,8 +152,8 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
|
|
||||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
|
const { mutate: updateRequest } = useUpdateAnyHttpRequest();
|
||||||
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
||||||
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
|
|
||||||
const connection = useLatestWebsocketConnection(activeRequestId);
|
const connection = useLatestWebsocketConnection(activeRequestId);
|
||||||
|
|
||||||
const activeTab = activeTabs?.[activeRequestId];
|
const activeTab = activeTabs?.[activeRequestId];
|
||||||
@@ -212,6 +213,26 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
[activeRequest],
|
[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 messageLanguage = languageFromContentType(null, activeRequest.message);
|
||||||
|
|
||||||
const isLoading = connection !== null && connection.state !== 'closed';
|
const isLoading = connection !== null && connection.state !== 'closed';
|
||||||
@@ -242,7 +263,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
placeholder="wss://example.com"
|
placeholder="wss://example.com"
|
||||||
onPasteOverwrite={importQuerystring}
|
onPasteOverwrite={handlePaste}
|
||||||
autocomplete={autocomplete}
|
autocomplete={autocomplete}
|
||||||
onSend={isLoading ? handleSend : handleConnect}
|
onSend={isLoading ? handleSend : handleConnect}
|
||||||
onCancel={cancelResponse}
|
onCancel={cancelResponse}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export interface EditorProps {
|
|||||||
useTemplating?: boolean;
|
useTemplating?: boolean;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onPaste?: (value: string) => void;
|
onPaste?: (value: string) => void;
|
||||||
onPasteOverwrite?: (value: string) => void;
|
onPasteOverwrite?: (e: ClipboardEvent, value: string) => void;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
onKeyDown?: (e: KeyboardEvent) => void;
|
onKeyDown?: (e: KeyboardEvent) => void;
|
||||||
@@ -173,7 +173,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
}, [onPaste]);
|
}, [onPaste]);
|
||||||
|
|
||||||
// Use ref so we can update the handler without re-initializing the editor
|
// Use ref so we can update the handler without re-initializing the editor
|
||||||
const handlePasteOverwrite = useRef<EditorProps['onPasteOverwrite']>(onPaste);
|
const handlePasteOverwrite = useRef<EditorProps['onPasteOverwrite']>(onPasteOverwrite);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handlePasteOverwrite.current = onPasteOverwrite;
|
handlePasteOverwrite.current = onPasteOverwrite;
|
||||||
}, [onPasteOverwrite]);
|
}, [onPasteOverwrite]);
|
||||||
@@ -606,7 +606,7 @@ function getExtensions({
|
|||||||
const textData = e.clipboardData?.getData('text/plain') ?? '';
|
const textData = e.clipboardData?.getData('text/plain') ?? '';
|
||||||
onPaste.current?.(textData);
|
onPaste.current?.(textData);
|
||||||
if (v.state.selection.main.from === 0 && v.state.selection.main.to === v.state.doc.length) {
|
if (v.state.selection.main.from === 0 && v.state.selection.main.to === v.state.doc.length) {
|
||||||
onPasteOverwrite.current?.(textData);
|
onPasteOverwrite.current?.(e, textData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export type InputProps = Pick<
|
|||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
onPaste?: (value: string) => void;
|
onPaste?: (value: string) => void;
|
||||||
onPasteOverwrite?: (value: string) => void;
|
onPasteOverwrite?: EditorProps['onPasteOverwrite'];
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
leftSlot?: ReactNode;
|
leftSlot?: ReactNode;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
25
src-web/lib/prepareImportQuerystring.ts
Normal file
25
src-web/lib/prepareImportQuerystring.ts
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user