Task block changes (#989)

This commit is contained in:
Shuchang Zheng
2024-10-16 12:45:06 -07:00
committed by GitHub
parent a988fe9410
commit a239030830
3 changed files with 354 additions and 448 deletions

View File

@@ -27,7 +27,6 @@ import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch";
import { TaskNodeParametersPanel } from "./TaskNodeParametersPanel"; import { TaskNodeParametersPanel } from "./TaskNodeParametersPanel";
import { import {
dataSchemaExampleValue, dataSchemaExampleValue,
@@ -35,17 +34,11 @@ import {
fieldPlaceholders, fieldPlaceholders,
helpTooltipContent, helpTooltipContent,
type TaskNode, type TaskNode,
type TaskNodeDisplayMode,
} from "./types"; } from "./types";
import { useParams } from "react-router-dom"; import { Separator } from "@/components/ui/separator";
function getLocalStorageKey(workflowPermanentId: string, label: string) {
return `skyvern-task-block-${workflowPermanentId}-${label}`;
}
function TaskNode({ id, data }: NodeProps<TaskNode>) { function TaskNode({ id, data }: NodeProps<TaskNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const { workflowPermanentId } = useParams();
const { editable } = data; const { editable } = data;
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const nodes = useNodes<AppNode>(); const nodes = useNodes<AppNode>();
@@ -56,15 +49,6 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
initialValue: data.label, initialValue: data.label,
}); });
const [displayMode, setDisplayMode] = useState<TaskNodeDisplayMode>(
workflowPermanentId &&
localStorage.getItem(getLocalStorageKey(workflowPermanentId, label))
? (localStorage.getItem(
getLocalStorageKey(workflowPermanentId, label),
) as TaskNodeDisplayMode)
: "basic",
);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
url: data.url, url: data.url,
navigationGoal: data.navigationGoal, navigationGoal: data.navigationGoal,
@@ -74,6 +58,7 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
maxStepsOverride: data.maxStepsOverride, maxStepsOverride: data.maxStepsOverride,
allowDownloads: data.allowDownloads, allowDownloads: data.allowDownloads,
continueOnFailure: data.continueOnFailure, continueOnFailure: data.continueOnFailure,
cacheActions: data.cacheActions,
downloadSuffix: data.downloadSuffix, downloadSuffix: data.downloadSuffix,
errorCodeMapping: data.errorCodeMapping, errorCodeMapping: data.errorCodeMapping,
totpVerificationUrl: data.totpVerificationUrl, totpVerificationUrl: data.totpVerificationUrl,
@@ -88,58 +73,44 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
const basicContent = ( return (
<> <div>
<div className="space-y-2"> <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-2 rounded-lg bg-slate-elevation3 px-6 py-4">
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-xs text-slate-300">URL</Label> <div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<HelpTooltip content={helpTooltipContent["url"]} /> <ListBulletIcon className="h-6 w-6" />
</div> </div>
<AutoResizingTextarea <div className="flex flex-col gap-1">
value={inputs.url} <EditableNodeTitle
className="nopan text-xs" value={label}
name="url" editable={editable}
onChange={(event) => { onChange={setLabel}
if (!editable) { titleClassName="text-base"
return; inputClassName="text-base"
}
handleChange("url", event.target.value);
}}
placeholder={fieldPlaceholders["url"]}
/> />
<span className="text-xs text-slate-400">Task Block</span>
</div> </div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">Goal</Label>
<HelpTooltip content={helpTooltipContent["navigationGoal"]} />
</div> </div>
<AutoResizingTextarea <NodeActionMenu
onChange={(event) => { onDelete={() => {
if (!editable) { deleteNodeCallback(id);
return;
}
handleChange("navigationGoal", event.target.value);
}}
value={inputs.navigationGoal}
placeholder={fieldPlaceholders["navigationGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<TaskNodeParametersPanel
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}} }}
/> />
</div> </div>
</> <Accordion type="multiple" defaultValue={["content", "extraction"]}>
);
const advancedContent = (
<>
<Accordion type="multiple" defaultValue={["content"]}>
<AccordionItem value="content"> <AccordionItem value="content">
<AccordionTrigger>Content</AccordionTrigger> <AccordionTrigger>Content</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1"> <AccordionContent className="pl-[1.5rem] pr-1">
@@ -164,7 +135,9 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-xs text-slate-300">Goal</Label> <Label className="text-xs text-slate-300">Goal</Label>
<HelpTooltip content={helpTooltipContent["navigationGoal"]} /> <HelpTooltip
content={helpTooltipContent["navigationGoal"]}
/>
</div> </div>
<AutoResizingTextarea <AutoResizingTextarea
onChange={(event) => { onChange={(event) => {
@@ -257,8 +230,8 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
</div> </div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem value="limits"> <AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger>Limits</AccordionTrigger> <AccordionTrigger>Advanced Settings</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1 pt-1"> <AccordionContent className="pl-[1.5rem] pr-1 pt-1">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -313,65 +286,6 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
}} }}
/> />
</div> </div>
<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={helpTooltipContent["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">
Continue on Failure
</Label>
</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">
File Suffix
</Label>
<HelpTooltip content={helpTooltipContent["fileSuffix"]} />
</div>
<Input
type="text"
placeholder={fieldPlaceholders["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(event) => {
if (!editable) {
return;
}
handleChange("downloadSuffix", event.target.value);
}}
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex gap-2"> <div className="flex gap-2">
@@ -414,13 +328,90 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
</div> </div>
)} )}
</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={helpTooltipContent["continueOnFailure"]}
/>
</div> </div>
</AccordionContent> <div className="w-52">
</AccordionItem> <Switch
<AccordionItem value="totp"> checked={inputs.continueOnFailure}
<AccordionTrigger>Two-Factor Authentication</AccordionTrigger> onCheckedChange={(checked) => {
<AccordionContent className="pl-[1.5rem] pr-1"> if (!editable) {
<div className="space-y-4"> 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={helpTooltipContent["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={helpTooltipContent["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={helpTooltipContent["fileSuffix"]} />
</div>
<Input
type="text"
placeholder={fieldPlaceholders["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="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-xs text-slate-300"> <Label className="text-xs text-slate-300">
@@ -447,7 +438,9 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
<Label className="text-xs text-slate-300"> <Label className="text-xs text-slate-300">
2FA Identifier 2FA Identifier
</Label> </Label>
<HelpTooltip content={helpTooltipContent["totpIdentifier"]} /> <HelpTooltip
content={helpTooltipContent["totpIdentifier"]}
/>
</div> </div>
<AutoResizingTextarea <AutoResizingTextarea
onChange={(event) => { onChange={(event) => {
@@ -465,60 +458,6 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</>
);
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">
<div 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">
<ListBulletIcon className="h-6 w-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">Task Block</span>
</div>
</div>
<NodeActionMenu
onDelete={() => {
deleteNodeCallback(id);
}}
/>
</div>
<TaskNodeDisplayModeSwitch
value={displayMode}
onChange={(mode) => {
setDisplayMode(mode);
if (workflowPermanentId) {
localStorage.setItem(
getLocalStorageKey(workflowPermanentId, label),
mode,
);
}
}}
/>
{displayMode === "basic" && basicContent}
{displayMode === "advanced" && advancedContent}
</div> </div>
</div> </div>
); );

View File

@@ -1,36 +0,0 @@
import { cn } from "@/util/utils";
import { TaskNodeDisplayMode } from "./types";
type Props = {
value: TaskNodeDisplayMode;
onChange: (mode: TaskNodeDisplayMode) => void;
};
function TaskNodeDisplayModeSwitch({ value, onChange }: Props) {
return (
<div className="flex w-fit gap-1 rounded-sm border border-slate-700 p-2">
<div
className={cn("cursor-pointer rounded-sm p-2 hover:bg-slate-700", {
"bg-slate-700": value === "basic",
})}
onClick={() => {
onChange("basic");
}}
>
Basic
</div>
<div
className={cn("cursor-pointer rounded-sm p-2 hover:bg-slate-700", {
"bg-slate-700": value === "advanced",
})}
onClick={() => {
onChange("advanced");
}}
>
Advanced
</div>
</div>
);
}
export { TaskNodeDisplayModeSwitch };

View File

@@ -74,6 +74,9 @@ export const helpTooltipContent = {
"If you have an internal system for storing TOTP codes, link the endpoint here.", "If you have an internal system for storing TOTP codes, link the endpoint here.",
totpIdentifier: 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.", "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; } as const;
export const fieldPlaceholders = { export const fieldPlaceholders = {