Files
resolutionflow/frontend/src/components/tree-editor/NodeEditorModal.tsx
Michael Chihlas 303a558432 refactor: replace hardcoded hex values with Tailwind semantic tokens
3,200+ hardcoded color values replaced with CSS variable-backed
Tailwind classes (bg-card, text-foreground, border-border, etc.).
Enables light mode via CSS variable swap. Only syntax highlighting
colors and intentional one-offs remain hardcoded (~15 values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 04:34:35 -04:00

125 lines
3.8 KiB
TypeScript

import { useState, useCallback } from 'react'
import { Modal } from '@/components/common/Modal'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { NodeFormDecision } from './NodeFormDecision'
import { NodeFormAction } from './NodeFormAction'
import { NodeFormResolution } from './NodeFormResolution'
import type { TreeStructure } from '@/types'
import { Button } from '@/components/ui/Button'
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, isNewNode = false }: NodeEditorModalProps) {
const { updateNode, deleteNode, validationErrors } = useTreeEditorStore()
const nodeErrors = validationErrors.filter(e => e.nodeId === node.id)
// Local draft state - changes are NOT persisted until "Done" is clicked
// Reset draft when node ID changes (switching to a different node)
const [draft, setDraft] = useState<TreeStructure>(() => structuredClone(node))
const [lastNodeId, setLastNodeId] = useState(node.id)
if (node.id !== lastNodeId) {
setDraft(structuredClone(node))
setLastNodeId(node.id)
}
const handleUpdate = useCallback((updates: Partial<TreeStructure>) => {
setDraft(prev => ({ ...prev, ...updates }))
}, [])
const handleSave = () => {
// Commit all draft changes to the store
// IMPORTANT: Exclude 'children' from the update - children are managed separately
// by addNode/deleteNode and we don't want to overwrite them with stale draft data
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { children, ...draftWithoutChildren } = draft
updateNode(node.id, draftWithoutChildren)
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 = () => {
switch (node.type) {
case 'decision':
return 'Edit Decision Node'
case 'action':
return 'Edit Action Node'
case 'solution':
return 'Edit Solution Node'
default:
return 'Edit Node'
}
}
const footerContent = (
<div className="flex justify-end gap-2">
<Button
type="button"
variant="secondary"
onClick={handleCancel}
>
Cancel
</Button>
<Button
type="button"
onClick={handleSave}
>
Done
</Button>
</div>
)
return (
<Modal isOpen={true} onClose={onClose} title={getTitle()} size="lg" footer={footerContent} allowFullScreen={true}>
{/* Node ID display */}
<div className="mb-4 text-xs text-muted-foreground">
Node ID: <code className="rounded bg-accent px-1 py-0.5">{node.id}</code>
</div>
{/* Validation errors */}
{nodeErrors.length > 0 && (
<div className="mb-4 space-y-1">
{nodeErrors.map((error, i) => (
<div
key={i}
className={`rounded-md px-3 py-2 text-sm ${
error.severity === 'error'
? 'bg-red-400/10 text-red-400'
: 'bg-yellow-400/10 text-yellow-400'
}`}
>
{error.message}
</div>
))}
</div>
)}
{/* Type-specific form - uses draft state, not the original node */}
{draft.type === 'decision' && (
<NodeFormDecision node={draft} onUpdate={handleUpdate} />
)}
{draft.type === 'action' && (
<NodeFormAction node={draft} onUpdate={handleUpdate} />
)}
{draft.type === 'solution' && (
<NodeFormResolution node={draft} onUpdate={handleUpdate} />
)}
</Modal>
)
}
export default NodeEditorModal