From 90c319fb252d3095a87dba06eb4862d6cd3e7f18 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Fri, 1 May 2026 06:58:52 +0000 Subject: [PATCH] Show inline dependency counts on the Roles admin list (Task #182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Apps, Services, Users, and Groups admin panels already surface their dependency counts inline so admins know what's affected before clicking. The Roles panel previously hid this — admins had to open the delete dialog to see how many users/groups would be affected. This change adds the same inline display to the Roles panel for consistency. Changes - lib/api-spec/openapi.yaml: Added optional `userCount` and `groupCount` fields to the `Role` schema (matching the App pattern: optional, populated only by the admin list endpoint, with descriptive comments). - artifacts/api-server/src/routes/roles.ts: GET /roles now batches two grouped count queries (user_roles, group_roles) and merges the counts into each list item — same shape as GET /apps. Empty-list short-circuits before running the aggregations. - lib/api-zod/src/generated/api.ts: Regenerated via the api-spec codegen script (orval). ListRolesResponseItem now includes the optional counts. - artifacts/tx-os/src/pages/admin.tsx (RolesPanel): Each role card renders an inline counts row using the existing `admin.roles.usersCount` / `admin.roles.groupsCount` translation keys (no new copy needed). Mirrors the Apps panel pattern: 11px muted-foreground text with bullet separators, only renders when at least one count is > 0, and exposes a `data-testid="role-counts-"` for tests. Notes / deviations - The task description said "the role list endpoint already returns userCount/groupCount" but it didn't — the counts only existed on /roles/:id/usage. Added them to the list endpoint following the same pattern Apps and Groups already use. - The pre-existing admin.roles.usersCount/groupsCount keys have no `_one`/`_other` plural variants; I kept it that way to stay consistent with the Apps panel keys (which also have no plural variants). Verification - `pnpm -w run typecheck` passes for tx-os and roles.ts (pre-existing unrelated typecheck errors in executive-meetings.ts remain — not touched by this change). - e2e test (testing skill, status: success): logged in as the seeded admin, opened the Roles panel, verified inline counts render on the admin and user roles, bullet separator is present, and roles with zero dependencies don't render an empty counts area. Replit-Task-Id: 8c99d912-8b3a-4e80-aca7-ec167e6e75e6 --- artifacts/api-server/src/routes/roles.ts | 36 ++++++++++++++++++- artifacts/tx-os/src/pages/admin.tsx | 21 +++++++++++ .../src/generated/api.schemas.ts | 10 ++++++ lib/api-spec/openapi.yaml | 12 +++++++ lib/api-zod/src/generated/api.ts | 24 +++++++++++++ 5 files changed, 102 insertions(+), 1 deletion(-) diff --git a/artifacts/api-server/src/routes/roles.ts b/artifacts/api-server/src/routes/roles.ts index 4992cdc1..da1a354f 100644 --- a/artifacts/api-server/src/routes/roles.ts +++ b/artifacts/api-server/src/routes/roles.ts @@ -54,7 +54,41 @@ function serializeRole(r: typeof rolesTable.$inferSelect) { router.get("/roles", requireAdmin, async (_req, res): Promise => { const rows = await db.select().from(rolesTable).orderBy(rolesTable.name); - res.json(rows.map(serializeRole)); + if (rows.length === 0) { + res.json([]); + return; + } + // Compute dependency counts so the admin list can show the same inline + // "5 users · 2 groups" hint as the Apps/Services/Users/Groups panels + // (mirrors the GET /apps and GET /groups behavior). The single-role + // /roles/:id/usage endpoint remains the source of truth for the delete + // dialog and edit dialog. + const roleIds = rows.map((r) => r.id); + const userCountRows = await db + .select({ + roleId: userRolesTable.roleId, + count: sql`count(*)::int`, + }) + .from(userRolesTable) + .where(inArray(userRolesTable.roleId, roleIds)) + .groupBy(userRolesTable.roleId); + const groupCountRows = await db + .select({ + roleId: groupRolesTable.roleId, + count: sql`count(*)::int`, + }) + .from(groupRolesTable) + .where(inArray(groupRolesTable.roleId, roleIds)) + .groupBy(groupRolesTable.roleId); + const userMap = new Map(userCountRows.map((r) => [r.roleId, r.count])); + const groupMap = new Map(groupCountRows.map((r) => [r.roleId, r.count])); + res.json( + rows.map((r) => ({ + ...serializeRole(r), + userCount: userMap.get(r.id) ?? 0, + groupCount: groupMap.get(r.id) ?? 0, + })), + ); }); router.get("/permissions", requireAdmin, async (_req, res): Promise => { diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index 256d5be5..3753f3b2 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -5246,6 +5246,27 @@ function RolesPanel() { {r.descriptionAr}

)} + {(() => { + const userCount = r.userCount ?? 0; + const groupCount = r.groupCount ?? 0; + const parts: string[] = []; + if (userCount > 0) parts.push(t("admin.roles.usersCount", { count: userCount })); + if (groupCount > 0) parts.push(t("admin.roles.groupsCount", { count: groupCount })); + if (parts.length === 0) return null; + return ( +
+ {parts.map((p, i) => ( + + {i > 0 && } + {p} + + ))} +
+ ); + })()}