feat: [FC-0099] add button & modal for adding new team members (#3)
* feat: add team roles management and update related hooks and types * feat: implement add new team member functionality with modal and actions * test: add some missing tests * test: add unit tests for AddNewTeamMemberModal and update context mocks * test: add toast close functionality and loading state handling in AddNewTeamMemberTrigger tests * fix: update LibrariesAuthZTeamView to include canManageTeam check for AddNewTeamMemberTrigger * fix: correct API endpoint paths and update authorization scope format * refactor: improve error handling & address PR feedback * refactor: group AddNewTeamMemberModal in 1 folder * fix: reset modal values to close action * refactor: replace useAddTeamMember with useAssignTeamMembersRole * feat: add tooltip * test: fix test after rebase * refactor: enhance user intruction with placeholder * style: remove unnecessary inline style * fix: remove the error style on change the textarea value * fix: add useState to display toast * fix: remove empty strings from the user input * fix: validate error users to apply style --------- Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
This commit is contained in:
@@ -65,4 +65,32 @@ describe('AuthZTitle', () => {
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders action buttons with icons', () => {
|
||||
const mockIcon = () => <span data-testid="mock-icon">Icon</span>;
|
||||
const onClick = jest.fn();
|
||||
const actions = [
|
||||
{ label: 'Save', icon: mockIcon, onClick },
|
||||
];
|
||||
|
||||
render(<AuthZTitle {...defaultProps} actions={actions} />);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Icon Save' });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(screen.getByTestId('mock-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ReactNode actions alongside button actions', () => {
|
||||
const onClick = jest.fn();
|
||||
const customAction = <div data-testid="custom-action">Custom Action</div>;
|
||||
const actions = [
|
||||
{ label: 'Save', onClick },
|
||||
customAction,
|
||||
];
|
||||
|
||||
render(<AuthZTitle {...defaultProps} actions={actions} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('custom-action')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ComponentType, isValidElement, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Breadcrumb, Col, Container, Row, Button, Badge,
|
||||
@@ -11,6 +11,7 @@ interface BreadcrumbLink {
|
||||
|
||||
interface Action {
|
||||
label: string;
|
||||
icon?: ComponentType;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
@@ -19,7 +20,7 @@ export interface AuthZTitleProps {
|
||||
pageTitle: string;
|
||||
pageSubtitle: string | ReactNode;
|
||||
navLinks?: BreadcrumbLink[];
|
||||
actions?: Action[];
|
||||
actions?: (Action | ReactNode)[];
|
||||
}
|
||||
|
||||
const AuthZTitle = ({
|
||||
@@ -41,7 +42,22 @@ const AuthZTitle = ({
|
||||
<Col xs={12} md={4}>
|
||||
<div className="d-flex justify-content-md-end">
|
||||
{
|
||||
actions.map(({ label, onClick }) => <Button key={`authz-header-action-${label}`} onClick={onClick}>{label}</Button>)
|
||||
actions.map((action) => {
|
||||
if (isValidElement(action)) {
|
||||
return action;
|
||||
}
|
||||
|
||||
const { label, icon, onClick } = action as Action;
|
||||
return (
|
||||
<Button
|
||||
key={`authz-header-action-${label}`}
|
||||
iconBefore={icon}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
@@ -2,3 +2,11 @@ export const ROUTES = {
|
||||
LIBRARIES_TEAM_PATH: '/libraries/:libraryId',
|
||||
LIBRARIES_USER_PATH: '/libraries/:libraryId/:username',
|
||||
};
|
||||
|
||||
export enum RoleOperationErrorStatus {
|
||||
USER_NOT_FOUND = 'user_not_found',
|
||||
USER_ALREADY_HAS_ROLE = 'user_already_has_role',
|
||||
USER_DOES_NOT_HAVE_ROLE = 'user_does_not_have_role',
|
||||
ROLE_ASSIGNMENT_ERROR = 'role_assignment_error',
|
||||
ROLE_REMOVAL_ERROR = 'role_removal_error',
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@ export type PermissionsByRole = {
|
||||
permissions: string[];
|
||||
userCount: number;
|
||||
};
|
||||
export interface PutAssignTeamMembersRoleResponse {
|
||||
completed: { user: string; status: string }[];
|
||||
errors: { userIdentifier: string; error: string }[];
|
||||
}
|
||||
|
||||
export interface AssignTeamMembersRoleRequest {
|
||||
users: string[];
|
||||
role: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
// TODO: replece api path once is created
|
||||
export const getTeamMembers = async (object: string): Promise<TeamMember[]> => {
|
||||
@@ -20,6 +30,13 @@ export const getTeamMembers = async (object: string): Promise<TeamMember[]> => {
|
||||
return camelCaseObject(data.results);
|
||||
};
|
||||
|
||||
export const assignTeamMembersRole = async (
|
||||
data: AssignTeamMembersRoleRequest,
|
||||
): Promise<PutAssignTeamMembersRoleResponse> => {
|
||||
const res = await getAuthenticatedHttpClient().put(getApiUrl('/api/authz/v1/roles/users/'), data);
|
||||
return camelCaseObject(res.data);
|
||||
};
|
||||
|
||||
// TODO: this should be replaced in the future with Console API
|
||||
export const getLibrary = async (libraryId: string): Promise<LibraryMetadata> => {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getStudioApiUrl(`/api/libraries/v2/${libraryId}/`));
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ReactNode } from 'react';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { useLibrary, usePermissionsByRole, useTeamMembers } from './hooks';
|
||||
import {
|
||||
useLibrary, usePermissionsByRole, useTeamMembers, useAssignTeamMembersRole,
|
||||
} from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
@@ -155,4 +157,74 @@ describe('usePermissionsByRole', () => {
|
||||
expect(e).toEqual(new Error('Not found'));
|
||||
}
|
||||
});
|
||||
|
||||
describe('useAssignTeamMembersRole', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('successfully adds team members', async () => {
|
||||
const mockResponse = {
|
||||
completed: [
|
||||
{
|
||||
user: 'jdoe',
|
||||
status: 'role_added',
|
||||
},
|
||||
{
|
||||
user: 'alice@example.com',
|
||||
status: 'already_has_role',
|
||||
},
|
||||
],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
put: jest.fn().mockResolvedValue({ data: mockResponse }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAssignTeamMembersRole(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
const addTeamMemberData = {
|
||||
scope: 'lib:123',
|
||||
users: ['jdoe'],
|
||||
role: 'author',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.mutate({ data: addTeamMemberData });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('handles error when adding team members fails', async () => {
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
put: jest.fn().mockRejectedValue(new Error('Failed to add members')),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAssignTeamMembersRole(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
const addTeamMemberData = {
|
||||
scope: 'lib:123',
|
||||
users: ['jdoe'],
|
||||
role: 'author',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.mutate({ data: addTeamMemberData });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
expect(result.current.error).toEqual(new Error('Failed to add members'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
useMutation, useQuery, useQueryClient, useSuspenseQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { appId } from '@src/constants';
|
||||
import { LibraryMetadata, TeamMember } from '@src/types';
|
||||
import {
|
||||
assignTeamMembersRole,
|
||||
AssignTeamMembersRoleRequest,
|
||||
getLibrary, getPermissionsByRole, getTeamMembers, PermissionsByRole,
|
||||
} from './api';
|
||||
|
||||
@@ -60,3 +64,23 @@ export const useLibrary = (libraryId: string) => useSuspenseQuery<LibraryMetadat
|
||||
queryFn: () => getLibrary(libraryId),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* React Query hook to add new team members to a specific scope or manage the corresponding roles.
|
||||
* It provides a mutation function to add users with specified roles to the team or assign new roles.
|
||||
*
|
||||
* @example
|
||||
* const { mutate: assignTeamMembersRole } = useAssignTeamMembersRole();
|
||||
* assignTeamMembersRole({ data: { libraryId: 'lib:123', users: ['jdoe'], role: 'editor' } });
|
||||
*/
|
||||
export const useAssignTeamMembersRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ data }: {
|
||||
data: AssignTeamMembersRoleRequest
|
||||
}) => assignTeamMembersRole(data),
|
||||
onSettled: (_data, _error, { data: { scope } }) => {
|
||||
queryClient.invalidateQueries({ queryKey: authzQueryKeys.teamMembers(scope) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,4 +34,13 @@
|
||||
height: var(--pgn-size-icon-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.toast-container {
|
||||
// Ensure toast appears above modal
|
||||
z-index: 1000;
|
||||
// Move toast to the right
|
||||
left: auto;
|
||||
right: var(--pgn-spacing-toast-container-gutter-lg);
|
||||
}
|
||||
@@ -24,6 +24,11 @@ jest.mock('./components/TeamTable', () => ({
|
||||
default: () => <div data-testid="team-table">MockTeamTable</div>,
|
||||
}));
|
||||
|
||||
jest.mock('./components/AddNewTeamMemberModal', () => ({
|
||||
__esModule: true,
|
||||
AddNewTeamMemberTrigger: () => <div data-testid="add-team-member-trigger">MockAddNewTeamMemberTrigger</div>,
|
||||
}));
|
||||
|
||||
describe('LibrariesTeamManager', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
@@ -63,5 +68,8 @@ describe('LibrariesTeamManager', () => {
|
||||
|
||||
// TeamTable is rendered
|
||||
expect(screen.getByTestId('team-table')).toBeInTheDocument();
|
||||
|
||||
// AddNewTeamMemberTrigger is rendered
|
||||
expect(screen.getByTestId('add-team-member-trigger')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Tab, Tabs } from '@openedx/paragon';
|
||||
import { useLibrary } from '@src/authz-module/data/hooks';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import TeamTable from './components/TeamTable';
|
||||
import AuthZLayout from '../components/AuthZLayout';
|
||||
import { useLibraryAuthZ } from './context';
|
||||
import { AddNewTeamMemberTrigger } from './components/AddNewTeamMemberModal';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LibrariesTeamManager = () => {
|
||||
const intl = useIntl();
|
||||
const { libraryId } = useLibraryAuthZ();
|
||||
const { hash } = useLocation();
|
||||
const { libraryId, canManageTeam } = useLibraryAuthZ();
|
||||
const { data: library } = useLibrary(libraryId);
|
||||
const rootBradecrumb = intl.formatMessage(messages['library.authz.breadcrumb.root']) || '';
|
||||
const pageTitle = intl.formatMessage(messages['library.authz.manage.page.title']);
|
||||
@@ -21,11 +24,15 @@ const LibrariesTeamManager = () => {
|
||||
activeLabel={pageTitle}
|
||||
pageTitle={pageTitle}
|
||||
pageSubtitle={libraryId}
|
||||
actions={[]}
|
||||
actions={
|
||||
canManageTeam
|
||||
? [<AddNewTeamMemberTrigger libraryId={libraryId} />]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Tabs
|
||||
variant="tabs"
|
||||
defaultActiveKey="team"
|
||||
defaultActiveKey={hash ? 'permissions' : 'team'}
|
||||
className="bg-light-100 px-5"
|
||||
>
|
||||
<Tab eventKey="team" title={intl.formatMessage(messages['library.authz.tabs.team'])} className="p-5">
|
||||
@@ -34,7 +41,7 @@ const LibrariesTeamManager = () => {
|
||||
<Tab eventKey="roles" title={intl.formatMessage(messages['library.authz.tabs.roles'])}>
|
||||
Role tab.
|
||||
</Tab>
|
||||
<Tab eventKey="permissions" title={intl.formatMessage(messages['library.authz.tabs.permissions'])}>
|
||||
<Tab id="libraries-permissions-tab" eventKey="permissions" title={intl.formatMessage(messages['library.authz.tabs.permissions'])}>
|
||||
Permissions tab.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { screen } 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 AddNewTeamMemberModal from './AddNewTeamMemberModal';
|
||||
|
||||
// Mock the context module
|
||||
jest.mock('@src/authz-module/libraries-manager/context', () => {
|
||||
const actual = jest.requireActual('@src/authz-module/libraries-manager/context');
|
||||
return {
|
||||
...actual,
|
||||
useLibraryAuthZ: jest.fn(),
|
||||
};
|
||||
});
|
||||
const mockedUseLibraryAuthZ = useLibraryAuthZ as jest.Mock;
|
||||
|
||||
jest.mock('@src/authz-module/data/hooks', () => ({
|
||||
useTeamRoles: jest.fn(),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
formValues: {
|
||||
users: '',
|
||||
role: '',
|
||||
},
|
||||
close: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
handleChangeForm: jest.fn(),
|
||||
};
|
||||
|
||||
const mockRoles = [
|
||||
{
|
||||
role: 'instructor',
|
||||
name: 'instructor',
|
||||
description: 'Can create and edit content',
|
||||
userCount: 3,
|
||||
objects: [
|
||||
{
|
||||
object: 'library',
|
||||
description: 'Library permissions',
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'admin',
|
||||
name: 'admin',
|
||||
description: 'Full access to the library',
|
||||
userCount: 1,
|
||||
objects: [
|
||||
{
|
||||
object: 'library',
|
||||
description: 'Library permissions',
|
||||
actions: ['view', 'edit', 'delete', 'manage'],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('AddNewTeamMemberModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockedUseLibraryAuthZ.mockReturnValue({
|
||||
username: 'testuser',
|
||||
libraryId: 'lib123',
|
||||
roles: mockRoles,
|
||||
canManageTeam: true,
|
||||
});
|
||||
});
|
||||
|
||||
const renderModal = (props = {}) => {
|
||||
const finalProps = { ...defaultProps, ...props };
|
||||
return renderWrapper(
|
||||
<AddNewTeamMemberModal {...finalProps} />,
|
||||
);
|
||||
};
|
||||
|
||||
describe('Modal Rendering', () => {
|
||||
it('renders the modal when isOpen is true', () => {
|
||||
renderModal();
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add New Team Member')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the modal when isOpen is false', () => {
|
||||
renderModal({ isOpen: false });
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Elements', () => {
|
||||
it('renders the users textarea with correct label', () => {
|
||||
renderModal();
|
||||
|
||||
expect(screen.getByLabelText('Add users by username or email')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /add users by username or email/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the role select dropdown with correct label', () => {
|
||||
renderModal();
|
||||
|
||||
expect(screen.getAllByLabelText('Roles')[0]).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox', { name: /roles/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders role options correctly', () => {
|
||||
renderModal();
|
||||
|
||||
expect(screen.getByText('Select a role')).toBeInTheDocument();
|
||||
mockRoles.forEach((role) => {
|
||||
expect(screen.getByText(role.role)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays form values correctly', () => {
|
||||
renderModal({
|
||||
formValues: {
|
||||
users: 'user1@example.com, user2@example.com',
|
||||
role: 'instructor',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByDisplayValue('user1@example.com, user2@example.com')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('instructor')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Interactions', () => {
|
||||
it('calls handleChangeForm when users textarea changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChangeForm = jest.fn();
|
||||
renderModal({ handleChangeForm });
|
||||
|
||||
const usersTextarea = screen.getByRole('textbox', { name: /add users by username or email/i });
|
||||
await user.type(usersTextarea, 'test@example.com');
|
||||
|
||||
expect(handleChangeForm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls handleChangeForm when role select changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChangeForm = jest.fn();
|
||||
renderModal({ handleChangeForm });
|
||||
|
||||
const roleSelect = screen.getByRole('combobox', { name: /roles/i });
|
||||
await user.selectOptions(roleSelect, 'instructor');
|
||||
|
||||
expect(handleChangeForm).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Actions', () => {
|
||||
it('renders Cancel and Save buttons', () => {
|
||||
renderModal();
|
||||
|
||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls close function when Cancel button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const close = jest.fn();
|
||||
renderModal({ close });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /cancel/i }));
|
||||
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onSave function when Save button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSave = jest.fn();
|
||||
renderModal({
|
||||
onSave,
|
||||
formValues: {
|
||||
users: 'test@example.com',
|
||||
role: 'instructor',
|
||||
},
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(onSave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('disables Save button when users field is empty', () => {
|
||||
renderModal({
|
||||
formValues: {
|
||||
users: '',
|
||||
role: 'instructor',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Save button when role field is empty', () => {
|
||||
renderModal({
|
||||
formValues: {
|
||||
users: 'test@example.com',
|
||||
role: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Save button when both fields are filled', () => {
|
||||
renderModal({
|
||||
formValues: {
|
||||
users: 'test@example.com',
|
||||
role: 'instructor',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('disables Cancel button when loading', () => {
|
||||
renderModal({ isLoading: true });
|
||||
|
||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Save button when loading', () => {
|
||||
renderModal({
|
||||
isLoading: true,
|
||||
formValues: {
|
||||
users: 'test@example.com',
|
||||
role: 'instructor',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: /saving/i })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { FC, useRef } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Form, Hyperlink, Icon, IconButton, ModalDialog,
|
||||
ModalPopup,
|
||||
Stack,
|
||||
StatefulButton,
|
||||
useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
|
||||
import { Info, SpinnerSimple } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
|
||||
interface AddNewTeamMemberModalProps {
|
||||
isOpen: boolean;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
formValues: {
|
||||
users: string;
|
||||
role: string;
|
||||
};
|
||||
close: () => void;
|
||||
onSave: () => void;
|
||||
handleChangeForm: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => void;
|
||||
}
|
||||
|
||||
const AddNewTeamMemberModal: FC<AddNewTeamMemberModalProps> = ({
|
||||
isOpen, isError, isLoading, formValues, close, onSave, handleChangeForm,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { roles } = useLibraryAuthZ();
|
||||
const [isOpenRolesPopUp, openRolesPopUp, closeRolesPopUp] = useToggle(false);
|
||||
const targetRolesPopUpRef = useRef<HTMLButtonElement | null>(null);
|
||||
return (
|
||||
<>
|
||||
<ModalPopup
|
||||
hasArrow
|
||||
placement="auto"
|
||||
positionRef={targetRolesPopUpRef.current}
|
||||
isOpen={isOpenRolesPopUp}
|
||||
onClose={closeRolesPopUp}
|
||||
>
|
||||
<div className="bg-white p-3 rounded shadow border x-small">
|
||||
<ul>
|
||||
{roles.map((role) => <li key={`role-tooltip-${role.role}`}><b>{role.name}: </b>{role.description}</li>)}
|
||||
</ul>
|
||||
<Hyperlink destination="#libraries-permissions-tab" target="_blank">{intl.formatMessage(messages['libraries.authz.manage.tooltip.roles.extra.info'])}</Hyperlink>
|
||||
</div>
|
||||
</ModalPopup>
|
||||
<ModalDialog
|
||||
title={intl.formatMessage(messages['libraries.authz.manage.add.member.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.add.member.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body className="my-4">
|
||||
<Stack gap={3}>
|
||||
<p>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.add.member.description'])}
|
||||
</p>
|
||||
|
||||
<Form.Group controlId="users_list">
|
||||
<Form.Label>{intl.formatMessage(messages['libraries.authz.manage.add.member.users.label'])}</Form.Label>
|
||||
<Form.Control
|
||||
isInvalid={isError}
|
||||
as="textarea"
|
||||
name="users"
|
||||
rows="3"
|
||||
value={formValues.users}
|
||||
onChange={(e) => handleChangeForm(e)}
|
||||
placeholder={intl.formatMessage(messages['libraries.authz.manage.add.member.users.placeholder'])}
|
||||
style={{ color: isError && 'var(--pgn-color-form-feedback-invalid)' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="role_options">
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.add.member.roles.label'])}
|
||||
<IconButton alt="tooptip-extra-info" size="inline" src={Info} onClick={openRolesPopUp} ref={targetRolesPopUpRef} />
|
||||
</Form.Label>
|
||||
<Form.Control as="select" name="role" value={formValues.role} onChange={(e) => handleChangeForm(e)}>
|
||||
<option value="" disabled>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.add.member.select.default'])}
|
||||
</option>
|
||||
{roles.map((role) => <option key={role.role} value={role.role}>{role.name}</option>)}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</Stack>
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary" disabled={isLoading}>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.cancel.button'])}
|
||||
</ModalDialog.CloseButton>
|
||||
<StatefulButton
|
||||
className="px-4"
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['libraries.authz.manage.save.button']),
|
||||
pending: intl.formatMessage(messages['libraries.authz.manage.saving.button']),
|
||||
}}
|
||||
icons={{
|
||||
pending: <Icon src={SpinnerSimple} />,
|
||||
}}
|
||||
state={isLoading ? 'pending' : 'default'}
|
||||
onClick={() => onSave()}
|
||||
disabledStates={['pending']}
|
||||
disabled={isLoading || !formValues.users || !formValues.role}
|
||||
/>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNewTeamMemberModal;
|
||||
@@ -0,0 +1,290 @@
|
||||
import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWrapper } from '@src/setupTest';
|
||||
import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks';
|
||||
import AddNewTeamMemberTrigger from './AddNewTeamMemberTrigger';
|
||||
|
||||
const mockMutate = jest.fn();
|
||||
|
||||
// Mock the hooks module
|
||||
jest.mock('@src/authz-module/data/hooks', () => ({
|
||||
useAssignTeamMembersRole: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./AddNewTeamMemberModal', () => {
|
||||
/* eslint-disable react/prop-types */
|
||||
const MockModal = ({
|
||||
isOpen, close, onSave, isLoading, formValues, handleChangeForm,
|
||||
}) => (
|
||||
isOpen ? (
|
||||
<div data-testid="add-team-member-modal">
|
||||
<button type="button" onClick={close} data-testid="close-modal">Close</button>
|
||||
<button type="button" onClick={onSave} data-testid="save-modal">Save</button>
|
||||
<textarea
|
||||
name="users"
|
||||
value={formValues?.users || ''}
|
||||
onChange={handleChangeForm}
|
||||
data-testid="users-input"
|
||||
/>
|
||||
<select
|
||||
name="role"
|
||||
value={formValues?.role || ''}
|
||||
onChange={handleChangeForm}
|
||||
data-testid="role-select"
|
||||
>
|
||||
<option value="">Select role</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="editor">Editor</option>
|
||||
</select>
|
||||
{isLoading && <div data-testid="loading-indicator">Loading...</div>}
|
||||
</div>
|
||||
) : null
|
||||
);
|
||||
/* eslint-enable react/prop-types */
|
||||
return MockModal;
|
||||
});
|
||||
|
||||
describe('AddNewTeamMemberTrigger', () => {
|
||||
const mockLibraryId = 'lib:123';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useAssignTeamMembersRole as jest.Mock).mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: false,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('renders the trigger button', () => {
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const button = screen.getByRole('button', { name: /add new team member/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens modal when trigger button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes modal when close button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal');
|
||||
await user.click(closeButton);
|
||||
|
||||
expect(screen.queryByTestId('add-team-member-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls addTeamMember with correct data when save is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
const usersInput = screen.getByTestId('users-input');
|
||||
const roleSelect = screen.getByTestId('role-select');
|
||||
const saveButton = screen.getByTestId('save-modal');
|
||||
|
||||
await user.type(usersInput, 'alice@example.com, bob@example.com');
|
||||
await user.selectOptions(roleSelect, 'editor');
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
{
|
||||
data: {
|
||||
users: ['alice@example.com', 'bob@example.com'],
|
||||
role: 'editor',
|
||||
scope: mockLibraryId,
|
||||
},
|
||||
},
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('displays success toast and closes modal on successful addition with no errors', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
const saveButton = screen.getByTestId('save-modal');
|
||||
await user.click(saveButton);
|
||||
|
||||
// Simulate successful response with no errors
|
||||
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
||||
onSuccess({
|
||||
completed: [
|
||||
{ userIdentifier: 'alice@example.com', status: 'role_added' },
|
||||
{ userIdentifier: 'bob@example.com', status: 'added_to_team' },
|
||||
],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('add-team-member-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('2 team members added successfully.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays mixed success and error toast on partial success', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
const saveButton = screen.getByTestId('save-modal');
|
||||
await user.click(saveButton);
|
||||
|
||||
// Simulate partial success response
|
||||
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
||||
onSuccess({
|
||||
completed: [
|
||||
{ userIdentifier: 'alice@example.com', status: 'role_added' },
|
||||
],
|
||||
errors: [
|
||||
{ userIdentifier: 'unknown@example.com', error: 'user_not_found' },
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/1 team member added successfully/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/We couldn't find a user for 1 email address or username/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Modal should remain open when there are errors
|
||||
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays only error toast when all additions fail', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
const saveButton = screen.getByTestId('save-modal');
|
||||
await user.click(saveButton);
|
||||
|
||||
// Simulate all failed response
|
||||
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
||||
onSuccess({
|
||||
completed: [],
|
||||
errors: [
|
||||
{ userIdentifier: 'unknown1@example.com', error: 'user_not_found' },
|
||||
{ userIdentifier: 'unknown2@example.com', error: 'user_not_found' },
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/We couldn't find a user for 2 email addresses or usernames/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Modal should remain open when there are errors
|
||||
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('resets form values after successful addition with no errors', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
const usersInput = screen.getByTestId('users-input');
|
||||
const roleSelect = screen.getByTestId('role-select');
|
||||
const saveButton = screen.getByTestId('save-modal');
|
||||
|
||||
await user.type(usersInput, 'alice@example.com');
|
||||
await user.selectOptions(roleSelect, 'editor');
|
||||
await user.click(saveButton);
|
||||
|
||||
// Simulate successful response with no errors
|
||||
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
||||
onSuccess({
|
||||
completed: [{ userIdentifier: 'alice@example.com', status: 'role_added' }],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
// Open modal again to check if form is reset
|
||||
await user.click(triggerButton);
|
||||
|
||||
const newUsersInput = screen.getByTestId('users-input');
|
||||
const newRoleSelect = screen.getByTestId('role-select');
|
||||
|
||||
expect(newUsersInput).toHaveValue('');
|
||||
expect(newRoleSelect).toHaveValue('');
|
||||
});
|
||||
|
||||
it('allows closing the success/error toast message', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
const saveButton = screen.getByTestId('save-modal');
|
||||
await user.click(saveButton);
|
||||
|
||||
// Simulate successful response
|
||||
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
||||
onSuccess({
|
||||
completed: [{ userIdentifier: 'alice@example.com', status: 'role_added' }],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
// Toast should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1 team member added successfully.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find and close the toast
|
||||
const toastCloseButton = screen.getByLabelText(/close/i);
|
||||
await user.click(toastCloseButton);
|
||||
|
||||
// Toast should be removed
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('1 team member added successfully.')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays loading state when adding team member', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock loading state
|
||||
(useAssignTeamMembersRole as jest.Mock).mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: true,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
} as any);
|
||||
|
||||
renderWrapper(<AddNewTeamMemberTrigger libraryId={mockLibraryId} />);
|
||||
|
||||
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
||||
await user.click(triggerButton);
|
||||
|
||||
// Loading indicator should be visible in the modal
|
||||
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
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 { PutAssignTeamMembersRoleResponse } from 'authz-module/data/api';
|
||||
import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks';
|
||||
import { RoleOperationErrorStatus } from '@src/authz-module/constants';
|
||||
import AddNewTeamMemberModal from './AddNewTeamMemberModal';
|
||||
import messages from './messages';
|
||||
|
||||
interface AddNewTeamMemberTriggerProps {
|
||||
libraryId: string;
|
||||
}
|
||||
|
||||
const DEFAULT_FORM_VALUES = {
|
||||
users: '',
|
||||
role: '',
|
||||
};
|
||||
|
||||
const AddNewTeamMemberTrigger: FC<AddNewTeamMemberTriggerProps> = ({
|
||||
libraryId,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [additionMessage, setAdditionMessage] = useState<string | null>(null);
|
||||
const [formValues, setFormValues] = useState(DEFAULT_FORM_VALUES);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [errorValidationUsers, setNotFoundUsers] = useState<string[]>([]);
|
||||
const { mutate: assignTeamMembersRole, isPending: isAssignTeamMembersRolePending } = useAssignTeamMembersRole();
|
||||
|
||||
const handleChangeForm = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const userIds = value
|
||||
.split(',')
|
||||
.map(userId => userId.trim())
|
||||
.filter(Boolean);
|
||||
const hasErrorUser = errorValidationUsers.find((noUser) => userIds.includes(noUser));
|
||||
|
||||
if (hasErrorUser) {
|
||||
setIsError(true);
|
||||
} else {
|
||||
setIsError(false);
|
||||
}
|
||||
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleErrors = (errors: PutAssignTeamMembersRoleResponse['errors']) => {
|
||||
setIsError(false);
|
||||
const notFoundUsers = errors.filter(err => err.error === RoleOperationErrorStatus.USER_NOT_FOUND)
|
||||
.map(err => err.userIdentifier.trim());
|
||||
|
||||
if (errors.length === 1 && errors[0].error === RoleOperationErrorStatus.USER_ALREADY_HAS_ROLE) {
|
||||
setFormValues(DEFAULT_FORM_VALUES);
|
||||
close();
|
||||
}
|
||||
|
||||
if (notFoundUsers.length) {
|
||||
setNotFoundUsers(notFoundUsers);
|
||||
setIsError(true);
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
users: notFoundUsers.join(', '),
|
||||
}));
|
||||
|
||||
setAdditionMessage((prevMessage) => (
|
||||
`${prevMessage ? `${prevMessage} ` : ''}${intl.formatMessage(
|
||||
messages['libraries.authz.manage.add.member.failure'],
|
||||
{ count: notFoundUsers.length },
|
||||
)}`
|
||||
));
|
||||
setShowToast(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTeamMember = () => {
|
||||
const normalizedUsers = new Set(formValues.users.split(',').map(user => user.trim()).filter(user => user));
|
||||
const data = {
|
||||
users: [...normalizedUsers],
|
||||
role: formValues.role,
|
||||
scope: libraryId,
|
||||
};
|
||||
|
||||
assignTeamMembersRole({ data }, {
|
||||
onSuccess: (successData) => {
|
||||
setAdditionMessage(null);
|
||||
|
||||
if (successData.completed.length) {
|
||||
setAdditionMessage(
|
||||
intl.formatMessage(
|
||||
messages['libraries.authz.manage.add.member.success'],
|
||||
{ count: successData.completed.length },
|
||||
),
|
||||
);
|
||||
setShowToast(true);
|
||||
}
|
||||
|
||||
if (successData.errors.length) {
|
||||
handleErrors(successData.errors);
|
||||
} else {
|
||||
setIsError(false);
|
||||
setNotFoundUsers([]);
|
||||
close();
|
||||
setFormValues(DEFAULT_FORM_VALUES);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleClose = () => {
|
||||
setFormValues(DEFAULT_FORM_VALUES);
|
||||
setNotFoundUsers([]);
|
||||
setIsError(false);
|
||||
setAdditionMessage(null);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
key="authz-header-action-new-team-member"
|
||||
iconBefore={Plus}
|
||||
onClick={open}
|
||||
>
|
||||
{intl.formatMessage(messages['libraries.authz.manage.add.member.title'])}
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<AddNewTeamMemberModal
|
||||
isOpen={isOpen}
|
||||
isError={isError}
|
||||
close={handleClose}
|
||||
onSave={handleAddTeamMember}
|
||||
isLoading={isAssignTeamMembersRolePending}
|
||||
formValues={formValues}
|
||||
handleChangeForm={handleChangeForm}
|
||||
/>
|
||||
)}
|
||||
|
||||
{additionMessage && (
|
||||
<Toast
|
||||
onClose={() => setShowToast(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{additionMessage}
|
||||
</Toast>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNewTeamMemberTrigger;
|
||||
@@ -0,0 +1,4 @@
|
||||
import AddNewTeamMemberModal from './AddNewTeamMemberModal';
|
||||
import AddNewTeamMemberTrigger from './AddNewTeamMemberTrigger';
|
||||
|
||||
export { AddNewTeamMemberModal, AddNewTeamMemberTrigger };
|
||||
@@ -0,0 +1,71 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'libraries.authz.manage.add.member.title': {
|
||||
id: 'libraries.authz.manage.add.member.title',
|
||||
defaultMessage: 'Add New Team Member',
|
||||
description: 'Title for the add new team member modal',
|
||||
},
|
||||
'libraries.authz.manage.add.member.users.label': {
|
||||
id: 'libraries.authz.manage.add.member.users.label',
|
||||
defaultMessage: 'Add users by username or email',
|
||||
description: 'Label for the users input field in the add new team member modal',
|
||||
},
|
||||
'libraries.authz.manage.add.member.users.placeholder': {
|
||||
id: 'libraries.authz.manage.add.member.users.placeholder',
|
||||
defaultMessage: 'Enter one or more email addresses or usernames, comma-separated.',
|
||||
description: 'Placeholder for the users input field in the add new team member modal',
|
||||
},
|
||||
'libraries.authz.manage.add.member.roles.label': {
|
||||
id: 'libraries.authz.manage.add.member.roles.label',
|
||||
defaultMessage: 'Roles',
|
||||
description: 'Label for the roles select field in the add new team member modal',
|
||||
},
|
||||
'libraries.authz.manage.add.member.invalid.users': {
|
||||
id: 'libraries.authz.manage.add.member.invalid.users',
|
||||
defaultMessage: 'The following users could not be found:',
|
||||
description: 'Error message for invalid users in the add new team member modal',
|
||||
},
|
||||
'libraries.authz.manage.add.member.select.default': {
|
||||
id: 'libraries.authz.manage.add.member.select.default',
|
||||
defaultMessage: 'Select a role',
|
||||
description: 'Default option for the roles select field in the add new team member modal',
|
||||
},
|
||||
'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.add.member.description': {
|
||||
id: 'libraries.authz.manage.add.member.description',
|
||||
defaultMessage: 'Add new members to this library\'s team and assign them a role to define their permissions.',
|
||||
description: 'Description for the add new team member modal',
|
||||
},
|
||||
'libraries.authz.manage.add.member.success': {
|
||||
id: 'libraries.authz.manage.add.member.success',
|
||||
defaultMessage: '{count, plural, one {# team member added successfully.} other {# team members added successfully.}}',
|
||||
description: 'Success message when adding new team members',
|
||||
},
|
||||
'libraries.authz.manage.add.member.failure': {
|
||||
id: 'libraries.authz.manage.add.member.failure',
|
||||
defaultMessage: 'We couldn\'t find a user for {count, plural, one {# email address or username.} other {# email addresses or usernames.}} Please check the values and try again, or invite them to join your organization first.',
|
||||
description: 'Error message when adding new team members',
|
||||
},
|
||||
'libraries.authz.manage.tooltip.roles.extra.info': {
|
||||
id: 'libraries.authz.manage.tooltip.roles.extra.info',
|
||||
defaultMessage: 'View detailed permissions for each role.',
|
||||
description: 'Invite the user to check a detailed view of permissions',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useValidateUserPermissions } from '@src/data/hooks';
|
||||
import { renderWrapper } from '@src/setupTest';
|
||||
import { usePermissionsByRole } from '@src/authz-module/data/hooks';
|
||||
import { LibraryAuthZProvider, useLibraryAuthZ } from './context';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@@ -45,6 +46,34 @@ describe('LibraryAuthZProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useParams as jest.Mock).mockReturnValue({ libraryId: 'lib123' });
|
||||
(usePermissionsByRole as jest.Mock).mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
role: 'instructor',
|
||||
description: 'Can create and edit content',
|
||||
userCount: 3,
|
||||
objects: [
|
||||
{
|
||||
object: 'library',
|
||||
description: 'Library permissions',
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'admin',
|
||||
description: 'Full access to the library',
|
||||
userCount: 1,
|
||||
objects: [
|
||||
{
|
||||
object: 'library',
|
||||
description: 'Library permissions',
|
||||
actions: ['view', 'edit', 'delete', 'manage'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('provides the correct context values to consumers', () => {
|
||||
|
||||
Reference in New Issue
Block a user