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}