Files
resolutionflow/frontend/src/pages/TreeLibraryPage.tsx
chihlasm dbd38afb73 Fix TypeScript build errors
- Remove unused variables (allFolders, getFolderDepth, hasChildren, legacyCategories)
- Fix Lucide icon title prop by wrapping in span elements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 01:39:32 -05:00

355 lines
13 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Plus, Pencil, Globe, Lock, X } from 'lucide-react'
import { treesApi, categoriesApi, foldersApi } from '@/api'
import type { TreeListItem, CategoryListItem, FolderListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { FolderSidebar } from '@/components/library/FolderSidebar'
import { FolderEditModal } from '@/components/library/FolderEditModal'
import { AddToFolderMenu } from '@/components/library/AddToFolderMenu'
import { cn } from '@/lib/utils'
export function TreeLibraryPage() {
const navigate = useNavigate()
const [trees, setTrees] = useState<TreeListItem[]>([])
const [categories, setCategories] = useState<CategoryListItem[]>([])
const [folders, setFolders] = useState<FolderListItem[]>([])
const [selectedCategoryId, setSelectedCategoryId] = useState<string>('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Folder modal state
const [folderModalOpen, setFolderModalOpen] = useState(false)
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
const [newFolderParentId, setNewFolderParentId] = useState<string | null>(null)
const loadFolders = useCallback(async () => {
try {
const foldersData = await foldersApi.list()
setFolders(foldersData)
} catch (err) {
console.error('Failed to load folders:', err)
}
}, [])
useEffect(() => {
loadData()
}, [selectedCategoryId, selectedTags, selectedFolderId])
// Load folders on mount and listen for changes
useEffect(() => {
loadFolders()
const handleFolderChange = () => loadFolders()
window.addEventListener('folder-changed', handleFolderChange)
return () => window.removeEventListener('folder-changed', handleFolderChange)
}, [loadFolders])
const loadData = async () => {
setIsLoading(true)
setError(null)
try {
const [treesData, categoriesData] = await Promise.all([
treesApi.list({
category_id: selectedCategoryId || undefined,
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined,
folder_id: selectedFolderId || undefined,
}),
categoriesApi.list(),
])
setTrees(treesData)
setCategories(categoriesData)
} catch (err) {
setError('Failed to load trees')
console.error(err)
} finally {
setIsLoading(false)
}
}
const handleSearch = async () => {
if (!searchQuery.trim()) {
loadData()
return
}
setIsLoading(true)
setError(null)
try {
const results = await treesApi.search(searchQuery)
setTrees(results)
} catch (err) {
setError('Search failed')
console.error(err)
} finally {
setIsLoading(false)
}
}
const handleTagClick = (tag: string) => {
if (!selectedTags.includes(tag)) {
setSelectedTags([...selectedTags, tag])
}
}
const removeTagFilter = (tag: string) => {
setSelectedTags(selectedTags.filter((t) => t !== tag))
}
const clearAllFilters = () => {
setSelectedCategoryId('')
setSelectedTags([])
setSelectedFolderId(null)
setSearchQuery('')
}
const handleStartSession = (treeId: string) => {
navigate(`/trees/${treeId}/navigate`)
}
const handleCreateFolder = (parentId?: string | null) => {
setEditingFolder(null)
setNewFolderParentId(parentId || null)
setFolderModalOpen(true)
}
const handleEditFolder = (folder: FolderListItem) => {
setEditingFolder(folder)
setNewFolderParentId(null)
setFolderModalOpen(true)
}
const hasActiveFilters =
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
return (
<div className="flex h-[calc(100vh-4rem)]">
{/* Folder Sidebar */}
<FolderSidebar
selectedFolderId={selectedFolderId}
onFolderSelect={setSelectedFolderId}
onCreateFolder={handleCreateFolder}
onEditFolder={handleEditFolder}
/>
{/* Main Content */}
<div className="flex-1 overflow-auto">
<div className="container mx-auto px-4 py-8">
<div className="mb-8 flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">Decision Trees</h1>
<p className="mt-2 text-muted-foreground">
Select a troubleshooting tree to start a new session
</p>
</div>
<Link
to="/trees/new"
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'
)}
>
<Plus className="h-4 w-4" />
Create Tree
</Link>
</div>
{/* Search and Filter */}
<div className="mb-4 flex flex-col gap-4 sm:flex-row">
<div className="flex flex-1 gap-2">
<input
type="text"
placeholder="Search trees..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className={cn(
'flex-1 rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
/>
<button
onClick={handleSearch}
className={cn(
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
)}
>
Search
</button>
</div>
<select
value={selectedCategoryId}
onChange={(e) => setSelectedCategoryId(e.target.value)}
aria-label="Filter by category"
className={cn(
'rounded-md border border-input bg-background px-3 py-2',
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
>
<option value="">All Categories</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.name} ({cat.tree_count})
</option>
))}
</select>
</div>
{/* Active Filters */}
{hasActiveFilters && (
<div className="mb-6 flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">Filters:</span>
{selectedFolderId && (
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
Folder
<button
onClick={() => setSelectedFolderId(null)}
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
>
<X className="h-3 w-3" />
</button>
</span>
)}
{selectedCategoryId && (
<span className="inline-flex items-center gap-1 rounded-full bg-secondary px-3 py-1 text-sm">
{categories.find((c) => c.id === selectedCategoryId)?.name}
<button
onClick={() => setSelectedCategoryId('')}
className="rounded-full p-0.5 hover:bg-secondary-foreground/10"
>
<X className="h-3 w-3" />
</button>
</span>
)}
{selectedTags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-sm text-primary"
>
{tag}
<button
onClick={() => removeTagFilter(tag)}
className="rounded-full p-0.5 hover:bg-primary/20"
>
<X className="h-3 w-3" />
</button>
</span>
))}
<button
onClick={clearAllFilters}
className="text-sm text-muted-foreground hover:text-foreground"
>
Clear all
</button>
</div>
)}
{/* Error State */}
{error && (
<div className="mb-6 rounded-md bg-destructive/10 p-4 text-destructive">{error}</div>
)}
{/* Loading State */}
{isLoading ? (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
) : trees.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
No trees found.{' '}
{(searchQuery || hasActiveFilters) && 'Try adjusting your filters.'}
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{trees.map((tree) => (
<div
key={tree.id}
className="rounded-lg border border-border bg-card p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div className="mb-2 flex items-start justify-between gap-2">
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
<div className="flex items-center gap-2">
{tree.is_public ? (
<span title="Public tree">
<Globe className="h-4 w-4 text-muted-foreground" />
</span>
) : (
<span title="Private tree">
<Lock className="h-4 w-4 text-muted-foreground" />
</span>
)}
{tree.category_info && (
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
{tree.category_info.name}
</span>
)}
</div>
</div>
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
{tree.description || 'No description available'}
</p>
{/* Tags */}
{tree.tags && tree.tags.length > 0 && (
<div className="mb-3">
<TagBadges tags={tree.tags} maxVisible={3} onTagClick={handleTagClick} />
</div>
)}
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">
v{tree.version} · {tree.usage_count} uses
</span>
<div className="flex items-center gap-2">
<AddToFolderMenu treeId={tree.id} onFolderCreated={handleCreateFolder} />
<Link
to={`/trees/${tree.id}/edit`}
className={cn(
'rounded-md border border-input p-1.5 text-muted-foreground',
'hover:bg-accent hover:text-accent-foreground'
)}
title="Edit tree"
>
<Pencil className="h-4 w-4" />
</Link>
<button
type="button"
onClick={() => handleStartSession(tree.id)}
className={cn(
'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
)}
>
Start Session
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Folder Edit Modal */}
<FolderEditModal
folder={editingFolder}
parentId={newFolderParentId}
folders={folders}
isOpen={folderModalOpen}
onClose={() => {
setFolderModalOpen(false)
setNewFolderParentId(null)
}}
onSave={loadData}
/>
</div>
)
}
export default TreeLibraryPage