Implement Tree Editor with visual preview and documentation updates
Tree Editor Features: - Zustand store with immer middleware and zundo for undo/redo - Form-based node editing (Decision, Action, Solution types) - Visual tree preview with solution connection indicators - NodePicker with type-grouped dropdown (Decisions/Actions/Solutions) - SharedLinksMap for detecting nodes with multiple sources - Modal component with scrollable body, fixed header/footer New Components: - TreeEditorLayout, TreeMetadataForm, NodeList, NodeEditorModal - NodeFormDecision, NodeFormAction, NodeFormResolution - DynamicArrayField, NodePicker - TreePreviewPanel, TreePreviewNode Documentation: - Updated README.md status to Phase 2 - Added Tree Editor details to CURRENT-STATE.md - Added modal/Zustand lessons to LESSONS-LEARNED.md - Updated file structure in CLAUDE-SETUP.md - Added Tree Editor progress to PROGRESS.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
132
frontend/src/components/tree-editor/TreeMetadataForm.tsx
Normal file
132
frontend/src/components/tree-editor/TreeMetadataForm.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { treesApi } from '@/api'
|
||||
import { useTreeEditorStore } from '@/store/treeEditorStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function TreeMetadataForm() {
|
||||
const { name, description, category, setName, setDescription, setCategory, validationErrors } =
|
||||
useTreeEditorStore()
|
||||
|
||||
const [categories, setCategories] = useState<string[]>([])
|
||||
const [customCategory, setCustomCategory] = useState(false)
|
||||
|
||||
// Load existing categories
|
||||
useEffect(() => {
|
||||
treesApi.categories().then(setCategories).catch(console.error)
|
||||
}, [])
|
||||
|
||||
const handleCategoryChange = (value: string) => {
|
||||
if (value === '__custom__') {
|
||||
setCustomCategory(true)
|
||||
setCategory('')
|
||||
} else {
|
||||
setCustomCategory(false)
|
||||
setCategory(value)
|
||||
}
|
||||
}
|
||||
|
||||
const nameError = validationErrors.find(
|
||||
(e) => !e.nodeId && e.message.toLowerCase().includes('name')
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
|
||||
<h2 className="text-sm font-semibold text-card-foreground">Tree Details</h2>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="tree-name" className="block text-sm font-medium text-foreground">
|
||||
Name <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="tree-name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g., VDA Registration Troubleshooting"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
nameError ? 'border-destructive' : 'border-input'
|
||||
)}
|
||||
/>
|
||||
{nameError && (
|
||||
<p className="mt-1 text-xs text-destructive">{nameError.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label htmlFor="tree-description" className="block text-sm font-medium text-foreground">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="tree-description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Brief description of what this tree troubleshoots..."
|
||||
rows={2}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label htmlFor="tree-category" className="block text-sm font-medium text-foreground">
|
||||
Category
|
||||
</label>
|
||||
{!customCategory ? (
|
||||
<select
|
||||
id="tree-category"
|
||||
value={category || ''}
|
||||
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'bg-background text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
>
|
||||
<option value="">No category</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
</option>
|
||||
))}
|
||||
<option value="__custom__">+ Add custom category</option>
|
||||
</select>
|
||||
) : (
|
||||
<div className="mt-1 flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
placeholder="Enter new category"
|
||||
className={cn(
|
||||
'block flex-1 rounded-md border border-input px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCustomCategory(false)
|
||||
setCategory('')
|
||||
}}
|
||||
className="rounded-md border border-input px-3 py-2 text-sm hover:bg-accent"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TreeMetadataForm
|
||||
Reference in New Issue
Block a user