Allow nested loop blocks (#1977)
This commit is contained in:
@@ -73,6 +73,7 @@ import {
|
|||||||
convertEchoParameters,
|
convertEchoParameters,
|
||||||
createNode,
|
createNode,
|
||||||
defaultEdge,
|
defaultEdge,
|
||||||
|
descendants,
|
||||||
generateNodeLabel,
|
generateNodeLabel,
|
||||||
getAdditionalParametersForEmailBlock,
|
getAdditionalParametersForEmailBlock,
|
||||||
getOutputParameterKey,
|
getOutputParameterKey,
|
||||||
@@ -408,12 +409,22 @@ function FlowRenderer({
|
|||||||
if (!node || !isWorkflowBlockNode(node)) {
|
if (!node || !isWorkflowBlockNode(node)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const nodesToDelete = descendants(nodes, id);
|
||||||
const deletedNodeLabel = node.data.label;
|
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) => {
|
const newEdges = edges.flatMap((edge) => {
|
||||||
if (edge.source === id) {
|
if (edge.source === id) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
nodesToDelete.some(
|
||||||
|
(node) => node.id === edge.source || node.id === edge.target,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
if (edge.target === id) {
|
if (edge.target === id) {
|
||||||
const nextEdge = edges.find((edge) => edge.source === id);
|
const nextEdge = edges.find((edge) => edge.source === id);
|
||||||
if (nextEdge) {
|
if (nextEdge) {
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ function EdgeWithAddButton({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-4 w-4 rounded-full transition-all hover:scale-150"
|
className="h-4 w-4 rounded-full transition-all hover:scale-150"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const disableLoop = Boolean(sourceNode?.parentId);
|
|
||||||
setWorkflowPanelState({
|
setWorkflowPanelState({
|
||||||
active: true,
|
active: true,
|
||||||
content: "nodeLibrary",
|
content: "nodeLibrary",
|
||||||
@@ -64,7 +63,6 @@ function EdgeWithAddButton({
|
|||||||
previous: source,
|
previous: source,
|
||||||
next: target,
|
next: target,
|
||||||
parent: sourceNode?.parentId,
|
parent: sourceNode?.parentId,
|
||||||
disableLoop,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -21,10 +21,15 @@ import type { LoopNode } from "./types";
|
|||||||
import { useState } from "react";
|
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";
|
||||||
|
|
||||||
function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||||
const { updateNodeData } = useReactFlow();
|
const { updateNodeData } = useReactFlow();
|
||||||
const nodes = useNodes<AppNode>();
|
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({
|
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||||
id,
|
id,
|
||||||
initialValue: data.label,
|
initialValue: data.label,
|
||||||
@@ -55,6 +60,7 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
|||||||
(furthestDownChild?.position.y ?? 0) +
|
(furthestDownChild?.position.y ?? 0) +
|
||||||
24;
|
24;
|
||||||
|
|
||||||
|
const loopNodeWidth = getLoopNodeWidth(node, nodes);
|
||||||
function handleChange(key: string, value: unknown) {
|
function handleChange(key: string, value: unknown) {
|
||||||
if (!data.editable) {
|
if (!data.editable) {
|
||||||
return;
|
return;
|
||||||
@@ -78,8 +84,9 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
|||||||
className="opacity-0"
|
className="opacity-0"
|
||||||
/>
|
/>
|
||||||
<div
|
<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={{
|
style={{
|
||||||
|
width: loopNodeWidth,
|
||||||
height: childrenHeightExtent,
|
height: childrenHeightExtent,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ function NodeAdderNode({ id, parentId }: NodeProps<NodeAdderNode>) {
|
|||||||
className="rounded-full bg-slate-50 p-2"
|
className="rounded-full bg-slate-50 p-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const previous = edges.find((edge) => edge.target === id)?.source;
|
const previous = edges.find((edge) => edge.target === id)?.source;
|
||||||
const disableLoop = Boolean(parentId);
|
|
||||||
setWorkflowPanelState({
|
setWorkflowPanelState({
|
||||||
active: true,
|
active: true,
|
||||||
content: "nodeLibrary",
|
content: "nodeLibrary",
|
||||||
@@ -36,7 +35,6 @@ function NodeAdderNode({ id, parentId }: NodeProps<NodeAdderNode>) {
|
|||||||
next: id,
|
next: id,
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
connectingEdgeType: "default",
|
connectingEdgeType: "default",
|
||||||
disableLoop,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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(
|
function layout(
|
||||||
nodes: Array<AppNode>,
|
nodes: Array<AppNode>,
|
||||||
edges: Array<Edge>,
|
edges: Array<Edge>,
|
||||||
): { nodes: Array<AppNode>; edges: Array<Edge> } {
|
): { nodes: Array<AppNode>; edges: Array<Edge> } {
|
||||||
const loopNodes = nodes.filter(
|
const loopNodes = nodes.filter((node) => node.type === "loop");
|
||||||
(node) => node.type === "loop" && !node.parentId,
|
|
||||||
);
|
|
||||||
const loopNodeChildren: Array<Array<AppNode>> = loopNodes.map(() => []);
|
const loopNodeChildren: Array<Array<AppNode>> = loopNodes.map(() => []);
|
||||||
|
|
||||||
loopNodes.forEach((node, index) => {
|
loopNodes.forEach((node, index) => {
|
||||||
@@ -151,7 +174,7 @@ function layout(
|
|||||||
const maxChildWidth = Math.max(
|
const maxChildWidth = Math.max(
|
||||||
...childNodes.map((node) => node.measured?.width ?? 0),
|
...childNodes.map((node) => node.measured?.width ?? 0),
|
||||||
);
|
);
|
||||||
const loopNodeWidth = 600; // 600 px
|
const loopNodeWidth = getLoopNodeWidth(node, nodes);
|
||||||
const layouted = layoutUtil(childNodes, childEdges, {
|
const layouted = layoutUtil(childNodes, childEdges, {
|
||||||
marginx: (loopNodeWidth - maxChildWidth) / 2,
|
marginx: (loopNodeWidth - maxChildWidth) / 2,
|
||||||
marginy: 225,
|
marginy: 225,
|
||||||
@@ -1169,7 +1192,23 @@ function getOrderedChildrenBlocks(
|
|||||||
const children: Array<BlockYAML> = [];
|
const children: Array<BlockYAML> = [];
|
||||||
let currentNode: WorkflowBlockNode | undefined = firstChild;
|
let currentNode: WorkflowBlockNode | undefined = firstChild;
|
||||||
while (currentNode) {
|
while (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));
|
children.push(getWorkflowBlock(currentNode));
|
||||||
|
}
|
||||||
const nextId = edges.find(
|
const nextId = edges.find(
|
||||||
(edge) => edge.source === currentNode?.id,
|
(edge) => edge.source === currentNode?.id,
|
||||||
)?.target;
|
)?.target;
|
||||||
@@ -1991,12 +2030,14 @@ export {
|
|||||||
createNode,
|
createNode,
|
||||||
generateNodeData,
|
generateNodeData,
|
||||||
generateNodeLabel,
|
generateNodeLabel,
|
||||||
|
getNestingLevel,
|
||||||
getAdditionalParametersForEmailBlock,
|
getAdditionalParametersForEmailBlock,
|
||||||
getAvailableOutputParameterKeys,
|
getAvailableOutputParameterKeys,
|
||||||
getBlockNameOfOutputParameterKey,
|
getBlockNameOfOutputParameterKey,
|
||||||
getDefaultValueForParameterType,
|
getDefaultValueForParameterType,
|
||||||
getElements,
|
getElements,
|
||||||
getLabelForWorkflowParameterType,
|
getLabelForWorkflowParameterType,
|
||||||
|
maxNestingLevel,
|
||||||
getWorkflowSettings,
|
getWorkflowSettings,
|
||||||
getOutputParameterKey,
|
getOutputParameterKey,
|
||||||
getPreviousNodeIds,
|
getPreviousNodeIds,
|
||||||
|
|||||||
Reference in New Issue
Block a user