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
This commit is contained in:
Vikhyath Mondreti
2025-08-20 18:03:47 -07:00
committed by GitHub
parent 26e6286fda
commit 9ffaf305bd
4 changed files with 122 additions and 138 deletions

View File

@@ -1,4 +1,4 @@
import { useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { ChevronDown, Plus, Trash } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -8,10 +8,16 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { formatDisplayText } from '@/components/ui/formatted-text'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
@@ -64,22 +70,26 @@ export function FieldFormat({
config,
}: FieldFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
const [tagDropdownStates, setTagDropdownStates] = useState<
Record<
string,
{
visible: boolean
cursorPosition: number
}
>
>({})
const [dragHighlight, setDragHighlight] = useState<Record<string, boolean>>({})
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
const [localValues, setLocalValues] = useState<Record<string, string>>({})
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const fields: Field[] = value || []
useEffect(() => {
const initial: Record<string, string> = {}
;(fields || []).forEach((f) => {
if (localValues[f.id] === undefined) {
initial[f.id] = (f.value as string) || ''
}
})
if (Object.keys(initial).length > 0) {
setLocalValues((prev) => ({ ...prev, ...initial }))
}
}, [fields])
// Field operations
const addField = () => {
if (isPreview || disabled) return
@@ -88,12 +98,12 @@ export function FieldFormat({
...DEFAULT_FIELD,
id: crypto.randomUUID(),
}
setStoreValue([...fields, newField])
setStoreValue([...(fields || []), newField])
}
const removeField = (id: string) => {
if (isPreview || disabled) return
setStoreValue(fields.filter((field: Field) => field.id !== id))
setStoreValue((fields || []).filter((field: Field) => field.id !== id))
}
// Validate field name for API safety
@@ -103,38 +113,22 @@ export function FieldFormat({
return name.replace(/[\x00-\x1F"\\]/g, '').trim()
}
// Tag dropdown handlers
const handleValueInputChange = (fieldId: string, newValue: string) => {
const input = valueInputRefs.current[fieldId]
if (!input) return
const cursorPosition = input.selectionStart || 0
const shouldShow = checkTagTrigger(newValue, cursorPosition)
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: {
visible: shouldShow.show,
cursorPosition,
},
}))
updateField(fieldId, 'value', newValue)
setLocalValues((prev) => ({ ...prev, [fieldId]: newValue }))
}
const handleTagSelect = (fieldId: string, newValue: string) => {
updateField(fieldId, 'value', newValue)
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: { ...prev[fieldId], visible: false },
}))
}
// Value normalization: keep it simple for string types
const handleTagDropdownClose = (fieldId: string) => {
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: { ...prev[fieldId], visible: false },
}))
const handleValueInputBlur = (field: Field) => {
if (isPreview || disabled) return
const inputEl = valueInputRefs.current[field.id]
if (!inputEl) return
const current = localValues[field.id] ?? inputEl.value ?? ''
const trimmed = current.trim()
if (!trimmed) return
updateField(field.id, 'value', current)
}
// Drag and drop handlers for connection blocks
@@ -152,47 +146,8 @@ export function FieldFormat({
const handleDrop = (e: React.DragEvent, fieldId: string) => {
e.preventDefault()
setDragHighlight((prev) => ({ ...prev, [fieldId]: false }))
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'))
if (data.type === 'connectionBlock' && data.connectionData) {
const input = valueInputRefs.current[fieldId]
if (!input) return
// Focus the input first
input.focus()
// Get current cursor position or use end of field
const dropPosition = input.selectionStart ?? (input.value?.length || 0)
// Insert '<' at drop position to trigger the dropdown
const currentValue = input.value || ''
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
// Update the field value
updateField(fieldId, 'value', newValue)
// Set cursor position and show dropdown
setTimeout(() => {
input.selectionStart = dropPosition + 1
input.selectionEnd = dropPosition + 1
// Trigger dropdown by simulating the tag check
const cursorPosition = dropPosition + 1
const shouldShow = checkTagTrigger(newValue, cursorPosition)
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: {
visible: shouldShow.show,
cursorPosition,
},
}))
}, 0)
}
} catch (error) {
console.error('Error handling drop:', error)
}
const input = valueInputRefs.current[fieldId]
input?.focus()
}
// Update handlers
@@ -204,12 +159,14 @@ export function FieldFormat({
value = validateFieldName(value)
}
setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, [field]: value } : f)))
setStoreValue((fields || []).map((f: Field) => (f.id === id ? { ...f, [field]: value } : f)))
}
const toggleCollapse = (id: string) => {
if (isPreview || disabled) return
setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)))
setStoreValue(
(fields || []).map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))
)
}
// Field header
@@ -371,54 +328,66 @@ export function FieldFormat({
<div className='space-y-1.5'>
<Label className='text-xs'>Value</Label>
<div className='relative'>
<Input
ref={(el) => {
if (el) valueInputRefs.current[field.id] = el
}}
name='value'
value={field.value || ''}
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
handleTagDropdownClose(field.id)
{field.type === 'boolean' ? (
<Select
value={localValues[field.id] ?? (field.value as string) ?? ''}
onValueChange={(v) => {
setLocalValues((prev) => ({ ...prev, [field.id]: v }))
if (!isPreview && !disabled) updateField(field.id, 'value', v)
}}
>
<SelectTrigger className='h-9 w-full justify-between font-normal'>
<SelectValue placeholder='Select value' className='truncate' />
</SelectTrigger>
<SelectContent>
<SelectItem value='true'>true</SelectItem>
<SelectItem value='false'>false</SelectItem>
</SelectContent>
</Select>
) : field.type === 'object' || field.type === 'array' ? (
<Textarea
ref={(el) => {
if (el) valueInputRefs.current[field.id] = el
}}
name='value'
value={localValues[field.id] ?? (field.value as string) ?? ''}
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
onBlur={() => handleValueInputBlur(field)}
placeholder={
field.type === 'object' ? '{\n "key": "value"\n}' : '[\n 1, 2, 3\n]'
}
}}
onDragOver={(e) => handleDragOver(e, field.id)}
onDragLeave={(e) => handleDragLeave(e, field.id)}
onDrop={(e) => handleDrop(e, field.id)}
placeholder={valuePlaceholder}
disabled={isPreview || disabled}
className={cn(
'h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50',
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
isConnecting &&
config?.connectionDroppable !== false &&
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
)}
/>
{field.value && (
<div className='pointer-events-none absolute inset-0 flex items-center px-3 py-2'>
<div className='w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm'>
{formatDisplayText(field.value, true)}
</div>
</div>
disabled={isPreview || disabled}
className={cn(
'min-h-[120px] font-mono text-sm placeholder:text-muted-foreground/50',
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
isConnecting &&
config?.connectionDroppable !== false &&
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
)}
/>
) : (
<Input
ref={(el) => {
if (el) valueInputRefs.current[field.id] = el
}}
name='value'
value={localValues[field.id] ?? field.value ?? ''}
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
onBlur={() => handleValueInputBlur(field)}
onDragOver={(e) => handleDragOver(e, field.id)}
onDragLeave={(e) => handleDragLeave(e, field.id)}
onDrop={(e) => handleDrop(e, field.id)}
placeholder={valuePlaceholder}
disabled={isPreview || disabled}
className={cn(
'h-9 placeholder:text-muted-foreground/50',
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
isConnecting &&
config?.connectionDroppable !== false &&
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
)}
/>
)}
<TagDropdown
visible={tagDropdownStates[field.id]?.visible || false}
onSelect={(newValue) => handleTagSelect(field.id, newValue)}
blockId={blockId}
activeSourceBlockId={null}
inputValue={field.value || ''}
cursorPosition={tagDropdownStates[field.id]?.cursorPosition || 0}
onClose={() => handleTagDropdownClose(field.id)}
style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
zIndex: 9999,
}}
/>
</div>
</div>
)}

