Add download node (#1273)
This commit is contained in:
@@ -0,0 +1,366 @@
|
|||||||
|
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||||
|
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||||
|
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||||
|
import { DownloadIcon } from "@radix-ui/react-icons";
|
||||||
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
commonFieldPlaceholders,
|
||||||
|
commonHelpTooltipContent,
|
||||||
|
} from "../../constants";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
import { NodeActionMenu } from "../NodeActionMenu";
|
||||||
|
import { errorMappingExampleValue } from "../types";
|
||||||
|
import type { FileDownloadNode } from "./types";
|
||||||
|
|
||||||
|
const urlTooltip =
|
||||||
|
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
|
||||||
|
const urlPlaceholder = "https://";
|
||||||
|
const navigationGoalTooltip =
|
||||||
|
"Give Skyvern an objective that describes how to download the file.";
|
||||||
|
const navigationGoalPlaceholder = "Tell Skyvern which file to download.";
|
||||||
|
|
||||||
|
function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const { editable } = data;
|
||||||
|
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||||
|
id,
|
||||||
|
initialValue: data.label,
|
||||||
|
});
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
url: data.url,
|
||||||
|
navigationGoal: data.navigationGoal,
|
||||||
|
errorCodeMapping: data.errorCodeMapping,
|
||||||
|
maxRetries: data.maxRetries,
|
||||||
|
maxStepsOverride: data.maxStepsOverride,
|
||||||
|
continueOnFailure: data.continueOnFailure,
|
||||||
|
cacheActions: data.cacheActions,
|
||||||
|
downloadSuffix: data.downloadSuffix,
|
||||||
|
totpVerificationUrl: data.totpVerificationUrl,
|
||||||
|
totpIdentifier: data.totpIdentifier,
|
||||||
|
});
|
||||||
|
const deleteNodeCallback = useDeleteNodeCallback();
|
||||||
|
|
||||||
|
function handleChange(key: string, value: unknown) {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputs({ ...inputs, [key]: value });
|
||||||
|
updateNodeData(id, { [key]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="a"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="b"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||||
|
<header className="flex h-[2.75rem] justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||||
|
<DownloadIcon className="size-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<EditableNodeTitle
|
||||||
|
value={label}
|
||||||
|
editable={editable}
|
||||||
|
onChange={setLabel}
|
||||||
|
titleClassName="text-base"
|
||||||
|
inputClassName="text-base"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
File Download Block
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NodeActionMenu
|
||||||
|
onDelete={() => {
|
||||||
|
deleteNodeCallback(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">URL</Label>
|
||||||
|
<HelpTooltip content={urlTooltip} />
|
||||||
|
</div>
|
||||||
|
<AutoResizingTextarea
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("url", event.target.value);
|
||||||
|
}}
|
||||||
|
value={inputs.url}
|
||||||
|
placeholder={urlPlaceholder}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">Download Goal</Label>
|
||||||
|
<HelpTooltip content={navigationGoalTooltip} />
|
||||||
|
</div>
|
||||||
|
<AutoResizingTextarea
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("navigationGoal", event.target.value);
|
||||||
|
}}
|
||||||
|
value={inputs.navigationGoal}
|
||||||
|
placeholder={navigationGoalPlaceholder}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md bg-slate-800 p-2 text-xs text-slate-400">
|
||||||
|
Once the file is downloaded, this block will complete.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<Accordion type="single" collapsible>
|
||||||
|
<AccordionItem value="advanced" className="border-b-0">
|
||||||
|
<AccordionTrigger className="py-0">
|
||||||
|
Advanced Settings
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pl-6 pr-1 pt-1">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Max Retries
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["maxRetries"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={commonFieldPlaceholders["maxRetries"]}
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
min="0"
|
||||||
|
value={inputs.maxRetries ?? ""}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value =
|
||||||
|
event.target.value === ""
|
||||||
|
? null
|
||||||
|
: Number(event.target.value);
|
||||||
|
handleChange("maxRetries", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Max Steps Override
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["maxStepsOverride"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={commonFieldPlaceholders["maxStepsOverride"]}
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
min="0"
|
||||||
|
value={inputs.maxStepsOverride ?? ""}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value =
|
||||||
|
event.target.value === ""
|
||||||
|
? null
|
||||||
|
: Number(event.target.value);
|
||||||
|
handleChange("maxStepsOverride", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Error Messages
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["errorCodeMapping"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={inputs.errorCodeMapping !== "null"}
|
||||||
|
disabled={!editable}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange(
|
||||||
|
"errorCodeMapping",
|
||||||
|
checked
|
||||||
|
? JSON.stringify(errorMappingExampleValue, null, 2)
|
||||||
|
: "null",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{inputs.errorCodeMapping !== "null" && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={inputs.errorCodeMapping}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("errorCodeMapping", value);
|
||||||
|
}}
|
||||||
|
className="nowheel nopan"
|
||||||
|
fontSize={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Continue on Failure
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["continueOnFailure"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.continueOnFailure}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("continueOnFailure", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Cache Actions
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["cacheActions"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.cacheActions}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("cacheActions", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
File Suffix
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["fileSuffix"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={commonFieldPlaceholders["downloadSuffix"]}
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
value={inputs.downloadSuffix ?? ""}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("downloadSuffix", event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">
|
||||||
|
2FA Verification URL
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["totpVerificationUrl"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AutoResizingTextarea
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("totpVerificationUrl", event.target.value);
|
||||||
|
}}
|
||||||
|
value={inputs.totpVerificationUrl ?? ""}
|
||||||
|
placeholder={commonFieldPlaceholders["totpVerificationUrl"]}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">
|
||||||
|
2FA Identifier
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["totpIdentifier"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AutoResizingTextarea
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("totpIdentifier", event.target.value);
|
||||||
|
}}
|
||||||
|
value={inputs.totpIdentifier ?? ""}
|
||||||
|
placeholder={commonFieldPlaceholders["totpIdentifier"]}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FileDownloadNode };
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Node } from "@xyflow/react";
|
||||||
|
import { NodeBaseData } from "../types";
|
||||||
|
|
||||||
|
export type FileDownloadNodeData = NodeBaseData & {
|
||||||
|
url: string;
|
||||||
|
navigationGoal: string;
|
||||||
|
errorCodeMapping: string;
|
||||||
|
maxRetries: number | null;
|
||||||
|
maxStepsOverride: number | null;
|
||||||
|
downloadSuffix: string | null;
|
||||||
|
parameterKeys: Array<string>;
|
||||||
|
totpVerificationUrl: string | null;
|
||||||
|
totpIdentifier: string | null;
|
||||||
|
cacheActions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileDownloadNode = Node<FileDownloadNodeData, "fileDownload">;
|
||||||
|
|
||||||
|
export const fileDownloadNodeDefaultData: FileDownloadNodeData = {
|
||||||
|
label: "",
|
||||||
|
url: "",
|
||||||
|
navigationGoal: "",
|
||||||
|
errorCodeMapping: "null",
|
||||||
|
maxRetries: null,
|
||||||
|
maxStepsOverride: null,
|
||||||
|
downloadSuffix: null,
|
||||||
|
editable: true,
|
||||||
|
parameterKeys: [],
|
||||||
|
totpVerificationUrl: null,
|
||||||
|
totpIdentifier: null,
|
||||||
|
continueOnFailure: false,
|
||||||
|
cacheActions: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function isFileDownloadNode(node: Node): node is FileDownloadNode {
|
||||||
|
return node.type === "fileDownload";
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ import { LoginNode } from "./LoginNode/types";
|
|||||||
import { LoginNode as LoginNodeComponent } from "./LoginNode/LoginNode";
|
import { LoginNode as LoginNodeComponent } from "./LoginNode/LoginNode";
|
||||||
import { WaitNode } from "./WaitNode/types";
|
import { WaitNode } from "./WaitNode/types";
|
||||||
import { WaitNode as WaitNodeComponent } from "./WaitNode/WaitNode";
|
import { WaitNode as WaitNodeComponent } from "./WaitNode/WaitNode";
|
||||||
|
import { FileDownloadNode } from "./FileDownloadNode/types";
|
||||||
|
import { FileDownloadNode as FileDownloadNodeComponent } from "./FileDownloadNode/FileDownloadNode";
|
||||||
|
|
||||||
export type UtilityNode = StartNode | NodeAdderNode;
|
export type UtilityNode = StartNode | NodeAdderNode;
|
||||||
|
|
||||||
@@ -48,7 +50,8 @@ export type WorkflowBlockNode =
|
|||||||
| NavigationNode
|
| NavigationNode
|
||||||
| ExtractionNode
|
| ExtractionNode
|
||||||
| LoginNode
|
| LoginNode
|
||||||
| WaitNode;
|
| WaitNode
|
||||||
|
| FileDownloadNode;
|
||||||
|
|
||||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||||
return node.type === "nodeAdder" || node.type === "start";
|
return node.type === "nodeAdder" || node.type === "start";
|
||||||
@@ -77,4 +80,5 @@ export const nodeTypes = {
|
|||||||
extraction: memo(ExtractionNodeComponent),
|
extraction: memo(ExtractionNodeComponent),
|
||||||
login: memo(LoginNodeComponent),
|
login: memo(LoginNodeComponent),
|
||||||
wait: memo(WaitNodeComponent),
|
wait: memo(WaitNodeComponent),
|
||||||
|
fileDownload: memo(FileDownloadNodeComponent),
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CheckCircledIcon,
|
CheckCircledIcon,
|
||||||
Cross2Icon,
|
Cross2Icon,
|
||||||
CursorTextIcon,
|
CursorTextIcon,
|
||||||
|
DownloadIcon,
|
||||||
EnvelopeClosedIcon,
|
EnvelopeClosedIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
ListBulletIcon,
|
ListBulletIcon,
|
||||||
@@ -111,6 +112,12 @@ const nodeLibraryItems: Array<{
|
|||||||
title: "Wait Block",
|
title: "Wait Block",
|
||||||
description: "Wait for some time",
|
description: "Wait for some time",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
nodeType: "fileDownload",
|
||||||
|
icon: <DownloadIcon className="size-6" />,
|
||||||
|
title: "File Download Block",
|
||||||
|
description: "Download a file",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
ExtractionBlockYAML,
|
ExtractionBlockYAML,
|
||||||
LoginBlockYAML,
|
LoginBlockYAML,
|
||||||
WaitBlockYAML,
|
WaitBlockYAML,
|
||||||
|
FileDownloadBlockYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import {
|
import {
|
||||||
EMAIL_BLOCK_SENDER,
|
EMAIL_BLOCK_SENDER,
|
||||||
@@ -79,6 +80,7 @@ import {
|
|||||||
} from "./nodes/ExtractionNode/types";
|
} from "./nodes/ExtractionNode/types";
|
||||||
import { loginNodeDefaultData } from "./nodes/LoginNode/types";
|
import { loginNodeDefaultData } from "./nodes/LoginNode/types";
|
||||||
import { waitNodeDefaultData } from "./nodes/WaitNode/types";
|
import { waitNodeDefaultData } from "./nodes/WaitNode/types";
|
||||||
|
import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
|
||||||
import { ProxyLocation } from "@/api/types";
|
import { ProxyLocation } from "@/api/types";
|
||||||
|
|
||||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||||
@@ -293,6 +295,26 @@ function convertToNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "file_download": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "fileDownload",
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
url: block.url ?? "",
|
||||||
|
navigationGoal: block.navigation_goal ?? "",
|
||||||
|
errorCodeMapping: JSON.stringify(block.error_code_mapping, null, 2),
|
||||||
|
downloadSuffix: block.download_suffix ?? null,
|
||||||
|
maxRetries: block.max_retries ?? null,
|
||||||
|
parameterKeys: block.parameters.map((p) => p.key),
|
||||||
|
totpIdentifier: block.totp_identifier ?? null,
|
||||||
|
totpVerificationUrl: block.totp_verification_url ?? null,
|
||||||
|
cacheActions: block.cache_actions,
|
||||||
|
maxStepsOverride: block.max_steps_per_run ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "code": {
|
case "code": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -667,6 +689,17 @@ function createNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "fileDownload": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "fileDownload",
|
||||||
|
data: {
|
||||||
|
...fileDownloadNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "loop": {
|
case "loop": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -888,6 +921,28 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
wait_sec: node.data.waitInSeconds,
|
wait_sec: node.data.waitInSeconds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "fileDownload": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
block_type: "file_download",
|
||||||
|
title: node.data.label,
|
||||||
|
navigation_goal: node.data.navigationGoal,
|
||||||
|
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
> | null,
|
||||||
|
url: node.data.url,
|
||||||
|
...(node.data.maxRetries !== null && {
|
||||||
|
max_retries: node.data.maxRetries,
|
||||||
|
}),
|
||||||
|
max_steps_per_run: node.data.maxStepsOverride,
|
||||||
|
download_suffix: node.data.downloadSuffix,
|
||||||
|
parameter_keys: node.data.parameterKeys,
|
||||||
|
totp_identifier: node.data.totpIdentifier,
|
||||||
|
totp_verification_url: node.data.totpVerificationUrl,
|
||||||
|
cache_actions: node.data.cacheActions,
|
||||||
|
};
|
||||||
|
}
|
||||||
case "sendEmail": {
|
case "sendEmail": {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -1473,6 +1528,24 @@ function convertBlocksToBlockYAML(
|
|||||||
};
|
};
|
||||||
return blockYaml;
|
return blockYaml;
|
||||||
}
|
}
|
||||||
|
case "file_download": {
|
||||||
|
const blockYaml: FileDownloadBlockYAML = {
|
||||||
|
...base,
|
||||||
|
block_type: "file_download",
|
||||||
|
url: block.url,
|
||||||
|
title: block.title,
|
||||||
|
navigation_goal: block.navigation_goal,
|
||||||
|
error_code_mapping: block.error_code_mapping,
|
||||||
|
max_retries: block.max_retries,
|
||||||
|
max_steps_per_run: block.max_steps_per_run,
|
||||||
|
download_suffix: block.download_suffix,
|
||||||
|
parameter_keys: block.parameters.map((p) => p.key),
|
||||||
|
totp_identifier: block.totp_identifier,
|
||||||
|
totp_verification_url: block.totp_verification_url,
|
||||||
|
cache_actions: block.cache_actions,
|
||||||
|
};
|
||||||
|
return blockYaml;
|
||||||
|
}
|
||||||
case "for_loop": {
|
case "for_loop": {
|
||||||
const blockYaml: ForLoopBlockYAML = {
|
const blockYaml: ForLoopBlockYAML = {
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ export type WorkflowBlock =
|
|||||||
| NavigationBlock
|
| NavigationBlock
|
||||||
| ExtractionBlock
|
| ExtractionBlock
|
||||||
| LoginBlock
|
| LoginBlock
|
||||||
| WaitBlock;
|
| WaitBlock
|
||||||
|
| FileDownloadBlock;
|
||||||
|
|
||||||
export const WorkflowBlockType = {
|
export const WorkflowBlockType = {
|
||||||
Task: "task",
|
Task: "task",
|
||||||
@@ -134,6 +135,7 @@ export const WorkflowBlockType = {
|
|||||||
Extraction: "extraction",
|
Extraction: "extraction",
|
||||||
Login: "login",
|
Login: "login",
|
||||||
Wait: "wait",
|
Wait: "wait",
|
||||||
|
FileDownload: "file_download",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type WorkflowBlockType =
|
export type WorkflowBlockType =
|
||||||
@@ -284,6 +286,21 @@ export type WaitBlock = WorkflowBlockBase & {
|
|||||||
wait_sec?: number;
|
wait_sec?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FileDownloadBlock = WorkflowBlockBase & {
|
||||||
|
block_type: "file_download";
|
||||||
|
url: string | null;
|
||||||
|
title: string;
|
||||||
|
navigation_goal: string | null;
|
||||||
|
error_code_mapping: Record<string, string> | null;
|
||||||
|
max_retries?: number;
|
||||||
|
max_steps_per_run?: number | null;
|
||||||
|
download_suffix?: string | null;
|
||||||
|
parameters: Array<WorkflowParameter>;
|
||||||
|
totp_verification_url?: string | null;
|
||||||
|
totp_identifier?: string | null;
|
||||||
|
cache_actions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowDefinition = {
|
export type WorkflowDefinition = {
|
||||||
parameters: Array<Parameter>;
|
parameters: Array<Parameter>;
|
||||||
blocks: Array<WorkflowBlock>;
|
blocks: Array<WorkflowBlock>;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const BlockTypes = {
|
|||||||
EXTRACTION: "extraction",
|
EXTRACTION: "extraction",
|
||||||
LOGIN: "login",
|
LOGIN: "login",
|
||||||
WAIT: "wait",
|
WAIT: "wait",
|
||||||
|
FILE_DOWNLOAD: "file_download",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
||||||
@@ -100,7 +101,8 @@ export type BlockYAML =
|
|||||||
| NavigationBlockYAML
|
| NavigationBlockYAML
|
||||||
| ExtractionBlockYAML
|
| ExtractionBlockYAML
|
||||||
| LoginBlockYAML
|
| LoginBlockYAML
|
||||||
| WaitBlockYAML;
|
| WaitBlockYAML
|
||||||
|
| FileDownloadBlockYAML;
|
||||||
|
|
||||||
export type BlockYAMLBase = {
|
export type BlockYAMLBase = {
|
||||||
block_type: BlockType;
|
block_type: BlockType;
|
||||||
@@ -196,6 +198,21 @@ export type WaitBlockYAML = BlockYAMLBase & {
|
|||||||
wait_sec?: number;
|
wait_sec?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FileDownloadBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "file_download";
|
||||||
|
url: string | null;
|
||||||
|
title?: string;
|
||||||
|
navigation_goal: string | null;
|
||||||
|
error_code_mapping: Record<string, string> | null;
|
||||||
|
max_retries?: number;
|
||||||
|
max_steps_per_run?: number | null;
|
||||||
|
parameter_keys?: Array<string> | null;
|
||||||
|
download_suffix?: string | null;
|
||||||
|
totp_verification_url?: string | null;
|
||||||
|
totp_identifier?: string | null;
|
||||||
|
cache_actions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type CodeBlockYAML = BlockYAMLBase & {
|
export type CodeBlockYAML = BlockYAMLBase & {
|
||||||
block_type: "code";
|
block_type: "code";
|
||||||
code: string;
|
code: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user