Rework state management arch for blocks (fix rando max recursion errors, maybe other bugs) (#3755)
This commit is contained in:
@@ -3,8 +3,9 @@ import { json } from "@codemirror/lang-json";
|
||||
import { python } from "@codemirror/lang-python";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { tokyoNightStorm } from "@uiw/codemirror-theme-tokyo-night-storm";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/util/utils";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import "./code-mirror-overrides.css";
|
||||
|
||||
@@ -50,6 +51,20 @@ function CodeEditor({
|
||||
fullHeight = false,
|
||||
}: Props) {
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const debouncedOnChange = useDebouncedCallback((newValue: string) => {
|
||||
onChange?.(newValue);
|
||||
}, 300);
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
setInternalValue(newValue);
|
||||
debouncedOnChange(newValue);
|
||||
};
|
||||
|
||||
const extensions = language
|
||||
? [getLanguageExtension(language), lineWrap ? EditorView.lineWrapping : []]
|
||||
@@ -103,8 +118,8 @@ function CodeEditor({
|
||||
|
||||
return (
|
||||
<CodeMirror
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
extensions={extensions}
|
||||
theme={tokyoNightStorm}
|
||||
minHeight={minHeight}
|
||||
@@ -118,6 +133,9 @@ function CodeEditor({
|
||||
onUpdate={(viewUpdate) => {
|
||||
if (!viewRef.current) viewRef.current = viewUpdate.view;
|
||||
}}
|
||||
onBlur={() => {
|
||||
debouncedOnChange.flush();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,8 +83,6 @@ import { getWorkflowErrors } from "./workflowEditorUtils";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { useAutoPan } from "./useAutoPan";
|
||||
|
||||
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
function convertToParametersYAML(
|
||||
parameters: ParametersState,
|
||||
): Array<
|
||||
@@ -278,7 +276,6 @@ function FlowRenderer({
|
||||
const parameters = useWorkflowParametersStore((state) => state.parameters);
|
||||
const nodesInitialized = useNodesInitialized();
|
||||
const [shouldConstrainPan, setShouldConstrainPan] = useState(false);
|
||||
const onNodesChangeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const flowIsConstrained = debugStore.isDebugMode;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -672,6 +669,7 @@ function FlowRenderer({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (dimensionChanges.length > 0) {
|
||||
doLayout(tempNodes, edges);
|
||||
}
|
||||
@@ -687,20 +685,23 @@ function FlowRenderer({
|
||||
workflowChangesStore.setHasChanges(true);
|
||||
}
|
||||
|
||||
// only allow one update in _this_ render cycle
|
||||
if (onNodesChangeTimeoutRef.current === null) {
|
||||
onNodesChange(changes);
|
||||
onNodesChangeTimeoutRef.current = setTimeout(() => {
|
||||
onNodesChangeTimeoutRef.current = null;
|
||||
}, 0);
|
||||
} else {
|
||||
// if we have an update in this render cycle already, then to
|
||||
// prevent max recursion errors, defer the update to next render
|
||||
// cycle
|
||||
nextTick().then(() => {
|
||||
onNodesChange(changes);
|
||||
});
|
||||
}
|
||||
onNodesChange(changes);
|
||||
|
||||
// NOTE: should no longer be needed (woot!) - delete if true (want real-world testing first)
|
||||
// // only allow one update in _this_ render cycle
|
||||
// if (onNodesChangeTimeoutRef.current === null) {
|
||||
// onNodesChange(changes);
|
||||
// onNodesChangeTimeoutRef.current = setTimeout(() => {
|
||||
// onNodesChangeTimeoutRef.current = null;
|
||||
// }, 0);
|
||||
// } else {
|
||||
// // if we have an update in this render cycle already, then to
|
||||
// // prevent max recursion errors, defer the update to next render
|
||||
// // cycle
|
||||
// nextTick().then(() => {
|
||||
// onNodesChange(changes);
|
||||
// });
|
||||
// }
|
||||
}}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
|
||||
@@ -8,14 +8,7 @@ import {
|
||||
} from "@/components/ui/accordion";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import type { ActionNode } from "./types";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
@@ -40,6 +33,7 @@ import { useParams } from "react-router-dom";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
|
||||
@@ -51,25 +45,10 @@ const navigationGoalTooltip =
|
||||
const navigationGoalPlaceholder = 'Input {{ name }} into "Name" field.';
|
||||
|
||||
function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
const script = blockScriptStore.scripts[label];
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
allowDownloads: data.allowDownloads,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
cacheActions: data.cacheActions,
|
||||
disableCache: data.disableCache,
|
||||
downloadSuffix: data.downloadSuffix,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
model: data.model,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
engine: data.engine,
|
||||
});
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
const workflowRunIsRunningOrQueued =
|
||||
@@ -79,19 +58,10 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
const update = useUpdate<ActionNode["data"]>({ id, editable });
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -128,8 +98,8 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
totpIdentifier={data.totpIdentifier}
|
||||
totpUrl={data.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div
|
||||
@@ -154,9 +124,9 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
update({ url: value });
|
||||
}}
|
||||
value={inputs.url}
|
||||
value={data.url}
|
||||
placeholder={placeholders["action"]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -171,9 +141,9 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("navigationGoal", value);
|
||||
update({ navigationGoal: value });
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
value={data.navigationGoal}
|
||||
placeholder={navigationGoalPlaceholder}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -203,16 +173,16 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
<div className="space-y-2">
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<ParametersMultiSelect
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -223,9 +193,9 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
</Label>
|
||||
</div>
|
||||
<RunEngineSelector
|
||||
value={inputs.engine}
|
||||
value={data.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
update({ engine: value });
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
@@ -241,35 +211,34 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("errorCodeMapping", value);
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
@@ -289,25 +258,25 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("continueOnFailure", checked);
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisableCache
|
||||
cacheActions={inputs.cacheActions}
|
||||
disableCache={inputs.disableCache}
|
||||
cacheActions={data.cacheActions}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
@@ -322,12 +291,12 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.allowDownloads}
|
||||
checked={data.allowDownloads}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("allowDownloads", checked);
|
||||
update({ allowDownloads: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -346,9 +315,9 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
type="text"
|
||||
placeholder={placeholders["action"]["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
value={data.downloadSuffix ?? ""}
|
||||
onChange={(value) => {
|
||||
handleChange("downloadSuffix", value);
|
||||
update({ downloadSuffix: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -365,9 +334,9 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
update({ totpIdentifier: value });
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
value={data.totpIdentifier ?? ""}
|
||||
placeholder={placeholders["action"]["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -384,9 +353,9 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
update({ totpVerificationUrl: value });
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
value={data.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { WorkflowBlockInputSet } from "@/components/WorkflowBlockInputSet";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import type { CodeBlockNode } from "./types";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { deepEqualStringArrays } from "@/util/equality";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
import type { CodeBlockNode } from "./types";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
|
||||
function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable, label } = data;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
@@ -22,10 +23,7 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
code: data.code,
|
||||
parameterKeys: data.parameterKeys,
|
||||
});
|
||||
const update = useUpdate<CodeBlockNode["data"]>({ id, editable });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -65,36 +63,23 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
|
||||
<WorkflowBlockInputSet
|
||||
nodeId={id}
|
||||
onChange={(parameterKeys) => {
|
||||
const differs = !deepEqualStringArrays(
|
||||
inputs.parameterKeys,
|
||||
Array.from(parameterKeys),
|
||||
);
|
||||
|
||||
if (!differs) {
|
||||
return;
|
||||
const newParameterKeys = Array.from(parameterKeys);
|
||||
if (
|
||||
!deepEqualStringArrays(data.parameterKeys, newParameterKeys)
|
||||
) {
|
||||
update({ parameterKeys: newParameterKeys });
|
||||
}
|
||||
|
||||
setInputs({
|
||||
...inputs,
|
||||
parameterKeys: Array.from(parameterKeys),
|
||||
});
|
||||
|
||||
updateNodeData(id, { parameterKeys: Array.from(parameterKeys) });
|
||||
}}
|
||||
values={new Set(inputs.parameterKeys ?? [])}
|
||||
values={new Set(data.parameterKeys ?? [])}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-slate-300">Code Input</Label>
|
||||
<CodeEditor
|
||||
language="python"
|
||||
value={inputs.code}
|
||||
value={data.code}
|
||||
onChange={(value) => {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, code: value });
|
||||
updateNodeData(id, { code: value });
|
||||
update({ code: value });
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
|
||||
@@ -11,14 +11,7 @@ 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 {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { dataSchemaExampleValue } from "../types";
|
||||
import type { ExtractionNode } from "./types";
|
||||
@@ -40,12 +33,12 @@ import { NodeTabs } from "../components/NodeTabs";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
|
||||
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
@@ -58,31 +51,12 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
dataExtractionGoal: data.dataExtractionGoal,
|
||||
dataSchema: data.dataSchema,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
cacheActions: data.cacheActions,
|
||||
disableCache: data.disableCache,
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
});
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const update = useUpdate<ExtractionNode["data"]>({ id, editable });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -144,22 +118,22 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("dataExtractionGoal", value);
|
||||
update({ dataExtractionGoal: value });
|
||||
}}
|
||||
value={inputs.dataExtractionGoal}
|
||||
value={data.dataExtractionGoal}
|
||||
placeholder={placeholders["extraction"]["dataExtractionGoal"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<WorkflowDataSchemaInputGroup
|
||||
value={inputs.dataSchema}
|
||||
value={data.dataSchema}
|
||||
onChange={(value) => {
|
||||
handleChange("dataSchema", value);
|
||||
update({ dataSchema: value });
|
||||
}}
|
||||
exampleValue={dataSchemaExampleValue}
|
||||
suggestionContext={{
|
||||
data_extraction_goal: inputs.dataExtractionGoal,
|
||||
current_schema: inputs.dataSchema,
|
||||
data_extraction_goal: data.dataExtractionGoal,
|
||||
current_schema: data.dataSchema,
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
@@ -177,16 +151,16 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
<div className="space-y-2">
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<ParametersMultiSelect
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -197,9 +171,9 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
</Label>
|
||||
</div>
|
||||
<RunEngineSelector
|
||||
value={inputs.engine}
|
||||
value={data.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
update({ engine: value });
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
@@ -220,7 +194,7 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
value={data.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
@@ -229,7 +203,7 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
update({ maxStepsOverride: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -247,25 +221,25 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("continueOnFailure", checked);
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisableCache
|
||||
cacheActions={inputs.cacheActions}
|
||||
disableCache={inputs.disableCache}
|
||||
cacheActions={data.cacheActions}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -16,14 +16,7 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
@@ -39,6 +32,7 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
import { BROWSER_DOWNLOAD_TIMEOUT_SECONDS } from "@/api/types";
|
||||
|
||||
@@ -52,7 +46,6 @@ const navigationGoalTooltip =
|
||||
const navigationGoalPlaceholder = "Tell Skyvern which file to download.";
|
||||
|
||||
function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
@@ -65,34 +58,12 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
cacheActions: data.cacheActions,
|
||||
disableCache: data.disableCache,
|
||||
downloadSuffix: data.downloadSuffix,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
downloadTimeout: data.downloadTimeout,
|
||||
});
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const update = useUpdate<FileDownloadNode["data"]>({ id, editable });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -127,8 +98,8 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
totpIdentifier={data.totpIdentifier}
|
||||
totpUrl={data.totpVerificationUrl}
|
||||
type="file_download" // sic: the naming for this block is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
@@ -148,9 +119,9 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
update({ url: value });
|
||||
}}
|
||||
value={inputs.url}
|
||||
value={data.url}
|
||||
placeholder={urlPlaceholder}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -163,9 +134,9 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("navigationGoal", value);
|
||||
update({ navigationGoal: value });
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
value={data.navigationGoal}
|
||||
placeholder={navigationGoalPlaceholder}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -181,7 +152,7 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
|
||||
<Input
|
||||
className="ml-auto w-16 text-right"
|
||||
value={inputs.downloadTimeout ?? undefined}
|
||||
value={data.downloadTimeout ?? undefined}
|
||||
placeholder={`${BROWSER_DOWNLOAD_TIMEOUT_SECONDS}`}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
@@ -190,7 +161,7 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
: Number(event.target.value);
|
||||
|
||||
if (value) {
|
||||
handleChange("downloadTimeout", value);
|
||||
update({ downloadTimeout: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -215,16 +186,16 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
<div className="space-y-2">
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<ParametersMultiSelect
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -235,9 +206,9 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
</Label>
|
||||
</div>
|
||||
<RunEngineSelector
|
||||
value={inputs.engine}
|
||||
value={data.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
update({ engine: value });
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
@@ -256,13 +227,13 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
placeholder={placeholders["download"]["maxStepsOverride"]}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
value={data.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
update({ maxStepsOverride: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -277,29 +248,28 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
handleChange("errorCodeMapping", value);
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
@@ -319,22 +289,22 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("continueOnFailure", checked);
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisableCache
|
||||
cacheActions={inputs.cacheActions}
|
||||
disableCache={inputs.disableCache}
|
||||
cacheActions={data.cacheActions}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
@@ -350,9 +320,9 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("downloadSuffix", value);
|
||||
update({ downloadSuffix: value });
|
||||
}}
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
value={data.downloadSuffix ?? ""}
|
||||
placeholder={placeholders["download"]["downloadSuffix"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -370,9 +340,9 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
update({ totpIdentifier: value });
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
value={data.totpIdentifier ?? ""}
|
||||
placeholder={placeholders["download"]["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -389,9 +359,9 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
update({ totpVerificationUrl: value });
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
value={data.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { type FileParserNode } from "./types";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
@@ -13,10 +12,10 @@ import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/
|
||||
import { dataSchemaExampleForFileExtraction } from "../types";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
|
||||
function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable, label } = data;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
@@ -26,21 +25,8 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
fileUrl: data.fileUrl,
|
||||
jsonSchema: data.jsonSchema,
|
||||
model: data.model,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<FileParserNode["data"]>({ id, editable });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -90,26 +76,26 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
|
||||
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
value={inputs.fileUrl}
|
||||
value={data.fileUrl}
|
||||
onChange={(value) => {
|
||||
handleChange("fileUrl", value);
|
||||
update({ fileUrl: value });
|
||||
}}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<WorkflowDataSchemaInputGroup
|
||||
exampleValue={dataSchemaExampleForFileExtraction}
|
||||
value={inputs.jsonSchema}
|
||||
value={data.jsonSchema}
|
||||
onChange={(value) => {
|
||||
handleChange("jsonSchema", value);
|
||||
update({ jsonSchema: value });
|
||||
}}
|
||||
suggestionContext={{}}
|
||||
/>
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { type FileUploadNode } from "./types";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
@@ -18,9 +17,9 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable, label } = data;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
@@ -30,26 +29,7 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
storageType: data.storageType,
|
||||
awsAccessKeyId: data.awsAccessKeyId ?? "",
|
||||
awsSecretAccessKey: data.awsSecretAccessKey ?? "",
|
||||
s3Bucket: data.s3Bucket ?? "",
|
||||
regionName: data.regionName ?? "",
|
||||
path: data.path ?? "",
|
||||
azureStorageAccountName: data.azureStorageAccountName ?? "",
|
||||
azureStorageAccountKey: data.azureStorageAccountKey ?? "",
|
||||
azureBlobContainerName: data.azureBlobContainerName ?? "",
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const update = useUpdate<FileUploadNode["data"]>({ id, editable });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -92,8 +72,10 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={inputs.storageType}
|
||||
onValueChange={(value) => handleChange("storageType", value)}
|
||||
value={data.storageType}
|
||||
onValueChange={(value) =>
|
||||
value && update({ storageType: value as "s3" | "azure" })
|
||||
}
|
||||
disabled={!editable}
|
||||
>
|
||||
<SelectTrigger className="nopan text-xs">
|
||||
@@ -106,7 +88,7 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{inputs.storageType === "s3" && (
|
||||
{data.storageType === "s3" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -120,9 +102,9 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("awsAccessKeyId", value);
|
||||
update({ awsAccessKeyId: value });
|
||||
}}
|
||||
value={inputs.awsAccessKeyId as string}
|
||||
value={data.awsAccessKeyId as string}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -140,10 +122,10 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
type="password"
|
||||
value={inputs.awsSecretAccessKey as string}
|
||||
value={data.awsSecretAccessKey as string}
|
||||
className="nopan text-xs"
|
||||
onChange={(value) => {
|
||||
handleChange("awsSecretAccessKey", value);
|
||||
update({ awsSecretAccessKey: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -157,9 +139,9 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("s3Bucket", value);
|
||||
update({ s3Bucket: value });
|
||||
}}
|
||||
value={inputs.s3Bucket as string}
|
||||
value={data.s3Bucket as string}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -173,9 +155,9 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("regionName", value);
|
||||
update({ regionName: value });
|
||||
}}
|
||||
value={inputs.regionName as string}
|
||||
value={data.regionName as string}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -189,16 +171,16 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("path", value);
|
||||
update({ path: value });
|
||||
}}
|
||||
value={inputs.path as string}
|
||||
value={data.path as string}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{inputs.storageType === "azure" && (
|
||||
{data.storageType === "azure" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -214,9 +196,9 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("azureStorageAccountName", value);
|
||||
update({ azureStorageAccountName: value });
|
||||
}}
|
||||
value={inputs.azureStorageAccountName as string}
|
||||
value={data.azureStorageAccountName as string}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -234,10 +216,10 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
type="password"
|
||||
value={inputs.azureStorageAccountKey as string}
|
||||
value={data.azureStorageAccountKey as string}
|
||||
className="nopan text-xs"
|
||||
onChange={(value) => {
|
||||
handleChange("azureStorageAccountKey", value);
|
||||
update({ azureStorageAccountKey: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -255,9 +237,9 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("azureBlobContainerName", value);
|
||||
update({ azureBlobContainerName: value });
|
||||
}}
|
||||
value={inputs.azureBlobContainerName as string}
|
||||
value={data.azureBlobContainerName as string}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -271,9 +253,9 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("path", value);
|
||||
update({ path: value });
|
||||
}}
|
||||
value={inputs.path as string}
|
||||
value={data.path as string}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,15 +8,8 @@ import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useCallback } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import type { HttpRequestNode as HttpRequestNodeType } from "./types";
|
||||
@@ -31,6 +24,7 @@ import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -67,83 +61,59 @@ const followRedirectsTooltip =
|
||||
"Whether to automatically follow HTTP redirects.";
|
||||
|
||||
function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const [inputs, setInputs] = useState({
|
||||
method: data.method,
|
||||
url: data.url,
|
||||
headers: data.headers,
|
||||
body: data.body,
|
||||
timeout: data.timeout,
|
||||
followRedirects: data.followRedirects,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const update = useUpdate<HttpRequestNodeType["data"]>({ id, editable });
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const handleCurlImport = useCallback(
|
||||
(importedData: {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: string;
|
||||
body: string;
|
||||
timeout: number;
|
||||
followRedirects: boolean;
|
||||
}) => {
|
||||
update({
|
||||
method: importedData.method,
|
||||
url: importedData.url,
|
||||
headers: importedData.headers,
|
||||
body: importedData.body,
|
||||
timeout: importedData.timeout,
|
||||
followRedirects: importedData.followRedirects,
|
||||
});
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
const handleCurlImport = (importedData: {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: string;
|
||||
body: string;
|
||||
timeout: number;
|
||||
followRedirects: boolean;
|
||||
}) => {
|
||||
const newInputs = {
|
||||
...inputs,
|
||||
method: importedData.method,
|
||||
url: importedData.url,
|
||||
headers: importedData.headers,
|
||||
body: importedData.body,
|
||||
timeout: importedData.timeout,
|
||||
followRedirects: importedData.followRedirects,
|
||||
};
|
||||
setInputs(newInputs);
|
||||
updateNodeData(id, {
|
||||
method: importedData.method,
|
||||
url: importedData.url,
|
||||
headers: importedData.headers,
|
||||
body: importedData.body,
|
||||
timeout: importedData.timeout,
|
||||
followRedirects: importedData.followRedirects,
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuickHeaders = (headers: Record<string, string>) => {
|
||||
try {
|
||||
const existingHeaders = JSON.parse(inputs.headers || "{}");
|
||||
const mergedHeaders = { ...existingHeaders, ...headers };
|
||||
const newHeadersString = JSON.stringify(mergedHeaders, null, 2);
|
||||
handleChange("headers", newHeadersString);
|
||||
} catch (error) {
|
||||
// If existing headers are invalid, just use the new ones
|
||||
const newHeadersString = JSON.stringify(headers, null, 2);
|
||||
handleChange("headers", newHeadersString);
|
||||
}
|
||||
};
|
||||
const handleQuickHeaders = useCallback(
|
||||
(headers: Record<string, string>) => {
|
||||
try {
|
||||
const existingHeaders = JSON.parse(data.headers || "{}");
|
||||
const mergedHeaders = { ...existingHeaders, ...headers };
|
||||
const newHeadersString = JSON.stringify(mergedHeaders, null, 2);
|
||||
update({ headers: newHeadersString });
|
||||
} catch (error) {
|
||||
// If existing headers are invalid, just use the new ones
|
||||
const newHeadersString = JSON.stringify(headers, null, 2);
|
||||
update({ headers: newHeadersString });
|
||||
}
|
||||
},
|
||||
[data.headers, update],
|
||||
);
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const showBodyEditor =
|
||||
inputs.method !== "GET" &&
|
||||
inputs.method !== "HEAD" &&
|
||||
inputs.method !== "DELETE";
|
||||
data.method !== "GET" && data.method !== "HEAD" && data.method !== "DELETE";
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -210,13 +180,13 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
<HelpTooltip content={methodTooltip} />
|
||||
</div>
|
||||
<Select
|
||||
value={inputs.method}
|
||||
onValueChange={(value) => handleChange("method", value)}
|
||||
value={data.method}
|
||||
onValueChange={(value) => update({ method: value })}
|
||||
disabled={!editable}
|
||||
>
|
||||
<SelectTrigger className="nopan text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<MethodBadge method={inputs.method} />
|
||||
<MethodBadge method={data.method} />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -247,13 +217,13 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
update({ url: value });
|
||||
}}
|
||||
value={inputs.url}
|
||||
value={data.url}
|
||||
placeholder={placeholders["httpRequest"]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
<UrlValidator url={inputs.url} />
|
||||
<UrlValidator url={data.url} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -279,9 +249,9 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="json"
|
||||
value={inputs.headers}
|
||||
value={data.headers}
|
||||
onChange={(value) => {
|
||||
handleChange("headers", value || "{}");
|
||||
update({ headers: value || "{}" });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
minHeight="80px"
|
||||
@@ -299,9 +269,9 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="json"
|
||||
value={inputs.body}
|
||||
value={data.body}
|
||||
onChange={(value) => {
|
||||
handleChange("body", value || "{}");
|
||||
update({ body: value || "{}" });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
minHeight="100px"
|
||||
@@ -312,10 +282,10 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
|
||||
{/* Request Preview */}
|
||||
<RequestPreview
|
||||
method={inputs.method}
|
||||
url={inputs.url}
|
||||
headers={inputs.headers}
|
||||
body={inputs.body}
|
||||
method={data.method}
|
||||
url={data.url}
|
||||
headers={data.headers}
|
||||
body={data.body}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -336,7 +306,7 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
@@ -349,9 +319,11 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
type="number"
|
||||
min="1"
|
||||
max="300"
|
||||
value={inputs.timeout}
|
||||
value={data.timeout}
|
||||
onChange={(e) =>
|
||||
handleChange("timeout", parseInt(e.target.value) || 30)
|
||||
update({
|
||||
timeout: parseInt(e.target.value) || 30,
|
||||
})
|
||||
}
|
||||
className="nopan text-xs"
|
||||
disabled={!editable}
|
||||
@@ -369,9 +341,9 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
Automatically follow HTTP redirects
|
||||
</span>
|
||||
<Switch
|
||||
checked={inputs.followRedirects}
|
||||
checked={data.followRedirects}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("followRedirects", checked)
|
||||
update({ followRedirects: checked })
|
||||
}
|
||||
disabled={!editable}
|
||||
/>
|
||||
@@ -390,9 +362,9 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("continueOnFailure", checked)
|
||||
update({ continueOnFailure: checked })
|
||||
}
|
||||
disabled={!editable}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Flippable } from "@/components/Flippable";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import {
|
||||
@@ -16,15 +16,7 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import type { LoginNode } from "./types";
|
||||
@@ -40,13 +32,12 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
|
||||
function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
const script = blockScriptStore.scripts[label];
|
||||
@@ -58,36 +49,15 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
cacheActions: data.cacheActions,
|
||||
disableCache: data.disableCache,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
completeCriterion: data.completeCriterion,
|
||||
terminateCriterion: data.terminateCriterion,
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
});
|
||||
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<LoginNode["data"]>({ id, editable });
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
// Manage flippable facing state
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
}, [data.showCode]);
|
||||
@@ -122,8 +92,8 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
totpIdentifier={data.totpIdentifier}
|
||||
totpUrl={data.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
@@ -143,10 +113,8 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
}}
|
||||
value={inputs.url}
|
||||
onChange={(value) => update({ url: value })}
|
||||
value={data.url}
|
||||
placeholder={placeholders["login"]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -161,9 +129,9 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("navigationGoal", value);
|
||||
update({ navigationGoal: value });
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
value={data.navigationGoal}
|
||||
placeholder={placeholders["login"]["navigationGoal"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -181,7 +149,7 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
updateNodeData(id, { parameterKeys: [value] });
|
||||
update({ parameterKeys: [value] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -201,16 +169,16 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
<div className="space-y-2">
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<ParametersMultiSelect
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -221,9 +189,9 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("completeCriterion", value);
|
||||
update({ completeCriterion: value });
|
||||
}}
|
||||
value={inputs.completeCriterion}
|
||||
value={data.completeCriterion}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -235,9 +203,9 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
</Label>
|
||||
</div>
|
||||
<RunEngineSelector
|
||||
value={inputs.engine}
|
||||
value={data.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
update({ engine: value });
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
@@ -256,13 +224,13 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
placeholder={placeholders["login"]["maxStepsOverride"]}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
value={data.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
update({ maxStepsOverride: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -277,29 +245,28 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
handleChange("errorCodeMapping", value);
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
@@ -319,22 +286,22 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("continueOnFailure", checked);
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisableCache
|
||||
cacheActions={inputs.cacheActions}
|
||||
disableCache={inputs.disableCache}
|
||||
cacheActions={data.cacheActions}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
@@ -350,9 +317,9 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
update({ totpIdentifier: value });
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
value={data.totpIdentifier ?? ""}
|
||||
placeholder={placeholders["login"]["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -369,9 +336,9 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
update({ totpVerificationUrl: value });
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
value={data.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["login"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -2,17 +2,10 @@ import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useNodes } from "@xyflow/react";
|
||||
import { AppNode } from "..";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import type { LoopNode } from "./types";
|
||||
import { useState } from "react";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { getLoopNodeWidth } from "../../workflowEditorUtils";
|
||||
@@ -21,9 +14,9 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const node = nodes.find((n) => n.id === id);
|
||||
if (!node) {
|
||||
@@ -38,13 +31,10 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
loopVariableReference: data.loopVariableReference,
|
||||
});
|
||||
|
||||
const update = useUpdate<LoopNode["data"]>({ id, editable });
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const children = nodes.filter((node) => node.parentId === id);
|
||||
|
||||
const furthestDownChild: Node | null = children.reduce(
|
||||
(acc, child) => {
|
||||
if (!acc) {
|
||||
@@ -64,13 +54,6 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
24;
|
||||
|
||||
const loopNodeWidth = getLoopNodeWidth(node, nodes);
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -127,9 +110,9 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
</div>
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
value={inputs.loopVariableReference}
|
||||
value={data.loopVariableReference}
|
||||
onChange={(value) => {
|
||||
handleChange("loopVariableReference", value);
|
||||
update({ loopVariableReference: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -141,7 +124,10 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
checked={data.completeIfEmpty}
|
||||
disabled={!data.editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("completeIfEmpty", checked);
|
||||
update({
|
||||
completeIfEmpty:
|
||||
checked === "indeterminate" ? false : checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label className="text-xs text-slate-300">
|
||||
@@ -155,7 +141,10 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
checked={data.continueOnFailure}
|
||||
disabled={!data.editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("continueOnFailure", checked);
|
||||
update({
|
||||
continueOnFailure:
|
||||
checked === "indeterminate" ? false : checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label className="text-xs text-slate-300">
|
||||
|
||||
@@ -6,6 +6,7 @@ export type LoopNodeData = NodeBaseData & {
|
||||
loopValue: string;
|
||||
loopVariableReference: string;
|
||||
completeIfEmpty: boolean;
|
||||
continueOnFailure: boolean;
|
||||
};
|
||||
|
||||
export type LoopNode = Node<LoopNodeData, "loop">;
|
||||
|
||||
@@ -18,14 +18,7 @@ import { useRerender } from "@/hooks/useRerender";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
@@ -41,12 +34,12 @@ import { useParams } from "react-router-dom";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
|
||||
function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
@@ -59,38 +52,11 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
const [inputs, setInputs] = useState({
|
||||
allowDownloads: data.allowDownloads,
|
||||
cacheActions: data.cacheActions,
|
||||
disableCache: data.disableCache,
|
||||
completeCriterion: data.completeCriterion,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
downloadSuffix: data.downloadSuffix,
|
||||
engine: data.engine,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
includeActionHistoryInVerification: data.includeActionHistoryInVerification,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
model: data.model,
|
||||
navigationGoal: data.navigationGoal,
|
||||
terminateCriterion: data.terminateCriterion,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
url: data.url,
|
||||
});
|
||||
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const update = useUpdate<NavigationNode["data"]>({ id, editable });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -127,8 +93,8 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
totpIdentifier={data.totpIdentifier}
|
||||
totpUrl={data.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div
|
||||
@@ -153,9 +119,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
update({ url: value });
|
||||
}}
|
||||
value={inputs.url}
|
||||
value={data.url}
|
||||
placeholder={placeholders["navigation"]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -172,9 +138,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("navigationGoal", value);
|
||||
update({ navigationGoal: value });
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
value={data.navigationGoal}
|
||||
placeholder={placeholders["navigation"]["navigationGoal"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -209,7 +175,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -220,18 +186,18 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("completeCriterion", value);
|
||||
update({ completeCriterion: value });
|
||||
}}
|
||||
value={inputs.completeCriterion}
|
||||
value={data.completeCriterion}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -241,9 +207,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
</Label>
|
||||
</div>
|
||||
<RunEngineSelector
|
||||
value={inputs.engine}
|
||||
value={data.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
update({ engine: value });
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
@@ -264,13 +230,13 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
value={data.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
update({ maxStepsOverride: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -287,29 +253,28 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
handleChange("errorCodeMapping", value);
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
@@ -333,12 +298,11 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.includeActionHistoryInVerification}
|
||||
checked={data.includeActionHistoryInVerification}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"includeActionHistoryInVerification",
|
||||
checked,
|
||||
);
|
||||
update({
|
||||
includeActionHistoryInVerification: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -356,22 +320,22 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("continueOnFailure", checked);
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisableCache
|
||||
cacheActions={inputs.cacheActions}
|
||||
disableCache={inputs.disableCache}
|
||||
cacheActions={data.cacheActions}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
@@ -388,9 +352,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.allowDownloads}
|
||||
checked={data.allowDownloads}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("allowDownloads", checked);
|
||||
update({ allowDownloads: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -409,9 +373,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
type="text"
|
||||
placeholder={placeholders["navigation"]["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
value={data.downloadSuffix ?? ""}
|
||||
onChange={(value) => {
|
||||
handleChange("downloadSuffix", value);
|
||||
update({ downloadSuffix: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -428,9 +392,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
update({ totpIdentifier: value });
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
value={data.totpIdentifier ?? ""}
|
||||
placeholder={placeholders["navigation"]["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -447,9 +411,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
update({ totpVerificationUrl: value });
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
value={data.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { dataSchemaExampleForFileExtraction } from "../types";
|
||||
import { type PDFParserNode } from "./types";
|
||||
@@ -13,10 +12,10 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
|
||||
function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable, label } = data;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
@@ -26,21 +25,8 @@ function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
fileUrl: data.fileUrl,
|
||||
jsonSchema: data.jsonSchema,
|
||||
model: data.model,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<PDFParserNode["data"]>({ id, editable });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -89,26 +75,26 @@ function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
|
||||
</div>
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
value={inputs.fileUrl}
|
||||
value={data.fileUrl}
|
||||
onChange={(value) => {
|
||||
handleChange("fileUrl", value);
|
||||
update({ fileUrl: value });
|
||||
}}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<WorkflowDataSchemaInputGroup
|
||||
exampleValue={dataSchemaExampleForFileExtraction}
|
||||
value={inputs.jsonSchema}
|
||||
value={data.jsonSchema}
|
||||
onChange={(value) => {
|
||||
handleChange("jsonSchema", value);
|
||||
update({ jsonSchema: value });
|
||||
}}
|
||||
suggestionContext={{}}
|
||||
/>
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { type SendEmailNode } from "./types";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
@@ -13,9 +12,9 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable, label } = data;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
@@ -25,22 +24,8 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
recipients: data.recipients,
|
||||
subject: data.subject,
|
||||
body: data.body,
|
||||
fileAttachments: data.fileAttachments,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<SendEmailNode["data"]>({ id, editable });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -86,9 +71,9 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("recipients", value);
|
||||
update({ recipients: value });
|
||||
}}
|
||||
value={inputs.recipients}
|
||||
value={data.recipients}
|
||||
placeholder="example@gmail.com, example2@gmail.com..."
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -99,9 +84,9 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("subject", value);
|
||||
update({ subject: value });
|
||||
}}
|
||||
value={inputs.subject}
|
||||
value={data.subject}
|
||||
placeholder="What is the gist?"
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -111,9 +96,9 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("body", value);
|
||||
update({ body: value });
|
||||
}}
|
||||
value={inputs.body}
|
||||
value={data.body}
|
||||
placeholder="What would you like to say?"
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -128,9 +113,9 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
</div>
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
value={inputs.fileAttachments}
|
||||
value={data.fileAttachments}
|
||||
onChange={(value) => {
|
||||
handleChange("fileAttachments", value);
|
||||
update({ fileAttachments: value });
|
||||
}}
|
||||
disabled
|
||||
className="nopan text-xs"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import type { StartNode } from "./types";
|
||||
import {
|
||||
@@ -16,16 +15,13 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ProxySelector } from "@/components/ProxySelector";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ModelsResponse } from "@/api/types";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { WorkflowModel } from "@/routes/workflows/types/workflowTypes";
|
||||
import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "../Taskv2Node/types";
|
||||
@@ -41,78 +37,58 @@ import { Flippable } from "@/components/Flippable";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
interface StartSettings {
|
||||
webhookCallbackUrl: string;
|
||||
proxyLocation: ProxyLocation;
|
||||
persistBrowserSession: boolean;
|
||||
model: WorkflowModel | null;
|
||||
maxScreenshotScrollingTimes: number | null;
|
||||
extraHttpHeaders: string | Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
const workflowSettingsStore = useWorkflowSettingsStore();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const reactFlowInstance = useReactFlow();
|
||||
|
||||
const { data: availableModels } = useQuery<ModelsResponse>({
|
||||
queryKey: ["models"],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
|
||||
return client.get("/models").then((res) => res.data);
|
||||
},
|
||||
});
|
||||
|
||||
const modelNames = availableModels?.models ?? {};
|
||||
const firstKey = Object.keys(modelNames)[0];
|
||||
const workflowModel: WorkflowModel | null = firstKey
|
||||
? { model_name: modelNames[firstKey] || "" }
|
||||
: null;
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
webhookCallbackUrl: data.withWorkflowSettings
|
||||
? data.webhookCallbackUrl
|
||||
: "",
|
||||
proxyLocation: data.withWorkflowSettings
|
||||
? data.proxyLocation
|
||||
: ProxyLocation.Residential,
|
||||
persistBrowserSession: data.withWorkflowSettings
|
||||
? data.persistBrowserSession
|
||||
: false,
|
||||
model: data.withWorkflowSettings ? data.model : workflowModel,
|
||||
maxScreenshotScrolls: data.withWorkflowSettings
|
||||
? data.maxScreenshotScrolls
|
||||
: null,
|
||||
extraHttpHeaders: data.withWorkflowSettings ? data.extraHttpHeaders : null,
|
||||
runWith: data.withWorkflowSettings ? data.runWith : "agent",
|
||||
scriptCacheKey: data.withWorkflowSettings ? data.scriptCacheKey : null,
|
||||
aiFallback: data.withWorkflowSettings ? data.aiFallback : true,
|
||||
runSequentially: data.withWorkflowSettings ? data.runSequentially : false,
|
||||
sequentialKey: data.withWorkflowSettings ? data.sequentialKey : null,
|
||||
});
|
||||
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const script = blockScriptStore.scripts.__start_block__;
|
||||
const rerender = useRerender({ prefix: "accordion" });
|
||||
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
|
||||
|
||||
const makeStartSettings = (data: StartNode["data"]): StartSettings => {
|
||||
return {
|
||||
webhookCallbackUrl: data.withWorkflowSettings
|
||||
? data.webhookCallbackUrl
|
||||
: "",
|
||||
proxyLocation: data.withWorkflowSettings
|
||||
? data.proxyLocation
|
||||
: ProxyLocation.Residential,
|
||||
persistBrowserSession: data.withWorkflowSettings
|
||||
? data.persistBrowserSession
|
||||
: false,
|
||||
model: data.withWorkflowSettings ? data.model : null,
|
||||
maxScreenshotScrollingTimes: data.withWorkflowSettings
|
||||
? data.maxScreenshotScrolls
|
||||
: null,
|
||||
extraHttpHeaders: data.withWorkflowSettings
|
||||
? data.extraHttpHeaders
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
const update = useUpdate<StartNode["data"]>({ id, editable: true });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
}, [data.showCode]);
|
||||
|
||||
useEffect(() => {
|
||||
workflowSettingsStore.setWorkflowSettings(inputs);
|
||||
workflowSettingsStore.setWorkflowSettings(makeStartSettings(data));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputs]);
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputs[key as keyof typeof inputs] === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
function nodeIsFlippable(node: Node) {
|
||||
return (
|
||||
@@ -183,9 +159,9 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<div className="space-y-2">
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -195,13 +171,12 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<HelpTooltip content="The URL of a webhook endpoint to send the workflow results" />
|
||||
</div>
|
||||
<Input
|
||||
value={inputs.webhookCallbackUrl}
|
||||
value={data.webhookCallbackUrl}
|
||||
placeholder="https://"
|
||||
onChange={(event) => {
|
||||
handleChange(
|
||||
"webhookCallbackUrl",
|
||||
event.target.value,
|
||||
);
|
||||
update({
|
||||
webhookCallbackUrl: event.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -211,9 +186,9 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<HelpTooltip content="Route Skyvern through one of our available proxies." />
|
||||
</div>
|
||||
<ProxySelector
|
||||
value={inputs.proxyLocation}
|
||||
value={data.proxyLocation}
|
||||
onChange={(value) => {
|
||||
handleChange("proxyLocation", value);
|
||||
update({ proxyLocation: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -225,9 +200,9 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<HelpTooltip content="If code has been generated and saved from a previously successful run, set this to 'Code' to use that code when executing the workflow. To avoid using code, set this to 'Skyvern Agent'." />
|
||||
</div>
|
||||
<Select
|
||||
value={inputs.runWith ?? "agent"}
|
||||
value={data.runWith ?? "agent"}
|
||||
onValueChange={(value) => {
|
||||
handleChange("runWith", value);
|
||||
update({ runWith: value });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-48">
|
||||
@@ -247,9 +222,9 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<HelpTooltip content="If a run with code fails, fallback to AI and regenerate the code." />
|
||||
<Switch
|
||||
className="ml-auto"
|
||||
checked={inputs.aiFallback}
|
||||
checked={data.aiFallback}
|
||||
onCheckedChange={(value) => {
|
||||
handleChange("aiFallback", value);
|
||||
update({ aiFallback: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -263,9 +238,9 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
const v = value.length ? value : null;
|
||||
handleChange("scriptCacheKey", v);
|
||||
update({ scriptCacheKey: v });
|
||||
}}
|
||||
value={inputs.scriptCacheKey ?? ""}
|
||||
value={data.scriptCacheKey ?? ""}
|
||||
placeholder={placeholders["scripts"]["scriptKey"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -280,14 +255,14 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<HelpTooltip content="Run the workflow in a sequential order" />
|
||||
<Switch
|
||||
className="ml-auto"
|
||||
checked={inputs.runSequentially}
|
||||
checked={data.runSequentially}
|
||||
onCheckedChange={(value) => {
|
||||
handleChange("runSequentially", value);
|
||||
update({ runSequentially: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{inputs.runSequentially && (
|
||||
{data.runSequentially && (
|
||||
<div className="flex flex-col gap-4 rounded-md bg-slate-elevation4 p-4 pl-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
@@ -298,9 +273,9 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
const v = value.length ? value : null;
|
||||
handleChange("sequentialKey", v);
|
||||
update({ sequentialKey: v });
|
||||
}}
|
||||
value={inputs.sequentialKey ?? ""}
|
||||
value={data.sequentialKey ?? ""}
|
||||
placeholder={placeholders["sequentialKey"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -314,9 +289,9 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<HelpTooltip content="Persist session information across workflow runs" />
|
||||
<Switch
|
||||
className="ml-auto"
|
||||
checked={inputs.persistBrowserSession}
|
||||
checked={data.persistBrowserSession}
|
||||
onCheckedChange={(value) => {
|
||||
handleChange("persistBrowserSession", value);
|
||||
update({ persistBrowserSession: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -327,10 +302,28 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
<HelpTooltip content="Specify some self-defined HTTP requests headers" />
|
||||
</div>
|
||||
<KeyValueInput
|
||||
value={inputs.extraHttpHeaders ?? null}
|
||||
onChange={(val) =>
|
||||
handleChange("extraHttpHeaders", val || "{}")
|
||||
value={
|
||||
data.extraHttpHeaders &&
|
||||
typeof data.extraHttpHeaders === "object"
|
||||
? JSON.stringify(data.extraHttpHeaders)
|
||||
: data.extraHttpHeaders ?? null
|
||||
}
|
||||
onChange={(val) => {
|
||||
const v =
|
||||
val === null
|
||||
? "{}"
|
||||
: typeof val === "string"
|
||||
? val.trim()
|
||||
: JSON.stringify(val);
|
||||
|
||||
const normalized = v === "" ? "{}" : v;
|
||||
|
||||
if (normalized === data.extraHttpHeaders) {
|
||||
return;
|
||||
}
|
||||
|
||||
update({ extraHttpHeaders: normalized });
|
||||
}}
|
||||
addButtonText="Add Header"
|
||||
/>
|
||||
</div>
|
||||
@@ -342,7 +335,7 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
value={inputs.maxScreenshotScrolls ?? ""}
|
||||
value={data.maxScreenshotScrolls ?? ""}
|
||||
placeholder={`Default: ${MAX_SCREENSHOT_SCROLLS_DEFAULT}`}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
@@ -350,7 +343,7 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
|
||||
handleChange("maxScreenshotScrolls", value);
|
||||
update({ maxScreenshotScrolls: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ export type WorkflowStartNodeData = {
|
||||
persistBrowserSession: boolean;
|
||||
model: WorkflowModel | null;
|
||||
maxScreenshotScrolls: number | null;
|
||||
extraHttpHeaders: string | null;
|
||||
extraHttpHeaders: string | Record<string, unknown> | null;
|
||||
editable: boolean;
|
||||
runWith: string | null;
|
||||
scriptCacheKey: string | null;
|
||||
|
||||
@@ -17,14 +17,7 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { AppNode } from "..";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
@@ -41,12 +34,12 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
|
||||
function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
@@ -59,41 +52,12 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
dataExtractionGoal: data.dataExtractionGoal,
|
||||
completeCriterion: data.completeCriterion,
|
||||
terminateCriterion: data.terminateCriterion,
|
||||
dataSchema: data.dataSchema,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
allowDownloads: data.allowDownloads,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
cacheActions: data.cacheActions,
|
||||
disableCache: data.disableCache,
|
||||
downloadSuffix: data.downloadSuffix,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
includeActionHistoryInVerification: data.includeActionHistoryInVerification,
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const update = useUpdate<TaskNode["data"]>({ id, editable });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -129,8 +93,8 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
totpIdentifier={data.totpIdentifier}
|
||||
totpUrl={data.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<Accordion
|
||||
@@ -158,9 +122,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
update({ url: value });
|
||||
}}
|
||||
value={inputs.url}
|
||||
value={data.url}
|
||||
placeholder={placeholders["task"]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -175,9 +139,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("navigationGoal", value);
|
||||
update({ navigationGoal: value });
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
value={data.navigationGoal}
|
||||
placeholder={placeholders["task"]["navigationGoal"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -187,7 +151,7 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -210,9 +174,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("dataExtractionGoal", value);
|
||||
update({ dataExtractionGoal: value });
|
||||
}}
|
||||
value={inputs.dataExtractionGoal}
|
||||
value={data.dataExtractionGoal}
|
||||
placeholder={placeholders["task"]["dataExtractionGoal"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -220,13 +184,13 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
<WorkflowDataSchemaInputGroup
|
||||
exampleValue={dataSchemaExampleValue}
|
||||
onChange={(value) => {
|
||||
handleChange("dataSchema", value);
|
||||
update({ dataSchema: value });
|
||||
}}
|
||||
value={inputs.dataSchema}
|
||||
value={data.dataSchema}
|
||||
suggestionContext={{
|
||||
data_extraction_goal: inputs.dataExtractionGoal,
|
||||
current_schema: inputs.dataSchema,
|
||||
navigation_goal: inputs.navigationGoal,
|
||||
data_extraction_goal: data.dataExtractionGoal,
|
||||
current_schema: data.dataSchema,
|
||||
navigation_goal: data.navigationGoal,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -243,18 +207,18 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("completeCriterion", value);
|
||||
update({ completeCriterion: value });
|
||||
}}
|
||||
value={inputs.completeCriterion}
|
||||
value={data.completeCriterion}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -264,9 +228,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
</Label>
|
||||
</div>
|
||||
<RunEngineSelector
|
||||
value={inputs.engine}
|
||||
value={data.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
update({ engine: value });
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
@@ -285,13 +249,13 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
placeholder={placeholders["task"]["maxStepsOverride"]}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
value={data.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
update({ maxStepsOverride: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -306,29 +270,28 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
handleChange("errorCodeMapping", value);
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
@@ -352,12 +315,11 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.includeActionHistoryInVerification}
|
||||
checked={data.includeActionHistoryInVerification}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"includeActionHistoryInVerification",
|
||||
checked,
|
||||
);
|
||||
update({
|
||||
includeActionHistoryInVerification: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -373,22 +335,22 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("continueOnFailure", checked);
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisableCache
|
||||
cacheActions={inputs.cacheActions}
|
||||
disableCache={inputs.disableCache}
|
||||
cacheActions={data.cacheActions}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
@@ -403,9 +365,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.allowDownloads}
|
||||
checked={data.allowDownloads}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("allowDownloads", checked);
|
||||
update({ allowDownloads: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -424,9 +386,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
type="text"
|
||||
placeholder={placeholders["task"]["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
value={data.downloadSuffix ?? ""}
|
||||
onChange={(value) => {
|
||||
handleChange("downloadSuffix", value);
|
||||
update({ downloadSuffix: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -443,9 +405,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
update({ totpIdentifier: value });
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
value={data.totpIdentifier ?? ""}
|
||||
placeholder={placeholders["task"]["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -462,9 +424,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
update({ totpVerificationUrl: value });
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
value={data.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { MAX_STEPS_DEFAULT, type Taskv2Node } from "./types";
|
||||
@@ -24,6 +24,7 @@ import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
@@ -38,30 +39,12 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const script = blockScriptStore.scripts[label];
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
prompt: data.prompt,
|
||||
url: data.url,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
maxSteps: data.maxSteps,
|
||||
disableCache: data.disableCache,
|
||||
model: data.model,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const update = useUpdate<Taskv2Node["data"]>({ id, editable });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -96,8 +79,8 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
totpIdentifier={data.totpIdentifier}
|
||||
totpUrl={data.totpVerificationUrl}
|
||||
type="task_v2" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
@@ -113,9 +96,9 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("prompt", value);
|
||||
update({ prompt: value });
|
||||
}}
|
||||
value={inputs.prompt}
|
||||
value={data.prompt}
|
||||
placeholder={placeholders[type]["prompt"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -126,9 +109,9 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
update({ url: value });
|
||||
}}
|
||||
value={inputs.url}
|
||||
value={data.url}
|
||||
placeholder={placeholders[type]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -148,9 +131,9 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
<div className="space-y-4">
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
@@ -166,19 +149,21 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
className="nopan text-xs"
|
||||
value={data.maxSteps ?? MAX_STEPS_DEFAULT}
|
||||
onChange={(event) => {
|
||||
handleChange("maxSteps", Number(event.target.value));
|
||||
update({
|
||||
maxSteps: Number(event.target.value),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<DisableCache
|
||||
disableCache={inputs.disableCache}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
@@ -194,9 +179,9 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
update({ totpIdentifier: value });
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
value={data.totpIdentifier ?? ""}
|
||||
placeholder={placeholders["navigation"]["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -213,9 +198,9 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
update({ totpVerificationUrl: value });
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
value={data.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Taskv2NodeData = NodeBaseData & {
|
||||
totpVerificationUrl: string | null;
|
||||
totpIdentifier: string | null;
|
||||
maxSteps: number | null;
|
||||
cacheActions: boolean;
|
||||
disableCache: boolean;
|
||||
maxScreenshotScrolls: number | null;
|
||||
};
|
||||
@@ -27,6 +28,7 @@ export const taskv2NodeDefaultData: Taskv2NodeData = {
|
||||
totpIdentifier: null,
|
||||
totpVerificationUrl: null,
|
||||
maxSteps: MAX_STEPS_DEFAULT,
|
||||
cacheActions: false,
|
||||
disableCache: false,
|
||||
model: null,
|
||||
maxScreenshotScrolls: null,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { type TextPromptNode } from "./types";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
@@ -15,9 +14,9 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable, label } = data;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
@@ -27,21 +26,8 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
prompt: data.prompt,
|
||||
jsonSchema: data.jsonSchema,
|
||||
model: data.model,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<TextPromptNode["data"]>({ id, editable });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -91,9 +77,9 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("prompt", value);
|
||||
update({ prompt: value });
|
||||
}}
|
||||
value={inputs.prompt}
|
||||
value={data.prompt}
|
||||
placeholder="What do you want to generate?"
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
@@ -101,16 +87,16 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
<Separator />
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<WorkflowDataSchemaInputGroup
|
||||
exampleValue={dataSchemaExampleValue}
|
||||
value={inputs.jsonSchema}
|
||||
value={data.jsonSchema}
|
||||
onChange={(value) => {
|
||||
handleChange("jsonSchema", value);
|
||||
update({ jsonSchema: value });
|
||||
}}
|
||||
suggestionContext={{}}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { Flippable } from "@/components/Flippable";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import type { URLNode } from "./types";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { useState } from "react";
|
||||
@@ -14,9 +14,9 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
function URLNode({ id, data, type }: NodeProps<URLNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
@@ -30,18 +30,7 @@ function URLNode({ id, data, type }: NodeProps<URLNode>) {
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
const update = useUpdate<URLNode["data"]>({ id, editable });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -94,9 +83,9 @@ function URLNode({ id, data, type }: NodeProps<URLNode>) {
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
update({ url: value });
|
||||
}}
|
||||
value={inputs.url}
|
||||
value={data.url}
|
||||
placeholder={placeholders[type]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -15,14 +15,7 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
Position,
|
||||
useEdges,
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
@@ -37,12 +30,12 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
|
||||
function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, label } = data;
|
||||
@@ -55,28 +48,12 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
completeCriterion: data.completeCriterion,
|
||||
terminateCriterion: data.terminateCriterion,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
model: data.model,
|
||||
disableCache: data.disableCache,
|
||||
});
|
||||
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<ValidationNode["data"]>({ id, editable });
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -128,9 +105,9 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("completeCriterion", value);
|
||||
update({ completeCriterion: value });
|
||||
}}
|
||||
value={inputs.completeCriterion}
|
||||
value={data.completeCriterion}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -139,9 +116,9 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("terminateCriterion", value);
|
||||
update({ terminateCriterion: value });
|
||||
}}
|
||||
value={inputs.terminateCriterion}
|
||||
value={data.terminateCriterion}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -160,16 +137,16 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
<div className="space-y-2">
|
||||
<ModelSelector
|
||||
className="nopan mr-[1px] w-52 text-xs"
|
||||
value={inputs.model}
|
||||
value={data.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
update({ model: value });
|
||||
}}
|
||||
/>
|
||||
<ParametersMultiSelect
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
update({ parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -186,35 +163,34 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("errorCodeMapping", value);
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
@@ -241,19 +217,19 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
updateNodeData(id, { continueOnFailure: checked });
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DisableCache
|
||||
disableCache={inputs.disableCache}
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
onCacheActionsChange={(cacheActions) => {
|
||||
handleChange("cacheActions", cacheActions);
|
||||
update({ cacheActions });
|
||||
}}
|
||||
onDisableCacheChange={(disableCache) => {
|
||||
handleChange("disableCache", disableCache);
|
||||
update({ disableCache });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ export type ValidationNodeData = NodeBaseData & {
|
||||
terminateCriterion: string;
|
||||
errorCodeMapping: string;
|
||||
parameterKeys: Array<string>;
|
||||
cacheActions?: boolean;
|
||||
disableCache: boolean;
|
||||
};
|
||||
|
||||
@@ -20,6 +21,7 @@ export const validationNodeDefaultData: ValidationNodeData = {
|
||||
continueOnFailure: false,
|
||||
editable: true,
|
||||
parameterKeys: [],
|
||||
cacheActions: false,
|
||||
disableCache: false,
|
||||
model: null,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import type { WaitNode } from "./types";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
@@ -11,9 +10,9 @@ import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
function WaitNode({ id, data, type }: NodeProps<WaitNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable, label } = data;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
@@ -23,20 +22,10 @@ function WaitNode({ id, data, type }: NodeProps<WaitNode>) {
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const thisBlockIsPlaying =
|
||||
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
|
||||
const [inputs, setInputs] = useState({
|
||||
waitInSeconds: data.waitInSeconds,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const update = useUpdate<WaitNode["data"]>({ id, editable });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
@@ -84,9 +73,9 @@ function WaitNode({ id, data, type }: NodeProps<WaitNode>) {
|
||||
) : null}
|
||||
</div>
|
||||
<Input
|
||||
value={inputs.waitInSeconds}
|
||||
value={data.waitInSeconds}
|
||||
onChange={(event) => {
|
||||
handleChange("waitInSeconds", event.target.value);
|
||||
update({ waitInSeconds: event.target.value });
|
||||
}}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
|
||||
@@ -91,7 +91,9 @@ const getPayload = (opts: {
|
||||
extraHttpHeaders =
|
||||
opts.workflowSettings.extraHttpHeaders === null
|
||||
? null
|
||||
: JSON.parse(opts.workflowSettings.extraHttpHeaders);
|
||||
: typeof opts.workflowSettings.extraHttpHeaders === "object"
|
||||
? opts.workflowSettings.extraHttpHeaders
|
||||
: JSON.parse(opts.workflowSettings.extraHttpHeaders);
|
||||
} catch (e: unknown) {
|
||||
toast({
|
||||
variant: "warning",
|
||||
|
||||
38
skyvern-frontend/src/routes/workflows/editor/useUpdate.ts
Normal file
38
skyvern-frontend/src/routes/workflows/editor/useUpdate.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type UseUpdateOptions = {
|
||||
id: string;
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A reusable hook for updating node data in React Flow.
|
||||
*
|
||||
* @template T - The root data type that extends Record<string, unknown>
|
||||
* @param options - Configuration object containing node id and editable flag
|
||||
* @returns An update function that accepts partial updates of type T
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const update = useUpdate<WaitNode["data"]>({ id, editable });
|
||||
* update({ waitInSeconds: "5" });
|
||||
* ```
|
||||
*/
|
||||
export function useUpdate<T extends Record<string, unknown>>({
|
||||
id,
|
||||
editable,
|
||||
}: UseUpdateOptions) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
|
||||
const update = useCallback(
|
||||
(updates: Partial<T>) => {
|
||||
if (!editable) return;
|
||||
|
||||
updateNodeData(id, updates);
|
||||
},
|
||||
[id, editable, updateNodeData],
|
||||
);
|
||||
|
||||
return update;
|
||||
}
|
||||
@@ -264,6 +264,7 @@ function convertToNode(
|
||||
prompt: block.prompt,
|
||||
url: block.url ?? "",
|
||||
maxSteps: block.max_steps,
|
||||
cacheActions: block.cache_actions ?? false,
|
||||
disableCache: block.disable_cache ?? false,
|
||||
totpIdentifier: block.totp_identifier,
|
||||
totpVerificationUrl: block.totp_verification_url,
|
||||
@@ -1457,7 +1458,10 @@ function getWorkflowSettings(nodes: Array<AppNode>): WorkflowSettings {
|
||||
webhookCallbackUrl: data.webhookCallbackUrl,
|
||||
model: data.model,
|
||||
maxScreenshotScrolls: data.maxScreenshotScrolls,
|
||||
extraHttpHeaders: data.extraHttpHeaders,
|
||||
extraHttpHeaders:
|
||||
data.extraHttpHeaders && typeof data.extraHttpHeaders === "object"
|
||||
? JSON.stringify(data.extraHttpHeaders)
|
||||
: data.extraHttpHeaders,
|
||||
runWith: data.runWith,
|
||||
scriptCacheKey: data.scriptCacheKey,
|
||||
aiFallback: data.aiFallback,
|
||||
|
||||
@@ -318,6 +318,7 @@ export type Taskv2Block = WorkflowBlockBase & {
|
||||
totp_verification_url: string | null;
|
||||
totp_identifier: string | null;
|
||||
max_steps: number | null;
|
||||
cache_actions?: boolean;
|
||||
disable_cache: boolean;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user