Allow nested loop blocks (#1977)

This commit is contained in:
Shuchang Zheng
2025-03-20 10:03:03 -07:00
committed by GitHub
parent ee1f6dd10e
commit b90ae3b30c
5 changed files with 66 additions and 11 deletions

View File

@@ -73,6 +73,7 @@ import {
convertEchoParameters,
createNode,
defaultEdge,
descendants,
generateNodeLabel,
getAdditionalParametersForEmailBlock,
getOutputParameterKey,
@@ -408,12 +409,22 @@ function FlowRenderer({
if (!node || !isWorkflowBlockNode(node)) {
return;
}
const nodesToDelete = descendants(nodes, id);
const deletedNodeLabel = node.data.label;
const newNodes = nodes.filter((node) => node.id !== id);
const newNodes = nodes.filter(
(node) => !nodesToDelete.includes(node) && node.id !== id,
);
const newEdges = edges.flatMap((edge) => {
if (edge.source === id) {
return [];
}
if (
nodesToDelete.some(
(node) => node.id === edge.source || node.id === edge.target,
)
) {
return [];
}
if (edge.target === id) {
const nextEdge = edges.find((edge) => edge.source === id);
if (nextEdge) {

View File

@@ -56,7 +56,6 @@ function EdgeWithAddButton({
size="icon"
className="h-4 w-4 rounded-full transition-all hover:scale-150"
onClick={() => {
const disableLoop = Boolean(sourceNode?.parentId);
setWorkflowPanelState({
active: true,
content: "nodeLibrary",
@@ -64,7 +63,6 @@ function EdgeWithAddButton({
previous: source,
next: target,
parent: sourceNode?.parentId,
disableLoop,
},
});
}}

View File

@@ -21,10 +21,15 @@ import type { LoopNode } from "./types";
import { useState } from "react";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { Checkbox } from "@/components/ui/checkbox";
import { getLoopNodeWidth } from "../../workflowEditorUtils";
function LoopNode({ id, data }: NodeProps<LoopNode>) {
const { updateNodeData } = useReactFlow();
const nodes = useNodes<AppNode>();
const node = nodes.find((n) => n.id === id);
if (!node) {
throw new Error("Node not found"); // not possible
}
const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
@@ -55,6 +60,7 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
(furthestDownChild?.position.y ?? 0) +
24;
const loopNodeWidth = getLoopNodeWidth(node, nodes);
function handleChange(key: string, value: unknown) {
if (!data.editable) {
return;
@@ -78,8 +84,9 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
className="opacity-0"
/>
<div
className="w-[600px] rounded-xl border-2 border-dashed border-slate-600 p-2"
className="rounded-xl border-2 border-dashed border-slate-600 p-2"
style={{
width: loopNodeWidth,
height: childrenHeightExtent,
}}
>

View File

@@ -27,7 +27,6 @@ function NodeAdderNode({ id, parentId }: NodeProps<NodeAdderNode>) {
className="rounded-full bg-slate-50 p-2"
onClick={() => {
const previous = edges.find((edge) => edge.target === id)?.source;
const disableLoop = Boolean(parentId);
setWorkflowPanelState({
active: true,
content: "nodeLibrary",
@@ -36,7 +35,6 @@ function NodeAdderNode({ id, parentId }: NodeProps<NodeAdderNode>) {
next: id,
parent: parentId,
connectingEdgeType: "default",
disableLoop,
},
});
}}

View File

@@ -132,13 +132,36 @@ function layoutUtil(
};
}
export function descendants(nodes: Array<AppNode>, id: string): Array<AppNode> {
const children = nodes.filter((n) => n.parentId === id);
return children.concat(...children.map((c) => descendants(nodes, c.id)));
}
export function getLoopNodeWidth(node: AppNode, nodes: Array<AppNode>): number {
const maxNesting = maxNestingLevel(nodes);
const nestingLevel = getNestingLevel(node, nodes);
return 600 + (maxNesting - nestingLevel) * 50;
}
function maxNestingLevel(nodes: Array<AppNode>): number {
return Math.max(...nodes.map((node) => getNestingLevel(node, nodes)));
}
function getNestingLevel(node: AppNode, nodes: Array<AppNode>): number {
let level = 0;
let current = nodes.find((n) => n.id === node.parentId);
while (current) {
level++;
current = nodes.find((n) => n.id === current?.parentId);
}
return level;
}
function layout(
nodes: Array<AppNode>,
edges: Array<Edge>,
): { nodes: Array<AppNode>; edges: Array<Edge> } {
const loopNodes = nodes.filter(
(node) => node.type === "loop" && !node.parentId,
);
const loopNodes = nodes.filter((node) => node.type === "loop");
const loopNodeChildren: Array<Array<AppNode>> = loopNodes.map(() => []);
loopNodes.forEach((node, index) => {
@@ -151,7 +174,7 @@ function layout(
const maxChildWidth = Math.max(
...childNodes.map((node) => node.measured?.width ?? 0),
);
const loopNodeWidth = 600; // 600 px
const loopNodeWidth = getLoopNodeWidth(node, nodes);
const layouted = layoutUtil(childNodes, childEdges, {
marginx: (loopNodeWidth - maxChildWidth) / 2,
marginy: 225,
@@ -1169,7 +1192,23 @@ function getOrderedChildrenBlocks(
const children: Array<BlockYAML> = [];
let currentNode: WorkflowBlockNode | undefined = firstChild;
while (currentNode) {
children.push(getWorkflowBlock(currentNode));
if (currentNode.type === "loop") {
const loopChildren = getOrderedChildrenBlocks(
nodes,
edges,
currentNode.id,
);
children.push({
block_type: "for_loop",
label: currentNode.data.label,
continue_on_failure: currentNode.data.continueOnFailure,
loop_blocks: loopChildren,
loop_variable_reference: currentNode.data.loopVariableReference,
complete_if_empty: currentNode.data.completeIfEmpty,
});
} else {
children.push(getWorkflowBlock(currentNode));
}
const nextId = edges.find(
(edge) => edge.source === currentNode?.id,
)?.target;
@@ -1991,12 +2030,14 @@ export {
createNode,
generateNodeData,
generateNodeLabel,
getNestingLevel,
getAdditionalParametersForEmailBlock,
getAvailableOutputParameterKeys,
getBlockNameOfOutputParameterKey,
getDefaultValueForParameterType,
getElements,
getLabelForWorkflowParameterType,
maxNestingLevel,
getWorkflowSettings,
getOutputParameterKey,
getPreviousNodeIds,