Skip to content

Commit b5da0ad

Browse files
committed
feat(input-format): add value field to test input formats
1 parent b39bdfd commit b5da0ad

4 files changed

Lines changed: 114 additions & 136 deletions

File tree

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

Lines changed: 93 additions & 130 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,11 @@ import {
88
DropdownMenuItem,
99
DropdownMenuTrigger,
1010
} from '@/components/ui/dropdown-menu'
11-
import { formatDisplayText } from '@/components/ui/formatted-text'
11+
1212
import { Input } from '@/components/ui/input'
1313
import { Label } from '@/components/ui/label'
14-
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
14+
import { Switch } from '@/components/ui/switch'
15+
import { Textarea } from '@/components/ui/textarea'
1516
import { cn } from '@/lib/utils'
1617
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
1718

@@ -64,22 +65,26 @@ export function FieldFormat({
6465
config,
6566
}: FieldFormatProps) {
6667
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-
>({})
7668
const [dragHighlight, setDragHighlight] = useState<Record<string, boolean>>({})
7769
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
70+
const [localValues, setLocalValues] = useState<Record<string, string>>({})
7871

7972
// Use preview value when in preview mode, otherwise use store value
8073
const value = isPreview ? previewValue : storeValue
8174
const fields: Field[] = value || []
8275

76+
useEffect(() => {
77+
const initial: Record<string, string> = {}
78+
;(fields || []).forEach((f) => {
79+
if (localValues[f.id] === undefined) {
80+
initial[f.id] = (f.value as string) || ''
81+
}
82+
})
83+
if (Object.keys(initial).length > 0) {
84+
setLocalValues((prev) => ({ ...prev, ...initial }))
85+
}
86+
}, [fields])
87+
8388
// Field operations
8489
const addField = () => {
8590
if (isPreview || disabled) return
@@ -88,12 +93,12 @@ export function FieldFormat({
8893
...DEFAULT_FIELD,
8994
id: crypto.randomUUID(),
9095
}
91-
setStoreValue([...fields, newField])
96+
setStoreValue([...(fields || []), newField])
9297
}
9398

9499
const removeField = (id: string) => {
95100
if (isPreview || disabled) return
96-
setStoreValue(fields.filter((field: Field) => field.id !== id))
101+
setStoreValue((fields || []).filter((field: Field) => field.id !== id))
97102
}
98103

99104
// Validate field name for API safety
@@ -103,38 +108,22 @@ export function FieldFormat({
103108
return name.replace(/[\x00-\x1F"\\]/g, '').trim()
104109
}
105110

106-
// Tag dropdown handlers
107111
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)
112+
setLocalValues((prev) => ({ ...prev, [fieldId]: newValue }))
113+
}
113114

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

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

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-
}
120+
const inputEl = valueInputRefs.current[field.id]
121+
if (!inputEl) return
132122

133-
const handleTagDropdownClose = (fieldId: string) => {
134-
setTagDropdownStates((prev) => ({
135-
...prev,
136-
[fieldId]: { ...prev[fieldId], visible: false },
137-
}))
123+
const current = localValues[field.id] ?? inputEl.value ?? ''
124+
const trimmed = current.trim()
125+
if (!trimmed) return
126+
updateField(field.id, 'value', current)
138127
}
139128

140129
// Drag and drop handlers for connection blocks
@@ -152,47 +141,8 @@ export function FieldFormat({
152141
const handleDrop = (e: React.DragEvent, fieldId: string) => {
153142
e.preventDefault()
154143
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-
}
144+
const input = valueInputRefs.current[fieldId]
145+
input?.focus()
196146
}
197147

198148
// Update handlers
@@ -204,12 +154,14 @@ export function FieldFormat({
204154
value = validateFieldName(value)
205155
}
206156

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

210160
const toggleCollapse = (id: string) => {
211161
if (isPreview || disabled) return
212-
setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)))
162+
setStoreValue(
163+
(fields || []).map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))
164+
)
213165
}
214166

215167
// Field header
@@ -371,54 +323,65 @@ export function FieldFormat({
371323
<div className='space-y-1.5'>
372324
<Label className='text-xs'>Value</Label>
373325
<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)
384-
}
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>
326+
{field.type === 'boolean' ? (
327+
<div className='flex h-9 items-center'>
328+
<Switch
329+
checked={
330+
(localValues[field.id] ?? String(field.value ?? '')) === 'true'
331+
}
332+
onCheckedChange={(checked) => {
333+
const v = checked ? 'true' : 'false'
334+
setLocalValues((prev) => ({ ...prev, [field.id]: v }))
335+
if (!isPreview && !disabled) updateField(field.id, 'value', v)
336+
}}
337+
disabled={isPreview || disabled}
338+
/>
404339
</div>
340+
) : field.type === 'object' || field.type === 'array' ? (
341+
<Textarea
342+
ref={(el) => {
343+
if (el)
344+
valueInputRefs.current[field.id] = el as unknown as HTMLInputElement
345+
}}
346+
name='value'
347+
value={localValues[field.id] ?? (field.value as string) ?? ''}
348+
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
349+
onBlur={() => handleValueInputBlur(field)}
350+
placeholder={
351+
field.type === 'object' ? '{\n "key": "value"\n}' : '[\n 1, 2, 3\n]'
352+
}
353+
disabled={isPreview || disabled}
354+
className={cn(
355+
'min-h-[120px] font-mono text-sm placeholder:text-muted-foreground/50',
356+
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
357+
isConnecting &&
358+
config?.connectionDroppable !== false &&
359+
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
360+
)}
361+
/>
362+
) : (
363+
<Input
364+
ref={(el) => {
365+
if (el) valueInputRefs.current[field.id] = el
366+
}}
367+
name='value'
368+
value={localValues[field.id] ?? field.value ?? ''}
369+
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
370+
onBlur={() => handleValueInputBlur(field)}
371+
onDragOver={(e) => handleDragOver(e, field.id)}
372+
onDragLeave={(e) => handleDragLeave(e, field.id)}
373+
onDrop={(e) => handleDrop(e, field.id)}
374+
placeholder={valuePlaceholder}
375+
disabled={isPreview || disabled}
376+
className={cn(
377+
'h-9 placeholder:text-muted-foreground/50',
378+
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
379+
isConnecting &&
380+
config?.connectionDroppable !== false &&
381+
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
382+
)}
383+
/>
405384
)}
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-
/>
422385
</div>
423386
</div>
424387
)}

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)