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:
@@ -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)
|
||||
|
||||
@@ -317,3 +317,73 @@ class TestTrees:
|
||||
response = await client.post("/api/v1/trees", json=tree_data)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_trees_sorting(self, client: AsyncClient, auth_headers: dict):
|
||||
"""Test sorting trees by different criteria."""
|
||||
# Create multiple trees with different attributes
|
||||
import asyncio
|
||||
|
||||
# Create trees with different names and versions
|
||||
trees_data = [
|
||||
{"name": "Alpha Tree", "description": "First alphabetically"},
|
||||
{"name": "Zulu Tree", "description": "Last alphabetically"},
|
||||
{"name": "Beta Tree", "description": "Second alphabetically"},
|
||||
]
|
||||
|
||||
created_trees = []
|
||||
for tree_data in trees_data:
|
||||
tree_data["tree_structure"] = {
|
||||
"id": "root",
|
||||
"type": "solution",
|
||||
"title": "Test",
|
||||
"description": "Test tree"
|
||||
}
|
||||
response = await client.post("/api/v1/trees", json=tree_data, headers=auth_headers)
|
||||
assert response.status_code == 201
|
||||
created_trees.append(response.json())
|
||||
# Small delay to ensure different timestamps
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Test sorting by name (A-Z)
|
||||
response = await client.get("/api/v1/trees?sort_by=name", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
trees = response.json()
|
||||
names = [t["name"] for t in trees if t["id"] in [c["id"] for c in created_trees]]
|
||||
assert names == sorted(names)
|
||||
|
||||
# Test sorting by name descending (Z-A)
|
||||
response = await client.get("/api/v1/trees?sort_by=name_desc", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
trees = response.json()
|
||||
names = [t["name"] for t in trees if t["id"] in [c["id"] for c in created_trees]]
|
||||
assert names == sorted(names, reverse=True)
|
||||
|
||||
# Test sorting by created_at (most recent first)
|
||||
response = await client.get("/api/v1/trees?sort_by=created_at", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
trees = response.json()
|
||||
# Most recently created should be first
|
||||
filtered_trees = [t for t in trees if t["id"] in [c["id"] for c in created_trees]]
|
||||
if len(filtered_trees) >= 2:
|
||||
# Verify descending order
|
||||
for i in range(len(filtered_trees) - 1):
|
||||
assert filtered_trees[i]["created_at"] >= filtered_trees[i+1]["created_at"]
|
||||
|
||||
# Test sorting by updated_at
|
||||
response = await client.get("/api/v1/trees?sort_by=updated_at", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
trees = response.json()
|
||||
assert isinstance(trees, list)
|
||||
|
||||
# Test sorting by version
|
||||
response = await client.get("/api/v1/trees?sort_by=version", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
trees = response.json()
|
||||
assert isinstance(trees, list)
|
||||
|
||||
# Test default sorting (usage_count)
|
||||
response = await client.get("/api/v1/trees", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
trees = response.json()
|
||||
assert isinstance(trees, list)
|
||||
|
||||
Reference in New Issue
Block a user