From 679b384b32cbde944754bdd235b10772ee537fa7 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Wed, 21 Jan 2026 19:02:16 -0800 Subject: [PATCH] feat(workflow-block): preview --- .../templates/components/template-card.tsx | 1 - .../execution-snapshot/execution-snapshot.tsx | 5 +- .../templates/components/template-card.tsx | 1 - .../components/general/general.tsx | 4 +- .../components/template/template.tsx | 1 - .../input-mapping/input-mapping.tsx | 9 +- .../components/tool-input/tool-input.tsx | 9 +- .../subflow-editor/subflow-editor.tsx | 2 +- .../panel/components/editor/editor.tsx | 104 +++++++++- .../w/components/preview/components/block.tsx | 8 +- ...details-sidebar.tsx => preview-editor.tsx} | 43 ++-- .../components/preview/components/subflow.tsx | 7 +- .../w/components/preview/index.ts | 2 +- .../w/components/preview/preview.tsx | 190 ++++++------------ apps/sim/blocks/blocks/workflow_input.ts | 2 +- apps/sim/hooks/queries/workflows.ts | 51 ++--- 16 files changed, 233 insertions(+), 206 deletions(-) rename apps/sim/app/workspace/[workspaceId]/w/components/preview/components/{block-details-sidebar.tsx => preview-editor.tsx} (99%) diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 3c1c32a414..0be5079e0a 100644 --- a/apps/sim/app/templates/components/template-card.tsx +++ b/apps/sim/app/templates/components/template-card.tsx @@ -207,7 +207,6 @@ function TemplateCardInner({ isPannable={false} defaultZoom={0.8} fitPadding={0.2} - lightweight /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx index 49d0e316c5..a0f2a73764 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx @@ -16,8 +16,8 @@ import { import { redactApiKeys } from '@/lib/core/security/redaction' import { cn } from '@/lib/core/utils/cn' import { - BlockDetailsSidebar, getLeftmostBlockId, + PreviewEditor, WorkflowPreview, } from '@/app/workspace/[workspaceId]/w/components/preview' import { useExecutionSnapshot } from '@/hooks/queries/logs' @@ -248,11 +248,10 @@ export function ExecutionSnapshot({ cursorStyle='pointer' executedBlocks={blockExecutions} selectedBlockId={pinnedBlockId} - lightweight />
{pinnedBlockId && workflowState.blocks[pinnedBlockId] && ( - ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx index 28e66c2c43..06a19c89c3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx @@ -18,8 +18,8 @@ import { import { Skeleton } from '@/components/ui' import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils' import { - BlockDetailsSidebar, getLeftmostBlockId, + PreviewEditor, WorkflowPreview, } from '@/app/workspace/[workspaceId]/w/components/preview' import { useDeploymentVersionState, useRevertToVersion } from '@/hooks/queries/workflows' @@ -337,7 +337,7 @@ export function GeneralDeploy({ /> {expandedSelectedBlockId && workflowToShow.blocks?.[expandedSelectedBlockId] && ( - ((_, ref) => { isPannable={false} defaultZoom={0.8} fitPadding={0.2} - lightweight /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx index 79555d90aa..16d1f7f5a9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx @@ -2,12 +2,13 @@ import { useMemo, useRef, useState } from 'react' import { Badge, Input } from '@/components/emcn' import { Label } from '@/components/ui/label' import { cn } from '@/lib/core/utils/cn' +import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' -import { useWorkflowInputFields } from '@/hooks/queries/workflows' +import { useWorkflowState } from '@/hooks/queries/workflows' /** * Props for the InputMappingField component @@ -70,7 +71,11 @@ export function InputMapping({ const overlayRefs = useRef>(new Map()) const workflowId = typeof selectedWorkflowId === 'string' ? selectedWorkflowId : undefined - const { data: childInputFields = [], isLoading } = useWorkflowInputFields(workflowId) + const { data: workflowState, isLoading } = useWorkflowState(workflowId) + const childInputFields = useMemo( + () => (workflowState?.blocks ? extractInputFieldsFromBlocks(workflowState.blocks) : []), + [workflowState?.blocks] + ) const [collapsedFields, setCollapsedFields] = useState>({}) const valueObj: Record = useMemo(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index a064b74864..a08a5833a0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -29,6 +29,7 @@ import { type OAuthProvider, type OAuthService, } from '@/lib/oauth' +import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { CheckboxList, @@ -65,7 +66,7 @@ import { useForceRefreshMcpTools, useMcpServers, useStoredMcpTools } from '@/hoo import { useChildDeploymentStatus, useDeployChildWorkflow, - useWorkflowInputFields, + useWorkflowState, useWorkflows, } from '@/hooks/queries/workflows' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -771,7 +772,11 @@ function WorkflowInputMapperSyncWrapper({ disabled: boolean workflowId: string }) { - const { data: inputFields = [], isLoading } = useWorkflowInputFields(workflowId) + const { data: workflowState, isLoading } = useWorkflowState(workflowId) + const inputFields = useMemo( + () => (workflowState?.blocks ? extractInputFieldsFromBlocks(workflowState.blocks) : []), + [workflowState?.blocks] + ) const parsedValue = useMemo(() => { try { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx index 7046dcf00c..4a8a8dee19 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx @@ -68,7 +68,7 @@ export function SubflowEditor({
{/* Subflow Editor Section */}
-
+
{/* Type Selection */}
{/* Rename button */} - {currentBlock && !isSubflow && ( + {currentBlock && ( + + Open workflow + + + ) : ( +
+ + Unable to load preview + +
+ )} +
+
+
+
+
+ + )} + {subBlocks.length === 0 && !isWorkflowBlock ? (
This block has no subblocks
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx index 41507ca6be..83058b04d8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx @@ -21,11 +21,9 @@ interface WorkflowPreviewBlockData { } /** - * Lightweight block component for workflow previews. - * Renders block header, dummy subblocks skeleton, and handles. - * Respects horizontalHandles and enabled state from workflow. - * No heavy hooks, store subscriptions, or interactive features. - * Used in template cards and other preview contexts for performance. + * Preview block component for workflow visualization. + * Renders block header, subblocks skeleton, and handles without + * hooks, store subscriptions, or interactive features. */ function WorkflowPreviewBlockInner({ data }: NodeProps) { const { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor.tsx similarity index 99% rename from apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx rename to apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor.tsx index cfb1697e7b..c389a7dcd5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor.tsx @@ -685,7 +685,7 @@ interface WorkflowVariable { value: unknown } -interface BlockDetailsSidebarProps { +interface PreviewEditorProps { block: BlockState executionData?: ExecutionData /** All block execution data for resolving variable references */ @@ -722,7 +722,7 @@ const DEFAULT_CONNECTIONS_HEIGHT = 150 /** * Readonly sidebar panel showing block configuration using SubBlock components. */ -function BlockDetailsSidebarContent({ +function PreviewEditorContent({ block, executionData, allBlockExecutions, @@ -732,7 +732,7 @@ function BlockDetailsSidebarContent({ parallels, isExecutionMode = false, onClose, -}: BlockDetailsSidebarProps) { +}: PreviewEditorProps) { // Convert Record to Array for iteration const normalizedWorkflowVariables = useMemo(() => { if (!workflowVariables) return [] @@ -998,6 +998,22 @@ function BlockDetailsSidebarContent({ }) }, [extractedRefs.envVars]) + const rawValues = useMemo(() => { + return Object.entries(subBlockValues).reduce>((acc, [key, entry]) => { + if (entry && typeof entry === 'object' && 'value' in entry) { + acc[key] = (entry as { value: unknown }).value + } else { + acc[key] = entry + } + return acc + }, {}) + }, [subBlockValues]) + + const canonicalIndex = useMemo( + () => buildCanonicalIndex(blockConfig?.subBlocks || []), + [blockConfig?.subBlocks] + ) + // Check if this is a subflow block (loop or parallel) const isSubflow = block.type === 'loop' || block.type === 'parallel' const loopConfig = block.type === 'loop' ? loops?.[block.id] : undefined @@ -1079,21 +1095,6 @@ function BlockDetailsSidebarContent({ ) } - const rawValues = useMemo(() => { - return Object.entries(subBlockValues).reduce>((acc, [key, entry]) => { - if (entry && typeof entry === 'object' && 'value' in entry) { - acc[key] = (entry as { value: unknown }).value - } else { - acc[key] = entry - } - return acc - }, {}) - }, [subBlockValues]) - - const canonicalIndex = useMemo( - () => buildCanonicalIndex(blockConfig.subBlocks), - [blockConfig.subBlocks] - ) const canonicalModeOverrides = block.data?.canonicalModes const effectiveAdvanced = (block.advancedMode ?? false) || @@ -1371,12 +1372,12 @@ function BlockDetailsSidebarContent({ } /** - * Block details sidebar wrapped in ReactFlowProvider for hook compatibility. + * Preview editor wrapped in ReactFlowProvider for hook compatibility. */ -export function BlockDetailsSidebar(props: BlockDetailsSidebarProps) { +export function PreviewEditor(props: PreviewEditorProps) { return ( - + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx index c4d102c05b..a9aa913a2c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx @@ -15,10 +15,9 @@ interface WorkflowPreviewSubflowData { } /** - * Lightweight subflow component for workflow previews. - * Matches the styling of the actual SubflowNodeComponent but without - * hooks, store subscriptions, or interactive features. - * Used in template cards and other preview contexts for performance. + * Preview subflow component for workflow visualization. + * Renders loop/parallel containers without hooks, store subscriptions, + * or interactive features. */ function WorkflowPreviewSubflowInner({ data }: NodeProps) { const { name, width = 500, height = 300, kind, isPreviewSelected = false } = data diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts index 89c096d6eb..d64f8979cf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts @@ -1,2 +1,2 @@ -export { BlockDetailsSidebar } from './components/block-details-sidebar' +export { PreviewEditor } from './components/preview-editor' export { getLeftmostBlockId, WorkflowPreview } from './preview' 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 b04d3f8e39..0f844300fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -15,14 +15,10 @@ import 'reactflow/dist/style.css' import { createLogger } from '@sim/logger' import { cn } from '@/lib/core/utils/cn' import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' -import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block' -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]/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' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowPreview') @@ -134,8 +130,6 @@ interface WorkflowPreviewProps { onNodeContextMenu?: (blockId: string, mousePosition: { x: number; y: number }) => void /** Callback when the canvas (empty area) is clicked */ onPaneClick?: () => void - /** Use lightweight blocks for better performance in template cards */ - lightweight?: boolean /** Cursor style to show when hovering the canvas */ cursorStyle?: 'default' | 'pointer' | 'grab' /** Map of executed block IDs to their status for highlighting the execution path */ @@ -145,19 +139,10 @@ interface WorkflowPreviewProps { } /** - * Full node types with interactive WorkflowBlock for detailed previews + * Preview node types using minimal components without hooks or store subscriptions. + * This prevents interaction issues while allowing canvas panning and node clicking. */ -const fullNodeTypes: NodeTypes = { - workflowBlock: WorkflowBlock, - noteBlock: NoteBlock, - subflowNode: SubflowNodeComponent, -} - -/** - * Lightweight node types for template cards and other high-volume previews. - * Uses minimal components without hooks or store subscriptions. - */ -const lightweightNodeTypes: NodeTypes = { +const previewNodeTypes: NodeTypes = { workflowBlock: WorkflowPreviewBlock, noteBlock: WorkflowPreviewBlock, subflowNode: WorkflowPreviewSubflow, @@ -172,17 +157,19 @@ const edgeTypes: EdgeTypes = { interface FitViewOnChangeProps { nodeIds: string fitPadding: number + containerRef: React.RefObject } /** - * Helper component that calls fitView when the set of nodes changes. + * Helper component that calls fitView when the set of nodes changes or when the container resizes. * Only triggers on actual node additions/removals, not on selection changes. * Must be rendered inside ReactFlowProvider. */ -function FitViewOnChange({ nodeIds, fitPadding }: FitViewOnChangeProps) { +function FitViewOnChange({ nodeIds, fitPadding, containerRef }: FitViewOnChangeProps) { const { fitView } = useReactFlow() const lastNodeIdsRef = useRef(null) + // Fit view when nodes change useEffect(() => { if (!nodeIds.length) return const shouldFit = lastNodeIdsRef.current !== nodeIds @@ -195,6 +182,27 @@ function FitViewOnChange({ nodeIds, fitPadding }: FitViewOnChangeProps) { return () => clearTimeout(timeoutId) }, [nodeIds, fitPadding, fitView]) + // Fit view when container resizes (debounced to avoid excessive calls during drag) + useEffect(() => { + const container = containerRef.current + if (!container) return + + let timeoutId: ReturnType | null = null + + const resizeObserver = new ResizeObserver(() => { + if (timeoutId) clearTimeout(timeoutId) + timeoutId = setTimeout(() => { + fitView({ padding: fitPadding, duration: 150 }) + }, 100) + }) + + resizeObserver.observe(container) + return () => { + if (timeoutId) clearTimeout(timeoutId) + resizeObserver.disconnect() + } + }, [containerRef, fitPadding, fitView]) + return null } @@ -210,15 +218,12 @@ export function WorkflowPreview({ onNodeClick, onNodeContextMenu, onPaneClick, - lightweight = false, cursorStyle = 'grab', executedBlocks, selectedBlockId, }: WorkflowPreviewProps) { - const nodeTypes = useMemo( - () => (lightweight ? lightweightNodeTypes : fullNodeTypes), - [lightweight] - ) + const containerRef = useRef(null) + const nodeTypes = previewNodeTypes const isValidWorkflowState = workflowState?.blocks && workflowState.edges const blocksStructure = useMemo(() => { @@ -288,119 +293,28 @@ export function WorkflowPreview({ const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks) - if (lightweight) { - if (block.type === 'loop' || block.type === 'parallel') { - const isSelected = selectedBlockId === blockId - const dimensions = calculateContainerDimensions(blockId, workflowState.blocks) - nodeArray.push({ - id: blockId, - type: 'subflowNode', - position: absolutePosition, - draggable: false, - data: { - name: block.name, - width: dimensions.width, - height: dimensions.height, - kind: block.type as 'loop' | 'parallel', - isPreviewSelected: isSelected, - }, - }) - return - } - - const isSelected = selectedBlockId === blockId - - let lightweightExecutionStatus: ExecutionStatus | undefined - if (executedBlocks) { - const blockExecution = executedBlocks[blockId] - if (blockExecution) { - if (blockExecution.status === 'error') { - lightweightExecutionStatus = 'error' - } else if (blockExecution.status === 'success') { - lightweightExecutionStatus = 'success' - } else { - lightweightExecutionStatus = 'not-executed' - } - } else { - lightweightExecutionStatus = 'not-executed' - } - } - - nodeArray.push({ - id: blockId, - type: 'workflowBlock', - position: absolutePosition, - draggable: false, - // Blocks inside subflows need higher z-index to appear above the container - zIndex: block.data?.parentId ? 10 : undefined, - data: { - type: block.type, - name: block.name, - isTrigger: block.triggerMode === true, - horizontalHandles: block.horizontalHandles ?? false, - enabled: block.enabled ?? true, - isPreviewSelected: isSelected, - executionStatus: lightweightExecutionStatus, - }, - }) - return - } - - if (block.type === 'loop') { + // Handle loop/parallel containers + if (block.type === 'loop' || block.type === 'parallel') { const isSelected = selectedBlockId === blockId const dimensions = calculateContainerDimensions(blockId, workflowState.blocks) nodeArray.push({ id: blockId, type: 'subflowNode', position: absolutePosition, - parentId: block.data?.parentId, - extent: block.data?.extent || undefined, draggable: false, data: { - ...block.data, name: block.name, width: dimensions.width, height: dimensions.height, - state: 'valid', - isPreview: true, + kind: block.type as 'loop' | 'parallel', isPreviewSelected: isSelected, - kind: 'loop', }, }) return } - if (block.type === 'parallel') { - const isSelected = selectedBlockId === blockId - const dimensions = calculateContainerDimensions(blockId, workflowState.blocks) - nodeArray.push({ - id: blockId, - type: 'subflowNode', - position: absolutePosition, - parentId: block.data?.parentId, - extent: block.data?.extent || undefined, - draggable: false, - data: { - ...block.data, - name: block.name, - width: dimensions.width, - height: dimensions.height, - state: 'valid', - isPreview: true, - isPreviewSelected: isSelected, - kind: 'parallel', - }, - }) - return - } - - const blockConfig = getBlock(block.type) - if (!blockConfig) { - logger.error(`No configuration found for block type: ${block.type}`, { blockId }) - return - } - - const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' + // Handle regular blocks + const isSelected = selectedBlockId === blockId let executionStatus: ExecutionStatus | undefined if (executedBlocks) { @@ -418,24 +332,20 @@ export function WorkflowPreview({ } } - const isSelected = selectedBlockId === blockId - nodeArray.push({ id: blockId, - type: nodeType, + type: 'workflowBlock', position: absolutePosition, draggable: false, // Blocks inside subflows need higher z-index to appear above the container zIndex: block.data?.parentId ? 10 : undefined, data: { type: block.type, - config: blockConfig, name: block.name, - blockState: block, - canEdit: false, - isPreview: true, + isTrigger: block.triggerMode === true, + horizontalHandles: block.horizontalHandles ?? false, + enabled: block.enabled ?? true, isPreviewSelected: isSelected, - subBlockValues: block.subBlocks ?? {}, executionStatus, }, }) @@ -448,7 +358,6 @@ export function WorkflowPreview({ parallelsStructure, workflowState.blocks, isValidWorkflowState, - lightweight, executedBlocks, selectedBlockId, ]) @@ -506,6 +415,7 @@ export function WorkflowPreview({ return (
@@ -516,6 +426,20 @@ export function WorkflowPreview({ .preview-mode .react-flow__selectionpane { cursor: ${cursorStyle} !important; } .preview-mode .react-flow__renderer { cursor: ${cursorStyle}; } + /* Active/grabbing cursor when dragging */ + ${ + cursorStyle === 'grab' + ? ` + .preview-mode .react-flow:active { cursor: grabbing; } + .preview-mode .react-flow__pane:active { cursor: grabbing !important; } + .preview-mode .react-flow__selectionpane:active { cursor: grabbing !important; } + .preview-mode .react-flow__renderer:active { cursor: grabbing; } + .preview-mode .react-flow__node:active { cursor: grabbing !important; } + .preview-mode .react-flow__node:active * { cursor: grabbing !important; } + ` + : '' + } + /* Node cursor - pointer on nodes when onNodeClick is provided */ .preview-mode.interactive-nodes .react-flow__node { cursor: pointer !important; } .preview-mode.interactive-nodes .react-flow__node > div { cursor: pointer !important; } @@ -563,7 +487,11 @@ export function WorkflowPreview({ } onPaneClick={onPaneClick} /> - +
) diff --git a/apps/sim/blocks/blocks/workflow_input.ts b/apps/sim/blocks/blocks/workflow_input.ts index 16e48c4f2d..73e3c833b6 100644 --- a/apps/sim/blocks/blocks/workflow_input.ts +++ b/apps/sim/blocks/blocks/workflow_input.ts @@ -24,7 +24,7 @@ export const WorkflowInputBlock: BlockConfig = { }, { id: 'inputMapping', - title: 'Input Mapping', + title: 'Inputs', type: 'input-mapping', description: "Map fields defined in the child workflow's Start block to variables/values in this workflow.", diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts index 29327e7735..5dd0f5b0c9 100644 --- a/apps/sim/hooks/queries/workflows.ts +++ b/apps/sim/hooks/queries/workflows.ts @@ -3,7 +3,6 @@ import { createLogger } from '@sim/logger' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getNextWorkflowColor } from '@/lib/workflows/colors' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' -import { extractInputFieldsFromBlocks, type WorkflowInputField } from '@/lib/workflows/input-format' import { deploymentKeys } from '@/hooks/queries/deployments' import { createOptimisticMutationHandlers, @@ -26,34 +25,35 @@ export const workflowKeys = { deploymentVersions: () => [...workflowKeys.all, 'deploymentVersion'] as const, deploymentVersion: (workflowId: string | undefined, version: number | undefined) => [...workflowKeys.deploymentVersions(), workflowId ?? '', version ?? 0] as const, - inputFields: (workflowId: string | undefined) => - [...workflowKeys.all, 'inputFields', workflowId ?? ''] as const, + state: (workflowId: string | undefined) => + [...workflowKeys.all, 'state', workflowId ?? ''] as const, } /** - * Fetches workflow input fields from the workflow state. + * Fetches workflow state from the API. + * Used as the base query for both state preview and input fields extraction. */ -async function fetchWorkflowInputFields(workflowId: string): Promise { +async function fetchWorkflowState(workflowId: string): Promise { const response = await fetch(`/api/workflows/${workflowId}`) if (!response.ok) throw new Error('Failed to fetch workflow') const { data } = await response.json() - return extractInputFieldsFromBlocks(data?.state?.blocks) + return data?.state ?? null } /** - * Hook to fetch workflow input fields for configuration. - * Uses React Query for caching and deduplication. + * Hook to fetch workflow state. + * Used by workflow blocks to show a preview of the child workflow + * and as a base query for input fields extraction. * - * @param workflowId - The workflow ID to fetch input fields for - * @returns Query result with input fields array + * @param workflowId - The workflow ID to fetch state for + * @returns Query result with workflow state */ -export function useWorkflowInputFields(workflowId: string | undefined) { +export function useWorkflowState(workflowId: string | undefined) { return useQuery({ - queryKey: workflowKeys.inputFields(workflowId), - queryFn: () => fetchWorkflowInputFields(workflowId!), + queryKey: workflowKeys.state(workflowId), + queryFn: () => fetchWorkflowState(workflowId!), enabled: Boolean(workflowId), - staleTime: 0, - refetchOnMount: 'always', + staleTime: 30 * 1000, // 30 seconds }) } @@ -532,13 +532,19 @@ export interface ChildDeploymentStatus { } /** - * Fetches deployment status for a child workflow + * Fetches deployment status for a child workflow. + * Uses Promise.all to fetch status and deployments in parallel for better performance. */ async function fetchChildDeploymentStatus(workflowId: string): Promise { - const statusRes = await fetch(`/api/workflows/${workflowId}/status`, { - cache: 'no-store', + const fetchOptions = { + cache: 'no-store' as const, headers: { 'Cache-Control': 'no-cache' }, - }) + } + + const [statusRes, deploymentsRes] = await Promise.all([ + fetch(`/api/workflows/${workflowId}/status`, fetchOptions), + fetch(`/api/workflows/${workflowId}/deployments`, fetchOptions), + ]) if (!statusRes.ok) { throw new Error('Failed to fetch workflow status') @@ -546,11 +552,6 @@ async function fetchChildDeploymentStatus(workflowId: string): Promise fetchChildDeploymentStatus(workflowId!), enabled: Boolean(workflowId), - staleTime: 30 * 1000, // 30 seconds + staleTime: 0, retry: false, }) }