Add login block (#1264)
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useWorkflowParametersState } from "../../useWorkflowParametersState";
|
||||
import { useId } from "react";
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
function CredentialParameterSelector({ value, onChange }: Props) {
|
||||
const [workflowParameters] = useWorkflowParametersState();
|
||||
const credentialParameters = workflowParameters.filter(
|
||||
(parameter) => parameter.parameterType === "credential",
|
||||
);
|
||||
const noneItemValue = useId();
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(value) => {
|
||||
if (value === noneItemValue) {
|
||||
onChange?.("");
|
||||
} else {
|
||||
onChange?.(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a credential parameter" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{credentialParameters.map((parameter) => (
|
||||
<SelectItem key={parameter.key} value={parameter.key}>
|
||||
{parameter.key}
|
||||
</SelectItem>
|
||||
))}
|
||||
{credentialParameters.length === 0 && (
|
||||
<SelectItem value={noneItemValue} key={noneItemValue}>
|
||||
No credential parameters found
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export { CredentialParameterSelector };
|
||||
@@ -0,0 +1,364 @@
|
||||
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 { LoginNode } from "./types";
|
||||
import {
|
||||
commonFieldPlaceholders,
|
||||
commonHelpTooltipContent,
|
||||
} from "../../constants";
|
||||
import { LockOpen1Icon } from "@radix-ui/react-icons";
|
||||
import { CredentialParameterSelector } from "./CredentialParameterSelector";
|
||||
|
||||
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 LoginNode({ id, data }: NodeProps<LoginNode>) {
|
||||
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,
|
||||
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">
|
||||
<LockOpen1Icon 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">Login 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">Login 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="space-y-2">
|
||||
<Label className="text-xs text-slate-300">Credential Key</Label>
|
||||
<CredentialParameterSelector
|
||||
value={
|
||||
data.parameterKeys.length > 0
|
||||
? data.parameterKeys[0]
|
||||
: undefined
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
updateNodeData(id, { parameterKeys: [value] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md bg-slate-800 p-2">
|
||||
<div className="space-y-1 text-xs text-slate-400">
|
||||
<div>Credentials need to be added with the help of our team.</div>
|
||||
<div>
|
||||
Reach out to{" "}
|
||||
<span className="text-slate-200">support@skyvern.com</span> for
|
||||
assistance.
|
||||
</div>
|
||||
</div>
|
||||
</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="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 { LoginNode };
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
|
||||
export type LoginNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
navigationGoal: string;
|
||||
errorCodeMapping: string;
|
||||
maxRetries: number | null;
|
||||
maxStepsOverride: number | null;
|
||||
parameterKeys: Array<string>;
|
||||
totpVerificationUrl: string | null;
|
||||
totpIdentifier: string | null;
|
||||
cacheActions: boolean;
|
||||
};
|
||||
|
||||
export type LoginNode = Node<LoginNodeData, "login">;
|
||||
|
||||
export const loginNodeDefaultData: LoginNodeData = {
|
||||
label: "",
|
||||
url: "",
|
||||
navigationGoal:
|
||||
"If you're not on the login page, navigate to login page and login using the credentials given. First, take actions on promotional popups or cookie prompts that could prevent taking other action on the web page. If you fail to login to find the login page or can't login after several trials, terminate. If login is completed, you're successful. ",
|
||||
errorCodeMapping: "null",
|
||||
maxRetries: null,
|
||||
maxStepsOverride: null,
|
||||
editable: true,
|
||||
parameterKeys: [],
|
||||
totpVerificationUrl: null,
|
||||
totpIdentifier: null,
|
||||
continueOnFailure: false,
|
||||
cacheActions: false,
|
||||
} as const;
|
||||
|
||||
export function isLoginNode(node: Node): node is LoginNode {
|
||||
return node.type === "login";
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import { NavigationNode } from "./NavigationNode/types";
|
||||
import { NavigationNode as NavigationNodeComponent } from "./NavigationNode/NavigationNode";
|
||||
import { ExtractionNode } from "./ExtractionNode/types";
|
||||
import { ExtractionNode as ExtractionNodeComponent } from "./ExtractionNode/ExtractionNode";
|
||||
import { LoginNode } from "./LoginNode/types";
|
||||
import { LoginNode as LoginNodeComponent } from "./LoginNode/LoginNode";
|
||||
|
||||
export type UtilityNode = StartNode | NodeAdderNode;
|
||||
|
||||
@@ -42,7 +44,8 @@ export type WorkflowBlockNode =
|
||||
| ValidationNode
|
||||
| ActionNode
|
||||
| NavigationNode
|
||||
| ExtractionNode;
|
||||
| ExtractionNode
|
||||
| LoginNode;
|
||||
|
||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||
return node.type === "nodeAdder" || node.type === "start";
|
||||
@@ -69,4 +72,5 @@ export const nodeTypes = {
|
||||
action: memo(ActionNodeComponent),
|
||||
navigation: memo(NavigationNodeComponent),
|
||||
extraction: memo(ExtractionNodeComponent),
|
||||
login: memo(LoginNodeComponent),
|
||||
} as const;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
EnvelopeClosedIcon,
|
||||
FileIcon,
|
||||
ListBulletIcon,
|
||||
LockOpen1Icon,
|
||||
PlusIcon,
|
||||
UpdateIcon,
|
||||
UploadIcon,
|
||||
@@ -97,6 +98,12 @@ const nodeLibraryItems: Array<{
|
||||
title: "Extraction Block",
|
||||
description: "Extract data from the page",
|
||||
},
|
||||
{
|
||||
nodeType: "login",
|
||||
icon: <LockOpen1Icon className="size-6" />,
|
||||
title: "Login Block",
|
||||
description: "Login to a website",
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
NavigationBlockYAML,
|
||||
WorkflowCreateYAMLRequest,
|
||||
ExtractionBlockYAML,
|
||||
LoginBlockYAML,
|
||||
} from "../types/workflowYamlTypes";
|
||||
import {
|
||||
EMAIL_BLOCK_SENDER,
|
||||
@@ -69,6 +70,7 @@ import {
|
||||
extractionNodeDefaultData,
|
||||
isExtractionNode,
|
||||
} from "./nodes/ExtractionNode/types";
|
||||
import { loginNodeDefaultData } from "./nodes/LoginNode/types";
|
||||
|
||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||
|
||||
@@ -252,6 +254,25 @@ function convertToNode(
|
||||
},
|
||||
};
|
||||
}
|
||||
case "login": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "login",
|
||||
data: {
|
||||
...commonData,
|
||||
url: block.url ?? "",
|
||||
navigationGoal: block.navigation_goal ?? "",
|
||||
errorCodeMapping: JSON.stringify(block.error_code_mapping, null, 2),
|
||||
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,
|
||||
@@ -586,6 +607,17 @@ function createNode(
|
||||
},
|
||||
};
|
||||
}
|
||||
case "login": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "login",
|
||||
data: {
|
||||
...loginNodeDefaultData,
|
||||
label,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "loop": {
|
||||
return {
|
||||
...identifiers,
|
||||
@@ -779,6 +811,27 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
||||
cache_actions: node.data.cacheActions,
|
||||
};
|
||||
}
|
||||
case "login": {
|
||||
return {
|
||||
...base,
|
||||
block_type: "login",
|
||||
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,
|
||||
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,
|
||||
@@ -1315,6 +1368,23 @@ function convertBlocksToBlockYAML(
|
||||
};
|
||||
return blockYaml;
|
||||
}
|
||||
case "login": {
|
||||
const blockYaml: LoginBlockYAML = {
|
||||
...base,
|
||||
block_type: "login",
|
||||
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,
|
||||
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,
|
||||
|
||||
@@ -113,7 +113,8 @@ export type WorkflowBlock =
|
||||
| ValidationBlock
|
||||
| ActionBlock
|
||||
| NavigationBlock
|
||||
| ExtractionBlock;
|
||||
| ExtractionBlock
|
||||
| LoginBlock;
|
||||
|
||||
export const WorkflowBlockType = {
|
||||
Task: "task",
|
||||
@@ -128,6 +129,7 @@ export const WorkflowBlockType = {
|
||||
Action: "action",
|
||||
Navigation: "navigation",
|
||||
Extraction: "extraction",
|
||||
Login: "login",
|
||||
} as const;
|
||||
|
||||
export type WorkflowBlockType =
|
||||
@@ -259,6 +261,20 @@ export type ExtractionBlock = WorkflowBlockBase & {
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type LoginBlock = WorkflowBlockBase & {
|
||||
block_type: "login";
|
||||
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>;
|
||||
totp_verification_url?: string | null;
|
||||
totp_identifier?: string | null;
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowDefinition = {
|
||||
parameters: Array<Parameter>;
|
||||
blocks: Array<WorkflowBlock>;
|
||||
|
||||
@@ -79,6 +79,7 @@ const BlockTypes = {
|
||||
ACTION: "action",
|
||||
NAVIGATION: "navigation",
|
||||
EXTRACTION: "extraction",
|
||||
LOGIN: "login",
|
||||
} as const;
|
||||
|
||||
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
||||
@@ -95,7 +96,8 @@ export type BlockYAML =
|
||||
| ValidationBlockYAML
|
||||
| ActionBlockYAML
|
||||
| NavigationBlockYAML
|
||||
| ExtractionBlockYAML;
|
||||
| ExtractionBlockYAML
|
||||
| LoginBlockYAML;
|
||||
|
||||
export type BlockYAMLBase = {
|
||||
block_type: BlockType;
|
||||
@@ -172,6 +174,20 @@ export type ExtractionBlockYAML = BlockYAMLBase & {
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type LoginBlockYAML = BlockYAMLBase & {
|
||||
block_type: "login";
|
||||
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;
|
||||
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