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