feat: [FC-0099] Add a new role to a library member (#9)
* refactor: support components as action & connect API to add roles to users * fix: the value of LIBRARY_AUTHZ_SCOPE was misspelled * feat: add role assignment functionality with modal and trigger components * feat: add success toast message for role assignment and update user management logic * refactor: update handleAddRole to display all roles * refactor: group under AssignNewRoleModal * test: add unit tests for AssignNewRoleModal and AssignNewRoleTrigger components * fix: remove duplicated exports after rebase --------- Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
This commit is contained in:
@@ -24,7 +24,6 @@ export interface AssignTeamMembersRoleRequest {
|
||||
scope: string;
|
||||
}
|
||||
|
||||
// TODO: replece api path once is created
|
||||
export const getTeamMembers = async (object: string): Promise<TeamMember[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getApiUrl(`/api/authz/v1/roles/users/?scope=${object}`));
|
||||
return camelCaseObject(data.results);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ROUTES } from '@src/authz-module/constants';
|
||||
import AuthZLayout from '../components/AuthZLayout';
|
||||
import { useLibraryAuthZ } from './context';
|
||||
import RoleCard from '../components/RoleCard';
|
||||
import { AssignNewRoleTrigger } from './components/AssignNewRoleModal';
|
||||
import { useLibrary, useTeamMembers } from '../data/hooks';
|
||||
import { buildPermissionsByRoleMatrix } from './utils';
|
||||
|
||||
@@ -15,7 +16,7 @@ const LibrariesUserManager = () => {
|
||||
const intl = useIntl();
|
||||
const { username } = useParams();
|
||||
const {
|
||||
libraryId, permissions, roles, resources,
|
||||
libraryId, permissions, roles, resources, canManageTeam,
|
||||
} = useLibraryAuthZ();
|
||||
const { data: library } = useLibrary(libraryId);
|
||||
const rootBreadcrumb = intl.formatMessage(messages['library.authz.breadcrumb.root']) || '';
|
||||
@@ -42,7 +43,13 @@ const LibrariesUserManager = () => {
|
||||
activeLabel={user?.username || ''}
|
||||
pageTitle={user?.username || ''}
|
||||
pageSubtitle={<p>{user?.email}</p>}
|
||||
actions={[]}
|
||||
actions={user && canManageTeam
|
||||
? [<AssignNewRoleTrigger
|
||||
username={user.username}
|
||||
libraryId={libraryId}
|
||||
currentUserRoles={userRoles.map(role => role.role)}
|
||||
/>]
|
||||
: []}
|
||||
>
|
||||
<Container className="bg-light-200 p-5">
|
||||
{isLoading ? <Skeleton count={2} height={200} /> : null}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWrapper } from '@src/setupTest';
|
||||
import { Role } from 'types';
|
||||
import AssignNewRoleModal from './AssignNewRoleModal';
|
||||
|
||||
describe('AssignNewRoleModal', () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
isLoading: false,
|
||||
roleOptions: [
|
||||
{
|
||||
role: 'instructor',
|
||||
name: 'Instructor',
|
||||
description: 'Can create and edit content',
|
||||
userCount: 5,
|
||||
permissions: ['view', 'edit'],
|
||||
},
|
||||
{
|
||||
role: 'admin',
|
||||
name: 'Administrator',
|
||||
description: 'Full access to the library',
|
||||
userCount: 2,
|
||||
permissions: ['view', 'edit', 'delete', 'manage'],
|
||||
},
|
||||
{
|
||||
role: 'viewer',
|
||||
name: 'Viewer',
|
||||
description: 'Can only view content',
|
||||
userCount: 10,
|
||||
permissions: ['view'],
|
||||
},
|
||||
] as Role[],
|
||||
selectedRole: '',
|
||||
close: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
handleChangeSelectedRole: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderComponent = (props = {}) => {
|
||||
const finalProps = { ...defaultProps, ...props };
|
||||
return renderWrapper(<AssignNewRoleModal {...finalProps} />);
|
||||
};
|
||||
|
||||
describe('Modal Visibility', () => {
|
||||
it('renders modal when isOpen is true', () => {
|
||||
renderComponent({ isOpen: true });
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add New Role')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render modal when isOpen is false', () => {
|
||||
renderComponent({ isOpen: false });
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Add New Role')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Structure', () => {
|
||||
it('renders modal header with correct title', () => {
|
||||
renderComponent({ isOpen: true });
|
||||
|
||||
expect(screen.getByText('Add New Role')).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders close button in header', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Role Selection Form', () => {
|
||||
it('renders role selection form with correct label', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText('Roles')).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default option', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText('Select a role')).toBeInTheDocument();
|
||||
expect(screen.getByRole('option', { name: 'Select a role' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders all role options', () => {
|
||||
renderComponent();
|
||||
|
||||
defaultProps.roleOptions.forEach((role) => {
|
||||
expect(screen.getByRole('option', { name: role.name })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays selected role correctly', () => {
|
||||
renderComponent({ selectedRole: 'instructor' });
|
||||
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
expect(selectElement).toHaveValue('instructor');
|
||||
});
|
||||
|
||||
it('calls handleChangeSelectedRole when role selection changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
await user.selectOptions(selectElement, 'admin');
|
||||
|
||||
expect(defaultProps.handleChangeSelectedRole).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Action Buttons', () => {
|
||||
it('renders Cancel button with correct text', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Save button with correct text when not loading', () => {
|
||||
renderComponent({ isLoading: false });
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Save button with loading text when loading', () => {
|
||||
renderComponent({ isLoading: true });
|
||||
|
||||
expect(screen.getByRole('button', { name: /saving/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls close when Cancel button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(defaultProps.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onSave when Save button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({ selectedRole: 'instructor' });
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: /save/i });
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(defaultProps.onSave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button States', () => {
|
||||
it('disables Save button when no role is selected', () => {
|
||||
renderComponent({ selectedRole: '' });
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: /save/i });
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Save button when role is selected and not loading', () => {
|
||||
renderComponent({ selectedRole: 'instructor', isLoading: false });
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: /save/i });
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Save button when loading', () => {
|
||||
renderComponent({ selectedRole: 'instructor', isLoading: true });
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: /saving/i });
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Cancel button when loading', () => {
|
||||
renderComponent({ isLoading: true });
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
expect(cancelButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Cancel button when not loading', () => {
|
||||
renderComponent({ isLoading: false });
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
expect(cancelButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Close Behavior', () => {
|
||||
it('does not call close when modal header close is clicked during loading', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({ isLoading: true });
|
||||
|
||||
const headerCloseButton = screen.getByRole('button', { name: /close/i });
|
||||
await user.click(headerCloseButton);
|
||||
|
||||
expect(defaultProps.close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls close when modal header close is clicked and not loading', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({ isLoading: false });
|
||||
|
||||
const headerCloseButton = screen.getByRole('button', { name: /close/i });
|
||||
await user.click(headerCloseButton);
|
||||
|
||||
expect(defaultProps.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { FC } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Button, Form, ModalDialog,
|
||||
} from '@openedx/paragon';
|
||||
import { Role } from 'types';
|
||||
import messages from '../messages';
|
||||
|
||||
interface AssignNewRoleModalProps {
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
roleOptions: Role[];
|
||||
selectedRole: string;
|
||||
close: () => void;
|
||||
onSave: () => void;
|
||||
handleChangeSelectedRole: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => void;
|
||||
}
|
||||
|
||||
const AssignNewRoleModal: FC<AssignNewRoleModalProps> = ({
|
||||
isOpen, isLoading, selectedRole, roleOptions, close, onSave, handleChangeSelectedRole,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<ModalDialog
|
||||
title={intl.formatMessage(messages['libraries.authz.manage.assign.new.role.title'])}
|
||||
isOpen={isOpen}
|
||||
onClose={isLoading ? () => {} : close}
|
||||
size="lg"
|
||||
variant="dark"
|
||||
hasCloseButton
|
||||
isOverflowVisible={false}
|
||||
zIndex={5}
|
||||
>
|
||||
<ModalDialog.Header className="bg-primary-500 text-light-100">
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.assign.new.role.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body className="my-4">
|
||||
<Form.Group controlId="role_options">
|
||||
<Form.Label>{intl.formatMessage(messages['library.authz.team.table.roles'])}</Form.Label>
|
||||
<Form.Control as="select" name="role" value={selectedRole} onChange={handleChangeSelectedRole}>
|
||||
<option value="" disabled>Select a role</option>
|
||||
{roleOptions.map((role) => <option key={role.role} value={role.role}>{role.name}</option>)}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary" disabled={isLoading}>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.cancel.button'])}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
className="px-4"
|
||||
onClick={() => onSave()}
|
||||
disabled={!selectedRole || isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? intl.formatMessage(messages['libraries.authz.manage.saving.button'])
|
||||
: intl.formatMessage(messages['libraries.authz.manage.save.button'])}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignNewRoleModal;
|
||||
@@ -0,0 +1,265 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWrapper } from '@src/setupTest';
|
||||
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
|
||||
import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks';
|
||||
import AssignNewRoleTrigger from './AssignNewRoleTrigger';
|
||||
|
||||
jest.mock('@src/authz-module/libraries-manager/context', () => ({
|
||||
useLibraryAuthZ: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@src/authz-module/data/hooks', () => ({
|
||||
useAssignTeamMembersRole: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./AssignNewRoleModal', () => {
|
||||
const MockAssignNewRoleModal = ({
|
||||
isOpen,
|
||||
close,
|
||||
onSave,
|
||||
isLoading,
|
||||
roleOptions,
|
||||
selectedRole,
|
||||
handleChangeSelectedRole,
|
||||
}: any) => (isOpen ? (
|
||||
<div data-testid="assign-new-role-modal">
|
||||
<h2>Add New Role</h2>
|
||||
<select data-testid="role-select" value={selectedRole} onChange={handleChangeSelectedRole}>
|
||||
<option value="">Select a role</option>
|
||||
{roleOptions.map((role: any) => (
|
||||
<option key={role.role} value={role.role}>
|
||||
{role.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" onClick={onSave} disabled={isLoading} data-testid="save-button">
|
||||
{isLoading ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button type="button" onClick={close} data-testid="cancel-button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : null);
|
||||
MockAssignNewRoleModal.displayName = 'AssignNewRoleModal';
|
||||
return MockAssignNewRoleModal;
|
||||
});
|
||||
|
||||
const mockUseLibraryAuthZ = useLibraryAuthZ as jest.Mock;
|
||||
const mockUseAssignTeamMembersRole = useAssignTeamMembersRole as jest.Mock;
|
||||
|
||||
describe('AssignNewRoleTrigger', () => {
|
||||
const defaultProps = {
|
||||
username: 'testuser',
|
||||
libraryId: 'lib:test-library',
|
||||
currentUserRoles: ['instructor'],
|
||||
};
|
||||
|
||||
const mockRoles = [
|
||||
{
|
||||
role: 'instructor',
|
||||
name: 'Instructor',
|
||||
description: 'Can create and edit content',
|
||||
},
|
||||
{
|
||||
role: 'admin',
|
||||
name: 'Administrator',
|
||||
description: 'Full access to the library',
|
||||
},
|
||||
{
|
||||
role: 'viewer',
|
||||
name: 'Viewer',
|
||||
description: 'Can only view content',
|
||||
},
|
||||
];
|
||||
|
||||
const mockMutate = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseLibraryAuthZ.mockReturnValue({
|
||||
roles: mockRoles,
|
||||
});
|
||||
|
||||
mockUseAssignTeamMembersRole.mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: false,
|
||||
});
|
||||
});
|
||||
|
||||
const renderComponent = (props = {}) => {
|
||||
const finalProps = { ...defaultProps, ...props };
|
||||
return renderWrapper(<AssignNewRoleTrigger {...finalProps} />);
|
||||
};
|
||||
|
||||
describe('Initial Render', () => {
|
||||
it('renders the trigger button with correct text', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByRole('button', { name: /add new role/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show modal initially', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByTestId('assign-new-role-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show toast initially', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByText(/role added successfully/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Interaction', () => {
|
||||
it('opens modal when trigger button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new role/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
expect(screen.getByTestId('assign-new-role-modal')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /add new role/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes modal when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
// Open modal
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
expect(screen.getByTestId('assign-new-role-modal')).toBeInTheDocument();
|
||||
|
||||
// Close modal
|
||||
await user.click(screen.getByTestId('cancel-button'));
|
||||
expect(screen.queryByTestId('assign-new-role-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Role Selection and Assignment', () => {
|
||||
it('updates selected role when role select changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
|
||||
const roleSelect = screen.getByTestId('role-select');
|
||||
await user.selectOptions(roleSelect, 'admin');
|
||||
|
||||
expect(roleSelect).toHaveValue('admin');
|
||||
});
|
||||
|
||||
it('calls assignTeamMembersRole with correct data when save is clicked', async () => {
|
||||
const choosenRole = mockRoles[1].role; // 'admin'
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
|
||||
// Select a role
|
||||
const roleSelect = screen.getByTestId('role-select');
|
||||
await user.selectOptions(roleSelect, choosenRole);
|
||||
|
||||
// Click save
|
||||
await user.click(screen.getByTestId('save-button'));
|
||||
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
{
|
||||
data: {
|
||||
users: [defaultProps.username],
|
||||
role: choosenRole,
|
||||
scope: defaultProps.libraryId,
|
||||
},
|
||||
},
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not call assignTeamMembersRole if user already has the selected role', async () => {
|
||||
const choosenRole = mockRoles[1].role; // 'admin'
|
||||
const user = userEvent.setup();
|
||||
renderComponent({ currentUserRoles: ['instructor', choosenRole] });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
|
||||
// Select a role that user already has
|
||||
const roleSelect = screen.getByTestId('role-select');
|
||||
await user.selectOptions(roleSelect, choosenRole);
|
||||
|
||||
await user.click(screen.getByTestId('save-button'));
|
||||
|
||||
// Should not call assignTeamMembersRole
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
// Modal should be closed
|
||||
expect(screen.queryByTestId('assign-new-role-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('shows loading state in modal when assignment is pending', async () => {
|
||||
mockUseAssignTeamMembersRole.mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: true,
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
|
||||
expect(screen.getByTestId('save-button')).toBeDisabled();
|
||||
expect(screen.getByText('Saving...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Success Handling', () => {
|
||||
it('shows success toast after successful role assignment', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
|
||||
const roleSelect = screen.getByTestId('role-select');
|
||||
await user.selectOptions(roleSelect, 'admin');
|
||||
|
||||
await user.click(screen.getByTestId('save-button'));
|
||||
|
||||
// Simulate successful API call
|
||||
const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
|
||||
onSuccessCallback();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/role added successfully/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes modal and resets role after successful assignment', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
|
||||
const roleSelect = screen.getByTestId('role-select');
|
||||
await user.selectOptions(roleSelect, 'admin');
|
||||
|
||||
await user.click(screen.getByTestId('save-button'));
|
||||
|
||||
// Simulate successful API call
|
||||
const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
|
||||
onSuccessCallback();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('assign-new-role-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Open modal again to check if role is reset
|
||||
await user.click(screen.getByRole('button', { name: /add new role/i }));
|
||||
expect(screen.getByTestId('role-select')).toHaveValue('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Toast, useToggle } from '@openedx/paragon';
|
||||
import { Plus } from '@openedx/paragon/icons';
|
||||
|
||||
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
|
||||
import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks';
|
||||
import messages from '../messages';
|
||||
import AssignNewRoleModal from './AssignNewRoleModal';
|
||||
|
||||
interface AssignNewRoleTriggerProps {
|
||||
username: string;
|
||||
libraryId: string;
|
||||
currentUserRoles: string[];
|
||||
}
|
||||
|
||||
const AssignNewRoleTrigger: FC<AssignNewRoleTriggerProps> = ({
|
||||
username,
|
||||
libraryId,
|
||||
currentUserRoles,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
const { roles } = useLibraryAuthZ();
|
||||
|
||||
const [newRole, setNewRole] = useState<string>('');
|
||||
|
||||
const { mutate: assignTeamMembersRole, isPending: isAssignTeamMembersRolePending } = useAssignTeamMembersRole();
|
||||
|
||||
const handleAddRole = () => {
|
||||
const data = {
|
||||
users: [username],
|
||||
role: newRole,
|
||||
scope: libraryId,
|
||||
};
|
||||
|
||||
if (currentUserRoles.includes(newRole)) {
|
||||
close();
|
||||
setNewRole('');
|
||||
return;
|
||||
}
|
||||
|
||||
assignTeamMembersRole({ data }, {
|
||||
onSuccess: () => {
|
||||
setToastMessage(
|
||||
intl.formatMessage(
|
||||
messages['libraries.authz.manage.assign.role.success'],
|
||||
),
|
||||
);
|
||||
close();
|
||||
setNewRole('');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
key="authz-header-action-new-team-member"
|
||||
iconBefore={Plus}
|
||||
onClick={open}
|
||||
>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.assign.new.role.title'])}
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<AssignNewRoleModal
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
onSave={handleAddRole}
|
||||
isLoading={isAssignTeamMembersRolePending}
|
||||
roleOptions={roles}
|
||||
selectedRole={newRole}
|
||||
handleChangeSelectedRole={(e) => setNewRole(e.target.value)}
|
||||
|
||||
/>
|
||||
)}
|
||||
|
||||
{toastMessage && (
|
||||
<Toast
|
||||
onClose={() => setToastMessage(null)}
|
||||
show={!!toastMessage}
|
||||
>
|
||||
{toastMessage}
|
||||
</Toast>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignNewRoleTrigger;
|
||||
@@ -0,0 +1,4 @@
|
||||
import AssignNewRoleModal from './AssignNewRoleModal';
|
||||
import AssignNewRoleTrigger from './AssignNewRoleTrigger';
|
||||
|
||||
export { AssignNewRoleModal, AssignNewRoleTrigger };
|
||||
@@ -31,6 +31,31 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Edit',
|
||||
description: 'Edit action',
|
||||
},
|
||||
'libraries.authz.manage.assign.new.role.title': {
|
||||
id: 'libraries.authz.manage.assign.new.role.title',
|
||||
defaultMessage: 'Add New Role',
|
||||
description: 'Libraries AuthZ assign a new role to a user button title',
|
||||
},
|
||||
'libraries.authz.manage.cancel.button': {
|
||||
id: 'libraries.authz.manage.cancel.button',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Libraries AuthZ cancel button title',
|
||||
},
|
||||
'libraries.authz.manage.saving.button': {
|
||||
id: 'libraries.authz.manage.saving.button',
|
||||
defaultMessage: 'Saving...',
|
||||
description: 'Libraries AuthZ saving button title',
|
||||
},
|
||||
'libraries.authz.manage.save.button': {
|
||||
id: 'libraries.authz.manage.save.button',
|
||||
defaultMessage: 'Save',
|
||||
description: 'Libraries AuthZ save button title',
|
||||
},
|
||||
'libraries.authz.manage.assign.role.success': {
|
||||
id: 'libraries.authz.manage.assign.role.success',
|
||||
defaultMessage: 'Role added successfully.',
|
||||
description: 'Libraries AuthZ assign role success message',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user