mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-14 23:52:10 +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 type { CSSProperties } from "react";
|
||||
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { importCurl, looksLikeCurl } from "../commands/importCurl";
|
||||
import { allRequestUrlsAtom } from "../hooks/useAllRequests";
|
||||
import { useAuthTab } from "../hooks/useAuthTab";
|
||||
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
||||
import { useHeadersTab } from "../hooks/useHeadersTab";
|
||||
import { useImportCurl } from "../hooks/useImportCurl";
|
||||
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
||||
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
||||
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
|
||||
@@ -278,7 +278,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
const updateKey = useRequestUpdateKey(activeRequestId);
|
||||
const { mutate: importCurl } = useImportCurl();
|
||||
|
||||
const handleBodyChange = useCallback(
|
||||
(body: HttpRequest["body"]) => patchModelDebounced(activeRequest, { body }),
|
||||
@@ -299,8 +298,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
|
||||
const handlePaste = useCallback(
|
||||
async (e: ClipboardEvent, text: string) => {
|
||||
if (text.startsWith("curl ")) {
|
||||
importCurl({ overwriteRequestId: activeRequestId, command: text });
|
||||
if (looksLikeCurl(text)) {
|
||||
importCurl.mutate({ overwriteRequestId: activeRequestId, command: text });
|
||||
} else {
|
||||
const patch = prepareImportQuerystring(text);
|
||||
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(
|
||||
() => 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 { PillButton } from "./core/PillButton";
|
||||
import { EnvironmentActionsDropdown } from "./EnvironmentActionsDropdown";
|
||||
import { ImportCurlButton } from "./ImportCurlButton";
|
||||
import { ImportCurl } from "./ImportCurl";
|
||||
import { LicenseBadge } from "./LicenseBadge";
|
||||
import { RecentRequestsDropdown } from "./RecentRequestsDropdown";
|
||||
import { SettingsDropdown } from "./SettingsDropdown";
|
||||
@@ -56,7 +56,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
||||
<ImportCurlButton />
|
||||
<ImportCurl />
|
||||
{showEncryptionSetup ? (
|
||||
<PillButton color="danger" onClick={setupOrConfigureEncryption}>
|
||||
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