11import { useEffect , useRef , useState } from 'react'
22import { Plus } from 'lucide-react'
33import { useParams } from 'next/navigation'
4- import { Badge , Button , Combobox , Input , Label , Textarea } from '@/components/emcn'
4+ import {
5+ Badge ,
6+ Button ,
7+ Combobox ,
8+ type ComboboxOption ,
9+ Input ,
10+ Label ,
11+ Textarea ,
12+ } from '@/components/emcn'
513import { Trash } from '@/components/emcn/icons/trash'
614import { cn } from '@/lib/core/utils/cn'
715import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
@@ -38,6 +46,14 @@ const DEFAULT_ASSIGNMENT: Omit<VariableAssignment, 'id'> = {
3846 isExisting : false ,
3947}
4048
49+ /**
50+ * Boolean value options for Combobox
51+ */
52+ const BOOLEAN_OPTIONS : ComboboxOption [ ] = [
53+ { label : 'true' , value : 'true' } ,
54+ { label : 'false' , value : 'false' } ,
55+ ]
56+
4157/**
4258 * Parses a value that might be a JSON string or already an array of VariableAssignment.
4359 * This handles the case where workflows are imported with stringified values.
@@ -104,8 +120,6 @@ export function VariablesInput({
104120 const allVariablesAssigned =
105121 ! hasNoWorkflowVariables && getAvailableVariablesFor ( 'new' ) . length === 0
106122
107- // Initialize with one empty assignment if none exist and not in preview/disabled mode
108- // Also add assignment when first variable is created
109123 useEffect ( ( ) => {
110124 if ( ! isReadOnly && assignments . length === 0 && currentWorkflowVariables . length > 0 ) {
111125 const initialAssignment : VariableAssignment = {
@@ -116,45 +130,46 @@ export function VariablesInput({
116130 }
117131 } , [ currentWorkflowVariables . length , isReadOnly , assignments . length , setStoreValue ] )
118132
119- // Clean up assignments when their associated variables are deleted
120133 useEffect ( ( ) => {
121134 if ( isReadOnly || assignments . length === 0 ) return
122135
123136 const currentVariableIds = new Set ( currentWorkflowVariables . map ( ( v ) => v . id ) )
124137 const validAssignments = assignments . filter ( ( assignment ) => {
125- // Keep assignments that haven't selected a variable yet
126138 if ( ! assignment . variableId ) return true
127- // Keep assignments whose variable still exists
128139 return currentVariableIds . has ( assignment . variableId )
129140 } )
130141
131- // If all variables were deleted, clear all assignments
132142 if ( currentWorkflowVariables . length === 0 ) {
133143 setStoreValue ( [ ] )
134144 } else if ( validAssignments . length !== assignments . length ) {
135- // Some assignments reference deleted variables, remove them
136145 setStoreValue ( validAssignments . length > 0 ? validAssignments : [ ] )
137146 }
138147 } , [ currentWorkflowVariables , assignments , isReadOnly , setStoreValue ] )
139148
140149 const addAssignment = ( ) => {
141- if ( isPreview || disabled || allVariablesAssigned ) return
150+ if ( isReadOnly || allVariablesAssigned ) return
142151
143152 const newAssignment : VariableAssignment = {
144153 ...DEFAULT_ASSIGNMENT ,
145154 id : crypto . randomUUID ( ) ,
146155 }
147- setStoreValue ( [ ...( assignments || [ ] ) , newAssignment ] )
156+ setStoreValue ( [ ...assignments , newAssignment ] )
148157 }
149158
150159 const removeAssignment = ( id : string ) => {
151- if ( isPreview || disabled ) return
152- setStoreValue ( ( assignments || [ ] ) . filter ( ( a ) => a . id !== id ) )
160+ if ( isReadOnly ) return
161+
162+ if ( assignments . length === 1 ) {
163+ setStoreValue ( [ { ...DEFAULT_ASSIGNMENT , id : crypto . randomUUID ( ) } ] )
164+ return
165+ }
166+
167+ setStoreValue ( assignments . filter ( ( a ) => a . id !== id ) )
153168 }
154169
155170 const updateAssignment = ( id : string , updates : Partial < VariableAssignment > ) => {
156- if ( isPreview || disabled ) return
157- setStoreValue ( ( assignments || [ ] ) . map ( ( a ) => ( a . id === id ? { ...a , ...updates } : a ) ) )
171+ if ( isReadOnly ) return
172+ setStoreValue ( assignments . map ( ( a ) => ( a . id === id ? { ...a , ...updates } : a ) ) )
158173 }
159174
160175 const handleVariableSelect = ( assignmentId : string , variableId : string ) => {
@@ -169,28 +184,17 @@ export function VariablesInput({
169184 }
170185 }
171186
172- const handleTagSelect = ( tag : string ) => {
187+ const handleTagSelect = ( newValue : string ) => {
173188 if ( ! activeFieldId ) return
174189
175- const assignment = assignments . find ( ( a ) => a . id === activeFieldId )
176- if ( ! assignment ) return
177-
178- const currentValue = assignment . value || ''
179-
180- const textBeforeCursor = currentValue . slice ( 0 , cursorPosition )
181- const lastOpenBracket = textBeforeCursor . lastIndexOf ( '<' )
182-
183- const newValue =
184- currentValue . slice ( 0 , lastOpenBracket ) + tag + currentValue . slice ( cursorPosition )
185-
186190 updateAssignment ( activeFieldId , { value : newValue } )
187191 setShowTags ( false )
188192
189193 setTimeout ( ( ) => {
190194 const inputEl = valueInputRefs . current [ activeFieldId ]
191195 if ( inputEl ) {
192196 inputEl . focus ( )
193- const newCursorPos = lastOpenBracket + tag . length
197+ const newCursorPos = newValue . length
194198 inputEl . setSelectionRange ( newCursorPos , newCursorPos )
195199 }
196200 } , 10 )
@@ -272,6 +276,18 @@ export function VariablesInput({
272276 } ) )
273277 }
274278
279+ const syncOverlayScroll = ( assignmentId : string , scrollLeft : number ) => {
280+ const overlay = overlayRefs . current [ assignmentId ]
281+ if ( overlay ) overlay . scrollLeft = scrollLeft
282+ }
283+
284+ const handleKeyDown = ( e : React . KeyboardEvent < HTMLInputElement | HTMLTextAreaElement > ) => {
285+ if ( e . key === 'Escape' ) {
286+ setShowTags ( false )
287+ setActiveSourceBlockId ( null )
288+ }
289+ }
290+
275291 if ( isPreview && ( ! assignments || assignments . length === 0 ) ) {
276292 return (
277293 < div className = 'flex flex-col items-center justify-center rounded-md border border-border/40 bg-muted/20 py-8 text-center' >
@@ -302,7 +318,7 @@ export function VariablesInput({
302318
303319 return (
304320 < div className = 'space-y-[8px]' >
305- { assignments && assignments . length > 0 && (
321+ { assignments . length > 0 && (
306322 < div className = 'space-y-[8px]' >
307323 { assignments . map ( ( assignment , index ) => {
308324 const collapsed = collapsedAssignments [ assignment . id ] || false
@@ -334,7 +350,7 @@ export function VariablesInput({
334350 < Button
335351 variant = 'ghost'
336352 onClick = { addAssignment }
337- disabled = { isPreview || disabled || allVariablesAssigned }
353+ disabled = { isReadOnly || allVariablesAssigned }
338354 className = 'h-auto p-0'
339355 >
340356 < Plus className = 'h-[14px] w-[14px]' />
@@ -343,7 +359,7 @@ export function VariablesInput({
343359 < Button
344360 variant = 'ghost'
345361 onClick = { ( ) => removeAssignment ( assignment . id ) }
346- disabled = { isPreview || disabled || assignments . length === 1 }
362+ disabled = { isReadOnly }
347363 className = 'h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
348364 >
349365 < Trash className = 'h-[14px] w-[14px]' />
@@ -358,16 +374,26 @@ export function VariablesInput({
358374 < Label className = 'text-[13px]' > Variable</ Label >
359375 < Combobox
360376 options = { availableVars . map ( ( v ) => ( { label : v . name , value : v . id } ) ) }
361- value = { assignment . variableId || assignment . variableName || '' }
377+ value = { assignment . variableId || '' }
362378 onChange = { ( value ) => handleVariableSelect ( assignment . id , value ) }
363379 placeholder = 'Select a variable...'
364- disabled = { isPreview || disabled }
380+ disabled = { isReadOnly }
365381 />
366382 </ div >
367383
368384 < div className = 'flex flex-col gap-[6px]' >
369385 < Label className = 'text-[13px]' > Value</ Label >
370- { assignment . type === 'object' || assignment . type === 'array' ? (
386+ { assignment . type === 'boolean' ? (
387+ < Combobox
388+ options = { BOOLEAN_OPTIONS }
389+ value = { assignment . value ?? '' }
390+ onChange = { ( v ) =>
391+ ! isReadOnly && updateAssignment ( assignment . id , { value : v } )
392+ }
393+ placeholder = 'Select value'
394+ disabled = { isReadOnly }
395+ />
396+ ) : assignment . type === 'object' || assignment . type === 'array' ? (
371397 < div className = 'relative' >
372398 < Textarea
373399 ref = { ( el ) => {
@@ -381,26 +407,32 @@ export function VariablesInput({
381407 e . target . selectionStart ?? undefined
382408 )
383409 }
410+ onKeyDown = { handleKeyDown }
384411 onFocus = { ( ) => {
385- if ( ! isPreview && ! disabled && ! assignment . value ?. trim ( ) ) {
412+ if ( ! isReadOnly && ! assignment . value ?. trim ( ) ) {
386413 setActiveFieldId ( assignment . id )
387414 setCursorPosition ( 0 )
388415 setShowTags ( true )
389416 }
390417 } }
418+ onScroll = { ( e ) => {
419+ const overlay = overlayRefs . current [ assignment . id ]
420+ if ( overlay ) {
421+ overlay . scrollTop = e . currentTarget . scrollTop
422+ overlay . scrollLeft = e . currentTarget . scrollLeft
423+ }
424+ } }
391425 placeholder = {
392426 assignment . type === 'object'
393427 ? '{\n "key": "value"\n}'
394428 : '[\n 1, 2, 3\n]'
395429 }
396- disabled = { isPreview || disabled }
430+ disabled = { isReadOnly }
397431 className = { cn (
398432 'min-h-[120px] font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50' ,
399433 dragHighlight [ assignment . id ] && 'ring-2 ring-blue-500 ring-offset-2'
400434 ) }
401435 style = { {
402- fontFamily : 'inherit' ,
403- lineHeight : 'inherit' ,
404436 wordBreak : 'break-word' ,
405437 whiteSpace : 'pre-wrap' ,
406438 } }
@@ -413,10 +445,7 @@ export function VariablesInput({
413445 if ( el ) overlayRefs . current [ assignment . id ] = el
414446 } }
415447 className = 'pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 font-mono text-sm'
416- style = { {
417- fontFamily : 'inherit' ,
418- lineHeight : 'inherit' ,
419- } }
448+ style = { { scrollbarWidth : 'none' } }
420449 >
421450 < div className = 'w-full whitespace-pre-wrap break-words' >
422451 { formatDisplayText ( assignment . value || '' , {
@@ -441,21 +470,34 @@ export function VariablesInput({
441470 e . target . selectionStart ?? undefined
442471 )
443472 }
473+ onKeyDown = { handleKeyDown }
444474 onFocus = { ( ) => {
445- if ( ! isPreview && ! disabled && ! assignment . value ?. trim ( ) ) {
475+ if ( ! isReadOnly && ! assignment . value ?. trim ( ) ) {
446476 setActiveFieldId ( assignment . id )
447477 setCursorPosition ( 0 )
448478 setShowTags ( true )
449479 }
450480 } }
481+ onScroll = { ( e ) =>
482+ syncOverlayScroll ( assignment . id , e . currentTarget . scrollLeft )
483+ }
484+ onPaste = { ( ) =>
485+ setTimeout ( ( ) => {
486+ const input = valueInputRefs . current [ assignment . id ]
487+ if ( input )
488+ syncOverlayScroll (
489+ assignment . id ,
490+ ( input as HTMLInputElement ) . scrollLeft
491+ )
492+ } , 0 )
493+ }
451494 placeholder = { `${ assignment . type } value` }
452- disabled = { isPreview || disabled }
495+ disabled = { isReadOnly }
453496 autoComplete = 'off'
454497 className = { cn (
455- 'allow-scroll w-full overflow-auto text-transparent caret-foreground' ,
498+ 'allow-scroll w-full overflow-x- auto overflow-y-hidden text-transparent caret-foreground' ,
456499 dragHighlight [ assignment . id ] && 'ring-2 ring-blue-500 ring-offset-2'
457500 ) }
458- style = { { overflowX : 'auto' } }
459501 onDrop = { ( e ) => handleDrop ( e , assignment . id ) }
460502 onDragOver = { ( e ) => handleDragOver ( e , assignment . id ) }
461503 onDragLeave = { ( e ) => handleDragLeave ( e , assignment . id ) }
@@ -465,7 +507,7 @@ export function VariablesInput({
465507 if ( el ) overlayRefs . current [ assignment . id ] = el
466508 } }
467509 className = 'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm'
468- style = { { overflowX : 'auto ' } }
510+ style = { { scrollbarWidth : 'none ' } }
469511 >
470512 < div
471513 className = 'w-full whitespace-pre'
0 commit comments