Add navigation block (#1256)
Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
@@ -14,3 +14,33 @@ export const SMTP_USERNAME_AWS_KEY = "SKYVERN_SMTP_USERNAME_SES";
|
||||
export const SMTP_PASSWORD_AWS_KEY = "SKYVERN_SMTP_PASSWORD_SES";
|
||||
|
||||
export const EMAIL_BLOCK_SENDER = "hello@skyvern.com";
|
||||
|
||||
export const commonHelpTooltipContent = {
|
||||
maxRetries:
|
||||
"Specify how many times you would like a task to retry upon failure.",
|
||||
maxStepsOverride:
|
||||
"Specify the maximum number of steps a task can take in total.",
|
||||
completeOnDownload:
|
||||
"Allow Skyvern to auto-complete the task when it downloads a file.",
|
||||
fileSuffix:
|
||||
"A file suffix that's automatically added to all downloaded files.",
|
||||
errorCodeMapping:
|
||||
"Knowing about why a task terminated can be important, specify error messages here.",
|
||||
totpVerificationUrl:
|
||||
"If you have an internal system for storing TOTP codes, link the endpoint here.",
|
||||
totpIdentifier:
|
||||
"If you are running multiple tasks or workflows at once, you will need to give the task an identifier to know that this TOTP goes with this task.",
|
||||
continueOnFailure:
|
||||
"Allow the workflow to continue if it encounters a failure.",
|
||||
cacheActions: "Cache the actions of this task.",
|
||||
} as const;
|
||||
|
||||
export const commonFieldPlaceholders = {
|
||||
url: "https://",
|
||||
navigationGoal: 'Input text into "Name" field.',
|
||||
maxRetries: "Default: 3",
|
||||
maxStepsOverride: "Default: 10",
|
||||
downloadSuffix: "Add an ID for downloaded files",
|
||||
totpVerificationUrl: "Provide your 2FA endpoint",
|
||||
totpIdentifier: "Add an ID that links your TOTP to the task",
|
||||
} as const;
|
||||
|
||||
@@ -13,15 +13,23 @@ import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { helpTooltipContent, type ActionNode } from "./types";
|
||||
import type { ActionNode } from "./types";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { fieldPlaceholders } from "./types";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ClickIcon } from "@/components/icons/ClickIcon";
|
||||
import {
|
||||
commonFieldPlaceholders,
|
||||
commonHelpTooltipContent,
|
||||
} from "../../constants";
|
||||
|
||||
const navigationGoalTooltip =
|
||||
"Specify a single step or action you'd like Skyvern to complete. Actions are one-off tasks like filling a field or interacting with a specific element on the page.\n\nCurrently supported actions are click, input text, upload file, and select.";
|
||||
|
||||
const navigationGoalPlaceholder = 'Input text into "Name" field.';
|
||||
|
||||
function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
@@ -91,7 +99,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">Action Instruction</Label>
|
||||
<HelpTooltip content={helpTooltipContent["navigationGoal"]} />
|
||||
<HelpTooltip content={navigationGoalTooltip} />
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
onChange={(event) => {
|
||||
@@ -101,6 +109,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
handleChange("navigationGoal", event.target.value);
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
placeholder={navigationGoalPlaceholder}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -117,11 +126,13 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Retries
|
||||
</Label>
|
||||
<HelpTooltip content={helpTooltipContent["maxRetries"]} />
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["maxRetries"]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={fieldPlaceholders["maxRetries"]}
|
||||
placeholder={commonFieldPlaceholders["maxRetries"]}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxRetries ?? ""}
|
||||
@@ -144,7 +155,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
Error Messages
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["errorCodeMapping"]}
|
||||
content={commonHelpTooltipContent["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
@@ -187,7 +198,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["continueOnFailure"]}
|
||||
content={commonHelpTooltipContent["continueOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
@@ -207,7 +218,9 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Cache Actions
|
||||
</Label>
|
||||
<HelpTooltip content={helpTooltipContent["cacheActions"]} />
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["cacheActions"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
@@ -228,7 +241,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
Complete on Download
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["completeOnDownload"]}
|
||||
content={commonHelpTooltipContent["completeOnDownload"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
@@ -248,11 +261,13 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
File Suffix
|
||||
</Label>
|
||||
<HelpTooltip content={helpTooltipContent["fileSuffix"]} />
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["fileSuffix"]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={fieldPlaceholders["downloadSuffix"]}
|
||||
placeholder={commonFieldPlaceholders["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
onChange={(event) => {
|
||||
@@ -270,7 +285,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
2FA Verification URL
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["totpVerificationUrl"]}
|
||||
content={commonHelpTooltipContent["totpVerificationUrl"]}
|
||||
/>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
@@ -281,7 +296,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
handleChange("totpVerificationUrl", event.target.value);
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
placeholder={fieldPlaceholders["totpVerificationUrl"]}
|
||||
placeholder={commonFieldPlaceholders["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -291,7 +306,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
2FA Identifier
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["totpIdentifier"]}
|
||||
content={commonHelpTooltipContent["totpIdentifier"]}
|
||||
/>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
@@ -302,7 +317,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
handleChange("totpIdentifier", event.target.value);
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
placeholder={fieldPlaceholders["totpIdentifier"]}
|
||||
placeholder={commonFieldPlaceholders["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -35,34 +35,3 @@ export const actionNodeDefaultData: ActionNodeData = {
|
||||
export function isActionNode(node: Node): node is ActionNode {
|
||||
return node.type === "action";
|
||||
}
|
||||
|
||||
export const helpTooltipContent = {
|
||||
navigationGoal:
|
||||
"Specify a single step or action you'd like Skyvern to complete. Actions are one-off tasks like filling a field or interacting with a specific element on the page.\n\nCurrently supported actions are click, input text, upload file, and select.",
|
||||
maxRetries:
|
||||
"Specify how many times you would like a task to retry upon failure.",
|
||||
maxStepsOverride:
|
||||
"Specify the maximum number of steps a task can take in total.",
|
||||
completeOnDownload:
|
||||
"Allow Skyvern to auto-complete the task when it downloads a file.",
|
||||
fileSuffix:
|
||||
"A file suffix that's automatically added to all downloaded files.",
|
||||
errorCodeMapping:
|
||||
"Knowing about why a task terminated can be important, specify error messages here.",
|
||||
totpVerificationUrl:
|
||||
"If you have an internal system for storing TOTP codes, link the endpoint here.",
|
||||
totpIdentifier:
|
||||
"If you are running multiple tasks or workflows at once, you will need to give the task an identifier to know that this TOTP goes with this task.",
|
||||
continueOnFailure:
|
||||
"Allow the workflow to continue if it encounters a failure.",
|
||||
cacheActions: "Cache the actions of this task.",
|
||||
} as const;
|
||||
|
||||
export const fieldPlaceholders = {
|
||||
navigationGoal: 'Input text into "Name" field.',
|
||||
maxRetries: "Default: 3",
|
||||
maxStepsOverride: "Default: 10",
|
||||
downloadSuffix: "Add an ID for downloaded files",
|
||||
totpVerificationUrl: "Provide your 2FA endpoint",
|
||||
totpIdentifier: "Add an ID that links your TOTP to the task",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { NavigationNode } from "./types";
|
||||
import {
|
||||
commonFieldPlaceholders,
|
||||
commonHelpTooltipContent,
|
||||
} from "../../constants";
|
||||
import { RobotIcon } from "@/components/icons/RobotIcon";
|
||||
|
||||
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. Make sure to include when the task is complete, when it should self-terminate, and any guardrails.";
|
||||
const navigationGoalPlaceholder = "Tell Skyvern what to do.";
|
||||
|
||||
function NavigationNode({ id, data }: NodeProps<NavigationNode>) {
|
||||
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,
|
||||
allowDownloads: data.allowDownloads,
|
||||
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">
|
||||
<RobotIcon 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">Navigation Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div className="space-y-2">
|
||||
<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">Navigation 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>
|
||||
<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">
|
||||
Complete on Download
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["completeOnDownload"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.allowDownloads}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("allowDownloads", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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 { NavigationNode };
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
|
||||
export type NavigationNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
navigationGoal: string;
|
||||
errorCodeMapping: string;
|
||||
maxRetries: number | null;
|
||||
maxStepsOverride: number | null;
|
||||
allowDownloads: boolean;
|
||||
downloadSuffix: string | null;
|
||||
parameterKeys: Array<string>;
|
||||
totpVerificationUrl: string | null;
|
||||
totpIdentifier: string | null;
|
||||
cacheActions: boolean;
|
||||
};
|
||||
|
||||
export type NavigationNode = Node<NavigationNodeData, "navigation">;
|
||||
|
||||
export const navigationNodeDefaultData: NavigationNodeData = {
|
||||
label: "",
|
||||
url: "",
|
||||
navigationGoal: "",
|
||||
errorCodeMapping: "null",
|
||||
maxRetries: null,
|
||||
maxStepsOverride: null,
|
||||
allowDownloads: false,
|
||||
downloadSuffix: null,
|
||||
editable: true,
|
||||
parameterKeys: [],
|
||||
totpVerificationUrl: null,
|
||||
totpIdentifier: null,
|
||||
continueOnFailure: false,
|
||||
cacheActions: false,
|
||||
} as const;
|
||||
|
||||
export function isNavigationNode(node: Node): node is NavigationNode {
|
||||
return node.type === "navigation";
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import type { ValidationNode } from "./ValidationNode/types";
|
||||
import { ValidationNode as ValidationNodeComponent } from "./ValidationNode/ValidationNode";
|
||||
import type { ActionNode } from "./ActionNode/types";
|
||||
import { ActionNode as ActionNodeComponent } from "./ActionNode/ActionNode";
|
||||
import { NavigationNode } from "./NavigationNode/types";
|
||||
import { NavigationNode as NavigationNodeComponent } from "./NavigationNode/NavigationNode";
|
||||
|
||||
export type UtilityNode = StartNode | NodeAdderNode;
|
||||
|
||||
@@ -36,7 +38,8 @@ export type WorkflowBlockNode =
|
||||
| UploadNode
|
||||
| DownloadNode
|
||||
| ValidationNode
|
||||
| ActionNode;
|
||||
| ActionNode
|
||||
| NavigationNode;
|
||||
|
||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||
return node.type === "nodeAdder" || node.type === "start";
|
||||
@@ -61,4 +64,5 @@ export const nodeTypes = {
|
||||
start: memo(StartNodeComponent),
|
||||
validation: memo(ValidationNodeComponent),
|
||||
action: memo(ActionNodeComponent),
|
||||
};
|
||||
navigation: memo(NavigationNodeComponent),
|
||||
} as const;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { WorkflowBlockNode } from "../nodes";
|
||||
import { AddNodeProps } from "../FlowRenderer";
|
||||
import { ClickIcon } from "@/components/icons/ClickIcon";
|
||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||
import { RobotIcon } from "@/components/icons/RobotIcon";
|
||||
|
||||
const nodeLibraryItems: Array<{
|
||||
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
||||
@@ -83,6 +84,12 @@ const nodeLibraryItems: Array<{
|
||||
title: "Action Block",
|
||||
description: "Take a single action",
|
||||
},
|
||||
{
|
||||
nodeType: "navigation",
|
||||
icon: <RobotIcon className="size-6" />,
|
||||
title: "Navigation Block",
|
||||
description: "Navigate on the page",
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
TextPromptBlockYAML,
|
||||
UploadToS3BlockYAML,
|
||||
ValidationBlockYAML,
|
||||
NavigationBlockYAML,
|
||||
WorkflowCreateYAMLRequest,
|
||||
} from "../types/workflowYamlTypes";
|
||||
import {
|
||||
@@ -52,6 +53,7 @@ import { NodeBaseData } from "./nodes/types";
|
||||
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
||||
import { validationNodeDefaultData } from "./nodes/ValidationNode/types";
|
||||
import { actionNodeDefaultData } from "./nodes/ActionNode/types";
|
||||
import { navigationNodeDefaultData } from "./nodes/NavigationNode/types";
|
||||
|
||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||
|
||||
@@ -197,6 +199,27 @@ function convertToNode(
|
||||
},
|
||||
};
|
||||
}
|
||||
case "navigation": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "navigation",
|
||||
data: {
|
||||
...commonData,
|
||||
url: block.url ?? "",
|
||||
navigationGoal: block.navigation_goal ?? "",
|
||||
errorCodeMapping: JSON.stringify(block.error_code_mapping, null, 2),
|
||||
allowDownloads: block.complete_on_download ?? false,
|
||||
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": {
|
||||
return {
|
||||
...identifiers,
|
||||
@@ -509,6 +532,17 @@ function createNode(
|
||||
},
|
||||
};
|
||||
}
|
||||
case "navigation": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "navigation",
|
||||
data: {
|
||||
...navigationNodeDefaultData,
|
||||
label,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "loop": {
|
||||
return {
|
||||
...identifiers,
|
||||
@@ -662,6 +696,28 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
||||
cache_actions: node.data.cacheActions,
|
||||
};
|
||||
}
|
||||
case "navigation": {
|
||||
return {
|
||||
...base,
|
||||
block_type: "navigation",
|
||||
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,
|
||||
complete_on_download: node.data.allowDownloads,
|
||||
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": {
|
||||
return {
|
||||
...base,
|
||||
@@ -1042,7 +1098,7 @@ function getAvailableOutputParameterKeys(
|
||||
return outputParameterKeys;
|
||||
}
|
||||
|
||||
function convertParameters(
|
||||
function convertParametersToParameterYAML(
|
||||
parameters: Array<Exclude<Parameter, OutputParameter>>,
|
||||
): Array<ParameterYAML> {
|
||||
return parameters.map((parameter) => {
|
||||
@@ -1105,7 +1161,9 @@ function convertParameters(
|
||||
});
|
||||
}
|
||||
|
||||
function convertBlocks(blocks: Array<WorkflowBlock>): Array<BlockYAML> {
|
||||
function convertBlocksToBlockYAML(
|
||||
blocks: Array<WorkflowBlock>,
|
||||
): Array<BlockYAML> {
|
||||
return blocks.map((block) => {
|
||||
const base = {
|
||||
label: block.label,
|
||||
@@ -1160,12 +1218,30 @@ function convertBlocks(blocks: Array<WorkflowBlock>): Array<BlockYAML> {
|
||||
};
|
||||
return blockYaml;
|
||||
}
|
||||
case "navigation": {
|
||||
const blockYaml: NavigationBlockYAML = {
|
||||
...base,
|
||||
block_type: "navigation",
|
||||
url: block.url,
|
||||
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,
|
||||
complete_on_download: block.complete_on_download,
|
||||
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": {
|
||||
const blockYaml: ForLoopBlockYAML = {
|
||||
...base,
|
||||
block_type: "for_loop",
|
||||
loop_over_parameter_key: block.loop_over.key,
|
||||
loop_blocks: convertBlocks(block.loop_blocks),
|
||||
loop_blocks: convertBlocksToBlockYAML(block.loop_blocks),
|
||||
};
|
||||
return blockYaml;
|
||||
}
|
||||
@@ -1244,8 +1320,8 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest {
|
||||
webhook_callback_url: workflow.webhook_callback_url,
|
||||
totp_verification_url: workflow.totp_verification_url,
|
||||
workflow_definition: {
|
||||
parameters: convertParameters(userParameters),
|
||||
blocks: convertBlocks(workflow.workflow_definition.blocks),
|
||||
parameters: convertParametersToParameterYAML(userParameters),
|
||||
blocks: convertBlocksToBlockYAML(workflow.workflow_definition.blocks),
|
||||
},
|
||||
is_saved_task: workflow.is_saved_task,
|
||||
};
|
||||
|
||||
@@ -111,7 +111,9 @@ export const WorkflowBlockType = {
|
||||
SendEmail: "send_email",
|
||||
FileURLParser: "file_url_parser",
|
||||
Validation: "validation",
|
||||
};
|
||||
Action: "action",
|
||||
Navigation: "navigation",
|
||||
} as const;
|
||||
|
||||
export type WorkflowBlockType =
|
||||
(typeof WorkflowBlockType)[keyof typeof WorkflowBlockType];
|
||||
@@ -198,9 +200,36 @@ export type ValidationBlock = WorkflowBlockBase & {
|
||||
parameters: Array<WorkflowParameter>;
|
||||
};
|
||||
|
||||
export type ActionBlock = Omit<TaskBlock, "block_type"> & {
|
||||
export type ActionBlock = WorkflowBlockBase & {
|
||||
block_type: "action";
|
||||
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;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
complete_on_download?: boolean;
|
||||
download_suffix?: string | null;
|
||||
totp_verification_url?: string | null;
|
||||
totp_identifier?: string | null;
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type NavigationBlock = WorkflowBlockBase & {
|
||||
block_type: "navigation";
|
||||
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;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
complete_on_download?: boolean;
|
||||
download_suffix?: string | null;
|
||||
totp_verification_url?: string | null;
|
||||
totp_identifier?: string | null;
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowBlock =
|
||||
@@ -213,7 +242,8 @@ export type WorkflowBlock =
|
||||
| SendEmailBlock
|
||||
| FileURLParserBlock
|
||||
| ValidationBlock
|
||||
| ActionBlock;
|
||||
| ActionBlock
|
||||
| NavigationBlock;
|
||||
|
||||
export type WorkflowDefinition = {
|
||||
parameters: Array<Parameter>;
|
||||
|
||||
@@ -77,6 +77,7 @@ const BlockTypes = {
|
||||
FILE_URL_PARSER: "file_url_parser",
|
||||
VALIDATION: "validation",
|
||||
ACTION: "action",
|
||||
NAVIGATION: "navigation",
|
||||
} as const;
|
||||
|
||||
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
||||
@@ -91,7 +92,8 @@ export type BlockYAML =
|
||||
| FileUrlParserBlockYAML
|
||||
| ForLoopBlockYAML
|
||||
| ValidationBlockYAML
|
||||
| ActionBlockYAML;
|
||||
| ActionBlockYAML
|
||||
| NavigationBlockYAML;
|
||||
|
||||
export type BlockYAMLBase = {
|
||||
block_type: BlockType;
|
||||
@@ -139,6 +141,21 @@ export type ActionBlockYAML = BlockYAMLBase & {
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type NavigationBlockYAML = BlockYAMLBase & {
|
||||
block_type: "navigation";
|
||||
url: string | null;
|
||||
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;
|
||||
complete_on_download?: boolean;
|
||||
download_suffix?: string | null;
|
||||
totp_verification_url?: string | null;
|
||||
totp_identifier?: string | null;
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type CodeBlockYAML = BlockYAMLBase & {
|
||||
block_type: "code";
|
||||
code: string;
|
||||
|
||||
Reference in New Issue
Block a user