diff --git a/skyvern-frontend/src/routes/workflows/WorkflowActions.tsx b/skyvern-frontend/src/routes/workflows/WorkflowActions.tsx index 7e7dd24a10..ffeb5acafe 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowActions.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowActions.tsx @@ -1,4 +1,5 @@ import { getClient } from "@/api/AxiosClient"; +import { GarbageIcon } from "@/components/icons/GarbageIcon"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -14,6 +15,10 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { toast } from "@/components/ui/use-toast"; @@ -21,28 +26,56 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { CopyIcon, DotsHorizontalIcon, + DownloadIcon, ReloadIcon, } from "@radix-ui/react-icons"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; -import { useWorkflowQuery } from "./hooks/useWorkflowQuery"; +import { useNavigate } from "react-router-dom"; import { stringify as convertToYAML } from "yaml"; +import { convert } from "./editor/workflowEditorUtils"; +import { useWorkflowQuery } from "./hooks/useWorkflowQuery"; import { WorkflowApiResponse } from "./types/workflowTypes"; -import { useNavigate } from "react-router-dom"; import { WorkflowCreateYAMLRequest } from "./types/workflowYamlTypes"; -import { convert } from "./editor/workflowEditorUtils"; -import { GarbageIcon } from "@/components/icons/GarbageIcon"; type Props = { id: string; }; +function downloadFile(fileName: string, contents: string) { + const element = document.createElement("a"); + element.setAttribute( + "href", + "data:text/plain;charset=utf-8," + encodeURIComponent(contents), + ); + element.setAttribute("download", fileName); + + element.style.display = "none"; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + function WorkflowActions({ id }: Props) { const credentialGetter = useCredentialGetter(); const queryClient = useQueryClient(); const { data: workflow } = useWorkflowQuery({ workflowPermanentId: id }); const navigate = useNavigate(); + function handleExport(type: "json" | "yaml") { + if (!workflow) { + return; + } + const fileName = `${workflow.title}.${type}`; + const contents = + type === "json" + ? JSON.stringify(convert(workflow), null, 2) + : convertToYAML(convert(workflow)); + downloadFile(fileName, contents); + } + const createWorkflowMutation = useMutation({ mutationFn: async (workflow: WorkflowCreateYAMLRequest) => { const client = await getClient(credentialGetter); @@ -98,7 +131,10 @@ function WorkflowActions({ id }: Props) { if (!workflow) { return; } - const clonedWorkflow = convert(workflow); + const clonedWorkflow = convert({ + ...workflow, + title: `Copy of ${workflow.title}`, + }); createWorkflowMutation.mutate(clonedWorkflow); }} className="p-2" @@ -106,6 +142,31 @@ function WorkflowActions({ id }: Props) { Clone Workflow + + + + Export as... + + + + { + handleExport("yaml"); + }} + > + YAML + + { + handleExport("json"); + }} + > + JSON + + + + + diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index 3c1ca8ec4f..8f2781a1e7 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -1065,12 +1065,11 @@ function convertBlocks(blocks: Array): Array { } function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest { - const title = `Copy of ${workflow.title}`; const userParameters = workflow.workflow_definition.parameters.filter( (parameter) => parameter.parameter_type !== "output", ); return { - title: title, + title: workflow.title, description: workflow.description, proxy_location: workflow.proxy_location, webhook_callback_url: workflow.webhook_callback_url,