diff --git a/backend/btrixcloud/orgs.py b/backend/btrixcloud/orgs.py index 2da2d526..7736fe0b 100644 --- a/backend/btrixcloud/orgs.py +++ b/backend/btrixcloud/orgs.py @@ -134,6 +134,8 @@ class OrgOps: role: UserRole = UserRole.VIEWER, page_size: int = DEFAULT_PAGE_SIZE, page: int = 1, + sort_by: Optional[str] = "name", + sort_direction: int = 1, calculate_total=True, ): """Get all orgs a user is a member of""" @@ -142,19 +144,52 @@ class OrgOps: skip = page_size * page if user.is_superuser: - query = {} + query: Dict[str, object] = {} else: - query = {f"users.{user.id}": {"$gte": role.value}} + query: Dict[str, object] = {f"users.{user.id}": {"$gte": role.value}} - total = 0 - if calculate_total: - total = await self.orgs.count_documents(query) + aggregate = [{"$match": query}] - cursor = self.orgs.find(query, skip=skip, limit=page_size) - results = await cursor.to_list(length=page_size) - orgs = [Organization.from_dict(res) for res in results] + # Ensure default org is always first, then sort on sort_by if set + sort_query = {"default": -1} - return orgs, total + if sort_by: + sort_fields = ("name", "slug", "readOnly") + if sort_by not in sort_fields: + raise HTTPException(status_code=400, detail="invalid_sort_by") + if sort_direction not in (1, -1): + raise HTTPException(status_code=400, detail="invalid_sort_direction") + + sort_query[sort_by] = sort_direction + + aggregate.extend([{"$sort": sort_query}]) + + aggregate.extend( + [ + { + "$facet": { + "items": [ + {"$skip": skip}, + {"$limit": page_size}, + ], + "total": [{"$count": "count"}], + } + }, + ] + ) + + # Get total + cursor = self.orgs.aggregate(aggregate) + results = await cursor.to_list(length=1) + result = results[0] + items = result["items"] + + try: + total = int(result["total"][0]["count"]) + except (IndexError, ValueError): + total = 0 + + return [Organization.from_dict(data) for data in items], total async def get_org_for_user_by_id( self, oid: UUID, user: User, role: UserRole = UserRole.VIEWER @@ -788,9 +823,15 @@ def init_orgs_api(app, mdb, user_manager, invites, user_dep, user_or_shared_secr user: User = Depends(user_dep), pageSize: int = DEFAULT_PAGE_SIZE, page: int = 1, + sortBy: Optional[str] = "name", + sortDirection: Optional[int] = 1, ): results, total = await ops.get_orgs_for_user( - user, page_size=pageSize, page=page + user, + page_size=pageSize, + page=page, + sort_by=sortBy, + sort_direction=sortDirection, ) serialized_results = [ await res.serialize_for_user(user, user_manager) for res in results diff --git a/backend/test/test_org.py b/backend/test/test_org.py index ade1605a..13c146c4 100644 --- a/backend/test/test_org.py +++ b/backend/test/test_org.py @@ -703,3 +703,60 @@ def test_create_org_and_invite_existing_user(admin_auth_headers): "giftedExecMinutes": 0, } assert "subData" not in org + + +def test_sort_orgs(admin_auth_headers): + # Create a few new orgs for testing + r = requests.post( + f"{API_PREFIX}/orgs/create", + headers=admin_auth_headers, + json={"name": "abc", "slug": "abc"}, + ) + assert r.status_code == 200 + + r = requests.post( + f"{API_PREFIX}/orgs/create", + headers=admin_auth_headers, + json={"name": "mno", "slug": "mno"}, + ) + assert r.status_code == 200 + + r = requests.post( + f"{API_PREFIX}/orgs/create", + headers=admin_auth_headers, + json={"name": "xyz", "slug": "xyz"}, + ) + assert r.status_code == 200 + + # Check default sorting + # Default org should come first, followed by alphabetical sorting ascending + r = requests.get(f"{API_PREFIX}/orgs", headers=admin_auth_headers) + data = r.json() + orgs = data["items"] + + assert orgs[0]["default"] + + other_orgs = orgs[1:] + last_name = None + for org in other_orgs: + org_name = org["name"] + if last_name: + assert org_name > last_name + last_name = org_name + + # Sort by name descending, ensure default org still first + r = requests.get( + f"{API_PREFIX}/orgs?sortBy=name&sortDirection=-1", headers=admin_auth_headers + ) + data = r.json() + orgs = data["items"] + + assert orgs[0]["default"] + + other_orgs = orgs[1:] + last_name = None + for org in other_orgs: + org_name = org["name"] + if last_name: + assert org_name < last_name + last_name = org_name diff --git a/frontend/src/pages/home.ts b/frontend/src/pages/home.ts index 8c5477da..37d13a70 100644 --- a/frontend/src/pages/home.ts +++ b/frontend/src/pages/home.ts @@ -2,7 +2,6 @@ import { localized, msg, str } from "@lit/localize"; import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; import { type PropertyValues, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import sortBy from "lodash/fp/sortBy"; import type { InviteSuccessDetail } from "@/features/accounts/invite-form"; import type { APIPaginatedList } from "@/types/api"; @@ -278,12 +277,12 @@ export class Home extends LiteElement { } private async fetchOrgs() { - this.orgList = sortBy("name")(await this.getOrgs()); + this.orgList = await this.getOrgs(); } private async getOrgs() { const data = await this.apiFetch>( - "/orgs", + "/orgs?sortBy=name", this.authState!, );