View File

@@ -435,6 +435,7 @@ export function SubBlock({
disabled={isDisabled}
isConnecting={isConnecting}
config={config}
showValue={true}
/>
)
}

View File

@@ -28,6 +28,8 @@ export const StarterBlock: BlockConfig = {
title: 'Input Format (for API calls)',
type: 'input-format',
layout: 'full',
description:
'Name and Type define your input schema. Value is used only for manual test runs.',
mode: 'advanced',
condition: { field: 'startWorkflow', value: 'manual' },
},

View File

@@ -771,7 +771,7 @@ export class Executor {
// Get the field value from workflow input if available
// First try to access via input.field, then directly from field
// This handles both input formats: { input: { field: value } } and { field: value }
const inputValue =
let inputValue =
this.workflowInput?.input?.[field.name] !== undefined
? this.workflowInput.input[field.name] // Try to get from input.field
: this.workflowInput?.[field.name] // Fallback to direct field access
@@ -781,13 +781,25 @@ export class Executor {
inputValue !== undefined ? JSON.stringify(inputValue) : 'undefined'
)
// Convert the value to the appropriate type
if (inputValue === undefined || inputValue === null) {
if (Object.hasOwn(field, 'value')) {
inputValue = (field as any).value
}
}
let typedValue = inputValue
if (inputValue !== undefined) {
if (field.type === 'number' && typeof inputValue !== 'number') {
typedValue = Number(inputValue)
if (inputValue !== undefined && inputValue !== null) {
if (field.type === 'string' && typeof inputValue !== 'string') {
typedValue = String(inputValue)
} else if (field.type === 'number' && typeof inputValue !== 'number') {
const num = Number(inputValue)
typedValue = Number.isNaN(num) ? inputValue : num
} else if (field.type === 'boolean' && typeof inputValue !== 'boolean') {
typedValue = inputValue === 'true' || inputValue === true
typedValue =
inputValue === 'true' ||
inputValue === true ||
inputValue === 1 ||
inputValue === '1'
} else if (
(field.type === 'object' || field.type === 'array') &&
typeof inputValue === 'string'