AcadeDoc/apps/client/src/features/acadenice/rbac/components/role-row.tsx
Corentin 022add9acc feat(rbac): R2.2 frontend pages settings RBAC dynamique avec PermissionMatrix
- Pages /settings/roles (liste + filtres + create), /settings/roles/:id (matrix
  permissions + danger zone), /settings/users/:userId/roles (multi-select +
  preview union)
- PermissionMatrix : groupes Mantine cards, wildcard <group>:* qui grise les
  individuals, admin:* qui court-circuite tout, indeterminate states, tooltips
  avec descriptions du catalogue
- React Query hooks pour CRUD roles + assignations user-roles, notifications
  Mantine sur succes / erreurs avec extraction du message backend
- Hook useAcadenicePermissions : best-effort lecture du claim JWT R2.1, fallback
  sur role natif Docmost (defense en profondeur — backend reste source de verite)
- i18n complet FR + EN (~80 cles)
- Vitest + Testing Library introduits dans apps/client (devDeps + config + setup)
- 22 tests couvrant matrix wildcards, list filters, detail save/delete flow,
  multi-select assignments
- Patches upstream minimaux : 3 routes ajoutees au router, 1 entree sidebar
  (visible si canManageRoles)
- Documente comme Patch 004 dans ACADENICE_PATCHES.md
2026-05-07 22:42:39 +02:00

69 lines
1.9 KiB
TypeScript

import { ActionIcon, Badge, Group, Table, Text, Tooltip } from "@mantine/core";
import { IconChevronRight, IconLock } from "@tabler/icons-react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { IRole } from "@/features/acadenice/rbac/types/rbac.types";
export interface RoleRowProps {
role: IRole;
}
export function RoleRow({ role }: RoleRowProps) {
const { t } = useTranslation();
const target = `/settings/roles/${role.id}`;
return (
<Table.Tr data-testid={`role-row-${role.id}`}>
<Table.Td>
<Group gap="sm" wrap="nowrap">
<Text fw={500} size="sm">
{role.name}
</Text>
{role.isSystemRole ? (
<Tooltip
label={t("System role — name and existence are protected")}
withArrow
>
<Badge
size="xs"
color="gray"
variant="light"
leftSection={<IconLock size={10} />}
data-testid={`role-badge-system-${role.id}`}
>
{t("system")}
</Badge>
</Tooltip>
) : (
<Badge size="xs" color="blue" variant="light">
{t("custom")}
</Badge>
)}
</Group>
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed" lineClamp={2}>
{role.description ?? "—"}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">
{typeof role.memberCount === "number" ? role.memberCount : "—"}
</Text>
</Table.Td>
<Table.Td>
<ActionIcon
component={Link}
to={target}
variant="subtle"
aria-label={t("Open role {{name}}", { name: role.name })}
data-testid={`role-open-${role.id}`}
>
<IconChevronRight size={16} />
</ActionIcon>
</Table.Td>
</Table.Tr>
);
}
export default RoleRow;