diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.test.ts index 5c45ab5c1cb..4d13e4450c6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.test.ts @@ -4,12 +4,39 @@ import { describe, expect, it } from 'vitest' import { getArrowNavigationDirection, + getWorkflowCanvasInteractionPolicy, isPositionalTriggerBlock, reconcileCanvasEdges, reconcileCanvasNodes, shouldHighlightContainerDropTarget, } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers' +describe('getWorkflowCanvasInteractionPolicy', () => { + it('allows position changes and re-parenting in the editable workflow editor', () => { + expect(getWorkflowCanvasInteractionPolicy({ embedded: false, canEdit: true })).toEqual({ + canDragNodes: true, + canReparentNodes: true, + }) + }) + + it('allows position changes without re-parenting in an editable embedded canvas', () => { + expect(getWorkflowCanvasInteractionPolicy({ embedded: true, canEdit: true })).toEqual({ + canDragNodes: true, + canReparentNodes: false, + }) + }) + + it.each([false, true])( + 'disables dragging when edit access is denied (embedded=%s)', + (embedded) => { + expect(getWorkflowCanvasInteractionPolicy({ embedded, canEdit: false })).toEqual({ + canDragNodes: false, + canReparentNodes: false, + }) + } + ) +}) + describe('getArrowNavigationDirection', () => { it('moves once for a fresh horizontal arrow press', () => { expect( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts index f740c4baab3..05f639742cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts @@ -7,6 +7,22 @@ import type { BlockState } from '@/stores/workflows/workflow/types' export const SUBFLOW_DROP_TARGET_CLASS = 'subflow-node-drop-target' +interface WorkflowCanvasInteractionPolicyInput { + embedded: boolean + canEdit: boolean +} + +/** Separates position editing from structural re-parenting for embedded canvases. */ +export function getWorkflowCanvasInteractionPolicy({ + embedded, + canEdit, +}: WorkflowCanvasInteractionPolicyInput) { + return { + canDragNodes: canEdit, + canReparentNodes: canEdit && !embedded, + } as const +} + type ArrowNavigationEvent = Pick< KeyboardEvent, 'key' | 'repeat' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 53687ea837f..5ebbfcfea21 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -92,6 +92,7 @@ import { getEdgeSelectionContextId, getNodeSelectionContextId, getRunFromBlockDependencyState, + getWorkflowCanvasInteractionPolicy, getWorkflowLockToggleIds, isBlockProtected, isEdgeProtected, @@ -732,6 +733,10 @@ const WorkflowContent = React.memo( } return userPermissions }, [userPermissions, currentWorkflow.isSnapshotView, workflowReadOnly]) + const { canDragNodes, canReparentNodes } = getWorkflowCanvasInteractionPolicy({ + embedded: embedded === true, + canEdit: effectivePermissions.canEdit === true, + }) const { collaborativeBatchAddEdges, collaborativeBatchRemoveEdges, @@ -2869,7 +2874,7 @@ const WorkflowContent = React.memo( parentId: block.data?.parentId, extent: block.data?.extent || undefined, dragHandle: '.workflow-drag-handle', - draggable: !workflowReadOnly && !isBlockProtected(block.id, blocks), + draggable: canDragNodes && !isBlockProtected(block.id, blocks), zIndex: depth, data: { ...block.data, @@ -2914,7 +2919,7 @@ const WorkflowContent = React.memo( position, parentId: block.data?.parentId, dragHandle, - draggable: !workflowReadOnly && !isBlockProtected(block.id, blocks), + draggable: canDragNodes && !isBlockProtected(block.id, blocks), zIndex: cardZIndex, extent: (() => { // Clamp children to subflow body (exclude header) @@ -2968,6 +2973,7 @@ const WorkflowContent = React.memo( isDebugging, getBlockConfig, embedded, + canDragNodes, workflowReadOnly, collaborativeSetBlockErrorEnabled, collaborativeBatchRemoveEdges, @@ -3677,7 +3683,7 @@ const WorkflowContent = React.memo( // paths bail when potentialParentId still equals the drag-start parent, so // positions persist but a block can never be inserted into (or pulled out // of) a loop/parallel from the embedded view. - if (embedded) return + if (!canReparentNodes) return // Check if this is a starter block - starter blocks should never be in containers const isStarterBlock = node.data?.type === 'starter' @@ -3804,7 +3810,7 @@ const WorkflowContent = React.memo( getNodes, potentialParentId, blocks, - embedded, + canReparentNodes, getNodeAbsolutePosition, getNodeDepth, isDescendantOf, @@ -5172,16 +5178,14 @@ const WorkflowContent = React.memo( multiSelectionKeyCode={embedded ? null : ['Meta', 'Control', 'Shift']} nodesConnectable={!embedded && effectivePermissions.canEdit} connectOnClick={false} - nodesDraggable={!embedded && effectivePermissions.canEdit} + nodesDraggable={canDragNodes} draggable={false} noWheelClassName='allow-scroll' edgesFocusable={!embedded} edgesUpdatable={!embedded && effectivePermissions.canEdit} className={`workflow-container h-full bg-[var(--bg)] transition-opacity duration-150 ${reactFlowStyles} ${canvasOpacityClass} ${isHandMode ? 'canvas-mode-hand' : 'canvas-mode-cursor'}`} - onNodeDrag={effectivePermissions.canEdit ? onNodeDrag : undefined} - onNodeDragStop={ - !embedded && effectivePermissions.canEdit ? onNodeDragStop : undefined - } + onNodeDrag={canDragNodes ? onNodeDrag : undefined} + onNodeDragStop={canDragNodes ? onNodeDragStop : undefined} onSelectionDragStart={ effectivePermissions.canEdit ? onSelectionDragStart : undefined } @@ -5189,9 +5193,7 @@ const WorkflowContent = React.memo( onSelectionDragStop={ effectivePermissions.canEdit ? onSelectionDragStop : undefined } - onNodeDragStart={ - !embedded && effectivePermissions.canEdit ? onNodeDragStart : undefined - } + onNodeDragStart={canDragNodes ? onNodeDragStart : undefined} snapToGrid={snapToGrid} snapGrid={snapGrid} elevateEdgesOnSelect={false}