make other script enabled blocks flip-to-script; add type-checked lis… (#3179)

This commit is contained in:
Jonathan Dobson
2025-08-13 15:17:04 -04:00
committed by GitHub
parent a22a3aedba
commit 399fd4ea74
10 changed files with 1667 additions and 1508 deletions

View File

@@ -1,3 +1,5 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@@ -23,6 +25,7 @@ import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { placeholders, helpTooltips } from "../../helpContent"; import { placeholders, helpTooltips } from "../../helpContent";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
@@ -31,6 +34,7 @@ import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"
import { RunEngineSelector } from "@/components/EngineSelector"; import { RunEngineSelector } from "@/components/EngineSelector";
import { ModelSelector } from "@/components/ModelSelector"; import { ModelSelector } from "@/components/ModelSelector";
import { useDebugStore } from "@/store/useDebugStore"; import { useDebugStore } from "@/store/useDebugStore";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { NodeHeader } from "../components/NodeHeader"; import { NodeHeader } from "../components/NodeHeader";
@@ -44,7 +48,10 @@ const navigationGoalPlaceholder = 'Input {{ name }} into "Name" field.';
function ActionNode({ id, data, type }: NodeProps<ActionNode>) { function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { editable, debuggable, label } = data; const { editable, debuggable, label } = data;
const script = blockScriptStore.scripts[label];
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
url: data.url, url: data.url,
navigationGoal: data.navigationGoal, navigationGoal: data.navigationGoal,
@@ -78,308 +85,319 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
}, [data.showCode]);
return ( return (
<div> <Flippable facing={facing} preserveFrontsideHeight={true}>
<Handle <div>
type="source" <Handle
position={Position.Bottom} type="source"
id="a" position={Position.Bottom}
className="opacity-0" id="a"
/> className="opacity-0"
<Handle />
type="target" <Handle
position={Position.Top} type="target"
id="b" position={Position.Top}
className="opacity-0" id="b"
/> className="opacity-0"
<div
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={inputs.totpIdentifier}
totpUrl={inputs.totpVerificationUrl}
type={type}
/> />
<div <div
className={cn("space-y-4", { className={cn(
"opacity-50": thisBlockIsPlaying, "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
})} {
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
> >
<div className="space-y-2"> <NodeHeader
<div className="flex justify-between"> blockLabel={label}
<div className="flex gap-2"> disabled={elideFromDebugging}
<Label className="text-xs text-slate-300">URL</Label> editable={editable}
<HelpTooltip content={urlTooltip} /> nodeId={id}
</div> totpIdentifier={inputs.totpIdentifier}
{isFirstWorkflowBlock ? ( totpUrl={inputs.totpVerificationUrl}
<div className="flex justify-end text-xs text-slate-400"> type={type}
Tip: Use the {"+"} button to add parameters! />
<div
className={cn("space-y-4", {
"opacity-50": thisBlockIsPlaying,
})}
>
<div className="space-y-2">
<div className="flex justify-between">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">URL</Label>
<HelpTooltip content={urlTooltip} />
</div> </div>
) : null} {isFirstWorkflowBlock ? (
</div> <div className="flex justify-end text-xs text-slate-400">
Tip: Use the {"+"} button to add parameters!
<WorkflowBlockInputTextarea
canWriteTitle={true}
nodeId={id}
onChange={(value) => {
handleChange("url", value);
}}
value={inputs.url}
placeholder={placeholders["action"]["url"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Action Instruction
</Label>
<HelpTooltip content={navigationGoalTooltip} />
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("navigationGoal", value);
}}
value={inputs.navigationGoal}
placeholder={navigationGoalPlaceholder}
className="nopan text-xs"
/>
</div>
<div className="rounded-md bg-slate-800 p-2">
<div className="space-y-1 text-xs text-slate-400">
Tip: While executing the action block, Skyvern will only take one
action.
</div>
</div>
</div>
<Separator />
<Accordion
className={cn({
"pointer-events-none opacity-50": thisBlockIsPlaying,
})}
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="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Engine
</Label>
</div> </div>
<RunEngineSelector ) : null}
value={inputs.engine} </div>
onChange={(value) => {
handleChange("engine", value); <WorkflowBlockInputTextarea
}} canWriteTitle={true}
className="nopan w-52 text-xs" nodeId={id}
/> onChange={(value) => {
</div> handleChange("url", value);
<div className="space-y-2"> }}
<div className="flex gap-4"> value={inputs.url}
<div className="flex gap-2"> placeholder={placeholders["action"]["url"]}
<Label className="text-xs font-normal text-slate-300"> className="nopan text-xs"
Error Messages />
</Label> </div>
<HelpTooltip <div className="space-y-2">
content={helpTooltips["action"]["errorCodeMapping"]} <div className="flex gap-2">
/> <Label className="text-xs text-slate-300">
</div> Action Instruction
<Checkbox </Label>
checked={inputs.errorCodeMapping !== "null"} <HelpTooltip content={navigationGoalTooltip} />
disabled={!editable} </div>
onCheckedChange={(checked) => { <WorkflowBlockInputTextarea
if (!editable) { nodeId={id}
return; onChange={(value) => {
} handleChange("navigationGoal", value);
handleChange( }}
"errorCodeMapping", value={inputs.navigationGoal}
checked placeholder={navigationGoalPlaceholder}
? JSON.stringify(errorMappingExampleValue, null, 2) className="nopan text-xs"
: "null", />
); </div>
<div className="rounded-md bg-slate-800 p-2">
<div className="space-y-1 text-xs text-slate-400">
Tip: While executing the action block, Skyvern will only take
one action.
</div>
</div>
</div>
<Separator />
<Accordion
className={cn({
"pointer-events-none opacity-50": thisBlockIsPlaying,
})}
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="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}} }}
/> />
</div> </div>
{inputs.errorCodeMapping !== "null" && ( <div className="flex items-center justify-between">
<div> <div className="flex gap-2">
<CodeEditor <Label className="text-xs font-normal text-slate-300">
language="json" Engine
value={inputs.errorCodeMapping} </Label>
onChange={(value) => { </div>
<RunEngineSelector
value={inputs.engine}
onChange={(value) => {
handleChange("engine", value);
}}
className="nopan w-52 text-xs"
/>
</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={helpTooltips["action"]["errorCodeMapping"]}
/>
</div>
<Checkbox
checked={inputs.errorCodeMapping !== "null"}
disabled={!editable}
onCheckedChange={(checked) => {
if (!editable) { if (!editable) {
return; return;
} }
handleChange("errorCodeMapping", value); handleChange(
"errorCodeMapping",
checked
? JSON.stringify(
errorMappingExampleValue,
null,
2,
)
: "null",
);
}} }}
className="nowheel nopan"
fontSize={8}
/> />
</div> </div>
)} {inputs.errorCodeMapping !== "null" && (
</div> <div>
<Separator /> <CodeEditor
<div className="flex items-center justify-between"> language="json"
<div className="flex gap-2"> value={inputs.errorCodeMapping}
<Label className="text-xs font-normal text-slate-300"> onChange={(value) => {
Continue on Failure if (!editable) {
</Label> return;
<HelpTooltip }
content={helpTooltips["action"]["continueOnFailure"]} handleChange("errorCodeMapping", value);
/> }}
className="nowheel nopan"
fontSize={8}
/>
</div>
)}
</div> </div>
<div className="w-52"> <Separator />
<Switch <div className="flex items-center justify-between">
checked={inputs.continueOnFailure} <div className="flex gap-2">
onCheckedChange={(checked) => { <Label className="text-xs font-normal text-slate-300">
if (!editable) { Continue on Failure
return; </Label>
} <HelpTooltip
handleChange("continueOnFailure", checked); content={helpTooltips["action"]["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={helpTooltips["action"]["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={helpTooltips["action"]["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={helpTooltips["action"]["fileSuffix"]}
/>
</div>
<WorkflowBlockInput
nodeId={id}
type="text"
placeholder={placeholders["action"]["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(value) => {
handleChange("downloadSuffix", value);
}} }}
/> />
</div> </div>
</div> <Separator />
<div className="flex items-center justify-between"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300"> <Label className="text-xs text-slate-300">
Cache Actions 2FA Identifier
</Label> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["action"]["cacheActions"]} content={helpTooltips["action"]["totpIdentifier"]}
/> />
</div> </div>
<div className="w-52"> <WorkflowBlockInputTextarea
<Switch nodeId={id}
checked={inputs.cacheActions} onChange={(value) => {
onCheckedChange={(checked) => { handleChange("totpIdentifier", value);
if (!editable) {
return;
}
handleChange("cacheActions", checked);
}} }}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["action"]["totpIdentifier"]}
className="nopan text-xs"
/> />
</div> </div>
</div> <div className="space-y-2">
<Separator /> <div className="flex gap-2">
<div className="flex items-center justify-between"> <Label className="text-xs text-slate-300">
<div className="flex gap-2"> 2FA Verification URL
<Label className="text-xs font-normal text-slate-300"> </Label>
Complete on Download <HelpTooltip
</Label> content={helpTooltips["task"]["totpVerificationUrl"]}
<HelpTooltip />
content={helpTooltips["action"]["completeOnDownload"]} </div>
/> <WorkflowBlockInputTextarea
</div> nodeId={id}
<div className="w-52"> onChange={(value) => {
<Switch handleChange("totpVerificationUrl", value);
checked={inputs.allowDownloads}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("allowDownloads", checked);
}} }}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["task"]["totpVerificationUrl"]}
className="nopan text-xs"
/> />
</div> </div>
</div> </div>
<div className="flex items-center justify-between"> </AccordionContent>
<div className="flex gap-2"> </AccordionItem>
<Label className="text-xs font-normal text-slate-300"> </Accordion>
File Suffix </div>
</Label>
<HelpTooltip
content={helpTooltips["action"]["fileSuffix"]}
/>
</div>
<WorkflowBlockInput
nodeId={id}
type="text"
placeholder={placeholders["action"]["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(value) => {
handleChange("downloadSuffix", value);
}}
/>
</div>
<Separator />
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Identifier
</Label>
<HelpTooltip
content={helpTooltips["action"]["totpIdentifier"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpIdentifier", value);
}}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["action"]["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Verification URL
</Label>
<HelpTooltip
content={helpTooltips["task"]["totpVerificationUrl"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpVerificationUrl", value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["task"]["totpVerificationUrl"]}
className="nopan text-xs"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div> </div>
</div> <BlockCodeEditor blockLabel={label} blockType={type} script={script} />
</Flippable>
); );
} }

View File

@@ -1,3 +1,5 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { HelpTooltip } from "@/components/HelpTooltip"; import { HelpTooltip } from "@/components/HelpTooltip";
import { import {
Accordion, Accordion,
@@ -22,6 +24,7 @@ import { dataSchemaExampleValue } from "../types";
import type { ExtractionNode } from "./types"; import type { ExtractionNode } from "./types";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { helpTooltips, placeholders } from "../../helpContent"; import { helpTooltips, placeholders } from "../../helpContent";
import { AppNode } from ".."; import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
@@ -31,13 +34,17 @@ import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"
import { RunEngineSelector } from "@/components/EngineSelector"; import { RunEngineSelector } from "@/components/EngineSelector";
import { ModelSelector } from "@/components/ModelSelector"; import { ModelSelector } from "@/components/ModelSelector";
import { useDebugStore } from "@/store/useDebugStore"; import { useDebugStore } from "@/store/useDebugStore";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader"; import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) { function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { debuggable, editable, label } = data; const { debuggable, editable, label } = data;
const script = blockScriptStore.scripts[label];
const debugStore = useDebugStore(); const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable; const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
@@ -67,193 +74,204 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
return ( useEffect(() => {
<div> setFacing(data.showCode ? "back" : "front");
<Handle }, [data.showCode]);
type="source"
position={Position.Bottom}
id="a"
className="opacity-0"
/>
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={null}
totpUrl={null}
type={type}
/>
<div className="space-y-2">
<div className="flex justify-between">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Data Extraction Goal
</Label>
<HelpTooltip
content={helpTooltips["extraction"]["dataExtractionGoal"]}
/>
</div>
{isFirstWorkflowBlock ? (
<div className="flex justify-end text-xs text-slate-400">
Tip: Use the {"+"} button to add parameters!
</div>
) : null}
</div>
<WorkflowBlockInputTextarea return (
nodeId={id} <Flippable facing={facing} preserveFrontsideHeight={true}>
onChange={(value) => { <div>
if (!editable) { <Handle
return; type="source"
} position={Position.Bottom}
handleChange("dataExtractionGoal", value); id="a"
}} className="opacity-0"
value={inputs.dataExtractionGoal}
placeholder={placeholders["extraction"]["dataExtractionGoal"]}
className="nopan text-xs"
/>
</div>
<WorkflowDataSchemaInputGroup
value={inputs.dataSchema}
onChange={(value) => {
handleChange("dataSchema", value);
}}
exampleValue={dataSchemaExampleValue}
suggestionContext={{
data_extraction_goal: inputs.dataExtractionGoal,
current_schema: inputs.dataSchema,
}}
/> />
<Separator /> <Handle
<Accordion type="single" collapsible> type="target"
<AccordionItem value="advanced" className="border-b-0"> position={Position.Top}
<AccordionTrigger className="py-0"> id="b"
Advanced Settings className="opacity-0"
</AccordionTrigger> />
<AccordionContent className="pl-6 pr-1 pt-1"> <div
<div className="space-y-4"> className={cn(
<div className="space-y-2"> "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
<ModelSelector {
className="nopan w-52 text-xs" "pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
value={inputs.model} thisBlockIsPlaying,
onChange={(value) => { },
handleChange("model", value); )}
}} >
/> <NodeHeader
<ParametersMultiSelect blockLabel={label}
availableOutputParameters={outputParameterKeys} disabled={elideFromDebugging}
parameters={data.parameterKeys} editable={editable}
onParametersChange={(parameterKeys) => { nodeId={id}
updateNodeData(id, { parameterKeys }); totpIdentifier={null}
}} totpUrl={null}
/> type={type}
</div> />
<div className="flex items-center justify-between"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex justify-between">
<Label className="text-xs font-normal text-slate-300"> <div className="flex gap-2">
Engine <Label className="text-xs text-slate-300">
</Label> Data Extraction Goal
</div> </Label>
<RunEngineSelector <HelpTooltip
value={inputs.engine} content={helpTooltips["extraction"]["dataExtractionGoal"]}
onChange={(value) => { />
handleChange("engine", value);
}}
className="nopan w-52 text-xs"
/>
</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={helpTooltips["extraction"]["maxStepsOverride"]}
/>
</div>
<Input
type="number"
placeholder={placeholders["extraction"]["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>
<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={helpTooltips["extraction"]["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={helpTooltips["extraction"]["cacheActions"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.cacheActions}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("cacheActions", checked);
}}
/>
</div>
</div>
</div> </div>
</AccordionContent> {isFirstWorkflowBlock ? (
</AccordionItem> <div className="flex justify-end text-xs text-slate-400">
</Accordion> Tip: Use the {"+"} button to add parameters!
</div>
) : null}
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
if (!editable) {
return;
}
handleChange("dataExtractionGoal", value);
}}
value={inputs.dataExtractionGoal}
placeholder={placeholders["extraction"]["dataExtractionGoal"]}
className="nopan text-xs"
/>
</div>
<WorkflowDataSchemaInputGroup
value={inputs.dataSchema}
onChange={(value) => {
handleChange("dataSchema", value);
}}
exampleValue={dataSchemaExampleValue}
suggestionContext={{
data_extraction_goal: inputs.dataExtractionGoal,
current_schema: inputs.dataSchema,
}}
/>
<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="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Engine
</Label>
</div>
<RunEngineSelector
value={inputs.engine}
onChange={(value) => {
handleChange("engine", value);
}}
className="nopan w-52 text-xs"
/>
</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={helpTooltips["extraction"]["maxStepsOverride"]}
/>
</div>
<Input
type="number"
placeholder={
placeholders["extraction"]["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>
<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={
helpTooltips["extraction"]["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={helpTooltips["extraction"]["cacheActions"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.cacheActions}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("cacheActions", checked);
}}
/>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div> </div>
</div> <BlockCodeEditor blockLabel={label} blockType={type} script={script} />
</Flippable>
); );
} }

View File

@@ -1,3 +1,5 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { HelpTooltip } from "@/components/HelpTooltip"; import { HelpTooltip } from "@/components/HelpTooltip";
import { import {
Accordion, Accordion,
@@ -11,7 +13,9 @@ import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { import {
Handle, Handle,
NodeProps, NodeProps,
@@ -44,7 +48,10 @@ const navigationGoalPlaceholder = "Tell Skyvern which file to download.";
function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) { function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { debuggable, editable, label } = data; const { debuggable, editable, label } = data;
const script = blockScriptStore.scripts[label];
const debugStore = useDebugStore(); const debugStore = useDebugStore();
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying = const thisBlockIsPlaying =
@@ -75,282 +82,297 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
}, [data.showCode]);
return ( return (
<div> <Flippable facing={facing} preserveFrontsideHeight={true}>
<Handle <div>
type="source" <Handle
position={Position.Bottom} type="source"
id="a" position={Position.Bottom}
className="opacity-0" id="a"
/> className="opacity-0"
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={inputs.totpIdentifier}
totpUrl={inputs.totpVerificationUrl}
type="file_download" // sic: the naming for this block is not consistent
/> />
<div className="space-y-4"> <Handle
<div className="space-y-2"> type="target"
<div className="flex justify-between"> position={Position.Top}
<div className="flex gap-2"> id="b"
<Label className="text-xs text-slate-300">URL</Label> className="opacity-0"
<HelpTooltip content={urlTooltip} /> />
</div> <div
{isFirstWorkflowBlock ? ( className={cn(
<div className="flex justify-end text-xs text-slate-400"> "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
Tip: Use the {"+"} button to add parameters! {
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={inputs.totpIdentifier}
totpUrl={inputs.totpVerificationUrl}
type="file_download" // sic: the naming for this block is not consistent
/>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">URL</Label>
<HelpTooltip content={urlTooltip} />
</div> </div>
) : null} {isFirstWorkflowBlock ? (
</div> <div className="flex justify-end text-xs text-slate-400">
<WorkflowBlockInputTextarea Tip: Use the {"+"} button to add parameters!
canWriteTitle={true}
nodeId={id}
onChange={(value) => {
handleChange("url", 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>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("navigationGoal", 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="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Engine
</Label>
</div> </div>
<RunEngineSelector ) : null}
value={inputs.engine} </div>
onChange={(value) => { <WorkflowBlockInputTextarea
handleChange("engine", value); canWriteTitle={true}
}} nodeId={id}
className="nopan w-52 text-xs" onChange={(value) => {
/> handleChange("url", value);
</div> }}
<div className="flex items-center justify-between"> value={inputs.url}
<div className="flex gap-2"> placeholder={urlPlaceholder}
<Label className="text-xs font-normal text-slate-300"> className="nopan text-xs"
Max Steps Override />
</Label> </div>
<HelpTooltip <div className="space-y-2">
content={helpTooltips["download"]["maxStepsOverride"]} <div className="flex gap-2">
<Label className="text-xs text-slate-300">Download Goal</Label>
<HelpTooltip content={navigationGoalTooltip} />
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("navigationGoal", 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="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/> />
</div> </div>
<Input <div className="flex items-center justify-between">
type="number"
placeholder={placeholders["download"]["maxStepsOverride"]}
className="nopan w-52 text-xs"
min="0"
value={inputs.maxStepsOverride ?? ""}
onChange={(event) => {
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"> <div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300"> <Label className="text-xs font-normal text-slate-300">
Error Messages Engine
</Label>
</div>
<RunEngineSelector
value={inputs.engine}
onChange={(value) => {
handleChange("engine", value);
}}
className="nopan w-52 text-xs"
/>
</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> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["download"]["errorCodeMapping"]} content={helpTooltips["download"]["maxStepsOverride"]}
/> />
</div> </div>
<Checkbox <Input
checked={inputs.errorCodeMapping !== "null"} type="number"
disabled={!editable} placeholder={placeholders["download"]["maxStepsOverride"]}
onCheckedChange={(checked) => { className="nopan w-52 text-xs"
handleChange( min="0"
"errorCodeMapping", value={inputs.maxStepsOverride ?? ""}
checked onChange={(event) => {
? JSON.stringify(errorMappingExampleValue, null, 2) const value =
: "null", event.target.value === ""
); ? null
: Number(event.target.value);
handleChange("maxStepsOverride", value);
}} }}
/> />
</div> </div>
{inputs.errorCodeMapping !== "null" && ( <div className="space-y-2">
<div> <div className="flex gap-4">
<CodeEditor <div className="flex gap-2">
language="json" <Label className="text-xs font-normal text-slate-300">
value={inputs.errorCodeMapping} Error Messages
onChange={(value) => { </Label>
handleChange("errorCodeMapping", value); <HelpTooltip
content={helpTooltips["download"]["errorCodeMapping"]}
/>
</div>
<Checkbox
checked={inputs.errorCodeMapping !== "null"}
disabled={!editable}
onCheckedChange={(checked) => {
handleChange(
"errorCodeMapping",
checked
? JSON.stringify(
errorMappingExampleValue,
null,
2,
)
: "null",
);
}} }}
className="nowheel nopan"
fontSize={8}
/> />
</div> </div>
)} {inputs.errorCodeMapping !== "null" && (
</div> <div>
<Separator /> <CodeEditor
<div className="flex items-center justify-between"> language="json"
<div className="flex gap-2"> value={inputs.errorCodeMapping}
<Label className="text-xs font-normal text-slate-300"> onChange={(value) => {
Continue on Failure handleChange("errorCodeMapping", value);
</Label> }}
<HelpTooltip className="nowheel nopan"
content={helpTooltips["download"]["continueOnFailure"]} fontSize={8}
/> />
</div>
)}
</div> </div>
<div className="w-52"> <Separator />
<Switch <div className="flex items-center justify-between">
checked={inputs.continueOnFailure} <div className="flex gap-2">
onCheckedChange={(checked) => { <Label className="text-xs font-normal text-slate-300">
handleChange("continueOnFailure", checked); Continue on Failure
</Label>
<HelpTooltip
content={helpTooltips["download"]["continueOnFailure"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.continueOnFailure}
onCheckedChange={(checked) => {
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={helpTooltips["download"]["cacheActions"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.cacheActions}
onCheckedChange={(checked) => {
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={helpTooltips["download"]["fileSuffix"]}
/>
</div>
<Input
type="text"
placeholder={placeholders["download"]["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(event) => {
handleChange("downloadSuffix", event.target.value);
}} }}
/> />
</div> </div>
</div> <Separator />
<div className="flex items-center justify-between"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300"> <Label className="text-xs text-slate-300">
Cache Actions 2FA Identifier
</Label> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["download"]["cacheActions"]} content={helpTooltips["download"]["totpIdentifier"]}
/> />
</div> </div>
<div className="w-52"> <WorkflowBlockInputTextarea
<Switch nodeId={id}
checked={inputs.cacheActions} onChange={(value) => {
onCheckedChange={(checked) => { handleChange("totpIdentifier", value);
handleChange("cacheActions", checked);
}} }}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["download"]["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Verification URL
</Label>
<HelpTooltip
content={helpTooltips["task"]["totpVerificationUrl"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpVerificationUrl", value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["task"]["totpVerificationUrl"]}
className="nopan text-xs"
/> />
</div> </div>
</div> </div>
<Separator /> </AccordionContent>
<div className="flex items-center justify-between"> </AccordionItem>
<div className="flex gap-2"> </Accordion>
<Label className="text-xs font-normal text-slate-300"> </div>
File Suffix
</Label>
<HelpTooltip
content={helpTooltips["download"]["fileSuffix"]}
/>
</div>
<Input
type="text"
placeholder={placeholders["download"]["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(event) => {
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 Identifier
</Label>
<HelpTooltip
content={helpTooltips["download"]["totpIdentifier"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpIdentifier", value);
}}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["download"]["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Verification URL
</Label>
<HelpTooltip
content={helpTooltips["task"]["totpVerificationUrl"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpVerificationUrl", value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["task"]["totpVerificationUrl"]}
className="nopan text-xs"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div> </div>
</div> <BlockCodeEditor
blockLabel={label}
blockType="file_download" // sic: naming is not consistent
script={script}
/>
</Flippable>
); );
} }

View File

@@ -1,3 +1,5 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { HelpTooltip } from "@/components/HelpTooltip"; import { HelpTooltip } from "@/components/HelpTooltip";
import { import {
Accordion, Accordion,
@@ -11,7 +13,9 @@ import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { import {
Handle, Handle,
NodeProps, NodeProps,
@@ -38,7 +42,10 @@ import { useParams } from "react-router-dom";
function LoginNode({ id, data, type }: NodeProps<LoginNode>) { function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { debuggable, editable, label } = data; const { debuggable, editable, label } = data;
const script = blockScriptStore.scripts[label];
const debugStore = useDebugStore(); const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable; const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
@@ -72,291 +79,304 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
return ( useEffect(() => {
<div> setFacing(data.showCode ? "back" : "front");
<Handle }, [data.showCode]);
type="source"
position={Position.Bottom}
id="a"
className="opacity-0"
/>
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={inputs.totpIdentifier}
totpUrl={inputs.totpVerificationUrl}
type={type}
/>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">URL</Label>
<HelpTooltip content={helpTooltips["login"]["url"]} />
</div>
{isFirstWorkflowBlock ? (
<div className="flex justify-end text-xs text-slate-400">
Tip: Use the {"+"} button to add parameters!
</div>
) : null}
</div>
<WorkflowBlockInputTextarea return (
canWriteTitle={true} <Flippable facing={facing} preserveFrontsideHeight={true}>
nodeId={id} <div>
onChange={(value) => { <Handle
handleChange("url", value); type="source"
}} position={Position.Bottom}
value={inputs.url} id="a"
placeholder={placeholders["login"]["url"]} className="opacity-0"
className="nopan text-xs" />
/> <Handle
</div> type="target"
<div className="space-y-2"> position={Position.Top}
<div className="flex gap-2"> id="b"
<Label className="text-xs text-slate-300">Login Goal</Label> className="opacity-0"
<HelpTooltip content={helpTooltips["login"]["navigationGoal"]} /> />
</div> <div
<WorkflowBlockInputTextarea className={cn(
nodeId={id} "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
onChange={(value) => { {
handleChange("navigationGoal", value); "pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
}} thisBlockIsPlaying,
value={inputs.navigationGoal} },
placeholder={placeholders["login"]["navigationGoal"]} )}
className="nopan text-xs" >
/> <NodeHeader
</div> blockLabel={label}
<div className="space-y-2"> disabled={elideFromDebugging}
<Label className="text-xs text-slate-300">Credential</Label> editable={editable}
<LoginBlockCredentialSelector nodeId={id}
nodeId={id} totpIdentifier={inputs.totpIdentifier}
value={ totpUrl={inputs.totpVerificationUrl}
data.parameterKeys.length > 0 type={type}
? data.parameterKeys[0] />
: undefined <div className="space-y-4">
} <div className="space-y-2">
onChange={(value) => { <div className="flex justify-between">
if (!editable) { <div className="flex gap-2">
return; <Label className="text-xs text-slate-300">URL</Label>
} <HelpTooltip content={helpTooltips["login"]["url"]} />
updateNodeData(id, { parameterKeys: [value] });
}}
/>
</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="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div> </div>
<div className="space-y-2"> {isFirstWorkflowBlock ? (
<Label className="text-xs text-slate-300"> <div className="flex justify-end text-xs text-slate-400">
Complete if... Tip: Use the {"+"} button to add parameters!
</Label>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("completeCriterion", value);
}}
value={inputs.completeCriterion}
className="nopan text-xs"
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Engine
</Label>
</div> </div>
<RunEngineSelector ) : null}
value={inputs.engine} </div>
onChange={(value) => {
handleChange("engine", value); <WorkflowBlockInputTextarea
}} canWriteTitle={true}
className="nopan w-52 text-xs" nodeId={id}
/> onChange={(value) => {
</div> handleChange("url", value);
<div className="flex items-center justify-between"> }}
<div className="flex gap-2"> value={inputs.url}
<Label className="text-xs font-normal text-slate-300"> placeholder={placeholders["login"]["url"]}
Max Steps Override className="nopan text-xs"
</Label> />
<HelpTooltip </div>
content={helpTooltips["login"]["maxStepsOverride"]} <div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">Login Goal</Label>
<HelpTooltip
content={helpTooltips["login"]["navigationGoal"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("navigationGoal", value);
}}
value={inputs.navigationGoal}
placeholder={placeholders["login"]["navigationGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-slate-300">Credential</Label>
<LoginBlockCredentialSelector
nodeId={id}
value={
data.parameterKeys.length > 0
? data.parameterKeys[0]
: undefined
}
onChange={(value) => {
if (!editable) {
return;
}
updateNodeData(id, { parameterKeys: [value] });
}}
/>
</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="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/> />
</div> </div>
<Input <div className="space-y-2">
type="number" <Label className="text-xs text-slate-300">
placeholder={placeholders["login"]["maxStepsOverride"]} Complete if...
className="nopan w-52 text-xs" </Label>
min="0" <WorkflowBlockInputTextarea
value={inputs.maxStepsOverride ?? ""} nodeId={id}
onChange={(event) => { onChange={(value) => {
const value = handleChange("completeCriterion", value);
event.target.value === "" }}
? null value={inputs.completeCriterion}
: Number(event.target.value); className="nopan text-xs"
handleChange("maxStepsOverride", value); />
}} </div>
/> <Separator />
</div> <div className="flex items-center justify-between">
<div className="space-y-2">
<div className="flex gap-4">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300"> <Label className="text-xs font-normal text-slate-300">
Error Messages Engine
</Label>
</div>
<RunEngineSelector
value={inputs.engine}
onChange={(value) => {
handleChange("engine", value);
}}
className="nopan w-52 text-xs"
/>
</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> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["login"]["errorCodeMapping"]} content={helpTooltips["login"]["maxStepsOverride"]}
/> />
</div> </div>
<Checkbox <Input
checked={inputs.errorCodeMapping !== "null"} type="number"
disabled={!editable} placeholder={placeholders["login"]["maxStepsOverride"]}
onCheckedChange={(checked) => { className="nopan w-52 text-xs"
handleChange( min="0"
"errorCodeMapping", value={inputs.maxStepsOverride ?? ""}
checked onChange={(event) => {
? JSON.stringify(errorMappingExampleValue, null, 2) const value =
: "null", event.target.value === ""
); ? null
: Number(event.target.value);
handleChange("maxStepsOverride", value);
}} }}
/> />
</div> </div>
{inputs.errorCodeMapping !== "null" && ( <div className="space-y-2">
<div> <div className="flex gap-4">
<CodeEditor <div className="flex gap-2">
language="json" <Label className="text-xs font-normal text-slate-300">
value={inputs.errorCodeMapping} Error Messages
onChange={(value) => { </Label>
handleChange("errorCodeMapping", value); <HelpTooltip
content={helpTooltips["login"]["errorCodeMapping"]}
/>
</div>
<Checkbox
checked={inputs.errorCodeMapping !== "null"}
disabled={!editable}
onCheckedChange={(checked) => {
handleChange(
"errorCodeMapping",
checked
? JSON.stringify(
errorMappingExampleValue,
null,
2,
)
: "null",
);
}} }}
className="nowheel nopan"
fontSize={8}
/> />
</div> </div>
)} {inputs.errorCodeMapping !== "null" && (
</div> <div>
<Separator /> <CodeEditor
<div className="flex items-center justify-between"> language="json"
<div className="flex gap-2"> value={inputs.errorCodeMapping}
<Label className="text-xs font-normal text-slate-300"> onChange={(value) => {
Continue on Failure handleChange("errorCodeMapping", value);
</Label> }}
<HelpTooltip className="nowheel nopan"
content={helpTooltips["login"]["continueOnFailure"]} fontSize={8}
/> />
</div>
)}
</div> </div>
<div className="w-52"> <Separator />
<Switch <div className="flex items-center justify-between">
checked={inputs.continueOnFailure} <div className="flex gap-2">
onCheckedChange={(checked) => { <Label className="text-xs font-normal text-slate-300">
handleChange("continueOnFailure", checked); Continue on Failure
</Label>
<HelpTooltip
content={helpTooltips["login"]["continueOnFailure"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.continueOnFailure}
onCheckedChange={(checked) => {
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={helpTooltips["login"]["cacheActions"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.cacheActions}
onCheckedChange={(checked) => {
handleChange("cacheActions", checked);
}}
/>
</div>
</div>
<Separator />
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Identifier
</Label>
<HelpTooltip
content={helpTooltips["login"]["totpIdentifier"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpIdentifier", value);
}} }}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["login"]["totpIdentifier"]}
className="nopan text-xs"
/> />
</div> </div>
</div> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex gap-2">
<div className="flex gap-2"> <Label className="text-xs text-slate-300">
<Label className="text-xs font-normal text-slate-300"> 2FA Verification URL
Cache Actions </Label>
</Label> <HelpTooltip
<HelpTooltip content={helpTooltips["login"]["totpVerificationUrl"]}
content={helpTooltips["login"]["cacheActions"]} />
/> </div>
</div> <WorkflowBlockInputTextarea
<div className="w-52"> nodeId={id}
<Switch onChange={(value) => {
checked={inputs.cacheActions} handleChange("totpVerificationUrl", value);
onCheckedChange={(checked) => {
handleChange("cacheActions", checked);
}} }}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["login"]["totpVerificationUrl"]}
className="nopan text-xs"
/> />
</div> </div>
</div> </div>
<Separator /> </AccordionContent>
<div className="space-y-2"> </AccordionItem>
<div className="flex gap-2"> </Accordion>
<Label className="text-xs text-slate-300"> </div>
2FA Identifier
</Label>
<HelpTooltip
content={helpTooltips["login"]["totpIdentifier"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpIdentifier", value);
}}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["login"]["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Verification URL
</Label>
<HelpTooltip
content={helpTooltips["login"]["totpVerificationUrl"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpVerificationUrl", value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["login"]["totpVerificationUrl"]}
className="nopan text-xs"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div> </div>
</div> <BlockCodeEditor blockLabel={label} blockType={type} script={script} />
</Flippable>
); );
} }

View File

@@ -10,11 +10,16 @@ import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { OrgWalled } from "@/components/Orgwalled"; import { OrgWalled } from "@/components/Orgwalled";
type Props = { type Props = {
isScriptable?: boolean;
onDelete: () => void; onDelete: () => void;
onShowScript?: () => void; onShowScript?: () => void;
}; };
function NodeActionMenu({ onDelete, onShowScript }: Props) { function NodeActionMenu({
isScriptable = false,
onDelete,
onShowScript,
}: Props) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -30,17 +35,19 @@ function NodeActionMenu({ onDelete, onShowScript }: Props) {
> >
Delete Block Delete Block
</DropdownMenuItem> </DropdownMenuItem>
<OrgWalled className="p-0"> {isScriptable && (
{onShowScript && ( <OrgWalled className="p-0">
<DropdownMenuItem {onShowScript && (
onSelect={() => { <DropdownMenuItem
onShowScript(); onSelect={() => {
}} onShowScript();
> }}
Show Script >
</DropdownMenuItem> Show Script
)} </DropdownMenuItem>
</OrgWalled> )}
</OrgWalled>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View File

@@ -1,3 +1,5 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { HelpTooltip } from "@/components/HelpTooltip"; import { HelpTooltip } from "@/components/HelpTooltip";
import { import {
Accordion, Accordion,
@@ -12,7 +14,9 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { import {
Handle, Handle,
NodeProps, NodeProps,
@@ -39,7 +43,10 @@ import { useParams } from "react-router-dom";
function TaskNode({ id, data, type }: NodeProps<TaskNode>) { function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { debuggable, editable, label } = data; const { debuggable, editable, label } = data;
const script = blockScriptStore.scripts[label];
const debugStore = useDebugStore(); const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable; const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
@@ -78,377 +85,390 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
}, [data.showCode]);
return ( return (
<div> <Flippable facing={facing} preserveFrontsideHeight={true}>
<Handle <div>
type="source" <Handle
position={Position.Bottom} type="source"
id="a" position={Position.Bottom}
className="opacity-0" id="a"
/> className="opacity-0"
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={inputs.totpIdentifier}
totpUrl={inputs.totpVerificationUrl}
type={type}
/> />
<Accordion type="multiple" defaultValue={["content", "extraction"]}> <Handle
<AccordionItem value="content"> type="target"
<AccordionTrigger>Content</AccordionTrigger> position={Position.Top}
<AccordionContent className="pl-[1.5rem] pr-1"> id="b"
<div className="space-y-4"> className="opacity-0"
<div className="space-y-2"> />
<div className="flex justify-between"> <div
<div className="flex gap-2"> className={cn(
<Label className="text-xs text-slate-300">URL</Label> "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
<HelpTooltip content={helpTooltips["task"]["url"]} /> {
</div> "pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
{isFirstWorkflowBlock ? ( thisBlockIsPlaying,
<div className="flex justify-end text-xs text-slate-400"> },
Tip: Use the {"+"} button to add parameters! )}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={inputs.totpIdentifier}
totpUrl={inputs.totpVerificationUrl}
type={type}
/>
<Accordion type="multiple" defaultValue={["content", "extraction"]}>
<AccordionItem value="content">
<AccordionTrigger>Content</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">URL</Label>
<HelpTooltip content={helpTooltips["task"]["url"]} />
</div> </div>
) : null} {isFirstWorkflowBlock ? (
</div> <div className="flex justify-end text-xs text-slate-400">
<WorkflowBlockInputTextarea Tip: Use the {"+"} button to add parameters!
canWriteTitle={true} </div>
nodeId={id} ) : null}
onChange={(value) => { </div>
handleChange("url", value); <WorkflowBlockInputTextarea
}} canWriteTitle={true}
value={inputs.url} nodeId={id}
placeholder={placeholders["task"]["url"]} onChange={(value) => {
className="nopan text-xs" handleChange("url", value);
/> }}
</div> value={inputs.url}
<div className="space-y-2"> placeholder={placeholders["task"]["url"]}
<div className="flex gap-2"> className="nopan text-xs"
<Label className="text-xs text-slate-300">Goal</Label>
<HelpTooltip
content={helpTooltips["task"]["navigationGoal"]}
/> />
</div> </div>
<WorkflowBlockInputTextarea <div className="space-y-2">
nodeId={id}
onChange={(value) => {
handleChange("navigationGoal", value);
}}
value={inputs.navigationGoal}
placeholder={placeholders["task"]["navigationGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="extraction">
<AccordionTrigger>Extraction</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Data Extraction Goal
</Label>
<HelpTooltip
content={helpTooltips["task"]["dataExtractionGoal"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("dataExtractionGoal", value);
}}
value={inputs.dataExtractionGoal}
placeholder={placeholders["task"]["dataExtractionGoal"]}
className="nopan text-xs"
/>
</div>
<WorkflowDataSchemaInputGroup
exampleValue={dataSchemaExampleValue}
onChange={(value) => {
handleChange("dataSchema", value);
}}
value={inputs.dataSchema}
suggestionContext={{
data_extraction_goal: inputs.dataExtractionGoal,
current_schema: inputs.dataSchema,
navigation_goal: inputs.navigationGoal,
}}
/>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger>Advanced Settings</AccordionTrigger>
<AccordionContent className="pl-6 pr-1 pt-1">
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs text-slate-300">
Complete if...
</Label>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("completeCriterion", value);
}}
value={inputs.completeCriterion}
className="nopan text-xs"
/>
</div>
<Separator />
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Engine
</Label>
</div>
<RunEngineSelector
value={inputs.engine}
onChange={(value) => {
handleChange("engine", value);
}}
className="nopan w-52 text-xs"
/>
</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={helpTooltips["task"]["maxStepsOverride"]}
/>
</div>
<Input
type="number"
placeholder={placeholders["task"]["maxStepsOverride"]}
className="nopan w-52 text-xs"
min="0"
value={inputs.maxStepsOverride ?? ""}
onChange={(event) => {
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"> <div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300"> <Label className="text-xs text-slate-300">Goal</Label>
Error Messages <HelpTooltip
content={helpTooltips["task"]["navigationGoal"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("navigationGoal", value);
}}
value={inputs.navigationGoal}
placeholder={placeholders["task"]["navigationGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="extraction">
<AccordionTrigger>Extraction</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Data Extraction Goal
</Label> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["task"]["errorCodeMapping"]} content={helpTooltips["task"]["dataExtractionGoal"]}
/> />
</div> </div>
<Checkbox <WorkflowBlockInputTextarea
checked={inputs.errorCodeMapping !== "null"} nodeId={id}
disabled={!editable} onChange={(value) => {
onCheckedChange={(checked) => { handleChange("dataExtractionGoal", value);
handleChange(
"errorCodeMapping",
checked
? JSON.stringify(errorMappingExampleValue, null, 2)
: "null",
);
}} }}
value={inputs.dataExtractionGoal}
placeholder={placeholders["task"]["dataExtractionGoal"]}
className="nopan text-xs"
/> />
</div> </div>
{inputs.errorCodeMapping !== "null" && ( <WorkflowDataSchemaInputGroup
<div> exampleValue={dataSchemaExampleValue}
<CodeEditor onChange={(value) => {
language="json" handleChange("dataSchema", value);
value={inputs.errorCodeMapping} }}
onChange={(value) => { value={inputs.dataSchema}
handleChange("errorCodeMapping", value); suggestionContext={{
}} data_extraction_goal: inputs.dataExtractionGoal,
className="nowheel nopan" current_schema: inputs.dataSchema,
fontSize={8} navigation_goal: inputs.navigationGoal,
/> }}
</div> />
)}
</div> </div>
<Separator /> </AccordionContent>
<div className="flex items-center justify-between"> </AccordionItem>
<div className="flex gap-2"> <AccordionItem value="advanced" className="border-b-0">
<Label className="text-xs font-normal text-slate-300"> <AccordionTrigger>Advanced Settings</AccordionTrigger>
Include Action History <AccordionContent className="pl-6 pr-1 pt-1">
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs text-slate-300">
Complete if...
</Label> </Label>
<HelpTooltip <WorkflowBlockInputTextarea
content={ nodeId={id}
helpTooltips["task"][ onChange={(value) => {
"includeActionHistoryInVerification" handleChange("completeCriterion", value);
]
}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.includeActionHistoryInVerification}
onCheckedChange={(checked) => {
handleChange(
"includeActionHistoryInVerification",
checked,
);
}} }}
value={inputs.completeCriterion}
className="nopan text-xs"
/> />
</div> </div>
</div> <Separator />
<div className="flex items-center justify-between"> <ModelSelector
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Continue on Failure
</Label>
<HelpTooltip
content={helpTooltips["task"]["continueOnFailure"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.continueOnFailure}
onCheckedChange={(checked) => {
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={helpTooltips["task"]["cacheActions"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.cacheActions}
onCheckedChange={(checked) => {
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={helpTooltips["task"]["completeOnDownload"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.allowDownloads}
onCheckedChange={(checked) => {
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={helpTooltips["task"]["fileSuffix"]} />
</div>
<WorkflowBlockInput
nodeId={id}
type="text"
placeholder={placeholders["task"]["downloadSuffix"]}
className="nopan w-52 text-xs" className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""} value={inputs.model}
onChange={(value) => { onChange={(value) => {
handleChange("downloadSuffix", value); handleChange("model", value);
}} }}
/> />
</div> <div className="flex items-center justify-between">
<Separator /> <div className="flex gap-2">
<div className="space-y-2"> <Label className="text-xs font-normal text-slate-300">
<div className="flex gap-2"> Engine
<Label className="text-xs text-slate-300"> </Label>
2FA Identifier </div>
</Label> <RunEngineSelector
<HelpTooltip value={inputs.engine}
content={helpTooltips["task"]["totpIdentifier"]} onChange={(value) => {
handleChange("engine", value);
}}
className="nopan w-52 text-xs"
/> />
</div> </div>
<WorkflowBlockInputTextarea <div className="flex items-center justify-between">
nodeId={id} <div className="flex gap-2">
onChange={(value) => { <Label className="text-xs font-normal text-slate-300">
handleChange("totpIdentifier", value); Max Steps Override
}} </Label>
value={inputs.totpIdentifier ?? ""} <HelpTooltip
placeholder={placeholders["task"]["totpIdentifier"]} content={helpTooltips["task"]["maxStepsOverride"]}
className="nopan text-xs" />
/> </div>
</div> <Input
<div className="space-y-2"> type="number"
<div className="flex gap-2"> placeholder={placeholders["task"]["maxStepsOverride"]}
<Label className="text-xs text-slate-300"> className="nopan w-52 text-xs"
2FA Verification URL min="0"
</Label> value={inputs.maxStepsOverride ?? ""}
<HelpTooltip onChange={(event) => {
content={helpTooltips["task"]["totpVerificationUrl"]} 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={helpTooltips["task"]["errorCodeMapping"]}
/>
</div>
<Checkbox
checked={inputs.errorCodeMapping !== "null"}
disabled={!editable}
onCheckedChange={(checked) => {
handleChange(
"errorCodeMapping",
checked
? JSON.stringify(
errorMappingExampleValue,
null,
2,
)
: "null",
);
}}
/>
</div>
{inputs.errorCodeMapping !== "null" && (
<div>
<CodeEditor
language="json"
value={inputs.errorCodeMapping}
onChange={(value) => {
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">
Include Action History
</Label>
<HelpTooltip
content={
helpTooltips["task"][
"includeActionHistoryInVerification"
]
}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.includeActionHistoryInVerification}
onCheckedChange={(checked) => {
handleChange(
"includeActionHistoryInVerification",
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>
<HelpTooltip
content={helpTooltips["task"]["continueOnFailure"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.continueOnFailure}
onCheckedChange={(checked) => {
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={helpTooltips["task"]["cacheActions"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.cacheActions}
onCheckedChange={(checked) => {
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={helpTooltips["task"]["completeOnDownload"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.allowDownloads}
onCheckedChange={(checked) => {
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={helpTooltips["task"]["fileSuffix"]}
/>
</div>
<WorkflowBlockInput
nodeId={id}
type="text"
placeholder={placeholders["task"]["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(value) => {
handleChange("downloadSuffix", value);
}}
/>
</div>
<Separator />
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Identifier
</Label>
<HelpTooltip
content={helpTooltips["task"]["totpIdentifier"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpIdentifier", value);
}}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["task"]["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Verification URL
</Label>
<HelpTooltip
content={helpTooltips["task"]["totpVerificationUrl"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpVerificationUrl", value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["task"]["totpVerificationUrl"]}
className="nopan text-xs"
/> />
</div> </div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpVerificationUrl", value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={placeholders["task"]["totpVerificationUrl"]}
className="nopan text-xs"
/>
</div> </div>
</div> </AccordionContent>
</AccordionContent> </AccordionItem>
</AccordionItem> </Accordion>
</Accordion> </div>
</div> </div>
</div> <BlockCodeEditor blockLabel={label} blockType={type} script={script} />
</Flippable>
); );
} }

View File

@@ -1,10 +1,14 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import type { URLNode } from "./types"; import type { URLNode } from "./types";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { useState } from "react"; import { useState } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { placeholders } from "../../helpContent"; import { placeholders } from "../../helpContent";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { useDebugStore } from "@/store/useDebugStore"; import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader"; import { NodeHeader } from "../components/NodeHeader";
@@ -12,7 +16,10 @@ import { useParams } from "react-router-dom";
function URLNode({ id, data, type }: NodeProps<URLNode>) { function URLNode({ id, data, type }: NodeProps<URLNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { debuggable, editable, label } = data; const { debuggable, editable, label } = data;
const script = blockScriptStore.scripts[label];
const debugStore = useDebugStore(); const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable; const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
@@ -32,62 +39,73 @@ function URLNode({ id, data, type }: NodeProps<URLNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
}, [data.showCode]);
return ( return (
<div> <Flippable facing={facing} preserveFrontsideHeight={true}>
<Handle <div>
type="source" <Handle
position={Position.Bottom} type="source"
id="a" position={Position.Bottom}
className="opacity-0" id="a"
/> className="opacity-0"
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={null}
totpUrl={null}
type="goto_url"
/> />
<div className="space-y-4"> <Handle
<div className="space-y-2"> type="target"
<div className="flex justify-between"> position={Position.Top}
<Label className="text-xs text-slate-300">URL</Label> id="b"
{isFirstWorkflowBlock ? ( className="opacity-0"
<div className="flex justify-end text-xs text-slate-400"> />
Tip: Use the {"+"} button to add parameters! <div
</div> className={cn(
) : null} "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={null}
totpUrl={null}
type="goto_url"
/>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<Label className="text-xs text-slate-300">URL</Label>
{isFirstWorkflowBlock ? (
<div className="flex justify-end text-xs text-slate-400">
Tip: Use the {"+"} button to add parameters!
</div>
) : null}
</div>
<WorkflowBlockInputTextarea
canWriteTitle={true}
nodeId={id}
onChange={(value) => {
handleChange("url", value);
}}
value={inputs.url}
placeholder={placeholders[type]["url"]}
className="nopan text-xs"
/>
</div> </div>
<WorkflowBlockInputTextarea
canWriteTitle={true}
nodeId={id}
onChange={(value) => {
handleChange("url", value);
}}
value={inputs.url}
placeholder={placeholders[type]["url"]}
className="nopan text-xs"
/>
</div> </div>
</div> </div>
</div> </div>
</div> <BlockCodeEditor
blockLabel={label}
blockType="goto_url" // sic: the naming is inconsistent
script={script}
/>
</Flippable>
); );
} }

View File

@@ -1,3 +1,5 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { HelpTooltip } from "@/components/HelpTooltip"; import { HelpTooltip } from "@/components/HelpTooltip";
import { import {
Accordion, Accordion,
@@ -10,7 +12,9 @@ import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { import {
Handle, Handle,
NodeProps, NodeProps,
@@ -35,7 +39,10 @@ import { useParams } from "react-router-dom";
function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) { function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { debuggable, editable, label } = data; const { debuggable, editable, label } = data;
const script = blockScriptStore.scripts[label];
const debugStore = useDebugStore(); const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable; const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
@@ -61,162 +68,177 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
}, [data.showCode]);
return ( return (
<div> <Flippable facing={facing} preserveFrontsideHeight={true}>
<Handle <div>
type="source" <Handle
position={Position.Bottom} type="source"
id="a" position={Position.Bottom}
className="opacity-0" id="a"
/> className="opacity-0"
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div
className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id}
totpIdentifier={null}
totpUrl={null}
type={type}
/> />
<div className="space-y-2"> <Handle
<div className="flex justify-between"> type="target"
<Label className="text-xs text-slate-300">Complete if...</Label> position={Position.Top}
{isFirstWorkflowBlock ? ( id="b"
<div className="flex justify-end text-xs text-slate-400"> className="opacity-0"
Tip: Use the {"+"} button to add parameters! />
</div> <div
) : null} className={cn(
</div> "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
<WorkflowBlockInputTextarea {
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsPlaying,
},
)}
>
<NodeHeader
blockLabel={label}
disabled={elideFromDebugging}
editable={editable}
nodeId={id} nodeId={id}
onChange={(value) => { totpIdentifier={null}
handleChange("completeCriterion", value); totpUrl={null}
}} type={type}
value={inputs.completeCriterion}
className="nopan text-xs"
/> />
</div> <div className="space-y-2">
<div className="space-y-2"> <div className="flex justify-between">
<Label className="text-xs text-slate-300">Terminate if...</Label> <Label className="text-xs text-slate-300">Complete if...</Label>
<WorkflowBlockInputTextarea {isFirstWorkflowBlock ? (
nodeId={id} <div className="flex justify-end text-xs text-slate-400">
onChange={(value) => { Tip: Use the {"+"} button to add parameters!
handleChange("terminateCriterion", value);
}}
value={inputs.terminateCriterion}
className="nopan text-xs"
/>
</div>
<Separator />
<Accordion type="single" collapsible>
<AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger className="py-0">
Advanced Settings
</AccordionTrigger>
<AccordionContent>
<div className="ml-6 mt-4 space-y-4">
<div className="space-y-2">
<ModelSelector
className="nopan mr-[1px] w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div> </div>
<div className="space-y-2"> ) : null}
<div className="flex gap-4"> </div>
<div className="flex gap-2"> <WorkflowBlockInputTextarea
<Label className="text-xs font-normal text-slate-300"> nodeId={id}
Error Messages onChange={(value) => {
</Label> handleChange("completeCriterion", value);
<HelpTooltip }}
content={helpTooltips["validation"]["errorCodeMapping"]} value={inputs.completeCriterion}
/> className="nopan text-xs"
</div> />
<Checkbox </div>
checked={inputs.errorCodeMapping !== "null"} <div className="space-y-2">
disabled={!editable} <Label className="text-xs text-slate-300">Terminate if...</Label>
onCheckedChange={(checked) => { <WorkflowBlockInputTextarea
if (!editable) { nodeId={id}
return; onChange={(value) => {
} handleChange("terminateCriterion", value);
handleChange( }}
"errorCodeMapping", value={inputs.terminateCriterion}
checked className="nopan text-xs"
? JSON.stringify(errorMappingExampleValue, null, 2) />
: "null", </div>
); <Separator />
<Accordion type="single" collapsible>
<AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger className="py-0">
Advanced Settings
</AccordionTrigger>
<AccordionContent>
<div className="ml-6 mt-4 space-y-4">
<div className="space-y-2">
<ModelSelector
className="nopan mr-[1px] w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
<ParametersMultiSelect
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}} }}
/> />
</div> </div>
{inputs.errorCodeMapping !== "null" && ( <div className="space-y-2">
<div> <div className="flex gap-4">
<CodeEditor <div className="flex gap-2">
language="json" <Label className="text-xs font-normal text-slate-300">
value={inputs.errorCodeMapping} Error Messages
onChange={(value) => { </Label>
<HelpTooltip
content={
helpTooltips["validation"]["errorCodeMapping"]
}
/>
</div>
<Checkbox
checked={inputs.errorCodeMapping !== "null"}
disabled={!editable}
onCheckedChange={(checked) => {
if (!editable) { if (!editable) {
return; return;
} }
handleChange("errorCodeMapping", value); handleChange(
"errorCodeMapping",
checked
? JSON.stringify(
errorMappingExampleValue,
null,
2,
)
: "null",
);
}} }}
className="nowheel nopan"
fontSize={8}
/> />
</div> </div>
)} {inputs.errorCodeMapping !== "null" && (
</div> <div>
<Separator /> <CodeEditor
<div className="flex items-center justify-between"> language="json"
<div className="flex gap-2"> value={inputs.errorCodeMapping}
<Label className="text-xs font-normal text-slate-300"> onChange={(value) => {
Continue on Failure if (!editable) {
</Label> return;
<HelpTooltip }
content={helpTooltips["validation"]["continueOnFailure"]} handleChange("errorCodeMapping", value);
/> }}
className="nowheel nopan"
fontSize={8}
/>
</div>
)}
</div> </div>
<div className="w-52"> <Separator />
<Switch <div className="flex items-center justify-between">
checked={data.continueOnFailure} <div className="flex gap-2">
onCheckedChange={(checked) => { <Label className="text-xs font-normal text-slate-300">
if (!editable) { Continue on Failure
return; </Label>
<HelpTooltip
content={
helpTooltips["validation"]["continueOnFailure"]
} }
updateNodeData(id, { continueOnFailure: checked }); />
}} </div>
/> <div className="w-52">
<Switch
checked={data.continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
updateNodeData(id, { continueOnFailure: checked });
}}
/>
</div>
</div> </div>
</div> </div>
</div> </AccordionContent>
</AccordionContent> </AccordionItem>
</AccordionItem> </Accordion>
</Accordion> </div>
</div> </div>
</div> <BlockCodeEditor blockLabel={label} blockType={type} script={script} />
</Flippable>
); );
} }

View File

@@ -17,6 +17,7 @@ import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQu
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { import {
debuggableWorkflowBlockTypes, debuggableWorkflowBlockTypes,
scriptableWorkflowBlockTypes,
type WorkflowBlockType, type WorkflowBlockType,
type WorkflowApiResponse, type WorkflowApiResponse,
} from "@/routes/workflows/types/workflowTypes"; } from "@/routes/workflows/types/workflowTypes";
@@ -154,6 +155,7 @@ function NodeHeader({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const location = useLocation(); const location = useLocation();
const isDebuggable = debuggableWorkflowBlockTypes.has(type); const isDebuggable = debuggableWorkflowBlockTypes.has(type);
const isScriptable = scriptableWorkflowBlockTypes.has(type);
const { data: workflowRun } = useWorkflowRunQuery(); const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued = const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun); workflowRun && statusIsRunningOrQueued(workflowRun);
@@ -414,6 +416,7 @@ function NodeHeader({
})} })}
> >
<NodeActionMenu <NodeActionMenu
isScriptable={isScriptable}
onDelete={() => { onDelete={() => {
deleteNodeCallback(nodeId); deleteNodeCallback(nodeId);
}} }}

View File

@@ -228,6 +228,17 @@ export const debuggableWorkflowBlockTypes: Set<WorkflowBlockType> = new Set([
"validation", "validation",
]); ]);
export const scriptableWorkflowBlockTypes: Set<WorkflowBlockType> = new Set([
"action",
"extraction",
"file_download",
"goto_url",
"login",
"navigation",
"task",
"validation",
]);
export function isTaskVariantBlock(item: { export function isTaskVariantBlock(item: {
block_type: WorkflowBlockType; block_type: WorkflowBlockType;
}): boolean { }): boolean {