Skip to content

Commit 9ffaf30

Browse files
feat(input-format): add value field to test input formats (#1059)
* feat(input-format): add value field to test input formats * fix lint * fix typing issue * change to dropdown for boolean
1 parent 26e6286 commit 9ffaf30

File tree

4 files changed

+121
-137
lines changed

4 files changed

+121
-137
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx

Lines changed: 100 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef, useState } from 'react'
1+
import { useEffect, useRef, useState } from 'react'
22
import { ChevronDown, Plus, Trash } from 'lucide-react'
33
import { Badge } from '@/components/ui/badge'
44
import { Button } from '@/components/ui/button'
@@ -8,10 +8,16 @@ import {
88
DropdownMenuItem,
99
DropdownMenuTrigger,
1010
} from '@/components/ui/dropdown-menu'
11-
import { formatDisplayText } from '@/components/ui/formatted-text'
1211
import { Input } from '@/components/ui/input'
1312
import { Label } from '@/components/ui/label'
14-
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
13+
import {
14+
Select,
15+
SelectContent,
16+
SelectItem,
17+
SelectTrigger,
18+
SelectValue,
19+
} from '@/components/ui/select'
20+
import { Textarea } from '@/components/ui/textarea'
1521
import { cn } from '@/lib/utils'
1622
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
1723

@@ -64,22 +70,26 @@ export function FieldFormat({
6470
config,
6571
}: FieldFormatProps) {
6672
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
67-
const [tagDropdownStates, setTagDropdownStates] = useState<
68-
Record<
69-
string,
70-
{
71-
visible: boolean
72-
cursorPosition: number
73-
}
74-
>
75-
>({})
7673
const [dragHighlight, setDragHighlight] = useState<Record<string, boolean>>({})
77-
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
74+
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
75+
const [localValues, setLocalValues] = useState<Record<string, string>>({})
7876

7977
// Use preview value when in preview mode, otherwise use store value
8078
const value = isPreview ? previewValue : storeValue
8179
const fields: Field[] = value || []
8280

81+
useEffect(() => {
82+
const initial: Record<string, string> = {}
83+
;(fields || []).forEach((f) => {
84+
if (localValues[f.id] === undefined) {
85+
initial[f.id] = (f.value as string) || ''
86+
}
87+
})
88+
if (Object.keys(initial).length > 0) {
89+
setLocalValues((prev) => ({ ...prev, ...initial }))
90+
}
91+
}, [fields])
92+
8393
// Field operations
8494
const addField = () => {
8595
if (isPreview || disabled) return
@@ -88,12 +98,12 @@ export function FieldFormat({
8898
...DEFAULT_FIELD,
8999
id: crypto.randomUUID(),
90100
}
91-
setStoreValue([...fields, newField])
101+
setStoreValue([...(fields || []), newField])
92102
}
93103

94104
const removeField = (id: string) => {
95105
if (isPreview || disabled) return
96-
setStoreValue(fields.filter((field: Field) => field.id !== id))
106+
setStoreValue((fields || []).filter((field: Field) => field.id !== id))
97107
}
98108

99109
// Validate field name for API safety
@@ -103,38 +113,22 @@ export function FieldFormat({
103113
return name.replace(/[\x00-\x1F"\\]/g, '').trim()
104114
}
105115

106-
// Tag dropdown handlers
107116
const handleValueInputChange = (fieldId: string, newValue: string) => {
108-
const input = valueInputRefs.current[fieldId]
109-
if (!input) return
110-
111-
const cursorPosition = input.selectionStart || 0
112-
const shouldShow = checkTagTrigger(newValue, cursorPosition)
117+
setLocalValues((prev) => ({ ...prev, [fieldId]: newValue }))
118+
}
113119

114-
setTagDropdownStates((prev) => ({
115-
...prev,
116-
[fieldId]: {
117-
visible: shouldShow.show,
118-
cursorPosition,
119-
},
120-
}))
120+
// Value normalization: keep it simple for string types
121121

122-
updateField(fieldId, 'value', newValue)
123-
}
122+
const handleValueInputBlur = (field: Field) => {
123+
if (isPreview || disabled) return
124124

125-
const handleTagSelect = (fieldId: string, newValue: string) => {
126-
updateField(fieldId, 'value', newValue)
127-
setTagDropdownStates((prev) => ({
128-
...prev,
129-
[fieldId]: { ...prev[fieldId], visible: false },
130-
}))
131-
}
125+
const inputEl = valueInputRefs.current[field.id]
126+
if (!inputEl) return
132127

133-
const handleTagDropdownClose = (fieldId: string) => {
134-
setTagDropdownStates((prev) => ({
135-
...prev,
136-
[fieldId]: { ...prev[fieldId], visible: false },
137-
}))
128+
const current = localValues[field.id] ?? inputEl.value ?? ''
129+
const trimmed = current.trim()
130+
if (!trimmed) return
131+
updateField(field.id, 'value', current)
138132
}
139133

140134
// Drag and drop handlers for connection blocks
@@ -152,47 +146,8 @@ export function FieldFormat({
152146
const handleDrop = (e: React.DragEvent, fieldId: string) => {
153147
e.preventDefault()
154148
setDragHighlight((prev) => ({ ...prev, [fieldId]: false }))
155-
156-
try {
157-
const data = JSON.parse(e.dataTransfer.getData('application/json'))
158-
if (data.type === 'connectionBlock' && data.connectionData) {
159-
const input = valueInputRefs.current[fieldId]
160-
if (!input) return
161-
162-
// Focus the input first
163-
input.focus()
164-
165-
// Get current cursor position or use end of field
166-
const dropPosition = input.selectionStart ?? (input.value?.length || 0)
167-
168-
// Insert '<' at drop position to trigger the dropdown
169-
const currentValue = input.value || ''
170-
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
171-
172-
// Update the field value
173-
updateField(fieldId, 'value', newValue)
174-
175-
// Set cursor position and show dropdown
176-
setTimeout(() => {
177-
input.selectionStart = dropPosition + 1
178-
input.selectionEnd = dropPosition + 1
179-
180-
// Trigger dropdown by simulating the tag check
181-
const cursorPosition = dropPosition + 1
182-
const shouldShow = checkTagTrigger(newValue, cursorPosition)
183-
184-
setTagDropdownStates((prev) => ({
185-
...prev,
186-
[fieldId]: {
187-
visible: shouldShow.show,
188-
cursorPosition,
189-
},
190-
}))
191-
}, 0)
192-
}
193-
} catch (error) {
194-
console.error('Error handling drop:', error)
195-
}
149+
const input = valueInputRefs.current[fieldId]
150+
input?.focus()
196151
}
197152

198153
// Update handlers
@@ -204,12 +159,14 @@ export function FieldFormat({
204159
value = validateFieldName(value)
205160
}
206161

207-
setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, [field]: value } : f)))
162+
setStoreValue((fields || []).map((f: Field) => (f.id === id ? { ...f, [field]: value } : f)))
208163
}
209164

210165
const toggleCollapse = (id: string) => {
211166
if (isPreview || disabled) return
212-
setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)))
167+
setStoreValue(
168+
(fields || []).map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))
169+
)
213170
}
214171

