Add Start Node to workflows (#1026)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useNodes } from "@xyflow/react";
|
||||
import { useWorkflowParametersState } from "../editor/useWorkflowParametersState";
|
||||
import { AppNode } from "../editor/nodes";
|
||||
import { AppNode, isWorkflowBlockNode } from "../editor/nodes";
|
||||
import { getOutputParameterKey } from "../editor/workflowEditorUtils";
|
||||
import {
|
||||
Select,
|
||||
@@ -22,7 +22,7 @@ function SourceParameterKeySelector({ value, onChange }: Props) {
|
||||
.filter((parameter) => parameter.parameterType !== "credential")
|
||||
.map((parameter) => parameter.key);
|
||||
const outputParameterKeys = nodes
|
||||
.filter((node) => node.type !== "nodeAdder")
|
||||
.filter(isWorkflowBlockNode)
|
||||
.map((node) => getOutputParameterKey(node.data.label));
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,18 +31,21 @@ import {
|
||||
import { WorkflowHeader } from "./WorkflowHeader";
|
||||
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
||||
import { edgeTypes } from "./edges";
|
||||
import { AppNode, nodeTypes } from "./nodes";
|
||||
import { AppNode, nodeTypes, WorkflowBlockNode } from "./nodes";
|
||||
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||
import "./reactFlowOverrideStyles.css";
|
||||
import {
|
||||
convertEchoParameters,
|
||||
createNode,
|
||||
defaultEdge,
|
||||
generateNodeLabel,
|
||||
getAdditionalParametersForEmailBlock,
|
||||
getOutputParameterKey,
|
||||
getWorkflowBlocks,
|
||||
layout,
|
||||
nodeAdderNode,
|
||||
startNode,
|
||||
} from "./workflowEditorUtils";
|
||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||
import { useBlocker, useParams } from "react-router-dom";
|
||||
@@ -139,7 +142,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export type AddNodeProps = {
|
||||
nodeType: Exclude<keyof typeof nodeTypes, "nodeAdder">;
|
||||
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
||||
previous: string | null;
|
||||
next: string | null;
|
||||
parent?: string;
|
||||
@@ -316,15 +319,11 @@ function FlowRenderer({
|
||||
|
||||
if (nodeType === "loop") {
|
||||
// when loop node is first created it needs an adder node so nodes can be added inside the loop
|
||||
newNodes.push({
|
||||
id: nanoid(),
|
||||
type: "nodeAdder",
|
||||
parentId: id,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
});
|
||||
const startNodeId = nanoid();
|
||||
const adderNodeId = nanoid();
|
||||
newNodes.push(startNode(startNodeId, id));
|
||||
newNodes.push(nodeAdderNode(adderNodeId, id));
|
||||
newEdges.push(defaultEdge(startNodeId, adderNodeId));
|
||||
}
|
||||
|
||||
const editedEdges = previous
|
||||
@@ -343,26 +342,6 @@ function FlowRenderer({
|
||||
...nodes.slice(previousNodeIndex + 1),
|
||||
];
|
||||
|
||||
if (nodes.length === 0) {
|
||||
// if there were no nodes before, add a nodeAdder node and connect it to the new node
|
||||
newNodesAfter.push({
|
||||
id: `${id}-nodeAdder`,
|
||||
type: "nodeAdder",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
});
|
||||
newEdges.push({
|
||||
id: `edge-0-${id}`,
|
||||
type: "default",
|
||||
source: id,
|
||||
target: `${id}-nodeAdder`,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
setHasChanges(true);
|
||||
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
||||
}
|
||||
@@ -619,16 +598,6 @@ function FlowRenderer({
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
{nodes.length === 0 && (
|
||||
<Panel position="top-right">
|
||||
<WorkflowNodeLibraryPanel
|
||||
onNodeClick={(props) => {
|
||||
addNode(props);
|
||||
}}
|
||||
first
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</DeleteNodeCallbackContext.Provider>
|
||||
</WorkflowParametersStateContext.Provider>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useParams } from "react-router-dom";
|
||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||
import { FlowRenderer } from "./FlowRenderer";
|
||||
import { getElements } from "./workflowEditorUtils";
|
||||
import { LogoMinimized } from "@/components/LogoMinimized";
|
||||
|
||||
function WorkflowEditor() {
|
||||
const { workflowPermanentId } = useParams();
|
||||
@@ -29,7 +30,7 @@ function WorkflowEditor() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
Loading...
|
||||
<LogoMinimized />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
|
||||
function StartNode() {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] rounded-lg bg-slate-elevation3 px-6 py-4 text-center">
|
||||
Start
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { StartNode };
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type StartNodeData = Record<string, never>;
|
||||
|
||||
export type StartNode = Node<StartNodeData, "start">;
|
||||
@@ -20,8 +20,6 @@ export type TaskNodeData = NodeBaseData & {
|
||||
|
||||
export type TaskNode = Node<TaskNodeData, "task">;
|
||||
|
||||
export type TaskNodeDisplayMode = "basic" | "advanced";
|
||||
|
||||
export const taskNodeDefaultData: TaskNodeData = {
|
||||
url: "",
|
||||
navigationGoal: "",
|
||||
|
||||
@@ -17,8 +17,12 @@ import type { DownloadNode } from "./DownloadNode/types";
|
||||
import { DownloadNode as DownloadNodeComponent } from "./DownloadNode/DownloadNode";
|
||||
import type { NodeAdderNode } from "./NodeAdderNode/types";
|
||||
import { NodeAdderNode as NodeAdderNodeComponent } from "./NodeAdderNode/NodeAdderNode";
|
||||
import { StartNode as StartNodeComponent } from "./StartNode/StartNode";
|
||||
import type { StartNode } from "./StartNode/types";
|
||||
|
||||
export type AppNode =
|
||||
export type UtilityNode = StartNode | NodeAdderNode;
|
||||
|
||||
export type WorkflowBlockNode =
|
||||
| LoopNode
|
||||
| TaskNode
|
||||
| TextPromptNode
|
||||
@@ -26,8 +30,17 @@ export type AppNode =
|
||||
| CodeBlockNode
|
||||
| FileParserNode
|
||||
| UploadNode
|
||||
| DownloadNode
|
||||
| NodeAdderNode;
|
||||
| DownloadNode;
|
||||
|
||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||
return node.type === "nodeAdder" || node.type === "start";
|
||||
}
|
||||
|
||||
export function isWorkflowBlockNode(node: AppNode): node is WorkflowBlockNode {
|
||||
return node.type !== "nodeAdder" && node.type !== "start";
|
||||
}
|
||||
|
||||
export type AppNode = UtilityNode | WorkflowBlockNode;
|
||||
|
||||
export const nodeTypes = {
|
||||
loop: memo(LoopNodeComponent),
|
||||
@@ -39,4 +52,5 @@ export const nodeTypes = {
|
||||
upload: memo(UploadNodeComponent),
|
||||
download: memo(DownloadNodeComponent),
|
||||
nodeAdder: memo(NodeAdderNodeComponent),
|
||||
start: memo(StartNodeComponent),
|
||||
};
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
UpdateIcon,
|
||||
UploadIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { nodeTypes } from "../nodes";
|
||||
import { WorkflowBlockNode } from "../nodes";
|
||||
import { AddNodeProps } from "../FlowRenderer";
|
||||
|
||||
const nodeLibraryItems: Array<{
|
||||
nodeType: Exclude<keyof typeof nodeTypes, "nodeAdder">;
|
||||
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
||||
icon: JSX.Element;
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
SMTP_USERNAME_PARAMETER_KEY,
|
||||
} from "./constants";
|
||||
import { ParametersState } from "./FlowRenderer";
|
||||
import { AppNode, nodeTypes } from "./nodes";
|
||||
import { AppNode, isWorkflowBlockNode, WorkflowBlockNode } from "./nodes";
|
||||
import { codeBlockNodeDefaultData } from "./nodes/CodeBlockNode/types";
|
||||
import { downloadNodeDefaultData } from "./nodes/DownloadNode/types";
|
||||
import { fileParserNodeDefaultData } from "./nodes/FileParserNode/types";
|
||||
@@ -49,6 +49,7 @@ import { taskNodeDefaultData } from "./nodes/TaskNode/types";
|
||||
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
|
||||
import { NodeBaseData } from "./nodes/types";
|
||||
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
||||
import { StartNode } from "./nodes/StartNode/types";
|
||||
|
||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||
|
||||
@@ -313,6 +314,61 @@ function getNodeData(
|
||||
return data;
|
||||
}
|
||||
|
||||
export function defaultEdge(source: string, target: string) {
|
||||
return {
|
||||
id: nanoid(),
|
||||
type: "default",
|
||||
source,
|
||||
target,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function edgeWithAddButton(source: string, target: string) {
|
||||
return {
|
||||
id: nanoid(),
|
||||
type: "edgeWithAddButton",
|
||||
source,
|
||||
target,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
zIndex: REACT_FLOW_EDGE_Z_INDEX,
|
||||
};
|
||||
}
|
||||
|
||||
export function startNode(id: string, parentId?: string): StartNode {
|
||||
const node: StartNode = {
|
||||
id,
|
||||
type: "start",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
};
|
||||
if (parentId) {
|
||||
node.parentId = parentId;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
export function nodeAdderNode(id: string, parentId?: string): NodeAdderNode {
|
||||
const node: NodeAdderNode = {
|
||||
id,
|
||||
type: "nodeAdder",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
};
|
||||
if (parentId) {
|
||||
node.parentId = parentId;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function getElements(blocks: Array<WorkflowBlock>): {
|
||||
nodes: Array<AppNode>;
|
||||
edges: Array<Edge>;
|
||||
@@ -331,64 +387,47 @@ function getElements(blocks: Array<WorkflowBlock>): {
|
||||
);
|
||||
nodes.push(node);
|
||||
if (d.previous) {
|
||||
edges.push({
|
||||
id: nanoid(),
|
||||
type: "edgeWithAddButton",
|
||||
source: d.previous,
|
||||
target: d.id,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
zIndex: REACT_FLOW_EDGE_Z_INDEX,
|
||||
});
|
||||
edges.push(edgeWithAddButton(d.previous, d.id));
|
||||
}
|
||||
});
|
||||
|
||||
const loopBlocks = data.filter((d) => d.block.block_type === "for_loop");
|
||||
loopBlocks.forEach((block) => {
|
||||
const startNodeId = nanoid();
|
||||
nodes.push(startNode(startNodeId, block.id));
|
||||
const children = data.filter((b) => b.parentId === block.id);
|
||||
if (children.length === 0) {
|
||||
const adderNodeId = nanoid();
|
||||
nodes.push(nodeAdderNode(adderNodeId, block.id));
|
||||
edges.push(defaultEdge(startNodeId, adderNodeId));
|
||||
} else {
|
||||
const firstChild = children.find((c) => c.previous === null)!;
|
||||
edges.push(edgeWithAddButton(startNodeId, firstChild.id));
|
||||
}
|
||||
const lastChild = children.find((c) => c.next === null);
|
||||
nodes.push({
|
||||
id: `${block.id}-nodeAdder`,
|
||||
type: "nodeAdder",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
parentId: block.id,
|
||||
});
|
||||
const adderNodeId = nanoid();
|
||||
nodes.push(nodeAdderNode(adderNodeId, block.id));
|
||||
if (lastChild) {
|
||||
edges.push({
|
||||
id: `${block.id}-nodeAdder-edge`,
|
||||
type: "default",
|
||||
source: lastChild.id,
|
||||
target: `${block.id}-nodeAdder`,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
});
|
||||
edges.push(defaultEdge(lastChild.id, adderNodeId));
|
||||
}
|
||||
});
|
||||
|
||||
if (nodes.length > 0) {
|
||||
const lastNode = data.find((d) => d.next === null && d.parentId === null);
|
||||
edges.push({
|
||||
id: "edge-nodeAdder",
|
||||
type: "default",
|
||||
source: lastNode!.id,
|
||||
target: "nodeAdder",
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
});
|
||||
nodes.push({
|
||||
id: "nodeAdder",
|
||||
type: "nodeAdder",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
});
|
||||
const startNodeId = nanoid();
|
||||
const adderNodeId = nanoid();
|
||||
|
||||
if (nodes.length === 0) {
|
||||
nodes.push(startNode(startNodeId));
|
||||
nodes.push(nodeAdderNode(adderNodeId));
|
||||
edges.push(defaultEdge(startNodeId, adderNodeId));
|
||||
} else {
|
||||
const firstNode = data.find(
|
||||
(d) => d.previous === null && d.parentId === null,
|
||||
);
|
||||
nodes.push(startNode(startNodeId));
|
||||
edges.push(edgeWithAddButton(startNodeId, firstNode!.id));
|
||||
const lastNode = data.find((d) => d.next === null && d.parentId === null)!;
|
||||
edges.push(defaultEdge(lastNode.id, adderNodeId));
|
||||
nodes.push(nodeAdderNode(adderNodeId));
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
@@ -396,7 +435,7 @@ function getElements(blocks: Array<WorkflowBlock>): {
|
||||
|
||||
function createNode(
|
||||
identifiers: { id: string; parentId?: string },
|
||||
nodeType: Exclude<keyof typeof nodeTypes, "nodeAdder">,
|
||||
nodeType: NonNullable<WorkflowBlockNode["type"]>,
|
||||
label: string,
|
||||
): AppNode {
|
||||
const common = {
|
||||
@@ -503,9 +542,7 @@ function JSONParseSafe(json: string): Record<string, unknown> | null {
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkflowBlock(
|
||||
node: Exclude<AppNode, LoopNode | NodeAdderNode>,
|
||||
): BlockYAML {
|
||||
function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
||||
const base = {
|
||||
label: node.data.label,
|
||||
continue_on_failure: node.data.continueOnFailure,
|
||||
@@ -616,22 +653,22 @@ function getWorkflowBlocksUtil(nodes: Array<AppNode>): Array<BlockYAML> {
|
||||
.filter((n) => n.parentId === node.id)
|
||||
.map((n) => {
|
||||
return getWorkflowBlock(
|
||||
n as Exclude<AppNode, LoopNode | NodeAdderNode>,
|
||||
n as Exclude<AppNode, LoopNode | NodeAdderNode | StartNode>,
|
||||
);
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
getWorkflowBlock(node as Exclude<AppNode, LoopNode | NodeAdderNode>),
|
||||
getWorkflowBlock(
|
||||
node as Exclude<AppNode, LoopNode | NodeAdderNode | StartNode>,
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function getWorkflowBlocks(nodes: Array<AppNode>): Array<BlockYAML> {
|
||||
return getWorkflowBlocksUtil(
|
||||
nodes.filter((node) => node.type !== "nodeAdder"),
|
||||
);
|
||||
return getWorkflowBlocksUtil(nodes.filter(isWorkflowBlockNode));
|
||||
}
|
||||
|
||||
function generateNodeLabel(existingLabels: Array<string>) {
|
||||
@@ -892,7 +929,7 @@ function getAvailableOutputParameterKeys(
|
||||
previousNodeIds.includes(node.id),
|
||||
);
|
||||
const labels = previousNodes
|
||||
.filter((node) => node.type !== "nodeAdder")
|
||||
.filter(isWorkflowBlockNode)
|
||||
.map((node) => node.data.label);
|
||||
const outputParameterKeys = labels.map((label) =>
|
||||
getOutputParameterKey(label),
|
||||
|
||||
Reference in New Issue
Block a user