Skip to content

Commit 319a15a

Browse files
committed
feat: implement Add New Team Member functionality and enhance role management permissions
1 parent f10ec2b commit 319a15a

7 files changed

Lines changed: 236 additions & 122 deletions

File tree

src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,11 @@ import userEvent from '@testing-library/user-event';
33
import { renderWrapper } from '@src/setupTest';
44
import { initializeMockApp } from '@edx/frontend-platform/testing';
55
import { useLibrary, useUpdateLibrary } from '@src/authz-module/data/hooks';
6-
import { useNavigate, useLocation } from 'react-router-dom';
76
import { useLibraryAuthZ } from './context';
87
import LibrariesTeamManager from './LibrariesTeamManager';
98
import { ToastManagerProvider } from './ToastManagerContext';
109
import { CONTENT_LIBRARY_PERMISSIONS } from './constants';
1110

12-
jest.mock('react-router-dom', () => ({
13-
...jest.requireActual('react-router-dom'),
14-
useNavigate: jest.fn(),
15-
useLocation: jest.fn().mockReturnValue({ hash: '' }),
16-
}));
17-
1811
jest.mock('./context', () => {
1912
const actual = jest.requireActual('./context');
2013
return {
@@ -36,6 +29,11 @@ jest.mock('./components/TeamTable', () => ({
3629
default: () => <div role="table" aria-label="Team Members Table">Team member list</div>,
3730
}));
3831

32+
jest.mock('./components/AddNewTeamMemberModal', () => ({
33+
__esModule: true,
34+
AddNewTeamMemberTrigger: () => <button type="button">Add Team Member</button>,
35+
}));
36+
3937
jest.mock('../components/RoleCard', () => ({
4038
__esModule: true,
4139
default: ({ title, description, permissionsByResource }: {
@@ -60,7 +58,6 @@ describe('LibrariesTeamManager', () => {
6058
allowPublicRead: false,
6159
};
6260
const mutate = jest.fn();
63-
const mockNavigate = jest.fn();
6461
const libraryAuthZContext = {
6562
libraryId: libraryData.id,
6663
libraryName: libraryData.title,
@@ -98,8 +95,6 @@ describe('LibrariesTeamManager', () => {
9895
mutate,
9996
isPending: false,
10097
});
101-
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
102-
(useLocation as jest.Mock).mockReturnValue({ hash: '' });
10398
});
10499

105100
it('renders tabs and layout content correctly', () => {
@@ -116,8 +111,8 @@ describe('LibrariesTeamManager', () => {
116111
// TeamTable is rendered
117112
expect(screen.getByRole('table', { name: 'Team Members Table' })).toBeInTheDocument();
118113

119-
// Assign Role button is rendered
120-
expect(screen.getByRole('button', { name: 'Assign Role' })).toBeInTheDocument();
114+
// AddNewTeamMemberTrigger is rendered
115+
expect(screen.getByRole('button', { name: 'Add Team Member' })).toBeInTheDocument();
121116
});
122117

123118
it('renders role cards when "Roles" tab is selected', async () => {
@@ -146,7 +141,7 @@ describe('LibrariesTeamManager', () => {
146141
const permissionsTab = await screen.findByRole('tab', { name: /permissions/i });
147142
await user.click(permissionsTab);
148143

149-
const tablePermissionMatrix = screen.getByRole('table');
144+
const tablePermissionMatrix = await screen.getByRole('table');
150145
const matrixScope = within(tablePermissionMatrix);
151146

152147
expect(matrixScope.getByText('Library')).toBeInTheDocument();
@@ -162,25 +157,4 @@ describe('LibrariesTeamManager', () => {
162157
// TODO: Update expected URL when dedicated Manage Access page is created
163158
expect(navLink).toHaveAttribute('href', '/authz/libraries/lib-001');
164159
});
165-
166-
it('navigates to assign role wizard when "Assign Role" button is clicked', async () => {
167-
const user = userEvent.setup();
168-
renderTeamManager();
169-
await user.click(screen.getByRole('button', { name: 'Assign Role' }));
170-
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role?scope=lib-001');
171-
});
172-
173-
it('does not render Assign Role button when canManageTeam is false', () => {
174-
mockedUseLibraryAuthZ.mockReturnValue({ ...libraryAuthZContext, canManageTeam: false });
175-
renderTeamManager();
176-
expect(screen.queryByRole('button', { name: 'Assign Role' })).not.toBeInTheDocument();
177-
});
178-
179-
it('defaults to permissions tab when hash is present in location', () => {
180-
(useLocation as jest.Mock).mockReturnValue({ hash: '#permissions' });
181-
renderTeamManager();
182-
// Tabs renders with defaultActiveKey="permissions" when hash is truthy
183-
const permissionsTab = screen.getByRole('tab', { name: /permissions/i });
184-
expect(permissionsTab).toBeInTheDocument();
185-
});
186160
});

src/authz-module/libraries-manager/LibrariesTeamManager.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ import AuthZLayout from '../components/AuthZLayout';
1111
import RoleCard from '../components/RoleCard';
1212
import PermissionTable from '../components/PermissionTable';
1313
import { useLibraryAuthZ } from './context';
14-
import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from '@src/authz-module/roles-permissions/libraries/utils';
14+
import { AddNewTeamMemberTrigger } from './components/AddNewTeamMemberModal';
15+
import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from './utils';
1516

1617
import messages from './messages';
1718

1819
const LibrariesTeamManager = () => {
1920
const intl = useIntl();
2021
const { hash } = useLocation();
2122
const {
22-
libraryId, roles, permissions, resources,
23+
libraryId, canManageTeam, roles, permissions, resources,
2324
} = useLibraryAuthZ();
2425
const { data: library } = useLibrary(libraryId);
2526
const rootBreadcrumb = intl.formatMessage(messages['library.authz.breadcrumb.root']) || '';
@@ -48,7 +49,11 @@ const LibrariesTeamManager = () => {
4849
activeLabel={pageTitle}
4950
pageTitle={pageTitle}
5051
pageSubtitle={libraryId}
51-
actions={[]}
52+
actions={
53+
[
54+
...(canManageTeam ? [<AddNewTeamMemberTrigger libraryId={libraryId} key="add-new-member" />] : []),
55+
]
56+
}
5257
>
5358
<Tabs
5459
variant="tabs"

src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx

Lines changed: 11 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
import { act } from 'react';
1+
import React, { act } from 'react';
22
import { screen, waitFor } from '@testing-library/react';
33
import userEvent from '@testing-library/user-event';
44
import { renderWrapper } from '@src/setupTest';
5-
import { useAssignTeamMembersRole, useValidateUsers } from '@src/authz-module/data/hooks';
5+
import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks';
66
import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext';
77
import AddNewTeamMemberTrigger from './AddNewTeamMemberTrigger';
88

99
jest.mock('@edx/frontend-platform/logging');
1010

1111
const mockMutate = jest.fn();
12-
const mockMutateAsync = jest.fn();
1312

1413
// Mock the hooks module
1514
jest.mock('@src/authz-module/data/hooks', () => ({
1615
useAssignTeamMembersRole: jest.fn(),
17-
useValidateUsers: jest.fn(),
1816
}));
1917

2018
jest.mock('./AddNewTeamMemberModal', () => {
@@ -56,17 +54,12 @@ describe('AddNewTeamMemberTrigger', () => {
5654

5755
beforeEach(() => {
5856
jest.clearAllMocks();
59-
mockMutateAsync.mockResolvedValue({ validUsers: [], invalidUsers: [] });
6057
(useAssignTeamMembersRole as jest.Mock).mockReturnValue({
6158
mutate: mockMutate,
6259
isPending: false,
6360
isError: false,
6461
isSuccess: false,
6562
} as any);
66-
(useValidateUsers as jest.Mock).mockReturnValue({
67-
mutateAsync: mockMutateAsync,
68-
isPending: false,
69-
} as any);
7063
});
7164

7265
it('renders the trigger button', () => {
@@ -116,7 +109,7 @@ describe('AddNewTeamMemberTrigger', () => {
116109
await user.selectOptions(roleSelect, 'editor');
117110
await user.click(saveButton);
118111

119-
await waitFor(() => expect(mockMutate).toHaveBeenCalledWith(
112+
expect(mockMutate).toHaveBeenCalledWith(
120113
{
121114
data: {
122115
@@ -127,7 +120,7 @@ describe('AddNewTeamMemberTrigger', () => {
127120
expect.objectContaining({
128121
onSuccess: expect.any(Function),
129122
}),
130-
));
123+
);
131124
});
132125

133126
it('displays success toast and closes modal on successful addition with no errors', async () => {
@@ -137,14 +130,10 @@ describe('AddNewTeamMemberTrigger', () => {
137130
const triggerButton = screen.getByRole('button', { name: /assign role/i });
138131
await user.click(triggerButton);
139132

140-
const usersInput = screen.getByRole('textbox', { name: 'Enter user emails or usernames' });
141-
await user.type(usersInput, '[email protected], [email protected]');
142-
143133
const saveButton = screen.getByRole('button', { name: 'Save team member' });
144134
await user.click(saveButton);
145135

146-
await waitFor(() => expect(mockMutate).toHaveBeenCalled());
147-
136+
// Simulate successful response with no errors
148137
const [, { onSuccess }] = mockMutate.mock.calls[0];
149138
onSuccess({
150139
completed: [
@@ -168,14 +157,10 @@ describe('AddNewTeamMemberTrigger', () => {
168157
const triggerButton = screen.getByRole('button', { name: /assign role/i });
169158
await user.click(triggerButton);
170159

171-
const usersInput = screen.getByRole('textbox', { name: 'Enter user emails or usernames' });
172-
await user.type(usersInput, '[email protected], [email protected]');
173-
174160
const saveButton = screen.getByRole('button', { name: 'Save team member' });
175161
await user.click(saveButton);
176162

177-
await waitFor(() => expect(mockMutate).toHaveBeenCalled());
178-
163+
// Simulate partial success response
179164
const [, { onSuccess }] = mockMutate.mock.calls[0];
180165
onSuccess({
181166
completed: [
@@ -246,14 +231,10 @@ describe('AddNewTeamMemberTrigger', () => {
246231
const triggerButton = screen.getByRole('button', { name: /assign role/i });
247232
await user.click(triggerButton);
248233

249-
const usersInput = screen.getByRole('textbox', { name: 'Enter user emails or usernames' });
250-
await user.type(usersInput, '[email protected], [email protected]');
251-
252234
const saveButton = screen.getByRole('button', { name: 'Save team member' });
253235
await user.click(saveButton);
254236

255-
await waitFor(() => expect(mockMutate).toHaveBeenCalled());
256-
237+
// Simulate all failed response
257238
const [, { onSuccess }] = mockMutate.mock.calls[0];
258239
onSuccess({
259240
completed: [],
@@ -278,14 +259,9 @@ describe('AddNewTeamMemberTrigger', () => {
278259
const triggerButton = screen.getByRole('button', { name: /assign role/i });
279260
await user.click(triggerButton);
280261

281-
const usersInput = screen.getByRole('textbox', { name: 'Enter user emails or usernames' });
282-
await user.type(usersInput, '[email protected], [email protected]');
283-
284262
const saveButton = screen.getByRole('button', { name: 'Save team member' });
285263
await user.click(saveButton);
286264

287-
await waitFor(() => expect(mockMutate).toHaveBeenCalled());
288-
289265
const [, { onSuccess }] = mockMutate.mock.calls[0];
290266
onSuccess({
291267
completed: [],
@@ -318,8 +294,7 @@ describe('AddNewTeamMemberTrigger', () => {
318294
await user.selectOptions(roleSelect, 'editor');
319295
await user.click(saveButton);
320296

321-
await waitFor(() => expect(mockMutate).toHaveBeenCalled());
322-
297+
// Simulate successful response with no errors
323298
const [, { onSuccess }] = mockMutate.mock.calls[0];
324299
onSuccess({
325300
completed: [{ userIdentifier: '[email protected]', status: 'role_added' }],
@@ -343,14 +318,10 @@ describe('AddNewTeamMemberTrigger', () => {
343318
const triggerButton = screen.getByRole('button', { name: /assign role/i });
344319
await user.click(triggerButton);
345320

346-
const usersInput = screen.getByRole('textbox', { name: 'Enter user emails or usernames' });
347-
await user.type(usersInput, '[email protected]');
348-
349321
const saveButton = screen.getByRole('button', { name: 'Save team member' });
350322
await user.click(saveButton);
351323

352-
await waitFor(() => expect(mockMutate).toHaveBeenCalled());
353-
324+
// Simulate successful response
354325
const [, { onSuccess }] = mockMutate.mock.calls[0];
355326
onSuccess({
356327
completed: [{ userIdentifier: '[email protected]', status: 'role_added' }],
@@ -390,9 +361,6 @@ describe('AddNewTeamMemberTrigger', () => {
390361
const triggerButton = screen.getByRole('button', { name: /assign role/i });
391362
await user.click(triggerButton);
392363

393-
const usersInput = screen.getByRole('textbox', { name: 'Enter user emails or usernames' });
394-
await user.type(usersInput, '[email protected]');
395-
396364
const saveButton = screen.getByRole('button', { name: 'Save team member' });
397365
await user.click(saveButton);
398366

@@ -416,38 +384,6 @@ describe('AddNewTeamMemberTrigger', () => {
416384
expect(mockMutate).toHaveBeenCalledTimes(2);
417385
});
418386

419-
it('shows error toast and highlights invalid users when validation finds unknown users', async () => {
420-
const user = userEvent.setup();
421-
422-
mockMutateAsync.mockResolvedValue({ validUsers: [], invalidUsers: ['[email protected]'] });
423-
424-
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
425-
426-
const triggerButton = screen.getByRole('button', { name: /assign role/i });
427-
await user.click(triggerButton);
428-
429-
const usersInput = screen.getByRole('textbox', { name: 'Enter user emails or usernames' });
430-
const roleSelect = screen.getByRole('combobox', { name: 'Select role' });
431-
const saveButton = screen.getByRole('button', { name: 'Save team member' });
432-
433-
await user.type(usersInput, '[email protected]');
434-
await user.selectOptions(roleSelect, 'editor');
435-
await user.click(saveButton);
436-
437-
await waitFor(() => {
438-
expect(screen.getByText(/We couldn't find a user for 1 email address or username/)).toBeInTheDocument();
439-
});
440-
441-
// assignTeamMembersRole should NOT have been called
442-
expect(mockMutate).not.toHaveBeenCalled();
443-
444-
// Modal should remain open
445-
expect(screen.getByRole('dialog', { name: 'assign role' })).toBeInTheDocument();
446-
447-
// Input should be updated to show only invalid users
448-
expect(usersInput).toHaveValue('[email protected]');
449-
});
450-
451387
it('displays loading state when adding team member', async () => {
452388
const user = userEvent.setup();
453389

@@ -502,7 +438,7 @@ describe('AddNewTeamMemberTrigger', () => {
502438
expect(loadingIndicator).toHaveTextContent('Loading...');
503439
});
504440

505-
await waitFor(() => expect(mutateMock).toHaveBeenCalledWith(
441+
expect(mutateMock).toHaveBeenCalledWith(
506442
{
507443
data: {
508444
users: ['[email protected]'],
@@ -511,6 +447,6 @@ describe('AddNewTeamMemberTrigger', () => {
511447
},
512448
},
513449
expect.any(Object),
514-
));
450+
);
515451
});
516452
});

src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,6 @@ const AddNewTeamMemberTrigger = ({ libraryId }: AddNewTeamMemberTriggerProps) =>
136136
.filter(Boolean),
137137
)];
138138

139-
if (normalizedUsers.length === 0) { return; }
140-
141139
const payload = {
142140
users: normalizedUsers,
143141
role: formValues.role,

0 commit comments

Comments
 (0)