feat: [US-059] - Variable node variable dropdown

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gustavo Henrique Santos Souza de Miranda
2026-01-23 10:25:57 -03:00
co-authored by Claude Opus 4.5
parent 548f3743d1
commit 5493adf44a
3 changed files with 237 additions and 67 deletions
+24
View File
@@ -0,0 +1,24 @@
'use client'
import { createContext, useContext } from 'react'
import type { Character, Variable } from '@/types/flowchart'
type EditorContextValue = {
characters: Character[]
onAddCharacter: (name: string, color: string) => string // returns new character id
variables: Variable[]
onAddVariable: (name: string, type: 'numeric' | 'string' | 'boolean', initialValue: number | string | boolean) => string // returns new variable id
}
const EditorContext = createContext<EditorContextValue>({
characters: [],
onAddCharacter: () => '',
variables: [],
onAddVariable: () => '',
})
export const EditorProvider = EditorContext.Provider
export function useEditorContext() {
return useContext(EditorContext)
}
+148 -30
View File
@@ -1,23 +1,52 @@
'use client'
import { useCallback, ChangeEvent } from 'react'
import { useCallback, useMemo, useState, ChangeEvent } from 'react'
import { Handle, Position, NodeProps, useReactFlow } from 'reactflow'
import Combobox from '@/components/editor/Combobox'
import type { ComboboxItem } from '@/components/editor/Combobox'
import { useEditorContext } from '@/components/editor/EditorContext'
type VariableNodeData = {
variableName: string
variableId?: string
operation: 'set' | 'add' | 'subtract'
value: number
}
export default function VariableNode({ id, data }: NodeProps<VariableNodeData>) {
const { setNodes } = useReactFlow()
const { variables, onAddVariable } = useEditorContext()
const updateVariableName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const [showAddForm, setShowAddForm] = useState(false)
const [newName, setNewName] = useState('')
const [newType, setNewType] = useState<'numeric' | 'string' | 'boolean'>('numeric')
const variableItems: ComboboxItem[] = useMemo(
() =>
variables.map((v) => ({
id: v.id,
label: v.name,
badge: v.type,
})),
[variables]
)
const selectedVariable = useMemo(() => {
if (!data.variableId) return undefined
return variables.find((v) => v.id === data.variableId)
}, [data.variableId, variables])
const hasInvalidReference = useMemo(() => {
if (!data.variableId) return false
return !variables.some((v) => v.id === data.variableId)
}, [data.variableId, variables])
const updateNodeData = useCallback(
(updates: Partial<VariableNodeData>) => {
setNodes((nodes) =>
nodes.map((node) =>
node.id === id
? { ...node, data: { ...node.data, variableName: e.target.value } }
? { ...node, data: { ...node.data, ...updates } }
: node
)
)
@@ -25,35 +54,69 @@ export default function VariableNode({ id, data }: NodeProps<VariableNodeData>)
[id, setNodes]
)
const handleVariableSelect = useCallback(
(variableId: string) => {
const variable = variables.find((v) => v.id === variableId)
const updates: Partial<VariableNodeData> = {
variableId,
variableName: variable?.name || '',
}
// Reset operation to 'set' if current operation is not valid for the variable's type
if (variable && variable.type !== 'numeric' && (data.operation === 'add' || data.operation === 'subtract')) {
updates.operation = 'set'
}
updateNodeData(updates)
},
[variables, data.operation, updateNodeData]
)
const handleAddNew = useCallback(() => {
setShowAddForm(true)
setNewName('')
setNewType('numeric')
}, [])
const handleSubmitNew = useCallback(() => {
if (!newName.trim()) return
const defaultValue = newType === 'numeric' ? 0 : newType === 'boolean' ? false : ''
const newId = onAddVariable(newName.trim(), newType, defaultValue)
updateNodeData({
variableId: newId,
variableName: newName.trim(),
})
setShowAddForm(false)
}, [newName, newType, onAddVariable, updateNodeData])
const handleCancelNew = useCallback(() => {
setShowAddForm(false)
}, [])
const updateOperation = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
setNodes((nodes) =>
nodes.map((node) =>
node.id === id
? { ...node, data: { ...node.data, operation: e.target.value as 'set' | 'add' | 'subtract' } }
: node
)
)
updateNodeData({ operation: e.target.value as 'set' | 'add' | 'subtract' })
},
[id, setNodes]
[updateNodeData]
)
const updateValue = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0
setNodes((nodes) =>
nodes.map((node) =>
node.id === id
? { ...node, data: { ...node.data, value } }
: node
)
)
updateNodeData({ value })
},
[id, setNodes]
[updateNodeData]
)
// Filter operations based on selected variable type
const isNumeric = !selectedVariable || selectedVariable.type === 'numeric'
return (
<div className="min-w-[200px] rounded-lg border-2 border-orange-500 bg-orange-50 p-3 shadow-md dark:border-orange-400 dark:bg-orange-950">
<div
className={`min-w-[200px] rounded-lg border-2 ${
hasInvalidReference
? 'border-orange-500 dark:border-orange-400'
: 'border-orange-500 dark:border-orange-400'
} bg-orange-50 p-3 shadow-md dark:bg-orange-950`}
>
<Handle
type="target"
position={Position.Top}
@@ -65,13 +128,68 @@ export default function VariableNode({ id, data }: NodeProps<VariableNodeData>)
Variable
</div>
<input
type="text"
value={data.variableName || ''}
onChange={updateVariableName}
placeholder="variableName"
className="mb-2 w-full rounded border border-orange-300 bg-white px-2 py-1 text-sm focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 dark:border-orange-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
/>
<div className={`mb-2 ${hasInvalidReference ? 'rounded ring-2 ring-orange-500' : ''}`}>
<Combobox
items={variableItems}
value={data.variableId}
onChange={handleVariableSelect}
placeholder="Select variable..."
onAddNew={handleAddNew}
/>
{hasInvalidReference && (
<div className="mt-1 text-xs text-orange-600 dark:text-orange-400">
Variable not found
</div>
)}
</div>
{showAddForm && (
<div className="mb-2 rounded border border-orange-300 bg-white p-2 dark:border-orange-600 dark:bg-zinc-800">
<div className="mb-1 text-xs font-medium text-zinc-600 dark:text-zinc-300">
New variable
</div>
<div className="flex items-center gap-1.5">
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="Name"
className="w-full rounded border border-zinc-300 bg-white px-2 py-0.5 text-sm focus:border-orange-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-700 dark:text-white dark:placeholder-zinc-400"
onKeyDown={(e) => {
if (e.key === 'Enter') handleSubmitNew()
if (e.key === 'Escape') handleCancelNew()
}}
autoFocus
/>
</div>
<div className="mt-1.5 flex items-center gap-1.5">
<select
value={newType}
onChange={(e) => setNewType(e.target.value as 'numeric' | 'string' | 'boolean')}
className="rounded border border-zinc-300 bg-white px-2 py-0.5 text-sm focus:border-orange-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-700 dark:text-white"
>
<option value="numeric">numeric</option>
<option value="string">string</option>
<option value="boolean">boolean</option>
</select>
</div>
<div className="mt-1.5 flex justify-end gap-1">
<button
onClick={handleCancelNew}
className="rounded px-2 py-0.5 text-xs text-zinc-500 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-700"
>
Cancel
</button>
<button
onClick={handleSubmitNew}
disabled={!newName.trim()}
className="rounded bg-orange-600 px-2 py-0.5 text-xs text-white hover:bg-orange-700 disabled:opacity-50"
>
Add
</button>
</div>
</div>
)}
<div className="mb-2 flex gap-2">
<select
@@ -80,8 +198,8 @@ export default function VariableNode({ id, data }: NodeProps<VariableNodeData>)
className="flex-1 rounded border border-orange-300 bg-white px-2 py-1 text-sm focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 dark:border-orange-600 dark:bg-zinc-800 dark:text-white"
>
<option value="set">set</option>
<option value="add">add</option>
<option value="subtract">subtract</option>
{isNumeric && <option value="add">add</option>}
{isNumeric && <option value="subtract">subtract</option>}
</select>
<input