diff --git a/src/authz-module/components/RoleCard/PermissionsRow.tsx b/src/authz-module/components/RoleCard/PermissionsRow.tsx
new file mode 100644
index 00000000..7c1a0073
--- /dev/null
+++ b/src/authz-module/components/RoleCard/PermissionsRow.tsx
@@ -0,0 +1,41 @@
+import { ComponentType } from 'react';
+import {
+ Chip, Col, Row,
+} from '@openedx/paragon';
+import { actionsDictionary, ActionKey } from './constants';
+
+interface Action {
+ key: string;
+ label?: string;
+ disabled?: boolean;
+}
+
+interface PermissionRowProps {
+ resourceLabel: string;
+ actions: Action[];
+}
+
+const PermissionRow = ({ resourceLabel, actions }: PermissionRowProps) => (
+
+
+ {resourceLabel}
+
+
+
+ {actions.map(action => (
+
+ {action.label}
+
+ ))}
+
+
+
+);
+
+export default PermissionRow;
diff --git a/src/authz-module/components/RoleCard/constants.ts b/src/authz-module/components/RoleCard/constants.ts
new file mode 100644
index 00000000..389ed003
--- /dev/null
+++ b/src/authz-module/components/RoleCard/constants.ts
@@ -0,0 +1,18 @@
+import {
+ Add, Delete, DownloadDone, Edit, ManageAccounts, Sync, Tag, Visibility,
+} from '@openedx/paragon/icons';
+
+export const actionsDictionary = {
+ create: Add,
+ edit: Edit,
+ delete: Delete,
+ import: Sync,
+ publish: DownloadDone,
+ view: Visibility,
+ reuse: Sync,
+ tag: Tag,
+ team: ManageAccounts,
+};
+
+export type ActionKey = keyof typeof actionsDictionary;
+export const actionKeys = Object.keys(actionsDictionary);
diff --git a/src/authz-module/components/RoleCard/index.test.tsx b/src/authz-module/components/RoleCard/index.test.tsx
new file mode 100644
index 00000000..2cc22900
--- /dev/null
+++ b/src/authz-module/components/RoleCard/index.test.tsx
@@ -0,0 +1,89 @@
+import { screen } from '@testing-library/react';
+import { renderWrapper } from '@src/setupTest';
+import userEvent from '@testing-library/user-event';
+import RoleCard from '.';
+
+jest.mock('@openedx/paragon/icons', () => ({
+ Delete: () => ,
+ Person: () => ,
+}));
+
+jest.mock('./constants', () => ({
+ actionsDictionary: {
+ view: () => ,
+ manage: () => ,
+ },
+}));
+
+describe('RoleCard', () => {
+ const defaultProps = {
+ title: 'Admin',
+ objectName: 'Test Library',
+ description: 'Can manage everything',
+ showDelete: true,
+ userCounter: 2,
+ permissions: [
+ {
+ key: 'library',
+ label: 'Library Resource',
+ actions: [
+ { key: 'view', label: 'View' },
+ { key: 'manage', label: 'Manage', disabled: true },
+ ],
+ },
+ ],
+ };
+
+ it('renders all role card sections correctly', async () => {
+ const user = userEvent.setup();
+ renderWrapper();
+
+ // Title
+ expect(screen.getByText('Admin')).toBeInTheDocument();
+
+ // User counter with icon
+ expect(screen.getByText('2')).toBeInTheDocument();
+ expect(screen.getByTestId('person-icon')).toBeInTheDocument();
+
+ // Subtitle (object name)
+ expect(screen.getByText('Test Library')).toBeInTheDocument();
+
+ // Description
+ expect(screen.getByText('Can manage everything')).toBeInTheDocument();
+
+ // Delete button
+ expect(screen.getByRole('button', { name: /delete role action/i })).toBeInTheDocument();
+
+ // Collapsible title
+ expect(screen.getByText('Permissions')).toBeInTheDocument();
+
+ await user.click(screen.getByText('Permissions'));
+
+ // Resource label
+ expect(screen.getByText('Library Resource')).toBeInTheDocument();
+
+ // Action chips
+ expect(screen.getByText('View')).toBeInTheDocument();
+ expect(screen.getByText('Manage')).toBeInTheDocument();
+
+ // Action icons
+ expect(screen.getByTestId('view-icon')).toBeInTheDocument();
+ expect(screen.getByTestId('manage-icon')).toBeInTheDocument();
+ });
+
+ it('does not show delete button when showDelete is false', () => {
+ renderWrapper();
+ expect(screen.queryByRole('button', { name: /delete role action/i })).not.toBeInTheDocument();
+ });
+
+ it('handles no userCounter gracefully', () => {
+ renderWrapper();
+ expect(screen.queryByTestId('person-icon')).not.toBeInTheDocument();
+ expect(screen.queryByText('2')).not.toBeInTheDocument();
+ });
+
+ it('handles empty permissions gracefully', () => {
+ renderWrapper();
+ expect(screen.queryByText('Library Resource')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/authz-module/components/RoleCard/index.tsx b/src/authz-module/components/RoleCard/index.tsx
new file mode 100644
index 00000000..9b2f9f3e
--- /dev/null
+++ b/src/authz-module/components/RoleCard/index.tsx
@@ -0,0 +1,68 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ Card, Collapsible, Container, Icon, IconButton,
+} from '@openedx/paragon';
+import { Delete, Person } from '@openedx/paragon/icons';
+import PermissionRow from './PermissionsRow';
+import messages from './messages';
+
+interface CardTitleProps {
+ title: string;
+ userCounter?: number | null;
+}
+
+interface RoleCardProps extends CardTitleProps {
+ objectName?: string | null;
+ description: string;
+ showDelete?: boolean;
+ permissions: any[];
+}
+
+const CardTitle = ({ title, userCounter }: CardTitleProps) => (
+
+ {title}
+ {userCounter !== null && (
+
+
+ {userCounter}
+
+ )}
+
+);
+
+const RoleCard = ({
+ title, objectName, description, showDelete, permissions, userCounter,
+}: RoleCardProps) => {
+ const intl = useIntl();
+
+ return (
+
+ }
+ subtitle={(objectName && {objectName}) || ''}
+ actions={
+ showDelete &&
+ }
+ />
+
+ {description}
+
+
+
+ {permissions.map(({ key, label, actions }) => (
+
+
+ ))}
+
+
+
+ );
+};
+
+export default RoleCard;
diff --git a/src/authz-module/components/RoleCard/messages.ts b/src/authz-module/components/RoleCard/messages.ts
new file mode 100644
index 00000000..e61fb1f9
--- /dev/null
+++ b/src/authz-module/components/RoleCard/messages.ts
@@ -0,0 +1,51 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'authz.permissions.title': {
+ id: 'authz.permissions.title',
+ defaultMessage: 'Permissions',
+ description: 'Title for the permissions section in the role card',
+ },
+ 'authz.permissions.actions.create': {
+ id: 'authz.permissions.actions.create',
+ defaultMessage: 'Create {resource}',
+ description: 'Default label for the create action',
+ },
+ 'authz.permissions.actions.edit': {
+ id: 'authz.permissions.actions.edit',
+ defaultMessage: 'Edit {resource}',
+ description: 'Default label for the edit action',
+ },
+ 'authz.permissions.actions.import': {
+ id: 'authz.permissions.actions.import',
+ defaultMessage: 'Import {resource}',
+ description: 'Default label for the import action',
+ },
+ 'authz.permissions.actions.delete': {
+ id: 'authz.permissions.actions.delete',
+ defaultMessage: 'Delete {resource}',
+ description: 'Default label for the delete action',
+ },
+ 'authz.permissions.actions.manage': {
+ id: 'authz.permissions.actions.manage',
+ defaultMessage: 'Manage {resource}',
+ description: 'Default label for the manage action',
+ },
+ 'authz.permissions.actions.publish': {
+ id: 'authz.permissions.actions.publish',
+ defaultMessage: 'Publish {resource}',
+ description: 'Default label for the publish action',
+ },
+ 'authz.permissions.actions.view': {
+ id: 'authz.permissions.actions.view',
+ defaultMessage: 'View {resource}',
+ description: 'Default label for the view action',
+ },
+ 'authz.permissions.actions.reuse': {
+ id: 'authz.permissions.actions.reuse',
+ defaultMessage: 'Reuse {resource}',
+ description: 'Default label for the reuse action',
+ },
+});
+
+export default messages;
diff --git a/src/authz-module/constants.ts b/src/authz-module/constants.ts
index 78641474..62feb715 100644
--- a/src/authz-module/constants.ts
+++ b/src/authz-module/constants.ts
@@ -1,4 +1,4 @@
export const ROUTES = {
LIBRARIES_TEAM_PATH: '/libraries/:libraryId',
- LIBRARIES_USER_PATH: '/libraries/user/:username',
+ LIBRARIES_USER_PATH: '/libraries/:libraryId/:username',
};
diff --git a/src/authz-module/index.scss b/src/authz-module/index.scss
index 70ca529f..17f9719a 100644
--- a/src/authz-module/index.scss
+++ b/src/authz-module/index.scss
@@ -7,4 +7,31 @@
.tab-content {
background-color: var(--pgn-color-light-200);
}
-}
+
+ .collapsible-card {
+ border: none;
+
+ .collapsible-body {
+ padding: 0;
+ }
+ }
+
+ .collapsible-trigger {
+ background-color: var(--pgn-color-info-100);
+ border: none;
+ border-radius: 0 !important;
+ color: var(--pgn-color-primary-base);
+ padding: 1rem 2rem 1rem 1rem;
+ }
+
+ .permission-chip {
+ .pgn__chip__label {
+ font-weight: var(--pgn-typography-font-weight-base);
+ }
+
+ svg {
+ width: var(--pgn-size-icon-xs);
+ height: var(--pgn-size-icon-xs);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/authz-module/index.test.tsx b/src/authz-module/index.test.tsx
index 5b5ab025..8963adce 100644
--- a/src/authz-module/index.test.tsx
+++ b/src/authz-module/index.test.tsx
@@ -1,11 +1,16 @@
import { ComponentType, lazy } from 'react';
import { render, screen, waitFor } from '@testing-library/react';
-import { MemoryRouter } from 'react-router-dom';
+import { MemoryRouter, Outlet } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { initializeMockApp } from '@edx/frontend-platform/testing';
import AuthZModule from './index';
-// eslint-disable-next-line no-promise-executor-return
-jest.mock('./libraries-manager/LibrariesTeamManager', () => lazy(() => new Promise<{ default: ComponentType }>(resolve => setTimeout(() => resolve({ default: () => Loaded
}), 100))));
+jest.mock('./libraries-manager', () => ({
+ // eslint-disable-next-line no-promise-executor-return
+ LibrariesLayout: lazy(() => new Promise<{ default: ComponentType }>(resolve => setTimeout(() => resolve({ default: () =>
}), 100))),
+ LibrariesTeamManager: () => Libraries Team Page
,
+ LibrariesUserManager: () => Libraries User Page
,
+}));
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
@@ -16,6 +21,12 @@ const createTestQueryClient = () => new QueryClient({
});
describe('AuthZModule', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ initializeMockApp({
+ authenticatedUser: { username: 'testuser' },
+ });
+ });
it('renders LoadingPage then LibrariesTeamManager when route matches', async () => {
const queryClient = createTestQueryClient();
const path = '/libraries/lib:123';
@@ -34,4 +45,20 @@ describe('AuthZModule', () => {
expect(screen.getByTestId('libraries-manager')).toBeInTheDocument();
});
});
+
+ it('renders LoadingPage then LibrariesUserManager when user route matches', async () => {
+ const queryClient = createTestQueryClient();
+ const path = '/libraries/lib:123/testuser';
+
+ render(
+
+
+
+
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.getByTestId('libraries-user-manager')).toBeInTheDocument();
+ });
+ });
});
diff --git a/src/authz-module/index.tsx b/src/authz-module/index.tsx
index 2812df37..3f7d0172 100644
--- a/src/authz-module/index.tsx
+++ b/src/authz-module/index.tsx
@@ -2,7 +2,7 @@ import { Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from '@edx/frontend-platform/react';
import LoadingPage from '@src/components/LoadingPage';
-import { LibrariesTeamManager } from './libraries-manager';
+import { LibrariesTeamManager, LibrariesUserManager, LibrariesLayout } from './libraries-manager';
import { ROUTES } from './constants';
import './index.scss';
@@ -11,7 +11,10 @@ const AuthZModule = () => (
}>
- } />
+ }>
+ } />
+ } />
+
diff --git a/src/authz-module/libraries-manager/LibrariesTeamManager.tsx b/src/authz-module/libraries-manager/LibrariesTeamManager.tsx
index ae81a3e6..48c13dcb 100644
--- a/src/authz-module/libraries-manager/LibrariesTeamManager.tsx
+++ b/src/authz-module/libraries-manager/LibrariesTeamManager.tsx
@@ -3,11 +3,11 @@ import { Tab, Tabs } from '@openedx/paragon';
import { useLibrary } from '@src/authz-module/data/hooks';
import TeamTable from './components/TeamTable';
import AuthZLayout from '../components/AuthZLayout';
-import { LibraryAuthZProvider, useLibraryAuthZ } from './context';
+import { useLibraryAuthZ } from './context';
import messages from './messages';
-const LibrariesAuthZTeamView = () => {
+const LibrariesTeamManager = () => {
const intl = useIntl();
const { libraryId } = useLibraryAuthZ();
const { data: library } = useLibrary(libraryId);
@@ -42,10 +42,5 @@ const LibrariesAuthZTeamView = () => {
);
};
-const LibrariesTeamManager = () => (
-
-
-
-);
export default LibrariesTeamManager;
diff --git a/src/authz-module/libraries-manager/LibrariesUserManager.test.tsx b/src/authz-module/libraries-manager/LibrariesUserManager.test.tsx
new file mode 100644
index 00000000..bbd08530
--- /dev/null
+++ b/src/authz-module/libraries-manager/LibrariesUserManager.test.tsx
@@ -0,0 +1,90 @@
+import { useParams } from 'react-router-dom';
+import { screen } from '@testing-library/react';
+import { renderWrapper } from '@src/setupTest';
+import LibrariesUserManager from './LibrariesUserManager';
+import { useLibraryAuthZ } from './context';
+import { useLibrary, useTeamMembers } from '../data/hooks';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: jest.fn(),
+}));
+
+jest.mock('./context', () => ({
+ useLibraryAuthZ: jest.fn(),
+}));
+
+jest.mock('../data/hooks', () => ({
+ useLibrary: jest.fn(),
+ useTeamMembers: jest.fn(),
+}));
+jest.mock('../components/RoleCard', () => ({
+ __esModule: true,
+ default: ({ title, description }: { title: string, description: string }) => (
+
+
{title}
+
{description}
+
+ ),
+}));
+
+describe('LibrariesUserManager', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ // Mock route params
+ (useParams as jest.Mock).mockReturnValue({ username: 'testuser' });
+
+ // Mock library authz context
+ (useLibraryAuthZ as jest.Mock).mockReturnValue({
+ libraryId: 'lib:123',
+ permissions: [{ key: 'view' }, { key: 'reuse' }],
+ roles: [
+ {
+ role: 'admin',
+ name: 'Admin',
+ description: 'Administrator Role',
+ permissions: ['view', 'reuse'],
+ },
+ ],
+ resources: [
+ { key: 'library', label: 'Library', description: '' },
+ ],
+ });
+
+ // Mock library data
+ (useLibrary as jest.Mock).mockReturnValue({
+ data: {
+ title: 'Test Library',
+ org: 'Test Org',
+ },
+ });
+
+ // Mock team members
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: [
+ {
+ username: 'testuser',
+ email: 'testuser@example.com',
+ roles: ['admin'],
+ },
+ ],
+ });
+ });
+
+ it('renders the user roles correctly', () => {
+ renderWrapper();
+
+ // Breadcrumb check
+ expect(screen.getByText('Manage Access')).toBeInTheDocument();
+ expect(screen.getByText('Library Team Management')).toBeInTheDocument();
+ expect(screen.getByRole('listitem', { current: 'page' })).toHaveTextContent('testuser');
+ // Page title and subtitle
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('testuser');
+ expect(screen.getByRole('paragraph')).toHaveTextContent('testuser@example.com');
+
+ // RoleCard rendering
+ expect(screen.getByTestId('role-card')).toHaveTextContent('Admin');
+ expect(screen.getByTestId('role-card')).toHaveTextContent('Administrator Role');
+ });
+});
diff --git a/src/authz-module/libraries-manager/LibrariesUserManager.tsx b/src/authz-module/libraries-manager/LibrariesUserManager.tsx
new file mode 100644
index 00000000..9952ae1c
--- /dev/null
+++ b/src/authz-module/libraries-manager/LibrariesUserManager.tsx
@@ -0,0 +1,65 @@
+import { useMemo } from 'react';
+import { useParams } from 'react-router-dom';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Container, Skeleton } from '@openedx/paragon';
+import { ROUTES } from '@src/authz-module/constants';
+import AuthZLayout from '../components/AuthZLayout';
+import { useLibraryAuthZ } from './context';
+import RoleCard from '../components/RoleCard';
+import { useLibrary, useTeamMembers } from '../data/hooks';
+import { buildPermissionsByRoleMatrix } from './utils';
+
+import messages from './messages';
+
+const LibrariesUserManager = () => {
+ const intl = useIntl();
+ const { username } = useParams();
+ const {
+ libraryId, permissions, roles, resources,
+ } = useLibraryAuthZ();
+ const { data: library } = useLibrary(libraryId);
+ const rootBreadcrumb = intl.formatMessage(messages['library.authz.breadcrumb.root']) || '';
+ const pageManageTitle = intl.formatMessage(messages['library.authz.manage.page.title']);
+
+ const { data: teamMembers, isLoading } = useTeamMembers(libraryId);
+ const user = teamMembers?.find(member => member.username === username);
+ const userRoles = useMemo(() => {
+ const assignedRoles = roles.filter(role => user?.roles.includes(role.role))
+ .map(role => ({
+ ...role,
+ permissions: buildPermissionsByRoleMatrix({
+ rolePermissions: role.permissions, permissions, resources, intl,
+ }),
+ }));
+ return assignedRoles;
+ }, [roles, user?.roles, permissions, resources, intl]);
+
+ return (
+
+
{user?.email}}
+ actions={[]}
+ >
+
+ {isLoading ? : null}
+ {userRoles && userRoles.map(role => (
+
+ ))}
+
+
+
+ );
+};
+
+export default LibrariesUserManager;
diff --git a/src/authz-module/libraries-manager/components/TeamTable.test.tsx b/src/authz-module/libraries-manager/components/TeamTable.test.tsx
index a802e22e..3189db97 100644
--- a/src/authz-module/libraries-manager/components/TeamTable.test.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable.test.tsx
@@ -1,6 +1,5 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { ROUTES } from '@src/authz-module/constants';
import { renderWrapper } from '@src/setupTest';
import { useTeamMembers } from '@src/authz-module/data/hooks';
import TeamTable from './TeamTable';
@@ -91,7 +90,7 @@ describe('TeamTable', () => {
await userEvent.click(editButtons[0]);
expect(mockNavigate).toHaveBeenCalledWith(
- `/authz/${ROUTES.LIBRARIES_USER_PATH.replace(':username', 'bob')}`,
+ `/authz/libraries/${mockAuthZ.libraryId}/bob`,
);
});
diff --git a/src/authz-module/libraries-manager/components/TeamTable.tsx b/src/authz-module/libraries-manager/components/TeamTable.tsx
index cca1d3c0..ff1248bc 100644
--- a/src/authz-module/libraries-manager/components/TeamTable.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable.tsx
@@ -5,7 +5,6 @@ import {
} from '@openedx/paragon';
import { Edit } from '@openedx/paragon/icons';
import { TableCellValue, TeamMember } from '@src/types';
-import { ROUTES } from '@src/authz-module/constants';
import { useTeamMembers } from '@src/authz-module/data/hooks';
import { useLibraryAuthZ } from '../context';
import messages from './messages';
@@ -81,8 +80,7 @@ const TeamTable = () => {
iconBefore={Edit}
variant="link"
size="sm"
- // TODO: update the view with the team member view
- onClick={() => navigate(`/authz/${ROUTES.LIBRARIES_USER_PATH.replace(':username', row.original.username)}`)}
+ onClick={() => navigate(`/authz/libraries/${libraryId}/${row.original.username}`)}
>
{intl.formatMessage(messages['authz.libraries.team.table.edit.action'])}
diff --git a/src/authz-module/libraries-manager/index.ts b/src/authz-module/libraries-manager/index.ts
deleted file mode 100644
index b12092e0..00000000
--- a/src/authz-module/libraries-manager/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import LibrariesTeamManager from './LibrariesTeamManager';
-
-export {
- LibrariesTeamManager,
-};
diff --git a/src/authz-module/libraries-manager/index.tsx b/src/authz-module/libraries-manager/index.tsx
new file mode 100644
index 00000000..ed1fc1e9
--- /dev/null
+++ b/src/authz-module/libraries-manager/index.tsx
@@ -0,0 +1,12 @@
+import { Outlet } from 'react-router-dom';
+import LibrariesTeamManager from './LibrariesTeamManager';
+import LibrariesUserManager from './LibrariesUserManager';
+import { LibraryAuthZProvider } from './context';
+
+const LibrariesLayout = () => ();
+
+export {
+ LibrariesLayout,
+ LibrariesTeamManager,
+ LibrariesUserManager,
+};
diff --git a/src/authz-module/libraries-manager/utils.test.ts b/src/authz-module/libraries-manager/utils.test.ts
new file mode 100644
index 00000000..c3c4a670
--- /dev/null
+++ b/src/authz-module/libraries-manager/utils.test.ts
@@ -0,0 +1,23 @@
+import { buildPermissionsByRoleMatrix } from './utils';
+
+describe('buildPermissionsByRoleMatrix', () => {
+ it('returns permissions matrix for given role', () => {
+ const rolePermissions = ['create_library'];
+ const permissions = [
+ { key: 'create_library', resource: 'library', label: 'Create Library' },
+ { key: 'edit_library', resource: 'library', label: 'Edit Library' },
+ ];
+ const resources = [
+ { key: 'library', label: 'Library', description: '' },
+ ];
+
+ const intl = { formatMessage: jest.fn((msg: any) => msg.defaultMessage) };
+ const matrix = buildPermissionsByRoleMatrix({
+ rolePermissions, permissions, resources, intl,
+ }) as Array<{ key: string; actions: Array<{ disabled: boolean }> }>;
+ expect(matrix[0].key).toBe('library');
+ expect(matrix[0].actions.length).toBe(2);
+ expect(matrix[0].actions[0].disabled).toBe(false);
+ expect(matrix[0].actions[1].disabled).toBe(true);
+ });
+});
diff --git a/src/authz-module/libraries-manager/utils.ts b/src/authz-module/libraries-manager/utils.ts
new file mode 100644
index 00000000..bf27f606
--- /dev/null
+++ b/src/authz-module/libraries-manager/utils.ts
@@ -0,0 +1,45 @@
+import { actionKeys } from '@src/authz-module/components/RoleCard/constants';
+import actionMessages from '../components/RoleCard/messages';
+
+/**
+ * Builds a permission matrix for a role.
+ *
+ * Builds a permission matrix grouped by resource, mapping each action to its display label
+ * and enabled/disabled state based on the role's allowed permissions.
+ *
+ * @param rolePermissions - Array of permission keys allowed for the current role.
+ * @param permissions - Permissions metadata.
+ * @param resources - Resources metadata.
+ * @param intl - the i18n function to enable label translations.
+ * @returns An array of permission groupings by resource with action-level details.
+ */
+const buildPermissionsByRoleMatrix = ({
+ rolePermissions, permissions, resources, intl,
+}) => {
+ const permissionsMatrix = {};
+ const allowedPermissions = new Set(rolePermissions);
+
+ permissions.forEach((permission) => {
+ const resourceLabel = resources.find(r => r.key === permission.resource)?.label || permission.resource;
+ const actionKey = actionKeys.find(action => permission.key.includes(action));
+ let messageKey = `authz.permissions.actions.${actionKey}`;
+ let messageResource = '';
+
+ permissionsMatrix[permission.resource] = permissionsMatrix[permission.resource]
+ || { key: permission.resource, label: resourceLabel, actions: [] };
+
+ if (actionKey === 'tag' || actionKey === 'team') {
+ messageKey = 'authz.permissions.actions.manage';
+ messageResource = actionKey === 'tag' ? 'Tags' : messageResource;
+ }
+
+ permissionsMatrix[permission.resource].actions.push({
+ key: actionKey,
+ label: permission.label || intl.formatMessage(actionMessages[messageKey], { resource: messageResource }),
+ disabled: !allowedPermissions.has(permission.key),
+ });
+ });
+ return Object.values(permissionsMatrix);
+};
+
+export { buildPermissionsByRoleMatrix };