mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 12:54:09 +02:00
Import Curl from a paste in the browser build (#538)
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||||
|
import { patchModelById } from "@yaakapp-internal/models";
|
||||||
|
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
|
import { createFastMutation } from "../hooks/useFastMutation";
|
||||||
|
import { wasUpdatedExternally } from "../hooks/useRequestUpdateKey";
|
||||||
|
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
||||||
|
import { jotaiStore } from "../lib/jotai";
|
||||||
|
import { invokeCmd } from "../lib/tauri";
|
||||||
|
import { showToast } from "../lib/toast";
|
||||||
|
|
||||||
|
export function looksLikeCurl(text: string) {
|
||||||
|
return text.trim().startsWith("curl ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importCurl = createFastMutation<
|
||||||
|
void,
|
||||||
|
string,
|
||||||
|
{ overwriteRequestId?: string; command: string }
|
||||||
|
>({
|
||||||
|
mutationKey: ["import_curl"],
|
||||||
|
mutationFn: async ({ overwriteRequestId, command }) => {
|
||||||
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
|
const importedRequest: HttpRequest = await invokeCmd("cmd_curl_to_request", {
|
||||||
|
command,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
let verb: string;
|
||||||
|
if (overwriteRequestId == null) {
|
||||||
|
verb = "Created";
|
||||||
|
await createRequestAndNavigate(importedRequest);
|
||||||
|
} else {
|
||||||
|
verb = "Updated";
|
||||||
|
await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
|
||||||
|
...importedRequest,
|
||||||
|
id: r.id,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
workspaceId: r.workspaceId,
|
||||||
|
folderId: r.folderId,
|
||||||
|
name: r.name,
|
||||||
|
sortPriority: r.sortPriority,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
color: "success",
|
||||||
|
message: `${verb} request from Curl`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -5,11 +5,11 @@ import classNames from "classnames";
|
|||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
|
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { importCurl, looksLikeCurl } from "../commands/importCurl";
|
||||||
import { allRequestUrlsAtom } from "../hooks/useAllRequests";
|
import { allRequestUrlsAtom } from "../hooks/useAllRequests";
|
||||||
import { useAuthTab } from "../hooks/useAuthTab";
|
import { useAuthTab } from "../hooks/useAuthTab";
|
||||||
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
||||||
import { useHeadersTab } from "../hooks/useHeadersTab";
|
import { useHeadersTab } from "../hooks/useHeadersTab";
|
||||||
import { useImportCurl } from "../hooks/useImportCurl";
|
|
||||||
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
||||||
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
||||||
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
|
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
|
||||||
@@ -278,7 +278,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||||
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 handleBodyChange = useCallback(
|
const handleBodyChange = useCallback(
|
||||||
(body: HttpRequest["body"]) => patchModelDebounced(activeRequest, { body }),
|
(body: HttpRequest["body"]) => patchModelDebounced(activeRequest, { body }),
|
||||||
@@ -299,8 +298,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
|
|
||||||
const handlePaste = useCallback(
|
const handlePaste = useCallback(
|
||||||
async (e: ClipboardEvent, text: string) => {
|
async (e: ClipboardEvent, text: string) => {
|
||||||
if (text.startsWith("curl ")) {
|
if (looksLikeCurl(text)) {
|
||||||
importCurl({ overwriteRequestId: activeRequestId, command: text });
|
importCurl.mutate({ overwriteRequestId: activeRequestId, command: text });
|
||||||
} else {
|
} else {
|
||||||
const patch = prepareImportQuerystring(text);
|
const patch = prepareImportQuerystring(text);
|
||||||
if (patch != null) {
|
if (patch != null) {
|
||||||
@@ -322,7 +321,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh, importCurl],
|
[activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh],
|
||||||
);
|
);
|
||||||
const handleSend = useCallback(
|
const handleSend = useCallback(
|
||||||
() => sendRequest(activeRequest.id ?? null),
|
() => sendRequest(activeRequest.id ?? null),
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
|
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
|
import * as m from "motion/react-m";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { importCurl, looksLikeCurl } from "../commands/importCurl";
|
||||||
|
import { useWindowFocus } from "../hooks/useWindowFocus";
|
||||||
|
import { showToast } from "../lib/toast";
|
||||||
|
import { Button } from "./core/Button";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offers to create a request from a Curl command on the clipboard. The desktop app can
|
||||||
|
* read the clipboard whenever the window is focused, but a browser can't without
|
||||||
|
* prompting for permission, so it waits for the user to paste instead.
|
||||||
|
*/
|
||||||
|
export function ImportCurl() {
|
||||||
|
return isTauri() ? <ImportCurlButton /> : <ImportCurlOnPaste />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportCurlButton() {
|
||||||
|
const focused = useWindowFocus();
|
||||||
|
const [clipboardText, setClipboardText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// oxlint-disable-next-line react-hooks/exhaustive-deps -- none
|
||||||
|
useEffect(() => {
|
||||||
|
void readText().then(setClipboardText);
|
||||||
|
}, [focused]);
|
||||||
|
|
||||||
|
if (!looksLikeCurl(clipboardText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<m.div
|
||||||
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="2xs"
|
||||||
|
variant="border"
|
||||||
|
color="success"
|
||||||
|
className="rounded-full"
|
||||||
|
rightSlot={<Icon icon="import" size="sm" />}
|
||||||
|
isLoading={isLoading}
|
||||||
|
title="Import Curl command from clipboard"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await importCurl.mutateAsync({ command: clipboardText });
|
||||||
|
setClipboardText(""); // Hide the button until the clipboard changes
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to import curl", e);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import Curl
|
||||||
|
</Button>
|
||||||
|
</m.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportCurlOnPaste() {
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
|
const command = e.clipboardData?.getData("text/plain") ?? "";
|
||||||
|
if (!looksLikeCurl(command)) return;
|
||||||
|
|
||||||
|
// Editable targets keep the text as text. The URL editor imports it itself.
|
||||||
|
if (isEditable(e.target)) return;
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
id: "curl-paste",
|
||||||
|
color: "success",
|
||||||
|
message: (
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Curl command detected</h2>
|
||||||
|
<p className="text-text-subtle text-sm">Create a request from the pasted command?</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
action: ({ hide }) => (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="success"
|
||||||
|
className="mr-auto min-w-20"
|
||||||
|
rightSlot={<Icon icon="import" size="sm" />}
|
||||||
|
onClick={() => {
|
||||||
|
hide();
|
||||||
|
importCurl.mutate({ command });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Request
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("paste", handlePaste);
|
||||||
|
return () => document.removeEventListener("paste", handlePaste);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditable(target: EventTarget | null) {
|
||||||
|
if (!(target instanceof HTMLElement)) return false;
|
||||||
|
if (target.isContentEditable) return true;
|
||||||
|
if (target.closest(".cm-editor") != null) return true;
|
||||||
|
return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { clear, readText } from "@tauri-apps/plugin-clipboard-manager";
|
|
||||||
import * as m from "motion/react-m";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useImportCurl } from "../hooks/useImportCurl";
|
|
||||||
import { useWindowFocus } from "../hooks/useWindowFocus";
|
|
||||||
import { Button } from "./core/Button";
|
|
||||||
import { Icon } from "@yaakapp-internal/ui";
|
|
||||||
|
|
||||||
export function ImportCurlButton() {
|
|
||||||
const focused = useWindowFocus();
|
|
||||||
const [clipboardText, setClipboardText] = useState("");
|
|
||||||
|
|
||||||
const importCurl = useImportCurl();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- none
|
|
||||||
useEffect(() => {
|
|
||||||
void readText().then(setClipboardText);
|
|
||||||
}, [focused]);
|
|
||||||
|
|
||||||
if (!clipboardText?.trim().startsWith("curl ")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<m.div
|
|
||||||
initial={{ opacity: 0, scale: 0 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ delay: 0.5 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="2xs"
|
|
||||||
variant="border"
|
|
||||||
color="success"
|
|
||||||
className="rounded-full"
|
|
||||||
rightSlot={<Icon icon="import" size="sm" />}
|
|
||||||
isLoading={isLoading}
|
|
||||||
title="Import Curl command from clipboard"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await importCurl.mutateAsync({ command: clipboardText });
|
|
||||||
await clear(); // Clear the clipboard so the button goes away
|
|
||||||
setClipboardText("");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Failed to import curl", e);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Import Curl
|
|
||||||
</Button>
|
|
||||||
</m.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import { CookieDropdown } from "./CookieDropdown";
|
|||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { PillButton } from "./core/PillButton";
|
import { PillButton } from "./core/PillButton";
|
||||||
import { EnvironmentActionsDropdown } from "./EnvironmentActionsDropdown";
|
import { EnvironmentActionsDropdown } from "./EnvironmentActionsDropdown";
|
||||||
import { ImportCurlButton } from "./ImportCurlButton";
|
import { ImportCurl } from "./ImportCurl";
|
||||||
import { LicenseBadge } from "./LicenseBadge";
|
import { LicenseBadge } from "./LicenseBadge";
|
||||||
import { RecentRequestsDropdown } from "./RecentRequestsDropdown";
|
import { RecentRequestsDropdown } from "./RecentRequestsDropdown";
|
||||||
import { SettingsDropdown } from "./SettingsDropdown";
|
import { SettingsDropdown } from "./SettingsDropdown";
|
||||||
@@ -56,7 +56,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({
|
|||||||
<RecentRequestsDropdown />
|
<RecentRequestsDropdown />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
||||||
<ImportCurlButton />
|
<ImportCurl />
|
||||||
{showEncryptionSetup ? (
|
{showEncryptionSetup ? (
|
||||||
<PillButton color="danger" onClick={setupOrConfigureEncryption}>
|
<PillButton color="danger" onClick={setupOrConfigureEncryption}>
|
||||||
Enter Encryption Key
|
Enter Encryption Key
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
|
||||||
import { patchModelById } from "@yaakapp-internal/models";
|
|
||||||
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
|
||||||
import { jotaiStore } from "../lib/jotai";
|
|
||||||
import { invokeCmd } from "../lib/tauri";
|
|
||||||
import { showToast } from "../lib/toast";
|
|
||||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
|
||||||
import { useFastMutation } from "./useFastMutation";
|
|
||||||
import { wasUpdatedExternally } from "./useRequestUpdateKey";
|
|
||||||
|
|
||||||
export function useImportCurl() {
|
|
||||||
return useFastMutation({
|
|
||||||
mutationKey: ["import_curl"],
|
|
||||||
mutationFn: async ({
|
|
||||||
overwriteRequestId,
|
|
||||||
command,
|
|
||||||
}: {
|
|
||||||
overwriteRequestId?: string;
|
|
||||||
command: string;
|
|
||||||
}) => {
|
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
|
||||||
const importedRequest: HttpRequest = await invokeCmd("cmd_curl_to_request", {
|
|
||||||
command,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
let verb: string;
|
|
||||||
if (overwriteRequestId == null) {
|
|
||||||
verb = "Created";
|
|
||||||
await createRequestAndNavigate(importedRequest);
|
|
||||||
} else {
|
|
||||||
verb = "Updated";
|
|
||||||
await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
|
|
||||||
...importedRequest,
|
|
||||||
id: r.id,
|
|
||||||
createdAt: r.createdAt,
|
|
||||||
workspaceId: r.workspaceId,
|
|
||||||
folderId: r.folderId,
|
|
||||||
name: r.name,
|
|
||||||
sortPriority: r.sortPriority,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast({
|
|
||||||
color: "success",
|
|
||||||
message: `${verb} request from Curl`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user