From 4cff59f8ca1e2b004d75d05c5ce24fd4116ec44e Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 14 Jul 2026 15:55:08 +0000 Subject: [PATCH] Collapse user permissions into expandable section on admin users page Co-Authored-By: Claude Sonnet 4.6 --- src/pages/AdminUsers.tsx | 227 ++++++++++++++++++++++----------------- 1 file changed, 131 insertions(+), 96 deletions(-) diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index b6e69c6..fe4de1c 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -21,6 +21,7 @@ export function AdminUsers({ user }: { user: User }) { const [allRoles, setAllRoles] = useState([]) const [allCaps, setAllCaps] = useState([]) const [showCreate, setShowCreate] = useState(false) + const [expanded, setExpanded] = useState>(new Set()) async function load() { const [u, a, r, c] = await Promise.all([ @@ -105,109 +106,143 @@ export function AdminUsers({ user }: { user: User }) { {showCreate && { setShowCreate(false); load() }} />}
- {users.map(u => ( -
-
-
-
{u.name}
-
{u.email}
+ {users.map(u => { + const isExpanded = expanded.has(u.id) + const toggleExpanded = () => setExpanded(prev => { + const next = new Set(prev) + next.has(u.id) ? next.delete(u.id) : next.add(u.id) + return next + }) + const inherited = roleDerivedCaps(u) + return ( +
+ {/* Always-visible row */} +
+
+
{u.name}
+
{u.email}
+
+
+ toggle(u.id, 'active', u.active)} /> + toggle(u.id, 'is_admin', u.is_admin)} /> + toggle(u.id, 'offsite_allowed', u.offsite_allowed)} /> +
-
- toggle(u.id, 'active', u.active)} /> - toggle(u.id, 'is_admin', u.is_admin)} /> - toggle(u.id, 'offsite_allowed', u.offsite_allowed)} /> + + {/* Summary row: role badges + expand toggle */} +
+ {u.roles.length > 0 + ? u.roles.map(r => ( + {r.name} + )) + : No roles + } +
-
- {/* App grants + per-app capabilities */} -
- {allApps.map(app => { - const has = u.app_slugs.includes(app.slug) - const hasViaRole = u.roles.some(ur => allRoles.find(r => r.id === ur.id)?.app_slugs.includes(app.slug)) - const appCaps = allCaps.filter(c => c.app_slug === app.slug) - const inherited = roleDerivedCaps(u) - return ( -
- + {(has || hasViaRole) && appCaps.length > 0 && ( +
+ {appCaps.map(cap => { + const capKey = `${cap.app_slug}:${cap.slug}` + const direct = u.capabilities.includes(capKey) + const viaRole = inherited.has(capKey) + const on = direct || viaRole + return ( + + ) + })} +
+ )} +
+ ) + })} +
+ + {/* Role editor */} +
+ Roles: + {u.roles.map(r => ( + - {(has || hasViaRole) && appCaps.length > 0 && ( -
- {appCaps.map(cap => { - const capKey = `${cap.app_slug}:${cap.slug}` - const direct = u.capabilities.includes(capKey) - const viaRole = inherited.has(capKey) - const on = direct || viaRole - return ( - - ) - })} -
+ {r.name} × + + ))} + + {u.workforce_user_id && ( + )}
- ) - })} -
- - {/* Roles */} -
- Roles: - {u.roles.map(r => ( - - ))} - - {u.workforce_user_id && ( - + )}
-
- ))} + ) + })}