Refactor: Move courseTeam from redux store to react query (#2888)
* refactor: Move `courseTeam` from redux store to react query * fix: Broken types * fix: Show user readable error * fix: Broken coverage * fix: Broken coverage * refactor: Add default message error
This commit is contained in:
@@ -1,219 +0,0 @@
|
||||
// @ts-check
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
|
||||
import { getCourseTeamApiUrl, updateCourseTeamUserApiUrl } from './data/api';
|
||||
import CourseTeam from './CourseTeam';
|
||||
import messages from './messages';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { executeThunk } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { changeRoleTeamUserQuery, deleteCourseTeamQuery } from './data/thunk';
|
||||
import {
|
||||
fireEvent,
|
||||
initializeMocks,
|
||||
render as baseRender,
|
||||
waitFor,
|
||||
} from '../testUtils';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
const render = () => baseRender(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseTeam />
|
||||
</CourseAuthoringProvider>,
|
||||
{ path: mockPathname },
|
||||
);
|
||||
|
||||
describe('<CourseTeam />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
it('render CourseTeam component with 3 team members correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
|
||||
const {
|
||||
getByText, getByRole, getByTestId, queryAllByTestId,
|
||||
} = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByTestId('course-team-sidebar')).toBeInTheDocument();
|
||||
expect(queryAllByTestId('course-team-member')).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('render CourseTeam component with 1 team member correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
|
||||
const {
|
||||
getByText, getByRole, getByTestId, getAllByTestId,
|
||||
} = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByTestId('course-team-sidebar')).toBeInTheDocument();
|
||||
expect(getAllByTestId('course-team-member')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('render CourseTeam component without team member correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithoutUsers);
|
||||
|
||||
const {
|
||||
getByText, getByRole, getByTestId, queryAllByTestId,
|
||||
} = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByTestId('course-team-sidebar__initial')).toBeInTheDocument();
|
||||
expect(queryAllByTestId('course-team-member')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('render CourseTeam component with initial sidebar correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithoutUsers);
|
||||
|
||||
const { getByTestId, queryByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('course-team-sidebar__initial')).toBeInTheDocument();
|
||||
expect(queryByTestId('course-team-sidebar')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('render CourseTeam component without initial sidebar correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
|
||||
const { getByTestId, queryByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('course-team-sidebar__initial')).not.toBeInTheDocument();
|
||||
expect(getByTestId('course-team-sidebar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays AddUserForm when clicking the "Add New Member" button', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
|
||||
const { getByRole, queryByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('add-user-form')).not.toBeInTheDocument();
|
||||
const addButton = getByRole('button', { name: messages.addNewMemberButton.defaultMessage });
|
||||
fireEvent.click(addButton);
|
||||
expect(queryByTestId('add-user-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays AddUserForm when clicking the "Add a New Team member" button', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
|
||||
const { getByRole, queryByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('add-user-form')).not.toBeInTheDocument();
|
||||
const addButton = getByRole('button', { name: 'Add a new team member' });
|
||||
fireEvent.click(addButton);
|
||||
expect(queryByTestId('add-user-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('not displays "Add New Member" and AddTeamMember component when isAllowActions is false', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseTeamWithOneUser,
|
||||
allowActions: false,
|
||||
});
|
||||
|
||||
const { queryByRole, queryByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByRole('button', { name: messages.addNewMemberButton.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(queryByTestId('add-team-member')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete user', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
|
||||
const { queryByText } = render();
|
||||
|
||||
axiosMock
|
||||
.onDelete(updateCourseTeamUserApiUrl(courseId, 'staff@example.com'))
|
||||
.reply(200);
|
||||
|
||||
await executeThunk(deleteCourseTeamQuery(courseId, 'staff@example.com'), store.dispatch);
|
||||
expect(queryByText('staff@example.com')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should change role user', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
|
||||
const { getAllByText } = render();
|
||||
|
||||
axiosMock
|
||||
.onPut(updateCourseTeamUserApiUrl(courseId, 'staff@example.com'))
|
||||
.reply(200, { role: USER_ROLES.admin });
|
||||
|
||||
await executeThunk(changeRoleTeamUserQuery(courseId, 'staff@example.com', { role: USER_ROLES.admin }), store.dispatch);
|
||||
expect(getAllByText('Admin')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByRole } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
const { loadingCourseTeamStatus } = store.getState().courseTeam;
|
||||
expect(loadingCourseTeamStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets loading status to FAILED upon receiving a 404 response from the API', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(404);
|
||||
|
||||
render();
|
||||
|
||||
await waitFor(() => {
|
||||
const { loadingCourseTeamStatus } = store.getState().courseTeam;
|
||||
expect(loadingCourseTeamStatus).toEqual(RequestStatus.FAILED);
|
||||
});
|
||||
});
|
||||
});
|
||||
242
src/course-team/CourseTeam.test.tsx
Normal file
242
src/course-team/CourseTeam.test.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
screen,
|
||||
initializeMocks,
|
||||
render as baseRender,
|
||||
waitFor,
|
||||
} from '@src/testUtils';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { USER_ROLES } from '@src/constants';
|
||||
|
||||
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
|
||||
import { getCourseTeamApiUrl, updateCourseTeamUserApiUrl } from './data/api';
|
||||
import CourseTeam from './CourseTeam';
|
||||
import messages from './messages';
|
||||
import addUserFormMessages from './add-user-form/messages';
|
||||
|
||||
let axiosMock;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
const render = () => baseRender(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseTeam />
|
||||
</CourseAuthoringProvider>,
|
||||
{ path: mockPathname },
|
||||
);
|
||||
|
||||
describe('<CourseTeam />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
it('render CourseTeam component with 3 team members correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
|
||||
render();
|
||||
|
||||
expect(await screen.findByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('course-team-sidebar')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('course-team-member')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('render CourseTeam component with 1 team member correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
|
||||
render();
|
||||
|
||||
expect(await screen.findByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('course-team-sidebar')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('course-team-member')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('render CourseTeam component without team member correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithoutUsers);
|
||||
|
||||
render();
|
||||
|
||||
expect(await screen.findByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('course-team-sidebar__initial')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('course-team-member')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('render CourseTeam component with initial sidebar correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithoutUsers);
|
||||
|
||||
render();
|
||||
|
||||
expect(await screen.findByTestId('course-team-sidebar__initial')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('course-team-sidebar')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render CourseTeam component without initial sidebar correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
|
||||
render();
|
||||
|
||||
expect(await screen.findByTestId('course-team-sidebar')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('course-team-sidebar__initial')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays AddUserForm when clicking the "Add New Member" button', async () => {
|
||||
const user = userEvent.setup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
|
||||
render();
|
||||
|
||||
expect(screen.queryByTestId('add-user-form')).not.toBeInTheDocument();
|
||||
|
||||
const addButton = await screen.findByRole('button', { name: messages.addNewMemberButton.defaultMessage });
|
||||
expect(addButton).toBeInTheDocument();
|
||||
await user.click(addButton);
|
||||
expect(screen.queryByTestId('add-user-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays AddUserForm when clicking the "Add a New Team member" button', async () => {
|
||||
const user = userEvent.setup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
|
||||
render();
|
||||
|
||||
expect(screen.queryByTestId('add-user-form')).not.toBeInTheDocument();
|
||||
|
||||
const addButton = await screen.findByRole('button', { name: 'Add a new team member' });
|
||||
expect(addButton).toBeInTheDocument();
|
||||
await user.click(addButton);
|
||||
expect(screen.queryByTestId('add-user-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('not displays "Add New Member" and AddTeamMember component when isAllowActions is false', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseTeamWithOneUser,
|
||||
allowActions: false,
|
||||
});
|
||||
|
||||
render();
|
||||
|
||||
await screen.findByText(messages.headingTitle.defaultMessage);
|
||||
expect(screen.queryByRole('button', { name: messages.addNewMemberButton.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('add-team-member')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should delete user', async () => {
|
||||
const user = userEvent.setup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
const deleteUrl = updateCourseTeamUserApiUrl(courseId, 'staff@example.com');
|
||||
|
||||
axiosMock
|
||||
.onDelete(deleteUrl)
|
||||
.reply(200);
|
||||
|
||||
render();
|
||||
|
||||
const deleteButton = (await screen.findAllByRole('button', { name: /delete user/i }))[0];
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(await screen.findByText('Delete course team member'));
|
||||
const confirmDelete = screen.getByRole('button', { name: /delete/i });
|
||||
expect(confirmDelete).toBeInTheDocument();
|
||||
await user.click(confirmDelete);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.delete.length).toBe(1);
|
||||
});
|
||||
expect(axiosMock.history.delete[0].url).toEqual(deleteUrl);
|
||||
});
|
||||
|
||||
it('should change role user', async () => {
|
||||
const user = userEvent.setup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamMock);
|
||||
|
||||
const updateUrl = updateCourseTeamUserApiUrl(courseId, 'staff@example.com');
|
||||
|
||||
axiosMock
|
||||
.onPut(updateUrl)
|
||||
.reply(200, { role: USER_ROLES.admin });
|
||||
|
||||
render();
|
||||
|
||||
const updateButton = (await screen.findAllByRole('button', { name: /add admin access/i }))[0];
|
||||
expect(updateButton).toBeInTheDocument();
|
||||
await user.click(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.put.length).toBe(1);
|
||||
});
|
||||
expect(axiosMock.history.put[0].url).toEqual(updateUrl);
|
||||
});
|
||||
|
||||
it('should show warning modal when submitting an already existing user email', async () => {
|
||||
const user = userEvent.setup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
|
||||
render();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: messages.addNewMemberButton.defaultMessage }));
|
||||
await user.type(screen.getByRole('textbox'), 'staff@example.com');
|
||||
await user.click(screen.getByRole('button', { name: addUserFormMessages.addUserButton.defaultMessage }));
|
||||
|
||||
expect(await screen.findByText('Already a course team member')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide the form after successfully adding a new user', async () => {
|
||||
const user = userEvent.setup();
|
||||
const newEmail = 'newuser@example.com';
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, courseTeamWithOneUser);
|
||||
axiosMock
|
||||
.onPost(updateCourseTeamUserApiUrl(courseId, newEmail))
|
||||
.reply(200);
|
||||
|
||||
render();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: messages.addNewMemberButton.defaultMessage }));
|
||||
await user.type(screen.getByRole('textbox'), newEmail);
|
||||
await user.click(screen.getByRole('button', { name: addUserFormMessages.addUserButton.defaultMessage }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('add-user-form')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
render();
|
||||
|
||||
expect(await screen.findByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,11 +5,14 @@ import {
|
||||
Layout,
|
||||
} from '@openedx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import InternetConnectionAlert from '@src/generic/internet-connection-alert';
|
||||
import SubHeader from '@src/generic/sub-header/SubHeader';
|
||||
import { USER_ROLES } from '@src/constants';
|
||||
import getPageHeadTitle from '@src/generic/utils';
|
||||
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
|
||||
|
||||
import messages from './messages';
|
||||
import CourseTeamSideBar from './course-team-sidebar/CourseTeamSidebar';
|
||||
import AddUserForm from './add-user-form/AddUserForm';
|
||||
@@ -17,12 +20,9 @@ import AddTeamMember from './add-team-member/AddTeamMember';
|
||||
import CourseTeamMember from './course-team-member/CourseTeamMember';
|
||||
import InfoModal from './info-modal/InfoModal';
|
||||
import { useCourseTeam } from './hooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const CourseTeam = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
|
||||
const {
|
||||
@@ -43,7 +43,6 @@ const CourseTeam = () => {
|
||||
isShowAddTeamMember,
|
||||
isShowInitialSidebar,
|
||||
isShowUserFilledSidebar,
|
||||
isInternetConnectionAlertFailed,
|
||||
openForm,
|
||||
hideForm,
|
||||
closeInfoModal,
|
||||
@@ -51,8 +50,7 @@ const CourseTeam = () => {
|
||||
handleOpenDeleteModal,
|
||||
handleDeleteUserSubmit,
|
||||
handleChangeRoleUserSubmit,
|
||||
handleInternetConnectionFailed,
|
||||
} = useCourseTeam({ intl, courseId });
|
||||
} = useCourseTeam();
|
||||
|
||||
document.title = getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
@@ -86,7 +84,7 @@ const CourseTeam = () => {
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
headerActions={isAllowActions && (
|
||||
headerActions={isAllowActions ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
iconBefore={IconAdd}
|
||||
@@ -96,7 +94,7 @@ const CourseTeam = () => {
|
||||
>
|
||||
{intl.formatMessage(messages.addNewMemberButton)}
|
||||
</Button>
|
||||
)}
|
||||
) : undefined}
|
||||
/>
|
||||
<section className="course-team-section">
|
||||
<div className="members-container">
|
||||
@@ -139,7 +137,7 @@ const CourseTeam = () => {
|
||||
isOpen={isInfoModalOpen}
|
||||
close={closeInfoModal}
|
||||
currentEmail={currentEmail}
|
||||
errorMessage={errorMessage}
|
||||
errorMessage={errorMessage ?? ''}
|
||||
courseName={courseName}
|
||||
modalType={modalType}
|
||||
onDeleteSubmit={handleDeleteUserSubmit}
|
||||
@@ -161,9 +159,9 @@ const CourseTeam = () => {
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
<InternetConnectionAlert
|
||||
isFailed={isInternetConnectionAlertFailed}
|
||||
isFailed={errorMessage !== undefined}
|
||||
isQueryPending={isQueryPending}
|
||||
onInternetConnectionFailed={handleInternetConnectionFailed}
|
||||
onInternetConnectionFailed={/* istanbul ignore next */ () => {}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -1,128 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
act,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { EXAMPLE_USER_EMAIL } from '../constants';
|
||||
import initializeStore from '../../store';
|
||||
import { USER_ROLES } from '../../constants';
|
||||
import { updateCourseTeamUserApiUrl } from '../data/api';
|
||||
import { createCourseTeamQuery } from '../data/thunk';
|
||||
import { executeThunk } from '../../utils';
|
||||
import AddUserForm from './AddUserForm';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const onSubmitMock = jest.fn();
|
||||
const onCancelMock = jest.fn();
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<AddUserForm
|
||||
onSubmit={onSubmitMock}
|
||||
onCancel={onCancelMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<AddUserForm />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('render AddUserForm component correctly', () => {
|
||||
const { getByText, getByPlaceholderText } = render(<RootWrapper />);
|
||||
|
||||
expect(getByText(messages.formTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.formLabel.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByPlaceholderText(messages.formPlaceholder.defaultMessage
|
||||
.replace('{email}', EXAMPLE_USER_EMAIL))).toBeInTheDocument();
|
||||
expect(getByText(messages.cancelButton.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.addUserButton.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onSubmit when the "Add User" button is clicked with a valid email', async () => {
|
||||
const { getByPlaceholderText, getByRole } = render(<RootWrapper />);
|
||||
|
||||
const emailInput = getByPlaceholderText(messages.formPlaceholder.defaultMessage.replace('{email}', EXAMPLE_USER_EMAIL));
|
||||
const addUserButton = getByRole('button', { name: messages.addUserButton.defaultMessage });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: EXAMPLE_USER_EMAIL } });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(addUserButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmitMock).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmitMock).toHaveBeenCalledWith(
|
||||
{ email: EXAMPLE_USER_EMAIL },
|
||||
expect.objectContaining({ submitForm: expect.any(Function) }),
|
||||
);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(updateCourseTeamUserApiUrl(courseId, EXAMPLE_USER_EMAIL), { role: USER_ROLES.staff })
|
||||
.reply(200, { role: USER_ROLES.staff });
|
||||
|
||||
await executeThunk(createCourseTeamQuery(courseId, EXAMPLE_USER_EMAIL), store.dispatch);
|
||||
});
|
||||
|
||||
it('calls onCancel when the "Cancel" button is clicked', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
const cancelButton = getByText(messages.cancelButton.defaultMessage);
|
||||
fireEvent.click(cancelButton);
|
||||
expect(onCancelMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('"Add User" button is disabled when the email input field is empty', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
const addUserButton = getByText(messages.addUserButton.defaultMessage);
|
||||
expect(addUserButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('"Add User" button is not disabled when the email input field is not empty', () => {
|
||||
const { getByPlaceholderText, getByText } = render(<RootWrapper />);
|
||||
|
||||
const emailInput = getByPlaceholderText(
|
||||
messages.formPlaceholder.defaultMessage.replace('{email}', EXAMPLE_USER_EMAIL),
|
||||
);
|
||||
const addUserButton = getByText(messages.addUserButton.defaultMessage);
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'user@example.com' } });
|
||||
expect(addUserButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
96
src/course-team/add-user-form/AddUserForm.test.tsx
Normal file
96
src/course-team/add-user-form/AddUserForm.test.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
|
||||
import { EXAMPLE_USER_EMAIL } from '../constants';
|
||||
import AddUserForm from './AddUserForm';
|
||||
import messages from './messages';
|
||||
|
||||
const mockPathname = '/foo-bar';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const onSubmitMock = jest.fn();
|
||||
const onCancelMock = jest.fn();
|
||||
|
||||
const renderComponent = () => render(
|
||||
<AddUserForm
|
||||
onSubmit={onSubmitMock}
|
||||
onCancel={onCancelMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
describe('<AddUserForm />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
});
|
||||
|
||||
it('render AddUserForm component correctly', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText(messages.formTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.formLabel.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(messages.formPlaceholder.defaultMessage
|
||||
.replace('{email}', EXAMPLE_USER_EMAIL))).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.cancelButton.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.addUserButton.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onSubmit when the "Add User" button is clicked with a valid email', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
const emailInput = screen.getByPlaceholderText(messages.formPlaceholder.defaultMessage.replace('{email}', EXAMPLE_USER_EMAIL));
|
||||
const addUserButton = screen.getByRole('button', { name: messages.addUserButton.defaultMessage });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: EXAMPLE_USER_EMAIL } });
|
||||
|
||||
await user.click(addUserButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmitMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(onSubmitMock).toHaveBeenCalledWith(
|
||||
{ email: EXAMPLE_USER_EMAIL },
|
||||
expect.objectContaining({ submitForm: expect.any(Function) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls onCancel when the "Cancel" button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
const cancelButton = screen.getByText(messages.cancelButton.defaultMessage);
|
||||
await user.click(cancelButton);
|
||||
expect(onCancelMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('"Add User" button is disabled when the email input field is empty', () => {
|
||||
renderComponent();
|
||||
|
||||
const addUserButton = screen.getByText(messages.addUserButton.defaultMessage);
|
||||
expect(addUserButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('"Add User" button is not disabled when the email input field is not empty', () => {
|
||||
renderComponent();
|
||||
|
||||
const emailInput = screen.getByPlaceholderText(
|
||||
messages.formPlaceholder.defaultMessage.replace('{email}', EXAMPLE_USER_EMAIL),
|
||||
);
|
||||
const addUserButton = screen.getByText(messages.addUserButton.defaultMessage);
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'user@example.com' } });
|
||||
expect(addUserButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ export const MODAL_TYPES = {
|
||||
warning: 'warning',
|
||||
} as const;
|
||||
|
||||
export type ModalType = typeof MODAL_TYPES[keyof typeof MODAL_TYPES];
|
||||
|
||||
export const BADGE_STATES = {
|
||||
admin: 'primary-700',
|
||||
staff: 'gray-500',
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { USER_ROLES } from '../../constants';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getCourseTeamApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_team/${courseId}`;
|
||||
export const updateCourseTeamUserApiUrl = (courseId, email) => `${getApiBaseUrl()}/course_team/${courseId}/${email}`;
|
||||
|
||||
/**
|
||||
* Get course team.
|
||||
* @param {string} courseId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getCourseTeam(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseTeamApiUrl(courseId));
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create course team user.
|
||||
* @param {string} courseId
|
||||
* @param {string} email
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function createTeamUser(courseId, email) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.post(updateCourseTeamUserApiUrl(courseId, email), { role: USER_ROLES.staff });
|
||||
}
|
||||
|
||||
/**
|
||||
* Change role course team user.
|
||||
* @param {string} courseId
|
||||
* @param {string} email
|
||||
* @param {string} role
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function changeRoleTeamUser(courseId, email, role) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.put(updateCourseTeamUserApiUrl(courseId, email), { role });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete course team user.
|
||||
* @param {string} courseId
|
||||
* @param {string} email
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function deleteTeamUser(courseId, email) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(updateCourseTeamUserApiUrl(courseId, email));
|
||||
}
|
||||
54
src/course-team/data/api.ts
Normal file
54
src/course-team/data/api.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { USER_ROLES } from '@src/constants';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getCourseTeamApiUrl = (courseId: string) => `${getApiBaseUrl()}/api/contentstore/v1/course_team/${courseId}`;
|
||||
export const updateCourseTeamUserApiUrl = (courseId: string, email: string) => `${getApiBaseUrl()}/course_team/${courseId}/${email}`;
|
||||
|
||||
export interface CourseTeamUser {
|
||||
id: number;
|
||||
email: string;
|
||||
role: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface CourseTeam {
|
||||
users: CourseTeamUser[];
|
||||
allowActions: boolean;
|
||||
showTransferOwnershipHint: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get course team.
|
||||
*/
|
||||
export async function getCourseTeam(courseId: string): Promise<CourseTeam> {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseTeamApiUrl(courseId));
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create course team user.
|
||||
*/
|
||||
export async function createTeamUser(courseId: string, email: string) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.post(updateCourseTeamUserApiUrl(courseId, email), { role: USER_ROLES.staff });
|
||||
}
|
||||
|
||||
/**
|
||||
* Change role course team user.
|
||||
*/
|
||||
export async function changeRoleTeamUser(courseId: string, email: string, role: string) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.put(updateCourseTeamUserApiUrl(courseId, email), { role });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete course team user.
|
||||
*/
|
||||
export async function deleteTeamUser(courseId: string, email: string) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(updateCourseTeamUserApiUrl(courseId, email));
|
||||
}
|
||||
64
src/course-team/data/apiHooks.ts
Normal file
64
src/course-team/data/apiHooks.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import * as api from './api';
|
||||
|
||||
export const courseTeamQueryKeys = {
|
||||
all: ['courseTeam'],
|
||||
/** Base key for course team data specific to a courseId */
|
||||
courseTeam: (courseId: string) => [...courseTeamQueryKeys.all, courseId],
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch the course team for the given courseId
|
||||
*/
|
||||
export const useCourseTeamData = (courseId: string) => (
|
||||
useQuery<api.CourseTeam, AxiosError>({
|
||||
queryKey: courseTeamQueryKeys.courseTeam(courseId),
|
||||
queryFn: () => api.getCourseTeam(courseId),
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook to create a new course team user
|
||||
*/
|
||||
export const useCreateTeamUser = (courseId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, AxiosError, string>({
|
||||
mutationFn: (email: string) => api.createTeamUser(courseId, email),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: courseTeamQueryKeys.courseTeam(courseId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type ChangeRoleRequest = {
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to change the role of a course team user
|
||||
*/
|
||||
export const useChangeRoleTeamUser = (courseId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, AxiosError, ChangeRoleRequest>({
|
||||
mutationFn: ({ email, role }: ChangeRoleRequest) => api.changeRoleTeamUser(courseId, email, role),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: courseTeamQueryKeys.courseTeam(courseId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to delete a course team user
|
||||
*/
|
||||
export const useDeleteTeamUser = (courseId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, AxiosError, string>({
|
||||
mutationFn: (email: string) => api.deleteTeamUser(courseId, email),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: courseTeamQueryKeys.courseTeam(courseId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export const getCourseTeamUsers = (state) => state.courseTeam.users;
|
||||
export const getCourseTeamLoadingStatus = (state) => state.courseTeam.loadingCourseTeamStatus;
|
||||
export const getErrorMessage = (state) => state.courseTeam.errorMessage;
|
||||
export const getIsAllowActions = (state) => state.courseTeam.allowActions;
|
||||
export const getIsOwnershipHint = (state) => state.courseTeam.showTransferOwnershipHint;
|
||||
export const getSavingStatus = (state) => state.courseTeam.savingStatus;
|
||||
@@ -1,46 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'courseTeam',
|
||||
initialState: {
|
||||
loadingCourseTeamStatus: RequestStatus.IN_PROGRESS,
|
||||
savingStatus: '',
|
||||
users: [],
|
||||
showTransferOwnershipHint: false,
|
||||
allowActions: false,
|
||||
errorMessage: '',
|
||||
},
|
||||
reducers: {
|
||||
fetchCourseTeamSuccess: (state, { payload }) => {
|
||||
state.users = payload.users;
|
||||
state.showTransferOwnershipHint = payload.showTransferOwnershipHint;
|
||||
state.allowActions = payload.allowActions;
|
||||
},
|
||||
updateLoadingCourseTeamStatus: (state, { payload }) => {
|
||||
state.loadingCourseTeamStatus = payload.status;
|
||||
},
|
||||
deleteCourseTeamUser: (state, { payload }) => {
|
||||
state.users = state.users.filter((user) => user.email !== payload);
|
||||
},
|
||||
updateSavingStatus: (state, { payload }) => {
|
||||
state.savingStatus = payload.status;
|
||||
},
|
||||
setErrorMessage: (state, { payload }) => {
|
||||
state.errorMessage = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
fetchCourseTeamSuccess,
|
||||
updateLoadingCourseTeamStatus,
|
||||
deleteCourseTeamUser,
|
||||
updateSavingStatus,
|
||||
setErrorMessage,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
reducer,
|
||||
} = slice;
|
||||
@@ -1,91 +0,0 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import {
|
||||
getCourseTeam,
|
||||
deleteTeamUser,
|
||||
createTeamUser,
|
||||
changeRoleTeamUser,
|
||||
} from './api';
|
||||
import {
|
||||
fetchCourseTeamSuccess,
|
||||
updateLoadingCourseTeamStatus,
|
||||
deleteCourseTeamUser,
|
||||
updateSavingStatus,
|
||||
setErrorMessage,
|
||||
} from './slice';
|
||||
|
||||
export function fetchCourseTeamQuery(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const courseTeam = await getCourseTeam(courseId);
|
||||
dispatch(fetchCourseTeamSuccess(courseTeam));
|
||||
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createCourseTeamQuery(courseId, email) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
await createTeamUser(courseId, email);
|
||||
const courseTeam = await getCourseTeam(courseId);
|
||||
dispatch(fetchCourseTeamSuccess(courseTeam));
|
||||
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message = error?.response?.data?.error || '';
|
||||
dispatch(setErrorMessage(message));
|
||||
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function changeRoleTeamUserQuery(courseId, email, role) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
await changeRoleTeamUser(courseId, email, role);
|
||||
const courseTeam = await getCourseTeam(courseId);
|
||||
dispatch(fetchCourseTeamSuccess(courseTeam));
|
||||
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteCourseTeamQuery(courseId, email) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
await deleteTeamUser(courseId, email);
|
||||
dispatch(deleteCourseTeamUser(email));
|
||||
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import {
|
||||
changeRoleTeamUserQuery,
|
||||
createCourseTeamQuery,
|
||||
deleteCourseTeamQuery,
|
||||
fetchCourseTeamQuery,
|
||||
} from './data/thunk';
|
||||
import {
|
||||
getCourseTeamLoadingStatus,
|
||||
getCourseTeamUsers,
|
||||
getErrorMessage,
|
||||
getIsAllowActions,
|
||||
getIsOwnershipHint, getSavingStatus,
|
||||
} from './data/selectors';
|
||||
import { setErrorMessage } from './data/slice';
|
||||
import { MODAL_TYPES } from './constants';
|
||||
|
||||
const useCourseTeam = ({ courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { email: currentUserEmail } = getAuthenticatedUser();
|
||||
const { courseDetails } = useCourseAuthoringContext();
|
||||
|
||||
const [modalType, setModalType] = useState(MODAL_TYPES.delete);
|
||||
const [isInfoModalOpen, openInfoModal, closeInfoModal] = useToggle(false);
|
||||
const [isFormVisible, openForm, hideForm] = useToggle(false);
|
||||
const [currentEmail, setCurrentEmail] = useState('');
|
||||
const [isQueryPending, setIsQueryPending] = useState(false);
|
||||
const courseTeamUsers = useSelector(getCourseTeamUsers);
|
||||
const errorMessage = useSelector(getErrorMessage);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const isAllowActions = useSelector(getIsAllowActions);
|
||||
const isOwnershipHint = useSelector(getIsOwnershipHint);
|
||||
const loadingCourseTeamStatus = useSelector(getCourseTeamLoadingStatus);
|
||||
|
||||
const isSingleAdmin = courseTeamUsers.filter((user) => user.role === USER_ROLES.admin).length === 1;
|
||||
|
||||
const handleOpenInfoModal = (type, email) => {
|
||||
setCurrentEmail(email);
|
||||
setModalType(type);
|
||||
openInfoModal();
|
||||
};
|
||||
|
||||
const handleCloseInfoModal = () => {
|
||||
dispatch(setErrorMessage(''));
|
||||
closeInfoModal();
|
||||
};
|
||||
|
||||
const handleAddUserSubmit = (data) => {
|
||||
setIsQueryPending(true);
|
||||
|
||||
const { email } = data;
|
||||
const isUserContains = courseTeamUsers.some((user) => user.email === email);
|
||||
|
||||
if (isUserContains) {
|
||||
handleOpenInfoModal(MODAL_TYPES.warning, email);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(createCourseTeamQuery(courseId, email)).then((result) => {
|
||||
if (result) {
|
||||
hideForm();
|
||||
dispatch(setErrorMessage(''));
|
||||
return;
|
||||
}
|
||||
|
||||
handleOpenInfoModal(MODAL_TYPES.error, email);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteUserSubmit = () => {
|
||||
setIsQueryPending(true);
|
||||
dispatch(deleteCourseTeamQuery(courseId, currentEmail));
|
||||
handleCloseInfoModal();
|
||||
};
|
||||
|
||||
const handleChangeRoleUserSubmit = (email, role) => {
|
||||
setIsQueryPending(true);
|
||||
dispatch(changeRoleTeamUserQuery(courseId, email, role));
|
||||
};
|
||||
|
||||
const handleInternetConnectionFailed = () => {
|
||||
setIsQueryPending(false);
|
||||
};
|
||||
|
||||
const handleOpenDeleteModal = (email) => {
|
||||
handleOpenInfoModal(MODAL_TYPES.delete, email);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseTeamQuery(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
||||
setIsQueryPending(false);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
}, [savingStatus]);
|
||||
|
||||
return {
|
||||
modalType,
|
||||
errorMessage,
|
||||
courseName: courseDetails?.name || '',
|
||||
currentEmail,
|
||||
courseTeamUsers,
|
||||
currentUserEmail,
|
||||
isLoading: loadingCourseTeamStatus === RequestStatus.IN_PROGRESS,
|
||||
isLoadingDenied: loadingCourseTeamStatus === RequestStatus.DENIED,
|
||||
isSingleAdmin,
|
||||
isFormVisible,
|
||||
isAllowActions,
|
||||
isInfoModalOpen,
|
||||
isOwnershipHint,
|
||||
isQueryPending,
|
||||
isInternetConnectionAlertFailed: savingStatus === RequestStatus.FAILED,
|
||||
isShowAddTeamMember: courseTeamUsers.length === 1 && isAllowActions,
|
||||
isShowInitialSidebar: !courseTeamUsers.length && !isFormVisible,
|
||||
isShowUserFilledSidebar: Boolean(courseTeamUsers.length) || isFormVisible,
|
||||
openForm,
|
||||
hideForm,
|
||||
closeInfoModal,
|
||||
handleAddUserSubmit,
|
||||
handleOpenInfoModal,
|
||||
handleOpenDeleteModal,
|
||||
handleDeleteUserSubmit,
|
||||
handleChangeRoleUserSubmit,
|
||||
handleInternetConnectionFailed,
|
||||
};
|
||||
};
|
||||
|
||||
export { useCourseTeam };
|
||||
118
src/course-team/hooks.tsx
Normal file
118
src/course-team/hooks.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useState } from 'react';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import messages from './messages';
|
||||
import { MODAL_TYPES, type ModalType } from './constants';
|
||||
import {
|
||||
useChangeRoleTeamUser,
|
||||
useCourseTeamData,
|
||||
useCreateTeamUser,
|
||||
useDeleteTeamUser,
|
||||
} from './data/apiHooks';
|
||||
|
||||
const useCourseTeam = () => {
|
||||
const intl = useIntl();
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
|
||||
const { email: currentUserEmail } = getAuthenticatedUser();
|
||||
const { courseDetails } = useCourseAuthoringContext();
|
||||
const {
|
||||
data,
|
||||
isPending: isLoadingCourseTeamStatus,
|
||||
failureReason: courseTeamQueryError,
|
||||
} = useCourseTeamData(courseId);
|
||||
|
||||
const {
|
||||
users: courseTeamUsers = [],
|
||||
allowActions: isAllowActions = false,
|
||||
showTransferOwnershipHint: isOwnershipHint = false,
|
||||
} = data ?? {};
|
||||
|
||||
const addUserMutation = useCreateTeamUser(courseId);
|
||||
const editUserRoleMutation = useChangeRoleTeamUser(courseId);
|
||||
const deleteUserMutation = useDeleteTeamUser(courseId);
|
||||
|
||||
const [modalType, setModalType] = useState<ModalType>(MODAL_TYPES.delete);
|
||||
const [isInfoModalOpen, openInfoModal, closeInfoModal] = useToggle(false);
|
||||
const [isFormVisible, openForm, hideForm] = useToggle(false);
|
||||
const [currentEmail, setCurrentEmail] = useState('');
|
||||
|
||||
const courseTeamStatusIsDenied = courseTeamQueryError?.response?.status === 403;
|
||||
|
||||
const isSingleAdmin = courseTeamUsers.filter((user) => user.role === USER_ROLES.admin).length === 1;
|
||||
|
||||
const handleOpenInfoModal = (type: ModalType, email: string) => {
|
||||
setCurrentEmail(email);
|
||||
setModalType(type);
|
||||
openInfoModal();
|
||||
};
|
||||
|
||||
const handleAddUserSubmit = (body: { email: string }) => {
|
||||
const { email } = body;
|
||||
const isUserContains = courseTeamUsers.some((user) => user.email === email);
|
||||
|
||||
if (isUserContains) {
|
||||
handleOpenInfoModal(MODAL_TYPES.warning, email);
|
||||
return;
|
||||
}
|
||||
|
||||
addUserMutation.mutateAsync(email).then(() => {
|
||||
hideForm();
|
||||
}).catch(() => {
|
||||
handleOpenInfoModal(MODAL_TYPES.error, email);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteUserSubmit = () => {
|
||||
deleteUserMutation.mutate(currentEmail);
|
||||
closeInfoModal();
|
||||
};
|
||||
|
||||
const handleChangeRoleUserSubmit = (email: string, role: string) => {
|
||||
editUserRoleMutation.mutate({ email, role });
|
||||
};
|
||||
|
||||
const handleOpenDeleteModal = (email: string) => {
|
||||
handleOpenInfoModal(MODAL_TYPES.delete, email);
|
||||
};
|
||||
|
||||
const getErrorMessage = () => {
|
||||
const errorObject = addUserMutation.error ?? editUserRoleMutation.error ?? deleteUserMutation.error;
|
||||
// @ts-ignore
|
||||
return errorObject?.response?.data?.error ?? intl.formatMessage(messages.unknownError);
|
||||
};
|
||||
|
||||
return {
|
||||
modalType,
|
||||
courseName: courseDetails?.name ?? '',
|
||||
currentEmail,
|
||||
courseTeamUsers,
|
||||
currentUserEmail,
|
||||
errorMessage: getErrorMessage(),
|
||||
isLoading: isLoadingCourseTeamStatus,
|
||||
isLoadingDenied: courseTeamStatusIsDenied,
|
||||
isSingleAdmin,
|
||||
isFormVisible,
|
||||
isAllowActions,
|
||||
isInfoModalOpen,
|
||||
isOwnershipHint,
|
||||
isQueryPending: addUserMutation.isPending || deleteUserMutation.isPending || editUserRoleMutation.isPending,
|
||||
isShowAddTeamMember: courseTeamUsers.length === 1 && isAllowActions,
|
||||
isShowInitialSidebar: !courseTeamUsers.length && !isFormVisible,
|
||||
isShowUserFilledSidebar: Boolean(courseTeamUsers?.length) || isFormVisible,
|
||||
openForm,
|
||||
hideForm,
|
||||
closeInfoModal,
|
||||
handleAddUserSubmit,
|
||||
handleOpenInfoModal,
|
||||
handleOpenDeleteModal,
|
||||
handleDeleteUserSubmit,
|
||||
handleChangeRoleUserSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export { useCourseTeam };
|
||||
@@ -13,6 +13,11 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-team.button.new-team-member',
|
||||
defaultMessage: 'New team member',
|
||||
},
|
||||
unknownError: {
|
||||
id: 'course-authoring.course-team.error.unknown',
|
||||
defaultMessage: 'An unexpected error occurred. Please try again.',
|
||||
description: 'Fallback error message shown when the API returns an error in an unexpected format.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -15,7 +15,6 @@ import { reducer as advancedSettingsReducer } from './advanced-settings/data/sli
|
||||
import { reducer as studioHomeReducer } from './studio-home/data/slice';
|
||||
import { reducer as scheduleAndDetailsReducer } from './schedule-and-details/data/slice';
|
||||
import { reducer as filesReducer } from './files-and-videos/files-page/data/slice';
|
||||
import { reducer as courseTeamReducer } from './course-team/data/slice';
|
||||
import { reducer as CourseUpdatesReducer } from './course-updates/data/slice';
|
||||
import { reducer as processingNotificationReducer } from './generic/processing-notification/data/slice';
|
||||
import { reducer as courseExportReducer } from './export-page/data/slice';
|
||||
@@ -45,7 +44,6 @@ export interface DeprecatedReduxState {
|
||||
studioHome: InferState<typeof studioHomeReducer>;
|
||||
models: Record<string, any>;
|
||||
live: Record<string, any>;
|
||||
courseTeam: Record<string, any>;
|
||||
courseUpdates: Record<string, any>;
|
||||
processingNotification: Record<string, any>;
|
||||
courseExport: Record<string, any>;
|
||||
@@ -79,7 +77,6 @@ export default function initializeStore(preloadedState: Partial<DeprecatedReduxS
|
||||
studioHome: studioHomeReducer,
|
||||
models: modelsReducer,
|
||||
live: liveReducer,
|
||||
courseTeam: courseTeamReducer,
|
||||
courseUpdates: CourseUpdatesReducer,
|
||||
processingNotification: processingNotificationReducer,
|
||||
courseExport: courseExportReducer,
|
||||
|
||||
Reference in New Issue
Block a user