feat: variable node
This commit is contained in:
133
src/components/graph/VariableNode.tsx
Normal file
133
src/components/graph/VariableNode.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { memo, useCallback, useContext } from 'react'
|
||||
import FlowContext from '../../lib/flowContext'
|
||||
import {
|
||||
BaseNode,
|
||||
BaseNodeContent,
|
||||
BaseNodeHeader,
|
||||
BaseNodeHeaderTitle,
|
||||
} from './BaseNode'
|
||||
import { OutputHandle } from './NodeHandles'
|
||||
import { Variable } from 'lucide-react'
|
||||
|
||||
type ValueType = 'string' | 'number' | 'boolean'
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
data: {
|
||||
value?: string | number | boolean
|
||||
valueType?: ValueType
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_BY_TYPE: Record<ValueType, string | number | boolean> = {
|
||||
string: '',
|
||||
number: 0,
|
||||
boolean: false,
|
||||
}
|
||||
|
||||
function coerceValue(raw: string, valueType: ValueType): string | number | boolean {
|
||||
switch (valueType) {
|
||||
case 'number': {
|
||||
const n = Number(raw)
|
||||
return Number.isNaN(n) ? 0 : n
|
||||
}
|
||||
case 'boolean':
|
||||
return /^(1|true|yes|on)$/i.test(raw.trim())
|
||||
default:
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
export const VariableNode = memo(function VariableNode({ id, data }: Props) {
|
||||
const ctx = useContext(FlowContext)
|
||||
const setNodes = ctx?.setNodes
|
||||
|
||||
const valueType: ValueType = data?.valueType ?? 'string'
|
||||
const value = data?.value ?? DEFAULT_BY_TYPE[valueType]
|
||||
const displayValue = typeof value === 'string' ? value : String(value)
|
||||
|
||||
const updateData = useCallback(
|
||||
(updates: { value?: string | number | boolean; valueType?: ValueType }) => {
|
||||
if (!setNodes) return
|
||||
setNodes((nds: any[]) =>
|
||||
nds.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, ...updates } } : n
|
||||
)
|
||||
)
|
||||
},
|
||||
[id, setNodes]
|
||||
)
|
||||
|
||||
const onTypeChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const nextType = e.target.value as ValueType
|
||||
const raw = typeof value === 'boolean' ? (value ? 'true' : 'false') : String(value)
|
||||
const nextValue = coerceValue(raw, nextType)
|
||||
updateData({ valueType: nextType, value: nextValue })
|
||||
},
|
||||
[value, updateData]
|
||||
)
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const raw = e.target.type === 'checkbox' ? (e.target.checked ? 'true' : 'false') : e.target.value
|
||||
const nextValue = coerceValue(raw, valueType)
|
||||
updateData({ value: nextValue })
|
||||
},
|
||||
[valueType, updateData]
|
||||
)
|
||||
|
||||
return (
|
||||
<BaseNode className="w-56">
|
||||
<BaseNodeHeader className="border-b">
|
||||
<Variable className="size-4" />
|
||||
<BaseNodeHeaderTitle>{id}</BaseNodeHeaderTitle>
|
||||
</BaseNodeHeader>
|
||||
|
||||
<BaseNodeContent className="gap-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">Type</label>
|
||||
<select
|
||||
value={valueType}
|
||||
onChange={onTypeChange}
|
||||
className="w-full rounded border border-input bg-background px-2 py-1.5 text-sm"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">Value</label>
|
||||
{valueType === 'boolean' ? (
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value === true}
|
||||
onChange={onValueChange}
|
||||
className="rounded border-input"
|
||||
/>
|
||||
{value ? 'true' : 'false'}
|
||||
</label>
|
||||
) : (
|
||||
<input
|
||||
type={valueType === 'number' ? 'number' : 'text'}
|
||||
value={valueType === 'number' ? (value as number) : displayValue}
|
||||
onChange={onValueChange}
|
||||
className="w-full rounded border border-input bg-background px-2 py-1.5 text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Use in config: <code className="rounded bg-muted px-0.5">prop: ${{id}}</code>
|
||||
</p>
|
||||
</BaseNodeContent>
|
||||
|
||||
<OutputHandle id="out" />
|
||||
</BaseNode>
|
||||
)
|
||||
})
|
||||
|
||||
VariableNode.displayName = 'VariableNode'
|
||||
|
||||
export default VariableNode
|
||||
Reference in New Issue
Block a user