This commit is contained in:
Gregory Schier
2025-02-21 13:16:09 -08:00
parent 6a63cc26b9
commit 84ecbe0cd6
7 changed files with 82 additions and 78 deletions

View File

@@ -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<number>(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],

View File

@@ -18,7 +18,7 @@ type Props = Pick<HttpRequest, 'url'> & {
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;

View File

@@ -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}

View File

@@ -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<EditorView | undefined, EditorProps>(function E
}, [onPaste]);
// 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(() => {
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);
}
},
}),

View File

@@ -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;

View File

@@ -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);
},
});
}

View 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 };
}