feat: Add tree editor validation UI (Workstream A complete)

Implements comprehensive validation feedback system for tree editor:

Task A.1 - Circular Reference Detection:
- Added detectCircularRefs() function in treeEditorStore
- Detects loops in both decision options and action next_node_id chains
- Prevents infinite navigation paths

Task A.2 - ValidationSummary Component:
- Created collapsible panel showing error/warning count
- Click error to select problematic node
- Color-coded: red for errors, yellow for warnings
- Icon indicators (AlertCircle, AlertTriangle)

Task A.3 - TreeEditorPage Integration:
- Added ValidationSummary component display
- Save button disabled when errors exist
- Warnings are informational only (don't block save)
- Added manual "Validate" button in toolbar
- Imported CheckCircle2 icon for validate button

Task A.4 - Visual Node Error Indicators:
- Added error/warning badges on problem nodes
- Tooltip on hover showing specific error messages
- Red ring for errors, yellow ring for warnings
- Shows count of errors/warnings per node

All tasks from implementation plan completed.
Build tested successfully.

Related: Issue #1

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-03 19:01:27 -05:00
parent 4378ec4b20
commit f93c8d84df
4 changed files with 237 additions and 12 deletions

View File

@@ -1,11 +1,12 @@
import { useEffect, useState, useCallback } from 'react'
import { useParams, useNavigate, useBlocker } from 'react-router-dom'
import { useStore } from 'zustand'
import { Undo2, Redo2, Save } from 'lucide-react'
import { Undo2, Redo2, Save, CheckCircle2 } from 'lucide-react'
import { treesApi } from '@/api'
import type { TreeCreate, TreeUpdate } from '@/types'
import { useTreeEditorStore, useTreeEditorTemporal } from '@/store/treeEditorStore'
import { TreeEditorLayout } from '@/components/tree-editor/TreeEditorLayout'
import { ValidationSummary } from '@/components/tree-editor/ValidationSummary'
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
import { cn } from '@/lib/utils'
@@ -29,7 +30,8 @@ export function TreeEditorPage() {
getTreeForSave,
markSaved,
setLoading,
setSaving
setSaving,
selectNode
} = useTreeEditorStore()
// Access undo/redo from temporal store
@@ -38,6 +40,9 @@ export function TreeEditorPage() {
const [showDraftPrompt, setShowDraftPrompt] = useState(false)
const [saveError, setSaveError] = useState<string | null>(null)
// Calculate if there are blocking errors
const hasBlockingErrors = validationErrors.some(e => e.severity === 'error')
// Block navigation if there are unsaved changes
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
@@ -122,6 +127,14 @@ export function TreeEditorPage() {
setShowDraftPrompt(false)
}
const handleManualValidate = () => {
validate()
}
const handleSelectNode = (nodeId: string) => {
selectNode(nodeId)
}
const handleSave = useCallback(async () => {
setSaveError(null)
@@ -307,13 +320,28 @@ export function TreeEditorPage() {
<div className="mx-2 h-6 w-px bg-border" />
{/* Validate */}
<button
onClick={handleManualValidate}
disabled={isSaving}
title="Validate tree structure (checks for errors and warnings)"
className={cn(
'flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium',
'hover:bg-accent disabled:opacity-50'
)}
>
<CheckCircle2 className="h-4 w-4" />
Validate
</button>
{/* Save */}
<button
onClick={handleSave}
disabled={isSaving || !isDirty}
disabled={isSaving || !isDirty || hasBlockingErrors}
title={hasBlockingErrors ? 'Fix validation errors before saving' : undefined}
className={cn(
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90 disabled:opacity-50'
'hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<Save className="h-4 w-4" />
@@ -329,11 +357,13 @@ export function TreeEditorPage() {
</div>
)}
{/* Validation Errors Summary */}
{validationErrors.filter(e => e.severity === 'error').length > 0 && (
<div className="bg-destructive/10 px-4 py-2 text-sm text-destructive">
{validationErrors.filter(e => e.severity === 'error').length} validation error(s) found.
Please fix them before saving.
{/* Validation Summary */}
{validationErrors.length > 0 && (
<div className="px-4 py-3">
<ValidationSummary
errors={validationErrors}
onSelectNode={handleSelectNode}
/>
</div>
)}