diff --git a/skyvern-frontend/src/components/WorkflowBlockInput.tsx b/skyvern-frontend/src/components/WorkflowBlockInput.tsx new file mode 100644 index 00000000..c372ee73 --- /dev/null +++ b/skyvern-frontend/src/components/WorkflowBlockInput.tsx @@ -0,0 +1,22 @@ +import { PlusIcon } from "@radix-ui/react-icons"; +import { cn } from "@/util/utils"; +import { Input } from "./ui/input"; + +type Props = React.ComponentProps & { + onIconClick: () => void; +}; + +function WorkflowBlockInput(props: Props) { + return ( +
+ +
+
+ +
+
+
+ ); +} + +export { WorkflowBlockInput }; diff --git a/skyvern-frontend/src/components/WorkflowBlockInputTextarea.tsx b/skyvern-frontend/src/components/WorkflowBlockInputTextarea.tsx new file mode 100644 index 00000000..b5cf6b92 --- /dev/null +++ b/skyvern-frontend/src/components/WorkflowBlockInputTextarea.tsx @@ -0,0 +1,25 @@ +import { PlusIcon } from "@radix-ui/react-icons"; +import { cn } from "@/util/utils"; +import { AutoResizingTextarea } from "./AutoResizingTextarea/AutoResizingTextarea"; + +type Props = React.ComponentProps & { + onIconClick: () => void; +}; + +function WorkflowBlockInputTextarea(props: Props) { + return ( +
+ +
+
+ +
+
+
+ ); +} + +export { WorkflowBlockInputTextarea }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx index 27424495..a2a6c0e3 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx @@ -1,4 +1,3 @@ -import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { Accordion, AccordionContent, @@ -22,6 +21,8 @@ import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { Switch } from "@/components/ui/switch"; import { ClickIcon } from "@/components/icons/ClickIcon"; import { placeholders, helpTooltips } from "../../helpContent"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { WorkflowBlockParameterSelect } from "../WorkflowBlockParameterSelect"; const urlTooltip = "The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off."; @@ -31,6 +32,9 @@ const navigationGoalTooltip = const navigationGoalPlaceholder = 'Input {{ name }} into "Name" field.'; function ActionNode({ id, data }: NodeProps) { + const [parametersPanelField, setParametersPanelField] = useState< + string | null + >(null); const { updateNodeData } = useReactFlow(); const { editable } = data; const [label, setLabel] = useNodeLabelChangeHandler({ @@ -102,7 +106,10 @@ function ActionNode({ id, data }: NodeProps) { - { + setParametersPanelField("url"); + }} onChange={(event) => { if (!editable) { return; @@ -121,7 +128,10 @@ function ActionNode({ id, data }: NodeProps) { - { + setParametersPanelField("navigationGoal"); + }} onChange={(event) => { if (!editable) { return; @@ -309,11 +319,11 @@ function ActionNode({ id, data }: NodeProps) { content={helpTooltips["action"]["totpVerificationUrl"]} /> - { + setParametersPanelField("totpVerificationUrl"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpVerificationUrl", event.target.value); }} value={inputs.totpVerificationUrl ?? ""} @@ -330,7 +340,10 @@ function ActionNode({ id, data }: NodeProps) { content={helpTooltips["action"]["totpIdentifier"]} /> - { + setParametersPanelField("totpIdentifier"); + }} onChange={(event) => { if (!editable) { return; @@ -347,6 +360,25 @@ function ActionNode({ id, data }: NodeProps) { + {typeof parametersPanelField === "string" && ( + setParametersPanelField(null)} + onAdd={(parameterKey) => { + if (parametersPanelField === null || !editable) { + return; + } + if (parametersPanelField in inputs) { + const currentValue = + inputs[parametersPanelField as keyof typeof inputs]; + handleChange( + parametersPanelField, + `${currentValue ?? ""}{{ ${parameterKey} }}`, + ); + } + }} + /> + )} ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx index 770ead1f..add35e84 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx @@ -1,4 +1,3 @@ -import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { Accordion, AccordionContent, @@ -23,8 +22,13 @@ import type { ExtractionNode } from "./types"; import { ExtractIcon } from "@/components/icons/ExtractIcon"; import { helpTooltips, placeholders } from "../../helpContent"; +import { WorkflowBlockParameterSelect } from "../WorkflowBlockParameterSelect"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; function ExtractionNode({ id, data }: NodeProps) { + const [parametersPanelField, setParametersPanelField] = useState< + string | null + >(null); const { updateNodeData } = useReactFlow(); const { editable } = data; const [label, setLabel] = useNodeLabelChangeHandler({ @@ -96,7 +100,10 @@ function ExtractionNode({ id, data }: NodeProps) { content={helpTooltips["extraction"]["dataExtractionGoal"]} /> - { + setParametersPanelField("dataExtractionGoal"); + }} onChange={(event) => { if (!editable) { return; @@ -256,6 +263,25 @@ function ExtractionNode({ id, data }: NodeProps) { + {typeof parametersPanelField === "string" && ( + setParametersPanelField(null)} + onAdd={(parameterKey) => { + if (parametersPanelField === null || !editable) { + return; + } + if (parametersPanelField in inputs) { + const currentValue = + inputs[parametersPanelField as keyof typeof inputs]; + handleChange( + parametersPanelField, + `${currentValue ?? ""}{{ ${parameterKey} }}`, + ); + } + }} + /> + )} ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx index 478d5bad..b08238b2 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx @@ -1,4 +1,3 @@ -import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Accordion, @@ -22,6 +21,8 @@ import { NodeActionMenu } from "../NodeActionMenu"; import { errorMappingExampleValue } from "../types"; import type { FileDownloadNode } from "./types"; import { helpTooltips, placeholders } from "../../helpContent"; +import { WorkflowBlockParameterSelect } from "../WorkflowBlockParameterSelect"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; const urlTooltip = "The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off."; @@ -31,6 +32,9 @@ const navigationGoalTooltip = const navigationGoalPlaceholder = "Tell Skyvern which file to download."; function FileDownloadNode({ id, data }: NodeProps) { + const [parametersPanelField, setParametersPanelField] = useState< + string | null + >(null); const { updateNodeData } = useReactFlow(); const { editable } = data; const [label, setLabel] = useNodeLabelChangeHandler({ @@ -104,11 +108,11 @@ function FileDownloadNode({ id, data }: NodeProps) { - { + setParametersPanelField("url"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("url", event.target.value); }} value={inputs.url} @@ -121,11 +125,11 @@ function FileDownloadNode({ id, data }: NodeProps) { - { + setParametersPanelField("navigationGoal"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("navigationGoal", event.target.value); }} value={inputs.navigationGoal} @@ -161,9 +165,6 @@ function FileDownloadNode({ id, data }: NodeProps) { min="0" value={inputs.maxRetries ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -188,9 +189,6 @@ function FileDownloadNode({ id, data }: NodeProps) { min="0" value={inputs.maxStepsOverride ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -213,9 +211,6 @@ function FileDownloadNode({ id, data }: NodeProps) { checked={inputs.errorCodeMapping !== "null"} disabled={!editable} onCheckedChange={(checked) => { - if (!editable) { - return; - } handleChange( "errorCodeMapping", checked @@ -231,9 +226,6 @@ function FileDownloadNode({ id, data }: NodeProps) { language="json" value={inputs.errorCodeMapping} onChange={(value) => { - if (!editable) { - return; - } handleChange("errorCodeMapping", value); }} className="nowheel nopan" @@ -256,9 +248,6 @@ function FileDownloadNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("continueOnFailure", checked); }} /> @@ -277,9 +266,6 @@ function FileDownloadNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("cacheActions", checked); }} /> @@ -301,9 +287,6 @@ function FileDownloadNode({ id, data }: NodeProps) { className="nopan w-52 text-xs" value={inputs.downloadSuffix ?? ""} onChange={(event) => { - if (!editable) { - return; - } handleChange("downloadSuffix", event.target.value); }} /> @@ -318,11 +301,11 @@ function FileDownloadNode({ id, data }: NodeProps) { content={helpTooltips["download"]["totpVerificationUrl"]} /> - { + setParametersPanelField("totpVerificationUrl"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpVerificationUrl", event.target.value); }} value={inputs.totpVerificationUrl ?? ""} @@ -341,11 +324,11 @@ function FileDownloadNode({ id, data }: NodeProps) { content={helpTooltips["download"]["totpIdentifier"]} /> - { + setParametersPanelField("totpIdentifier"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpIdentifier", event.target.value); }} value={inputs.totpIdentifier ?? ""} @@ -358,6 +341,25 @@ function FileDownloadNode({ id, data }: NodeProps) { + {typeof parametersPanelField === "string" && ( + setParametersPanelField(null)} + onAdd={(parameterKey) => { + if (parametersPanelField === null || !editable) { + return; + } + if (parametersPanelField in inputs) { + const currentValue = + inputs[parametersPanelField as keyof typeof inputs]; + handleChange( + parametersPanelField, + `${currentValue ?? ""}{{ ${parameterKey} }}`, + ); + } + }} + /> + )} ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx index f6eb3318..9b02ad42 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx @@ -1,4 +1,3 @@ -import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { Accordion, AccordionContent, @@ -23,8 +22,13 @@ import type { LoginNode } from "./types"; import { LockOpen1Icon } from "@radix-ui/react-icons"; import { CredentialParameterSelector } from "./CredentialParameterSelector"; import { helpTooltips, placeholders } from "../../helpContent"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { WorkflowBlockParameterSelect } from "../WorkflowBlockParameterSelect"; function LoginNode({ id, data }: NodeProps) { + const [parametersPanelField, setParametersPanelField] = useState< + string | null + >(null); const { updateNodeData } = useReactFlow(); const { editable } = data; const [label, setLabel] = useNodeLabelChangeHandler({ @@ -53,7 +57,7 @@ function LoginNode({ id, data }: NodeProps) { } return ( -
+
) {
- { + setParametersPanelField("url"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("url", event.target.value); }} value={inputs.url} @@ -112,11 +116,11 @@ function LoginNode({ id, data }: NodeProps) {
- { + setParametersPanelField("navigationGoal"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("navigationGoal", event.target.value); }} value={inputs.navigationGoal} @@ -175,9 +179,6 @@ function LoginNode({ id, data }: NodeProps) { min="0" value={inputs.maxRetries ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -202,9 +203,6 @@ function LoginNode({ id, data }: NodeProps) { min="0" value={inputs.maxStepsOverride ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -227,9 +225,6 @@ function LoginNode({ id, data }: NodeProps) { checked={inputs.errorCodeMapping !== "null"} disabled={!editable} onCheckedChange={(checked) => { - if (!editable) { - return; - } handleChange( "errorCodeMapping", checked @@ -245,9 +240,6 @@ function LoginNode({ id, data }: NodeProps) { language="json" value={inputs.errorCodeMapping} onChange={(value) => { - if (!editable) { - return; - } handleChange("errorCodeMapping", value); }} className="nowheel nopan" @@ -270,9 +262,6 @@ function LoginNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("continueOnFailure", checked); }} /> @@ -291,9 +280,6 @@ function LoginNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("cacheActions", checked); }} /> @@ -309,11 +295,11 @@ function LoginNode({ id, data }: NodeProps) { content={helpTooltips["login"]["totpVerificationUrl"]} /> - { + setParametersPanelField("totpVerificationUrl"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpVerificationUrl", event.target.value); }} value={inputs.totpVerificationUrl ?? ""} @@ -330,11 +316,11 @@ function LoginNode({ id, data }: NodeProps) { content={helpTooltips["login"]["totpIdentifier"]} /> - { + setParametersPanelField("totpIdentifier"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpIdentifier", event.target.value); }} value={inputs.totpIdentifier ?? ""} @@ -347,6 +333,25 @@ function LoginNode({ id, data }: NodeProps) { + {typeof parametersPanelField === "string" && ( + setParametersPanelField(null)} + onAdd={(parameterKey) => { + if (parametersPanelField === null || !editable) { + return; + } + if (parametersPanelField in inputs) { + const currentValue = + inputs[parametersPanelField as keyof typeof inputs]; + handleChange( + parametersPanelField, + `${currentValue ?? ""}{{ ${parameterKey} }}`, + ); + } + }} + /> + )} ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx index 447a61b2..84d382fb 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx @@ -1,4 +1,3 @@ -import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { Accordion, AccordionContent, @@ -22,9 +21,16 @@ import { Switch } from "@/components/ui/switch"; import type { NavigationNode } from "./types"; import { RobotIcon } from "@/components/icons/RobotIcon"; import { helpTooltips, placeholders } from "../../helpContent"; +import { WorkflowBlockParameterSelect } from "../WorkflowBlockParameterSelect"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; function NavigationNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [parametersPanelField, setParametersPanelField] = useState< + string | null + >(null); + const { editable } = data; const [label, setLabel] = useNodeLabelChangeHandler({ id, @@ -54,7 +60,7 @@ function NavigationNode({ id, data }: NodeProps) { } return ( -
+
) {
- setParametersPanelField("url")} onChange={(event) => { - if (!editable) { - return; - } handleChange("url", event.target.value); }} value={inputs.url} @@ -115,11 +119,9 @@ function NavigationNode({ id, data }: NodeProps) { content={helpTooltips["navigation"]["navigationGoal"]} />
- setParametersPanelField("navigationGoal")} onChange={(event) => { - if (!editable) { - return; - } handleChange("navigationGoal", event.target.value); }} value={inputs.navigationGoal} @@ -152,9 +154,6 @@ function NavigationNode({ id, data }: NodeProps) { min="0" value={inputs.maxRetries ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -179,9 +178,6 @@ function NavigationNode({ id, data }: NodeProps) { min="0" value={inputs.maxStepsOverride ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -204,9 +200,6 @@ function NavigationNode({ id, data }: NodeProps) { checked={inputs.errorCodeMapping !== "null"} disabled={!editable} onCheckedChange={(checked) => { - if (!editable) { - return; - } handleChange( "errorCodeMapping", checked @@ -222,9 +215,6 @@ function NavigationNode({ id, data }: NodeProps) { language="json" value={inputs.errorCodeMapping} onChange={(value) => { - if (!editable) { - return; - } handleChange("errorCodeMapping", value); }} className="nowheel nopan" @@ -247,9 +237,6 @@ function NavigationNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("continueOnFailure", checked); }} /> @@ -268,9 +255,6 @@ function NavigationNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("cacheActions", checked); }} /> @@ -290,9 +274,6 @@ function NavigationNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("allowDownloads", checked); }} /> @@ -307,15 +288,15 @@ function NavigationNode({ id, data }: NodeProps) { content={helpTooltips["navigation"]["fileSuffix"]} /> - { + setParametersPanelField("downloadSuffix"); + }} type="text" placeholder={placeholders["navigation"]["downloadSuffix"]} className="nopan w-52 text-xs" value={inputs.downloadSuffix ?? ""} onChange={(event) => { - if (!editable) { - return; - } handleChange("downloadSuffix", event.target.value); }} /> @@ -332,11 +313,11 @@ function NavigationNode({ id, data }: NodeProps) { } /> - { + setParametersPanelField("totpVerificationUrl"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpVerificationUrl", event.target.value); }} value={inputs.totpVerificationUrl ?? ""} @@ -355,11 +336,11 @@ function NavigationNode({ id, data }: NodeProps) { content={helpTooltips["navigation"]["totpIdentifier"]} /> - { + setParametersPanelField("totpIdentifier"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpIdentifier", event.target.value); }} value={inputs.totpIdentifier ?? ""} @@ -372,6 +353,25 @@ function NavigationNode({ id, data }: NodeProps) { + {typeof parametersPanelField === "string" && ( + setParametersPanelField(null)} + onAdd={(parameterKey) => { + if (parametersPanelField === null || !editable) { + return; + } + if (parametersPanelField in inputs) { + const currentValue = + inputs[parametersPanelField as keyof typeof inputs]; + handleChange( + parametersPanelField, + `${currentValue ?? ""}{{ ${parameterKey} }}`, + ); + } + }} + /> + )} ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx index 18b2ac87..06934d1b 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx @@ -1,4 +1,3 @@ -import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Accordion, @@ -9,6 +8,7 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; @@ -24,22 +24,28 @@ import { } from "@xyflow/react"; import { useState } from "react"; import { AppNode } from ".."; +import { helpTooltips, placeholders } from "../../helpContent"; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; +import { dataSchemaExampleValue, errorMappingExampleValue } from "../types"; +import { WorkflowBlockParameterSelect } from "../WorkflowBlockParameterSelect"; import { ParametersMultiSelect } from "./ParametersMultiSelect"; import type { TaskNode } from "./types"; -import { Separator } from "@/components/ui/separator"; -import { dataSchemaExampleValue, errorMappingExampleValue } from "../types"; -import { helpTooltips, placeholders } from "../../helpContent"; +import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; function TaskNode({ id, data }: NodeProps) { + const [parametersPanelField, setParametersPanelField] = useState< + string | null + >(null); const { updateNodeData } = useReactFlow(); const { editable } = data; const deleteNodeCallback = useDeleteNodeCallback(); const nodes = useNodes(); const edges = useEdges(); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); + const [label, setLabel] = useNodeLabelChangeHandler({ id, initialValue: data.label, @@ -70,7 +76,7 @@ function TaskNode({ id, data }: NodeProps) { } return ( -
+
) {
- { - if (!editable) { - return; - } - handleChange("url", event.target.value); + { + setParametersPanelField("url"); + }} + onChange={(value) => { + handleChange("url", value); }} value={inputs.url} placeholder={placeholders["task"]["url"]} @@ -135,12 +141,12 @@ function TaskNode({ id, data }: NodeProps) { content={helpTooltips["task"]["navigationGoal"]} />
- { - if (!editable) { - return; - } - handleChange("navigationGoal", event.target.value); + { + setParametersPanelField("navigationGoal"); + }} + onChange={(value) => { + handleChange("navigationGoal", value); }} value={inputs.navigationGoal} placeholder={placeholders["task"]["navigationGoal"]} @@ -172,11 +178,11 @@ function TaskNode({ id, data }: NodeProps) { content={helpTooltips["task"]["dataExtractionGoal"]} /> - { + setParametersPanelField("dataExtractionGoal"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("dataExtractionGoal", event.target.value); }} value={inputs.dataExtractionGoal} @@ -197,9 +203,6 @@ function TaskNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange( "dataSchema", checked @@ -215,9 +218,6 @@ function TaskNode({ id, data }: NodeProps) { language="json" value={inputs.dataSchema} onChange={(value) => { - if (!editable) { - return; - } handleChange("dataSchema", value); }} className="nowheel nopan" @@ -247,9 +247,6 @@ function TaskNode({ id, data }: NodeProps) { min="0" value={inputs.maxRetries ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -274,9 +271,6 @@ function TaskNode({ id, data }: NodeProps) { min="0" value={inputs.maxStepsOverride ?? ""} onChange={(event) => { - if (!editable) { - return; - } const value = event.target.value === "" ? null @@ -299,9 +293,6 @@ function TaskNode({ id, data }: NodeProps) { checked={inputs.errorCodeMapping !== "null"} disabled={!editable} onCheckedChange={(checked) => { - if (!editable) { - return; - } handleChange( "errorCodeMapping", checked @@ -317,9 +308,6 @@ function TaskNode({ id, data }: NodeProps) { language="json" value={inputs.errorCodeMapping} onChange={(value) => { - if (!editable) { - return; - } handleChange("errorCodeMapping", value); }} className="nowheel nopan" @@ -342,9 +330,6 @@ function TaskNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("continueOnFailure", checked); }} /> @@ -363,9 +348,6 @@ function TaskNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("cacheActions", checked); }} /> @@ -385,9 +367,6 @@ function TaskNode({ id, data }: NodeProps) { { - if (!editable) { - return; - } handleChange("allowDownloads", checked); }} /> @@ -400,15 +379,15 @@ function TaskNode({ id, data }: NodeProps) { - { + setParametersPanelField("downloadSuffix"); + }} type="text" placeholder={placeholders["task"]["downloadSuffix"]} className="nopan w-52 text-xs" value={inputs.downloadSuffix ?? ""} onChange={(event) => { - if (!editable) { - return; - } handleChange("downloadSuffix", event.target.value); }} /> @@ -423,11 +402,11 @@ function TaskNode({ id, data }: NodeProps) { content={helpTooltips["task"]["totpVerificationUrl"]} /> - { + setParametersPanelField("totpVerificationUrl"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpVerificationUrl", event.target.value); }} value={inputs.totpVerificationUrl ?? ""} @@ -444,11 +423,11 @@ function TaskNode({ id, data }: NodeProps) { content={helpTooltips["task"]["totpIdentifier"]} /> - { + setParametersPanelField("totpIdentifier"); + }} onChange={(event) => { - if (!editable) { - return; - } handleChange("totpIdentifier", event.target.value); }} value={inputs.totpIdentifier ?? ""} @@ -461,6 +440,25 @@ function TaskNode({ id, data }: NodeProps) { + {typeof parametersPanelField === "string" && ( + setParametersPanelField(null)} + onAdd={(parameterKey) => { + if (parametersPanelField === null || !editable) { + return; + } + if (parametersPanelField in inputs) { + const currentValue = + inputs[parametersPanelField as keyof typeof inputs]; + handleChange( + parametersPanelField, + `${currentValue ?? ""}{{ ${parameterKey} }}`, + ); + } + }} + /> + )} ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx index df513c25..a9000b5b 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx @@ -7,7 +7,6 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { Label } from "@/components/ui/label"; -import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Checkbox } from "@/components/ui/checkbox"; import { errorMappingExampleValue } from "../types"; @@ -21,8 +20,13 @@ import { } from "@/components/ui/accordion"; import { Separator } from "@/components/ui/separator"; import { helpTooltips } from "../../helpContent"; +import { WorkflowBlockParameterSelect } from "../WorkflowBlockParameterSelect"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; function ValidationNode({ id, data }: NodeProps) { + const [parametersPanelField, setParametersPanelField] = useState< + string | null + >(null); const { updateNodeData } = useReactFlow(); const { editable } = data; const [label, setLabel] = useNodeLabelChangeHandler({ @@ -83,7 +87,10 @@ function ValidationNode({ id, data }: NodeProps) {
- { + setParametersPanelField("completeCriterion"); + }} onChange={(event) => { if (!editable) { return; @@ -96,7 +103,10 @@ function ValidationNode({ id, data }: NodeProps) {
- { + setParametersPanelField("terminateCriterion"); + }} onChange={(event) => { if (!editable) { return; @@ -185,6 +195,25 @@ function ValidationNode({ id, data }: NodeProps) {
+ {typeof parametersPanelField === "string" && ( + setParametersPanelField(null)} + onAdd={(parameterKey) => { + if (parametersPanelField === null || !editable) { + return; + } + if (parametersPanelField in inputs) { + const currentValue = + inputs[parametersPanelField as keyof typeof inputs]; + handleChange( + parametersPanelField, + `${currentValue ?? ""}{{ ${parameterKey} }}`, + ); + } + }} + /> + )} ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockParameterSelect.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockParameterSelect.tsx new file mode 100644 index 00000000..57572e40 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockParameterSelect.tsx @@ -0,0 +1,101 @@ +import { useEdges, useNodes } from "@xyflow/react"; +import { useWorkflowParametersState } from "../useWorkflowParametersState"; +import { AppNode } from "."; +import { getAvailableOutputParameterKeys } from "../workflowEditorUtils"; +import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons"; +import { SwitchBar } from "@/components/SwitchBar"; +import { useState } from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ScrollAreaViewport } from "@radix-ui/react-scroll-area"; + +type Props = { + nodeId: string; + onClose: () => void; + onAdd: (parameterKey: string) => void; +}; + +function WorkflowBlockParameterSelect({ nodeId, onClose, onAdd }: Props) { + const [content, setContent] = useState("parameters"); + const [workflowParameters] = useWorkflowParametersState(); + const nodes = useNodes(); + const edges = useEdges(); + const outputParameterKeys = getAvailableOutputParameterKeys( + nodes, + edges, + nodeId, + ); + const workflowParameterKeys = workflowParameters.map( + (parameter) => parameter.key, + ); + + return ( +
+
+

Add Parameter

+ +
+ setContent(value)} + value={content} + options={[ + { + label: "Parameters", + value: "parameters", + }, + { + label: "Block Outputs", + value: "outputs", + }, + ]} + /> + + + {content === "parameters" && ( +
+ {workflowParameterKeys.map((parameterKey) => { + return ( +
{ + onAdd(parameterKey); + }} + > + {parameterKey} + +
+ ); + })} + {workflowParameterKeys.length === 0 && ( +
No workflow parameters
+ )} +
+ )} + {content === "outputs" && ( +
+ {outputParameterKeys.map((parameterKey) => { + return ( +
{ + onAdd(parameterKey); + }} + > + {parameterKey} + +
+ ); + })} + {outputParameterKeys.length === 0 && ( +
No output parameters
+ )} +
+ )} +
+
+
+ ); +} + +export { WorkflowBlockParameterSelect };