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,19 +184,12 @@ export function VariablesInput({
169184 }
170185 }
171186
172- const handleTagSelect = ( tag : string ) => {
187+ const handleTagSelect = ( newValue : string ) => {
173188 if ( ! activeFieldId ) return
174189
175190 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 )
191+ const originalValue = assignment ?. value || ''
192+ const textAfterCursor = originalValue . slice ( cursorPosition )
185193
186194 updateAssignment ( activeFieldId , { value : newValue } )
187195 setShowTags ( false )
@@ -190,7 +198,7 @@ export function VariablesInput({
190198 const inputEl = valueInputRefs . current [ activeFieldId ]
191199 if ( inputEl ) {
192200 inputEl . focus ( )
193- const newCursorPos = lastOpenBracket + tag . length
201+ const newCursorPos = newValue . length - textAfterCursor . length
194202 inputEl . setSelectionRange ( newCursorPos , newCursorPos )
195203 }
196204 } , 10 )
@@ -272,6 +280,18 @@ export function VariablesInput({
272280 } ) )
273281 }
274282
283+ const syncOverlayScroll = ( assignmentId : string , scrollLeft : number ) => {
284+ const overlay = overlayRefs . current [ assignmentId ]
285+ if ( overlay ) overlay . scrollLeft = scrollLeft
286+ }
287+
288+ const handleKeyDown = ( e : React . KeyboardEvent < HTMLInputElement | HTMLTextAreaElement > ) => {
289+ if ( e . key === 'Escape' ) {
290+ setShowTags ( false )
291+ setActiveSourceBlockId ( null )
292+ }
293+ }
294+
275295 if ( isPreview && ( ! assignments || assignments . length === 0 ) ) {
276296 return (
277297 < div className = 'flex flex-col items-center justify-center rounded-md border border-border/40 bg-muted/20 py-8 text-center' >
@@ -302,7 +322,7 @@ export function VariablesInput({
302322
303323 return (
304324 < div className = 'space-y-[8px]' >
305- { assignments && assignments . length > 0 && (
325+ { assignments . length > 0 && (
306326 < div className = 'space-y-[8px]' >
307327 { assignments . map ( ( assignment , index ) => {
308328 const collapsed = collapsedAssignments [ assignment . id ] || false
@@ -334,7 +354,7 @@ export function VariablesInput({
334354 < Button
335355 variant = 'ghost'
336356 onClick = { addAssignment }
337- disabled = { isPreview || disabled || allVariablesAssigned }
357+ disabled = { isReadOnly || allVariablesAssigned }
338358 className = 'h-auto p-0'
339359 >
340360 < Plus className = 'h-[14px] w-[14px]' />
@@ -343,7 +363,7 @@ export function VariablesInput({
343363 < Button
344364 variant = 'ghost'
345365 onClick = { ( ) => removeAssignment ( assignment . id ) }
346- disabled = { isPreview || disabled || assignments . length === 1 }
366+ disabled = { isReadOnly }
347367 className = 'h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
348368 >
349369 < Trash className = 'h-[14px] w-[14px]' />
@@ -358,16 +378,26 @@ export function VariablesInput({
358378 < Label className = 'text-[13px]' > Variable</ Label >
359379 < Combobox
360380 options = { availableVars . map ( ( v ) => ( { label : v . name , value : v . id } ) ) }
361- value = { assignment . variableId || assignment . variableName || '' }
381+ value = { assignment . variableId || '' }
362382 onChange = { ( value ) => handleVariableSelect ( assignment . id , value ) }
363383 placeholder = 'Select a variable...'
364- disabled = { isPreview || disabled }
384+ disabled = { isReadOnly }
365385 />
366386 </ div >
367387
368388 < div className = 'flex flex-col gap-[6px]' >
369389 < Label className = 'text-[13px]' > Value</ Label >
370- { assignment . type === 'object' || assignment . type === 'array' ? (
390+ { assignment . type === 'boolean' ? (
391+ < Combobox
392+ options = { BOOLEAN_OPTIONS }
393+ value = { assignment . value ?? '' }
394+ onChange = { ( v ) =>
395+ ! isReadOnly && updateAssignment ( assignment . id , { value : v } )
396+ }
397+ placeholder = 'Select value'
398+ disabled = { isReadOnly }
399+ />
400+ ) : assignment . type === 'object' || assignment . type === 'array' ? (
371401 < div className = 'relative' >
372402 < Textarea
373403 ref = { ( el ) => {
@@ -381,26 +411,32 @@ export function VariablesInput({
381411 e . target . selectionStart ?? undefined
382412 )
383413 }
414+ onKeyDown = { handleKeyDown }
384415 onFocus = { ( ) => {
385- if ( ! isPreview && ! disabled && ! assignment . value ?. trim ( ) ) {
416+ if ( ! isReadOnly && ! assignment . value ?. trim ( ) ) {
386417 setActiveFieldId ( assignment . id )
387418 setCursorPosition ( 0 )
388419 setShowTags ( true )
389420 }
390421 } }
422+ onScroll = { ( e ) => {
423+ const overlay = overlayRefs . current [ assignment . id ]
424+ if ( overlay ) {
425+ overlay . scrollTop = e . currentTarget . scrollTop
426+ overlay . scrollLeft = e . currentTarget . scrollLeft
427+ }
428+ } }
391429 placeholder = {
392430 assignment . type === 'object'
393431 ? '{\n "key": "value"\n}'
394432 : '[\n 1, 2, 3\n]'
395433 }
396- disabled = { isPreview || disabled }
434+ disabled = { isReadOnly }
397435 className = { cn (
398436 'min-h-[120px] font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50' ,
399437 dragHighlight [ assignment . id ] && 'ring-2 ring-blue-500 ring-offset-2'
400438 ) }
401439 style = { {
402- fontFamily : 'inherit' ,
403- lineHeight : 'inherit' ,
404440 wordBreak : 'break-word' ,
405441 whiteSpace : 'pre-wrap' ,
406442 } }
@@ -413,10 +449,7 @@ export function VariablesInput({
413449 if ( el ) overlayRefs . current [ assignment . id ] = el
414450 } }
415451 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- } }
452+ style = { { scrollbarWidth : 'none' } }
420453 >
421454 < div className = 'w-full whitespace-pre-wrap break-words' >
422455 { formatDisplayText ( assignment . value || '' , {
@@ -441,21 +474,34 @@ export function VariablesInput({
441474 e . target . selectionStart ?? undefined
442475 )
443476 }
477+ onKeyDown = { handleKeyDown }
444478 onFocus = { ( ) => {
445- if ( ! isPreview && ! disabled && ! assignment . value ?. trim ( ) ) {
479+ if ( ! isReadOnly && ! assignment . value ?. trim ( ) ) {
446480 setActiveFieldId ( assignment . id )
447481 setCursorPosition ( 0 )
448482 setShowTags ( true )
449483 }
450484 } }
485+ onScroll = { ( e ) =>
486+ syncOverlayScroll ( assignment . id , e . currentTarget . scrollLeft )
487+ }
488+ onPaste = { ( ) =>
489+ setTimeout ( ( ) => {
490+ const input = valueInputRefs . current [ assignment . id ]
491+ if ( input )
492+ syncOverlayScroll (
493+ assignment . id ,
494+ ( input as HTMLInputElement ) . scrollLeft
495+ )
496+ } , 0 )
497+ }
451498 placeholder = { `${ assignment . type } value` }
452- disabled = { isPreview || disabled }
499+ disabled = { isReadOnly }
453500 autoComplete = 'off'
454501 className = { cn (
455- 'allow-scroll w-full overflow-auto text-transparent caret-foreground' ,
502+ 'allow-scroll w-full overflow-x- auto overflow-y-hidden text-transparent caret-foreground' ,
456503 dragHighlight [ assignment . id ] && 'ring-2 ring-blue-500 ring-offset-2'
457504 ) }
458- style = { { overflowX : 'auto' } }
459505 onDrop = { ( e ) => handleDrop ( e , assignment . id ) }
460506 onDragOver = { ( e ) => handleDragOver ( e , assignment . id ) }
461507 onDragLeave = { ( e ) => handleDragLeave ( e , assignment . id ) }
@@ -465,7 +511,7 @@ export function VariablesInput({
465511 if ( el ) overlayRefs . current [ assignment . id ] = el
466512 } }
467513 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 ' } }
514+ style = { { scrollbarWidth : 'none ' } }
469515 >
470516 < div
471517 className = 'w-full whitespace-pre'
0 commit comments