generated from openedx/frontend-template-application
-
Notifications
You must be signed in to change notification settings - Fork 4
feat(authz): [FC-0099] create LibrariesUserManager view to manage roles for a specific user #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
brian-smith-tcril
merged 9 commits into
openedx:master
from
eduNEXT:dcoa/user-details-view
Oct 21, 2025
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
329c021
refactor: use Link from react-router in Breadcrumb links
dcoa 5100031
feat: create RoleCard component
dcoa e37902a
feat: create the user management view
dcoa 914d4b6
feat: integrate the LibrariesUserManagement view in the application
dcoa 783b3a6
style: add ts doc to buildPermissionsByRoleMatrix function
dcoa 591f12b
feat: add loading state to the cards roles
dcoa a2d200b
style: nit space
dcoa 71d4c3a
fix: nit extra comment and validation
dcoa bcdb656
test: update fireEvent with useEvent
dcoa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) => ( | ||
| <Row className="row align-items-center border px-2 py-2"> | ||
| <Col md={3}> | ||
| <span className="small font-weight-bold">{resourceLabel}</span> | ||
| </Col> | ||
| <Col> | ||
| <div className="w-100 d-flex flex-wrap"> | ||
| {actions.map(action => ( | ||
| <Chip | ||
| key={action.key} | ||
| iconBefore={actionsDictionary[action.key as ActionKey] as ComponentType} | ||
| disabled={action.disabled} | ||
| className="mr-4 my-2 px-3 bg-primary-100 border-0 permission-chip" | ||
| variant="light" | ||
| > | ||
| {action.label} | ||
| </Chip> | ||
| ))} | ||
| </div> | ||
| </Col> | ||
| </Row> | ||
| ); | ||
|
|
||
| export default PermissionRow; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: () => <svg data-testid="delete-icon" />, | ||
| Person: () => <svg data-testid="person-icon" />, | ||
| })); | ||
|
|
||
| jest.mock('./constants', () => ({ | ||
| actionsDictionary: { | ||
| view: () => <svg data-testid="view-icon" />, | ||
| manage: () => <svg data-testid="manage-icon" />, | ||
| }, | ||
| })); | ||
|
|
||
| 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(<RoleCard {...defaultProps} />); | ||
|
|
||
| // 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(<RoleCard {...defaultProps} showDelete={false} />); | ||
| expect(screen.queryByRole('button', { name: /delete role action/i })).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('handles no userCounter gracefully', () => { | ||
| renderWrapper(<RoleCard {...defaultProps} userCounter={null} />); | ||
| expect(screen.queryByTestId('person-icon')).not.toBeInTheDocument(); | ||
| expect(screen.queryByText('2')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('handles empty permissions gracefully', () => { | ||
| renderWrapper(<RoleCard {...defaultProps} permissions={[]} />); | ||
| expect(screen.queryByText('Library Resource')).not.toBeInTheDocument(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -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 { | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: missing empty line
Suggested change
|
||||||||
| objectName?: string | null; | ||||||||
| description: string; | ||||||||
| showDelete?: boolean; | ||||||||
| permissions: any[]; | ||||||||
| } | ||||||||
|
|
||||||||
| const CardTitle = ({ title, userCounter }: CardTitleProps) => ( | ||||||||
| <div className="d-flex align-items-center"> | ||||||||
| <span className="mr-4 text-primary">{title}</span> | ||||||||
| {userCounter !== null && ( | ||||||||
| <span className="d-flex align-items-center font-weight-normal"> | ||||||||
| <Icon src={Person} className="mr-1" /> | ||||||||
| {userCounter} | ||||||||
| </span> | ||||||||
| )} | ||||||||
| </div> | ||||||||
| ); | ||||||||
|
|
||||||||
| const RoleCard = ({ | ||||||||
| title, objectName, description, showDelete, permissions, userCounter, | ||||||||
| }: RoleCardProps) => { | ||||||||
| const intl = useIntl(); | ||||||||
|
|
||||||||
| return ( | ||||||||
| <Card className="container-mw-lg mx-auto mb-4"> | ||||||||
| <Card.Header | ||||||||
| title={<CardTitle title={title} userCounter={userCounter} />} | ||||||||
| subtitle={(objectName && <span className="text-info-400 lead">{objectName}</span>) || ''} | ||||||||
| actions={ | ||||||||
| showDelete && <IconButton variant="danger" alt="Delete role action" src={Delete} /> | ||||||||
| } | ||||||||
| /> | ||||||||
| <Card.Section> | ||||||||
| {description} | ||||||||
| </Card.Section> | ||||||||
| <Collapsible | ||||||||
| title={intl.formatMessage(messages['authz.permissions.title'])} | ||||||||
| > | ||||||||
| <Container> | ||||||||
| {permissions.map(({ key, label, actions }) => ( | ||||||||
| <PermissionRow | ||||||||
| key={`${title}-${key}`} | ||||||||
| resourceLabel={label} | ||||||||
| actions={actions} | ||||||||
| /> | ||||||||
|
|
||||||||
| ))} | ||||||||
| </Container> | ||||||||
| </Collapsible> | ||||||||
| </Card> | ||||||||
| ); | ||||||||
| }; | ||||||||
|
|
||||||||
| export default RoleCard; | ||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| export const ROUTES = { | ||
| LIBRARIES_TEAM_PATH: '/libraries/:libraryId', | ||
| LIBRARIES_USER_PATH: '/libraries/user/:username', | ||
| LIBRARIES_USER_PATH: '/libraries/:libraryId/:username', | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this property being actually passed down, anywhere. The only instance of RoleCard is in LibrariesUserManager, and it doesn't seem to need
userCounter. Will a use for it be added later?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is a reusable component that displays actions when is in the
LibrariesUserManagementview, and information when is in theLibrariesTeamManagerview.The use of the prop is relevant for a different PR #7