Compare commits

..

1 Commits

Author SHA1 Message Date
Waleed Latif
9ecd07b12c fix(ci): update ci 2026-01-24 14:27:55 -08:00

View File

@@ -1,15 +1,7 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { import { Badge, Button, Combobox, Input, Label, Textarea } from '@/components/emcn'
Badge,
Button,
Combobox,
type ComboboxOption,
Input,
Label,
Textarea,
} from '@/components/emcn'
import { Trash } from '@/components/emcn/icons/trash' import { Trash } from '@/components/emcn/icons/trash'
import { cn } from '@/lib/core/utils/cn' import { cn } from '@/lib/core/utils/cn'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
@@ -46,14 +38,6 @@ const DEFAULT_ASSIGNMENT: Omit<VariableAssignment, 'id'> = {
isExisting: false, isExisting: false,
} }
/**
* Boolean value options for Combobox
*/
const BOOLEAN_OPTIONS: ComboboxOption[] = [
{ label: 'true', value: 'true' },
{ label: 'false', value: 'false' },
]
/** /**
* Parses a value that might be a JSON string or already an array of VariableAssignment. * Parses a value that might be a JSON string or already an array of VariableAssignment.
* This handles the case where workflows are imported with stringified values. * This handles the case where workflows are imported with stringified values.
@@ -120,6 +104,8 @@ export function VariablesInput({
const allVariablesAssigned = const allVariablesAssigned =
!hasNoWorkflowVariables && getAvailableVariablesFor('new').length === 0 !hasNoWorkflowVariables && getAvailableVariablesFor('new').length === 0
// Initialize with one empty assignment if none exist and not in preview/disabled mode
// Also add assignment when first variable is created
useEffect(() => { useEffect(() => {
if (!isReadOnly && assignments.length === 0 && currentWorkflowVariables.length > 0) { if (!isReadOnly && assignments.length === 0 && currentWorkflowVariables.length > 0) {
const initialAssignment: VariableAssignment = { const initialAssignment: VariableAssignment = {
@@ -130,46 +116,45 @@ export function VariablesInput({
} }
}, [currentWorkflowVariables.length, isReadOnly, assignments.length, setStoreValue]) }, [currentWorkflowVariables.length, isReadOnly, assignments.length, setStoreValue])
// Clean up assignments when their associated variables are deleted
useEffect(() => { useEffect(() => {
if (isReadOnly || assignments.length === 0) return if (isReadOnly || assignments.length === 0) return
const currentVariableIds = new Set(currentWorkflowVariables.map((v) => v.id)) const currentVariableIds = new Set(currentWorkflowVariables.map((v) => v.id))
const validAssignments = assignments.filter((assignment) => { const validAssignments = assignments.filter((assignment) => {
// Keep assignments that haven't selected a variable yet
if (!assignment.variableId) return true if (!assignment.variableId) return true
// Keep assignments whose variable still exists
return currentVariableIds.has(assignment.variableId) return currentVariableIds.has(assignment.variableId)
}) })
// If all variables were deleted, clear all assignments
if (currentWorkflowVariables.length === 0) { if (currentWorkflowVariables.length === 0) {
setStoreValue([]) setStoreValue([])
} else if (validAssignments.length !== assignments.length) { } else if (validAssignments.length !== assignments.length) {
// Some assignments reference deleted variables, remove them
setStoreValue(validAssignments.length > 0 ? validAssignments : []) setStoreValue(validAssignments.length > 0 ? validAssignments : [])
} }
}, [currentWorkflowVariables, assignments, isReadOnly, setStoreValue]) }, [currentWorkflowVariables, assignments, isReadOnly, setStoreValue])
const addAssignment = () => { const addAssignment = () => {
if (isReadOnly || allVariablesAssigned) return if (isPreview || disabled || allVariablesAssigned) return
const newAssignment: VariableAssignment = { const newAssignment: VariableAssignment = {
...DEFAULT_ASSIGNMENT, ...DEFAULT_ASSIGNMENT,
id: crypto.randomUUID(), id: crypto.randomUUID(),
} }
setStoreValue([...assignments, newAssignment]) setStoreValue([...(assignments || []), newAssignment])
} }
const removeAssignment = (id: string) => { const removeAssignment = (id: string) => {
if (isReadOnly) return if (isPreview || disabled) return
setStoreValue((assignments || []).filter((a) => a.id !== id))
if (assignments.length === 1) {
setStoreValue([{ ...DEFAULT_ASSIGNMENT, id: crypto.randomUUID() }])
return
}
setStoreValue(assignments.filter((a) => a.id !== id))
} }
const updateAssignment = (id: string, updates: Partial<VariableAssignment>) => { const updateAssignment = (id: string, updates: Partial<VariableAssignment>) => {
if (isReadOnly) return if (isPreview || disabled) return
setStoreValue(assignments.map((a) => (a.id === id ? { ...a, ...updates } : a))) setStoreValue((assignments || []).map((a) => (a.id === id ? { ...a, ...updates } : a)))
} }
const handleVariableSelect = (assignmentId: string, variableId: string) => { const handleVariableSelect = (assignmentId: string, variableId: string) => {
@@ -184,12 +169,19 @@ export function VariablesInput({
} }
} }
const handleTagSelect = (newValue: string) => { const handleTagSelect = (tag: string) => {
if (!activeFieldId) return if (!activeFieldId) return
const assignment = assignments.find((a) => a.id === activeFieldId) const assignment = assignments.find((a) => a.id === activeFieldId)
const originalValue = assignment?.value || '' if (!assignment) return
const textAfterCursor = originalValue.slice(cursorPosition)
const currentValue = assignment.value || ''
const textBeforeCursor = currentValue.slice(0, cursorPosition)
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
const newValue =
currentValue.slice(0, lastOpenBracket) + tag + currentValue.slice(cursorPosition)
updateAssignment(activeFieldId, { value: newValue }) updateAssignment(activeFieldId, { value: newValue })
setShowTags(false) setShowTags(false)
@@ -198,7 +190,7 @@ export function VariablesInput({
const inputEl = valueInputRefs.current[activeFieldId] const inputEl = valueInputRefs.current[activeFieldId]
if (inputEl) { if (inputEl) {
inputEl.focus() inputEl.focus()
const newCursorPos = newValue.length - textAfterCursor.length const newCursorPos = lastOpenBracket + tag.length
inputEl.setSelectionRange(newCursorPos, newCursorPos) inputEl.setSelectionRange(newCursorPos, newCursorPos)
} }
}, 10) }, 10)
@@ -280,18 +272,6 @@ export function VariablesInput({
})) }))
} }
const syncOverlayScroll = (assignmentId: string, scrollLeft: number) => {
const overlay = overlayRefs.current[assignmentId]
if (overlay) overlay.scrollLeft = scrollLeft
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (e.key === 'Escape') {
setShowTags(false)
setActiveSourceBlockId(null)
}
}
if (isPreview && (!assignments || assignments.length === 0)) { if (isPreview && (!assignments || assignments.length === 0)) {
return ( return (
<div className='flex flex-col items-center justify-center rounded-md border border-border/40 bg-muted/20 py-8 text-center'> <div className='flex flex-col items-center justify-center rounded-md border border-border/40 bg-muted/20 py-8 text-center'>
@@ -322,7 +302,7 @@ export function VariablesInput({
return ( return (
<div className='space-y-[8px]'> <div className='space-y-[8px]'>
{assignments.length > 0 && ( {assignments && assignments.length > 0 && (
<div className='space-y-[8px]'> <div className='space-y-[8px]'>
{assignments.map((assignment, index) => { {assignments.map((assignment, index) => {
const collapsed = collapsedAssignments[assignment.id] || false const collapsed = collapsedAssignments[assignment.id] || false
@@ -354,7 +334,7 @@ export function VariablesInput({
<Button <Button
variant='ghost' variant='ghost'
onClick={addAssignment} onClick={addAssignment}
disabled={isReadOnly || allVariablesAssigned} disabled={isPreview || disabled || allVariablesAssigned}
className='h-auto p-0' className='h-auto p-0'
> >
<Plus className='h-[14px] w-[14px]' /> <Plus className='h-[14px] w-[14px]' />
@@ -363,7 +343,7 @@ export function VariablesInput({
<Button <Button
variant='ghost' variant='ghost'
onClick={() => removeAssignment(assignment.id)} onClick={() => removeAssignment(assignment.id)}
disabled={isReadOnly} disabled={isPreview || disabled || assignments.length === 1}
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]' className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
> >
<Trash className='h-[14px] w-[14px]' /> <Trash className='h-[14px] w-[14px]' />
@@ -378,26 +358,16 @@ export function VariablesInput({
<Label className='text-[13px]'>Variable</Label> <Label className='text-[13px]'>Variable</Label>
<Combobox <Combobox
options={availableVars.map((v) => ({ label: v.name, value: v.id }))} options={availableVars.map((v) => ({ label: v.name, value: v.id }))}
value={assignment.variableId || ''} value={assignment.variableId || assignment.variableName || ''}
onChange={(value) => handleVariableSelect(assignment.id, value)} onChange={(value) => handleVariableSelect(assignment.id, value)}
placeholder='Select a variable...' placeholder='Select a variable...'
disabled={isReadOnly} disabled={isPreview || disabled}
/> />
</div> </div>
<div className='flex flex-col gap-[6px]'> <div className='flex flex-col gap-[6px]'>
<Label className='text-[13px]'>Value</Label> <Label className='text-[13px]'>Value</Label>
{assignment.type === 'boolean' ? ( {assignment.type === 'object' || assignment.type === 'array' ? (
<Combobox
options={BOOLEAN_OPTIONS}
value={assignment.value ?? ''}
onChange={(v) =>
!isReadOnly && updateAssignment(assignment.id, { value: v })
}
placeholder='Select value'
disabled={isReadOnly}
/>
) : assignment.type === 'object' || assignment.type === 'array' ? (
<div className='relative'> <div className='relative'>
<Textarea <Textarea
ref={(el) => { ref={(el) => {
@@ -411,32 +381,26 @@ export function VariablesInput({
e.target.selectionStart ?? undefined e.target.selectionStart ?? undefined
) )
} }
onKeyDown={handleKeyDown}
onFocus={() => { onFocus={() => {
if (!isReadOnly && !assignment.value?.trim()) { if (!isPreview && !disabled && !assignment.value?.trim()) {
setActiveFieldId(assignment.id) setActiveFieldId(assignment.id)
setCursorPosition(0) setCursorPosition(0)
setShowTags(true) setShowTags(true)
} }
}} }}
onScroll={(e) => {
const overlay = overlayRefs.current[assignment.id]
if (overlay) {
overlay.scrollTop = e.currentTarget.scrollTop
overlay.scrollLeft = e.currentTarget.scrollLeft
}
}}
placeholder={ placeholder={
assignment.type === 'object' assignment.type === 'object'
? '{\n "key": "value"\n}' ? '{\n "key": "value"\n}'
: '[\n 1, 2, 3\n]' : '[\n 1, 2, 3\n]'
} }
disabled={isReadOnly} disabled={isPreview || disabled}
className={cn( className={cn(
'min-h-[120px] font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50', 'min-h-[120px] font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50',
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2' dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
)} )}
style={{ style={{
fontFamily: 'inherit',
lineHeight: 'inherit',
wordBreak: 'break-word', wordBreak: 'break-word',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
}} }}
@@ -449,7 +413,10 @@ export function VariablesInput({
if (el) overlayRefs.current[assignment.id] = el if (el) overlayRefs.current[assignment.id] = el
}} }}
className='pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 font-mono text-sm' className='pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 font-mono text-sm'
style={{ scrollbarWidth: 'none' }} style={{
fontFamily: 'inherit',
lineHeight: 'inherit',
}}
> >
<div className='w-full whitespace-pre-wrap break-words'> <div className='w-full whitespace-pre-wrap break-words'>
{formatDisplayText(assignment.value || '', { {formatDisplayText(assignment.value || '', {
@@ -474,34 +441,21 @@ export function VariablesInput({
e.target.selectionStart ?? undefined e.target.selectionStart ?? undefined
) )
} }
onKeyDown={handleKeyDown}
onFocus={() => { onFocus={() => {
if (!isReadOnly && !assignment.value?.trim()) { if (!isPreview && !disabled && !assignment.value?.trim()) {
setActiveFieldId(assignment.id) setActiveFieldId(assignment.id)
setCursorPosition(0) setCursorPosition(0)
setShowTags(true) setShowTags(true)
} }
}} }}
onScroll={(e) =>
syncOverlayScroll(assignment.id, e.currentTarget.scrollLeft)
}
onPaste={() =>
setTimeout(() => {
const input = valueInputRefs.current[assignment.id]
if (input)
syncOverlayScroll(
assignment.id,
(input as HTMLInputElement).scrollLeft
)
}, 0)
}
placeholder={`${assignment.type} value`} placeholder={`${assignment.type} value`}
disabled={isReadOnly} disabled={isPreview || disabled}
autoComplete='off' autoComplete='off'
className={cn( className={cn(
'allow-scroll w-full overflow-x-auto overflow-y-hidden text-transparent caret-foreground', 'allow-scroll w-full overflow-auto text-transparent caret-foreground',
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2' dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
)} )}
style={{ overflowX: 'auto' }}
onDrop={(e) => handleDrop(e, assignment.id)} onDrop={(e) => handleDrop(e, assignment.id)}
onDragOver={(e) => handleDragOver(e, assignment.id)} onDragOver={(e) => handleDragOver(e, assignment.id)}
onDragLeave={(e) => handleDragLeave(e, assignment.id)} onDragLeave={(e) => handleDragLeave(e, assignment.id)}
@@ -511,7 +465,7 @@ export function VariablesInput({
if (el) overlayRefs.current[assignment.id] = el if (el) overlayRefs.current[assignment.id] = el
}} }}
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' 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'
style={{ scrollbarWidth: 'none' }} style={{ overflowX: 'auto' }}
> >
<div <div
className='w-full whitespace-pre' className='w-full whitespace-pre'