Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
26 changes: 14 additions & 12 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import {
getEdgeSelectionContextId,
getNodeSelectionContextId,
getRunFromBlockDependencyState,
getWorkflowCanvasInteractionPolicy,
getWorkflowLockToggleIds,
isBlockProtected,
isEdgeProtected,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -2968,6 +2973,7 @@ const WorkflowContent = React.memo(
isDebugging,
getBlockConfig,
embedded,
canDragNodes,
workflowReadOnly,
collaborativeSetBlockErrorEnabled,
collaborativeBatchRemoveEdges,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -3804,7 +3810,7 @@ const WorkflowContent = React.memo(
getNodes,
potentialParentId,
blocks,
embedded,
canReparentNodes,
getNodeAbsolutePosition,
getNodeDepth,
isDescendantOf,
Expand Down Expand Up @@ -5172,26 +5178,22 @@ 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
}
onSelectionDrag={effectivePermissions.canEdit ? onSelectionDrag : undefined}
onSelectionDragStop={
effectivePermissions.canEdit ? onSelectionDragStop : undefined
}
onNodeDragStart={
!embedded && effectivePermissions.canEdit ? onNodeDragStart : undefined
}
onNodeDragStart={canDragNodes ? onNodeDragStart : undefined}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
elevateEdgesOnSelect={false}
Expand Down
Loading