mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
fix(variables): added the same input vars mapping from workflow block, added type validation to variables block, updated UI (#1761)
* fix(variables): added the same input vars mapping from workflow block, added type validation to variables block, updated UI * cleanup * lint
This commit is contained in:
@@ -6,7 +6,6 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface InputFormatField {
|
||||
name: string
|
||||
@@ -57,24 +56,20 @@ interface InputMappingProps {
|
||||
isPreview?: boolean
|
||||
previewValue?: any
|
||||
disabled?: boolean
|
||||
isConnecting?: boolean
|
||||
}
|
||||
|
||||
// Simple mapping UI: for each field in child Input Trigger's inputFormat, render an input with TagDropdown support
|
||||
export function InputMapping({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
disabled = false,
|
||||
isConnecting = false,
|
||||
}: InputMappingProps) {
|
||||
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
|
||||
const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
||||
|
||||
const { workflows } = useWorkflowRegistry.getState()
|
||||
|
||||
// Fetch child workflow state via registry API endpoint, using cached metadata when possible
|
||||
// Here we rely on live store; the serializer/executor will resolve at runtime too.
|
||||
// We only need the inputFormat from an Input Trigger in the selected child workflow state.
|
||||
const [childInputFields, setChildInputFields] = useState<Array<{ name: string; type?: string }>>(
|
||||
[]
|
||||
)
|
||||
@@ -97,7 +92,6 @@ export function InputMapping({
|
||||
}
|
||||
const { data } = await res.json()
|
||||
const blocks = (data?.state?.blocks as Record<string, unknown>) || {}
|
||||
// Prefer new input_trigger
|
||||
const triggerEntry = Object.entries(blocks).find(([, b]) => isInputTriggerBlock(b))
|
||||
if (triggerEntry && isInputTriggerBlock(triggerEntry[1])) {
|
||||
const inputFormat = triggerEntry[1].subBlocks?.inputFormat?.value
|
||||
@@ -110,7 +104,6 @@ export function InputMapping({
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: legacy starter block inputFormat (subBlocks or config.params)
|
||||
const starterEntry = Object.entries(blocks).find(([, b]) => isStarterBlock(b))
|
||||
if (starterEntry && isStarterBlock(starterEntry[1])) {
|
||||
const starter = starterEntry[1]
|
||||
@@ -217,6 +210,7 @@ export function InputMapping({
|
||||
subBlockId={subBlockId}
|
||||
disabled={isPreview || disabled}
|
||||
accessiblePrefixes={accessiblePrefixes}
|
||||
isConnecting={isConnecting}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -224,7 +218,6 @@ export function InputMapping({
|
||||
)
|
||||
}
|
||||
|
||||
// Individual field component with TagDropdown support
|
||||
function InputMappingField({
|
||||
fieldName,
|
||||
fieldType,
|
||||
@@ -234,6 +227,7 @@ function InputMappingField({
|
||||
subBlockId,
|
||||
disabled,
|
||||
accessiblePrefixes,
|
||||
isConnecting,
|
||||
}: {
|
||||
fieldName: string
|
||||
fieldType?: string
|
||||
@@ -243,9 +237,12 @@ function InputMappingField({
|
||||
subBlockId: string
|
||||
disabled: boolean
|
||||
accessiblePrefixes: Set<string> | undefined
|
||||
isConnecting?: boolean
|
||||
}) {
|
||||
const [showTags, setShowTags] = useState(false)
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
|
||||
const [dragHighlight, setDragHighlight] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -261,12 +258,10 @@ function InputMappingField({
|
||||
onChange(newValue)
|
||||
setCursorPosition(newCursorPosition)
|
||||
|
||||
// Check for tag trigger
|
||||
const tagTrigger = checkTagTrigger(newValue, newCursorPosition)
|
||||
setShowTags(tagTrigger.show)
|
||||
}
|
||||
|
||||
// Sync scroll position between input and overlay
|
||||
const handleScroll = (e: React.UIEvent<HTMLInputElement>) => {
|
||||
if (overlayRef.current) {
|
||||
overlayRef.current.scrollLeft = e.currentTarget.scrollLeft
|
||||
@@ -281,6 +276,47 @@ function InputMappingField({
|
||||
|
||||
const handleTagSelect = (newValue: string) => {
|
||||
onChange(newValue)
|
||||
setShowTags(false)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragHighlight(false)
|
||||
const input = inputRef.current
|
||||
input?.focus()
|
||||
|
||||
if (input) {
|
||||
const dropPosition = input.selectionStart ?? value.length
|
||||
const newValue = `${value.slice(0, dropPosition)}<${value.slice(dropPosition)}`
|
||||
onChange(newValue)
|
||||
setCursorPosition(dropPosition + 1)
|
||||
setShowTags(true)
|
||||
|
||||
try {
|
||||
const data = JSON.parse(e.dataTransfer.getData('application/json'))
|
||||
if (data?.connectionData?.sourceBlockId) {
|
||||
setActiveSourceBlockId(data.connectionData.sourceBlockId)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
setTimeout(() => {
|
||||
if (input && typeof input.selectionStart === 'number') {
|
||||
input.selectionStart = dropPosition + 1
|
||||
input.selectionEnd = dropPosition + 1
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
setDragHighlight(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragHighlight(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -298,7 +334,9 @@ function InputMappingField({
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
'allow-scroll h-9 w-full overflow-auto text-transparent caret-foreground placeholder:text-muted-foreground/50',
|
||||
'border border-input bg-white transition-colors duration-200 dark:border-input/60 dark:bg-background'
|
||||
'border border-input bg-white transition-colors duration-200 dark:border-input/60 dark:bg-background',
|
||||
dragHighlight && 'ring-2 ring-blue-500 ring-offset-2',
|
||||
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
|
||||
)}
|
||||
type='text'
|
||||
value={value}
|
||||
@@ -311,6 +349,9 @@ function InputMappingField({
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
onKeyDown={handleKeyDown}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
autoComplete='off'
|
||||
style={{ overflowX: 'auto' }}
|
||||
disabled={disabled}
|
||||
@@ -335,11 +376,12 @@ function InputMappingField({
|
||||
visible={showTags}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={blockId}
|
||||
activeSourceBlockId={null}
|
||||
activeSourceBlockId={activeSourceBlockId}
|
||||
inputValue={value}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowTags(false)
|
||||
setActiveSourceBlockId(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { Plus, Trash } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -177,19 +176,39 @@ export function VariablesInput({
|
||||
const handleDrop = (e: React.DragEvent, assignmentId: string) => {
|
||||
e.preventDefault()
|
||||
setDragHighlight((prev) => ({ ...prev, [assignmentId]: false }))
|
||||
const input = valueInputRefs.current[assignmentId]
|
||||
input?.focus()
|
||||
|
||||
const tag = e.dataTransfer.getData('text/plain')
|
||||
if (tag?.startsWith('<')) {
|
||||
if (input) {
|
||||
const assignment = assignments.find((a) => a.id === assignmentId)
|
||||
if (!assignment) return
|
||||
const currentValue = assignment?.value || ''
|
||||
const dropPosition = (input as any).selectionStart ?? currentValue.length
|
||||
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
|
||||
updateAssignment(assignmentId, { value: newValue })
|
||||
setActiveFieldId(assignmentId)
|
||||
setCursorPosition(dropPosition + 1)
|
||||
setShowTags(true)
|
||||
|
||||
const currentValue = assignment.value || ''
|
||||
updateAssignment(assignmentId, { value: currentValue + tag })
|
||||
try {
|
||||
const data = JSON.parse(e.dataTransfer.getData('application/json'))
|
||||
if (data?.connectionData?.sourceBlockId) {
|
||||
setActiveSourceBlockId(data.connectionData.sourceBlockId)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
setTimeout(() => {
|
||||
const el = valueInputRefs.current[assignmentId]
|
||||
if (el && typeof (el as any).selectionStart === 'number') {
|
||||
;(el as any).selectionStart = dropPosition + 1
|
||||
;(el as any).selectionEnd = dropPosition + 1
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, assignmentId: string) => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
setDragHighlight((prev) => ({ ...prev, [assignmentId]: true }))
|
||||
}
|
||||
|
||||
@@ -200,8 +219,48 @@ export function VariablesInput({
|
||||
|
||||
if (isPreview && (!assignments || assignments.length === 0)) {
|
||||
return (
|
||||
<div className='flex items-center justify-center rounded-md border border-border/40 border-dashed bg-muted/20 p-4 text-center text-muted-foreground text-sm'>
|
||||
No variable assignments defined
|
||||
<div className='flex flex-col items-center justify-center rounded-md border border-border/40 border-dashed bg-muted/20 py-8 text-center'>
|
||||
<svg
|
||||
className='mb-3 h-10 w-10 text-muted-foreground/40'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={1.5}
|
||||
d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
|
||||
/>
|
||||
</svg>
|
||||
<p className='mb-1 font-medium text-foreground text-sm'>No variable assignments defined</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Add variables in the Variables panel to get started
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isPreview && hasNoWorkflowVariables && assignments.length === 0) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center rounded-lg border border-border/50 bg-muted/30 p-8 text-center'>
|
||||
<svg
|
||||
className='mb-3 h-10 w-10 text-muted-foreground/60'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={1.5}
|
||||
d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
|
||||
/>
|
||||
</svg>
|
||||
<p className='font-medium text-muted-foreground text-sm'>No variables found</p>
|
||||
<p className='mt-1 text-muted-foreground/80 text-xs'>
|
||||
Add variables in the Variables panel to get started
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -214,22 +273,29 @@ export function VariablesInput({
|
||||
return (
|
||||
<div
|
||||
key={assignment.id}
|
||||
className='group relative rounded-lg border border-border/60 bg-background p-3 transition-all hover:border-border'
|
||||
className='group relative rounded-lg border border-border/50 bg-background/50 p-3 transition-all hover:border-border hover:bg-background'
|
||||
>
|
||||
{!isPreview && !disabled && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='absolute top-2 right-2 h-6 w-6 opacity-0 group-hover:opacity-100'
|
||||
className='absolute top-2 right-2 h-6 w-6 opacity-0 transition-opacity group-hover:opacity-100'
|
||||
onClick={() => removeAssignment(assignment.id)}
|
||||
>
|
||||
<Trash className='h-3.5 w-3.5' />
|
||||
<Trash className='h-3.5 w-3.5 text-muted-foreground hover:text-destructive' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-muted-foreground text-xs'>Variable</Label>
|
||||
<div className='flex items-center justify-between pr-8'>
|
||||
<Label className='text-xs'>Variable</Label>
|
||||
{assignment.variableName && (
|
||||
<span className='rounded-md bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground'>
|
||||
{assignment.type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
value={assignment.variableId || assignment.variableName || ''}
|
||||
onValueChange={(value) => {
|
||||
@@ -240,7 +306,7 @@ export function VariablesInput({
|
||||
}}
|
||||
disabled={isPreview || disabled}
|
||||
>
|
||||
<SelectTrigger className='h-9 bg-white dark:bg-background'>
|
||||
<SelectTrigger className='h-9 border border-input bg-white dark:border-input/60 dark:bg-background'>
|
||||
<SelectValue placeholder='Select a variable...' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -249,12 +315,7 @@ export function VariablesInput({
|
||||
return availableVars.length > 0 ? (
|
||||
availableVars.map((variable) => (
|
||||
<SelectItem key={variable.id} value={variable.id}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>{variable.name}</span>
|
||||
<Badge variant='outline' className='text-[10px]'>
|
||||
{variable.type}
|
||||
</Badge>
|
||||
</div>
|
||||
{variable.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
@@ -276,16 +337,7 @@ export function VariablesInput({
|
||||
</div>
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-muted-foreground text-xs'>Type</Label>
|
||||
<Input
|
||||
value={assignment.type || 'string'}
|
||||
disabled={true}
|
||||
className='h-9 bg-muted/50 text-muted-foreground'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='relative space-y-1.5'>
|
||||
<Label className='text-muted-foreground text-xs'>Value</Label>
|
||||
<Label className='text-xs'>Value</Label>
|
||||
{assignment.type === 'object' || assignment.type === 'array' ? (
|
||||
<Textarea
|
||||
ref={(el) => {
|
||||
@@ -306,7 +358,7 @@ export function VariablesInput({
|
||||
}
|
||||
disabled={isPreview || disabled}
|
||||
className={cn(
|
||||
'min-h-[120px] border border-input bg-white font-mono text-sm dark:bg-background',
|
||||
'min-h-[120px] border border-input bg-white font-mono text-sm placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
|
||||
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2',
|
||||
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
|
||||
)}
|
||||
@@ -331,7 +383,7 @@ export function VariablesInput({
|
||||
placeholder={`${assignment.type} value`}
|
||||
disabled={isPreview || disabled}
|
||||
className={cn(
|
||||
'h-9 bg-white text-transparent caret-foreground dark:bg-background',
|
||||
'h-9 border border-input bg-white text-transparent caret-foreground placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
|
||||
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2',
|
||||
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
|
||||
)}
|
||||
@@ -375,20 +427,15 @@ export function VariablesInput({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isPreview && !disabled && (
|
||||
{!isPreview && !disabled && !hasNoWorkflowVariables && (
|
||||
<Button
|
||||
onClick={addAssignment}
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='w-full border-dashed'
|
||||
disabled={hasNoWorkflowVariables || allVariablesAssigned}
|
||||
className='h-9 w-full border-dashed'
|
||||
disabled={allVariablesAssigned}
|
||||
>
|
||||
{!hasNoWorkflowVariables && <Plus className='mr-2 h-4 w-4' />}
|
||||
{hasNoWorkflowVariables
|
||||
? 'No variables found'
|
||||
: allVariablesAssigned
|
||||
? 'All Variables Assigned'
|
||||
: 'Add Variable Assignment'}
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
{allVariablesAssigned ? 'All Variables Assigned' : 'Add Variable Assignment'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -471,6 +471,7 @@ export const SubBlock = memo(
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
disabled={isDisabled}
|
||||
isConnecting={isConnecting}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ export const VariablesBlock: BlockConfig = {
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
assignments: {
|
||||
type: 'json',
|
||||
description: 'JSON object mapping variable names to their assigned values',
|
||||
},
|
||||
// Dynamic outputs - each assigned variable will be available as a top-level output
|
||||
// For example, if you assign variable1=5, you can reference it as <variables_block.variable1>
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const WaitBlock: BlockConfig = {
|
||||
title: 'Wait Amount',
|
||||
type: 'short-input',
|
||||
layout: 'half',
|
||||
description: 'How long to wait. Max: 600 seconds or 10 minutes',
|
||||
description: 'Max: 600 seconds or 10 minutes',
|
||||
placeholder: '10',
|
||||
value: () => '10',
|
||||
required: true,
|
||||
|
||||
@@ -419,6 +419,21 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const outputPaths = generateOutputPaths(blockConfig.outputs)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
} else if (sourceBlock.type === 'variables') {
|
||||
// For variables block, show assigned variable names as outputs
|
||||
const variablesValue = getSubBlockValue(activeSourceBlockId, 'variables')
|
||||
|
||||
if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) {
|
||||
const validAssignments = variablesValue.filter((assignment: { variableName?: string }) =>
|
||||
assignment?.variableName?.trim()
|
||||
)
|
||||
blockTags = validAssignments.map(
|
||||
(assignment: { variableName: string }) =>
|
||||
`${normalizedBlockName}.${assignment.variableName.trim()}`
|
||||
)
|
||||
} else {
|
||||
blockTags = [normalizedBlockName]
|
||||
}
|
||||
} else if (responseFormat) {
|
||||
const schemaFields = extractFieldsFromSchema(responseFormat)
|
||||
if (schemaFields.length > 0) {
|
||||
@@ -765,6 +780,21 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const outputPaths = generateOutputPaths(blockConfig.outputs)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
} else if (accessibleBlock.type === 'variables') {
|
||||
// For variables block, show assigned variable names as outputs
|
||||
const variablesValue = getSubBlockValue(accessibleBlockId, 'variables')
|
||||
|
||||
if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) {
|
||||
const validAssignments = variablesValue.filter((assignment: { variableName?: string }) =>
|
||||
assignment?.variableName?.trim()
|
||||
)
|
||||
blockTags = validAssignments.map(
|
||||
(assignment: { variableName: string }) =>
|
||||
`${normalizedBlockName}.${assignment.variableName.trim()}`
|
||||
)
|
||||
} else {
|
||||
blockTags = [normalizedBlockName]
|
||||
}
|
||||
} else if (responseFormat) {
|
||||
const schemaFields = extractFieldsFromSchema(responseFormat)
|
||||
if (schemaFields.length > 0) {
|
||||
|
||||
@@ -29,17 +29,13 @@ export class VariablesBlockHandler implements BlockHandler {
|
||||
})
|
||||
|
||||
try {
|
||||
// Initialize workflowVariables if not present
|
||||
if (!context.workflowVariables) {
|
||||
context.workflowVariables = {}
|
||||
}
|
||||
|
||||
// Parse variable assignments from the custom input
|
||||
const assignments = this.parseAssignments(inputs.variables)
|
||||
|
||||
// Update context.workflowVariables with new values
|
||||
for (const assignment of assignments) {
|
||||
// Find the variable by ID or name
|
||||
const existingEntry = assignment.variableId
|
||||
? [assignment.variableId, context.workflowVariables[assignment.variableId]]
|
||||
: Object.entries(context.workflowVariables).find(
|
||||
@@ -47,7 +43,6 @@ export class VariablesBlockHandler implements BlockHandler {
|
||||
)
|
||||
|
||||
if (existingEntry?.[1]) {
|
||||
// Update existing variable value
|
||||
const [id, variable] = existingEntry
|
||||
context.workflowVariables[id] = {
|
||||
...variable,
|
||||
@@ -68,15 +63,12 @@ export class VariablesBlockHandler implements BlockHandler {
|
||||
})),
|
||||
})
|
||||
|
||||
// Return assignments as a JSON object mapping variable names to values
|
||||
const assignmentsOutput: Record<string, any> = {}
|
||||
const output: Record<string, any> = {}
|
||||
for (const assignment of assignments) {
|
||||
assignmentsOutput[assignment.variableName] = assignment.value
|
||||
output[assignment.variableName] = assignment.value
|
||||
}
|
||||
|
||||
return {
|
||||
assignments: assignmentsOutput,
|
||||
}
|
||||
return output
|
||||
} catch (error: any) {
|
||||
logger.error('Variables block execution failed:', error)
|
||||
throw new Error(`Variables block execution failed: ${error.message}`)
|
||||
@@ -97,7 +89,7 @@ export class VariablesBlockHandler implements BlockHandler {
|
||||
if (assignment?.variableName?.trim()) {
|
||||
const name = assignment.variableName.trim()
|
||||
const type = assignment.type || 'string'
|
||||
const value = this.parseValueByType(assignment.value, type)
|
||||
const value = this.parseValueByType(assignment.value, type, name)
|
||||
|
||||
result.push({
|
||||
variableId: assignment.variableId,
|
||||
@@ -111,9 +103,8 @@ export class VariablesBlockHandler implements BlockHandler {
|
||||
return result
|
||||
}
|
||||
|
||||
private parseValueByType(value: any, type: string): any {
|
||||
// Handle null/undefined early
|
||||
if (value === null || value === undefined) {
|
||||
private parseValueByType(value: any, type: string, variableName?: string): any {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
if (type === 'number') return 0
|
||||
if (type === 'boolean') return false
|
||||
if (type === 'array') return []
|
||||
@@ -121,7 +112,6 @@ export class VariablesBlockHandler implements BlockHandler {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Handle plain and string types (plain is for backward compatibility)
|
||||
if (type === 'string' || type === 'plain') {
|
||||
return typeof value === 'string' ? value : String(value)
|
||||
}
|
||||
@@ -129,35 +119,71 @@ export class VariablesBlockHandler implements BlockHandler {
|
||||
if (type === 'number') {
|
||||
if (typeof value === 'number') return value
|
||||
if (typeof value === 'string') {
|
||||
const num = Number(value)
|
||||
return Number.isNaN(num) ? 0 : num
|
||||
const trimmed = value.trim()
|
||||
if (trimmed === '') return 0
|
||||
const num = Number(trimmed)
|
||||
if (Number.isNaN(num)) {
|
||||
throw new Error(
|
||||
`Invalid number value for variable "${variableName || 'unknown'}": "${value}". Expected a valid number.`
|
||||
)
|
||||
}
|
||||
return num
|
||||
}
|
||||
return 0
|
||||
throw new Error(
|
||||
`Invalid type for variable "${variableName || 'unknown'}": expected number, got ${typeof value}`
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase() === 'true'
|
||||
const lower = value.toLowerCase().trim()
|
||||
if (lower === 'true') return true
|
||||
if (lower === 'false') return false
|
||||
throw new Error(
|
||||
`Invalid boolean value for variable "${variableName || 'unknown'}": "${value}". Expected "true" or "false".`
|
||||
)
|
||||
}
|
||||
return Boolean(value)
|
||||
}
|
||||
|
||||
if (type === 'object' || type === 'array') {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (type === 'array' && !Array.isArray(value)) {
|
||||
throw new Error(
|
||||
`Invalid array value for variable "${variableName || 'unknown'}": expected an array, got an object`
|
||||
)
|
||||
}
|
||||
if (type === 'object' && Array.isArray(value)) {
|
||||
throw new Error(
|
||||
`Invalid object value for variable "${variableName || 'unknown'}": expected an object, got an array`
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return type === 'array' ? [] : {}
|
||||
const parsed = JSON.parse(value)
|
||||
if (type === 'array' && !Array.isArray(parsed)) {
|
||||
throw new Error(
|
||||
`Invalid array value for variable "${variableName || 'unknown'}": parsed value is not an array`
|
||||
)
|
||||
}
|
||||
if (type === 'object' && (Array.isArray(parsed) || typeof parsed !== 'object')) {
|
||||
throw new Error(
|
||||
`Invalid object value for variable "${variableName || 'unknown'}": parsed value is not an object`
|
||||
)
|
||||
}
|
||||
return parsed
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Invalid JSON for variable "${variableName || 'unknown'}": ${error.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
return type === 'array' ? [] : {}
|
||||
}
|
||||
|
||||
// Default: return value as-is
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user