feat: [US-024] - Add/remove choice options
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
200b998485
commit
9082e15949
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useCallback, ChangeEvent } from 'react'
|
import { useCallback, ChangeEvent } from 'react'
|
||||||
import { Handle, Position, NodeProps, useReactFlow } from 'reactflow'
|
import { Handle, Position, NodeProps, useReactFlow } from 'reactflow'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
type ChoiceOption = {
|
type ChoiceOption = {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -13,6 +14,9 @@ type ChoiceNodeData = {
|
||||||
options: ChoiceOption[]
|
options: ChoiceOption[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIN_OPTIONS = 2
|
||||||
|
const MAX_OPTIONS = 6
|
||||||
|
|
||||||
export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
|
export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
|
||||||
const { setNodes } = useReactFlow()
|
const { setNodes } = useReactFlow()
|
||||||
|
|
||||||
|
|
@ -50,6 +54,48 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
|
||||||
[id, setNodes]
|
[id, setNodes]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const addOption = useCallback(() => {
|
||||||
|
if (data.options.length >= MAX_OPTIONS) return
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) =>
|
||||||
|
node.id === id
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
options: [
|
||||||
|
...node.data.options,
|
||||||
|
{ id: nanoid(), label: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, [id, data.options.length, setNodes])
|
||||||
|
|
||||||
|
const removeOption = useCallback(
|
||||||
|
(optionId: string) => {
|
||||||
|
if (data.options.length <= MIN_OPTIONS) return
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) =>
|
||||||
|
node.id === id
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
options: node.data.options.filter(
|
||||||
|
(opt: ChoiceOption) => opt.id !== optionId
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[id, data.options.length, setNodes]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-[220px] rounded-lg border-2 border-green-500 bg-green-50 p-3 shadow-md dark:border-green-400 dark:bg-green-950">
|
<div className="min-w-[220px] rounded-lg border-2 border-green-500 bg-green-50 p-3 shadow-md dark:border-green-400 dark:bg-green-950">
|
||||||
<Handle
|
<Handle
|
||||||
|
|
@ -73,14 +119,23 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{data.options.map((option, index) => (
|
{data.options.map((option, index) => (
|
||||||
<div key={option.id} className="relative">
|
<div key={option.id} className="relative flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => updateOptionLabel(option.id, e.target.value)}
|
onChange={(e) => updateOptionLabel(option.id, e.target.value)}
|
||||||
placeholder={`Option ${index + 1}`}
|
placeholder={`Option ${index + 1}`}
|
||||||
className="w-full rounded border border-green-300 bg-white px-2 py-1 pr-3 text-sm focus:border-green-500 focus:outline-none focus:ring-1 focus:ring-green-500 dark:border-green-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
className="flex-1 rounded border border-green-300 bg-white px-2 py-1 text-sm focus:border-green-500 focus:outline-none focus:ring-1 focus:ring-green-500 dark:border-green-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeOption(option.id)}
|
||||||
|
disabled={data.options.length <= MIN_OPTIONS}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-red-500 hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:hover:bg-red-900/30"
|
||||||
|
title="Remove option"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
|
|
@ -93,6 +148,16 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addOption}
|
||||||
|
disabled={data.options.length >= MAX_OPTIONS}
|
||||||
|
className="mt-2 flex w-full items-center justify-center gap-1 rounded border border-dashed border-green-400 py-1 text-sm text-green-600 hover:bg-green-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:border-green-500 dark:text-green-400 dark:hover:bg-green-900/30"
|
||||||
|
title="Add option"
|
||||||
|
>
|
||||||
|
+ Add Option
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue