fix(modals): fix z-index for various modals and output selector and variables (#2005)

This commit is contained in:
Waleed
2025-11-14 23:13:31 -08:00
committed by GitHub
parent d0767507b2
commit f1111ec16f
6 changed files with 452 additions and 313 deletions

View File

@@ -600,6 +600,7 @@ export function Chat() {
onOutputSelect={handleOutputSelection}
disabled={!activeWorkflowId}
placeholder='Select outputs'
align='end'
/>
</div>

View File

@@ -7,7 +7,6 @@ import {
Popover,
PopoverContent,
PopoverItem,
PopoverScrollArea,
PopoverSection,
PopoverTrigger,
} from '@/components/emcn'
@@ -24,6 +23,7 @@ interface OutputSelectProps {
disabled?: boolean
placeholder?: string
valueMode?: 'id' | 'label'
align?: 'start' | 'end' | 'center'
}
export function OutputSelect({
@@ -33,10 +33,13 @@ export function OutputSelect({
disabled = false,
placeholder = 'Select outputs',
valueMode = 'id',
align = 'start',
}: OutputSelectProps) {
const [open, setOpen] = useState(false)
const [highlightedIndex, setHighlightedIndex] = useState(-1)
const triggerRef = useRef<HTMLDivElement>(null)
const popoverRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const blocks = useWorkflowStore((state) => state.blocks)
const { isShowingDiff, isDiffReady, diffWorkflow } = useWorkflowDiffStore()
const subBlockValues = useSubBlockStore((state) =>
@@ -230,6 +233,13 @@ export function OutputSelect({
return blockConfig?.bgColor || '#2F55FF'
}
/**
* Flattened outputs for keyboard navigation
*/
const flattenedOutputs = useMemo(() => {
return Object.values(groupedOutputs).flat()
}, [groupedOutputs])
/**
* Handles output selection - toggle selection
*/
@@ -246,6 +256,75 @@ export function OutputSelect({
onOutputSelect(newSelectedOutputs)
}
/**
* Keyboard navigation handler
*/
const handleKeyDown = (e: React.KeyboardEvent) => {
if (flattenedOutputs.length === 0) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setHighlightedIndex((prev) => {
const next = prev < flattenedOutputs.length - 1 ? prev + 1 : 0
return next
})
break
case 'ArrowUp':
e.preventDefault()
setHighlightedIndex((prev) => {
const next = prev > 0 ? prev - 1 : flattenedOutputs.length - 1
return next
})
break
case 'Enter':
e.preventDefault()
if (highlightedIndex >= 0 && highlightedIndex < flattenedOutputs.length) {
handleOutputSelection(flattenedOutputs[highlightedIndex].label)
}
break
case 'Escape':
e.preventDefault()
setOpen(false)
break
}
}
/**
* Reset highlighted index when popover opens/closes
*/
useEffect(() => {
if (open) {
// Find first selected item, or start at -1
const firstSelectedIndex = flattenedOutputs.findIndex((output) => isSelectedValue(output))
setHighlightedIndex(firstSelectedIndex >= 0 ? firstSelectedIndex : -1)
// Focus the content for keyboard navigation
setTimeout(() => {
contentRef.current?.focus()
}, 0)
} else {
setHighlightedIndex(-1)
}
}, [open, flattenedOutputs])
/**
* Scroll highlighted item into view
*/
useEffect(() => {
if (highlightedIndex >= 0 && contentRef.current) {
const highlightedElement = contentRef.current.querySelector(
`[data-option-index="${highlightedIndex}"]`
)
if (highlightedElement) {
highlightedElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
}
}, [highlightedIndex])
/**
* Closes popover when clicking outside
*/
@@ -288,44 +367,57 @@ export function OutputSelect({
<PopoverContent
ref={popoverRef}
side='bottom'
align='start'
align={align}
sideOffset={4}
maxHeight={140}
maxWidth={140}
minWidth={140}
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
maxHeight={300}
maxWidth={300}
minWidth={200}
onKeyDown={handleKeyDown}
tabIndex={0}
style={{ outline: 'none' }}
>
<PopoverScrollArea className='space-y-[2px]'>
{Object.entries(groupedOutputs).map(([blockName, outputs]) => (
<div key={blockName}>
<PopoverSection>{blockName}</PopoverSection>
<div ref={contentRef} className='space-y-[2px]'>
{Object.entries(groupedOutputs).map(([blockName, outputs]) => {
// Calculate the starting index for this group
const startIndex = flattenedOutputs.findIndex((o) => o.blockName === blockName)
<div className='flex flex-col gap-[2px]'>
{outputs.map((output) => (
<PopoverItem
key={output.id}
active={isSelectedValue(output)}
onClick={() => handleOutputSelection(output.label)}
>
<div
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
style={{
backgroundColor: getOutputColor(output.blockId, output.blockType),
}}
>
<span className='font-bold text-[10px] text-white'>
{blockName.charAt(0).toUpperCase()}
</span>
</div>
<span className='min-w-0 flex-1 truncate'>{output.path}</span>
{isSelectedValue(output) && <Check className='h-3 w-3 flex-shrink-0' />}
</PopoverItem>
))}
return (
<div key={blockName}>
<PopoverSection>{blockName}</PopoverSection>
<div className='flex flex-col gap-[2px]'>
{outputs.map((output, localIndex) => {
const globalIndex = startIndex + localIndex
const isHighlighted = globalIndex === highlightedIndex
return (
<PopoverItem
key={output.id}
active={isSelectedValue(output) || isHighlighted}
data-option-index={globalIndex}
onClick={() => handleOutputSelection(output.label)}
onMouseEnter={() => setHighlightedIndex(globalIndex)}
>
<div
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
style={{
backgroundColor: getOutputColor(output.blockId, output.blockType),
}}
>
<span className='font-bold text-[10px] text-white'>
{blockName.charAt(0).toUpperCase()}
</span>
</div>
<span className='min-w-0 flex-1 truncate'>{output.path}</span>
{isSelectedValue(output) && <Check className='h-3 w-3 flex-shrink-0' />}
</PopoverItem>
)
})}
</div>
</div>
</div>
))}
</PopoverScrollArea>
)
})}
</div>
</PopoverContent>
</Popover>
)

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { Badge } from '@/components/emcn'
import { Input } from '@/components/emcn/components/input/input'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
@@ -50,12 +51,13 @@ interface InputMappingFieldProps {
value: string
onChange: (value: string) => void
blockId: string
subBlockId: string
disabled: boolean
accessiblePrefixes: Set<string> | undefined
inputController: ReturnType<typeof useSubBlockInput>
inputRefs: React.MutableRefObject<Map<string, HTMLInputElement>>
overlayRefs: React.MutableRefObject<Map<string, HTMLDivElement>>
inputRefs: React.RefObject<Map<string, HTMLInputElement>>
overlayRefs: React.RefObject<Map<string, HTMLDivElement>>
collapsed: boolean
onToggleCollapse: () => void
}
/**
@@ -169,6 +171,7 @@ export function InputMapping({
const [childInputFields, setChildInputFields] = useState<InputFormatField[]>([])
const [isLoading, setIsLoading] = useState(false)
const [collapsedFields, setCollapsedFields] = useState<Record<string, boolean>>({})
useEffect(() => {
let isMounted = true
@@ -245,6 +248,13 @@ export function InputMapping({
setMapping(updated)
}
const toggleCollapse = (fieldName: string) => {
setCollapsedFields((prev) => ({
...prev,
[fieldName]: !prev[fieldName],
}))
}
if (!selectedWorkflowId) {
return (
<div className='flex flex-col items-center justify-center rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F] p-8 text-center'>
@@ -278,12 +288,13 @@ export function InputMapping({
value=''
onChange={() => {}}
blockId={blockId}
subBlockId={subBlockId}
disabled={true}
accessiblePrefixes={accessiblePrefixes}
inputController={inputController}
inputRefs={inputRefs}
overlayRefs={overlayRefs}
collapsed={false}
onToggleCollapse={() => {}}
/>
</div>
)
@@ -303,12 +314,13 @@ export function InputMapping({
value={valueObj[field.name] || ''}
onChange={(value) => handleFieldUpdate(field.name, value)}
blockId={blockId}
subBlockId={subBlockId}
disabled={isPreview || disabled}
accessiblePrefixes={accessiblePrefixes}
inputController={inputController}
inputRefs={inputRefs}
overlayRefs={overlayRefs}
collapsed={collapsedFields[field.name] || false}
onToggleCollapse={() => toggleCollapse(field.name)}
/>
))}
</div>
@@ -326,12 +338,13 @@ function InputMappingField({
value,
onChange,
blockId,
subBlockId,
disabled,
accessiblePrefixes,
inputController,
inputRefs,
overlayRefs,
collapsed,
onToggleCollapse,
}: InputMappingFieldProps) {
const fieldId = fieldName
const fieldState = inputController.fieldHelpers.getFieldState(fieldId)
@@ -354,64 +367,91 @@ function InputMappingField({
}
return (
<div className='group relative overflow-visible rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'>
<div className='flex items-center justify-between bg-transparent px-[10px] py-[5px]'>
<Label className='font-medium text-[14px] text-[var(--text-tertiary)]'>{fieldName}</Label>
{fieldType && (
<span className='rounded-md bg-[#2A2A2A] px-1.5 py-0.5 font-mono text-[10px] text-[var(--text-tertiary)]'>
{fieldType}
<div
className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]',
collapsed ? 'overflow-hidden' : 'overflow-visible'
)}
>
<div
className='flex cursor-pointer items-center justify-between bg-transparent px-[10px] py-[5px]'
onClick={onToggleCollapse}
>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
{fieldName}
</span>
)}
{fieldType && <Badge className='h-[20px] text-[13px]'>{fieldType}</Badge>}
</div>
</div>
<div className='relative w-full border-[var(--border-strong)] border-t bg-transparent'>
<Input
ref={(el) => {
if (el) inputRefs.current.set(fieldId, el)
}}
className={cn(
'allow-scroll !bg-transparent w-full overflow-auto rounded-none border-0 px-[10px] py-[8px] text-transparent caret-white [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden'
)}
type='text'
value={value}
onChange={handlers.onChange}
onKeyDown={handlers.onKeyDown}
onScroll={handleScroll}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
autoComplete='off'
disabled={disabled}
/>
<div
ref={(el) => {
if (el) overlayRefs.current.set(fieldId, el)
}}
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[10px] py-[8px] font-medium font-sans text-[#eeeeee] text-sm [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
>
<div className='min-w-fit whitespace-pre'>
{formatDisplayText(value, {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
{!collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='space-y-[4px]'>
<Label className='text-[13px]'>Value</Label>
<div className='relative'>
<Input
ref={(el) => {
if (el) inputRefs.current.set(fieldId, el)
}}
name='value'
value={value}
onChange={handlers.onChange}
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onScroll={(e) => handleScroll(e)}
onPaste={() =>
setTimeout(() => {
const input = inputRefs.current.get(fieldId)
input && handleScroll({ currentTarget: input } as any)
}, 0)
}
placeholder='Enter value or reference'
disabled={disabled}
autoComplete='off'
className={cn(
'allow-scroll w-full overflow-auto text-transparent caret-foreground'
)}
style={{ overflowX: 'auto' }}
/>
<div
ref={(el) => {
if (el) overlayRefs.current.set(fieldId, 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'
style={{ overflowX: 'auto' }}
>
<div
className='w-full whitespace-pre'
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
>
{formatDisplayText(
value,
accessiblePrefixes ? { accessiblePrefixes } : { highlightAll: true }
)}
</div>
</div>
{fieldState.showTags && (
<TagDropdown
visible={fieldState.showTags}
onSelect={tagSelectHandler}
blockId={blockId}
activeSourceBlockId={fieldState.activeSourceBlockId}
inputValue={value}
cursorPosition={fieldState.cursorPosition}
onClose={() => inputController.fieldHelpers.hideFieldDropdowns(fieldId)}
inputRef={
{
current: inputRefs.current.get(fieldId) || null,
} as React.RefObject<HTMLInputElement>
}
/>
)}
</div>
</div>
</div>
{fieldState.showTags && (
<TagDropdown
visible={fieldState.showTags}
onSelect={tagSelectHandler}
blockId={blockId}
activeSourceBlockId={fieldState.activeSourceBlockId}
inputValue={value}
cursorPosition={fieldState.cursorPosition}
onClose={() => inputController.fieldHelpers.hideFieldDropdowns(fieldId)}
inputRef={
{
current: inputRefs.current.get(fieldId) || null,
} as React.RefObject<HTMLInputElement>
}
/>
)}
</div>
)}
</div>
)
}

View File

@@ -1,17 +1,9 @@
import { useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Badge, Button, Combobox, Input } from '@/components/emcn'
import { Trash } from '@/components/emcn/icons/trash'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text'
@@ -68,6 +60,7 @@ export function VariablesInput({
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
const [dragHighlight, setDragHighlight] = useState<Record<string, boolean>>({})
const [collapsedAssignments, setCollapsedAssignments] = useState<Record<string, boolean>>({})
const currentWorkflowVariables = Object.values(workflowVariables).filter(
(v: Variable) => v.workflowId === workflowId
@@ -75,6 +68,7 @@ export function VariablesInput({
const value = isPreview ? previewValue : storeValue
const assignments: VariableAssignment[] = value || []
const isReadOnly = isPreview || disabled
const getAvailableVariablesFor = (currentAssignmentId: string) => {
const otherSelectedIds = new Set(
@@ -91,8 +85,41 @@ export function VariablesInput({
const allVariablesAssigned =
!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(() => {
if (!isReadOnly && assignments.length === 0 && currentWorkflowVariables.length > 0) {
const initialAssignment: VariableAssignment = {
...DEFAULT_ASSIGNMENT,
id: crypto.randomUUID(),
}
setStoreValue([initialAssignment])
}
}, [currentWorkflowVariables.length, isReadOnly, assignments.length, setStoreValue])
// Clean up assignments when their associated variables are deleted
useEffect(() => {
if (isReadOnly || assignments.length === 0) return
const currentVariableIds = new Set(currentWorkflowVariables.map((v) => v.id))
const validAssignments = assignments.filter((assignment) => {
// Keep assignments that haven't selected a variable yet
if (!assignment.variableId) return true
// Keep assignments whose variable still exists
return currentVariableIds.has(assignment.variableId)
})
// If all variables were deleted, clear all assignments
if (currentWorkflowVariables.length === 0) {
setStoreValue([])
} else if (validAssignments.length !== assignments.length) {
// Some assignments reference deleted variables, remove them
setStoreValue(validAssignments.length > 0 ? validAssignments : [])
}
}, [currentWorkflowVariables, assignments, isReadOnly, setStoreValue])
const addAssignment = () => {
if (isPreview || disabled) return
if (isPreview || disabled || allVariablesAssigned) return
const newAssignment: VariableAssignment = {
...DEFAULT_ASSIGNMENT,
@@ -219,6 +246,13 @@ export function VariablesInput({
setDragHighlight((prev) => ({ ...prev, [assignmentId]: false }))
}
const toggleCollapse = (assignmentId: string) => {
setCollapsedAssignments((prev) => ({
...prev,
[assignmentId]: !prev[assignmentId],
}))
}
if (isPreview && (!assignments || assignments.length === 0)) {
return (
<div className='flex flex-col items-center justify-center rounded-md border border-border/40 border-dashed bg-muted/20 py-8 text-center'>
@@ -244,225 +278,195 @@ export function VariablesInput({
}
if (!isPreview && hasNoWorkflowVariables && assignments.length === 0) {
return (
<div className='flex flex-col items-center justify-center rounded-lg border border-border/50 bg-muted/30 p-8 text-center'>
<svg
className='mb-3 h-10 w-10 text-muted-foreground/60'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={1.5}
d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
/>
</svg>
<p className='font-medium text-muted-foreground text-sm'>No variables found</p>
<p className='mt-1 text-muted-foreground/80 text-xs'>
Add variables in the Variables panel to get started
</p>
</div>
)
return <p className='text-[var(--text-muted)] text-sm'>No variables available</p>
}
return (
<div className='space-y-2'>
{assignments && assignments.length > 0 ? (
<div className='space-y-2'>
{assignments.map((assignment) => {
<div className='space-y-[8px]'>
{assignments && assignments.length > 0 && (
<div className='space-y-[8px]'>
{assignments.map((assignment, index) => {
const collapsed = collapsedAssignments[assignment.id] || false
const availableVars = getAvailableVariablesFor(assignment.id)
return (
<div
key={assignment.id}
className='group relative rounded-lg border border-border/50 bg-background/50 p-3 transition-all hover:border-border hover:bg-background'
>
{!isPreview && !disabled && (
<Button
variant='ghost'
size='icon'
className='absolute top-2 right-2 h-6 w-6 opacity-0 transition-opacity group-hover:opacity-100'
onClick={() => removeAssignment(assignment.id)}
>
<Trash className='h-3.5 w-3.5 text-muted-foreground hover:text-destructive' />
</Button>
data-assignment-id={assignment.id}
className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]',
collapsed ? 'overflow-hidden' : 'overflow-visible'
)}
<div className='space-y-3'>
<div className='space-y-1.5'>
<div className='flex items-center justify-between pr-8'>
<Label className='text-xs'>Variable</Label>
{assignment.variableName && (
<span className='rounded-md bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground'>
{assignment.type}
</span>
)}
</div>
<Select
value={assignment.variableId || assignment.variableName || ''}
onValueChange={(value) => {
if (value === '__new__') {
return
}
handleVariableSelect(assignment.id, value)
}}
disabled={isPreview || disabled}
>
<SelectTrigger className='h-9 border border-input bg-white dark:border-input/60 dark:bg-background'>
<SelectValue placeholder='Select a variable...' />
</SelectTrigger>
<SelectContent>
{(() => {
const availableVars = getAvailableVariablesFor(assignment.id)
return availableVars.length > 0 ? (
availableVars.map((variable) => (
<SelectItem key={variable.id} value={variable.id}>
{variable.name}
</SelectItem>
))
) : (
<div className='p-2 text-center text-muted-foreground text-sm'>
{currentWorkflowVariables.length > 0
? 'All variables have been assigned.'
: 'No variables defined in this workflow.'}
{currentWorkflowVariables.length === 0 && (
<>
<br />
Add them in the Variables panel.
</>
)}
</div>
)
})()}
</SelectContent>
</Select>
>
<div
className='flex cursor-pointer items-center justify-between bg-transparent px-[10px] py-[5px]'
onClick={() => toggleCollapse(assignment.id)}
>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
{assignment.variableName || `Variable ${index + 1}`}
</span>
{assignment.variableName && (
<Badge className='h-[20px] text-[13px]'>{assignment.type}</Badge>
)}
</div>
<div className='space-y-1.5'>
<Label className='text-xs'>Value</Label>
{assignment.type === 'object' || assignment.type === 'array' ? (
<div className='relative'>
<Textarea
ref={(el) => {
if (el) valueInputRefs.current[assignment.id] = el
}}
value={assignment.value || ''}
onChange={(e) =>
handleValueInputChange(
assignment.id,
e.target.value,
e.target.selectionStart ?? undefined
)
}
placeholder={
assignment.type === 'object'
? '{\n "key": "value"\n}'
: '[\n 1, 2, 3\n]'
}
disabled={isPreview || disabled}
className={cn(
'min-h-[120px] border border-input bg-white font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
)}
style={{
fontFamily: 'inherit',
lineHeight: 'inherit',
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
}}
onDrop={(e) => handleDrop(e, assignment.id)}
onDragOver={(e) => handleDragOver(e, assignment.id)}
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
/>
<div
ref={(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'
style={{
fontFamily: 'inherit',
lineHeight: 'inherit',
}}
>
<div className='w-full whitespace-pre-wrap break-words'>
{formatDisplayText(assignment.value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
</div>
) : (
<div className='relative'>
<Input
ref={(el) => {
if (el) valueInputRefs.current[assignment.id] = el
}}
value={assignment.value || ''}
onChange={(e) =>
handleValueInputChange(
assignment.id,
e.target.value,
e.target.selectionStart ?? undefined
)
}
placeholder={`${assignment.type} value`}
disabled={isPreview || disabled}
autoComplete='off'
className={cn(
'h-9 border border-input bg-white text-transparent caret-foreground placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
)}
onDrop={(e) => handleDrop(e, assignment.id)}
onDragOver={(e) => handleDragOver(e, assignment.id)}
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
/>
<div
ref={(el) => {
if (el) overlayRefs.current[assignment.id] = el
}}
className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'
>
<div className='w-full whitespace-nowrap'>
{formatDisplayText(assignment.value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
</div>
)}
{showTags && activeFieldId === assignment.id && (
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={assignment.value || ''}
cursorPosition={cursorPosition}
onClose={() => setShowTags(false)}
className='absolute top-full left-0 z-50 mt-1'
/>
)}
<div
className='flex items-center gap-[8px] pl-[8px]'
onClick={(e) => e.stopPropagation()}
>
<Button
variant='ghost'
onClick={addAssignment}
disabled={isPreview || disabled || allVariablesAssigned}
className='h-auto p-0'
>
<Plus className='h-[14px] w-[14px]' />
<span className='sr-only'>Add Variable</span>
</Button>
<Button
variant='ghost'
onClick={() => removeAssignment(assignment.id)}
disabled={isPreview || disabled || assignments.length === 1}
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
>
<Trash className='h-[14px] w-[14px]' />
<span className='sr-only'>Delete Variable</span>
</Button>
</div>
</div>
{!collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[4px]'>
<Label className='text-[13px]'>Variable</Label>
<Combobox
options={availableVars.map((v) => ({ label: v.name, value: v.id }))}
value={assignment.variableId || assignment.variableName || ''}
onChange={(value) => handleVariableSelect(assignment.id, value)}
placeholder='Select a variable...'
disabled={isPreview || disabled}
/>
</div>
<div className='space-y-[4px]'>
<Label className='text-[13px]'>Value</Label>
{assignment.type === 'object' || assignment.type === 'array' ? (
<div className='relative'>
<Textarea
ref={(el) => {
if (el) valueInputRefs.current[assignment.id] = el
}}
value={assignment.value || ''}
onChange={(e) =>
handleValueInputChange(
assignment.id,
e.target.value,
e.target.selectionStart ?? undefined
)
}
placeholder={
assignment.type === 'object'
? '{\n "key": "value"\n}'
: '[\n 1, 2, 3\n]'
}
disabled={isPreview || disabled}
className={cn(
'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'
)}
style={{
fontFamily: 'inherit',
lineHeight: 'inherit',
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
}}
onDrop={(e) => handleDrop(e, assignment.id)}
onDragOver={(e) => handleDragOver(e, assignment.id)}
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
/>
<div
ref={(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'
style={{
fontFamily: 'inherit',
lineHeight: 'inherit',
}}
>
<div className='w-full whitespace-pre-wrap break-words'>
{formatDisplayText(assignment.value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
</div>
) : (
<div className='relative'>
<Input
ref={(el) => {
if (el) valueInputRefs.current[assignment.id] = el
}}
name='value'
value={assignment.value || ''}
onChange={(e) =>
handleValueInputChange(
assignment.id,
e.target.value,
e.target.selectionStart ?? undefined
)
}
placeholder={`${assignment.type} value`}
disabled={isPreview || disabled}
autoComplete='off'
className={cn(
'allow-scroll w-full overflow-auto text-transparent caret-foreground',
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
)}
style={{ overflowX: 'auto' }}
onDrop={(e) => handleDrop(e, assignment.id)}
onDragOver={(e) => handleDragOver(e, assignment.id)}
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
/>
<div
ref={(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'
style={{ overflowX: 'auto' }}
>
<div
className='w-full whitespace-pre'
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
>
{formatDisplayText(
assignment.value || '',
accessiblePrefixes ? { accessiblePrefixes } : { highlightAll: true }
)}
</div>
</div>
</div>
)}
{showTags && activeFieldId === assignment.id && (
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={assignment.value || ''}
cursorPosition={cursorPosition}
onClose={() => setShowTags(false)}
/>
)}
</div>
</div>
)}
</div>
)
})}
</div>
) : null}
{!isPreview && !disabled && !hasNoWorkflowVariables && (
<Button
onClick={addAssignment}
variant='outline'
className='h-9 w-full border-dashed'
disabled={allVariablesAssigned}
>
<Plus className='mr-2 h-4 w-4' />
{allVariablesAssigned ? 'All Variables Assigned' : 'Add Variable Assignment'}
</Button>
)}
</div>
)

View File

@@ -41,8 +41,9 @@ import { cn } from '@/lib/utils'
/**
* Modal z-index configuration
* Set higher than Dialog component (10000000) to ensure Settings modal appears on top when opened from Deploy modal
*/
const MODAL_Z_INDEX = 9999999
const MODAL_Z_INDEX = 10000100
/**
* Modal sizing constants

View File

@@ -296,7 +296,8 @@ const PopoverContent = React.forwardRef<
'z-[10000001] flex flex-col overflow-auto rounded-[8px] bg-[var(--surface-3)] px-[5.5px] py-[5px] text-foreground outline-none dark:bg-[var(--surface-3)]',
// If width is constrained by the caller (prop or style), ensure inner flexible text truncates by default,
// and also truncate section headers.
hasUserWidthConstraint && '[&_.flex-1]:truncate [&_[data-popover-section]]:truncate'
hasUserWidthConstraint && '[&_.flex-1]:truncate [&_[data-popover-section]]:truncate',
className
)}
style={{
maxHeight: `${maxHeight || 400}px`,