215172
// Field header
@@ -371,54 +328,66 @@ export function FieldFormat({
371328
<div className='space-y-1.5'>
372329
<Label className='text-xs'>Value</Label>
373330
<div className='relative'>
374-
<Input
375-
ref={(el) => {
376-
if (el) valueInputRefs.current[field.id] = el
377-
}}
378-
name='value'
379-
value={field.value || ''}
380-
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
381-
onKeyDown={(e) => {
382-
if (e.key === 'Escape') {
383-
handleTagDropdownClose(field.id)
331+
{field.type === 'boolean' ? (
332+
<Select
333+
value={localValues[field.id] ?? (field.value as string) ?? ''}
334+
onValueChange={(v) => {
335+
setLocalValues((prev) => ({ ...prev, [field.id]: v }))
336+
if (!isPreview && !disabled) updateField(field.id, 'value', v)
337+
}}
338+
>
339+
<SelectTrigger className='h-9 w-full justify-between font-normal'>
340+
<SelectValue placeholder='Select value' className='truncate' />
341+
</SelectTrigger>
342+
<SelectContent>
343+
<SelectItem value='true'>true</SelectItem>
344+
<SelectItem value='false'>false</SelectItem>
345+
</SelectContent>
346+
</Select>
347+
) : field.type === 'object' || field.type === 'array' ? (
348+
<Textarea
349+
ref={(el) => {
350+
if (el) valueInputRefs.current[field.id] = el
351+
}}
352+
name='value'
353+
value={localValues[field.id] ?? (field.value as string) ?? ''}
354+
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
355+
onBlur={() => handleValueInputBlur(field)}
356+
placeholder={
357+
field.type === 'object' ? '{\n "key": "value"\n}' : '[\n 1, 2, 3\n]'
384358
}
385-
}}
386-
onDragOver={(e) => handleDragOver(e, field.id)}
387-
onDragLeave={(e) => handleDragLeave(e, field.id)}
388-
onDrop={(e) => handleDrop(e, field.id)}
389-
placeholder={valuePlaceholder}
390-
disabled={isPreview || disabled}
391-
className={cn(
392-
'h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50',
393-
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
394-
isConnecting &&
395-
config?.connectionDroppable !== false &&
396-
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
397-
)}
398-
/>
399-
{field.value && (
400-
<div className='pointer-events-none absolute inset-0 flex items-center px-3 py-2'>
401-
<div className='w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm'>
402-
{formatDisplayText(field.value, true)}
403-
</div>
404-
</div>
359+
disabled={isPreview || disabled}
360+
className={cn(
361+
'min-h-[120px] font-mono text-sm placeholder:text-muted-foreground/50',
362+
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
363+
isConnecting &&
364+
config?.connectionDroppable !== false &&
365+
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
366+
)}
367+
/>
368+
) : (
369+
<Input
370+
ref={(el) => {
371+
if (el) valueInputRefs.current[field.id] = el
372+
}}
373+
name='value'
374+
value={localValues[field.id] ?? field.value ?? ''}
375+
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
376+
onBlur={() => handleValueInputBlur(field)}
377+
onDragOver={(e) => handleDragOver(e, field.id)}
378+
onDragLeave={(e) => handleDragLeave(e, field.id)}
379+
onDrop={(e) => handleDrop(e, field.id)}
380+
placeholder={valuePlaceholder}
381+
disabled={isPreview || disabled}
382+
className={cn(
383+
'h-9 placeholder:text-muted-foreground/50',
384+
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
385+
isConnecting &&
386+
config?.connectionDroppable !== false &&
387+
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
388+
)}
389+
/>
405390
)}
406-
<TagDropdown
407-
visible={tagDropdownStates[field.id]?.visible || false}
408-
onSelect={(newValue) => handleTagSelect(field.id, newValue)}
409-
blockId={blockId}
410-
activeSourceBlockId={null}
411-
inputValue={field.value || ''}
412-
cursorPosition={tagDropdownStates[field.id]?.cursorPosition || 0}
413-
onClose={() => handleTagDropdownClose(field.id)}
414-
style={{
415-
position: 'absolute',
416-
top: '100%',
417-
left: 0,
418-
right: 0,
419-
zIndex: 9999,
420-
}}
421-
/>
422391
</div>
423392
</div>
424393
)}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export function SubBlock({
435435
disabled={isDisabled}
436436
isConnecting={isConnecting}
437437
config={config}
438+
showValue={true}
438439
/>
439440
)
440441
}

apps/sim/blocks/blocks/starter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const StarterBlock: BlockConfig = {
2828
title: 'Input Format (for API calls)',
2929
type: 'input-format',
3030
layout: 'full',
31+
description:
32+
'Name and Type define your input schema. Value is used only for manual test runs.',
3133
mode: 'advanced',
3234
condition: { field: 'startWorkflow', value: 'manual' },
3335
},

apps/sim/executor/index.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ export class Executor {
771771
// Get the field value from workflow input if available
772772
// First try to access via input.field, then directly from field
773773
// This handles both input formats: { input: { field: value } } and { field: value }
774-
const inputValue =
774+
let inputValue =
775775
this.workflowInput?.input?.[field.name] !== undefined
776776
? this.workflowInput.input[field.name] // Try to get from input.field
777777
: this.workflowInput?.[field.name] // Fallback to direct field access
@@ -781,13 +781,25 @@ export class Executor {
781781
inputValue !== undefined ? JSON.stringify(inputValue) : 'undefined'
782782
)
783783

784-
// Convert the value to the appropriate type
784+
if (inputValue === undefined || inputValue === null) {
785+
if (Object.hasOwn(field, 'value')) {
786+
inputValue = (field as any).value
787+
}
788+
}
789+
785790
let typedValue = inputValue
786-
if (inputValue !== undefined) {
787-
if (field.type === 'number' && typeof inputValue !== 'number') {
788-
typedValue = Number(inputValue)
791+
if (inputValue !== undefined && inputValue !== null) {
792+
if (field.type === 'string' && typeof inputValue !== 'string') {
793+
typedValue = String(inputValue)
794+
} else if (field.type === 'number' && typeof inputValue !== 'number') {
795+
const num = Number(inputValue)
796+
typedValue = Number.isNaN(num) ? inputValue : num
789797
} else if (field.type === 'boolean' && typeof inputValue !== 'boolean') {
790-
typedValue = inputValue === 'true' || inputValue === true
798+
typedValue =
799+
inputValue === 'true' ||
800+
inputValue === true ||
801+
inputValue === 1 ||
802+
inputValue === '1'
791803
} else if (
792804
(field.type === 'object' || field.type === 'array') &&
793805
typeof inputValue === 'string'

0 commit comments

Comments
 (0)