Add Start Node to workflows (#1026)

This commit is contained in:
Shuchang Zheng
2024-10-22 10:02:24 -07:00
committed by GitHub
parent d571519a67
commit da92ccdc9f
9 changed files with 152 additions and 109 deletions

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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 };

View File

@@ -0,0 +1,5 @@
import type { Node } from "@xyflow/react";
export type StartNodeData = Record<string, never>;
export type StartNode = Node<StartNodeData, "start">;

View File

@@ -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: "",

View File

@@ -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),
};

View File

@@ -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;

View File

@@ -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),