mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix: added preview for subgroup nodes, changed position of child blocks in state
This commit is contained in:
@@ -21,6 +21,7 @@ interface LoopNodeData {
|
||||
loopType?: 'for' | 'forEach'
|
||||
count?: number
|
||||
collection?: string | any[] | Record<string, any>
|
||||
isPreview?: boolean
|
||||
executionState?: {
|
||||
currentIteration: number
|
||||
isExecuting: boolean
|
||||
@@ -35,6 +36,9 @@ interface LoopBadgesProps {
|
||||
}
|
||||
|
||||
export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
// Check if this is preview mode
|
||||
const isPreview = data?.isPreview || false
|
||||
|
||||
// State
|
||||
const [loopType, setLoopType] = useState(data?.loopType || 'for')
|
||||
const [iterations, setIterations] = useState(data?.count || 5)
|
||||
@@ -50,6 +54,8 @@ export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
// Get store methods
|
||||
const updateNodeData = useCallback(
|
||||
(updates: Partial<LoopNodeData>) => {
|
||||
if (isPreview) return // Don't update in preview mode
|
||||
|
||||
useWorkflowStore.setState((state) => ({
|
||||
blocks: {
|
||||
...state.blocks,
|
||||
@@ -63,7 +69,7 @@ export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
},
|
||||
}))
|
||||
},
|
||||
[nodeId]
|
||||
[nodeId, isPreview]
|
||||
)
|
||||
|
||||
const updateLoopType = useWorkflowStore((state) => state.updateLoopType)
|
||||
@@ -94,27 +100,36 @@ export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
// Handle loop type change
|
||||
const handleLoopTypeChange = useCallback(
|
||||
(newType: 'for' | 'forEach') => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
setLoopType(newType)
|
||||
updateLoopType(nodeId, newType)
|
||||
setTypePopoverOpen(false)
|
||||
},
|
||||
[nodeId, updateLoopType]
|
||||
[nodeId, updateLoopType, isPreview]
|
||||
)
|
||||
|
||||
// Handle iterations input change
|
||||
const handleIterationsChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = e.target.value.replace(/[^0-9]/g, '')
|
||||
const numValue = Number.parseInt(sanitizedValue)
|
||||
const handleIterationsChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
if (!Number.isNaN(numValue)) {
|
||||
setInputValue(Math.min(100, numValue).toString())
|
||||
} else {
|
||||
setInputValue(sanitizedValue)
|
||||
}
|
||||
}, [])
|
||||
const sanitizedValue = e.target.value.replace(/[^0-9]/g, '')
|
||||
const numValue = Number.parseInt(sanitizedValue)
|
||||
|
||||
if (!Number.isNaN(numValue)) {
|
||||
setInputValue(Math.min(100, numValue).toString())
|
||||
} else {
|
||||
setInputValue(sanitizedValue)
|
||||
}
|
||||
},
|
||||
[isPreview]
|
||||
)
|
||||
|
||||
// Handle iterations save
|
||||
const handleIterationsSave = useCallback(() => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
const value = Number.parseInt(inputValue)
|
||||
|
||||
if (!Number.isNaN(value)) {
|
||||
@@ -126,11 +141,13 @@ export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
setInputValue(iterations.toString())
|
||||
}
|
||||
setConfigPopoverOpen(false)
|
||||
}, [inputValue, iterations, nodeId, updateLoopCount])
|
||||
}, [inputValue, iterations, nodeId, updateLoopCount, isPreview])
|
||||
|
||||
// Handle editor change with tag dropdown support
|
||||
const handleEditorChange = useCallback(
|
||||
(value: string) => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
setEditorValue(value)
|
||||
updateLoopCollection(nodeId, value)
|
||||
|
||||
@@ -146,12 +163,14 @@ export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
setShowTagDropdown(triggerCheck.show)
|
||||
}
|
||||
},
|
||||
[nodeId, updateLoopCollection]
|
||||
[nodeId, updateLoopCollection, isPreview]
|
||||
)
|
||||
|
||||
// Handle tag selection
|
||||
const handleTagSelect = useCallback(
|
||||
(newValue: string) => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
setEditorValue(newValue)
|
||||
updateLoopCollection(nodeId, newValue)
|
||||
setShowTagDropdown(false)
|
||||
@@ -164,137 +183,149 @@ export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
[nodeId, updateLoopCollection]
|
||||
[nodeId, updateLoopCollection, isPreview]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='-top-9 absolute right-0 left-0 z-10 flex justify-between'>
|
||||
{/* Loop Type Badge */}
|
||||
<Popover open={typePopoverOpen} onOpenChange={setTypePopoverOpen}>
|
||||
<Popover
|
||||
open={!isPreview && typePopoverOpen}
|
||||
onOpenChange={isPreview ? undefined : setTypePopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'border-border bg-background/80 py-0.5 pr-1.5 pl-2.5 font-medium text-foreground text-sm backdrop-blur-sm',
|
||||
'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
!isPreview && 'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
'flex items-center gap-1'
|
||||
)}
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
>
|
||||
{loopType === 'for' ? 'For Loop' : 'For Each'}
|
||||
<ChevronDown className='h-3 w-3 text-muted-foreground' />
|
||||
{!isPreview && <ChevronDown className='h-3 w-3 text-muted-foreground' />}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-48 p-3' align='center' onClick={(e) => e.stopPropagation()}>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Loop Type</div>
|
||||
<div className='space-y-1'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
loopType === 'for' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleLoopTypeChange('for')}
|
||||
>
|
||||
<span className='text-sm'>For Loop</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
loopType === 'forEach' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleLoopTypeChange('forEach')}
|
||||
>
|
||||
<span className='text-sm'>For Each</span>
|
||||
{!isPreview && (
|
||||
<PopoverContent className='w-48 p-3' align='center' onClick={(e) => e.stopPropagation()}>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Loop Type</div>
|
||||
<div className='space-y-1'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
loopType === 'for' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleLoopTypeChange('for')}
|
||||
>
|
||||
<span className='text-sm'>For Loop</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
loopType === 'forEach' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleLoopTypeChange('forEach')}
|
||||
>
|
||||
<span className='text-sm'>For Each</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{/* Iterations/Collection Badge */}
|
||||
<Popover open={configPopoverOpen} onOpenChange={setConfigPopoverOpen}>
|
||||
<Popover
|
||||
open={!isPreview && configPopoverOpen}
|
||||
onOpenChange={isPreview ? undefined : setConfigPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'border-border bg-background/80 py-0.5 pr-1.5 pl-2.5 font-medium text-foreground text-sm backdrop-blur-sm',
|
||||
'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
!isPreview && 'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
'flex items-center gap-1'
|
||||
)}
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
>
|
||||
{loopType === 'for' ? `Iterations: ${iterations}` : 'Items'}
|
||||
<ChevronDown className='h-3 w-3 text-muted-foreground' />
|
||||
{!isPreview && <ChevronDown className='h-3 w-3 text-muted-foreground' />}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={cn('p-3', loopType !== 'for' ? 'w-72' : 'w-48')}
|
||||
align='center'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>
|
||||
{loopType === 'for' ? 'Loop Iterations' : 'Collection Items'}
|
||||
</div>
|
||||
|
||||
{loopType === 'for' ? (
|
||||
// Number input for 'for' loops
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
type='text'
|
||||
value={inputValue}
|
||||
onChange={handleIterationsChange}
|
||||
onBlur={handleIterationsSave}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleIterationsSave()}
|
||||
className='h-8 text-sm'
|
||||
autoFocus
|
||||
/>
|
||||
{!isPreview && (
|
||||
<PopoverContent
|
||||
className={cn('p-3', loopType !== 'for' ? 'w-72' : 'w-48')}
|
||||
align='center'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>
|
||||
{loopType === 'for' ? 'Loop Iterations' : 'Collection Items'}
|
||||
</div>
|
||||
) : (
|
||||
// Code editor for 'forEach' loops
|
||||
<div ref={editorContainerRef} className='relative'>
|
||||
<div className='relative min-h-[80px] rounded-md border border-input bg-background px-3 pt-2 pb-3 font-mono text-sm'>
|
||||
{editorValue === '' && (
|
||||
<div className='pointer-events-none absolute top-[8.5px] left-3 select-none text-muted-foreground/50'>
|
||||
["item1", "item2", "item3"]
|
||||
</div>
|
||||
|
||||
{loopType === 'for' ? (
|
||||
// Number input for 'for' loops
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
type='text'
|
||||
value={inputValue}
|
||||
onChange={handleIterationsChange}
|
||||
onBlur={handleIterationsSave}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleIterationsSave()}
|
||||
className='h-8 text-sm'
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Code editor for 'forEach' loops
|
||||
<div ref={editorContainerRef} className='relative'>
|
||||
<div className='relative min-h-[80px] rounded-md border border-input bg-background px-3 pt-2 pb-3 font-mono text-sm'>
|
||||
{editorValue === '' && (
|
||||
<div className='pointer-events-none absolute top-[8.5px] left-3 select-none text-muted-foreground/50'>
|
||||
["item1", "item2", "item3"]
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
value={editorValue}
|
||||
onValueChange={handleEditorChange}
|
||||
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
|
||||
padding={0}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: '21px',
|
||||
}}
|
||||
className='w-full focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full overflow-hidden whitespace-pre-wrap'
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2 text-[10px] text-muted-foreground'>
|
||||
Array or object to iterate over. Type "{'<'}" to reference other blocks.
|
||||
</div>
|
||||
{showTagDropdown && (
|
||||
<TagDropdown
|
||||
visible={showTagDropdown}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={nodeId}
|
||||
activeSourceBlockId={null}
|
||||
inputValue={editorValue}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => setShowTagDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
<Editor
|
||||
value={editorValue}
|
||||
onValueChange={handleEditorChange}
|
||||
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
|
||||
padding={0}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: '21px',
|
||||
}}
|
||||
className='w-full focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full overflow-hidden whitespace-pre-wrap'
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2 text-[10px] text-muted-foreground'>
|
||||
Array or object to iterate over. Type "{'<'}" to reference other blocks.
|
||||
</div>
|
||||
{showTagDropdown && (
|
||||
<TagDropdown
|
||||
visible={showTagDropdown}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={nodeId}
|
||||
activeSourceBlockId={null}
|
||||
inputValue={editorValue}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => setShowTagDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{loopType === 'for' && (
|
||||
<div className='text-[10px] text-muted-foreground'>
|
||||
Enter a number between 1 and 100
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
{loopType === 'for' && (
|
||||
<div className='text-[10px] text-muted-foreground'>
|
||||
Enter a number between 1 and 100
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -72,6 +72,9 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => {
|
||||
const removeBlock = useWorkflowStore((state) => state.removeBlock)
|
||||
const blockRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Check if this is preview mode
|
||||
const isPreview = data?.isPreview || false
|
||||
|
||||
// Determine nesting level by counting parents
|
||||
const nestingLevel = useMemo(() => {
|
||||
let level = 0
|
||||
@@ -91,7 +94,7 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => {
|
||||
const getNestedStyles = () => {
|
||||
// Base styles
|
||||
const styles: Record<string, string> = {
|
||||
backgroundColor: data?.state === 'valid' ? 'rgba(34,197,94,0.05)' : 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
|
||||
// Apply nested styles
|
||||
@@ -118,7 +121,7 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => {
|
||||
' relative cursor-default select-none',
|
||||
'transition-block-bg transition-ring',
|
||||
'z-[20]',
|
||||
data?.state === 'valid' && 'bg-[rgba(34,197,94,0.05)] ring-2 ring-[#2FB3FF]',
|
||||
data?.state === 'valid',
|
||||
nestingLevel > 0 &&
|
||||
`border border-[0.5px] ${nestingLevel % 2 === 0 ? 'border-slate-300/60' : 'border-slate-400/60'}`
|
||||
)}
|
||||
@@ -128,23 +131,27 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => {
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
...nestedStyles,
|
||||
pointerEvents: 'all',
|
||||
pointerEvents: isPreview ? 'none' : 'all',
|
||||
}}
|
||||
data-node-id={id}
|
||||
data-type='loopNode'
|
||||
data-nesting-level={nestingLevel}
|
||||
>
|
||||
{/* Critical drag handle that controls only the loop node movement */}
|
||||
<div
|
||||
className='workflow-drag-handle absolute top-0 right-0 left-0 z-10 h-10 cursor-move'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
{!isPreview && (
|
||||
<div
|
||||
className='workflow-drag-handle absolute top-0 right-0 left-0 z-10 h-10 cursor-move'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Custom visible resize handle */}
|
||||
<div
|
||||
className='absolute right-2 bottom-2 z-20 flex h-8 w-8 cursor-se-resize items-center justify-center text-muted-foreground'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
{!isPreview && (
|
||||
<div
|
||||
className='absolute right-2 bottom-2 z-20 flex h-8 w-8 cursor-se-resize items-center justify-center text-muted-foreground'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Child nodes container - Enable pointer events to allow dragging of children */}
|
||||
<div
|
||||
@@ -153,27 +160,29 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => {
|
||||
style={{
|
||||
position: 'relative',
|
||||
minHeight: '100%',
|
||||
pointerEvents: 'auto',
|
||||
pointerEvents: isPreview ? 'none' : 'auto',
|
||||
}}
|
||||
>
|
||||
{/* Delete button - styled like in action-bar.tsx */}
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeBlock(id)
|
||||
}}
|
||||
className='absolute top-2 right-2 z-20 text-gray-500 opacity-0 transition-opacity duration-200 hover:text-red-600 group-hover:opacity-100'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
{!isPreview && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeBlock(id)
|
||||
}}
|
||||
className='absolute top-2 right-2 z-20 text-gray-500 opacity-0 transition-opacity duration-200 hover:text-red-600 group-hover:opacity-100'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Loop Start Block */}
|
||||
<div
|
||||
className='-translate-y-1/2 absolute top-1/2 left-8 flex h-10 w-10 transform items-center justify-center rounded-md bg-[#2FB3FF] p-2'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
data-parent-id={id}
|
||||
data-node-role='loop-start'
|
||||
data-extent='parent'
|
||||
|
||||
@@ -21,6 +21,7 @@ interface ParallelNodeData {
|
||||
parallelType?: 'count' | 'collection'
|
||||
count?: number
|
||||
collection?: string | any[] | Record<string, any>
|
||||
isPreview?: boolean
|
||||
executionState?: {
|
||||
currentExecution: number
|
||||
isExecuting: boolean
|
||||
@@ -35,6 +36,9 @@ interface ParallelBadgesProps {
|
||||
}
|
||||
|
||||
export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
// Check if this is preview mode
|
||||
const isPreview = data?.isPreview || false
|
||||
|
||||
// State
|
||||
const [parallelType, setParallelType] = useState<'count' | 'collection'>(
|
||||
data?.parallelType || 'collection'
|
||||
@@ -56,6 +60,8 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
// Update node data to include parallel type
|
||||
const updateNodeData = useCallback(
|
||||
(updates: Partial<ParallelNodeData>) => {
|
||||
if (isPreview) return // Don't update in preview mode
|
||||
|
||||
useWorkflowStore.setState((state) => ({
|
||||
blocks: {
|
||||
...state.blocks,
|
||||
@@ -69,7 +75,7 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
},
|
||||
}))
|
||||
},
|
||||
[nodeId]
|
||||
[nodeId, isPreview]
|
||||
)
|
||||
|
||||
// Initialize state from data when it changes
|
||||
@@ -94,6 +100,8 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
// Handle parallel type change
|
||||
const handleParallelTypeChange = useCallback(
|
||||
(newType: 'count' | 'collection') => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
setParallelType(newType)
|
||||
updateNodeData({ parallelType: newType })
|
||||
|
||||
@@ -108,23 +116,38 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
|
||||
setTypePopoverOpen(false)
|
||||
},
|
||||
[nodeId, iterations, editorValue, updateNodeData, updateParallelCount, updateParallelCollection]
|
||||
[
|
||||
nodeId,
|
||||
iterations,
|
||||
editorValue,
|
||||
updateNodeData,
|
||||
updateParallelCount,
|
||||
updateParallelCollection,
|
||||
isPreview,
|
||||
]
|
||||
)
|
||||
|
||||
// Handle iterations input change
|
||||
const handleIterationsChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = e.target.value.replace(/[^0-9]/g, '')
|
||||
const numValue = Number.parseInt(sanitizedValue)
|
||||
const handleIterationsChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
if (!Number.isNaN(numValue)) {
|
||||
setInputValue(Math.min(20, numValue).toString())
|
||||
} else {
|
||||
setInputValue(sanitizedValue)
|
||||
}
|
||||
}, [])
|
||||
const sanitizedValue = e.target.value.replace(/[^0-9]/g, '')
|
||||
const numValue = Number.parseInt(sanitizedValue)
|
||||
|
||||
if (!Number.isNaN(numValue)) {
|
||||
setInputValue(Math.min(20, numValue).toString())
|
||||
} else {
|
||||
setInputValue(sanitizedValue)
|
||||
}
|
||||
},
|
||||
[isPreview]
|
||||
)
|
||||
|
||||
// Handle iterations save
|
||||
const handleIterationsSave = useCallback(() => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
const value = Number.parseInt(inputValue)
|
||||
|
||||
if (!Number.isNaN(value)) {
|
||||
@@ -136,11 +159,13 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
setInputValue(iterations.toString())
|
||||
}
|
||||
setConfigPopoverOpen(false)
|
||||
}, [inputValue, iterations, nodeId, updateParallelCount])
|
||||
}, [inputValue, iterations, nodeId, updateParallelCount, isPreview])
|
||||
|
||||
// Handle editor change and check for tag trigger
|
||||
const handleEditorChange = useCallback(
|
||||
(value: string) => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
setEditorValue(value)
|
||||
updateParallelCollection(nodeId, value)
|
||||
|
||||
@@ -156,12 +181,14 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
setShowTagDropdown(tagTrigger.show)
|
||||
}
|
||||
},
|
||||
[nodeId, updateParallelCollection]
|
||||
[nodeId, updateParallelCollection, isPreview]
|
||||
)
|
||||
|
||||
// Handle tag selection
|
||||
const handleTagSelect = useCallback(
|
||||
(newValue: string) => {
|
||||
if (isPreview) return // Don't allow changes in preview mode
|
||||
|
||||
setEditorValue(newValue)
|
||||
updateParallelCollection(nodeId, newValue)
|
||||
setShowTagDropdown(false)
|
||||
@@ -174,7 +201,7 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
[nodeId, updateParallelCollection]
|
||||
[nodeId, updateParallelCollection, isPreview]
|
||||
)
|
||||
|
||||
// Handle key events
|
||||
@@ -187,141 +214,153 @@ export function ParallelBadges({ nodeId, data }: ParallelBadgesProps) {
|
||||
return (
|
||||
<div className='-top-9 absolute right-0 left-0 z-10 flex justify-between'>
|
||||
{/* Parallel Type Badge */}
|
||||
<Popover open={typePopoverOpen} onOpenChange={setTypePopoverOpen}>
|
||||
<Popover
|
||||
open={!isPreview && typePopoverOpen}
|
||||
onOpenChange={isPreview ? undefined : setTypePopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'border-border bg-background/80 py-0.5 pr-1.5 pl-2.5 font-medium text-foreground text-sm backdrop-blur-sm',
|
||||
'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
!isPreview && 'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
'flex items-center gap-1'
|
||||
)}
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
>
|
||||
{parallelType === 'count' ? 'Parallel Count' : 'Parallel Each'}
|
||||
<ChevronDown className='h-3 w-3 text-muted-foreground' />
|
||||
{!isPreview && <ChevronDown className='h-3 w-3 text-muted-foreground' />}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-48 p-3' align='center' onClick={(e) => e.stopPropagation()}>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Parallel Type</div>
|
||||
<div className='space-y-1'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
parallelType === 'count' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleParallelTypeChange('count')}
|
||||
>
|
||||
<span className='text-sm'>Parallel Count</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
parallelType === 'collection' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleParallelTypeChange('collection')}
|
||||
>
|
||||
<span className='text-sm'>Parallel Each</span>
|
||||
{!isPreview && (
|
||||
<PopoverContent className='w-48 p-3' align='center' onClick={(e) => e.stopPropagation()}>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Parallel Type</div>
|
||||
<div className='space-y-1'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
parallelType === 'count' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleParallelTypeChange('count')}
|
||||
>
|
||||
<span className='text-sm'>Parallel Count</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5',
|
||||
parallelType === 'collection' ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => handleParallelTypeChange('collection')}
|
||||
>
|
||||
<span className='text-sm'>Parallel Each</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{/* Iterations/Collection Badge */}
|
||||
<Popover open={configPopoverOpen} onOpenChange={setConfigPopoverOpen}>
|
||||
<Popover
|
||||
open={!isPreview && configPopoverOpen}
|
||||
onOpenChange={isPreview ? undefined : setConfigPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'border-border bg-background/80 py-0.5 pr-1.5 pl-2.5 font-medium text-foreground text-sm backdrop-blur-sm',
|
||||
'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
!isPreview && 'cursor-pointer transition-colors duration-150 hover:bg-accent/50',
|
||||
'flex items-center gap-1'
|
||||
)}
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
>
|
||||
{parallelType === 'count' ? `Iterations: ${iterations}` : 'Items'}
|
||||
<ChevronDown className='h-3 w-3 text-muted-foreground' />
|
||||
{!isPreview && <ChevronDown className='h-3 w-3 text-muted-foreground' />}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={cn('p-3', parallelType !== 'count' ? 'w-72' : 'w-48')}
|
||||
align='center'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>
|
||||
{parallelType === 'count' ? 'Parallel Iterations' : 'Parallel Items'}
|
||||
</div>
|
||||
|
||||
{parallelType === 'count' ? (
|
||||
// Number input for count-based parallel
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
type='text'
|
||||
value={inputValue}
|
||||
onChange={handleIterationsChange}
|
||||
onBlur={handleIterationsSave}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleIterationsSave()}
|
||||
className='h-8 text-sm'
|
||||
autoFocus
|
||||
/>
|
||||
{!isPreview && (
|
||||
<PopoverContent
|
||||
className={cn('p-3', parallelType !== 'count' ? 'w-72' : 'w-48')}
|
||||
align='center'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>
|
||||
{parallelType === 'count' ? 'Parallel Iterations' : 'Parallel Items'}
|
||||
</div>
|
||||
) : (
|
||||
// Code editor for collection-based parallel
|
||||
<div className='relative'>
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
className='relative min-h-[80px] rounded-md border border-input bg-background px-3 pt-2 pb-3 font-mono text-sm'
|
||||
>
|
||||
{editorValue === '' && (
|
||||
<div className='pointer-events-none absolute top-[8.5px] left-3 select-none text-muted-foreground/50'>
|
||||
['item1', 'item2', 'item3']
|
||||
</div>
|
||||
|
||||
{parallelType === 'count' ? (
|
||||
// Number input for count-based parallel
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
type='text'
|
||||
value={inputValue}
|
||||
onChange={handleIterationsChange}
|
||||
onBlur={handleIterationsSave}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleIterationsSave()}
|
||||
className='h-8 text-sm'
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Code editor for collection-based parallel
|
||||
<div className='relative'>
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
className='relative min-h-[80px] rounded-md border border-input bg-background px-3 pt-2 pb-3 font-mono text-sm'
|
||||
>
|
||||
{editorValue === '' && (
|
||||
<div className='pointer-events-none absolute top-[8.5px] left-3 select-none text-muted-foreground/50'>
|
||||
['item1', 'item2', 'item3']
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
value={editorValue}
|
||||
onValueChange={handleEditorChange}
|
||||
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
|
||||
padding={0}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: '21px',
|
||||
}}
|
||||
className='w-full focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full overflow-hidden whitespace-pre-wrap'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowTagDropdown(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2 text-[10px] text-muted-foreground'>
|
||||
Array or object to use for parallel execution. Type "{'<'}" to reference other
|
||||
blocks.
|
||||
</div>
|
||||
{showTagDropdown && (
|
||||
<TagDropdown
|
||||
visible={showTagDropdown}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={nodeId}
|
||||
activeSourceBlockId={null}
|
||||
inputValue={editorValue}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => setShowTagDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
<Editor
|
||||
value={editorValue}
|
||||
onValueChange={handleEditorChange}
|
||||
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
|
||||
padding={0}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: '21px',
|
||||
}}
|
||||
className='w-full focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full overflow-hidden whitespace-pre-wrap'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowTagDropdown(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2 text-[10px] text-muted-foreground'>
|
||||
Array or object to use for parallel execution. Type "{'<'}" to reference other
|
||||
blocks.
|
||||
</div>
|
||||
{showTagDropdown && (
|
||||
<TagDropdown
|
||||
visible={showTagDropdown}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={nodeId}
|
||||
activeSourceBlockId={null}
|
||||
inputValue={editorValue}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => setShowTagDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{parallelType === 'count' && (
|
||||
<div className='text-[10px] text-muted-foreground'>
|
||||
Enter a number between 1 and 20
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
{parallelType === 'count' && (
|
||||
<div className='text-[10px] text-muted-foreground'>
|
||||
Enter a number between 1 and 20
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -88,6 +88,9 @@ export const ParallelNodeComponent = memo(({ data, selected, id }: NodeProps) =>
|
||||
const { getNodes } = useReactFlow()
|
||||
const blockRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Check if this is preview mode
|
||||
const isPreview = data?.isPreview || false
|
||||
|
||||
// Determine nesting level by counting parents
|
||||
const nestingLevel = useMemo(() => {
|
||||
const maxDepth = 100 // Prevent infinite loops
|
||||
@@ -108,7 +111,7 @@ export const ParallelNodeComponent = memo(({ data, selected, id }: NodeProps) =>
|
||||
const getNestedStyles = () => {
|
||||
// Base styles
|
||||
const styles: Record<string, string> = {
|
||||
backgroundColor: data?.state === 'valid' ? 'rgba(139, 195, 74, 0.05)' : 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
|
||||
// Apply nested styles
|
||||
@@ -135,7 +138,7 @@ export const ParallelNodeComponent = memo(({ data, selected, id }: NodeProps) =>
|
||||
'relative cursor-default select-none',
|
||||
'transition-block-bg transition-ring',
|
||||
'z-[20]',
|
||||
data?.state === 'valid' && 'bg-[rgba(139,195,74,0.05)] ring-2 ring-[#8BC34A]',
|
||||
data?.state === 'valid',
|
||||
nestingLevel > 0 &&
|
||||
`border border-[0.5px] ${nestingLevel % 2 === 0 ? 'border-slate-300/60' : 'border-slate-400/60'}`
|
||||
)}
|
||||
@@ -145,23 +148,27 @@ export const ParallelNodeComponent = memo(({ data, selected, id }: NodeProps) =>
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
...nestedStyles,
|
||||
pointerEvents: 'all',
|
||||
pointerEvents: isPreview ? 'none' : 'all',
|
||||
}}
|
||||
data-node-id={id}
|
||||
data-type='parallelNode'
|
||||
data-nesting-level={nestingLevel}
|
||||
>
|
||||
{/* Critical drag handle that controls only the parallel node movement */}
|
||||
<div
|
||||
className='workflow-drag-handle absolute top-0 right-0 left-0 z-10 h-10 cursor-move'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
{!isPreview && (
|
||||
<div
|
||||
className='workflow-drag-handle absolute top-0 right-0 left-0 z-10 h-10 cursor-move'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Custom visible resize handle */}
|
||||
<div
|
||||
className='absolute right-2 bottom-2 z-20 flex h-8 w-8 cursor-se-resize items-center justify-center text-muted-foreground'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
{!isPreview && (
|
||||
<div
|
||||
className='absolute right-2 bottom-2 z-20 flex h-8 w-8 cursor-se-resize items-center justify-center text-muted-foreground'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Child nodes container - Set pointerEvents to allow dragging of children */}
|
||||
<div
|
||||
@@ -170,27 +177,29 @@ export const ParallelNodeComponent = memo(({ data, selected, id }: NodeProps) =>
|
||||
style={{
|
||||
position: 'relative',
|
||||
minHeight: '100%',
|
||||
pointerEvents: 'auto',
|
||||
pointerEvents: isPreview ? 'none' : 'auto',
|
||||
}}
|
||||
>
|
||||
{/* Delete button - styled like in action-bar.tsx */}
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
useWorkflowStore.getState().removeBlock(id)
|
||||
}}
|
||||
className='absolute top-2 right-2 z-20 text-gray-500 opacity-0 transition-opacity duration-200 hover:text-red-600 group-hover:opacity-100'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
{!isPreview && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
useWorkflowStore.getState().removeBlock(id)
|
||||
}}
|
||||
className='absolute top-2 right-2 z-20 text-gray-500 opacity-0 transition-opacity duration-200 hover:text-red-600 group-hover:opacity-100'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Parallel Start Block */}
|
||||
<div
|
||||
className='-translate-y-1/2 absolute top-1/2 left-8 flex h-10 w-10 transform items-center justify-center rounded-md bg-[#8BC34A] p-2'
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
className='-translate-y-1/2 absolute top-1/2 left-8 flex h-10 w-10 transform items-center justify-center rounded-md bg-[#FEE12B] p-2'
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
data-parent-id={id}
|
||||
data-node-role='parallel-start'
|
||||
data-extent='parent'
|
||||
|
||||
@@ -15,7 +15,8 @@ import 'reactflow/dist/style.css'
|
||||
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { LoopTool } from '@/app/w/[id]/components/loop-node/loop-config'
|
||||
import { LoopNodeComponent } from '@/app/w/[id]/components/loop-node/loop-node'
|
||||
import { ParallelNodeComponent } from '@/app/w/[id]/components/parallel-node/parallel-node'
|
||||
import { WorkflowBlock } from '@/app/w/[id]/components/workflow-block/workflow-block'
|
||||
import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edge'
|
||||
import { getBlock } from '@/blocks'
|
||||
@@ -38,11 +39,11 @@ interface WorkflowPreviewProps {
|
||||
defaultZoom?: number
|
||||
}
|
||||
|
||||
// Define node types - using the actual workflow components
|
||||
// Define node types - the components now handle preview mode internally
|
||||
const nodeTypes: NodeTypes = {
|
||||
workflowBlock: WorkflowBlock,
|
||||
// loopLabel: LoopLabel,
|
||||
// loopInput: LoopInput,
|
||||
loopNode: LoopNodeComponent,
|
||||
parallelNode: ParallelNodeComponent,
|
||||
}
|
||||
|
||||
// Define edge types
|
||||
@@ -92,25 +93,37 @@ export function WorkflowPreview({
|
||||
[workflowState.edges]
|
||||
)
|
||||
|
||||
// Helper function to calculate absolute position for child blocks
|
||||
const calculateAbsolutePosition = (
|
||||
block: any,
|
||||
blocks: Record<string, any>
|
||||
): { x: number; y: number } => {
|
||||
// If no parent, use the block's position as-is
|
||||
if (!block.data?.parentId) {
|
||||
return block.position
|
||||
}
|
||||
|
||||
// Find the parent block
|
||||
const parentBlock = blocks[block.data.parentId]
|
||||
if (!parentBlock) {
|
||||
logger.warn(`Parent block not found for child block: ${block.id}`)
|
||||
return block.position
|
||||
}
|
||||
|
||||
// Recursively calculate parent's absolute position (for nested containers)
|
||||
const parentAbsolutePosition = calculateAbsolutePosition(parentBlock, blocks)
|
||||
|
||||
// Add parent's absolute position to child's relative position
|
||||
return {
|
||||
x: parentAbsolutePosition.x + block.position.x,
|
||||
y: parentAbsolutePosition.y + block.position.y,
|
||||
}
|
||||
}
|
||||
|
||||
// Transform blocks and loops into ReactFlow nodes
|
||||
const nodes: Node[] = useMemo(() => {
|
||||
const nodeArray: Node[] = []
|
||||
|
||||
// First, get all blocks with parent-child relationships
|
||||
const blocksWithParents: Record<string, any> = {}
|
||||
const topLevelBlocks: Record<string, any> = {}
|
||||
|
||||
// Categorize blocks as top-level or child blocks
|
||||
Object.entries(workflowState.blocks).forEach(([blockId, block]) => {
|
||||
if (block.data?.parentId) {
|
||||
// This is a child block
|
||||
blocksWithParents[blockId] = block
|
||||
} else {
|
||||
// This is a top-level block
|
||||
topLevelBlocks[blockId] = block
|
||||
}
|
||||
})
|
||||
|
||||
// Add block nodes using the same approach as workflow.tsx
|
||||
Object.entries(workflowState.blocks).forEach(([blockId, block]) => {
|
||||
if (!block || !block.type) {
|
||||
@@ -118,6 +131,49 @@ export function WorkflowPreview({
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate absolute position for proper preview positioning
|
||||
const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks)
|
||||
|
||||
// Handle container nodes (loop and parallel) differently
|
||||
if (block.type === 'loop') {
|
||||
nodeArray.push({
|
||||
id: block.id,
|
||||
type: 'loopNode',
|
||||
position: absolutePosition,
|
||||
parentId: block.data?.parentId,
|
||||
extent: block.data?.extent || undefined,
|
||||
draggable: false,
|
||||
data: {
|
||||
...block.data,
|
||||
width: block.data?.width || 500,
|
||||
height: block.data?.height || 300,
|
||||
state: 'valid',
|
||||
isPreview: true,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (block.type === 'parallel') {
|
||||
nodeArray.push({
|
||||
id: block.id,
|
||||
type: 'parallelNode',
|
||||
position: absolutePosition,
|
||||
parentId: block.data?.parentId,
|
||||
extent: block.data?.extent || undefined,
|
||||
draggable: false,
|
||||
data: {
|
||||
...block.data,
|
||||
width: block.data?.width || 500,
|
||||
height: block.data?.height || 300,
|
||||
state: 'valid',
|
||||
isPreview: true,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle regular blocks
|
||||
const blockConfig = getBlock(block.type)
|
||||
if (!blockConfig) {
|
||||
logger.error(`No configuration found for block type: ${block.type}`, { blockId })
|
||||
@@ -130,23 +186,23 @@ export function WorkflowPreview({
|
||||
nodeArray.push({
|
||||
id: blockId,
|
||||
type: 'workflowBlock',
|
||||
position: block.position,
|
||||
position: absolutePosition,
|
||||
draggable: false,
|
||||
data: {
|
||||
type: block.type,
|
||||
config: blockConfig || (block.type === 'loop' ? LoopTool : null),
|
||||
config: blockConfig,
|
||||
name: block.name,
|
||||
blockState: block,
|
||||
isReadOnly: true, // Set read-only mode for preview
|
||||
isPreview: true, // Indicate this is a preview
|
||||
subBlockValues: subBlocksClone, // Use the deep clone to avoid reference issues
|
||||
isReadOnly: true,
|
||||
isPreview: true,
|
||||
subBlockValues: subBlocksClone,
|
||||
},
|
||||
})
|
||||
|
||||
// Add children of this block if it's a loop
|
||||
// Add children of this block if it's a loop (for nested blocks)
|
||||
if (block.type === 'loop') {
|
||||
// Find all children of this loop
|
||||
const childBlocks = Object.entries(blocksWithParents).filter(
|
||||
const childBlocks = Object.entries(workflowState.blocks).filter(
|
||||
([_, childBlock]) => childBlock.data?.parentId === blockId
|
||||
)
|
||||
|
||||
@@ -154,39 +210,35 @@ export function WorkflowPreview({
|
||||
childBlocks.forEach(([childId, childBlock]) => {
|
||||
const childConfig = getBlock(childBlock.type)
|
||||
|
||||
nodeArray.push({
|
||||
id: childId,
|
||||
type: 'workflowBlock',
|
||||
// Position child blocks relative to the parent
|
||||
position: {
|
||||
x: block.position.x + 50, // Offset children to the right
|
||||
y: block.position.y + (childBlock.position?.y || 100), // Preserve vertical positioning
|
||||
},
|
||||
data: {
|
||||
type: childBlock.type,
|
||||
config: childConfig,
|
||||
name: childBlock.name,
|
||||
blockState: childBlock,
|
||||
showSubBlocks,
|
||||
isChild: true,
|
||||
parentId: blockId,
|
||||
},
|
||||
draggable: false,
|
||||
})
|
||||
if (childConfig) {
|
||||
nodeArray.push({
|
||||
id: childId,
|
||||
type: 'workflowBlock',
|
||||
// Position child blocks relative to the parent
|
||||
position: {
|
||||
x: block.position.x + 50, // Offset children to the right
|
||||
y: block.position.y + (childBlock.position?.y || 100), // Preserve vertical positioning
|
||||
},
|
||||
data: {
|
||||
type: childBlock.type,
|
||||
config: childConfig,
|
||||
name: childBlock.name,
|
||||
blockState: childBlock,
|
||||
showSubBlocks,
|
||||
isChild: true,
|
||||
parentId: blockId,
|
||||
isReadOnly: true,
|
||||
isPreview: true,
|
||||
},
|
||||
draggable: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return nodeArray
|
||||
}, [
|
||||
blocksStructure,
|
||||
loopsStructure,
|
||||
parallelsStructure,
|
||||
showSubBlocks,
|
||||
workflowState.blocks,
|
||||
workflowState.loops,
|
||||
workflowState.parallels,
|
||||
])
|
||||
}, [blocksStructure, loopsStructure, parallelsStructure, showSubBlocks, workflowState.blocks])
|
||||
|
||||
// Transform edges
|
||||
const edges: Edge[] = useMemo(() => {
|
||||
@@ -198,7 +250,7 @@ export function WorkflowPreview({
|
||||
targetHandle: edge.targetHandle,
|
||||
type: 'workflowEdge',
|
||||
}))
|
||||
}, [edgesStructure, workflowState.edges, workflowState.parallels])
|
||||
}, [edgesStructure, workflowState.edges])
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
|
||||
Reference in New Issue
Block a user