add support for nesting conditional blocks inside loop blocks - frontend (#4301)

This commit is contained in:
Celal Zamanoglu
2025-12-16 15:23:32 +03:00
committed by GitHub
parent 1a3d568198
commit 90e7f7b3cf
2 changed files with 128 additions and 22 deletions

View File

@@ -319,15 +319,6 @@ function WorkflowNodeLibraryPanel({
};
}
// Disable conditional inside loop
if (nodeType === "conditional" && parentType === "loop") {
return {
disabled: true,
reason:
"We're working on supporting conditionals inside loops. Soon you'll be able to use this feature!",
};
}
// Disable loop inside conditional
if (nodeType === "loop" && parentType === "conditional") {
return {

View File

@@ -938,12 +938,22 @@ function reconstructConditionalStructure(
if (block.block_type !== "conditional") {
if (block.block_type === "for_loop") {
// Recursively handle conditionals inside loops
reconstructConditionalStructure(
const recursiveResult = reconstructConditionalStructure(
block.loop_blocks,
newNodes,
labelToNodeMap,
blocksByLabel,
);
// Merge edges from recursive call
newEdges.push(...recursiveResult.edges);
// Merge nodes from recursive call (deduplicate by id)
const existingNodeIds = new Set(newNodes.map((n) => n.id));
recursiveResult.nodes.forEach((node) => {
if (!existingNodeIds.has(node.id)) {
newNodes.push(node);
existingNodeIds.add(node.id);
}
});
}
return;
}
@@ -1312,8 +1322,12 @@ function getElements(
}
});
const loopBlocks = data.filter((d) => d.block.block_type === "for_loop");
const loopBlocks = data.filter(
(d): d is typeof d & { block: ForLoopBlock } =>
d.block.block_type === "for_loop",
);
loopBlocks.forEach((block) => {
const loopBlock = block.block;
const startNodeId = nanoid();
nodes.push(
startNode(
@@ -1327,32 +1341,67 @@ function getElements(
block.id,
),
);
const children = data.filter((b) => b.parentId === block.id);
// Collect labels that belong to conditional branches inside this loop so we
// don't chain them as top-level loop children (they are handled by the
// conditional's own edges).
const branchLabels = new Set<string>();
const collectBranchLabels = (loopChildren: Array<WorkflowBlock>) => {
loopChildren.forEach((child) => {
if (child.block_type === "conditional") {
child.branch_conditions.forEach((branch) => {
collectLabelsForBranch(
branch.next_block_label,
child.next_block_label ?? null,
blocksByLabel,
).forEach((label) => branchLabels.add(label));
});
}
if (child.block_type === "for_loop") {
collectBranchLabels(child.loop_blocks);
}
});
};
collectBranchLabels(loopBlock.loop_blocks);
// Only keep loop children that are not part of any conditional branch.
const children = data.filter(
(b) => b.parentId === block.id && !branchLabels.has(b.block.label),
);
const adderNodeId = nanoid();
if (children.length === 0) {
edges.push(defaultEdge(startNodeId, adderNodeId));
} else {
const firstChild = children.find((c) => c.previous === null)!;
edges.push(edgeWithAddButton(startNodeId, firstChild.id));
// Chain loop children based on their next pointers
const childById = new Map<string, (typeof children)[number]>();
children.forEach((c) => childById.set(c.id, c));
const firstChild =
children.find(
(c) => c.previous === null || !childById.has(c.previous),
) ?? children[0]!;
edges.push(edgeWithAddButton(startNodeId, firstChild.id));
let current = firstChild;
while (current?.next) {
const nextChild = childById.get(current.next);
let lastChild = firstChild;
while (current) {
const nextChild = current.next ? childById.get(current.next) : null;
if (!nextChild) {
break;
}
edges.push(edgeWithAddButton(current.id, nextChild.id));
lastChild = nextChild;
current = nextChild;
}
nodes.push(nodeAdderNode(adderNodeId, block.id));
if (lastChild) {
edges.push(defaultEdge(lastChild.id, adderNodeId));
}
return;
}
const lastChild = children.find((c) => c.next === null);
nodes.push(nodeAdderNode(adderNodeId, block.id));
if (lastChild) {
edges.push(defaultEdge(lastChild.id, adderNodeId));
}
});
// Reconstruct conditional hierarchy and create conditional edges
@@ -2219,6 +2268,20 @@ function getOrderedChildrenBlocks(
edges: Array<Edge>,
parentId: string,
): Array<BlockYAML> {
const nodesById = new Map(nodes.map((n) => [n.id, n]));
const includedIds = new Set<string>();
const hasAncestor = (nodeId: string | null, ancestorId: string): boolean => {
let current = nodeId ? nodesById.get(nodeId) : undefined;
while (current) {
if (current.parentId === ancestorId) {
return true;
}
current = current.parentId ? nodesById.get(current.parentId) : undefined;
}
return false;
};
const parentNode = nodes.find((node) => node.id === parentId);
if (!parentNode) {
return [];
@@ -2240,6 +2303,7 @@ function getOrderedChildrenBlocks(
const children: Array<BlockYAML> = [];
let currentNode: WorkflowBlockNode | undefined = firstChild;
while (currentNode) {
includedIds.add(currentNode.id);
if (currentNode.type === "loop") {
const loopChildren = getOrderedChildrenBlocks(
nodes,
@@ -2264,6 +2328,38 @@ function getOrderedChildrenBlocks(
const next = nodes.find((node) => node.id === nextId);
currentNode = next && isWorkflowBlockNode(next) ? next : undefined;
}
// Add any additional workflow block nodes that belong under this parent (e.g., conditional branches)
nodes.forEach((node) => {
if (!isWorkflowBlockNode(node)) {
return;
}
if (includedIds.has(node.id)) {
return;
}
if (!hasAncestor(node.id, parentId)) {
return;
}
if (node.type === "loop") {
const loopChildren = getOrderedChildrenBlocks(nodes, edges, node.id);
children.push({
block_type: "for_loop",
label: node.data.label,
continue_on_failure: node.data.continueOnFailure,
next_loop_on_failure: node.data.nextLoopOnFailure,
loop_blocks: loopChildren,
loop_variable_reference: node.data.loopVariableReference,
complete_if_empty: node.data.completeIfEmpty,
});
includedIds.add(node.id);
return;
}
children.push(getWorkflowBlock(node, nodes, edges));
includedIds.add(node.id);
});
return children;
}
@@ -2271,6 +2367,20 @@ function getWorkflowBlocksUtil(
nodes: Array<AppNode>,
edges: Array<Edge>,
): Array<BlockYAML> {
const nodesById = new Map(nodes.map((n) => [n.id, n]));
const isInsideLoop = (nodeId: string): boolean => {
let current = nodesById.get(nodeId);
while (current?.parentId) {
const parent = nodesById.get(current.parentId);
if (parent?.type === "loop") {
return true;
}
current = parent;
}
return false;
};
return nodes.flatMap((node) => {
// Skip utility nodes
if (node.type === "start" || node.type === "nodeAdder") {
@@ -2281,6 +2391,11 @@ function getWorkflowBlocksUtil(
const isConditionalBranchNode =
isWorkflowBlockNode(node) && node.data.conditionalNodeId;
// If this node is inside any loop, it will be emitted through that loop's loop_blocks
if (isInsideLoop(node.id)) {
return [];
}
// Skip nodes with parentId UNLESS they're in a conditional branch
// (loop children should be filtered out, conditional branch children should stay)
if (node.parentId && !isConditionalBranchNode) {