Skip to content

Commit f00a7a0

Browse files
committed
fix(preview): subflows
1 parent 37c13c8 commit f00a7a0

File tree

6 files changed

+378
-34
lines changed

6 files changed

+378
-34
lines changed

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ export function ExecutionSnapshot({
258258
allBlockExecutions={blockExecutions}
259259
workflowBlocks={workflowState.blocks}
260260
workflowVariables={workflowState.variables}
261+
loops={workflowState.loops}
262+
parallels={workflowState.parallels}
261263
isExecutionMode
262264
onClose={() => setPinnedBlockId(null)}
263265
/>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ export function GeneralDeploy({
346346
<BlockDetailsSidebar
347347
block={workflowToShow.blocks[expandedSelectedBlockId]}
348348
workflowVariables={workflowToShow.variables}
349+
loops={workflowToShow.loops}
350+
parallels={workflowToShow.parallels}
349351
onClose={() => setExpandedSelectedBlockId(null)}
350352
/>
351353
)}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export interface SubflowNodeData {
4747
parentId?: string
4848
extent?: 'parent'
4949
isPreview?: boolean
50+
/** Whether this subflow is selected in preview mode */
51+
isPreviewSelected?: boolean
5052
kind: 'loop' | 'parallel'
5153
name?: string
5254
}
@@ -123,15 +125,17 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
123125
return { top: `${HANDLE_POSITIONS.DEFAULT_Y_OFFSET}px`, transform: 'translateY(-50%)' }
124126
}
125127

128+
const isPreviewSelected = data?.isPreviewSelected || false
129+
126130
/**
127131
* Determine the ring styling based on subflow state priority:
128-
* 1. Focused (selected in editor) - blue ring
132+
* 1. Focused (selected in editor) or preview selected - blue ring
129133
* 2. Diff status (version comparison) - green/orange ring
130134
*/
131-
const hasRing = isFocused || diffStatus === 'new' || diffStatus === 'edited'
135+
const hasRing = isFocused || isPreviewSelected || diffStatus === 'new' || diffStatus === 'edited'
132136
const ringStyles = cn(
133137
hasRing && 'ring-[1.75px]',
134-
isFocused && 'ring-[var(--brand-secondary)]',
138+
(isFocused || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
135139
diffStatus === 'new' && 'ring-[#22C55F]',
136140
diffStatus === 'edited' && 'ring-[var(--warning)]'
137141
)

apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx

Lines changed: 241 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
'use client'
22

33
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4-
import { ArrowDown, ArrowUp, ChevronDown as ChevronDownIcon, ChevronUp, X } from 'lucide-react'
4+
import {
5+
ArrowDown,
6+
ArrowUp,
7+
ChevronDown as ChevronDownIcon,
8+
ChevronUp,
9+
RepeatIcon,
10+
SplitIcon,
11+
X,
12+
} from 'lucide-react'
513
import { ReactFlowProvider } from 'reactflow'
6-
import { Badge, Button, ChevronDown, Code, Input } from '@/components/emcn'
14+
import { Badge, Button, ChevronDown, Code, Combobox, Input, Label } from '@/components/emcn'
715
import { cn } from '@/lib/core/utils/cn'
816
import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references'
917
import { SnapshotContextMenu } from '@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/components'
@@ -14,7 +22,7 @@ import type { BlockConfig, BlockIcon, SubBlockConfig } from '@/blocks/types'
1422
import { normalizeName } from '@/executor/constants'
1523
import { navigatePath } from '@/executor/variables/resolvers/reference'
1624
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
17-
import type { BlockState } from '@/stores/workflows/workflow/types'
25+
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
1826

1927
/**
2028
* Evaluate whether a subblock's condition is met based on current values.
@@ -547,6 +555,165 @@ function IconComponent({
547555
return <Icon className={className} />
548556
}
549557

558+
/**
559+
* Configuration for subflow types (loop and parallel) - matches use-subflow-editor.ts
560+
*/
561+
const SUBFLOW_CONFIG = {
562+
loop: {
563+
typeLabels: {
564+
for: 'For Loop',
565+
forEach: 'For Each',
566+
while: 'While Loop',
567+
doWhile: 'Do While Loop',
568+
},
569+
maxIterations: 1000,
570+
},
571+
parallel: {
572+
typeLabels: {
573+
count: 'Parallel Count',
574+
collection: 'Parallel Each',
575+
},
576+
maxIterations: 20,
577+
},
578+
} as const
579+
580+
interface SubflowConfigDisplayProps {
581+
block: BlockState
582+
loop?: Loop
583+
parallel?: Parallel
584+
}
585+
586+
/**
587+
* Display subflow (loop/parallel) configuration in preview mode.
588+
* Matches the exact UI structure of SubflowEditor.
589+
*/
590+
function SubflowConfigDisplay({ block, loop, parallel }: SubflowConfigDisplayProps) {
591+
const isLoop = block.type === 'loop'
592+
const config = isLoop ? SUBFLOW_CONFIG.loop : SUBFLOW_CONFIG.parallel
593+
594+
// Determine current type
595+
const currentType = isLoop
596+
? loop?.loopType || (block.data?.loopType as string) || 'for'
597+
: parallel?.parallelType || (block.data?.parallelType as string) || 'count'
598+
599+
// Build type options for combobox - matches SubflowEditor
600+
const typeOptions = Object.entries(config.typeLabels).map(([value, label]) => ({
601+
value,
602+
label,
603+
}))
604+
605+
// Determine mode
606+
const isCountMode = currentType === 'for' || currentType === 'count'
607+
const isConditionMode = currentType === 'while' || currentType === 'doWhile'
608+
609+
// Get iterations value
610+
const iterations = isLoop
611+
? (loop?.iterations ?? (block.data?.count as number) ?? 5)
612+
: (parallel?.count ?? (block.data?.count as number) ?? 1)
613+
614+
// Get collection/condition value
615+
const getEditorValue = (): string => {
616+
if (isConditionMode && isLoop) {
617+
if (currentType === 'while') {
618+
return loop?.whileCondition || (block.data?.whileCondition as string) || ''
619+
}
620+
return loop?.doWhileCondition || (block.data?.doWhileCondition as string) || ''
621+
}
622+
623+
if (isLoop) {
624+
const items = loop?.forEachItems ?? block.data?.collection
625+
return typeof items === 'string' ? items : JSON.stringify(items) || ''
626+
}
627+
628+
const distribution = parallel?.distribution ?? block.data?.collection
629+
return typeof distribution === 'string' ? distribution : JSON.stringify(distribution) || ''
630+
}
631+
632+
const editorValue = getEditorValue()
633+
634+
// Get label for configuration field - matches SubflowEditor exactly
635+
const getConfigLabel = (): string => {
636+
if (isCountMode) {
637+
return `${isLoop ? 'Loop' : 'Parallel'} Iterations`
638+
}
639+
if (isConditionMode) {
640+
return 'While Condition'
641+
}
642+
return `${isLoop ? 'Collection' : 'Parallel'} Items`
643+
}
644+
645+
return (
646+
<div className='flex-1 overflow-y-auto overflow-x-hidden pt-[5px] pb-[8px]'>
647+
{/* Type Selection - matches SubflowEditor */}
648+
<div>
649+
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
650+
{isLoop ? 'Loop Type' : 'Parallel Type'}
651+
</Label>
652+
<Combobox
653+
options={typeOptions}
654+
value={currentType}
655+
onChange={() => {}}
656+
disabled
657+
placeholder='Select type...'
658+
/>
659+
</div>
660+
661+
{/* Dashed Line Separator - matches SubflowEditor */}
662+
<div className='px-[2px] pt-[16px] pb-[10px]'>
663+
<div
664+
className='h-[1.25px]'
665+
style={{
666+
backgroundImage:
667+
'repeating-linear-gradient(to right, var(--border) 0px, var(--border) 6px, transparent 6px, transparent 12px)',
668+
}}
669+
/>
670+
</div>
671+
672+
{/* Configuration - matches SubflowEditor */}
673+
<div>
674+
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
675+
{getConfigLabel()}
676+
</Label>
677+
678+
{isCountMode ? (
679+
<div>
680+
<Input
681+
type='text'
682+
value={iterations.toString()}
683+
onChange={() => {}}
684+
disabled
685+
className='mb-[4px]'
686+
/>
687+
<div className='text-[10px] text-muted-foreground'>
688+
Enter a number between 1 and {config.maxIterations}
689+
</div>
690+
</div>
691+
) : (
692+
<div className='relative'>
693+
<Code.Container>
694+
<Code.Content>
695+
<Code.Placeholder gutterWidth={0} show={editorValue.length === 0}>
696+
{isConditionMode ? '<counter.value> < 10' : "['item1', 'item2', 'item3']"}
697+
</Code.Placeholder>
698+
<div
699+
className='min-h-[24px] whitespace-pre-wrap break-all px-[12px] py-[8px] font-mono text-[13px] text-[var(--text-secondary)]'
700+
style={{ pointerEvents: 'none' }}
701+
>
702+
{editorValue || (
703+
<span className='text-[var(--text-tertiary)]'>
704+
{isConditionMode ? '<counter.value> < 10' : "['item1', 'item2', 'item3']"}
705+
</span>
706+
)}
707+
</div>
708+
</Code.Content>
709+
</Code.Container>
710+
</div>
711+
)}
712+
</div>
713+
</div>
714+
)
715+
}
716+
550717
interface ExecutionData {
551718
input?: unknown
552719
output?: unknown
@@ -570,6 +737,10 @@ interface BlockDetailsSidebarProps {
570737
workflowBlocks?: Record<string, BlockState>
571738
/** Workflow variables for resolving variable references */
572739
workflowVariables?: Record<string, WorkflowVariable>
740+
/** Loop configurations for subflow blocks */
741+
loops?: Record<string, Loop>
742+
/** Parallel configurations for subflow blocks */
743+
parallels?: Record<string, Parallel>
573744
/** When true, shows "Not Executed" badge if no executionData is provided */
574745
isExecutionMode?: boolean
575746
/** Optional close handler - if not provided, no close button is shown */
@@ -600,6 +771,8 @@ function BlockDetailsSidebarContent({
600771
allBlockExecutions,
601772
workflowBlocks,
602773
workflowVariables,
774+
loops,
775+
parallels,
603776
isExecutionMode = false,
604777
onClose,
605778
}: BlockDetailsSidebarProps) {
@@ -868,6 +1041,71 @@ function BlockDetailsSidebarContent({
8681041
})
8691042
}, [extractedRefs.envVars])
8701043

1044+
// Check if this is a subflow block (loop or parallel)
1045+
const isSubflow = block.type === 'loop' || block.type === 'parallel'
1046+
const loopConfig = block.type === 'loop' ? loops?.[block.id] : undefined
1047+
const parallelConfig = block.type === 'parallel' ? parallels?.[block.id] : undefined
1048+
1049+
// Handle subflow blocks
1050+
if (isSubflow) {
1051+
const isLoop = block.type === 'loop'
1052+
const SubflowIcon = isLoop ? RepeatIcon : SplitIcon
1053+
const subflowBgColor = isLoop ? '#2FB3FF' : '#FEE12B'
1054+
const subflowName = block.name || (isLoop ? 'Loop' : 'Parallel')
1055+
1056+
return (
1057+
<div className='relative flex h-full w-80 flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--surface-1)]'>
1058+
{/* Header - styled like subflow header */}
1059+
<div className='mx-[-1px] flex flex-shrink-0 items-center gap-[8px] rounded-b-[4px] border-[var(--border)] border-x border-b bg-[var(--surface-4)] px-[12px] py-[6px]'>
1060+
<div
1061+
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px]'
1062+
style={{ backgroundColor: subflowBgColor }}
1063+
>
1064+
<SubflowIcon className='h-[12px] w-[12px] text-white' />
1065+
</div>
1066+
<span className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
1067+
{subflowName}
1068+
</span>
1069+
{onClose && (
1070+
<Button variant='ghost' className='!p-[4px] flex-shrink-0' onClick={onClose}>
1071+
<X className='h-[14px] w-[14px]' />
1072+
</Button>
1073+
)}
1074+
</div>
1075+
1076+
{/* Subflow Configuration */}
1077+
<div className='flex flex-1 flex-col overflow-hidden pt-[0px]'>
1078+
<div className='flex-1 overflow-y-auto overflow-x-hidden'>
1079+
<div className='readonly-preview px-[8px]'>
1080+
{/* CSS override to show full opacity and prevent interaction instead of dimmed disabled state */}
1081+
<style>{`
1082+
.readonly-preview,
1083+
.readonly-preview * {
1084+
cursor: default !important;
1085+
}
1086+
.readonly-preview [disabled],
1087+
.readonly-preview [data-disabled],
1088+
.readonly-preview input,
1089+
.readonly-preview textarea,
1090+
.readonly-preview [role="combobox"],
1091+
.readonly-preview [role="slider"],
1092+
.readonly-preview [role="switch"],
1093+
.readonly-preview [role="checkbox"] {
1094+
opacity: 1 !important;
1095+
pointer-events: none;
1096+
}
1097+
.readonly-preview .opacity-50 {
1098+
opacity: 1 !important;
1099+
}
1100+
`}</style>
1101+
<SubflowConfigDisplay block={block} loop={loopConfig} parallel={parallelConfig} />
1102+
</div>
1103+
</div>
1104+
</div>
1105+
</div>
1106+
)
1107+
}
1108+
8711109
if (!blockConfig) {
8721110
return (
8731111
<div className='flex h-full w-80 flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--surface-1)]'>

0 commit comments

Comments
 (0)