From a659296dc7f59b657a1272ae539713e58ebaf7de Mon Sep 17 00:00:00 2001 From: Novice Date: Mon, 23 Mar 2026 14:29:28 +0800 Subject: [PATCH 1/2] fix: improve sandbox storage mount handling and error logging --- api/core/sandbox/builder.py | 10 +++++----- api/core/sandbox/storage/archive_storage.py | 21 ++++++++------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/api/core/sandbox/builder.py b/api/core/sandbox/builder.py index 6ec2bd38aa..efb7e88c9b 100644 --- a/api/core/sandbox/builder.py +++ b/api/core/sandbox/builder.py @@ -175,11 +175,11 @@ class SandboxBuilder: if sandbox.is_cancelled(): return - # Storage mount is part of readiness. If restore/mount fails, - # the sandbox must surface initialization failure instead of - # becoming "ready" with missing files. - if not sandbox.mount(): - raise RuntimeError("Sandbox storage mount failed") + # Attempt to restore prior workspace state. mount() returns + # False when no archive exists yet (first run for this + # sandbox_id), which is a normal case — not an error. + # Actual failures (download/extract) surface as exceptions. + sandbox.mount() sandbox.mark_ready() except Exception as exc: try: diff --git a/api/core/sandbox/storage/archive_storage.py b/api/core/sandbox/storage/archive_storage.py index bdc8e2021b..2f6c436cad 100644 --- a/api/core/sandbox/storage/archive_storage.py +++ b/api/core/sandbox/storage/archive_storage.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging -from core.virtual_environment.__base.exec import PipelineExecutionError from core.virtual_environment.__base.helpers import pipeline from core.virtual_environment.__base.virtual_environment import VirtualEnvironment from extensions.storage.base_storage import BaseStorage @@ -47,19 +46,15 @@ class ArchiveSandboxStorage(SandboxStorage): download_url = self._storage.get_download_url(self._storage_key, _ARCHIVE_TIMEOUT) archive = "archive.tar.gz" - try: - ( - pipeline(sandbox) - .add(["curl", "-fsSL", download_url, "-o", archive], error_message="Failed to download archive") - .add( - ["sh", "-c", 'tar -xzf "$1" 2>/dev/null; exit $?', "sh", archive], error_message="Failed to extract" - ) - .add(["rm", archive], error_message="Failed to cleanup") - .execute(timeout=_ARCHIVE_TIMEOUT, raise_on_error=True) + ( + pipeline(sandbox) + .add(["curl", "-fsSL", download_url, "-o", archive], error_message="Failed to download archive") + .add( + ["sh", "-c", 'tar -xzf "$1" 2>/dev/null; exit $?', "sh", archive], error_message="Failed to extract" ) - except PipelineExecutionError: - logger.exception("Failed to mount archive for sandbox %s", self._sandbox_id) - return False + .add(["rm", archive], error_message="Failed to cleanup") + .execute(timeout=_ARCHIVE_TIMEOUT, raise_on_error=True) + ) logger.info("Mounted archive for sandbox %s", self._sandbox_id) return True From 2cbc8da9cbbf3275dc9f7538a277db18bdc9a37c Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 23 Mar 2026 16:32:48 +0800 Subject: [PATCH 2/2] chore: remove block code --- api/core/workflow/enums.py | 1 - api/core/workflow/graph/graph.py | 3 +- web/app/components/workflow/block-icon.tsx | 3 - web/app/components/workflow/custom-edge.tsx | 4 +- .../workflow/custom-group-node/constants.ts | 11 - .../custom-group-exit-port-node.tsx | 54 -- .../custom-group-input-node.tsx | 55 -- .../custom-group-node/custom-group-node.tsx | 94 -- .../workflow/custom-group-node/index.ts | 19 - .../workflow/custom-group-node/types.ts | 82 -- .../workflow/hooks/use-edges-interactions.ts | 46 +- .../workflow/hooks/use-make-group.ts | 138 --- .../workflow/hooks/use-nodes-interactions.ts | 836 +----------------- .../workflow/hooks/use-nodes-meta-data.ts | 11 +- .../workflow/hooks/use-shortcuts.ts | 25 - .../components/workflow/hooks/use-workflow.ts | 44 +- web/app/components/workflow/index.tsx | 11 - .../panel-operator/panel-operator-popup.tsx | 22 +- .../_base/components/workflow-panel/index.tsx | 6 +- .../workflow-panel/last-run/use-last-run.ts | 2 - .../components/workflow/nodes/_base/node.tsx | 2 +- .../components/workflow/nodes/components.ts | 4 - .../workflow/nodes/group/default.ts | 26 - .../components/workflow/nodes/group/node.tsx | 94 -- .../components/workflow/nodes/group/panel.tsx | 9 - .../components/workflow/nodes/group/types.ts | 21 - .../workflow/selection-contextmenu.tsx | 23 - web/app/components/workflow/types.ts | 3 - .../workflow/utils/workflow-init.ts | 149 +--- 29 files changed, 64 insertions(+), 1734 deletions(-) delete mode 100644 web/app/components/workflow/custom-group-node/constants.ts delete mode 100644 web/app/components/workflow/custom-group-node/custom-group-exit-port-node.tsx delete mode 100644 web/app/components/workflow/custom-group-node/custom-group-input-node.tsx delete mode 100644 web/app/components/workflow/custom-group-node/custom-group-node.tsx delete mode 100644 web/app/components/workflow/custom-group-node/index.ts delete mode 100644 web/app/components/workflow/custom-group-node/types.ts delete mode 100644 web/app/components/workflow/hooks/use-make-group.ts delete mode 100644 web/app/components/workflow/nodes/group/default.ts delete mode 100644 web/app/components/workflow/nodes/group/node.tsx delete mode 100644 web/app/components/workflow/nodes/group/panel.tsx delete mode 100644 web/app/components/workflow/nodes/group/types.ts diff --git a/api/core/workflow/enums.py b/api/core/workflow/enums.py index 37bc339f44..cbf4a0ba6a 100644 --- a/api/core/workflow/enums.py +++ b/api/core/workflow/enums.py @@ -65,7 +65,6 @@ class NodeType(StrEnum): HUMAN_INPUT = "human-input" COMMAND = "command" FILE_UPLOAD = "file-upload" - GROUP = "group" @property def is_trigger_node(self) -> bool: diff --git a/api/core/workflow/graph/graph.py b/api/core/workflow/graph/graph.py index b6f577d193..79300440f8 100644 --- a/api/core/workflow/graph/graph.py +++ b/api/core/workflow/graph/graph.py @@ -309,11 +309,10 @@ class Graph: # Filter out UI-only node types: # - custom-note: top-level type (node_config.type == "custom-note") - # - group: data-level type (node_config.data.type == "group") node_configs = [ node_config for node_config in node_configs - if node_config.get("type", "") != "custom-note" and node_config.get("data", {}).get("type", "") != "group" + if node_config.get("type", "") != "custom-note" ] # Parse node configurations diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index 6634ee0825..7307353da3 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -8,7 +8,6 @@ import { import { useStore as useAppStore } from '@/app/components/app/store' import AppIcon from '@/app/components/base/app-icon' import { useFeaturesStore } from '@/app/components/base/features/hooks' -import { Folder as FolderLine } from '@/app/components/base/icons/src/vender/line/files' import { Agent, Answer, @@ -69,7 +68,6 @@ const DEFAULT_ICON_MAP: Record = { [BlockEnum.VariableAssigner]: 'bg-util-colors-blue-blue-500', [BlockEnum.VariableAggregator]: 'bg-util-colors-blue-blue-500', [BlockEnum.Tool]: 'bg-util-colors-blue-blue-500', - [BlockEnum.Group]: 'bg-util-colors-blue-blue-500', [BlockEnum.Assigner]: 'bg-util-colors-blue-blue-500', [BlockEnum.ParameterExtractor]: 'bg-util-colors-blue-blue-500', [BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500', diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index f727d1fa0d..8cb3b121c2 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -26,7 +26,7 @@ import { useNodesInteractions, } from './hooks' import { useHooksStore } from './hooks-store' -import { BlockEnum, NodeRunningStatus } from './types' +import { NodeRunningStatus } from './types' import { getEdgeColor } from './utils' const CustomEdge = ({ @@ -139,7 +139,7 @@ const CustomEdge = ({ stroke, strokeWidth: 2, opacity: data._dimmed ? 0.3 : (data._waitingRun ? 0.7 : 1), - strokeDasharray: (data._isTemp && !data._isSubGraphTemp && data.sourceType !== BlockEnum.Group && data.targetType !== BlockEnum.Group) ? '8 8' : undefined, + strokeDasharray: (data._isTemp && !data._isSubGraphTemp) ? '8 8' : undefined, }} /> {allowGraphActions && ( diff --git a/web/app/components/workflow/custom-group-node/constants.ts b/web/app/components/workflow/custom-group-node/constants.ts deleted file mode 100644 index 5b65aaa80b..0000000000 --- a/web/app/components/workflow/custom-group-node/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const CUSTOM_GROUP_NODE = 'custom-group' -export const CUSTOM_GROUP_INPUT_NODE = 'custom-group-input' -export const CUSTOM_GROUP_EXIT_PORT_NODE = 'custom-group-exit-port' - -export const GROUP_CHILDREN_Z_INDEX = 1002 - -export const UI_ONLY_GROUP_NODE_TYPES = new Set([ - CUSTOM_GROUP_NODE, - CUSTOM_GROUP_INPUT_NODE, - CUSTOM_GROUP_EXIT_PORT_NODE, -]) diff --git a/web/app/components/workflow/custom-group-node/custom-group-exit-port-node.tsx b/web/app/components/workflow/custom-group-node/custom-group-exit-port-node.tsx deleted file mode 100644 index 969cf69935..0000000000 --- a/web/app/components/workflow/custom-group-node/custom-group-exit-port-node.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client' - -import type { FC } from 'react' -import type { CustomGroupExitPortNodeData } from './types' -import { memo } from 'react' -import { Handle, Position } from 'reactflow' -import { cn } from '@/utils/classnames' - -type CustomGroupExitPortNodeProps = { - id: string - data: CustomGroupExitPortNodeData -} - -const CustomGroupExitPortNode: FC = ({ id: _id, data }) => { - return ( -
- {/* Target handle - receives internal connections from leaf nodes */} - - - {/* Source handle - connects to external nodes */} - - - {/* Icon */} - - - -
- ) -} - -export default memo(CustomGroupExitPortNode) diff --git a/web/app/components/workflow/custom-group-node/custom-group-input-node.tsx b/web/app/components/workflow/custom-group-node/custom-group-input-node.tsx deleted file mode 100644 index 3476b3d154..0000000000 --- a/web/app/components/workflow/custom-group-node/custom-group-input-node.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client' - -import type { FC } from 'react' -import type { CustomGroupInputNodeData } from './types' -import { memo } from 'react' -import { Handle, Position } from 'reactflow' -import { cn } from '@/utils/classnames' - -type CustomGroupInputNodeProps = { - id: string - data: CustomGroupInputNodeData -} - -const CustomGroupInputNode: FC = ({ id: _id, data }) => { - return ( -
- {/* Target handle - receives external connections */} - - - {/* Source handle - connects to entry nodes */} - - - {/* Icon */} - - - - -
- ) -} - -export default memo(CustomGroupInputNode) diff --git a/web/app/components/workflow/custom-group-node/custom-group-node.tsx b/web/app/components/workflow/custom-group-node/custom-group-node.tsx deleted file mode 100644 index c51418a5de..0000000000 --- a/web/app/components/workflow/custom-group-node/custom-group-node.tsx +++ /dev/null @@ -1,94 +0,0 @@ -'use client' - -import type { FC } from 'react' -import type { CustomGroupNodeData } from './types' -import { memo } from 'react' -import { Handle, Position } from 'reactflow' -import { Plus02 } from '@/app/components/base/icons/src/vender/line/general' -import { cn } from '@/utils/classnames' - -type CustomGroupNodeProps = { - id: string - data: CustomGroupNodeData -} - -const CustomGroupNode: FC = ({ data }) => { - const { group } = data - const exitPorts = group.exitPorts ?? [] - const connectedSourceHandleIds = data._connectedSourceHandleIds ?? [] - - return ( -
- {/* Group Header */} -
- - {group.title} - -
- - {/* Target handle for incoming connections */} - - -
- {exitPorts.map((port, index) => { - const connected = connectedSourceHandleIds.includes(port.portNodeId) - - return ( -
-
- {port.name} -
- - - - {/* Visual "+" indicator (styling aligned with existing branch handles) */} - -
- ) - })} -
-
- ) -} - -export default memo(CustomGroupNode) diff --git a/web/app/components/workflow/custom-group-node/index.ts b/web/app/components/workflow/custom-group-node/index.ts deleted file mode 100644 index af8fa042e8..0000000000 --- a/web/app/components/workflow/custom-group-node/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { - CUSTOM_GROUP_EXIT_PORT_NODE, - CUSTOM_GROUP_INPUT_NODE, - CUSTOM_GROUP_NODE, - GROUP_CHILDREN_Z_INDEX, - UI_ONLY_GROUP_NODE_TYPES, -} from './constants' - -export { default as CustomGroupExitPortNode } from './custom-group-exit-port-node' - -export { default as CustomGroupInputNode } from './custom-group-input-node' -export { default as CustomGroupNode } from './custom-group-node' -export type { - CustomGroupExitPortNodeData, - CustomGroupInputNodeData, - CustomGroupNodeData, - ExitPortInfo, - GroupMember, -} from './types' diff --git a/web/app/components/workflow/custom-group-node/types.ts b/web/app/components/workflow/custom-group-node/types.ts deleted file mode 100644 index baf7b2362a..0000000000 --- a/web/app/components/workflow/custom-group-node/types.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { BlockEnum } from '../types' - -/** - * Exit port info stored in Group node - */ -export type ExitPortInfo = { - portNodeId: string - leafNodeId: string - sourceHandle: string - name: string -} - -/** - * Group node data structure - * node.type = 'custom-group' - * node.data.type = '' (empty string to bypass backend NodeType validation) - */ -export type CustomGroupNodeData = { - type: '' // Empty string bypasses backend NodeType validation - title: string - desc?: string - _connectedSourceHandleIds?: string[] - _connectedTargetHandleIds?: string[] - group: { - groupId: string - title: string - memberNodeIds: string[] - entryNodeIds: string[] - inputNodeId: string - exitPorts: ExitPortInfo[] - collapsed: boolean - } - width?: number - height?: number - selected?: boolean - _isTempNode?: boolean -} - -/** - * Group Input node data structure - * node.type = 'custom-group-input' - * node.data.type = '' - */ -export type CustomGroupInputNodeData = { - type: '' - title: string - desc?: string - groupInput: { - groupId: string - title: string - } - selected?: boolean - _isTempNode?: boolean -} - -/** - * Exit Port node data structure - * node.type = 'custom-group-exit-port' - * node.data.type = '' - */ -export type CustomGroupExitPortNodeData = { - type: '' - title: string - desc?: string - exitPort: { - groupId: string - leafNodeId: string - sourceHandle: string - name: string - } - selected?: boolean - _isTempNode?: boolean -} - -/** - * Member node info for display - */ -export type GroupMember = { - id: string - type: BlockEnum - label?: string -} diff --git a/web/app/components/workflow/hooks/use-edges-interactions.ts b/web/app/components/workflow/hooks/use-edges-interactions.ts index 7d943fbeec..4b64dafdf7 100644 --- a/web/app/components/workflow/hooks/use-edges-interactions.ts +++ b/web/app/components/workflow/hooks/use-edges-interactions.ts @@ -8,7 +8,6 @@ import type { } from '../types' import { produce } from 'immer' import { useCallback } from 'react' -import { BlockEnum } from '../types' import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../utils' import { useCollaborativeWorkflow } from './use-collaborative-workflow' import { useNodesSyncDraft } from './use-nodes-sync-draft' @@ -101,49 +100,6 @@ export const useEdgesInteractions = () => { return const currentEdge = edges[currentEdgeIndex] - // collect edges to delete (including corresponding real edges for temp edges) - const edgesToDelete: Set = new Set([currentEdge.id]) - - // if deleting a temp edge connected to a group, also delete the corresponding real hidden edge - if (currentEdge.data?._isTemp) { - const groupNode = nodes.find(n => - n.data.type === BlockEnum.Group - && (n.id === currentEdge.source || n.id === currentEdge.target), - ) - - if (groupNode) { - const memberIds = new Set((groupNode.data.members || []).map((m: { id: string }) => m.id)) - - if (currentEdge.target === groupNode.id) { - // inbound temp edge: find real edge with same source, target is a head node - edges.forEach((edge) => { - if (edge.source === currentEdge.source - && memberIds.has(edge.target) - && edge.sourceHandle === currentEdge.sourceHandle) { - edgesToDelete.add(edge.id) - } - }) - } - else if (currentEdge.source === groupNode.id) { - // outbound temp edge: sourceHandle format is "leafNodeId-originalHandle" - const sourceHandle = currentEdge.sourceHandle || '' - const lastDashIndex = sourceHandle.lastIndexOf('-') - if (lastDashIndex > 0) { - const leafNodeId = sourceHandle.substring(0, lastDashIndex) - const originalHandle = sourceHandle.substring(lastDashIndex + 1) - - edges.forEach((edge) => { - if (edge.source === leafNodeId - && edge.target === currentEdge.target - && (edge.sourceHandle || 'source') === originalHandle) { - edgesToDelete.add(edge.id) - } - }) - } - } - } - } - const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( [ { type: 'remove', edge: currentEdge }, @@ -163,7 +119,7 @@ export const useEdgesInteractions = () => { setNodes(newNodes) const newEdges = produce(edges, (draft) => { for (let i = draft.length - 1; i >= 0; i--) { - if (edgesToDelete.has(draft[i].id)) + if (draft[i].id === currentEdge.id) draft.splice(i, 1) } }) diff --git a/web/app/components/workflow/hooks/use-make-group.ts b/web/app/components/workflow/hooks/use-make-group.ts deleted file mode 100644 index 321f0e393a..0000000000 --- a/web/app/components/workflow/hooks/use-make-group.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { PredecessorHandle } from '../utils' -import { useMemo } from 'react' -import { useStore as useReactFlowStore } from 'reactflow' -import { shallow } from 'zustand/shallow' -import { BlockEnum } from '../types' -import { getCommonPredecessorHandles } from '../utils' - -export type MakeGroupAvailability = { - canMakeGroup: boolean - branchEntryNodeIds: string[] - commonPredecessorHandle?: PredecessorHandle -} - -type MinimalEdge = { - id: string - source: string - sourceHandle: string - target: string -} - -/** - * Pure function to check if the selected nodes can be grouped. - * Can be called both from React hooks and imperatively. - */ -export const checkMakeGroupAvailability = ( - selectedNodeIds: string[], - edges: MinimalEdge[], - hasGroupNode = false, -): MakeGroupAvailability => { - if (selectedNodeIds.length <= 1 || hasGroupNode) { - return { - canMakeGroup: false, - branchEntryNodeIds: [], - commonPredecessorHandle: undefined, - } - } - - const selectedNodeIdSet = new Set(selectedNodeIds) - const inboundFromOutsideTargets = new Set() - const incomingEdgeCounts = new Map() - const incomingFromSelectedTargets = new Set() - - edges.forEach((edge) => { - // Only consider edges whose target is inside the selected subgraph. - if (!selectedNodeIdSet.has(edge.target)) - return - - incomingEdgeCounts.set(edge.target, (incomingEdgeCounts.get(edge.target) ?? 0) + 1) - - if (selectedNodeIdSet.has(edge.source)) - incomingFromSelectedTargets.add(edge.target) - else - inboundFromOutsideTargets.add(edge.target) - }) - - // Branch head (entry) definition: - // - has at least one incoming edge - // - and all its incoming edges come from outside the selected subgraph - const branchEntryNodeIds = selectedNodeIds.filter((nodeId) => { - const incomingEdgeCount = incomingEdgeCounts.get(nodeId) ?? 0 - if (incomingEdgeCount === 0) - return false - - return !incomingFromSelectedTargets.has(nodeId) - }) - - // No branch head means we cannot tell how many branches are represented by this selection. - if (branchEntryNodeIds.length === 0) { - return { - canMakeGroup: false, - branchEntryNodeIds, - commonPredecessorHandle: undefined, - } - } - - // Guardrail: disallow side entrances into the selected subgraph. - // If an outside node connects to a non-entry node inside the selection, the grouping boundary is ambiguous. - const branchEntryNodeIdSet = new Set(branchEntryNodeIds) - const hasInboundToNonEntryNode = Array.from(inboundFromOutsideTargets).some(nodeId => !branchEntryNodeIdSet.has(nodeId)) - - if (hasInboundToNonEntryNode) { - return { - canMakeGroup: false, - branchEntryNodeIds, - commonPredecessorHandle: undefined, - } - } - - // Compare the branch heads by their common predecessor "handler" (source node + sourceHandle). - // This is required for multi-handle nodes like If-Else / Classifier where different branches use different handles. - const commonPredecessorHandles = getCommonPredecessorHandles( - branchEntryNodeIds, - // Only look at edges coming from outside the selected subgraph when determining the "pre" handler. - edges.filter(edge => !selectedNodeIdSet.has(edge.source)), - ) - - if (commonPredecessorHandles.length !== 1) { - return { - canMakeGroup: false, - branchEntryNodeIds, - commonPredecessorHandle: undefined, - } - } - - return { - canMakeGroup: true, - branchEntryNodeIds, - commonPredecessorHandle: commonPredecessorHandles[0], - } -} - -export const useMakeGroupAvailability = (selectedNodeIds: string[]): MakeGroupAvailability => { - const edgeKeys = useReactFlowStore((state) => { - const delimiter = '\u0000' - const keys = state.edges.map(edge => `${edge.source}${delimiter}${edge.sourceHandle || 'source'}${delimiter}${edge.target}`) - keys.sort() - return keys - }, shallow) - - const hasGroupNode = useReactFlowStore((state) => { - return state.getNodes().some(node => node.selected && node.data.type === BlockEnum.Group) - }) - - return useMemo(() => { - const delimiter = '\u0000' - const edges = edgeKeys.map((key) => { - const [source, handleId, target] = key.split(delimiter) - return { - id: key, - source, - sourceHandle: handleId || 'source', - target, - } - }) - - return checkMakeGroupAvailability(selectedNodeIds, edges, hasGroupNode) - }, [edgeKeys, selectedNodeIds, hasGroupNode]) -} diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index f1188309c2..9188c2913c 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -8,7 +8,6 @@ import type { ResizeParamsWithDirection, } from 'reactflow' import type { PluginDefaultValue } from '../block-selector/types' -import type { GroupHandler, GroupMember, GroupNodeData } from '../nodes/group/types' import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' import type { ToolNodeType } from '../nodes/tool/types' @@ -59,7 +58,6 @@ import { useAutoGenerateWebhookUrl } from './use-auto-generate-webhook-url' import { useCollaborativeWorkflow } from './use-collaborative-workflow' import { useHelpline } from './use-helpline' import useInspectVarsCrud from './use-inspect-vars-crud' -import { checkMakeGroupAvailability } from './use-make-group' import { useNodesMetaData } from './use-nodes-meta-data' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { @@ -82,151 +80,6 @@ const ENTRY_NODE_WRAPPER_OFFSET = { y: 21, // Adjusted based on visual testing feedback } as const -/** - * Parse group handler id to get original node id and sourceHandle - * Handler id format: `${nodeId}-${sourceHandle}` - */ -function parseGroupHandlerId(handlerId: string): { originalNodeId: string, originalSourceHandle: string } { - const lastDashIndex = handlerId.lastIndexOf('-') - return { - originalNodeId: handlerId.substring(0, lastDashIndex), - originalSourceHandle: handlerId.substring(lastDashIndex + 1), - } -} - -/** - * Create a pair of edges for group node connections: - * - realEdge: hidden edge from original node to target (persisted to backend) - * - uiEdge: visible temp edge from group to target (UI-only, not persisted) - */ -function createGroupEdgePair(params: { - groupNodeId: string - handlerId: string - targetNodeId: string - targetHandle: string - nodes: Node[] - baseEdgeData?: Partial - zIndex?: number -}): { realEdge: Edge, uiEdge: Edge } | null { - const { groupNodeId, handlerId, targetNodeId, targetHandle, nodes, baseEdgeData = {}, zIndex = 0 } = params - - const groupNode = nodes.find(node => node.id === groupNodeId) - const groupData = groupNode?.data as GroupNodeData | undefined - const handler = groupData?.handlers?.find(h => h.id === handlerId) - - let originalNodeId: string - let originalSourceHandle: string - - if (handler?.nodeId && handler?.sourceHandle) { - originalNodeId = handler.nodeId - originalSourceHandle = handler.sourceHandle - } - else { - const parsed = parseGroupHandlerId(handlerId) - originalNodeId = parsed.originalNodeId - originalSourceHandle = parsed.originalSourceHandle - } - - const originalNode = nodes.find(node => node.id === originalNodeId) - const targetNode = nodes.find(node => node.id === targetNodeId) - - if (!originalNode || !targetNode) - return null - - // Create the real edge (from original node to target) - hidden because original node is in group - const realEdge: Edge = { - id: `${originalNodeId}-${originalSourceHandle}-${targetNodeId}-${targetHandle}`, - type: CUSTOM_EDGE, - source: originalNodeId, - sourceHandle: originalSourceHandle, - target: targetNodeId, - targetHandle, - hidden: true, - data: { - ...baseEdgeData, - sourceType: originalNode.data.type, - targetType: targetNode.data.type, - _hiddenInGroupId: groupNodeId, - }, - zIndex, - } - - // Create the UI edge (from group to target) - temporary, not persisted to backend - const uiEdge: Edge = { - id: `${groupNodeId}-${handlerId}-${targetNodeId}-${targetHandle}`, - type: CUSTOM_EDGE, - source: groupNodeId, - sourceHandle: handlerId, - target: targetNodeId, - targetHandle, - data: { - ...baseEdgeData, - sourceType: BlockEnum.Group, - targetType: targetNode.data.type, - _isTemp: true, - }, - zIndex, - } - - return { realEdge, uiEdge } -} - -function createGroupInboundEdges(params: { - sourceNodeId: string - sourceHandle: string - groupNodeId: string - groupData: GroupNodeData - nodes: Node[] - baseEdgeData?: Partial - zIndex?: number -}): { realEdges: Edge[], uiEdge: Edge } | null { - const { sourceNodeId, sourceHandle, groupNodeId, groupData, nodes, baseEdgeData = {}, zIndex = 0 } = params - - const sourceNode = nodes.find(node => node.id === sourceNodeId) - const headNodeIds = groupData.headNodeIds || [] - - if (!sourceNode || headNodeIds.length === 0) - return null - - const realEdges: Edge[] = headNodeIds.map((headNodeId) => { - const headNode = nodes.find(node => node.id === headNodeId) - return { - id: `${sourceNodeId}-${sourceHandle}-${headNodeId}-target`, - type: CUSTOM_EDGE, - source: sourceNodeId, - sourceHandle, - target: headNodeId, - targetHandle: 'target', - hidden: true, - data: { - ...baseEdgeData, - sourceType: sourceNode.data.type, - targetType: headNode?.data.type, - _hiddenInGroupId: groupNodeId, - }, - zIndex, - } as Edge - }) - - const uiEdge: Edge = { - id: `${sourceNodeId}-${sourceHandle}-${groupNodeId}-target`, - type: CUSTOM_EDGE, - source: sourceNodeId, - sourceHandle, - target: groupNodeId, - targetHandle: 'target', - data: { - ...baseEdgeData, - sourceType: sourceNode.data.type, - targetType: BlockEnum.Group, - _isTemp: true, - }, - zIndex, - } - - return { realEdges, uiEdge } -} - type NodesMetaDataMap = Record const buildNestedDeleteSet = ( @@ -712,146 +565,6 @@ export const useNodesInteractions = () => { return } - // Check if source is a group node - need special handling - const isSourceGroup = sourceNode?.data.type === BlockEnum.Group - - if (isSourceGroup && sourceHandle && target && targetHandle) { - const { originalNodeId, originalSourceHandle } = parseGroupHandlerId(sourceHandle) - - // Check if real edge already exists - if (edges.find(edge => - edge.source === originalNodeId - && edge.sourceHandle === originalSourceHandle - && edge.target === target - && edge.targetHandle === targetHandle, - )) { - return - } - - const parentNode = nodes.find(node => node.id === targetNode?.parentId) - const isInIteration = parentNode && parentNode.data.type === BlockEnum.Iteration - const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop - - const edgePair = createGroupEdgePair({ - groupNodeId: source!, - handlerId: sourceHandle, - targetNodeId: target, - targetHandle, - nodes, - baseEdgeData: { - isInIteration, - iteration_id: isInIteration ? targetNode?.parentId : undefined, - isInLoop, - loop_id: isInLoop ? targetNode?.parentId : undefined, - }, - }) - - if (!edgePair) - return - - const { realEdge, uiEdge } = edgePair - - // Update connected handle ids for the original node - const nodesConnectedSourceOrTargetHandleIdsMap - = getNodesConnectedSourceOrTargetHandleIdsMap( - [{ type: 'add', edge: realEdge }], - nodes, - ) - const newNodes = produce(nodes, (draft: Node[]) => { - draft.forEach((node) => { - if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { - node.data = { - ...node.data, - ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], - } - } - }) - }) - const newEdges = produce(edges, (draft) => { - draft.push(realEdge) - draft.push(uiEdge) - }) - - setNodes(newNodes) - setEdges(newEdges) - - handleSyncWorkflowDraft() - saveStateToHistory(WorkflowHistoryEvent.NodeConnect, { - nodeId: targetNode?.id, - }) - return - } - - const isTargetGroup = targetNode?.data.type === BlockEnum.Group - - if (isTargetGroup && source && sourceHandle) { - const groupData = targetNode.data as GroupNodeData - const headNodeIds = groupData.headNodeIds || [] - - if (edges.find(edge => - edge.source === source - && edge.sourceHandle === sourceHandle - && edge.target === target - && edge.targetHandle === targetHandle, - )) { - return - } - - const parentNode = nodes.find(node => node.id === sourceNode?.parentId) - const isInIteration = parentNode && parentNode.data.type === BlockEnum.Iteration - const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop - - const inboundResult = createGroupInboundEdges({ - sourceNodeId: source, - sourceHandle, - groupNodeId: target!, - groupData, - nodes, - baseEdgeData: { - isInIteration, - iteration_id: isInIteration ? sourceNode?.parentId : undefined, - isInLoop, - loop_id: isInLoop ? sourceNode?.parentId : undefined, - }, - }) - - if (!inboundResult) - return - - const { realEdges, uiEdge } = inboundResult - - const edgeChanges = realEdges.map(edge => ({ type: 'add' as const, edge })) - const nodesConnectedSourceOrTargetHandleIdsMap - = getNodesConnectedSourceOrTargetHandleIdsMap(edgeChanges, nodes) - - const newNodes = produce(nodes, (draft: Node[]) => { - draft.forEach((node) => { - if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { - node.data = { - ...node.data, - ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], - } - } - }) - }) - - const newEdges = produce(edges, (draft) => { - realEdges.forEach((edge) => { - draft.push(edge) - }) - draft.push(uiEdge) - }) - - setNodes(newNodes) - setEdges(newEdges) - - handleSyncWorkflowDraft() - saveStateToHistory(WorkflowHistoryEvent.NodeConnect, { - nodeId: headNodeIds[0], - }) - return - } - if ( edges.find( edge => @@ -1311,34 +1024,8 @@ export const useNodesInteractions = () => { } } - // Check if prevNode is a group node - need special handling - const isPrevNodeGroup = prevNode.data.type === BlockEnum.Group let newEdge: Edge | null = null - let newUiEdge: Edge | null = null - - if (isPrevNodeGroup && prevNodeSourceHandle && nodeType !== BlockEnum.DataSource) { - const edgePair = createGroupEdgePair({ - groupNodeId: prevNodeId, - handlerId: prevNodeSourceHandle, - targetNodeId: newNode.id, - targetHandle, - nodes: [...nodes, newNode], - baseEdgeData: { - isInIteration, - isInLoop, - iteration_id: isInIteration ? prevNode.parentId : undefined, - loop_id: isInLoop ? prevNode.parentId : undefined, - _connectedNodeIsSelected: true, - }, - }) - - if (edgePair) { - newEdge = edgePair.realEdge - newUiEdge = edgePair.uiEdge - } - } - else if (nodeType !== BlockEnum.DataSource) { - // Normal case: prevNode is not a group + if (nodeType !== BlockEnum.DataSource) { newEdge = { id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`, type: CUSTOM_EDGE, @@ -1363,7 +1050,7 @@ export const useNodesInteractions = () => { } } - const edgesToAdd = [newEdge, newUiEdge].filter(Boolean).map(edge => ({ type: 'add' as const, edge: edge! })) + const edgesToAdd = newEdge ? [{ type: 'add' as const, edge: newEdge }] : [] const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( edgesToAdd, @@ -1435,8 +1122,6 @@ export const useNodesInteractions = () => { }) if (newEdge) draft.push(newEdge) - if (newUiEdge) - draft.push(newUiEdge) }) setNodes(newNodes) @@ -1633,113 +1318,41 @@ export const useNodesInteractions = () => { } } - // Check if prevNode is a group node - need special handling - const isPrevNodeGroup = prevNode.data.type === BlockEnum.Group let newPrevEdge: Edge | null = null - let newPrevUiEdge: Edge | null = null const edgesToRemove: string[] = [] - if (isPrevNodeGroup && prevNodeSourceHandle && nodeType !== BlockEnum.DataSource) { - const { originalNodeId, originalSourceHandle } = parseGroupHandlerId(prevNodeSourceHandle) + const currentEdge = edges.find( + edge => edge.source === prevNodeId && edge.target === nextNodeId, + ) + if (currentEdge) + edgesToRemove.push(currentEdge.id) - // Find edges to remove: both hidden real edge and UI temp edge from group to nextNode - const hiddenEdge = edges.find( - edge => edge.source === originalNodeId - && edge.sourceHandle === originalSourceHandle - && edge.target === nextNodeId, - ) - const uiTempEdge = edges.find( - edge => edge.source === prevNodeId - && edge.sourceHandle === prevNodeSourceHandle - && edge.target === nextNodeId, - ) - if (hiddenEdge) - edgesToRemove.push(hiddenEdge.id) - if (uiTempEdge) - edgesToRemove.push(uiTempEdge.id) - - const edgePair = createGroupEdgePair({ - groupNodeId: prevNodeId, - handlerId: prevNodeSourceHandle, - targetNodeId: newNode.id, + if (nodeType !== BlockEnum.DataSource) { + newPrevEdge = { + id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`, + type: CUSTOM_EDGE, + source: prevNodeId, + sourceHandle: prevNodeSourceHandle, + target: newNode.id, targetHandle, - nodes: [...nodes, newNode], - baseEdgeData: { + data: { + sourceType: prevNode.data.type, + targetType: newNode.data.type, isInIteration, isInLoop, iteration_id: isInIteration ? prevNode.parentId : undefined, loop_id: isInLoop ? prevNode.parentId : undefined, _connectedNodeIsSelected: true, }, - }) - - if (edgePair) { - newPrevEdge = edgePair.realEdge - newPrevUiEdge = edgePair.uiEdge - } - } - else { - const isNextNodeGroupForRemoval = nextNode.data.type === BlockEnum.Group - - if (isNextNodeGroupForRemoval) { - const groupData = nextNode.data as GroupNodeData - const headNodeIds = groupData.headNodeIds || [] - - headNodeIds.forEach((headNodeId) => { - const realEdge = edges.find( - edge => edge.source === prevNodeId - && edge.sourceHandle === prevNodeSourceHandle - && edge.target === headNodeId, - ) - if (realEdge) - edgesToRemove.push(realEdge.id) - }) - - const uiEdge = edges.find( - edge => edge.source === prevNodeId - && edge.sourceHandle === prevNodeSourceHandle - && edge.target === nextNodeId, - ) - if (uiEdge) - edgesToRemove.push(uiEdge.id) - } - else { - const currentEdge = edges.find( - edge => edge.source === prevNodeId && edge.target === nextNodeId, - ) - if (currentEdge) - edgesToRemove.push(currentEdge.id) - } - - if (nodeType !== BlockEnum.DataSource) { - newPrevEdge = { - id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`, - type: CUSTOM_EDGE, - source: prevNodeId, - sourceHandle: prevNodeSourceHandle, - target: newNode.id, - targetHandle, - data: { - sourceType: prevNode.data.type, - targetType: newNode.data.type, - isInIteration, - isInLoop, - iteration_id: isInIteration ? prevNode.parentId : undefined, - loop_id: isInLoop ? prevNode.parentId : undefined, - _connectedNodeIsSelected: true, - }, - zIndex: prevNode.parentId - ? isInIteration - ? ITERATION_CHILDREN_Z_INDEX - : LOOP_CHILDREN_Z_INDEX - : 0, - } + zIndex: prevNode.parentId + ? isInIteration + ? ITERATION_CHILDREN_Z_INDEX + : LOOP_CHILDREN_Z_INDEX + : 0, } } let newNextEdge: Edge | null = null - let newNextUiEdge: Edge | null = null - const newNextRealEdges: Edge[] = [] const nextNodeParentNode = nodes.find(node => node.id === nextNode.parentId) || null @@ -1750,104 +1363,41 @@ export const useNodesInteractions = () => { = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Loop - const isNextNodeGroup = nextNode.data.type === BlockEnum.Group - if ( nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.HumanInput && nodeType !== BlockEnum.LoopEnd ) { - if (isNextNodeGroup) { - const groupData = nextNode.data as GroupNodeData - const headNodeIds = groupData.headNodeIds || [] - - headNodeIds.forEach((headNodeId) => { - const headNode = nodes.find(node => node.id === headNodeId) - newNextRealEdges.push({ - id: `${newNode.id}-${sourceHandle}-${headNodeId}-target`, - type: CUSTOM_EDGE, - source: newNode.id, - sourceHandle, - target: headNodeId, - targetHandle: 'target', - hidden: true, - data: { - sourceType: newNode.data.type, - targetType: headNode?.data.type, - isInIteration: isNextNodeInIteration, - isInLoop: isNextNodeInLoop, - iteration_id: isNextNodeInIteration ? nextNode.parentId : undefined, - loop_id: isNextNodeInLoop ? nextNode.parentId : undefined, - _hiddenInGroupId: nextNodeId, - _connectedNodeIsSelected: true, - }, - zIndex: nextNode.parentId - ? isNextNodeInIteration - ? ITERATION_CHILDREN_Z_INDEX - : LOOP_CHILDREN_Z_INDEX - : 0, - } as Edge) - }) - - newNextUiEdge = { - id: `${newNode.id}-${sourceHandle}-${nextNodeId}-target`, - type: CUSTOM_EDGE, - source: newNode.id, - sourceHandle, - target: nextNodeId, - targetHandle: 'target', - data: { - sourceType: newNode.data.type, - targetType: BlockEnum.Group, - isInIteration: isNextNodeInIteration, - isInLoop: isNextNodeInLoop, - iteration_id: isNextNodeInIteration ? nextNode.parentId : undefined, - loop_id: isNextNodeInLoop ? nextNode.parentId : undefined, - _isTemp: true, - _connectedNodeIsSelected: true, - }, - zIndex: nextNode.parentId - ? isNextNodeInIteration - ? ITERATION_CHILDREN_Z_INDEX - : LOOP_CHILDREN_Z_INDEX - : 0, - } - } - else { - newNextEdge = { - id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, - type: CUSTOM_EDGE, - source: newNode.id, - sourceHandle, - target: nextNodeId, - targetHandle: nextNodeTargetHandle, - data: { - sourceType: newNode.data.type, - targetType: nextNode.data.type, - isInIteration: isNextNodeInIteration, - isInLoop: isNextNodeInLoop, - iteration_id: isNextNodeInIteration - ? nextNode.parentId - : undefined, - loop_id: isNextNodeInLoop ? nextNode.parentId : undefined, - _connectedNodeIsSelected: true, - }, - zIndex: nextNode.parentId - ? isNextNodeInIteration - ? ITERATION_CHILDREN_Z_INDEX - : LOOP_CHILDREN_Z_INDEX - : 0, - } + newNextEdge = { + id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, + type: CUSTOM_EDGE, + source: newNode.id, + sourceHandle, + target: nextNodeId, + targetHandle: nextNodeTargetHandle, + data: { + sourceType: newNode.data.type, + targetType: nextNode.data.type, + isInIteration: isNextNodeInIteration, + isInLoop: isNextNodeInLoop, + iteration_id: isNextNodeInIteration + ? nextNode.parentId + : undefined, + loop_id: isNextNodeInLoop ? nextNode.parentId : undefined, + _connectedNodeIsSelected: true, + }, + zIndex: nextNode.parentId + ? isNextNodeInIteration + ? ITERATION_CHILDREN_Z_INDEX + : LOOP_CHILDREN_Z_INDEX + : 0, } } const edgeChanges = [ ...edgesToRemove.map(id => ({ type: 'remove' as const, edge: edges.find(e => e.id === id)! })).filter(c => c.edge), ...(newPrevEdge ? [{ type: 'add' as const, edge: newPrevEdge }] : []), - ...(newPrevUiEdge ? [{ type: 'add' as const, edge: newPrevUiEdge }] : []), ...(newNextEdge ? [{ type: 'add' as const, edge: newNextEdge }] : []), - ...newNextRealEdges.map(edge => ({ type: 'add' as const, edge })), - ...(newNextUiEdge ? [{ type: 'add' as const, edge: newNextUiEdge }] : []), ] const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( @@ -1928,15 +1478,8 @@ export const useNodesInteractions = () => { }) if (newPrevEdge) draft.push(newPrevEdge) - if (newPrevUiEdge) - draft.push(newPrevUiEdge) if (newNextEdge) draft.push(newNextEdge) - newNextRealEdges.forEach((edge) => { - draft.push(edge) - }) - if (newNextUiEdge) - draft.push(newNextUiEdge) }) setEdges(newEdges) } @@ -2686,290 +2229,6 @@ export const useNodesInteractions = () => { return nodes.some(node => node.data._isBundled) }, [collaborativeWorkflow]) - const getCanMakeGroup = useCallback(() => { - const { nodes, edges } = collaborativeWorkflow.getState() - const bundledNodes = nodes.filter(node => node.data._isBundled) - - if (bundledNodes.length <= 1) - return false - - const bundledNodeIds = bundledNodes.map(node => node.id) - const minimalEdges = edges.map(edge => ({ - id: edge.id, - source: edge.source, - sourceHandle: edge.sourceHandle || 'source', - target: edge.target, - })) - const hasGroupNode = bundledNodes.some(node => node.data.type === BlockEnum.Group) - - const { canMakeGroup } = checkMakeGroupAvailability(bundledNodeIds, minimalEdges, hasGroupNode) - return canMakeGroup - }, [collaborativeWorkflow]) - - const handleMakeGroup = useCallback(() => { - const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() - const bundledNodes = nodes.filter(node => node.data._isBundled) - - if (bundledNodes.length <= 1) - return - - const bundledNodeIds = bundledNodes.map(node => node.id) - const minimalEdges = edges.map(edge => ({ - id: edge.id, - source: edge.source, - sourceHandle: edge.sourceHandle || 'source', - target: edge.target, - })) - const hasGroupNode = bundledNodes.some(node => node.data.type === BlockEnum.Group) - - const { canMakeGroup } = checkMakeGroupAvailability(bundledNodeIds, minimalEdges, hasGroupNode) - if (!canMakeGroup) - return - - const bundledNodeIdSet = new Set(bundledNodeIds) - const bundledNodeIdIsLeaf = new Set() - const inboundEdges = edges.filter(edge => !bundledNodeIdSet.has(edge.source) && bundledNodeIdSet.has(edge.target)) - const outboundEdges = edges.filter(edge => bundledNodeIdSet.has(edge.source) && !bundledNodeIdSet.has(edge.target)) - - // leaf node: no outbound edges to other nodes in the selection - const handlers: GroupHandler[] = [] - const leafNodeIdSet = new Set() - - bundledNodes.forEach((node: Node) => { - const targetBranches = node.data._targetBranches || [{ id: 'source', name: node.data.title }] - targetBranches.forEach((branch) => { - // A branch should be a handler if it's either: - // 1. Connected to a node OUTSIDE the group - // 2. NOT connected to any node INSIDE the group - const isConnectedInside = edges.some(edge => - edge.source === node.id - && (edge.sourceHandle === branch.id || (!edge.sourceHandle && branch.id === 'source')) - && bundledNodeIdSet.has(edge.target), - ) - const isConnectedOutside = edges.some(edge => - edge.source === node.id - && (edge.sourceHandle === branch.id || (!edge.sourceHandle && branch.id === 'source')) - && !bundledNodeIdSet.has(edge.target), - ) - - if (isConnectedOutside || !isConnectedInside) { - const handlerId = `${node.id}-${branch.id}` - handlers.push({ - id: handlerId, - label: branch.name || node.data.title || node.id, - nodeId: node.id, - sourceHandle: branch.id, - }) - leafNodeIdSet.add(node.id) - } - }) - }) - - const leafNodeIds = Array.from(leafNodeIdSet) - leafNodeIds.forEach(id => bundledNodeIdIsLeaf.add(id)) - - const members: GroupMember[] = bundledNodes.map((node) => { - return { - id: node.id, - type: node.data.type, - label: node.data.title, - } - }) - - // head nodes: nodes that receive input from outside the group - const headNodeIds = [...new Set(inboundEdges.map(edge => edge.target))] - - // put the group node at the top-left corner of the selection, slightly offset - const { x: minX, y: minY } = getTopLeftNodePosition(bundledNodes) - - const groupNodeData: GroupNodeData = { - title: t('operator.makeGroup', { ns: 'workflow' }), - desc: '', - type: BlockEnum.Group, - members, - handlers, - headNodeIds, - leafNodeIds, - selected: true, - _targetBranches: handlers.map(handler => ({ - id: handler.id, - name: handler.label || handler.id, - })), - } - - const { newNode: groupNode } = generateNewNode({ - data: groupNodeData, - position: { - x: minX - 20, - y: minY - 20, - }, - }) - - const nodeTypeMap = new Map(nodes.map(node => [node.id, node.data.type])) - - const newNodes = produce(nodes, (draft) => { - draft.forEach((node) => { - if (bundledNodeIdSet.has(node.id)) { - node.data._isBundled = false - node.selected = false - node.hidden = true - node.data._hiddenInGroupId = groupNode.id - } - else { - node.data._isBundled = false - } - }) - draft.push(groupNode) - }) - - const newEdges = produce(edges, (draft) => { - draft.forEach((edge) => { - if (bundledNodeIdSet.has(edge.source) || bundledNodeIdSet.has(edge.target)) { - edge.hidden = true - edge.data = { - ...edge.data, - _hiddenInGroupId: groupNode.id, - _isBundled: false, - } - } - else if (edge.data?._isBundled) { - edge.data._isBundled = false - } - }) - - // re-add the external inbound edges to the group node as UI-only edges (not persisted to backend) - inboundEdges.forEach((edge) => { - draft.push({ - id: `${edge.id}__to-${groupNode.id}`, - type: edge.type || CUSTOM_EDGE, - source: edge.source, - target: groupNode.id, - sourceHandle: edge.sourceHandle, - targetHandle: 'target', - data: { - ...edge.data, - sourceType: nodeTypeMap.get(edge.source)!, - targetType: BlockEnum.Group, - _hiddenInGroupId: undefined, - _isBundled: false, - _isTemp: true, // UI-only edge, not persisted to backend - }, - zIndex: edge.zIndex, - }) - }) - - // outbound edges of the group node as UI-only edges (not persisted to backend) - outboundEdges.forEach((edge) => { - if (!bundledNodeIdIsLeaf.has(edge.source)) - return - - // Use the same handler id format: nodeId-sourceHandle - const originalSourceHandle = edge.sourceHandle || 'source' - const handlerId = `${edge.source}-${originalSourceHandle}` - - draft.push({ - id: `${groupNode.id}-${edge.target}-${edge.targetHandle || 'target'}-${handlerId}`, - type: edge.type || CUSTOM_EDGE, - source: groupNode.id, - target: edge.target, - sourceHandle: handlerId, - targetHandle: edge.targetHandle, - data: { - ...edge.data, - sourceType: BlockEnum.Group, - targetType: nodeTypeMap.get(edge.target)!, - _hiddenInGroupId: undefined, - _isBundled: false, - _isTemp: true, - }, - zIndex: edge.zIndex, - }) - }) - }) - - setNodes(newNodes) - setEdges(newEdges) - workflowStore.setState({ - selectionMenu: undefined, - }) - handleSyncWorkflowDraft() - saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { - nodeId: groupNode.id, - }) - }, [handleSyncWorkflowDraft, saveStateToHistory, collaborativeWorkflow, t, workflowStore]) - - // check if the current selection can be ungrouped (single selected Group node) - const getCanUngroup = useCallback(() => { - const { nodes } = collaborativeWorkflow.getState() - const selectedNodes = nodes.filter(node => node.selected) - - if (selectedNodes.length !== 1) - return false - - return selectedNodes[0].data.type === BlockEnum.Group - }, [collaborativeWorkflow]) - - // get the selected group node id for ungroup operation - const getSelectedGroupId = useCallback(() => { - const { nodes } = collaborativeWorkflow.getState() - const selectedNodes = nodes.filter(node => node.selected) - - if (selectedNodes.length === 1 && selectedNodes[0].data.type === BlockEnum.Group) - return selectedNodes[0].id - - return undefined - }, [collaborativeWorkflow]) - - const handleUngroup = useCallback((groupId: string) => { - const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() - const groupNode = nodes.find(n => n.id === groupId) - - if (!groupNode || groupNode.data.type !== BlockEnum.Group) - return - - const memberIds = new Set((groupNode.data.members || []).map((m: { id: string }) => m.id)) - - // restore hidden member nodes - const newNodes = produce(nodes, (draft) => { - draft.forEach((node) => { - if (memberIds.has(node.id)) { - node.hidden = false - delete node.data._hiddenInGroupId - } - }) - // remove group node - const groupIndex = draft.findIndex(n => n.id === groupId) - if (groupIndex !== -1) - draft.splice(groupIndex, 1) - }) - - // restore hidden edges and remove temp edges in single pass O(E) - const newEdges = produce(edges, (draft) => { - const indicesToRemove: number[] = [] - - for (let i = 0; i < draft.length; i++) { - const edge = draft[i] - // restore hidden edges that involve member nodes - if (edge.hidden && (memberIds.has(edge.source) || memberIds.has(edge.target))) - edge.hidden = false - // collect temp edges connected to group for removal - if (edge.data?._isTemp && (edge.source === groupId || edge.target === groupId)) - indicesToRemove.push(i) - } - - // remove collected indices in reverse order to avoid index shift - for (let i = indicesToRemove.length - 1; i >= 0; i--) - draft.splice(indicesToRemove[i], 1) - }) - - setNodes(newNodes) - setEdges(newEdges) - handleSyncWorkflowDraft() - saveStateToHistory(WorkflowHistoryEvent.NodeDelete, { - nodeId: groupId, - }) - }, [handleSyncWorkflowDraft, saveStateToHistory, collaborativeWorkflow]) - return { handleNodeDragStart, handleNodeDrag, @@ -2990,8 +2249,6 @@ export const useNodesInteractions = () => { handleNodesPaste, handleNodesDuplicate, handleNodesDelete, - handleMakeGroup, - handleUngroup, handleNodeResize, handleNodeDisconnect, handleHistoryBack, @@ -2999,8 +2256,5 @@ export const useNodesInteractions = () => { dimOtherNodes, undimAllNodes, hasBundledNodes, - getCanMakeGroup, - getCanUngroup, - getSelectedGroupId, } } diff --git a/web/app/components/workflow/hooks/use-nodes-meta-data.ts b/web/app/components/workflow/hooks/use-nodes-meta-data.ts index 36c071f4d4..2ea2fd9e9f 100644 --- a/web/app/components/workflow/hooks/use-nodes-meta-data.ts +++ b/web/app/components/workflow/hooks/use-nodes-meta-data.ts @@ -1,10 +1,8 @@ import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store' import type { Node } from '@/app/components/workflow/types' import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' import { CollectionType } from '@/app/components/tools/types' import { useHooksStore } from '@/app/components/workflow/hooks-store' -import GroupDefault from '@/app/components/workflow/nodes/group/default' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' import { useGetLanguage } from '@/context/i18n' @@ -27,7 +25,6 @@ export const useNodesMetaData = () => { } export const useNodeMetaData = (node: Node) => { - const { t } = useTranslation() const language = useGetLanguage() const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() @@ -37,9 +34,6 @@ export const useNodeMetaData = (node: Node) => { const { data } = node const nodeMetaData = availableNodesMetaData.nodesMap?.[data.type] const author = useMemo(() => { - if (data.type === BlockEnum.Group) - return GroupDefault.metaData.author - if (data.type === BlockEnum.DataSource) return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.author @@ -54,9 +48,6 @@ export const useNodeMetaData = (node: Node) => { }, [data, buildInTools, customTools, workflowTools, nodeMetaData, dataSourceList]) const description = useMemo(() => { - if (data.type === BlockEnum.Group) - return t('blocksAbout.group', { ns: 'workflow' }) - if (data.type === BlockEnum.DataSource) return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.description[language] if (data.type === BlockEnum.Tool) { @@ -67,7 +58,7 @@ export const useNodeMetaData = (node: Node) => { return customTools?.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language] } return nodeMetaData?.metaData.description - }, [data, buildInTools, customTools, workflowTools, nodeMetaData, dataSourceList, language, t]) + }, [data, buildInTools, customTools, workflowTools, nodeMetaData, dataSourceList, language]) return useMemo(() => { return { diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 7f91d3ab1e..1de892d8e3 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -29,11 +29,6 @@ export const useShortcuts = (enabled = true): void => { dimOtherNodes, undimAllNodes, hasBundledNodes, - getCanMakeGroup, - handleMakeGroup, - getCanUngroup, - getSelectedGroupId, - handleUngroup, } = useNodesInteractions() const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() const { handleSyncWorkflowDraft } = useNodesSyncDraft() @@ -113,26 +108,6 @@ export const useShortcuts = (enabled = true): void => { } }, { exactMatch: true, useCapture: true }) - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.g`, (e) => { - // Only intercept when the selection can be grouped - if (shouldHandleShortcut(e) && getCanMakeGroup()) { - e.preventDefault() - // Close selection context menu if open - workflowStore.setState({ selectionMenu: undefined }) - handleMakeGroup() - } - }, { exactMatch: true, useCapture: true }) - - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.g`, (e) => { - // Only intercept when the selection can be ungrouped - if (shouldHandleShortcut(e) && getCanUngroup()) { - e.preventDefault() - const groupId = getSelectedGroupId() - if (groupId) - handleUngroup(groupId) - } - }, { exactMatch: true, useCapture: true }) - useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => { if (shouldHandleShortcut(e)) { e.preventDefault() diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index d175db232d..5e4bf09296 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -1,10 +1,10 @@ import type { Connection, } from 'reactflow' -import type { GroupNodeData } from '../nodes/group/types' import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' import type { + BlockEnum, Edge, Node, ValueSelector, @@ -32,8 +32,7 @@ import { useStore, useWorkflowStore, } from '../store' - -import { BlockEnum, WorkflowRunningStatus } from '../types' +import { WorkflowRunningStatus } from '../types' import { getWorkflowEntryNode, isWorkflowEntryNode, @@ -346,7 +345,7 @@ export const useWorkflow = () => { return startNodes }, [nodesMap, getRootNodesById]) - const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => { + const isValidConnection = useCallback(({ source, target }: Connection) => { const { nodes, edges } = collaborativeWorkflow.getState() const sourceNode: Node = nodes.find(node => node.id === source)! const targetNode: Node = nodes.find(node => node.id === target)! @@ -357,42 +356,15 @@ export const useWorkflow = () => { if (sourceNode.parentId !== targetNode.parentId) return false - // For Group nodes, use the leaf node's type for validation - // sourceHandle format: "${leafNodeId}-${originalSourceHandle}" - let actualSourceType = sourceNode.data.type - if (sourceNode.data.type === BlockEnum.Group && sourceHandle) { - const lastDashIndex = sourceHandle.lastIndexOf('-') - if (lastDashIndex > 0) { - const leafNodeId = sourceHandle.substring(0, lastDashIndex) - const leafNode = nodes.find(node => node.id === leafNodeId) - if (leafNode) - actualSourceType = leafNode.data.type - } - } - if (sourceNode && targetNode) { - const sourceNodeAvailableNextNodes = getAvailableBlocks(actualSourceType, !!sourceNode.parentId).availableNextBlocks + const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks - if (targetNode.data.type === BlockEnum.Group) { - const groupData = targetNode.data as GroupNodeData - const headNodeIds = groupData.headNodeIds || [] - if (headNodeIds.length > 0) { - const headNode = nodes.find(node => node.id === headNodeIds[0]) - if (headNode) { - const headNodeAvailablePrevNodes = getAvailableBlocks(headNode.data.type, !!targetNode.parentId).availablePrevBlocks - if (!headNodeAvailablePrevNodes.includes(actualSourceType)) - return false - } - } - } - else { - if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type)) - return false + if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type)) + return false - if (!targetNodeAvailablePrevNodes.includes(actualSourceType)) - return false - } + if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type)) + return false } const hasCycle = (node: Node, visited = new Set()) => { diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 69b4cd0e75..7eb03de56d 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -66,14 +66,6 @@ import { } from './constants' import CustomConnectionLine from './custom-connection-line' import CustomEdge from './custom-edge' -import { - CUSTOM_GROUP_EXIT_PORT_NODE, - CUSTOM_GROUP_INPUT_NODE, - CUSTOM_GROUP_NODE, - CustomGroupExitPortNode, - CustomGroupInputNode, - CustomGroupNode, -} from './custom-group-node' import DatasetsDetailProvider from './datasets-detail-store/provider' import HelpLine from './help-line' import { @@ -139,9 +131,6 @@ const nodeTypes = { [CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode, [CUSTOM_LOOP_START_NODE]: CustomLoopStartNode, [CUSTOM_DATA_SOURCE_EMPTY_NODE]: CustomDataSourceEmptyNode, - [CUSTOM_GROUP_NODE]: CustomGroupNode, - [CUSTOM_GROUP_INPUT_NODE]: CustomGroupInputNode, - [CUSTOM_GROUP_EXIT_PORT_NODE]: CustomGroupExitPortNode, } const edgeTypes = { [CUSTOM_EDGE]: CustomEdge, diff --git a/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx b/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx index c095f7fcb3..b460aa651c 100644 --- a/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx @@ -41,14 +41,13 @@ const PanelOperatorPopup = ({ handleNodesDuplicate, handleNodeSelect, handleNodesCopy, - handleUngroup, } = useNodesInteractions() const { handleNodeDataUpdate } = useNodeDataUpdate() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { nodesReadOnly } = useNodesReadOnly() const edge = edges.find(edge => edge.target === id) const nodeMetaData = useNodeMetaData({ id, data } as Node) - const showChangeBlock = !nodeMetaData.isTypeFixed && !nodesReadOnly && data.type !== BlockEnum.Group + const showChangeBlock = !nodeMetaData.isTypeFixed && !nodesReadOnly const isChildNode = !!(data.isInIteration || data.isInLoop) const { data: workflowTools } = useAllWorkflowTools() @@ -62,25 +61,6 @@ const PanelOperatorPopup = ({ return (
- { - !nodesReadOnly && data.type === BlockEnum.Group && ( - <> -
-
{ - onClosePopup() - handleUngroup(id) - }} - > - {t('panel.ungroup', { ns: 'workflow' })} - -
-
-
- - ) - } { (showChangeBlock || canRunBySingle(data.type, isChildNode)) && ( <> diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 43b954027f..d0f5c43e24 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -651,7 +651,7 @@ const BasePanel: FC = ({ ) } { - !needsToolAuth && !currentDataSource && !currentTriggerPlugin && data.type !== BlockEnum.Group && ( + !needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
= ({
) } - {data.type !== BlockEnum.Group && } +
- {(tabType === TabType.settings || data.type === BlockEnum.Group) && ( + {tabType === TabType.settings && (
{cloneElement(children as any, { diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts index dc36ab8e99..0cf169a6fd 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -67,7 +67,6 @@ const singleRunFormParamsHooks: Record = { [BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams, [BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams, [BlockEnum.KnowledgeBase]: useKnowledgeBaseSingleRunFormParams, - [BlockEnum.Group]: undefined, [BlockEnum.VariableAssigner]: undefined, [BlockEnum.End]: undefined, [BlockEnum.Answer]: undefined, @@ -119,7 +118,6 @@ const getDataForCheckMoreHooks: Record = { [BlockEnum.DataSource]: undefined, [BlockEnum.DataSourceEmpty]: undefined, [BlockEnum.KnowledgeBase]: undefined, - [BlockEnum.Group]: undefined, [BlockEnum.TriggerWebhook]: undefined, [BlockEnum.TriggerSchedule]: undefined, [BlockEnum.TriggerPlugin]: useTriggerPluginGetDataForCheckMore, diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 7fec1615b2..f89887c64f 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -300,7 +300,7 @@ const BaseNode: FC = ({ ) } { - data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.Group && data.type !== BlockEnum.HumanInput && !data._isCandidate && ( + data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && ( > = { [BlockEnum.TriggerPlugin]: TriggerPluginNode, [BlockEnum.Command]: CommandNode, [BlockEnum.FileUpload]: FileUploadNode, - [BlockEnum.Group]: GroupNode, } export const PanelComponentMap: Record> = { @@ -118,5 +115,4 @@ export const PanelComponentMap: Record> = { [BlockEnum.TriggerPlugin]: TriggerPluginPanel, [BlockEnum.Command]: CommandPanel, [BlockEnum.FileUpload]: FileUploadPanel, - [BlockEnum.Group]: GroupPanel, } diff --git a/web/app/components/workflow/nodes/group/default.ts b/web/app/components/workflow/nodes/group/default.ts deleted file mode 100644 index b46d3544b6..0000000000 --- a/web/app/components/workflow/nodes/group/default.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { NodeDefault } from '../../types' -import type { GroupNodeData } from './types' -import { BlockEnum } from '@/app/components/workflow/types' -import { genNodeMetaData } from '@/app/components/workflow/utils' - -const metaData = genNodeMetaData({ - sort: 100, - type: BlockEnum.Group, -}) - -const nodeDefault: NodeDefault = { - metaData, - defaultValue: { - members: [], - handlers: [], - headNodeIds: [], - leafNodeIds: [], - }, - checkValid() { - return { - isValid: true, - } - }, -} - -export default nodeDefault diff --git a/web/app/components/workflow/nodes/group/node.tsx b/web/app/components/workflow/nodes/group/node.tsx deleted file mode 100644 index a42515bc10..0000000000 --- a/web/app/components/workflow/nodes/group/node.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import type { GroupHandler, GroupMember, GroupNodeData } from './types' -import type { BlockEnum, NodeProps } from '@/app/components/workflow/types' -import { RiArrowRightSLine } from '@remixicon/react' -import { memo, useMemo } from 'react' -import BlockIcon from '@/app/components/workflow/block-icon' -import { cn } from '@/utils/classnames' -import { NodeSourceHandle } from '../_base/components/node-handle' - -const MAX_MEMBER_ICONS = 12 - -const GroupNode = (props: NodeProps) => { - const { data } = props - - // show the explicitly passed members first; otherwise use the _children information to fill the type - const members: GroupMember[] = useMemo(() => ( - data.members?.length - ? data.members - : data._children?.length - ? data._children.map(child => ({ - id: child.nodeId, - type: child.nodeType as BlockEnum, - label: child.nodeType, - })) - : [] - ), [data._children, data.members]) - - const handlers: GroupHandler[] = useMemo(() => ( - data.handlers?.length - ? data.handlers - : members.length - ? members.map(member => ({ - id: `${member.id}-source`, - label: member.label || member.id, - nodeId: member.id, - sourceHandle: 'source', - })) - : [] - ), [data.handlers, members]) - - return ( -
- {members.length > 0 && ( -
-
- {members.slice(0, MAX_MEMBER_ICONS).map(member => ( -
- -
- ))} - {members.length > MAX_MEMBER_ICONS && ( -
- + - {members.length - MAX_MEMBER_ICONS} -
- )} -
- -
- )} - {handlers.length > 0 && ( -
- {handlers.map(handler => ( -
- {handler.label || handler.id} - -
- ))} -
- )} -
- ) -} - -GroupNode.displayName = 'GroupNode' - -export default memo(GroupNode) diff --git a/web/app/components/workflow/nodes/group/panel.tsx b/web/app/components/workflow/nodes/group/panel.tsx deleted file mode 100644 index a36d074e9d..0000000000 --- a/web/app/components/workflow/nodes/group/panel.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { memo } from 'react' - -const GroupPanel = () => { - return null -} - -GroupPanel.displayName = 'GroupPanel' - -export default memo(GroupPanel) diff --git a/web/app/components/workflow/nodes/group/types.ts b/web/app/components/workflow/nodes/group/types.ts deleted file mode 100644 index 5f16b0e981..0000000000 --- a/web/app/components/workflow/nodes/group/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { BlockEnum, CommonNodeType } from '../../types' - -export type GroupMember = { - id: string - type: BlockEnum - label?: string -} - -export type GroupHandler = { - id: string - label?: string - nodeId?: string // leaf node id for multi-branch nodes - sourceHandle?: string // original sourceHandle (e.g., case_id for if-else) -} - -export type GroupNodeData = CommonNodeType<{ - members?: GroupMember[] - handlers?: GroupHandler[] - headNodeIds?: string[] // nodes that receive input from outside the group - leafNodeIds?: string[] // nodes that send output to outside the group -}> diff --git a/web/app/components/workflow/selection-contextmenu.tsx b/web/app/components/workflow/selection-contextmenu.tsx index 849fcffb25..5b0c68fe5d 100644 --- a/web/app/components/workflow/selection-contextmenu.tsx +++ b/web/app/components/workflow/selection-contextmenu.tsx @@ -23,7 +23,6 @@ import { shallow } from 'zustand/shallow' import Tooltip from '@/app/components/base/tooltip' import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow' import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks' -import { useMakeGroupAvailability } from './hooks/use-make-group' import { useSelectionInteractions } from './hooks/use-selection-interactions' import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history' import ShortcutsName from './shortcuts-name' @@ -86,7 +85,6 @@ const SelectionContextmenu = () => { handleNodesCopy, handleNodesDuplicate, handleNodesDelete, - handleMakeGroup, } = useNodesInteractions() const selectionMenu = useStore(s => s.selectionMenu) @@ -100,8 +98,6 @@ const SelectionContextmenu = () => { return ids }, shallow) - const { canMakeGroup } = useMakeGroupAvailability(selectedNodeIds) - const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { saveStateToHistory } = useWorkflowHistory() @@ -434,25 +430,6 @@ const SelectionContextmenu = () => {
{!nodesReadOnly && ( <> -
-
{ - if (!canMakeGroup) - return - handleMakeGroup() - handleSelectionContextmenuCancel() - }} - > - {t('operator.makeGroup', { ns: 'workflow' })} - -
-
-
= { _isEntering?: boolean _showAddVariablePopup?: boolean _holdAddVariablePopup?: boolean - _hiddenInGroupId?: string _iterationLength?: number _iterationIndex?: number _waitingRun?: boolean @@ -128,7 +126,6 @@ export type CommonEdgeType = { _connectedNodeIsHovering?: boolean _connectedNodeIsSelected?: boolean _isBundled?: boolean - _hiddenInGroupId?: string _sourceRunningStatus?: NodeRunningStatus _targetRunningStatus?: NodeRunningStatus _waitingRun?: boolean diff --git a/web/app/components/workflow/utils/workflow-init.ts b/web/app/components/workflow/utils/workflow-init.ts index b6c29402d1..4143b031f5 100644 --- a/web/app/components/workflow/utils/workflow-init.ts +++ b/web/app/components/workflow/utils/workflow-init.ts @@ -1,5 +1,3 @@ -import type { CustomGroupNodeData } from '../custom-group-node' -import type { GroupNodeData } from '../nodes/group/types' import type { IfElseNodeType } from '../nodes/if-else/types' import type { IterationNodeType } from '../nodes/iteration/types' import type { LLMNodeType } from '../nodes/llm/types' @@ -20,7 +18,6 @@ import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION, } from '../constants' -import { CUSTOM_GROUP_NODE, GROUP_CHILDREN_Z_INDEX } from '../custom-group-node' import { branchNameCorrect } from '../nodes/if-else/utils' import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants' import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants' @@ -93,16 +90,10 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => { node => node.data.type === BlockEnum.Iteration, ) const hasLoopNode = nodes.some(node => node.data.type === BlockEnum.Loop) - const hasGroupNode = nodes.some(node => node.type === CUSTOM_GROUP_NODE) - const hasBusinessGroupNode = nodes.some( - node => node.data.type === BlockEnum.Group, - ) if ( !hasIterationNode && !hasLoopNode - && !hasGroupNode - && !hasBusinessGroupNode ) { return { nodes, @@ -231,137 +222,9 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => { } }) - // Derive Group internal edges (input → entries, leaves → exits) - const groupInternalEdges: Edge[] = [] - const groupNodes = nodes.filter(node => node.type === CUSTOM_GROUP_NODE) - - for (const groupNode of groupNodes) { - const groupData = groupNode.data as unknown as CustomGroupNodeData - const { group } = groupData - - if (!group) - continue - - const { inputNodeId, entryNodeIds, exitPorts } = group - - // Derive edges: input → each entry node - for (const entryId of entryNodeIds) { - const entryNode = nodesMap[entryId] - if (entryNode) { - groupInternalEdges.push({ - id: `group-internal-${inputNodeId}-source-${entryId}-target`, - type: 'custom', - source: inputNodeId, - sourceHandle: 'source', - target: entryId, - targetHandle: 'target', - data: { - sourceType: '' as any, // Group input has empty type - targetType: entryNode.data.type, - _isGroupInternal: true, - _groupId: groupNode.id, - }, - zIndex: GROUP_CHILDREN_Z_INDEX, - } as Edge) - } - } - - // Derive edges: each leaf node → exit port - for (const exitPort of exitPorts) { - const leafNode = nodesMap[exitPort.leafNodeId] - if (leafNode) { - groupInternalEdges.push({ - id: `group-internal-${exitPort.leafNodeId}-${exitPort.sourceHandle}-${exitPort.portNodeId}-target`, - type: 'custom', - source: exitPort.leafNodeId, - sourceHandle: exitPort.sourceHandle, - target: exitPort.portNodeId, - targetHandle: 'target', - data: { - sourceType: leafNode.data.type, - targetType: '' as string, // Exit port has empty type - _isGroupInternal: true, - _groupId: groupNode.id, - }, - zIndex: GROUP_CHILDREN_Z_INDEX, - } as Edge) - } - } - } - - // Rebuild isTemp edges for business Group nodes (BlockEnum.Group) - // These edges connect the group node to external nodes for visual display - const groupTempEdges: Edge[] = [] - const inboundEdgeIds = new Set() - - nodes.forEach((groupNode) => { - if (groupNode.data.type !== BlockEnum.Group) - return - - const groupData = groupNode.data as GroupNodeData - const { - members = [], - headNodeIds = [], - leafNodeIds = [], - handlers = [], - } = groupData - const memberSet = new Set(members.map(m => m.id)) - const headSet = new Set(headNodeIds) - const leafSet = new Set(leafNodeIds) - - edges.forEach((edge) => { - // Inbound edge: source outside group, target is a head node - // Use Set to dedupe since multiple head nodes may share same external source - if (!memberSet.has(edge.source) && headSet.has(edge.target)) { - const sourceHandle = edge.sourceHandle || 'source' - const edgeId = `${edge.source}-${sourceHandle}-${groupNode.id}-target` - if (!inboundEdgeIds.has(edgeId)) { - inboundEdgeIds.add(edgeId) - groupTempEdges.push({ - id: edgeId, - type: 'custom', - source: edge.source, - sourceHandle, - target: groupNode.id, - targetHandle: 'target', - data: { - sourceType: edge.data?.sourceType, - targetType: BlockEnum.Group, - _isTemp: true, - }, - } as Edge) - } - } - - // Outbound edge: source is a leaf node, target outside group - if (leafSet.has(edge.source) && !memberSet.has(edge.target)) { - const edgeSourceHandle = edge.sourceHandle || 'source' - const handler = handlers.find( - h => - h.nodeId === edge.source && h.sourceHandle === edgeSourceHandle, - ) - if (handler) { - groupTempEdges.push({ - id: `${groupNode.id}-${handler.id}-${edge.target}-${edge.targetHandle}`, - type: 'custom', - source: groupNode.id, - sourceHandle: handler.id, - target: edge.target!, - targetHandle: edge.targetHandle, - data: { - sourceType: BlockEnum.Group, - targetType: edge.data?.targetType, - _isTemp: true, - }, - } as Edge) - } - } - }) - }) - return { nodes: [...nodes, ...newIterationStartNodes, ...newLoopStartNodes], - edges: [...edges, ...newEdges, ...groupInternalEdges, ...groupTempEdges], + edges: [...edges, ...newEdges], } } @@ -449,16 +312,6 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { }) } - if (node.data.type === BlockEnum.Group) { - const groupData = node.data as GroupNodeData - if (groupData.handlers?.length) { - node.data._targetBranches = groupData.handlers.map(handler => ({ - id: handler.id, - name: handler.label || handler.id, - })) - } - } - if (node.data.type === BlockEnum.Iteration) { const iterationNodeData = node.data as IterationNodeType iterationNodeData._children = iterationOrLoopNodeMap[node.id] || []