diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index c2f7ee340a..7788f1b492 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -48,7 +48,7 @@ export const ActionBar = memo( collaborativeBatchToggleBlockEnabled, collaborativeBatchToggleBlockHandles, } = useCollaborativeWorkflow() - const { activeWorkflowId } = useWorkflowRegistry() + const { activeWorkflowId, setPendingSelection } = useWorkflowRegistry() const blocks = useWorkflowStore((state) => state.blocks) const subBlockStore = useSubBlockStore() @@ -68,6 +68,7 @@ export const ActionBar = memo( subBlockValues, }) + setPendingSelection([newId]) collaborativeBatchAddBlocks([block], [], {}, {}, { [newId]: filteredValues }) }, [ blockId, @@ -75,6 +76,7 @@ export const ActionBar = memo( activeWorkflowId, subBlockStore.workflowValues, collaborativeBatchAddBlocks, + setPendingSelection, ]) /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index 2cfa568165..25242087c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -3,13 +3,11 @@ import ReactMarkdown from 'react-markdown' import type { NodeProps } from 'reactflow' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/core/utils/cn' +import { BLOCK_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar' import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' -import { - BLOCK_DIMENSIONS, - useBlockDimensions, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' +import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import type { WorkflowBlockProps } from '../workflow-block/types' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx index 204e6c5259..b4829b903e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx @@ -11,8 +11,6 @@ import { ButtonGroupItem, Checkbox, Code, - Combobox, - type ComboboxOption, Input, Label, TagInput, @@ -271,14 +269,6 @@ export function A2aDeploy({ onNeedsRepublishChange?.(!!needsRepublish) }, [needsRepublish, onNeedsRepublishChange]) - const authSchemeOptions: ComboboxOption[] = useMemo( - () => [ - { label: 'API Key', value: 'apiKey' }, - { label: 'None (Public)', value: 'none' }, - ], - [] - ) - const canSave = name.trim().length > 0 && description.trim().length > 0 useEffect(() => { onCanSaveChange?.(canSave) @@ -758,17 +748,18 @@ console.log(data);` /> - {/* Authentication */} + {/* Access */}
- setAuthScheme(v as AuthScheme)} - placeholder='Select authentication...' - /> + onValueChange={(value) => setAuthScheme(value as AuthScheme)} + > + API Key + Public +

{authScheme === 'none' ? 'Anyone can call this agent without authentication' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index 8b56c5ab43..30a2bd79f3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -424,7 +424,7 @@ export function ChatDeploy({ > Cancel - diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 8c8897fbd3..b986ff6546 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -7,6 +7,7 @@ import { cn } from '@/lib/core/utils/cn' import { getBaseUrl } from '@/lib/core/utils/urls' import { createMcpToolId } from '@/lib/mcp/utils' import { getProviderIdFromServiceId } from '@/lib/oauth' +import { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' import { buildCanonicalIndex, evaluateSubBlockCondition, @@ -28,11 +29,7 @@ import { shouldSkipBlockRender, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils' import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' -import { - BLOCK_DIMENSIONS, - HANDLE_POSITIONS, - useBlockDimensions, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' +import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { getDependsOnFields } from '@/blocks/utils' import { useKnowledgeBase } from '@/hooks/kb/use-knowledge' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts index b80f7749a8..31ebdc27a9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts @@ -1,18 +1,14 @@ -export { - clearDragHighlights, - computeClampedPositionUpdates, - computeParentUpdateEntries, - getClampedPositionForNode, - isInEditableElement, - resolveParentChildSelectionConflicts, - validateTriggerPaste, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers' export { useFloatBoundarySync, useFloatDrag, useFloatResize } from './float' +export { useAccessibleReferencePrefixes } from './use-accessible-reference-prefixes' export { useAutoLayout } from './use-auto-layout' -export { BLOCK_DIMENSIONS, useBlockDimensions } from './use-block-dimensions' +export { useBlockDimensions } from './use-block-dimensions' +export { useBlockOutputFields } from './use-block-output-fields' export { useBlockVisual } from './use-block-visual' +export { useCanvasContextMenu } from './use-canvas-context-menu' export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow' -export { calculateContainerDimensions, useNodeUtilities } from './use-node-utilities' +export { useNodeUtilities } from './use-node-utilities' export { usePreventZoom } from './use-prevent-zoom' export { useScrollManagement } from './use-scroll-management' +export { useShiftSelectionLock } from './use-shift-selection-lock' +export { useWand, type WandConfig } from './use-wand' export { useWorkflowExecution } from './use-workflow-execution' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts index e8c7f9f154..aaa2025423 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts @@ -2,9 +2,6 @@ import { useEffect, useRef } from 'react' import { useUpdateNodeInternals } from 'reactflow' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -// Re-export for backwards compatibility -export { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' - interface BlockDimensions { width: number height: number diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts index e137506618..06329a6b71 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts @@ -2,107 +2,15 @@ import { useCallback } from 'react' import { createLogger } from '@sim/logger' import { useReactFlow } from 'reactflow' import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' -import { getBlock } from '@/blocks/registry' +import { + calculateContainerDimensions, + clampPositionToContainer, + estimateBlockDimensions, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('NodeUtilities') -/** - * Estimates block dimensions based on block type. - * Uses subblock count to estimate height for blocks that haven't been measured yet. - * - * @param blockType - The type of block (e.g., 'condition', 'agent') - * @returns Estimated width and height for the block - */ -export function estimateBlockDimensions(blockType: string): { width: number; height: number } { - const blockConfig = getBlock(blockType) - const subBlockCount = blockConfig?.subBlocks?.length ?? 3 - // Many subblocks are conditionally rendered (advanced mode, provider-specific, etc.) - // Use roughly half the config count as a reasonable estimate, capped between 3-7 rows - const estimatedRows = Math.max(3, Math.min(Math.ceil(subBlockCount / 2), 7)) - const hasErrorRow = blockType !== 'starter' && blockType !== 'response' ? 1 : 0 - - const height = - BLOCK_DIMENSIONS.HEADER_HEIGHT + - BLOCK_DIMENSIONS.WORKFLOW_CONTENT_PADDING + - (estimatedRows + hasErrorRow) * BLOCK_DIMENSIONS.WORKFLOW_ROW_HEIGHT - - return { - width: BLOCK_DIMENSIONS.FIXED_WIDTH, - height: Math.max(height, BLOCK_DIMENSIONS.MIN_HEIGHT), - } -} - -/** - * Clamps a position to keep a block fully inside a container's content area. - * Content area starts after the header and padding, and ends before the right/bottom padding. - * - * @param position - Raw position relative to container origin - * @param containerDimensions - Container width and height - * @param blockDimensions - Block width and height - * @returns Clamped position that keeps block inside content area - */ -export function clampPositionToContainer( - position: { x: number; y: number }, - containerDimensions: { width: number; height: number }, - blockDimensions: { width: number; height: number } -): { x: number; y: number } { - const { width: containerWidth, height: containerHeight } = containerDimensions - const { width: blockWidth, height: blockHeight } = blockDimensions - - // Content area bounds (where blocks can be placed) - const minX = CONTAINER_DIMENSIONS.LEFT_PADDING - const minY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING - const maxX = containerWidth - CONTAINER_DIMENSIONS.RIGHT_PADDING - blockWidth - const maxY = containerHeight - CONTAINER_DIMENSIONS.BOTTOM_PADDING - blockHeight - - return { - x: Math.max(minX, Math.min(position.x, Math.max(minX, maxX))), - y: Math.max(minY, Math.min(position.y, Math.max(minY, maxY))), - } -} - -/** - * Calculates container dimensions based on child block positions. - * Single source of truth for container sizing - ensures consistency between - * live drag updates and final dimension calculations. - * - * @param childPositions - Array of child positions with their dimensions - * @returns Calculated width and height for the container - */ -export function calculateContainerDimensions( - childPositions: Array<{ x: number; y: number; width: number; height: number }> -): { width: number; height: number } { - if (childPositions.length === 0) { - return { - width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH, - height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, - } - } - - let maxRight = 0 - let maxBottom = 0 - - for (const child of childPositions) { - maxRight = Math.max(maxRight, child.x + child.width) - maxBottom = Math.max(maxBottom, child.y + child.height) - } - - const width = Math.max( - CONTAINER_DIMENSIONS.DEFAULT_WIDTH, - CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING - ) - const height = Math.max( - CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, - CONTAINER_DIMENSIONS.HEADER_HEIGHT + - CONTAINER_DIMENSIONS.TOP_PADDING + - maxBottom + - CONTAINER_DIMENSIONS.BOTTOM_PADDING - ) - - return { width, height } -} - /** * Hook providing utilities for node position, hierarchy, and dimension calculations */ @@ -138,7 +46,6 @@ export function useNodeUtilities(blocks: Record) { } } - // Prefer deterministic height published by the block component; fallback to estimate if (block.height) { return { width: BLOCK_DIMENSIONS.FIXED_WIDTH, @@ -146,7 +53,6 @@ export function useNodeUtilities(blocks: Record) { } } - // Use shared estimation utility for blocks without measured height return estimateBlockDimensions(block.type) }, [blocks, isContainerType] @@ -230,8 +136,6 @@ export function useNodeUtilities(blocks: Record) { const parentPos = getNodeAbsolutePosition(parentId) - // Child positions are stored relative to the content area (after header and padding) - // Add these offsets when calculating absolute position const headerHeight = 50 const leftPadding = 16 const topPadding = 16 @@ -314,7 +218,6 @@ export function useNodeUtilities(blocks: Record) { }) .map((n) => ({ loopId: n.id, - // Return absolute position so callers can compute relative placement correctly loopPosition: getNodeAbsolutePosition(n.id), dimensions: { width: n.data?.width || CONTAINER_DIMENSIONS.DEFAULT_WIDTH, @@ -449,7 +352,6 @@ export function useNodeUtilities(blocks: Record) { return absPos } - // Use known defaults per node type without type casting const isSubflow = node.type === 'subflowNode' const width = isSubflow ? typeof node.data?.width === 'number' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-shift-selection-lock.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-shift-selection-lock.ts new file mode 100644 index 0000000000..d50ec82551 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-shift-selection-lock.ts @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useState } from 'react' + +interface UseShiftSelectionLockProps { + isHandMode: boolean +} + +interface UseShiftSelectionLockResult { + /** Whether a shift-selection is currently active (locked in until mouseup) */ + isShiftSelecting: boolean + /** Handler to attach to canvas mousedown */ + handleCanvasMouseDown: (event: React.MouseEvent) => void + /** Computed ReactFlow props based on current selection state */ + selectionProps: { + selectionOnDrag: boolean + panOnDrag: [number, number] | false + selectionKeyCode: string | null + } +} + +/** + * Locks shift-selection mode from mousedown to mouseup. + * Prevents selection from canceling when shift is released mid-drag. + */ +export function useShiftSelectionLock({ + isHandMode, +}: UseShiftSelectionLockProps): UseShiftSelectionLockResult { + const [isShiftSelecting, setIsShiftSelecting] = useState(false) + + const handleCanvasMouseDown = useCallback( + (event: React.MouseEvent) => { + if (!event.shiftKey) return + + const target = event.target as HTMLElement | null + const isPaneTarget = Boolean(target?.closest('.react-flow__pane, .react-flow__selectionpane')) + + if (isPaneTarget && isHandMode) { + setIsShiftSelecting(true) + } + + if (isPaneTarget) { + event.preventDefault() + window.getSelection()?.removeAllRanges() + } + }, + [isHandMode] + ) + + useEffect(() => { + if (!isShiftSelecting) return + + const handleMouseUp = () => setIsShiftSelecting(false) + window.addEventListener('mouseup', handleMouseUp) + return () => window.removeEventListener('mouseup', handleMouseUp) + }, [isShiftSelecting]) + + const selectionProps = { + selectionOnDrag: !isHandMode || isShiftSelecting, + panOnDrag: (isHandMode && !isShiftSelecting ? [0, 1] : false) as [number, number] | false, + selectionKeyCode: isShiftSelecting ? null : 'Shift', + } + + return { isShiftSelecting, handleCanvasMouseDown, selectionProps } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts index adf8b5f8a3..d2845af28b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts @@ -1,3 +1,5 @@ export * from './auto-layout-utils' export * from './block-ring-utils' +export * from './node-position-utils' +export * from './workflow-canvas-helpers' export * from './workflow-execution-utils' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils.ts new file mode 100644 index 0000000000..01068ff111 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils.ts @@ -0,0 +1,95 @@ +import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' +import { getBlock } from '@/blocks/registry' + +/** + * Estimates block dimensions based on block type. + * Uses subblock count to estimate height for blocks that haven't been measured yet. + * + * @param blockType - The type of block (e.g., 'condition', 'agent') + * @returns Estimated width and height for the block + */ +export function estimateBlockDimensions(blockType: string): { width: number; height: number } { + const blockConfig = getBlock(blockType) + const subBlockCount = blockConfig?.subBlocks?.length ?? 3 + const estimatedRows = Math.max(3, Math.min(Math.ceil(subBlockCount / 2), 7)) + const hasErrorRow = blockType !== 'starter' && blockType !== 'response' ? 1 : 0 + + const height = + BLOCK_DIMENSIONS.HEADER_HEIGHT + + BLOCK_DIMENSIONS.WORKFLOW_CONTENT_PADDING + + (estimatedRows + hasErrorRow) * BLOCK_DIMENSIONS.WORKFLOW_ROW_HEIGHT + + return { + width: BLOCK_DIMENSIONS.FIXED_WIDTH, + height: Math.max(height, BLOCK_DIMENSIONS.MIN_HEIGHT), + } +} + +/** + * Clamps a position to keep a block fully inside a container's content area. + * Content area starts after the header and padding, and ends before the right/bottom padding. + * + * @param position - Raw position relative to container origin + * @param containerDimensions - Container width and height + * @param blockDimensions - Block width and height + * @returns Clamped position that keeps block inside content area + */ +export function clampPositionToContainer( + position: { x: number; y: number }, + containerDimensions: { width: number; height: number }, + blockDimensions: { width: number; height: number } +): { x: number; y: number } { + const { width: containerWidth, height: containerHeight } = containerDimensions + const { width: blockWidth, height: blockHeight } = blockDimensions + + const minX = CONTAINER_DIMENSIONS.LEFT_PADDING + const minY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING + const maxX = containerWidth - CONTAINER_DIMENSIONS.RIGHT_PADDING - blockWidth + const maxY = containerHeight - CONTAINER_DIMENSIONS.BOTTOM_PADDING - blockHeight + + return { + x: Math.max(minX, Math.min(position.x, Math.max(minX, maxX))), + y: Math.max(minY, Math.min(position.y, Math.max(minY, maxY))), + } +} + +/** + * Calculates container dimensions based on child block positions. + * Single source of truth for container sizing - ensures consistency between + * live drag updates and final dimension calculations. + * + * @param childPositions - Array of child positions with their dimensions + * @returns Calculated width and height for the container + */ +export function calculateContainerDimensions( + childPositions: Array<{ x: number; y: number; width: number; height: number }> +): { width: number; height: number } { + if (childPositions.length === 0) { + return { + width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH, + height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, + } + } + + let maxRight = 0 + let maxBottom = 0 + + for (const child of childPositions) { + maxRight = Math.max(maxRight, child.x + child.width) + maxBottom = Math.max(maxBottom, child.y + child.height) + } + + const width = Math.max( + CONTAINER_DIMENSIONS.DEFAULT_WIDTH, + CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING + ) + const height = Math.max( + CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, + CONTAINER_DIMENSIONS.HEADER_HEIGHT + + CONTAINER_DIMENSIONS.TOP_PADDING + + maxBottom + + CONTAINER_DIMENSIONS.BOTTOM_PADDING + ) + + return { width, height } +} 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 a6d6ea9136..7f24907c47 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 @@ -1,7 +1,7 @@ import type { Edge, Node } from 'reactflow' import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' -import { clampPositionToContainer } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities' +import { clampPositionToContainer } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils' import type { BlockState } from '@/stores/workflows/workflow/types' /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index f2a0db4d7a..a32bbb96aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -42,22 +42,23 @@ import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp import { WorkflowControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' import { - clearDragHighlights, - computeClampedPositionUpdates, - getClampedPositionForNode, - isInEditableElement, - resolveParentChildSelectionConflicts, useAutoLayout, + useCanvasContextMenu, useCurrentWorkflow, useNodeUtilities, - validateTriggerPaste, + useShiftSelectionLock, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' -import { useCanvasContextMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu' import { calculateContainerDimensions, clampPositionToContainer, + clearDragHighlights, + computeClampedPositionUpdates, estimateBlockDimensions, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities' + getClampedPositionForNode, + isInEditableElement, + resolveParentChildSelectionConflicts, + validateTriggerPaste, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' import { isAnnotationOnlyBlock } from '@/executor/constants' @@ -235,6 +236,7 @@ const WorkflowContent = React.memo(() => { const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false) const canvasMode = useCanvasModeStore((state) => state.mode) const isHandMode = canvasMode === 'hand' + const { handleCanvasMouseDown, selectionProps } = useShiftSelectionLock({ isHandMode }) const [oauthModal, setOauthModal] = useState<{ provider: OAuthProvider serviceId: string @@ -264,6 +266,9 @@ const WorkflowContent = React.memo(() => { preparePasteData, hasClipboard, clipboard, + pendingSelection, + setPendingSelection, + clearPendingSelection, } = useWorkflowRegistry( useShallow((state) => ({ workflows: state.workflows, @@ -274,6 +279,9 @@ const WorkflowContent = React.memo(() => { preparePasteData: state.preparePasteData, hasClipboard: state.hasClipboard, clipboard: state.clipboard, + pendingSelection: state.pendingSelection, + setPendingSelection: state.setPendingSelection, + clearPendingSelection: state.clearPendingSelection, })) ) @@ -441,9 +449,6 @@ const WorkflowContent = React.memo(() => { new Map() ) - /** Stores node IDs to select on next derivedNodes sync (for paste/duplicate operations). */ - const pendingSelectionRef = useRef | null>(null) - /** Re-applies diff markers when blocks change after socket rehydration. */ const blocksRef = useRef(blocks) useEffect(() => { @@ -682,7 +687,7 @@ const WorkflowContent = React.memo(() => { autoConnectEdge?: Edge, triggerMode?: boolean ) => { - pendingSelectionRef.current = new Set([id]) + setPendingSelection([id]) setSelectedEdges(new Map()) const blockData: Record = { ...(data || {}) } @@ -719,7 +724,7 @@ const WorkflowContent = React.memo(() => { ) usePanelEditorStore.getState().setCurrentBlockId(id) }, - [collaborativeBatchAddBlocks, setSelectedEdges] + [collaborativeBatchAddBlocks, setSelectedEdges, setPendingSelection] ) const { activeBlockIds, pendingBlocks, isDebugging } = useExecutionStore( @@ -881,10 +886,7 @@ const WorkflowContent = React.memo(() => { } // Set pending selection before adding blocks - sync effect will apply it (accumulates for rapid pastes) - pendingSelectionRef.current = new Set([ - ...(pendingSelectionRef.current ?? []), - ...pastedBlocksArray.map((b) => b.id), - ]) + setPendingSelection(pastedBlocksArray.map((b) => b.id)) collaborativeBatchAddBlocks( pastedBlocksArray, @@ -894,7 +896,14 @@ const WorkflowContent = React.memo(() => { pasteData.subBlockValues ) }, - [preparePasteData, blocks, addNotification, activeWorkflowId, collaborativeBatchAddBlocks] + [ + preparePasteData, + blocks, + addNotification, + activeWorkflowId, + collaborativeBatchAddBlocks, + setPendingSelection, + ] ) const handleContextPaste = useCallback(() => { @@ -2041,26 +2050,28 @@ const WorkflowContent = React.memo(() => { useEffect(() => { // Check for pending selection (from paste/duplicate), otherwise preserve existing selection - const pendingSelection = pendingSelectionRef.current - pendingSelectionRef.current = null + if (pendingSelection && pendingSelection.length > 0) { + const pendingSet = new Set(pendingSelection) + clearPendingSelection() + + // Apply pending selection and resolve parent-child conflicts + const withSelection = derivedNodes.map((node) => ({ + ...node, + selected: pendingSet.has(node.id), + })) + setDisplayNodes(resolveParentChildSelectionConflicts(withSelection, blocks)) + return + } + // Preserve existing selection state setDisplayNodes((currentNodes) => { - if (pendingSelection) { - // Apply pending selection and resolve parent-child conflicts - const withSelection = derivedNodes.map((node) => ({ - ...node, - selected: pendingSelection.has(node.id), - })) - return resolveParentChildSelectionConflicts(withSelection, blocks) - } - // Preserve existing selection state const selectedIds = new Set(currentNodes.filter((n) => n.selected).map((n) => n.id)) return derivedNodes.map((node) => ({ ...node, selected: selectedIds.has(node.id), })) }) - }, [derivedNodes, blocks]) + }, [derivedNodes, blocks, pendingSelection, clearPendingSelection]) /** Handles ActionBar remove-from-subflow events. */ useEffect(() => { @@ -3010,23 +3021,6 @@ const WorkflowContent = React.memo(() => { usePanelEditorStore.getState().clearCurrentBlock() }, []) - /** Prevents native text selection when starting a shift-drag on the pane. */ - const handleCanvasMouseDown = useCallback((event: React.MouseEvent) => { - if (!event.shiftKey) return - - const target = event.target as HTMLElement | null - if (!target) return - - const isPaneTarget = Boolean(target.closest('.react-flow__pane, .react-flow__selectionpane')) - if (!isPaneTarget) return - - event.preventDefault() - const selection = window.getSelection() - if (selection && selection.rangeCount > 0) { - selection.removeAllRanges() - } - }, []) - /** * Handles node click to select the node in ReactFlow. * Parent-child conflict resolution happens automatically in onNodesChange. @@ -3226,9 +3220,10 @@ const WorkflowContent = React.memo(() => { onPointerMove={handleCanvasPointerMove} onPointerLeave={handleCanvasPointerLeave} elementsSelectable={true} - selectionOnDrag={!isHandMode} + selectionOnDrag={selectionProps.selectionOnDrag} selectionMode={SelectionMode.Partial} - panOnDrag={isHandMode ? [0, 1] : false} + panOnDrag={selectionProps.panOnDrag} + selectionKeyCode={selectionProps.selectionKeyCode} multiSelectionKeyCode={['Meta', 'Control', 'Shift']} nodesConnectable={effectivePermissions.canEdit} nodesDraggable={effectivePermissions.canEdit} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx index e3c5a159d0..77e2c1dcd7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -19,7 +19,7 @@ import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/componen import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' -import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities' +import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { WorkflowPreviewBlock } from '@/app/workspace/[workspaceId]/w/components/preview/components/block' import { WorkflowPreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/subflow' import { getBlock } from '@/blocks' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx index 38c2886139..067db3cc9d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx @@ -1078,7 +1078,7 @@ export function AccessControl() { @@ -321,12 +320,7 @@ export function BYOK() { - diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx index f59e970244..e2d85bca6c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx @@ -334,7 +334,7 @@ export function Copilot() { Cancel {hasConflicts || hasInvalidKeys ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx index 58e8a2acd3..82418420ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx @@ -117,7 +117,7 @@ export function TeamSeats({ - diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx index 8cb9efcaba..660389c247 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx @@ -709,7 +709,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr