From b7251ca624dcd19eec473485e562ea6934f98a26 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Thu, 12 Mar 2026 16:59:15 +0100 Subject: [PATCH] refactor: replace group module_ids with start_id/end_id, compute members dynamically Co-Authored-By: Claude Opus 4.6 --- .../flows/map/FlowModuleSchemaMap.svelte | 9 -- .../lib/components/graph/FlowGraphV2.svelte | 71 +++++++-- .../components/graph/GroupActionBar.svelte | 18 ++- .../lib/components/graph/GroupOverlay.svelte | 19 +-- .../components/graph/NodeContextMenu.svelte | 14 +- .../graph/SelectionBoundingBox.svelte | 15 +- .../components/graph/graphBuilder.svelte.ts | 13 +- .../src/lib/components/graph/graphContext.ts | 7 + .../components/graph/groupDetectionUtils.ts | 138 +++++++++++++++- .../components/graph/groupEditor.svelte.ts | 148 ++++++------------ frontend/src/lib/gen/types.gen.ts | 8 +- openflow.openapi.yaml | 16 +- 12 files changed, 322 insertions(+), 154 deletions(-) diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte index 2053017639..43cc6790e8 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte @@ -632,11 +632,6 @@ } targetModules.splice(insertIndex, 0, ...removedModules) selectionManager.selectByIds(removedModules.map((m) => m.id)) - for (const m of removedModules) { - groupEditorContext?.groupEditor.handleNodeMoved( - m.id, detail.sourceId, detail.targetId - ) - } } else { let indexToRemove = originalModules.findIndex((m) => moveManager.movingModuleId == m.id) let [removedModule] = originalModules.splice(indexToRemove, 1) @@ -647,9 +642,6 @@ } targetModules.splice(insertIndex, 0, removedModule) selectionManager.selectId(removedModule.id) - groupEditorContext?.groupEditor.handleNodeMoved( - removedModule.id, detail.sourceId, detail.targetId - ) } moveManager.clearMoving() } else { @@ -687,7 +679,6 @@ toolKind ) const id = targetModules[index].id - groupEditorContext?.groupEditor.addInsertedNode(id, detail.sourceId, detail.targetId) selectionManager.selectId(id) if (detail.inlineScript?.instructions) { diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index 9978bcd40b..262da4665d 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -66,8 +66,10 @@ import { getGroupEditorContext, GROUP_HEADER_HEIGHT, - GROUP_TOP_MARGIN + GROUP_TOP_MARGIN, + type FlowGroup } from './groupEditor.svelte' + import { computeGroupMembers, type GroupMembership } from './groupDetectionUtils' import SelectionTool from './SelectionTool.svelte' import PaneContextMenu from './PaneContextMenu.svelte' import { SelectionManager } from './selectionUtils.svelte' @@ -329,7 +331,10 @@ moveManager: untrack(() => moveManager), clearFlowSelection, yOffset, - diffManager + diffManager, + getFlowNodes: () => currentGraphNodeDeps, + getContainerDescendants: () => currentContainerDescendants, + getGroupMemberships: () => currentGroupMemberships } as any) if (triggerContext && untrack(() => allowSimplifiedPoll)) { @@ -370,6 +375,33 @@ | [NodeDep[], Map | undefined, (NodeDep & NodePos)[]] | undefined = undefined let currentContainerDescendants: Map = $state(new Map()) + let currentGraphNodeDeps: { id: string; parentIds?: string[] }[] = $state([]) + + /** Compute group memberships from start_id/end_id for a given set of flow nodes. + * Collapsed groups preserve their previous membership (their member nodes + * aren't in the graph — they've been replaced by a placeholder). */ + function computeAllGroupMemberships( + groups: FlowGroup[], + flowNodes: { id: string; parentIds?: string[] }[], + contDescendants: Map + ): Map { + const map = new Map() + for (const group of groups) { + if (groupEditorContext?.groupEditor.isRuntimeCollapsed(group.id)) { + const prev = currentGroupMemberships.get(group.id) + if (prev) map.set(group.id, prev) + continue + } + map.set( + group.id, + computeGroupMembers(group.start_id, group.end_id, flowNodes, contDescendants) + ) + } + return map + } + + // Current group memberships — recomputed when groups, nodes, or containerDescendants change + let currentGroupMemberships: Map = $state(new Map()) const MAX_TOOLS_PER_ROW = 2 @@ -458,7 +490,7 @@ // Need to find topmost node per group - use topological sort const sortedNodes = topologicalSort(graphNodes).reverse() for (const group of groups) { - if (group.module_ids.length === 0) continue + const memberIds = currentGroupMemberships.get(group.id)?.memberIds ?? [] // Check if it's collapsed const collapsedNodeId = `collapsed-group:${group.id}` const hasCollapsedNode = graphNodes.some((n) => n.id === collapsedNodeId) @@ -468,8 +500,8 @@ if (hasCollapsedNode) { topmostNodeId = collapsedNodeId isCollapsed = true - } else { - topmostNodeId = sortedNodes.find((node) => group.module_ids.includes(node.id))?.id + } else if (memberIds.length > 0) { + topmostNodeId = sortedNodes.find((node) => memberIds.includes(node.id))?.id } if (topmostNodeId) { @@ -490,7 +522,7 @@ if (!isCollapsed) { const bottommostNodeId = [...sortedNodes] .reverse() - .find((node) => group.module_ids.includes(node.id))?.id + .find((node) => memberIds.includes(node.id))?.id if (bottommostNodeId) { const prevBottom = extraSpace.get(bottommostNodeId) ?? { top: 0, @@ -789,12 +821,20 @@ parentIds: n.parentIds, data: { assets: (n.data as any).assets, module: (n.data as any).module } })) + currentGraphNodeDeps = graphNodeDeps - // Clean up groups: remove stale IDs, complete paths, split disconnected components + // Clean up groups: check start_id/end_id still exist if (editMode && groupEditorContext?.groupEditor?.isAvailable()) { groupEditorContext.groupEditor.cleanupGroups(graphNodeDeps) } + // Compute group memberships from start_id/end_id + currentGroupMemberships = computeAllGroupMemberships( + groupEditorContext?.groupEditor.getGroups() ?? [], + graphNodeDeps, + currentContainerDescendants + ) + // Pre-compute extra space per node for assets, AI tools, group notes, group headers const nodeExtraSpace = computeNodeExtraSpace(graphNodeDeps) @@ -855,7 +895,7 @@ : {} const sortedNodes = topologicalSort(graphNodeDeps).reverse() for (const group of groups) { - if (group.module_ids.length === 0) continue + const memberIds = currentGroupMemberships.get(group.id)?.memberIds ?? [] const collapsedNodeId = `collapsed-group:${group.id}` const hasCollapsedNode = graphNodeDeps.some((n) => n.id === collapsedNodeId) let topNodeId: string | undefined @@ -864,8 +904,8 @@ if (hasCollapsedNode) { topNodeId = collapsedNodeId isCollapsed = true - } else { - topNodeId = sortedNodes.find((node) => group.module_ids.includes(node.id))?.id + } else if (memberIds.length > 0) { + topNodeId = sortedNodes.find((node) => memberIds.includes(node.id))?.id } if (topNodeId) { @@ -880,7 +920,7 @@ if (!isCollapsed) { const bottomNodeId = [...sortedNodes] .reverse() - .find((node) => group.module_ids.includes(node.id))?.id + .find((node) => memberIds.includes(node.id))?.id if (bottomNodeId) { const groupPadding = 16 groupBottomOffsets[bottomNodeId] = Math.max( @@ -995,7 +1035,12 @@ effectiveModuleActions currentGroups - const collapsedGroups = groupEditorContext?.groupEditor.getCollapsedGroups() ?? [] + const collapsedGroups = (groupEditorContext?.groupEditor.getCollapsedGroups() ?? []).map( + (g) => ({ + ...g, + memberIds: untrack(() => currentGroupMemberships.get(g.id)?.memberIds ?? []) + }) + ) return graphBuilder( untrack(() => effectiveModules), @@ -1298,7 +1343,7 @@ allNodes={nodesWithOffset as (Node & { type: string })[]} {editMode} {showNotes} - containerDescendants={currentContainerDescendants} + groupMemberships={currentGroupMemberships} /> diff --git a/frontend/src/lib/components/graph/GroupActionBar.svelte b/frontend/src/lib/components/graph/GroupActionBar.svelte index ac571ccb98..ef540beb8e 100644 --- a/frontend/src/lib/components/graph/GroupActionBar.svelte +++ b/frontend/src/lib/components/graph/GroupActionBar.svelte @@ -64,7 +64,9 @@ {/snippet} {#snippet menu()} -
+
@@ -95,10 +97,15 @@ {#if onDeleteGroup} @@ -107,7 +114,10 @@