Skip to content

Commit bcf6dc8

Browse files
authored
fix(variables): boolean type support and input improvements (#2981)
* fix(variables): boolean type support and input improvements * fix formatting
1 parent 841cb63 commit bcf6dc8

File tree

1 file changed

+90
-44
lines changed
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input

1 file changed

+90
-44
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { useEffect, useRef, useState } from 'react'
22
import { Plus } from 'lucide-react'
33
import { 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'
513
import { Trash } from '@/components/emcn/icons/trash'
614
import { cn } from '@/lib/core/utils/cn'
715
import { 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

Comments
 (0)