141 lines
4.9 KiB
TypeScript
141 lines
4.9 KiB
TypeScript
import React, { memo, useCallback, useContext } from 'react'
|
|
import FlowContext from '../../lib/flowContext'
|
|
import {
|
|
BaseNode,
|
|
BaseNodeContent,
|
|
BaseNodeFooter,
|
|
BaseNodeFooterText,
|
|
BaseNodeHeaderRow,
|
|
} from './BaseNode'
|
|
import { Input } from '../ui/input'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
import { Switch } from '../ui/switch'
|
|
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(
|
|
(nextType: string) => {
|
|
const type = nextType as ValueType
|
|
const raw = typeof value === 'boolean' ? (value ? 'true' : 'false') : String(value)
|
|
const nextValue = coerceValue(raw, type)
|
|
updateData({ valueType: type, value: nextValue })
|
|
},
|
|
[value, updateData]
|
|
)
|
|
|
|
const onValueChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const raw = e.target.value
|
|
const nextValue = coerceValue(raw, valueType)
|
|
updateData({ value: nextValue })
|
|
},
|
|
[valueType, updateData]
|
|
)
|
|
|
|
const onBooleanChange = useCallback(
|
|
(checked: boolean) => {
|
|
updateData({ value: checked })
|
|
},
|
|
[updateData]
|
|
)
|
|
|
|
return (
|
|
<BaseNode className="min-w-56 min-h-[180px]" handles={<OutputHandle id="out" />}>
|
|
<BaseNodeHeaderRow icon={<Variable className="size-4" />} title={id} />
|
|
|
|
<BaseNodeContent className="gap-2 p-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs font-medium text-muted-foreground">Type</label>
|
|
<Select value={valueType} onValueChange={onTypeChange}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="string">String</SelectItem>
|
|
<SelectItem value="number">Number</SelectItem>
|
|
<SelectItem value="boolean">Boolean</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs font-medium text-muted-foreground">Value</label>
|
|
{valueType === 'boolean' ? (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Switch
|
|
checked={value === true}
|
|
onCheckedChange={onBooleanChange}
|
|
/>
|
|
<span className="text-muted-foreground">{value ? 'true' : 'false'}</span>
|
|
</div>
|
|
) : (
|
|
<Input
|
|
type={valueType === 'number' ? 'number' : 'text'}
|
|
value={valueType === 'number' ? (value as number) : displayValue}
|
|
onChange={onValueChange}
|
|
/>
|
|
)}
|
|
</div>
|
|
</BaseNodeContent>
|
|
|
|
<BaseNodeFooter>
|
|
<BaseNodeFooterText>
|
|
Use in config: <code className="rounded bg-muted px-0.5">prop: {'${'}{id}{'}'}</code>
|
|
</BaseNodeFooterText>
|
|
</BaseNodeFooter>
|
|
</BaseNode>
|
|
)
|
|
})
|
|
|
|
VariableNode.displayName = 'VariableNode'
|
|
|
|
export default VariableNode
|