Skip to content

Commit ae670a7

Browse files
authored
fix(start-input): restore tag dropdown in input-format component (#1294)
* update infra and remove railway * fix(input-format): restore tag dropdown in input-format component * Revert "update infra and remove railway" This reverts commit 7ade5fb. * style improvements
1 parent a5c224e commit ae670a7

File tree

1 file changed

+133
-28
lines changed
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter

1 file changed

+133
-28
lines changed

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

Lines changed: 133 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DropdownMenuItem,
99
DropdownMenuTrigger,
1010
} from '@/components/ui/dropdown-menu'
11+
import { formatDisplayText } from '@/components/ui/formatted-text'
1112
import { Input } from '@/components/ui/input'
1213
import { Label } from '@/components/ui/label'
1314
import {
@@ -17,6 +18,7 @@ import {
1718
SelectTrigger,
1819
SelectValue,
1920
} from '@/components/ui/select'
21+
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
2022
import { Textarea } from '@/components/ui/textarea'
2123
import { cn } from '@/lib/utils'
2224
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
@@ -72,7 +74,12 @@ export function FieldFormat({
7274
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
7375
const [dragHighlight, setDragHighlight] = useState<Record<string, boolean>>({})
7476
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
77+
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
7578
const [localValues, setLocalValues] = useState<Record<string, string>>({})
79+
const [showTags, setShowTags] = useState(false)
80+
const [cursorPosition, setCursorPosition] = useState(0)
81+
const [activeFieldId, setActiveFieldId] = useState<string | null>(null)
82+
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
7683

7784
// Use preview value when in preview mode, otherwise use store value
7885
const value = isPreview ? previewValue : storeValue
@@ -106,18 +113,19 @@ export function FieldFormat({
106113
setStoreValue((fields || []).filter((field: Field) => field.id !== id))
107114
}
108115

109-
// Validate field name for API safety
110116
const validateFieldName = (name: string): string => {
111-
// Remove only truly problematic characters for JSON/API usage
112-
// Allow most characters but remove control characters, quotes, and backslashes
113117
return name.replace(/[\x00-\x1F"\\]/g, '').trim()
114118
}
115119

116-
const handleValueInputChange = (fieldId: string, newValue: string) => {
120+
const handleValueInputChange = (fieldId: string, newValue: string, caretPosition?: number) => {
117121
setLocalValues((prev) => ({ ...prev, [fieldId]: newValue }))
118-
}
119122

120-
// Value normalization: keep it simple for string types
123+
const position = typeof caretPosition === 'number' ? caretPosition : newValue.length
124+
setCursorPosition(position)
125+
setActiveFieldId(fieldId)
126+
const trigger = checkTagTrigger(newValue, position)
127+
setShowTags(trigger.show)
128+
}
121129

122130
const handleValueInputBlur = (field: Field) => {
123131
if (isPreview || disabled) return
@@ -148,6 +156,47 @@ export function FieldFormat({
148156
setDragHighlight((prev) => ({ ...prev, [fieldId]: false }))
149157
const input = valueInputRefs.current[fieldId]
150158
input?.focus()
159+
160+
if (input) {
161+
const currentValue =
162+
localValues[fieldId] ?? (fields.find((f) => f.id === fieldId)?.value as string) ?? ''
163+
const dropPosition = (input as any).selectionStart ?? currentValue.length
164+
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
165+
setLocalValues((prev) => ({ ...prev, [fieldId]: newValue }))
166+
setActiveFieldId(fieldId)
167+
setCursorPosition(dropPosition + 1)
168+
setShowTags(true)
169+
170+
try {
171+
const data = JSON.parse(e.dataTransfer.getData('application/json'))
172+
if (data?.connectionData?.sourceBlockId) {
173+
setActiveSourceBlockId(data.connectionData.sourceBlockId)
174+
}
175+
} catch {}
176+
177+
setTimeout(() => {
178+
const el = valueInputRefs.current[fieldId]
179+
if (el && typeof (el as any).selectionStart === 'number') {
180+
;(el as any).selectionStart = dropPosition + 1
181+
;(el as any).selectionEnd = dropPosition + 1
182+
}
183+
}, 0)
184+
}
185+
}
186+
187+
const handleValueScroll = (fieldId: string, e: React.UIEvent<HTMLInputElement>) => {
188+
const overlay = overlayRefs.current[fieldId]
189+
if (overlay) {
190+
overlay.scrollLeft = e.currentTarget.scrollLeft
191+
}
192+
}
193+
194+
const handleValuePaste = (fieldId: string) => {
195+
setTimeout(() => {
196+
const input = valueInputRefs.current[fieldId] as HTMLInputElement | undefined
197+
const overlay = overlayRefs.current[fieldId]
198+
if (input && overlay) overlay.scrollLeft = input.scrollLeft
199+
}, 0)
151200
}
152201

153202
// Update handlers
@@ -351,7 +400,13 @@ export function FieldFormat({
351400
}}
352401
name='value'
353402
value={localValues[field.id] ?? (field.value as string) ?? ''}
354-
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
403+
onChange={(e) =>
404+
handleValueInputChange(
405+
field.id,
406+
e.target.value,
407+
e.target.selectionStart ?? undefined
408+
)
409+
}
355410
onBlur={() => handleValueInputBlur(field)}
356411
placeholder={
357412
field.type === 'object' ? '{\n "key": "value"\n}' : '[\n 1, 2, 3\n]'
@@ -364,30 +419,80 @@ export function FieldFormat({
364419
config?.connectionDroppable !== false &&
365420
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
366421
)}
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)}
379422
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-
)}
423+
onDragOver={(e) =>
424+
handleDragOver(e as unknown as React.DragEvent, field.id)
425+
}
426+
onDragLeave={(e) =>
427+
handleDragLeave(e as unknown as React.DragEvent, field.id)
428+
}
389429
/>
430+
) : (
431+
<>
432+
<Input
433+
ref={(el) => {
434+
if (el) valueInputRefs.current[field.id] = el
435+
}}
436+
name='value'
437+
value={localValues[field.id] ?? field.value ?? ''}
438+
onChange={(e) =>
439+
handleValueInputChange(
440+
field.id,
441+
e.target.value,
442+
e.target.selectionStart ?? undefined
443+
)
444+
}
445+
onBlur={() => handleValueInputBlur(field)}
446+
onDragOver={(e) => handleDragOver(e, field.id)}
447+
onDragLeave={(e) => handleDragLeave(e, field.id)}
448+
onDrop={(e) => handleDrop(e, field.id)}
449+
onScroll={(e) => handleValueScroll(field.id, e)}
450+
onPaste={() => handleValuePaste(field.id)}
451+
placeholder={valuePlaceholder}
452+
disabled={isPreview || disabled}
453+
className={cn(
454+
'allow-scroll h-9 w-full overflow-auto text-transparent caret-foreground placeholder:text-muted-foreground/50',
455+
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
456+
isConnecting &&
457+
config?.connectionDroppable !== false &&
458+
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
459+
)}
460+
style={{ overflowX: 'auto' }}
461+
/>
462+
<div
463+
ref={(el) => {
464+
if (el) overlayRefs.current[field.id] = el
465+
}}
466+
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-3 text-sm'
467+
style={{ overflowX: 'auto' }}
468+
>
469+
<div
470+
className='w-full whitespace-pre'
471+
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
472+
>
473+
{formatDisplayText(
474+
(localValues[field.id] ?? field.value ?? '')?.toString(),
475+
true
476+
)}
477+
</div>
478+
</div>
479+
</>
390480
)}
481+
{/* Tag dropdown for response value field */}
482+
<TagDropdown
483+
visible={showTags && activeFieldId === field.id}
484+
onSelect={(newValue) => {
485+
setLocalValues((prev) => ({ ...prev, [field.id]: newValue }))
486+
if (!isPreview && !disabled) updateField(field.id, 'value', newValue)
487+
setShowTags(false)
488+
setActiveSourceBlockId(null)
489+
}}
490+
blockId={blockId}
491+
activeSourceBlockId={activeSourceBlockId}
492+
inputValue={localValues[field.id] ?? (field.value as string) ?? ''}
493+
cursorPosition={cursorPosition}
494+
onClose={() => setShowTags(false)}
495+
/>
391496
</div>
392497
</div>
393498
)}

0 commit comments

Comments
 (0)