improvement(variables): add error context for duplicate variable names, only check for collision when focus is lost (#1791)

* improvement(variables): add error context for duplicate variable names, only check for collision when focus is lost

* disallow empty variable names, performance optimizations

* safety guard against empty variables names
This commit is contained in:
Waleed
2025-11-01 18:19:34 -07:00
committed by GitHub
parent e4d21568e3
commit f9980868a4
2 changed files with 204 additions and 258 deletions

View File

@@ -29,7 +29,7 @@ import {
TooltipTrigger,
} from '@/components/ui'
import { createLogger } from '@/lib/logs/console/logger'
import { validateName } from '@/lib/utils'
import { cn, validateName } from '@/lib/utils'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useVariablesStore } from '@/stores/panel/variables/store'
import type { Variable, VariableType } from '@/stores/panel/variables/types'
@@ -37,6 +37,17 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const logger = createLogger('Variables')
const TYPE_CONFIG: Record<VariableType, { icon: string; placeholder: string }> = {
plain: { icon: 'Abc', placeholder: 'Plain text value' },
number: { icon: '123', placeholder: '42' },
boolean: { icon: '0/1', placeholder: 'true' },
object: { icon: '{}', placeholder: '{\n "key": "value"\n}' },
array: { icon: '[]', placeholder: '[\n 1,\n 2,\n 3\n]' },
string: { icon: 'Abc', placeholder: 'Plain text value' },
}
const VARIABLE_TYPES: VariableType[] = ['plain', 'number', 'boolean', 'object', 'array']
export function Variables() {
const { activeWorkflowId } = useWorkflowRegistry()
const { getVariablesByWorkflowId } = useVariablesStore()
@@ -47,17 +58,33 @@ export function Variables() {
collaborativeDuplicateVariable,
} = useCollaborativeWorkflow()
// Get variables for the current workflow
const workflowVariables = activeWorkflowId ? getVariablesByWorkflowId(activeWorkflowId) : []
// Track editor references
const editorRefs = useRef<Record<string, HTMLDivElement | null>>({})
// Track which variables are currently being edited
const [_activeEditors, setActiveEditors] = useState<Record<string, boolean>>({})
// Collapsed state per variable
const [collapsedById, setCollapsedById] = useState<Record<string, boolean>>({})
const [localNames, setLocalNames] = useState<Record<string, string>>({})
const [nameErrors, setNameErrors] = useState<Record<string, string>>({})
const clearLocalState = (variableId: string) => {
setLocalNames((prev) => {
const updated = { ...prev }
delete updated[variableId]
return updated
})
setNameErrors((prev) => {
const updated = { ...prev }
delete updated[variableId]
return updated
})
}
const clearError = (variableId: string) => {
setNameErrors((prev) => {
if (!prev[variableId]) return prev
const updated = { ...prev }
delete updated[variableId]
return updated
})
}
const toggleCollapsed = (variableId: string) => {
setCollapsedById((prev) => ({
@@ -66,97 +93,75 @@ export function Variables() {
}))
}
// Handle variable name change with validation
const handleVariableNameChange = (variableId: string, newName: string) => {
const validatedName = validateName(newName)
collaborativeUpdateVariable(variableId, 'name', validatedName)
setLocalNames((prev) => ({
...prev,
[variableId]: validatedName,
}))
clearError(variableId)
}
const isDuplicateName = (variableId: string, name: string): boolean => {
if (!name.trim()) return false
return workflowVariables.some((v) => v.id !== variableId && v.name === name.trim())
}
const handleVariableNameBlur = (variableId: string) => {
const localName = localNames[variableId]
if (localName === undefined) return
const trimmedName = localName.trim()
if (!trimmedName) {
setNameErrors((prev) => ({
...prev,
[variableId]: 'Variable name cannot be empty',
}))
return
}
if (isDuplicateName(variableId, trimmedName)) {
setNameErrors((prev) => ({
...prev,
[variableId]: 'Two variables cannot have the same name',
}))
return
}
collaborativeUpdateVariable(variableId, 'name', trimmedName)
clearLocalState(variableId)
}
const handleVariableNameKeyDown = (
variableId: string,
e: React.KeyboardEvent<HTMLInputElement>
) => {
if (e.key === 'Enter') {
e.currentTarget.blur()
}
}
const handleAddVariable = () => {
if (!activeWorkflowId) return
const id = collaborativeAddVariable({
collaborativeAddVariable({
name: '',
type: 'string',
value: '',
workflowId: activeWorkflowId,
})
return id
}
const getTypeIcon = (type: VariableType) => {
switch (type) {
case 'plain':
return 'Abc'
case 'number':
return '123'
case 'boolean':
return '0/1'
case 'object':
return '{}'
case 'array':
return '[]'
case 'string':
return 'Abc'
default:
return '?'
}
}
const getPlaceholder = (type: VariableType) => {
switch (type) {
case 'plain':
return 'Plain text value'
case 'number':
return '42'
case 'boolean':
return 'true'
case 'object':
return '{\n "key": "value"\n}'
case 'array':
return '[\n 1,\n 2,\n 3\n]'
case 'string':
return 'Plain text value'
default:
return ''
}
}
const getEditorLanguage = (type: VariableType) => {
switch (type) {
case 'object':
case 'array':
case 'boolean':
case 'number':
case 'plain':
return 'javascript'
default:
return 'javascript'
}
}
const getTypeIcon = (type: VariableType) => TYPE_CONFIG[type]?.icon ?? '?'
const getPlaceholder = (type: VariableType) => TYPE_CONFIG[type]?.placeholder ?? ''
const handleEditorChange = (variable: Variable, newValue: string) => {
collaborativeUpdateVariable(variable.id, 'value', newValue)
}
const handleEditorBlur = (variableId: string) => {
setActiveEditors((prev) => ({
...prev,
[variableId]: false,
}))
}
const handleEditorFocus = (variableId: string) => {
setActiveEditors((prev) => ({
...prev,
[variableId]: true,
}))
}
const formatValue = (variable: Variable) => {
if (variable.value === '') return ''
return typeof variable.value === 'string' ? variable.value : JSON.stringify(variable.value)
}
@@ -213,13 +218,37 @@ export function Variables() {
}
useEffect(() => {
const variableIds = new Set(workflowVariables.map((v) => v.id))
Object.keys(editorRefs.current).forEach((id) => {
if (!workflowVariables.some((v) => v.id === id)) {
if (!variableIds.has(id)) {
delete editorRefs.current[id]
}
})
}, [workflowVariables])
useEffect(() => {
setLocalNames((prev) => {
const variableIds = new Set(workflowVariables.map((v) => v.id))
const updated = { ...prev }
let changed = false
Object.keys(updated).forEach((id) => {
if (!variableIds.has(id)) {
delete updated[id]
changed = true
} else {
const variable = workflowVariables.find((v) => v.id === id)
if (variable && updated[id] === variable.name) {
delete updated[id]
changed = true
}
}
})
return changed ? updated : prev
})
}, [workflowVariables])
return (
<div className='h-full pt-2'>
{workflowVariables.length === 0 ? (
@@ -239,106 +268,95 @@ export function Variables() {
{workflowVariables.map((variable) => (
<div key={variable.id} className='space-y-2'>
{/* Header: Variable name | Variable type | Options dropdown */}
<div className='flex items-center gap-2'>
<Input
className='h-9 flex-1 rounded-lg border-none bg-secondary/50 px-3 font-normal text-sm ring-0 ring-offset-0 placeholder:text-muted-foreground focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0'
placeholder='Variable name'
value={variable.name}
onChange={(e) => handleVariableNameChange(variable.id, e.target.value)}
/>
<div className='space-y-1'>
<div className='flex items-center gap-2'>
<Input
className={cn(
'h-9 flex-1 rounded-lg border-none px-3 font-normal text-sm ring-0 ring-offset-0 placeholder:text-muted-foreground focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0',
nameErrors[variable.id]
? 'border border-red-500 bg-[#F6D2D2] outline-none ring-0 focus:border-red-500 dark:bg-[#442929]'
: 'bg-secondary/50'
)}
placeholder='Variable name'
value={localNames[variable.id] ?? variable.name}
onChange={(e) => handleVariableNameChange(variable.id, e.target.value)}
onBlur={() => handleVariableNameBlur(variable.id)}
onKeyDown={(e) => handleVariableNameKeyDown(variable.id, e)}
/>
{/* Type selector */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className='flex h-9 w-16 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-secondary/50 px-3'>
<span className='font-normal text-sm'>{getTypeIcon(variable.type)}</span>
<ChevronDown className='ml-1 h-3 w-3 text-muted-foreground' />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align='end'
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<DropdownMenuItem
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'plain')}
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
{/* Type selector */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className='flex h-9 w-16 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-secondary/50 px-3'>
<span className='font-normal text-sm'>{getTypeIcon(variable.type)}</span>
<ChevronDown className='ml-1 h-3 w-3 text-muted-foreground' />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align='end'
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<div className='mr-2 w-5 text-center font-[380] text-sm'>Abc</div>
<span className='font-[380]'>Plain</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'number')}
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<div className='mr-2 w-5 text-center font-[380] text-sm'>123</div>
<span className='font-[380]'>Number</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'boolean')}
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<div className='mr-2 w-5 text-center font-[380] text-sm'>0/1</div>
<span className='font-[380]'>Boolean</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'object')}
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<div className='mr-2 w-5 text-center font-[380] text-sm'>{'{}'}</div>
<span className='font-[380]'>Object</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'array')}
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<div className='mr-2 w-5 text-center font-[380] text-sm'>[]</div>
<span className='font-[380]'>Array</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{VARIABLE_TYPES.map((type) => (
<DropdownMenuItem
key={type}
onClick={() => collaborativeUpdateVariable(variable.id, 'type', type)}
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<div className='mr-2 w-5 text-center font-[380] text-sm'>
{TYPE_CONFIG[type].icon}
</div>
<span className='font-[380] capitalize'>{type}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Options dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-9 w-9 shrink-0 rounded-lg bg-secondary/50 p-0 text-muted-foreground hover:bg-secondary/70 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0'
{/* Options dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-9 w-9 shrink-0 rounded-lg bg-secondary/50 p-0 text-muted-foreground hover:bg-secondary/70 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0'
>
<MoreVertical className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align='end'
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<MoreVertical className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align='end'
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<DropdownMenuItem
onClick={() => toggleCollapsed(variable.id)}
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
{(collapsedById[variable.id] ?? false) ? (
<Maximize2 className='mr-2 h-4 w-4 text-muted-foreground' />
) : (
<Minimize2 className='mr-2 h-4 w-4 text-muted-foreground' />
)}
{(collapsedById[variable.id] ?? false) ? 'Expand' : 'Collapse'}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeDuplicateVariable(variable.id)}
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<Copy className='mr-2 h-4 w-4 text-muted-foreground' />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeDeleteVariable(variable.id)}
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-destructive text-sm hover:bg-destructive/10 focus:bg-destructive/10 focus:text-destructive'
>
<Trash className='mr-2 h-4 w-4' />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuItem
onClick={() => toggleCollapsed(variable.id)}
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
{(collapsedById[variable.id] ?? false) ? (
<Maximize2 className='mr-2 h-4 w-4 text-muted-foreground' />
) : (
<Minimize2 className='mr-2 h-4 w-4 text-muted-foreground' />
)}
{(collapsedById[variable.id] ?? false) ? 'Expand' : 'Collapse'}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeDuplicateVariable(variable.id)}
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<Copy className='mr-2 h-4 w-4 text-muted-foreground' />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => collaborativeDeleteVariable(variable.id)}
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-destructive text-sm hover:bg-destructive/10 focus:bg-destructive/10 focus:text-destructive'
>
<Trash className='mr-2 h-4 w-4' />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{nameErrors[variable.id] && (
<div className='mt-1 text-red-400 text-xs'>{nameErrors[variable.id]}</div>
)}
</div>
{/* Value area */}
@@ -379,18 +397,12 @@ export function Variables() {
<Editor
key={`editor-${variable.id}-${variable.type}`}
value={formatValue(variable)}
onValueChange={handleEditorChange.bind(null, variable)}
onBlur={() => handleEditorBlur(variable.id)}
onFocus={() => handleEditorFocus(variable.id)}
onValueChange={(newValue) => handleEditorChange(variable, newValue)}
highlight={(code) =>
// Only apply syntax highlighting for non-basic text types
variable.type === 'plain' || variable.type === 'string'
? code
: highlight(
code,
languages[getEditorLanguage(variable.type)],
getEditorLanguage(variable.type)
)
: highlight(code, languages.javascript, 'javascript')
}
padding={0}
style={{

View File

@@ -7,53 +7,39 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
const logger = createLogger('VariablesStore')
/**
* Check if variable format is valid according to type without modifying it
* Only provides validation feedback - does not change the value
*/
function validateVariable(variable: Variable): string | undefined {
try {
// We only care about the validation result, not the parsed value
switch (variable.type) {
case 'number':
// Check if it's a valid number
if (Number.isNaN(Number(variable.value))) {
return 'Not a valid number'
}
break
case 'boolean':
// Check if it's a valid boolean
if (!/^(true|false)$/i.test(String(variable.value).trim())) {
return 'Expected "true" or "false"'
}
break
case 'object':
// Check if it's a valid JSON object
try {
// Handle both JavaScript and JSON syntax
const valueToEvaluate = String(variable.value).trim()
// Basic security check to prevent arbitrary code execution
if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) {
return 'Not a valid object format'
}
// Use Function constructor to safely evaluate the object expression
// This handles both JSON and JS object literal syntax
const parsed = new Function(`return ${valueToEvaluate}`)()
// Verify it's actually an object (not array or null)
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return 'Not a valid object'
}
return undefined // Valid object
return undefined
} catch (e) {
logger.error('Object parsing error:', e)
return 'Invalid object syntax'
}
case 'array':
// Check if it's a valid JSON array
try {
const parsed = JSON.parse(String(variable.value))
if (!Array.isArray(parsed)) {
@@ -70,23 +56,16 @@ function validateVariable(variable: Variable): string | undefined {
}
}
/**
* Migrates a variable from 'string' type to 'plain' type
* Handles the value conversion appropriately
*/
function migrateStringToPlain(variable: Variable): Variable {
if (variable.type !== 'string') {
return variable
}
// Convert string type to plain
const updated = {
...variable,
type: 'plain' as const,
}
// For plain text, we want to preserve values exactly as they are,
// including any quote characters that may be part of the text
return updated
}
@@ -128,12 +107,9 @@ export const useVariablesStore = create<VariablesStore>()(
addVariable: (variable, providedId?: string) => {
const id = providedId || crypto.randomUUID()
// Get variables for this workflow
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
// Auto-generate variable name if not provided or it's a default pattern name
if (!variable.name || /^variable\d+$/.test(variable.name)) {
// Find the highest existing Variable N number
const existingNumbers = workflowVariables
.map((v) => {
const match = v.name.match(/^variable(\d+)$/)
@@ -141,28 +117,23 @@ export const useVariablesStore = create<VariablesStore>()(
})
.filter((n) => !Number.isNaN(n))
// Set new number to max + 1, or 1 if none exist
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
variable.name = `variable${nextNumber}`
}
// Ensure name uniqueness within the workflow
let uniqueName = variable.name
let nameIndex = 1
// Check if name already exists in this workflow
while (workflowVariables.some((v) => v.name === uniqueName)) {
uniqueName = `${variable.name} (${nameIndex})`
nameIndex++
}
// Check for type conversion - only for backward compatibility
if (variable.type === 'string') {
variable.type = 'plain'
}
// Create the new variable with empty value
const newVariable: Variable = {
id,
workflowId: variable.workflowId,
@@ -172,7 +143,6 @@ export const useVariablesStore = create<VariablesStore>()(
validationError: undefined,
}
// Check for validation errors without modifying the value
const validationError = validateVariable(newVariable)
if (validationError) {
newVariable.validationError = validationError
@@ -192,69 +162,44 @@ export const useVariablesStore = create<VariablesStore>()(
set((state) => {
if (!state.variables[id]) return state
// If name is being updated, ensure it's unique
if (update.name !== undefined) {
const oldVariable = state.variables[id]
const oldVariableName = oldVariable.name
const workflowId = oldVariable.workflowId
const workflowVariables = Object.values(state.variables).filter(
(v) => v.workflowId === workflowId && v.id !== id
)
const newName = update.name.trim()
let uniqueName = update.name
let nameIndex = 1
// Only check uniqueness for non-empty names
// Empty names don't need to be unique as they're temporary states
if (uniqueName.trim() !== '') {
// Check if name already exists in this workflow
while (workflowVariables.some((v) => v.name === uniqueName)) {
uniqueName = `${update.name} (${nameIndex})`
nameIndex++
}
}
// Always update references in subblocks when name changes, even if empty
// This ensures references are updated even when name is completely cleared
if (uniqueName !== oldVariableName) {
// Update references in subblock store
if (!newName) {
update = { ...update }
update.name = undefined
} else if (newName !== oldVariableName) {
const subBlockStore = useSubBlockStore.getState()
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (activeWorkflowId) {
// Get the workflow values for the active workflow
const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
const updatedWorkflowValues = { ...workflowValues }
// Loop through blocks
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
// Loop through subblocks and update references
Object.entries(blockValues as Record<string, any>).forEach(
([subBlockId, value]) => {
const oldVarName = oldVariableName.replace(/\s+/g, '').toLowerCase()
const newVarName = uniqueName.replace(/\s+/g, '').toLowerCase()
const newVarName = newName.replace(/\s+/g, '').toLowerCase()
const regex = new RegExp(`<variable\.${oldVarName}>`, 'gi')
// Use a recursive function to handle all object types
updatedWorkflowValues[blockId][subBlockId] = updateReferences(
value,
regex,
`<variable.${newVarName}>`
)
// Helper function to recursively update references in any data structure
function updateReferences(value: any, regex: RegExp, replacement: string): any {
// Handle string values
if (typeof value === 'string') {
return regex.test(value) ? value.replace(regex, replacement) : value
}
// Handle arrays
if (Array.isArray(value)) {
return value.map((item) => updateReferences(item, regex, replacement))
}
// Handle objects
if (value !== null && typeof value === 'object') {
const result = { ...value }
for (const key in result) {
@@ -263,14 +208,12 @@ export const useVariablesStore = create<VariablesStore>()(
return result
}
// Return unchanged for other types
return value
}
}
)
})
// Update the subblock store with the new values
useSubBlockStore.setState({
workflowValues: {
...subBlockStore.workflowValues,
@@ -279,26 +222,19 @@ export const useVariablesStore = create<VariablesStore>()(
})
}
}
// Update with unique name
update = { ...update, name: uniqueName }
}
// If type is being updated to 'string', convert it to 'plain' instead
if (update.type === 'string') {
update = { ...update, type: 'plain' }
}
// Create updated variable to check for validation
const updatedVariable: Variable = {
...state.variables[id],
...update,
validationError: undefined, // Initialize property to be updated later
validationError: undefined,
}
// If the type or value changed, check for validation errors
if (update.type || update.value !== undefined) {
// Only add validation feedback - never modify the value
updatedVariable.validationError = validateVariable(updatedVariable)
}
@@ -329,13 +265,11 @@ export const useVariablesStore = create<VariablesStore>()(
const variable = state.variables[id]
const newId = providedId || crypto.randomUUID()
// Ensure the duplicated name is unique
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
const baseName = `${variable.name} (copy)`
let uniqueName = baseName
let nameIndex = 1
// Check if name already exists in this workflow
while (workflowVariables.some((v) => v.name === uniqueName)) {
uniqueName = `${baseName} (${nameIndex})`
nameIndex++