From 56726448fc5d4caa5aaa8d9b0d751dd636fb8107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Tue, 24 Feb 2026 10:11:36 -0500 Subject: [PATCH] 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 --- src/course-team/CourseTeam.test.jsx | 219 ---------------- src/course-team/CourseTeam.test.tsx | 242 ++++++++++++++++++ .../{CourseTeam.jsx => CourseTeam.tsx} | 28 +- .../add-user-form/AddUserForm.test.jsx | 128 --------- .../add-user-form/AddUserForm.test.tsx | 96 +++++++ src/course-team/constants.ts | 2 + src/course-team/data/api.js | 54 ---- src/course-team/data/api.ts | 54 ++++ src/course-team/data/apiHooks.ts | 64 +++++ src/course-team/data/selectors.js | 6 - src/course-team/data/slice.js | 46 ---- src/course-team/data/thunk.js | 91 ------- src/course-team/hooks.jsx | 139 ---------- src/course-team/hooks.tsx | 118 +++++++++ src/course-team/messages.ts | 5 + src/store.ts | 3 - 16 files changed, 594 insertions(+), 701 deletions(-) delete mode 100644 src/course-team/CourseTeam.test.jsx create mode 100644 src/course-team/CourseTeam.test.tsx rename src/course-team/{CourseTeam.jsx => CourseTeam.tsx} (89%) delete mode 100644 src/course-team/add-user-form/AddUserForm.test.jsx create mode 100644 src/course-team/add-user-form/AddUserForm.test.tsx delete mode 100644 src/course-team/data/api.js create mode 100644 src/course-team/data/api.ts create mode 100644 src/course-team/data/apiHooks.ts delete mode 100644 src/course-team/data/selectors.js delete mode 100644 src/course-team/data/slice.js delete mode 100644 src/course-team/data/thunk.js delete mode 100644 src/course-team/hooks.jsx create mode 100644 src/course-team/hooks.tsx diff --git a/src/course-team/CourseTeam.test.jsx b/src/course-team/CourseTeam.test.jsx deleted file mode 100644 index bc1ccad1f..000000000 --- a/src/course-team/CourseTeam.test.jsx +++ /dev/null @@ -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( - - - , - { path: mockPathname }, -); - -describe('', () => { - 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); - }); - }); -}); diff --git a/src/course-team/CourseTeam.test.tsx b/src/course-team/CourseTeam.test.tsx new file mode 100644 index 000000000..c19514284 --- /dev/null +++ b/src/course-team/CourseTeam.test.tsx @@ -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( + + + , + { path: mockPathname }, +); + +describe('', () => { + 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(); + }); +}); diff --git a/src/course-team/CourseTeam.jsx b/src/course-team/CourseTeam.tsx similarity index 89% rename from src/course-team/CourseTeam.jsx rename to src/course-team/CourseTeam.tsx index 9fd1d04de..f27e7235b 100644 --- a/src/course-team/CourseTeam.jsx +++ b/src/course-team/CourseTeam.tsx @@ -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 = () => { { > {intl.formatMessage(messages.addNewMemberButton)} - )} + ) : undefined} />
@@ -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 = () => {
{}} />
diff --git a/src/course-team/add-user-form/AddUserForm.test.jsx b/src/course-team/add-user-form/AddUserForm.test.jsx deleted file mode 100644 index d3716b14c..000000000 --- a/src/course-team/add-user-form/AddUserForm.test.jsx +++ /dev/null @@ -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 = () => ( - - - - - -); - -describe('', () => { - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - }); -}); diff --git a/src/course-team/add-user-form/AddUserForm.test.tsx b/src/course-team/add-user-form/AddUserForm.test.tsx new file mode 100644 index 000000000..57207c4d2 --- /dev/null +++ b/src/course-team/add-user-form/AddUserForm.test.tsx @@ -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( + , +); + +describe('', () => { + 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(); + }); +}); diff --git a/src/course-team/constants.ts b/src/course-team/constants.ts index 542ea5534..42ab37368 100644 --- a/src/course-team/constants.ts +++ b/src/course-team/constants.ts @@ -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', diff --git a/src/course-team/data/api.js b/src/course-team/data/api.js deleted file mode 100644 index 321671600..000000000 --- a/src/course-team/data/api.js +++ /dev/null @@ -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} - */ -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} - */ -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} - */ -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} - */ -export async function deleteTeamUser(courseId, email) { - await getAuthenticatedHttpClient() - .delete(updateCourseTeamUserApiUrl(courseId, email)); -} diff --git a/src/course-team/data/api.ts b/src/course-team/data/api.ts new file mode 100644 index 000000000..993ed9f76 --- /dev/null +++ b/src/course-team/data/api.ts @@ -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 { + 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)); +} diff --git a/src/course-team/data/apiHooks.ts b/src/course-team/data/apiHooks.ts new file mode 100644 index 000000000..0f913fb03 --- /dev/null +++ b/src/course-team/data/apiHooks.ts @@ -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({ + 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({ + 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({ + 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({ + mutationFn: (email: string) => api.deleteTeamUser(courseId, email), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: courseTeamQueryKeys.courseTeam(courseId) }); + }, + }); +}; diff --git a/src/course-team/data/selectors.js b/src/course-team/data/selectors.js deleted file mode 100644 index 99c602fda..000000000 --- a/src/course-team/data/selectors.js +++ /dev/null @@ -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; diff --git a/src/course-team/data/slice.js b/src/course-team/data/slice.js deleted file mode 100644 index 374210b03..000000000 --- a/src/course-team/data/slice.js +++ /dev/null @@ -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; diff --git a/src/course-team/data/thunk.js b/src/course-team/data/thunk.js deleted file mode 100644 index aa74bd91a..000000000 --- a/src/course-team/data/thunk.js +++ /dev/null @@ -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; - } - }; -} diff --git a/src/course-team/hooks.jsx b/src/course-team/hooks.jsx deleted file mode 100644 index 6376d625a..000000000 --- a/src/course-team/hooks.jsx +++ /dev/null @@ -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 }; diff --git a/src/course-team/hooks.tsx b/src/course-team/hooks.tsx new file mode 100644 index 000000000..df33a2e3a --- /dev/null +++ b/src/course-team/hooks.tsx @@ -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(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 }; diff --git a/src/course-team/messages.ts b/src/course-team/messages.ts index c6a6ebc05..3aa41af97 100644 --- a/src/course-team/messages.ts +++ b/src/course-team/messages.ts @@ -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; diff --git a/src/store.ts b/src/store.ts index 99c5fa2e2..f3b265baa 100644 --- a/src/store.ts +++ b/src/store.ts @@ -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; models: Record; live: Record; - courseTeam: Record; courseUpdates: Record; processingNotification: Record; courseExport: Record; @@ -79,7 +77,6 @@ export default function initializeStore(preloadedState: Partial