diff --git a/backend/app/api/endpoints/trees.py b/backend/app/api/endpoints/trees.py index 9a54e36e..743106f8 100644 --- a/backend/app/api/endpoints/trees.py +++ b/backend/app/api/endpoints/trees.py @@ -229,6 +229,94 @@ async def list_categories( return sorted(categories) +# --- Pinned Flows Endpoints (must be before /{tree_id} to avoid route shadowing) --- + +MAX_PINNED_FLOWS = 15 + + +@router.get("/pinned", response_model=PinnedFlowsListResponse) +async def list_pinned_flows( + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_active_user)] +): + """List user's pinned flows, ordered by display_order.""" + result = await db.execute( + select(UserPinnedTree, Tree) + .join(Tree, UserPinnedTree.tree_id == Tree.id) + .options(selectinload(Tree.category_rel)) + .where( + UserPinnedTree.user_id == current_user.id, + Tree.is_active == True, + Tree.deleted_at.is_(None) + ) + .order_by(UserPinnedTree.display_order, UserPinnedTree.pinned_at) + ) + rows = result.all() + + items = [] + for pin, tree in rows: + items.append(PinnedFlowResponse( + id=pin.id, + tree_id=tree.id, + tree_name=tree.name, + tree_type=tree.tree_type, + category_emoji=None, + category_name=tree.category_rel.name if tree.category_rel else None, + pinned_at=pin.pinned_at, + display_order=pin.display_order, + )) + + return PinnedFlowsListResponse(items=items, count=len(items)) + + +@router.patch("/pinned/reorder", response_model=PinnedFlowsListResponse) +async def reorder_pinned_flows( + reorder_data: PinnedFlowReorderRequest, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_active_user)] +): + """Update display_order for all pinned flows.""" + for item in reorder_data.order: + await db.execute( + update(UserPinnedTree) + .where( + UserPinnedTree.user_id == current_user.id, + UserPinnedTree.tree_id == item.tree_id + ) + .values(display_order=item.display_order) + ) + await db.commit() + + # Return updated list + result = await db.execute( + select(UserPinnedTree, Tree) + .join(Tree, UserPinnedTree.tree_id == Tree.id) + .options(selectinload(Tree.category_rel)) + .where( + UserPinnedTree.user_id == current_user.id, + Tree.is_active == True, + Tree.deleted_at.is_(None) + ) + .order_by(UserPinnedTree.display_order, UserPinnedTree.pinned_at) + ) + rows = result.all() + + items = [] + for pin, tree in rows: + items.append(PinnedFlowResponse( + id=pin.id, + tree_id=tree.id, + tree_name=tree.name, + tree_type=tree.tree_type, + category_emoji=None, + category_name=tree.category_rel.name if tree.category_rel else None, + pinned_at=pin.pinned_at, + display_order=pin.display_order, + )) + + return PinnedFlowsListResponse(items=items, count=len(items)) + + @router.get("/search", response_model=list[TreeListResponse]) async def search_trees( db: Annotated[AsyncSession, Depends(get_db)], @@ -1034,46 +1122,6 @@ async def check_tree_can_publish( ) -# --- Pinned Flows Endpoints --- - -MAX_PINNED_FLOWS = 15 - - -@router.get("/pinned", response_model=PinnedFlowsListResponse) -async def list_pinned_flows( - db: Annotated[AsyncSession, Depends(get_db)], - current_user: Annotated[User, Depends(get_current_active_user)] -): - """List user's pinned flows, ordered by display_order.""" - result = await db.execute( - select(UserPinnedTree, Tree) - .join(Tree, UserPinnedTree.tree_id == Tree.id) - .options(selectinload(Tree.category_rel)) - .where( - UserPinnedTree.user_id == current_user.id, - Tree.is_active == True, - Tree.deleted_at.is_(None) - ) - .order_by(UserPinnedTree.display_order, UserPinnedTree.pinned_at) - ) - rows = result.all() - - items = [] - for pin, tree in rows: - items.append(PinnedFlowResponse( - id=pin.id, - tree_id=tree.id, - tree_name=tree.name, - tree_type=tree.tree_type, - category_emoji=None, - category_name=tree.category_rel.name if tree.category_rel else None, - pinned_at=pin.pinned_at, - display_order=pin.display_order, - )) - - return PinnedFlowsListResponse(items=items, count=len(items)) - - @router.post("/{tree_id}/pin", response_model=PinnedFlowResponse) async def pin_flow( tree_id: UUID, @@ -1166,51 +1214,3 @@ async def unpin_flow( await db.delete(pin) await db.commit() return {"success": True} - - -@router.patch("/pinned/reorder", response_model=PinnedFlowsListResponse) -async def reorder_pinned_flows( - reorder_data: PinnedFlowReorderRequest, - db: Annotated[AsyncSession, Depends(get_db)], - current_user: Annotated[User, Depends(get_current_active_user)] -): - """Update display_order for all pinned flows.""" - for item in reorder_data.order: - await db.execute( - update(UserPinnedTree) - .where( - UserPinnedTree.user_id == current_user.id, - UserPinnedTree.tree_id == item.tree_id - ) - .values(display_order=item.display_order) - ) - await db.commit() - - # Return updated list - result = await db.execute( - select(UserPinnedTree, Tree) - .join(Tree, UserPinnedTree.tree_id == Tree.id) - .options(selectinload(Tree.category_rel)) - .where( - UserPinnedTree.user_id == current_user.id, - Tree.is_active == True, - Tree.deleted_at.is_(None) - ) - .order_by(UserPinnedTree.display_order, UserPinnedTree.pinned_at) - ) - rows = result.all() - - items = [] - for pin, tree in rows: - items.append(PinnedFlowResponse( - id=pin.id, - tree_id=tree.id, - tree_name=tree.name, - tree_type=tree.tree_type, - category_emoji=None, - category_name=tree.category_rel.name if tree.category_rel else None, - pinned_at=pin.pinned_at, - display_order=pin.display_order, - )) - - return PinnedFlowsListResponse(items=items, count=len(items)) diff --git a/frontend/src/components/library/TreeGridView.tsx b/frontend/src/components/library/TreeGridView.tsx index f867c249..bc9dd1f1 100644 --- a/frontend/src/components/library/TreeGridView.tsx +++ b/frontend/src/components/library/TreeGridView.tsx @@ -37,26 +37,6 @@ export function TreeGridView({ key={tree.id} className="relative bg-card border border-border rounded-2xl p-4 transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6" > - {onTogglePin && ( - - )}

{tree.name}

@@ -74,6 +54,26 @@ export function TreeGridView({ )}
+ {onTogglePin && ( + + )} {tree.is_public ? (