diff --git a/src/authz-module/components/AuthZTitle.test.tsx b/src/authz-module/components/AuthZTitle.test.tsx index 460cf886..1045912a 100644 --- a/src/authz-module/components/AuthZTitle.test.tsx +++ b/src/authz-module/components/AuthZTitle.test.tsx @@ -1,7 +1,12 @@ -import React from 'react'; +import { ReactNode } from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import AuthZTitle, { AuthZTitleProps } from './AuthZTitle'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + Link: ({ children, to }:{ children:ReactNode, to:string }) => {children}, +})); + describe('AuthZTitle', () => { const defaultProps: AuthZTitleProps = { activeLabel: 'Current Page', @@ -24,8 +29,9 @@ describe('AuthZTitle', () => { render(); - navLinks.forEach(({ label }) => { + navLinks.forEach(({ label, to }) => { expect(screen.getByText(label)).toBeInTheDocument(); + expect(screen.getByText(label)).toHaveAttribute('href', expect.stringContaining(to)); }); expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument(); diff --git a/src/authz-module/components/AuthZTitle.tsx b/src/authz-module/components/AuthZTitle.tsx index 6224ea93..f0319fca 100644 --- a/src/authz-module/components/AuthZTitle.tsx +++ b/src/authz-module/components/AuthZTitle.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; import { Breadcrumb, Col, Container, Row, Button, Badge, } from '@openedx/paragon'; @@ -26,6 +27,7 @@ const AuthZTitle = ({ }: AuthZTitleProps) => ( 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 };