refactor(admin): embed roles data in user list query and update role display

- remove separate `/zen/api/roles` fetch and `roleColorMap` state from UsersPage
- update SQL query to include aggregated roles array per user via subquery
- replace single role badge with multi-badge display supporting overflow indicator
This commit is contained in:
2026-04-24 15:20:51 -04:00
parent d6b7575444
commit 70000e0761
2 changed files with 26 additions and 21 deletions
+17 -20
View File
@@ -12,7 +12,6 @@ const UsersPageClient = ({ currentUserId }) => {
const toast = useToast(); const toast = useToast();
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [roleColorMap, setRoleColorMap] = useState({});
const [editingUserId, setEditingUserId] = useState(null); const [editingUserId, setEditingUserId] = useState(null);
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
@@ -48,12 +47,23 @@ const UsersPageClient = ({ currentUserId }) => {
key: 'role', key: 'role',
label: 'Rôle', label: 'Rôle',
sortable: true, sortable: true,
render: (user) => ( render: (user) => {
<Badge color={roleColorMap[user.role?.toLowerCase()]}> const roles = user.roles || [];
{user.role} const visible = roles.slice(0, 3);
</Badge> const overflow = roles.length - 3;
), return (
skeleton: { height: 'h-6', width: '80px', className: 'rounded-full' }, <div className="flex flex-wrap gap-1">
{visible.map(role => (
<Badge key={role.id} color={role.color || undefined}>
{role.name}
</Badge>
))}
{overflow > 0 && <Badge>+{overflow}</Badge>}
{roles.length === 0 && <span className="text-xs text-neutral-400"></span>}
</div>
);
},
skeleton: { height: 'h-6', width: '140px', className: 'rounded-full' },
}, },
{ {
key: 'email_verified', key: 'email_verified',
@@ -116,19 +126,6 @@ const UsersPageClient = ({ currentUserId }) => {
} }
}; };
useEffect(() => {
fetch('/zen/api/roles', { credentials: 'include' })
.then(r => r.json())
.then(data => {
const map = {};
for (const role of data.roles || []) {
if (role.color) map[role.name.toLowerCase()] = role.color;
}
setRoleColorMap(map);
})
.catch(() => {});
}, []);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
}, [sortBy, sortOrder, pagination.page, pagination.limit]); }, [sortBy, sortOrder, pagination.page, pagination.limit]);
+9 -1
View File
@@ -129,7 +129,15 @@ async function handleListUsers(request) {
const quotedSortColumn = `"${sortColumn}"`; const quotedSortColumn = `"${sortColumn}"`;
const result = await query( const result = await query(
`SELECT id, email, name, role, image, email_verified, created_at FROM zen_auth_users ORDER BY ${quotedSortColumn} ${order} LIMIT $1 OFFSET $2`, `SELECT u.id, u.email, u.name, u.role, u.image, u.email_verified, u.created_at,
COALESCE(
(SELECT json_agg(json_build_object('id', r.id, 'name', r.name, 'color', r.color) ORDER BY r.created_at ASC)
FROM zen_auth_roles r
JOIN zen_auth_user_roles ur ON ur.role_id = r.id
WHERE ur.user_id = u.id),
'[]'::json
) AS roles
FROM zen_auth_users u ORDER BY u.${quotedSortColumn} ${order} LIMIT $1 OFFSET $2`,
[limit, offset] [limit, offset]
); );