)}
-
+ {!data.isPreview && (
+
+ )}
{shouldShowDefaultHandles &&
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
index 14e64108c8..783b0b8f7b 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
@@ -55,7 +55,11 @@ const WorkflowEdgeComponent = ({
const dataSourceHandle = (data as { sourceHandle?: string } | undefined)?.sourceHandle
const isErrorEdge = (sourceHandle ?? dataSourceHandle) === 'error'
- const edgeRunStatus = lastRunEdges.get(id)
+ // Check for execution status from both live execution store and preview data
+ const previewExecutionStatus = (
+ data as { executionStatus?: 'success' | 'error' | 'not-executed' } | undefined
+ )?.executionStatus
+ const edgeRunStatus = previewExecutionStatus || lastRunEdges.get(id)
// Memoize diff status calculation to avoid recomputing on every render
const edgeDiffStatus = useMemo((): EdgeDiffStatus => {
@@ -87,18 +91,37 @@ const WorkflowEdgeComponent = ({
// Memoize edge style to prevent object recreation
const edgeStyle = useMemo(() => {
let color = 'var(--workflow-edge)'
- if (edgeDiffStatus === 'deleted') color = 'var(--text-error)'
- else if (isErrorEdge) color = 'var(--text-error)'
- else if (edgeDiffStatus === 'new') color = 'var(--brand-tertiary)'
- else if (edgeRunStatus === 'success') color = 'var(--border-success)'
- else if (edgeRunStatus === 'error') color = 'var(--text-error)'
+ let opacity = 1
+
+ if (edgeDiffStatus === 'deleted') {
+ color = 'var(--text-error)'
+ opacity = 0.7
+ } else if (isErrorEdge) {
+ color = 'var(--text-error)'
+ } else if (edgeDiffStatus === 'new') {
+ color = 'var(--brand-tertiary)'
+ } else if (edgeRunStatus === 'success') {
+ color = 'var(--border-success)'
+ } else if (edgeRunStatus === 'error') {
+ color = 'var(--text-error)'
+ }
+
+ if (isSelected) {
+ opacity = 0.5
+ }
return {
...(style ?? {}),
- strokeWidth: edgeDiffStatus ? 3 : isSelected ? 2.5 : 2,
+ strokeWidth: edgeDiffStatus
+ ? 3
+ : edgeRunStatus === 'success' || edgeRunStatus === 'error'
+ ? 2.5
+ : isSelected
+ ? 2.5
+ : 2,
stroke: color,
strokeDasharray: edgeDiffStatus === 'deleted' ? '10,5' : undefined,
- opacity: edgeDiffStatus === 'deleted' ? 0.7 : isSelected ? 0.5 : 1,
+ opacity,
}
}, [style, edgeDiffStatus, isSelected, isErrorEdge, edgeRunStatus])
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/block-details-sidebar.tsx
new file mode 100644
index 0000000000..756e68fed2
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx
@@ -0,0 +1,678 @@
+'use client'
+
+import { useMemo, useState } from 'react'
+import { ChevronDown as ChevronDownIcon, X } from 'lucide-react'
+import { ReactFlowProvider } from 'reactflow'
+import { Badge, Button, ChevronDown, Code } from '@/components/emcn'
+import { cn } from '@/lib/core/utils/cn'
+import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references'
+import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components'
+import { getBlock } from '@/blocks'
+import type { BlockConfig, BlockIcon, SubBlockConfig } from '@/blocks/types'
+import { normalizeName } from '@/executor/constants'
+import { navigatePath } from '@/executor/variables/resolvers/reference'
+import type { BlockState } from '@/stores/workflows/workflow/types'
+
+/**
+ * Evaluate whether a subblock's condition is met based on current values.
+ */
+function evaluateCondition(
+ condition: SubBlockConfig['condition'],
+ subBlockValues: Record
+): boolean {
+ if (!condition) return true
+
+ const actualCondition = typeof condition === 'function' ? condition() : condition
+
+ const fieldValueObj = subBlockValues[actualCondition.field]
+ const fieldValue =
+ fieldValueObj && typeof fieldValueObj === 'object' && 'value' in fieldValueObj
+ ? (fieldValueObj as { value: unknown }).value
+ : fieldValueObj
+
+ const conditionValues = Array.isArray(actualCondition.value)
+ ? actualCondition.value
+ : [actualCondition.value]
+
+ let isMatch = conditionValues.some((v) => v === fieldValue)
+
+ if (actualCondition.not) {
+ isMatch = !isMatch
+ }
+
+ if (actualCondition.and && isMatch) {
+ const andFieldValueObj = subBlockValues[actualCondition.and.field]
+ const andFieldValue =
+ andFieldValueObj && typeof andFieldValueObj === 'object' && 'value' in andFieldValueObj
+ ? (andFieldValueObj as { value: unknown }).value
+ : andFieldValueObj
+
+ const andConditionValues = Array.isArray(actualCondition.and.value)
+ ? actualCondition.and.value
+ : [actualCondition.and.value]
+
+ let andMatch = andConditionValues.some((v) => v === andFieldValue)
+
+ if (actualCondition.and.not) {
+ andMatch = !andMatch
+ }
+
+ isMatch = isMatch && andMatch
+ }
+
+ return isMatch
+}
+
+/**
+ * Format a value for display as JSON string
+ */
+function formatValueAsJson(value: unknown): string {
+ if (value === null || value === undefined || value === '') {
+ return '—'
+ }
+ if (typeof value === 'object') {
+ try {
+ return JSON.stringify(value, null, 2)
+ } catch {
+ return String(value)
+ }
+ }
+ return String(value)
+}
+
+interface ResolvedConnection {
+ blockId: string
+ blockName: string
+ blockType: string
+ fields: Array<{ path: string; value: string; tag: string }>
+}
+
+/**
+ * Extract all variable references from nested subblock values
+ */
+function extractAllReferencesFromSubBlocks(subBlockValues: Record): string[] {
+ const refs = new Set()
+
+ const processValue = (value: unknown) => {
+ if (typeof value === 'string') {
+ const extracted = extractReferencePrefixes(value)
+ extracted.forEach((ref) => refs.add(ref.raw))
+ } else if (Array.isArray(value)) {
+ value.forEach(processValue)
+ } else if (value && typeof value === 'object') {
+ if ('value' in value) {
+ processValue((value as { value: unknown }).value)
+ } else {
+ Object.values(value).forEach(processValue)
+ }
+ }
+ }
+
+ Object.values(subBlockValues).forEach(processValue)
+ return Array.from(refs)
+}
+
+/**
+ * Format a value for inline display (single line, truncated)
+ */
+function formatInlineValue(value: unknown): string {
+ if (value === null || value === undefined) return 'null'
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ if (typeof value === 'object') {
+ try {
+ return JSON.stringify(value)
+ } catch {
+ return String(value)
+ }
+ }
+ return String(value)
+}
+
+interface ExecutionDataSectionProps {
+ title: string
+ data: unknown
+ isError?: boolean
+}
+
+/**
+ * Collapsible section for execution data (input/output)
+ * Uses Code.Viewer for proper syntax highlighting matching the logs UI
+ */
+function ExecutionDataSection({ title, data, isError = false }: ExecutionDataSectionProps) {
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ const jsonString = useMemo(() => {
+ if (!data) return ''
+ return formatValueAsJson(data)
+ }, [data])
+
+ const isEmpty = jsonString === '—' || jsonString === ''
+
+ return (
+
+
setIsExpanded(!isExpanded)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ setIsExpanded(!isExpanded)
+ }
+ }}
+ role='button'
+ tabIndex={0}
+ aria-expanded={isExpanded}
+ aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${title.toLowerCase()}`}
+ >
+
+ {title}
+
+
+
+
+ {isExpanded && (
+ <>
+ {isEmpty ? (
+
+ No data
+
+ ) : (
+
+ )}
+ >
+ )}
+
+ )
+}
+
+/**
+ * Section showing resolved variable references - styled like the connections section in editor
+ */
+function ResolvedConnectionsSection({ connections }: { connections: ResolvedConnection[] }) {
+ const [isCollapsed, setIsCollapsed] = useState(false)
+ const [expandedBlocks, setExpandedBlocks] = useState>(() => {
+ // Start with all blocks expanded
+ return new Set(connections.map((c) => c.blockId))
+ })
+
+ if (connections.length === 0) return null
+
+ const toggleBlock = (blockId: string) => {
+ setExpandedBlocks((prev) => {
+ const next = new Set(prev)
+ if (next.has(blockId)) {
+ next.delete(blockId)
+ } else {
+ next.add(blockId)
+ }
+ return next
+ })
+ }
+
+ return (
+
+ {/* Header with Chevron */}
+
setIsCollapsed(!isCollapsed)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ setIsCollapsed(!isCollapsed)
+ }
+ }}
+ role='button'
+ tabIndex={0}
+ aria-label={isCollapsed ? 'Expand connections' : 'Collapse connections'}
+ >
+
+
Connections
+
+
+ {/* Content - styled like ConnectionBlocks */}
+ {!isCollapsed && (
+
+ {connections.map((connection) => {
+ const blockConfig = getBlock(connection.blockType)
+ const Icon = blockConfig?.icon
+ const bgColor = blockConfig?.bgColor || '#6B7280'
+ const isExpanded = expandedBlocks.has(connection.blockId)
+ const hasFields = connection.fields.length > 0
+
+ return (
+
+ {/* Block header - styled like ConnectionItem */}
+
hasFields && toggleBlock(connection.blockId)}
+ >
+
+ {Icon && (
+
+ )}
+
+
+ {connection.blockName}
+
+ {hasFields && (
+
+ )}
+
+
+ {/* Fields - styled like FieldItem but showing resolved values */}
+ {isExpanded && hasFields && (
+
+
+ {connection.fields.map((field) => (
+
+
+ {field.path}
+
+
+ {field.value}
+
+
+ ))}
+
+ )}
+
+ )
+ })}
+
+ )}
+
+ )
+}
+
+/**
+ * Icon component for rendering block icons
+ */
+function IconComponent({
+ icon: Icon,
+ className,
+}: {
+ icon: BlockIcon | undefined
+ className?: string
+}) {
+ if (!Icon) return null
+ return
+}
+
+interface ExecutionData {
+ input?: unknown
+ output?: unknown
+ status?: string
+ durationMs?: number
+}
+
+interface BlockDetailsSidebarProps {
+ block: BlockState
+ executionData?: ExecutionData
+ /** All block execution data for resolving variable references */
+ allBlockExecutions?: Record
+ /** All workflow blocks for mapping block names to IDs */
+ workflowBlocks?: Record
+ /** When true, shows "Not Executed" badge if no executionData is provided */
+ isExecutionMode?: boolean
+ /** Optional close handler - if not provided, no close button is shown */
+ onClose?: () => void
+}
+
+/**
+ * Format duration for display
+ */
+function formatDuration(ms: number): string {
+ if (ms < 1000) return `${ms}ms`
+ return `${(ms / 1000).toFixed(2)}s`
+}
+
+/**
+ * Readonly sidebar panel showing block configuration using SubBlock components.
+ */
+function BlockDetailsSidebarContent({
+ block,
+ executionData,
+ allBlockExecutions,
+ workflowBlocks,
+ isExecutionMode = false,
+ onClose,
+}: BlockDetailsSidebarProps) {
+ const blockConfig = getBlock(block.type) as BlockConfig | undefined
+ const subBlockValues = block.subBlocks || {}
+
+ const blockNameToId = useMemo(() => {
+ const map = new Map()
+ if (workflowBlocks) {
+ for (const [blockId, blockData] of Object.entries(workflowBlocks)) {
+ if (blockData.name) {
+ map.set(normalizeName(blockData.name), blockId)
+ }
+ }
+ }
+ return map
+ }, [workflowBlocks])
+
+ const resolveReference = useMemo(() => {
+ return (reference: string): unknown => {
+ if (!allBlockExecutions || !workflowBlocks) return undefined
+
+ const inner = reference.slice(1, -1) // Remove < and >
+ const parts = inner.split('.')
+ if (parts.length < 1) return undefined
+
+ const [blockName, ...pathParts] = parts
+ const normalizedBlockName = normalizeName(blockName)
+
+ const blockId = blockNameToId.get(normalizedBlockName)
+ if (!blockId) return undefined
+
+ const blockExecution = allBlockExecutions[blockId]
+ if (!blockExecution?.output) return undefined
+
+ if (pathParts.length === 0) {
+ return blockExecution.output
+ }
+
+ return navigatePath(blockExecution.output, pathParts)
+ }
+ }, [allBlockExecutions, workflowBlocks, blockNameToId])
+
+ // Group resolved variables by source block for display
+ const resolvedConnections = useMemo((): ResolvedConnection[] => {
+ if (!allBlockExecutions || !workflowBlocks) return []
+
+ const allRefs = extractAllReferencesFromSubBlocks(subBlockValues)
+ const seen = new Set()
+ const blockMap = new Map()
+
+ for (const ref of allRefs) {
+ if (seen.has(ref)) continue
+
+ // Parse reference:
+ const inner = ref.slice(1, -1)
+ const parts = inner.split('.')
+ if (parts.length < 1) continue
+
+ const [blockName, ...pathParts] = parts
+ const normalizedBlockName = normalizeName(blockName)
+ const blockId = blockNameToId.get(normalizedBlockName)
+ if (!blockId) continue
+
+ const sourceBlock = workflowBlocks[blockId]
+ if (!sourceBlock) continue
+
+ const resolvedValue = resolveReference(ref)
+ if (resolvedValue === undefined) continue
+
+ seen.add(ref)
+
+ // Get or create block entry
+ if (!blockMap.has(blockId)) {
+ blockMap.set(blockId, {
+ blockId,
+ blockName: sourceBlock.name || blockName,
+ blockType: sourceBlock.type,
+ fields: [],
+ })
+ }
+
+ const connection = blockMap.get(blockId)!
+ connection.fields.push({
+ path: pathParts.join('.') || 'output',
+ value: formatInlineValue(resolvedValue),
+ tag: ref,
+ })
+ }
+
+ return Array.from(blockMap.values())
+ }, [subBlockValues, allBlockExecutions, workflowBlocks, blockNameToId, resolveReference])
+
+ if (!blockConfig) {
+ return (
+
+
+
+
+ {block.name || 'Unknown Block'}
+
+
+
+
Block configuration not found.
+
+
+ )
+ }
+
+ const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => {
+ if (subBlock.hidden || subBlock.hideFromPreview) return false
+ if (subBlock.mode === 'trigger') return false
+ if (subBlock.condition) {
+ return evaluateCondition(subBlock.condition, subBlockValues)
+ }
+ return true
+ })
+
+ const statusVariant =
+ executionData?.status === 'error'
+ ? 'red'
+ : executionData?.status === 'success'
+ ? 'green'
+ : 'gray'
+
+ return (
+
+ {/* Header - styled like editor */}
+
+
+
+
+
+ {block.name || blockConfig.name}
+
+ {block.enabled === false && (
+
+ Disabled
+
+ )}
+ {onClose && (
+
+ )}
+
+
+ {/* Scrollable content */}
+
+ {/* Not Executed Banner - shown when in execution mode but block wasn't executed */}
+ {isExecutionMode && !executionData && (
+
+ )}
+
+ {/* Execution Input/Output (if provided) */}
+ {executionData &&
+ (executionData.input !== undefined || executionData.output !== undefined) ? (
+
+ {/* Execution Status & Duration Header */}
+ {(executionData.status || executionData.durationMs !== undefined) && (
+
+ {executionData.status && (
+
+ {executionData.status}
+
+ )}
+ {executionData.durationMs !== undefined && (
+
+ {formatDuration(executionData.durationMs)}
+
+ )}
+
+ )}
+
+ {/* Divider between Status/Duration and Input/Output */}
+ {(executionData.status || executionData.durationMs !== undefined) &&
+ (executionData.input !== undefined || executionData.output !== undefined) && (
+
+ )}
+
+ {/* Input Section */}
+ {executionData.input !== undefined && (
+
+ )}
+
+ {/* Divider between Input and Output */}
+ {executionData.input !== undefined && executionData.output !== undefined && (
+
+ )}
+
+ {/* Output Section */}
+ {executionData.output !== undefined && (
+
+ )}
+
+ ) : null}
+
+ {/* Subblock Values - Using SubBlock components in preview mode */}
+
+ {/* CSS override to show full opacity and prevent interaction instead of dimmed disabled state */}
+
+ {visibleSubBlocks.length > 0 ? (
+
+ {visibleSubBlocks.map((subBlockConfig, index) => (
+
+
+ {index < visibleSubBlocks.length - 1 && (
+
+ )}
+
+ ))}
+
+ ) : (
+
+
+ No configurable fields for this block.
+
+
+ )}
+
+
+
+ {/* Resolved Variables Section - Pinned at bottom, outside scrollable area */}
+ {resolvedConnections.length > 0 && (
+
+ )}
+
+ )
+}
+
+/**
+ * Block details sidebar wrapped in ReactFlowProvider for hook compatibility.
+ */
+export function BlockDetailsSidebar(props: BlockDetailsSidebarProps) {
+ return (
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx
similarity index 96%
rename from apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx
index d8986c7c47..874f1975a5 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx
@@ -29,10 +29,8 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
}
const IconComponent = blockConfig.icon
- // Hide input handle for triggers, starters, or blocks in trigger mode
const isStarterOrTrigger = blockConfig.category === 'triggers' || type === 'starter' || isTrigger
- // Get visible subblocks from config (no fetching, just config structure)
const visibleSubBlocks = useMemo(() => {
if (!blockConfig.subBlocks) return []
@@ -48,7 +46,6 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
const hasSubBlocks = visibleSubBlocks.length > 0
const showErrorRow = !isStarterOrTrigger
- // Handle styles based on orientation
const horizontalHandleClass = '!border-none !bg-[var(--surface-7)] !h-5 !w-[7px] !rounded-[2px]'
const verticalHandleClass = '!border-none !bg-[var(--surface-7)] !h-[7px] !w-5 !rounded-[2px]'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx
similarity index 96%
rename from apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx
index a292d661ea..67befddbda 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx
@@ -26,11 +26,9 @@ function WorkflowPreviewSubflowInner({ data }: NodeProps
}
/**
@@ -105,10 +110,9 @@ export function WorkflowPreview({
onNodeClick,
lightweight = false,
cursorStyle = 'grab',
+ executedBlocks,
}: WorkflowPreviewProps) {
- // Use lightweight node types for better performance in template cards
const nodeTypes = lightweight ? lightweightNodeTypes : fullNodeTypes
- // Check if the workflow state is valid
const isValidWorkflowState = workflowState?.blocks && workflowState.edges
const blocksStructure = useMemo(() => {
@@ -178,9 +182,7 @@ export function WorkflowPreview({
const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks)
- // Lightweight mode: create minimal node data for performance
if (lightweight) {
- // Handle loops and parallels as subflow nodes
if (block.type === 'loop' || block.type === 'parallel') {
nodeArray.push({
id: blockId,
@@ -197,7 +199,6 @@ export function WorkflowPreview({
return
}
- // Regular blocks
nodeArray.push({
id: blockId,
type: 'workflowBlock',
@@ -214,10 +215,9 @@ export function WorkflowPreview({
return
}
- // Full mode: create detailed node data for interactive previews
if (block.type === 'loop') {
nodeArray.push({
- id: block.id,
+ id: blockId,
type: 'subflowNode',
position: absolutePosition,
parentId: block.data?.parentId,
@@ -238,7 +238,7 @@ export function WorkflowPreview({
if (block.type === 'parallel') {
nodeArray.push({
- id: block.id,
+ id: blockId,
type: 'subflowNode',
position: absolutePosition,
parentId: block.data?.parentId,
@@ -265,11 +265,22 @@ export function WorkflowPreview({
const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock'
+ let executionStatus: ExecutionStatus | undefined
+ if (executedBlocks) {
+ const blockExecution = executedBlocks[blockId]
+ if (blockExecution) {
+ executionStatus = blockExecution.status === 'error' ? 'error' : 'success'
+ } else {
+ executionStatus = 'not-executed'
+ }
+ }
+
nodeArray.push({
id: blockId,
type: nodeType,
position: absolutePosition,
draggable: false,
+ className: executionStatus ? `execution-${executionStatus}` : undefined,
data: {
type: block.type,
config: blockConfig,
@@ -278,43 +289,9 @@ export function WorkflowPreview({
canEdit: false,
isPreview: true,
subBlockValues: block.subBlocks ?? {},
+ executionStatus,
},
})
-
- if (block.type === 'loop') {
- const childBlocks = Object.entries(workflowState.blocks || {}).filter(
- ([_, childBlock]) => childBlock.data?.parentId === blockId
- )
-
- childBlocks.forEach(([childId, childBlock]) => {
- const childConfig = getBlock(childBlock.type)
-
- if (childConfig) {
- const childNodeType = childBlock.type === 'note' ? 'noteBlock' : 'workflowBlock'
-
- nodeArray.push({
- id: childId,
- type: childNodeType,
- position: {
- x: block.position.x + 50,
- y: block.position.y + (childBlock.position?.y || 100),
- },
- data: {
- type: childBlock.type,
- config: childConfig,
- name: childBlock.name,
- blockState: childBlock,
- showSubBlocks,
- isChild: true,
- parentId: blockId,
- canEdit: false,
- isPreview: true,
- },
- draggable: false,
- })
- }
- })
- }
})
return nodeArray
@@ -326,21 +303,41 @@ export function WorkflowPreview({
workflowState.blocks,
isValidWorkflowState,
lightweight,
+ executedBlocks,
])
const edges: Edge[] = useMemo(() => {
if (!isValidWorkflowState) return []
- return (workflowState.edges || []).map((edge) => ({
- id: edge.id,
- source: edge.source,
- target: edge.target,
- sourceHandle: edge.sourceHandle,
- targetHandle: edge.targetHandle,
- }))
- }, [edgesStructure, workflowState.edges, isValidWorkflowState])
+ return (workflowState.edges || []).map((edge) => {
+ let executionStatus: ExecutionStatus | undefined
+ if (executedBlocks) {
+ const sourceExecuted = executedBlocks[edge.source]
+ const targetExecuted = executedBlocks[edge.target]
+
+ if (sourceExecuted && targetExecuted) {
+ if (targetExecuted.status === 'error') {
+ executionStatus = 'error'
+ } else {
+ executionStatus = 'success'
+ }
+ } else {
+ executionStatus = 'not-executed'
+ }
+ }
+
+ return {
+ id: edge.id,
+ source: edge.source,
+ target: edge.target,
+ sourceHandle: edge.sourceHandle,
+ targetHandle: edge.targetHandle,
+ data: executionStatus ? { executionStatus } : undefined,
+ className: executionStatus === 'not-executed' ? 'execution-not-executed' : undefined,
+ }
+ })
+ }, [edgesStructure, workflowState.edges, isValidWorkflowState, executedBlocks])
- // Handle migrated logs that don't have complete workflow state
if (!isValidWorkflowState) {
return (
- {cursorStyle && (
-
- )}
+
acc + char.charCodeAt(0), 0))
-}
-
-/**
- * Gets a consistent color for a user based on their ID.
- * The same user will always get the same color across cursors, avatars, and terminal.
- *
- * @param userId - The unique user identifier
- * @returns A hex color string
- */
-export function getUserColor(userId: string): string {
- const hash = hashUserId(userId)
- return USER_COLORS[hash % USER_COLORS.length]
-}
-
-/**
- * Creates a stable mapping of user IDs to color indices for a list of users.
- * Useful when you need to maintain consistent color assignments across renders.
- *
- * @param userIds - Array of user IDs to map
- * @returns Map of user ID to color index
- */
-export function createUserColorMap(userIds: string[]): Map {
- const colorMap = new Map()
- let colorIndex = 0
-
- for (const userId of userIds) {
- if (!colorMap.has(userId)) {
- colorMap.set(userId, colorIndex++)
- }
- }
-
- return colorMap
-}
diff --git a/apps/sim/components/emcn/components/checkbox/checkbox.tsx b/apps/sim/components/emcn/components/checkbox/checkbox.tsx
index 6e5b6f64c7..c32ba636c3 100644
--- a/apps/sim/components/emcn/components/checkbox/checkbox.tsx
+++ b/apps/sim/components/emcn/components/checkbox/checkbox.tsx
@@ -23,7 +23,7 @@ import { cn } from '@/lib/core/utils/cn'
* ```
*/
const checkboxVariants = cva(
- 'peer shrink-0 rounded-sm border border-[var(--border-1)] bg-[var(--surface-4)] ring-offset-background transition-colors hover:border-[var(--border-muted)] hover:bg-[var(--surface-7)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-[var(--text-muted)] data-[state=checked]:bg-[var(--text-muted)] data-[state=checked]:text-white dark:bg-[var(--surface-5)] dark:data-[state=checked]:border-[var(--surface-7)] dark:data-[state=checked]:bg-[var(--surface-7)] dark:data-[state=checked]:text-[var(--text-primary)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]',
+ 'peer shrink-0 rounded-sm border border-[var(--border-1)] bg-[var(--surface-4)] ring-offset-background transition-colors hover:border-[var(--border-muted)] hover:bg-[var(--surface-7)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[state=checked]:border-[var(--text-muted)] data-[state=checked]:bg-[var(--text-muted)] data-[state=checked]:text-white dark:bg-[var(--surface-5)] dark:data-[state=checked]:border-[var(--surface-7)] dark:data-[state=checked]:bg-[var(--surface-7)] dark:data-[state=checked]:text-[var(--text-primary)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]',
{
variants: {
size: {
diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx
index b72db43c00..19dc56a343 100644
--- a/apps/sim/components/emcn/components/combobox/combobox.tsx
+++ b/apps/sim/components/emcn/components/combobox/combobox.tsx
@@ -467,7 +467,12 @@ const Combobox = forwardRef(
{...inputProps}
/>
{(overlayContent || SelectedIcon) && (
-
+
{overlayContent ? (
overlayContent
) : (
@@ -505,6 +510,7 @@ const Combobox = forwardRef(
className={cn(
comboboxVariants({ variant, size }),
'relative cursor-pointer items-center justify-between',
+ disabled && 'cursor-not-allowed opacity-50',
className
)}
onClick={handleToggle}
diff --git a/apps/sim/components/emcn/components/date-picker/date-picker.tsx b/apps/sim/components/emcn/components/date-picker/date-picker.tsx
index 3196852bde..1fb2616dad 100644
--- a/apps/sim/components/emcn/components/date-picker/date-picker.tsx
+++ b/apps/sim/components/emcn/components/date-picker/date-picker.tsx
@@ -844,6 +844,7 @@ const DatePicker = React.forwardRef((props, ref
className={cn(
datePickerVariants({ variant, size }),
'relative cursor-pointer items-center justify-between',
+ disabled && 'cursor-not-allowed opacity-50',
className
)}
onClick={handleTriggerClick}
diff --git a/apps/sim/components/emcn/components/slider/slider.tsx b/apps/sim/components/emcn/components/slider/slider.tsx
index d6ccc54b72..71a84a9120 100644
--- a/apps/sim/components/emcn/components/slider/slider.tsx
+++ b/apps/sim/components/emcn/components/slider/slider.tsx
@@ -16,12 +16,13 @@ export interface SliderProps extends React.ComponentPropsWithoutRef, SliderProps>(
- ({ className, ...props }, ref) => (
+ ({ className, disabled, ...props }, ref) => (
,
React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
+>(({ className, disabled, ...props }, ref) => (
{
+ const colorMap = new Map()
+ let colorIndex = 0
+
+ for (const userId of userIds) {
+ if (!colorMap.has(userId)) {
+ colorMap.set(userId, colorIndex++)
+ }
+ }
+
+ return colorMap
+}