From fda09038722721c1a004c6e476a7cbb9789af50d Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Thu, 22 Jan 2026 18:08:37 -0300 Subject: [PATCH] feat: [US-031] - Condition editor modal Co-Authored-By: Claude Opus 4.5 --- .../editor/[projectId]/FlowchartEditor.tsx | 86 ++++++++- src/components/editor/ConditionEditor.tsx | 166 ++++++++++++++++++ 2 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 src/components/editor/ConditionEditor.tsx diff --git a/src/app/editor/[projectId]/FlowchartEditor.tsx b/src/app/editor/[projectId]/FlowchartEditor.tsx index 6a7abd7..3ecefad 100644 --- a/src/app/editor/[projectId]/FlowchartEditor.tsx +++ b/src/app/editor/[projectId]/FlowchartEditor.tsx @@ -25,7 +25,8 @@ import DialogueNode from '@/components/editor/nodes/DialogueNode' import ChoiceNode from '@/components/editor/nodes/ChoiceNode' import VariableNode from '@/components/editor/nodes/VariableNode' import ContextMenu, { ContextMenuType } from '@/components/editor/ContextMenu' -import type { FlowchartData, FlowchartNode, FlowchartEdge } from '@/types/flowchart' +import ConditionEditor from '@/components/editor/ConditionEditor' +import type { FlowchartData, FlowchartNode, FlowchartEdge, Condition } from '@/types/flowchart' type ContextMenuState = { x: number @@ -35,6 +36,11 @@ type ContextMenuState = { edgeId?: string } | null +type ConditionEditorState = { + edgeId: string + condition?: Condition +} | null + type FlowchartEditorProps = { projectId: string initialData: FlowchartData @@ -81,6 +87,7 @@ function FlowchartEditorInner({ initialData }: FlowchartEditorProps) { const { getViewport, screenToFlowPosition } = useReactFlow() const [contextMenu, setContextMenu] = useState(null) + const [conditionEditor, setConditionEditor] = useState(null) const [nodes, setNodes, onNodesChange] = useNodesState( toReactFlowNodes(initialData.nodes) @@ -290,11 +297,69 @@ function FlowchartEditorInner({ initialData }: FlowchartEditorProps) { setEdges((edges) => edges.filter((e) => e.id !== contextMenu.edgeId)) }, [contextMenu, setEdges]) - // Add condition to edge (will be implemented in US-031) + // Open condition editor for an edge + const openConditionEditor = useCallback( + (edgeId: string) => { + const edge = edges.find((e) => e.id === edgeId) + if (!edge) return + setConditionEditor({ + edgeId, + condition: edge.data?.condition, + }) + }, + [edges] + ) + + // Add condition to edge (opens ConditionEditor modal) const handleAddCondition = useCallback(() => { - // TODO: Implement in US-031 - will open ConditionEditor modal - console.log('Add condition to edge:', contextMenu?.edgeId) - }, [contextMenu]) + if (!contextMenu?.edgeId) return + openConditionEditor(contextMenu.edgeId) + }, [contextMenu, openConditionEditor]) + + // Handle double-click on edge to open condition editor + const onEdgeDoubleClick = useCallback( + (_event: React.MouseEvent, edge: Edge) => { + openConditionEditor(edge.id) + }, + [openConditionEditor] + ) + + // Save condition to edge + const handleSaveCondition = useCallback( + (edgeId: string, condition: Condition) => { + setEdges((eds) => + eds.map((edge) => + edge.id === edgeId + ? { ...edge, data: { ...edge.data, condition } } + : edge + ) + ) + setConditionEditor(null) + }, + [setEdges] + ) + + // Remove condition from edge + const handleRemoveCondition = useCallback( + (edgeId: string) => { + setEdges((eds) => + eds.map((edge) => { + if (edge.id !== edgeId) return edge + // Remove condition from data + const newData = { ...edge.data } + delete newData.condition + return { ...edge, data: newData } + }) + ) + setConditionEditor(null) + }, + [setEdges] + ) + + // Close condition editor + const closeConditionEditor = useCallback(() => { + setConditionEditor(null) + }, []) return (
@@ -318,6 +383,7 @@ function FlowchartEditorInner({ initialData }: FlowchartEditorProps) { onPaneContextMenu={onPaneContextMenu} onNodeContextMenu={onNodeContextMenu} onEdgeContextMenu={onEdgeContextMenu} + onEdgeDoubleClick={onEdgeDoubleClick} deleteKeyCode={['Delete', 'Backspace']} fitView > @@ -341,6 +407,16 @@ function FlowchartEditorInner({ initialData }: FlowchartEditorProps) { onAddCondition={handleAddCondition} /> )} + + {conditionEditor && ( + + )}
) } diff --git a/src/components/editor/ConditionEditor.tsx b/src/components/editor/ConditionEditor.tsx new file mode 100644 index 0000000..045af92 --- /dev/null +++ b/src/components/editor/ConditionEditor.tsx @@ -0,0 +1,166 @@ +'use client' + +import { useState, useCallback, useEffect } from 'react' +import type { Condition } from '@/types/flowchart' + +type ConditionEditorProps = { + edgeId: string + condition?: Condition + onSave: (edgeId: string, condition: Condition) => void + onRemove: (edgeId: string) => void + onCancel: () => void +} + +const OPERATORS: Condition['operator'][] = ['>', '<', '==', '>=', '<=', '!='] + +export default function ConditionEditor({ + edgeId, + condition, + onSave, + onRemove, + onCancel, +}: ConditionEditorProps) { + const [variableName, setVariableName] = useState(condition?.variableName ?? '') + const [operator, setOperator] = useState(condition?.operator ?? '==') + const [value, setValue] = useState(condition?.value ?? 0) + + // Close on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onCancel() + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [onCancel]) + + const handleSave = useCallback(() => { + if (!variableName.trim()) return + onSave(edgeId, { + variableName: variableName.trim(), + operator, + value, + }) + }, [edgeId, variableName, operator, value, onSave]) + + const handleRemove = useCallback(() => { + onRemove(edgeId) + }, [edgeId, onRemove]) + + const hasExistingCondition = !!condition + + return ( +
+
e.stopPropagation()} + > +

+ {hasExistingCondition ? 'Edit Condition' : 'Add Condition'} +

+ +
+ {/* Variable Name Input */} +
+ + setVariableName(e.target.value)} + placeholder="e.g., score, health, affection" + autoFocus + className="w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-100 dark:placeholder-zinc-500" + /> +
+ + {/* Operator Dropdown */} +
+ + +
+ + {/* Value Number Input */} +
+ + setValue(parseFloat(e.target.value) || 0)} + className="w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-100" + /> +
+ + {/* Preview */} + {variableName.trim() && ( +
+ + Condition: {variableName.trim()} {operator} {value} + +
+ )} +
+ + {/* Action Buttons */} +
+
+ {hasExistingCondition && ( + + )} +
+
+ + +
+
+
+
+ ) +}