Improve tree editor modal UX: cancel/save and inline node naming

Change 1: Add Cancel button and defer saving until Done is clicked
- NodeEditorModal now uses local draft state instead of updating store directly
- Cancel button discards changes; Done button commits to store
- If editing a brand new node, Cancel deletes it entirely
- NodeList tracks isEditingNewNode to pass to modal

Change 2: Inline node naming when creating from NodePicker dropdown
- Selecting "+ New Decision/Action/Solution" shows inline title input
- User enters title before node is created (Enter to create, Escape to cancel)
- Node appears in dropdown with human-readable title immediately

Change 3: Improved dropdown labels
- Format changed from "UUID (UUID...)" to "Title (UUID...)"
- Untitled nodes show "Untitled Question" or "Untitled {type}"
- Root node shows "Root Question (root)" when empty

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-01-30 01:01:23 -05:00
parent adcaf2f4fe
commit 1d5ba598ca
4 changed files with 244 additions and 81 deletions

View File

@@ -1,3 +1,4 @@
import { useState, useEffect, useCallback } from 'react'
import { Modal } from '@/components/common/Modal'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { NodeFormDecision } from './NodeFormDecision'
@@ -8,14 +9,39 @@ import type { TreeStructure } from '@/types'
interface NodeEditorModalProps {
node: TreeStructure
onClose: () => void
/** If true, this is a brand new node - cancel will delete it entirely */
isNewNode?: boolean
}
export function NodeEditorModal({ node, onClose }: NodeEditorModalProps) {
const { updateNode, validationErrors } = useTreeEditorStore()
export function NodeEditorModal({ node, onClose, isNewNode = false }: NodeEditorModalProps) {
const { updateNode, deleteNode, validationErrors } = useTreeEditorStore()
const nodeErrors = validationErrors.filter(e => e.nodeId === node.id)
const handleUpdate = (updates: Partial<TreeStructure>) => {
updateNode(node.id, updates)
// Local draft state - changes are NOT persisted until "Done" is clicked
const [draft, setDraft] = useState<TreeStructure>(() => structuredClone(node))
// Reset draft when node changes (e.g., external update)
useEffect(() => {
setDraft(structuredClone(node))
}, [node.id]) // Only reset when switching to a different node
const handleUpdate = useCallback((updates: Partial<TreeStructure>) => {
setDraft(prev => ({ ...prev, ...updates }))
}, [])
const handleSave = () => {
// Commit all draft changes to the store
updateNode(node.id, draft)
onClose()
}
const handleCancel = () => {
if (isNewNode) {
// Delete the unsaved new node entirely
deleteNode(node.id)
}
// Discard changes and close (draft is just thrown away)
onClose()
}
const getTitle = () => {
@@ -32,10 +58,17 @@ export function NodeEditorModal({ node, onClose }: NodeEditorModalProps) {
}
const footerContent = (
<div className="flex justify-end">
<div className="flex justify-end gap-2">
<button
type="button"
onClick={onClose}
onClick={handleCancel}
className="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-accent"
>
Cancel
</button>
<button
type="button"
onClick={handleSave}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Done
@@ -68,15 +101,15 @@ export function NodeEditorModal({ node, onClose }: NodeEditorModalProps) {
</div>
)}
{/* Type-specific form */}
{node.type === 'decision' && (
<NodeFormDecision node={node} onUpdate={handleUpdate} />
{/* Type-specific form - uses draft state, not the original node */}
{draft.type === 'decision' && (
<NodeFormDecision node={draft} onUpdate={handleUpdate} />
)}
{node.type === 'action' && (
<NodeFormAction node={node} onUpdate={handleUpdate} />
{draft.type === 'action' && (
<NodeFormAction node={draft} onUpdate={handleUpdate} />
)}
{node.type === 'solution' && (
<NodeFormResolution node={node} onUpdate={handleUpdate} />
{draft.type === 'solution' && (
<NodeFormResolution node={draft} onUpdate={handleUpdate} />
)}
</Modal>
)