Add tree organization system with categories, tags, and folders
Features: - Categories: Global and team-specific tree categorization (admin-managed) - Tags: Flexible tree tagging with autocomplete (author + admin) - User folders: Personal tree collections with subfolder support - Hierarchical structure (max 3 levels deep) - Right-click context menu for folder management - Cascade delete for subfolders - Filter trees by category, tags, and folder in library view Backend: - New models: Category, Tag, UserFolder with relationships - New API endpoints for categories, tags, and folders - Tree organization migrations (005, 006) Frontend: - FolderSidebar with hierarchical folder tree - FolderEditModal for create/edit with color picker - AddToFolderMenu for quick tree organization - TagInput with autocomplete and TagBadges display - Updated TreeMetadataForm and TreeLibraryPage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,54 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { treesApi } from '@/api'
|
||||
import { categoriesApi } from '@/api'
|
||||
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, setName, setDescription, setCategory, validationErrors } =
|
||||
useTreeEditorStore()
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
categoryId,
|
||||
tags,
|
||||
isPublic,
|
||||
setName,
|
||||
setDescription,
|
||||
setCategory,
|
||||
setCategoryId,
|
||||
setTags,
|
||||
setIsPublic,
|
||||
validationErrors,
|
||||
} = useTreeEditorStore()
|
||||
|
||||
const [categories, setCategories] = useState<string[]>([])
|
||||
const [categories, setCategories] = useState<CategoryListItem[]>([])
|
||||
const [legacyCategories, setLegacyCategories] = useState<string[]>([])
|
||||
const [customCategory, setCustomCategory] = useState(false)
|
||||
|
||||
// Load existing categories
|
||||
// Load categories
|
||||
useEffect(() => {
|
||||
treesApi.categories().then(setCategories).catch(console.error)
|
||||
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)
|
||||
setCategory(value)
|
||||
setCategoryId(value)
|
||||
// Find category name for display
|
||||
const cat = categories.find((c) => c.id === value)
|
||||
if (cat) {
|
||||
setCategory(cat.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +78,7 @@ export function TreeMetadataForm() {
|
||||
nameError ? 'border-destructive' : 'border-input'
|
||||
)}
|
||||
/>
|
||||
{nameError && (
|
||||
<p className="mt-1 text-xs text-destructive">{nameError.message}</p>
|
||||
)}
|
||||
{nameError && <p className="mt-1 text-xs text-destructive">{nameError.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
@@ -83,7 +108,7 @@ export function TreeMetadataForm() {
|
||||
{!customCategory ? (
|
||||
<select
|
||||
id="tree-category"
|
||||
value={category || ''}
|
||||
value={categoryId || ''}
|
||||
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
@@ -93,8 +118,9 @@ export function TreeMetadataForm() {
|
||||
>
|
||||
<option value="">No category</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
{cat.team_id ? ' (Team)' : ''}
|
||||
</option>
|
||||
))}
|
||||
<option value="__custom__">+ Add custom category</option>
|
||||
@@ -117,6 +143,7 @@ export function TreeMetadataForm() {
|
||||
onClick={() => {
|
||||
setCustomCategory(false)
|
||||
setCategory('')
|
||||
setCategoryId(null)
|
||||
}}
|
||||
className="rounded-md border border-input px-3 py-2 text-sm hover:bg-accent"
|
||||
>
|
||||
@@ -125,6 +152,60 @@ export function TreeMetadataForm() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">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-foreground">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 bg-primary/10' : 'border-input hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<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 bg-primary/10' : 'border-input hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<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-muted-foreground">
|
||||
{isPublic
|
||||
? 'Anyone can view this tree'
|
||||
: 'Only you and your team can view this tree'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user