feat: add tree library view system with grid/list/table modes and sorting

Implements Issue #34 - Tree Library Full View System

Backend Changes:
- Add sort_by parameter to GET /api/v1/trees endpoint
- Support 6 sorting options: usage_count, updated_at, created_at, name, name_desc, version
- Maintain backward compatibility (defaults to usage_count)
- Add comprehensive test for sorting functionality
- All 104 backend tests passing

Frontend Changes:
- Create ViewToggle component for switching between Grid/List/Table views
- Create SortDropdown component for 6 sort options
- Create TreeGridView component (extracted from TreeLibraryPage)
- Create TreeListView component (compact row-based layout)
- Create TreeTableView component (sortable table with columns)
- Update userPreferencesStore with view and sort preferences
- Update TreeFilters type to include sort_by parameter
- Update TreeLibraryPage to integrate new components
- View and sort preferences persist to localStorage

Features:
- Grid view: Best for discovery (default)
- List view: Best for quick scanning
- Table view: Best for sorting and comparison
- Responsive design: Mobile/tablet/desktop optimized
- Table view hides columns responsively
- Sortable table headers with visual indicators
- Smooth transitions and hover effects
- No layout shift when switching views

Testing:
- Backend: 104/104 tests pass
- Frontend: Build successful, no TypeScript errors
- All existing functionality preserved

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-07 20:36:20 -05:00
parent 469456c9c9
commit 89e09edc64
11 changed files with 967 additions and 148 deletions

View File

@@ -130,6 +130,7 @@ async def list_trees(
is_active: Optional[bool] = Query(None, description="Filter by active status"),
author_id: Optional[UUID] = Query(None, description="Filter by author ID"),
is_public: Optional[bool] = Query(None, description="Filter by public status"),
sort_by: Optional[str] = Query("usage_count", description="Sort order: usage_count, updated_at, created_at, name, name_desc, version"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100)
):
@@ -139,6 +140,7 @@ async def list_trees(
- category_id: Filter by category (from tree_categories table)
- tags: Comma-separated tag slugs (e.g., "citrix,networking")
- folder_id: Show only trees in a specific folder
- sort_by: Sort order (usage_count [default], updated_at, created_at, name, name_desc, version)
"""
query = select(Tree).options(
selectinload(Tree.category_rel),
@@ -188,7 +190,20 @@ async def list_trees(
# Apply access filter
query = query.where(build_tree_access_filter(current_user))
query = query.order_by(Tree.usage_count.desc(), Tree.updated_at.desc())
# Apply sorting
if sort_by == "updated_at":
query = query.order_by(Tree.updated_at.desc())
elif sort_by == "created_at":
query = query.order_by(Tree.created_at.desc())
elif sort_by == "name":
query = query.order_by(Tree.name.asc())
elif sort_by == "name_desc":
query = query.order_by(Tree.name.desc())
elif sort_by == "version":
query = query.order_by(Tree.version.desc(), Tree.updated_at.desc())
else: # Default to usage_count
query = query.order_by(Tree.usage_count.desc(), Tree.updated_at.desc())
query = query.offset(skip).limit(limit)
result = await db.execute(query)