Skip to content

Fix: clean up iteration child nodes and edges on delete (#13889)#14033

Open
owldev127 wants to merge 9 commits intoinfiniflow:mainfrom
owldev127:fix/13889-iteration-delete-cleanup
Open

Fix: clean up iteration child nodes and edges on delete (#13889)#14033
owldev127 wants to merge 9 commits intoinfiniflow:mainfrom
owldev127:fix/13889-iteration-delete-cleanup

Conversation

@owldev127
Copy link
Copy Markdown

What problem does this PR solve?

Fixes a workflow editor bug where deleting an Iteration Box could leave orphan child nodes and dangling edges in client state. Those stale references could be exported with the workflow and later cause rendering errors, broken connections, and unstable editing behavior.

Root Cause

Iteration deletion logic only removed the container, its direct children, and some internal edges. It did not consistently remove the full descendant subtree or all edges connected to deleted child nodes, and the keyboard delete path was not expanded to include Iteration descendants.

Type of change

  • Bug Fix (non-breaking change which fixes an issue)
  • New Feature (non-breaking change which adds functionality)
  • Documentation Update
  • Refactoring
  • Performance Improvement
  • Other (please describe):

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Apr 10, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0562b39f-ccc9-4a41-b310-8b93141b81ed

📥 Commits

Reviewing files that changed from the base of the PR and between 3e83667 and 9974388.

📒 Files selected for processing (1)
  • web/src/pages/agent/store.test.ts
✅ Files skipped from review due to trivial changes (1)
  • web/src/pages/agent/store.test.ts

📝 Walkthrough

Walkthrough

Expand deletion logic to remove iteration node subtrees and associated edges by traversing graph store nodes, update selection/click state accordingly, and extend the before-delete hook to consult the global graph state. Tests added for hook and store behavior.

Changes

Cohort / File(s) Summary
Tests
web/src/pages/agent/hooks/use-before-delete.test.tsx, web/src/pages/agent/store.test.ts
Added tests validating expanded deletion of iteration subtrees, edge cleanup, protection of Begin/IterationStart nodes, and selection/click state updates.
Hook: deletion preparation
web/src/pages/agent/hooks/use-before-delete.tsx
Hook now reads graphNodes/graphEdges from the store, expands toBeDeletedNodes by collecting iteration descendants via collectDeletionNodeIds, and rebuilds toBeDeletedEdges from store edges filtered by final deletion node set.
Store: subtree deletion utilities & behavior
web/src/pages/agent/store.ts
Added exported collectDeletionNodeIds(nodes, rootId) and removeEdgesForNodeIds(edges, nodeIds); updated deleteIterationNodeById to remove root + all descendants, clear affected selections, and clear clickedNodeId when applicable.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI (delete action)
    participant Hook as useBeforeDelete
    participant Store as Graph Store
    participant Utils as collectDeletionNodeIds

    UI->>Hook: request before-delete for nodeId
    Hook->>Store: read graphNodes, graphEdges, getNode, getOperatorTypeFromId
    Hook->>Utils: collectDeletionNodeIds(graphNodes, nodeId) (if Operator.Iteration)
    Utils-->>Hook: descendant node IDs
    Hook->>Store: filter graphEdges by deletion node ID set
    Hook-->>UI: return deletion result (nodes, edges)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hopping through nodes with care,

I gather kin from root to bear.
Edges swept clean, selections right,
Protected starts stay in the light.
A tidy forest, out of sight. 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: cleaning up iteration child nodes and edges when deleting an iteration box.
Description check ✅ Passed The description fully addresses the template requirements with a clear problem statement and properly marked bug fix type.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
web/src/pages/agent/store.test.ts (1)

5-25: Minor: baseNode is referenced before its definition.

While function declarations are hoisted in JavaScript/TypeScript, defining baseNode before createNode would improve readability.

♻️ Suggested reordering
+function baseNode(id: string, label: Operator) {
+  return {
+    id,
+    type: 'ragNode',
+    position: { x: 0, y: 0 },
+    data: {
+      label,
+      name: id,
+      form: {},
+    },
+  };
+}
+
 const createNode = (
   id: string,
   label: Operator,
   options: Partial<ReturnType<typeof baseNode>> = {},
 ) => ({
   ...baseNode(id, label),
   ...options,
 });
-
-function baseNode(id: string, label: Operator) {
-  return {
-    id,
-    type: 'ragNode',
-    position: { x: 0, y: 0 },
-    data: {
-      label,
-      name: id,
-      form: {},
-    },
-  };
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/agent/store.test.ts` around lines 5 - 25, Move the baseNode
function above createNode to avoid referencing baseNode before its definition;
locate the baseNode and createNode declarations in the file (functions named
baseNode and createNode) and reorder them so baseNode is declared first, leaving
the implementations unchanged to preserve behavior.
web/src/pages/agent/hooks/use-before-delete.tsx (1)

54-100: Edge filtering logic at lines 54-89 is dead code.

The intermediate toBeDeletedEdges computed in lines 54-70 and extended in lines 85-89 is completely replaced at line 96. The reassignment toBeDeletedEdges = graphEdges.filter(...) discards all prior edge filtering work.

If the intent is to use the graphEdges-based filtering as the authoritative source (which appears correct for comprehensive edge cleanup), consider removing or simplifying the dead code.

♻️ Proposed simplification
-    let toBeDeletedEdges = edges.filter((edge) => {
-      const sourceType = getOperatorTypeFromId(edge.source) as Operator;
-      const downStreamNodes = nodes.filter((x) => x.id === edge.target);
-
-      // This edge does not need to be deleted, the range of edges that do not need to be deleted is smaller, so consider the case where it does not need to be deleted
-      if (
-        UndeletableNodes.includes(sourceType) && // Upstream node is Begin or IterationStart
-        downStreamNodes.length === 0 // Downstream node does not exist in the nodes to be deleted
-      ) {
-        if (!nodes.some((x) => x.id === edge.source)) {
-          return true; // Can be deleted
-        }
-        return false; // Cannot be deleted
-      }
-
-      return true;
-    });
-
     // Delete the agent and tool nodes downstream of the agent node
     if (nodes.some(agentPredicate)) {
       nodes.filter(agentPredicate).forEach((node) => {
-        const { downstreamAgentAndToolEdges, downstreamAgentAndToolNodeIds } =
+        const { downstreamAgentAndToolNodeIds } =
           deleteAllDownstreamAgentsAndTool(node.id, edges);

         downstreamAgentAndToolNodeIds.forEach((nodeId) => {
           const currentNode = getNode(nodeId);
           if (toBeDeletedNodes.every((x) => x.id !== nodeId) && currentNode) {
             toBeDeletedNodes.push(currentNode);
           }
         });
-
-        downstreamAgentAndToolEdges.forEach((edge) => {
-          if (toBeDeletedEdges.every((x) => x.id !== edge.id)) {
-            toBeDeletedEdges.push(edge);
-          }
-        });
       }, []);
     }

     const toBeDeletedNodeIdSet = new Set(
       toBeDeletedNodes.map((node) => node.id),
     );
-    toBeDeletedEdges = graphEdges.filter(
+    const toBeDeletedEdges = graphEdges.filter(
       (edge) =>
         toBeDeletedNodeIdSet.has(edge.source) ||
         toBeDeletedNodeIdSet.has(edge.target),
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/agent/hooks/use-before-delete.tsx` around lines 54 - 100, The
initial computation and incremental pushes into toBeDeletedEdges (the filter
starting at toBeDeletedEdges = edges.filter(...) and the
downstreamAgentAndToolEdges.forEach(...) inside the agentPredicate block) are
dead because you always overwrite toBeDeletedEdges later with toBeDeletedEdges =
graphEdges.filter(...); either remove the initial edges.filter(...) and the
downstreamAgentAndToolEdges.forEach(...) logic, or instead merge them by
replacing the final reassignment with a union of graphEdges.filter(...) and the
previously accumulated toBeDeletedEdges so the results from
deleteAllDownstreamAgentsAndTool (and the initial source-type check) are
preserved; update references to toBeDeletedEdges, nodes, edges, agentPredicate,
deleteAllDownstreamAgentsAndTool, graphEdges, and toBeDeletedNodes accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/pages/agent/hooks/use-before-delete.tsx`:
- Around line 54-100: The initial computation and incremental pushes into
toBeDeletedEdges (the filter starting at toBeDeletedEdges = edges.filter(...)
and the downstreamAgentAndToolEdges.forEach(...) inside the agentPredicate
block) are dead because you always overwrite toBeDeletedEdges later with
toBeDeletedEdges = graphEdges.filter(...); either remove the initial
edges.filter(...) and the downstreamAgentAndToolEdges.forEach(...) logic, or
instead merge them by replacing the final reassignment with a union of
graphEdges.filter(...) and the previously accumulated toBeDeletedEdges so the
results from deleteAllDownstreamAgentsAndTool (and the initial source-type
check) are preserved; update references to toBeDeletedEdges, nodes, edges,
agentPredicate, deleteAllDownstreamAgentsAndTool, graphEdges, and
toBeDeletedNodes accordingly.

In `@web/src/pages/agent/store.test.ts`:
- Around line 5-25: Move the baseNode function above createNode to avoid
referencing baseNode before its definition; locate the baseNode and createNode
declarations in the file (functions named baseNode and createNode) and reorder
them so baseNode is declared first, leaving the implementations unchanged to
preserve behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34f05d35-fa34-4707-b57b-421fd4b84a9e

📥 Commits

Reviewing files that changed from the base of the PR and between 18cafff and 3e83667.

📒 Files selected for processing (4)
  • web/src/pages/agent/hooks/use-before-delete.test.tsx
  • web/src/pages/agent/hooks/use-before-delete.tsx
  • web/src/pages/agent/store.test.ts
  • web/src/pages/agent/store.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant