Files
resolutionflow/frontend/src/components/tree-editor/TreeMetadataForm.tsx
Michael Chihlas d1a56f0529 refactor: migrate remaining components to Design System v4
111 files across 14 directories: common, tree-editor, kb-accelerator,
copilot, assistant, analytics, library, procedural, procedural-editor,
public, script-editor, ui, admin, step-library.

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

214 lines
7.0 KiB
TypeScript

import { useEffect, useState } from 'react'
import { categoriesApi } from '@/api/categories'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { TagInput } from '@/components/common/TagInput'
import type { CategoryListItem } from '@/types'
import { cn } from '@/lib/utils'
import { Globe, Lock } from 'lucide-react'
export function TreeMetadataForm() {
const {
name,
description,
category,
categoryId,
tags,
isPublic,
setName,
setDescription,
setCategory,
setCategoryId,
setTags,
setIsPublic,
validationErrors,
} = useTreeEditorStore()
const [categories, setCategories] = useState<CategoryListItem[]>([])
const [customCategory, setCustomCategory] = useState(false)
// Load categories
useEffect(() => {
categoriesApi.list().then(setCategories).catch(console.error)
}, [])
const handleCategoryChange = (value: string) => {
if (value === '__custom__') {
setCustomCategory(true)
setCategory('')
setCategoryId(null)
} else if (value === '') {
setCustomCategory(false)
setCategory('')
setCategoryId(null)
} else {
setCustomCategory(false)
setCategoryId(value)
// Find category name for display
const cat = categories.find((c) => c.id === value)
if (cat) {
setCategory(cat.name)
}
}
}
const nameError = validationErrors.find(
(e) => !e.nodeId && e.message.toLowerCase().includes('name')
)
return (
<div className="space-y-4 bg-[#14161d] border border-[#1e2130] rounded-2xl p-4">
<h2 className="text-sm font-semibold text-[#e2e5eb]">Tree Details</h2>
{/* Name */}
<div>
<label htmlFor="tree-name" className="block text-sm font-medium text-[#e2e5eb]">
Name <span className="text-red-400">*</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-[#14161d] text-[#e2e5eb] placeholder:text-[#848b9b]',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
nameError ? 'border-red-400' : 'border-[#1e2130]'
)}
/>
{nameError && <p className="mt-1 text-xs text-red-400">{nameError.message}</p>}
</div>
{/* Description */}
<div>
<label htmlFor="tree-description" className="block text-sm font-medium text-[#e2e5eb]">
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-[#1e2130] px-3 py-2 text-sm',
'bg-[#14161d] text-[#e2e5eb] placeholder:text-[#848b9b]',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
/>
</div>
{/* Category */}
<div>
<label htmlFor="tree-category" className="block text-sm font-medium text-[#e2e5eb]">
Category
</label>
{!customCategory ? (
<select
id="tree-category"
value={categoryId || ''}
onChange={(e) => handleCategoryChange(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border border-[#1e2130] px-3 py-2 text-sm',
'bg-[#14161d] text-[#e2e5eb]',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
>
<option value="">No category</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.name}
{cat.account_id ? ' (Account)' : ''}
</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 min-w-0 flex-1 rounded-md border border-[#1e2130] px-3 py-2 text-sm',
'bg-[#14161d] text-[#e2e5eb] placeholder:text-[#848b9b]',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
autoFocus
/>
<button
type="button"
onClick={() => {
setCustomCategory(false)
setCategory('')
setCategoryId(null)
}}
className="shrink-0 rounded-md border border-[#1e2130] px-2.5 py-2 text-xs text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
>
Cancel
</button>
</div>
)}
</div>
{/* Tags */}
<div>
<label className="block text-sm font-medium text-[#e2e5eb]">Tags</label>
<div className="mt-1">
<TagInput tags={tags} onChange={setTags} maxTags={10} placeholder="Add tags..." />
</div>
</div>
{/* Visibility */}
<div>
<label className="block text-sm font-medium text-[#e2e5eb]">Visibility</label>
<div className="mt-2 flex gap-4">
<label
className={cn(
'flex cursor-pointer items-center gap-2 rounded-md border px-4 py-2',
'transition-colors',
!isPublic ? 'border-primary/30 bg-accent text-[#e2e5eb]' : 'border-[#1e2130] text-[#848b9b] hover:bg-accent/50'
)}
>
<input
type="radio"
name="visibility"
checked={!isPublic}
onChange={() => setIsPublic(false)}
className="sr-only"
/>
<Lock className="h-4 w-4" />
<span className="text-sm">Private</span>
</label>
<label
className={cn(
'flex cursor-pointer items-center gap-2 rounded-md border px-4 py-2',
'transition-colors',
isPublic ? 'border-primary/30 bg-accent text-[#e2e5eb]' : 'border-[#1e2130] text-[#848b9b] hover:bg-accent/50'
)}
>
<input
type="radio"
name="visibility"
checked={isPublic}
onChange={() => setIsPublic(true)}
className="sr-only"
/>
<Globe className="h-4 w-4" />
<span className="text-sm">Public</span>
</label>
</div>
<p className="mt-1 text-xs text-[#848b9b]">
{isPublic
? 'Anyone can view this tree'
: 'Only you and your team can view this tree'}
</p>
</div>
</div>
)
}
export default TreeMetadataForm