[ISSUE-1] Refactor NPC forms to modular node-based system #2
211
src/components/nodes/VariablesNode.tsx
Normal file
211
src/components/nodes/VariablesNode.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { Code, Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { VariablesNodeData } from '../../types/nodes';
|
||||
import type { MoLangConfigVariable } from '../../types/npc';
|
||||
|
||||
export const VariablesNode = memo(({ data }: NodeProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const nodeData = data as VariablesNodeData;
|
||||
|
||||
const handleChange = (variables: MoLangConfigVariable[]) => {
|
||||
Object.assign(nodeData, { configVariables: variables });
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
const newVariable: MoLangConfigVariable = {
|
||||
variableName: `var_${Date.now()}`,
|
||||
displayName: 'New Variable',
|
||||
description: 'Description',
|
||||
type: 'NUMBER',
|
||||
defaultValue: 0,
|
||||
};
|
||||
handleChange([...(nodeData.configVariables || []), newVariable]);
|
||||
};
|
||||
|
||||
const removeVariable = (index: number) => {
|
||||
const newVariables = [...(nodeData.configVariables || [])];
|
||||
newVariables.splice(index, 1);
|
||||
handleChange(newVariables);
|
||||
};
|
||||
|
||||
const updateVariable = (
|
||||
index: number,
|
||||
field: keyof MoLangConfigVariable,
|
||||
value: string | number | boolean
|
||||
) => {
|
||||
const newVariables = [...(nodeData.configVariables || [])];
|
||||
newVariables[index] = { ...newVariables[index], [field]: value };
|
||||
handleChange(newVariables);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl border-2 border-yellow-500 min-w-[350px]">
|
||||
{/* Node Header */}
|
||||
<div className="bg-yellow-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Config Variables</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white hover:bg-yellow-600 rounded p-1 transition-colors"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-yellow-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Node Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-700">
|
||||
Variables ({nodeData.configVariables?.length || 0})
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addVariable}
|
||||
className="inline-flex items-center px-2 py-1 text-xs font-medium rounded text-yellow-700 bg-yellow-100 hover:bg-yellow-200 transition-colors"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-80 overflow-y-auto">
|
||||
{(!nodeData.configVariables || nodeData.configVariables.length === 0) && (
|
||||
<div className="text-xs text-gray-500 italic py-4 text-center">
|
||||
No variables defined
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeData.configVariables?.map((variable, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-lg p-3 space-y-2 bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-700">
|
||||
Variable {index + 1}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeVariable(index)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Variable Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={variable.variableName}
|
||||
onChange={(e) =>
|
||||
updateVariable(index, 'variableName', e.target.value)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
placeholder="my_variable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={variable.displayName}
|
||||
onChange={(e) =>
|
||||
updateVariable(index, 'displayName', e.target.value)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
placeholder="My Variable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
value={variable.type}
|
||||
onChange={(e) =>
|
||||
updateVariable(
|
||||
index,
|
||||
'type',
|
||||
e.target.value as 'NUMBER' | 'TEXT' | 'BOOLEAN'
|
||||
)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
>
|
||||
<option value="NUMBER">Number</option>
|
||||
<option value="TEXT">Text</option>
|
||||
<option value="BOOLEAN">Boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Default Value
|
||||
</label>
|
||||
{variable.type === 'BOOLEAN' ? (
|
||||
<select
|
||||
value={variable.defaultValue.toString()}
|
||||
onChange={(e) =>
|
||||
updateVariable(
|
||||
index,
|
||||
'defaultValue',
|
||||
e.target.value === 'true'
|
||||
)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
>
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type={variable.type === 'NUMBER' ? 'number' : 'text'}
|
||||
value={String(variable.defaultValue)}
|
||||
onChange={(e) =>
|
||||
updateVariable(
|
||||
index,
|
||||
'defaultValue',
|
||||
variable.type === 'NUMBER'
|
||||
? parseFloat(e.target.value) || 0
|
||||
: e.target.value
|
||||
)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-yellow-500 border-2 border-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
VariablesNode.displayName = 'VariablesNode';
|
||||
Reference in New Issue
Block a user