From 01ddac380fd92298b74b3e2acf9fb33a3aa2b217 Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Thu, 28 Sep 2023 18:36:51 -0400 Subject: [PATCH] fix: studio home UI bugs (#611) --- src/assets/scss/_form.scss | 11 - .../CourseRerunSidebar.test.jsx | 2 +- .../course-rerun-sidebar/index.jsx | 25 +- .../course-rerun-sidebar/messages.js | 2 +- src/course-rerun/index.jsx | 16 +- src/course-rerun/messages.js | 2 +- .../export-modal-error/ExportModalError.jsx | 7 +- .../CreateOrRerunCourseForm.jsx | 2 +- .../CreateOrRerunCourseForm.test.jsx | 261 ++++++++++-------- .../create-or-rerun-course/constants.js | 4 - .../factories/mockApiResponses.jsx | 31 +++ src/generic/create-or-rerun-course/hooks.jsx | 14 +- src/studio-home/StudioHome.jsx | 55 +++- src/studio-home/StudioHome.test.jsx | 39 ++- src/studio-home/__mocks__/studioHomeMock.js | 2 +- src/studio-home/card-item/index.jsx | 1 + .../collapsible-state-with-action/index.jsx | 11 +- src/studio-home/messages.js | 4 + .../organization-section/index.jsx | 2 +- .../processing-courses/course-item/index.jsx | 16 +- src/studio-home/processing-courses/index.jsx | 6 +- src/studio-home/scss/StudioHome.scss | 2 +- .../tabs-section/courses-tab/index.jsx | 4 + src/studio-home/tabs-section/index.jsx | 6 +- 24 files changed, 340 insertions(+), 185 deletions(-) delete mode 100644 src/generic/create-or-rerun-course/constants.js create mode 100644 src/generic/create-or-rerun-course/factories/mockApiResponses.jsx diff --git a/src/assets/scss/_form.scss b/src/assets/scss/_form.scss index 326bd7a5a..622513d14 100644 --- a/src/assets/scss/_form.scss +++ b/src/assets/scss/_form.scss @@ -79,14 +79,3 @@ color: $black; } } - -.dropdown-group-wrapper { - position: relative; - z-index: $zindex-dropdown; - margin-left: auto; - - .dropdown-container { - position: absolute; - width: 100%; - } -} diff --git a/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx b/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx index ba70ee5f9..d497d873f 100644 --- a/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx +++ b/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx @@ -44,7 +44,7 @@ describe('', () => { const { getByText } = renderComponent(); expect(getByText(messages.sectionTitle1.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.sectionDescription1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.sectionDescription1.defaultMessage, { exact: false })).toBeInTheDocument(); expect(getByText(messages.sectionTitle2.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.sectionDescription2.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.sectionTitle3.defaultMessage)).toBeInTheDocument(); diff --git a/src/course-rerun/course-rerun-sidebar/index.jsx b/src/course-rerun/course-rerun-sidebar/index.jsx index db5fa6dee..32437b13a 100644 --- a/src/course-rerun/course-rerun-sidebar/index.jsx +++ b/src/course-rerun/course-rerun-sidebar/index.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { v4 as uuid } from 'uuid'; import { Hyperlink } from '@edx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n'; import { useHelpUrls } from '../../help-urls/hooks'; import { HelpSidebar } from '../../generic/help-sidebar'; @@ -10,11 +10,23 @@ import messages from './messages'; const CourseRerunSideBar = () => { const intl = useIntl(); const { default: learnMoreUrl } = useHelpUrls(['default']); + const defaultCourseDate = new Date(Date.UTC(2030, 0, 1, 0, 0)); + const localizedCourseDate = ( + + ); const sidebarMessages = [ { title: intl.formatMessage(messages.sectionTitle1), - description: intl.formatMessage(messages.sectionDescription1), + description: `${intl.formatMessage(messages.sectionDescription1)}`, + date: localizedCourseDate, }, { title: intl.formatMessage(messages.sectionTitle2), @@ -38,13 +50,18 @@ const CourseRerunSideBar = () => { showOtherSettings={false} className="mt-3" > - {sidebarMessages.map(({ title, description, link }, index) => { + {sidebarMessages.map(({ + title, + description, + link, + date, + }, index) => { const isLastSection = index === sidebarMessages.length - 1; return (

{title}

-

{description}

+

{description} {date}

{!!link && ( { return ( <>
- +
-

{intl.formatMessage(messages.rerunTitle)}

+ +

+ {intl.formatMessage(messages.rerunTitle)} {displayName} +

+ {originalCourseData} +
-

- -

{originalCourseData}

-

{displayName}

-
-
{ window.location.href = unitErrorUrl; }; - const handleRedirectCourseHome = () => history.push(`/course/${courseId}/outline`); - + const handleUnitRedirect = () => { window.location.assign(unitErrorUrl); }; + const handleRedirectCourseHome = () => { window.location.assign(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`); }; return ( - {errors[field.name]} + {errors[field.name]} )} diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx index 933c7266c..b1b22c12b 100644 --- a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx @@ -1,11 +1,14 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { act, fireEvent, + screen, render, waitFor, } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ReactDOM from 'react-dom'; + import { initializeMockApp } from '@edx/frontend-platform'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; @@ -22,6 +25,7 @@ import { updateCreateOrRerunCourseQuery } from '../data/thunks'; import { getCreateOrRerunCourseUrl } from '../data/api'; import messages from './messages'; import { CreateOrRerunCourseForm } from '.'; +import { initialState } from './factories/mockApiResponses'; jest.mock('react-router', () => ({ ...jest.requireActual('react-router'), @@ -30,15 +34,9 @@ jest.mock('react-router', () => ({ }), })); -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn(), - useDispatch: () => mockDispatch, -})); - let axiosMock; let store; +ReactDOM.createPortal = jest.fn(node => node); const onClickCancelMock = jest.fn(); @@ -62,7 +60,13 @@ const props = { onClickCancel: onClickCancelMock, }; -describe('', async () => { +const mockStore = async () => { + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); + + await executeThunk(fetchStudioHomeData, store.dispatch); +}; + +describe('', () => { afterEach(() => jest.clearAllMocks()); beforeEach(async () => { initializeMockApp({ @@ -74,116 +78,136 @@ describe('', async () => { }, }); - store = initializeStore(); + store = initializeStore(initialState); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); - axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200); - - await executeThunk(fetchStudioHomeData, store.dispatch); - await executeThunk(updateCreateOrRerunCourseQuery, store.dispatch); - useSelector.mockReturnValue(studioHomeMock); }); - it('renders form successfully', () => { - const { getByText, getByPlaceholderText } = render( - , - ); - expect(getByText(props.title)).toBeInTheDocument(); - expect(getByText(messages.courseDisplayNameLabel.defaultMessage)).toBeInTheDocument(); - expect(getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage)).toBeInTheDocument(); + it('renders form successfully', async () => { + render(); + await mockStore(); - expect(getByText(messages.courseOrgLabel.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.courseOrgNoOptions.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(props.title)).toBeInTheDocument(); + expect(screen.getByText(messages.courseDisplayNameLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.courseNumberLabel.defaultMessage)).toBeInTheDocument(); - expect(getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.courseOrgLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.courseOrgNoOptions.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.courseRunLabel.defaultMessage)).toBeInTheDocument(); - expect(getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.courseNumberLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage)).toBeInTheDocument(); + + expect(screen.getByText(messages.courseRunLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage)).toBeInTheDocument(); }); - it('renders create course form with help text successfully', () => { - const { getByText, getByRole } = render(); - expect(getByText(messages.courseDisplayNameCreateHelpText.defaultMessage)).toBeInTheDocument(); - expect(getByText('The name of the organization sponsoring the course.', { exact: false })).toBeInTheDocument(); - expect(getByText('The unique number that identifies your course within your organization.', { exact: false })).toBeInTheDocument(); - expect(getByText('The term in which your course will run.', { exact: false })).toBeInTheDocument(); - expect(getByRole('button', { name: messages.createButton.defaultMessage })).toBeInTheDocument(); + it('renders create course form with help text successfully', async () => { + render(); + await mockStore(); + expect(screen.getByText(messages.courseDisplayNameCreateHelpText.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText('The name of the organization sponsoring the course.', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('The unique number that identifies your course within your organization.', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('The term in which your course will run.', { exact: false })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: messages.createButton.defaultMessage })).toBeInTheDocument(); }); - it('renders rerun course form with help text successfully', () => { + it('renders rerun course form with help text successfully', async () => { const initialProps = { ...props, isCreateNewCourse: false }; - const { getByText, getByRole } = render( - , - ); - expect(getByText(messages.courseDisplayNameRerunHelpText.defaultMessage)).toBeInTheDocument(); - expect(getByText('The name of the organization sponsoring the new course. (This name is often the same as the original organization name.)', { exact: false })).toBeInTheDocument(); - expect(getByText(messages.courseNumberRerunHelpText.defaultMessage)).toBeInTheDocument(); - expect(getByText('The term in which the new course will run. (This value is often different than the original course run value.)', { exact: false })).toBeInTheDocument(); - expect(getByRole('button', { name: messages.rerunCreateButton.defaultMessage })).toBeInTheDocument(); + render(); + await mockStore(); + + expect(screen.getByText(messages.courseDisplayNameRerunHelpText.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText('The name of the organization sponsoring the new course. (This name is often the same as the original organization name.)', { exact: false })).toBeInTheDocument(); + expect(screen.getByText(messages.courseNumberRerunHelpText.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText('The term in which the new course will run. (This value is often different than the original course run value.)', { exact: false })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: messages.rerunCreateButton.defaultMessage })).toBeInTheDocument(); }); it('should call handleOnClickCancel if button cancel clicked', async () => { - const { getByRole } = render(); - const cancelBtn = getByRole('button', { name: messages.cancelButton.defaultMessage }); - act(() => { + render(); + await mockStore(); + const cancelBtn = screen.getByRole('button', { name: messages.cancelButton.defaultMessage }); + await act(async () => { fireEvent.click(cancelBtn); }); + expect(onClickCancelMock).toHaveBeenCalled(); - expect(mockDispatch).toHaveBeenCalledWith( - { - payload: {}, - type: 'generic/updatePostErrors', - }, - ); }); - it('should call handleOnClickCreate if button create clicked', async () => { - const { getByPlaceholderText, getByText, getByRole } = render(); - const displayNameInput = getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage); - const orgInput = getByText(messages.courseOrgNoOptions.defaultMessage); - const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); - const runInput = getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage); - const createBtn = getByRole('button', { name: messages.createButton.defaultMessage }); + describe('handleOnClickCreate', () => { + delete window.location; + window.location = { assign: jest.fn() }; + it('should call window.location.assign with url', async () => { + render(); + await mockStore(); + const url = '/course/courseId'; + const displayNameInput = screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage); + const orgInput = screen.getByText(messages.courseOrgNoOptions.defaultMessage); + const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage); + const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage }); - act(() => { - fireEvent.change(displayNameInput, { target: { value: 'foo course name' } }); - fireEvent.click(orgInput); - fireEvent.change(numberInput, { target: { value: '777' } }); - fireEvent.change(runInput, { target: { value: '1' } }); - fireEvent.click(createBtn); + await act(async () => { + userEvent.type(displayNameInput, 'foo course name'); + fireEvent.click(orgInput); + userEvent.type(numberInput, '777'); + userEvent.type(runInput, '1'); + userEvent.click(createBtn); + }); + await axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { url }); + await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch); + + expect(window.location.assign).toHaveBeenCalledWith(`${process.env.STUDIO_BASE_URL}${url}`); }); + it('should call window.location.assign with url and destinationCourseKey', async () => { + render(); + await mockStore(); + const url = '/course/'; + const destinationCourseKey = 'courseKey'; + const displayNameInput = screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage); + const orgInput = screen.getByText(messages.courseOrgNoOptions.defaultMessage); + const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage); + const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage }); + await axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { url, destinationCourseKey }); - expect(mockDispatch).toHaveBeenCalledWith( - { - payload: {}, - type: 'generic/updatePostErrors', - }, - ); + await act(async () => { + userEvent.type(displayNameInput, 'foo course name'); + fireEvent.click(orgInput); + userEvent.type(numberInput, '777'); + userEvent.type(runInput, '1'); + userEvent.click(createBtn); + }); + await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch); + + expect(window.location.assign).toHaveBeenCalledWith(`${process.env.STUDIO_BASE_URL}${url}${destinationCourseKey}`); + }); }); - it('should be disabled create button if form not filled', () => { - const { getByRole } = render(); - const createBtn = getByRole('button', { name: messages.createButton.defaultMessage }); + it('should be disabled create button if form not filled', async () => { + render(); + await mockStore(); + const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage }); expect(createBtn).toBeDisabled(); }); - it('should be disabled rerun button if form not filled', () => { + it('should be disabled rerun button if form not filled', async () => { const initialProps = { ...props, isCreateNewCourse: false }; - const { getByRole } = render(); - const rerunBtn = getByRole('button', { name: messages.rerunCreateButton.defaultMessage }); + render(); + await mockStore(); + const rerunBtn = screen.getByRole('button', { name: messages.rerunCreateButton.defaultMessage }); expect(rerunBtn).toBeDisabled(); }); - it('should be disabled create button if form has error', () => { - const { getByRole, getByPlaceholderText, getByText } = render(); - const createBtn = getByRole('button', { name: messages.createButton.defaultMessage }); - const displayNameInput = getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage); - const orgInput = getByText(messages.courseOrgNoOptions.defaultMessage); - const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); - const runInput = getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage); + it('should be disabled create button if form has error', async () => { + render(); + await mockStore(); + const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage }); + const displayNameInput = screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage); + const orgInput = screen.getByText(messages.courseOrgNoOptions.defaultMessage); + const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage); - act(() => { + await act(async () => { fireEvent.change(displayNameInput, { target: { value: 'foo course name' } }); fireEvent.click(orgInput); fireEvent.change(numberInput, { target: { value: 'number with invalid (+) symbol' } }); @@ -195,37 +219,54 @@ describe('', async () => { }); }); - it('shows typeahead dropdown with allowed to create org permissions', () => { - useSelector.mockReturnValue({ ...studioHomeMock, allowToCreateNewOrg: true }); - const { getByPlaceholderText } = render(); - expect(getByPlaceholderText(messages.courseOrgPlaceholder.defaultMessage)); - }); - - it('shows button pending state', () => { - useSelector.mockReturnValue(RequestStatus.PENDING); - const { getByRole } = render(); - expect(getByRole('button', { name: messages.creatingButton.defaultMessage })).toBeInTheDocument(); - }); - it('shows alert error if postErrors presents', () => { - useSelector.mockReturnValue({ - errMsg: 'aaa', - orgErrMsg: 'bbb', - courseErrMsg: 'ccc', + it('shows typeahead dropdown with allowed to create org permissions', async () => { + const updatedStudioData = { ...studioHomeMock, allowToCreateNewOrg: true }; + store = initializeStore({ + ...initialState, + studioHome: { + ...initialState.studioHome, + studioHomeData: updatedStudioData, + }, }); - const { getByText } = render(); - expect(getByText('aaa')).toBeInTheDocument(); + render(); + await mockStore(); + + expect(screen.getByPlaceholderText(messages.courseOrgPlaceholder.defaultMessage)); }); - it('shows error on field', () => { - const { getByPlaceholderText, getByText } = render(); - const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + it('shows button pending state', async () => { + store = initializeStore({ + ...initialState, + generic: { + ...initialState.generic, + savingStatus: RequestStatus.PENDING, + }, + }); + render(); + await mockStore(); + expect(screen.getByRole('button', { name: messages.creatingButton.defaultMessage })).toBeInTheDocument(); + }); - act(() => { + it('shows alert error if postErrors presents', async () => { + render(); + await mockStore(); + await axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { errMsg: 'aaa' }); + await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch); + + expect(screen.getByText('aaa')).toBeInTheDocument(); + }); + + it('shows error on field', async () => { + render(); + await mockStore(); + const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + + await act(async () => { fireEvent.change(numberInput, { target: { value: 'number with invalid (+) symbol' } }); }); waitFor(() => { - expect(getByText(messages.noSpaceError)).toBeInTheDocument(); + expect(screen.getByText(messages.noSpaceError)).toBeInTheDocument(); }); }); }); diff --git a/src/generic/create-or-rerun-course/constants.js b/src/generic/create-or-rerun-course/constants.js deleted file mode 100644 index 85b8bd90a..000000000 --- a/src/generic/create-or-rerun-course/constants.js +++ /dev/null @@ -1,4 +0,0 @@ -const redirectToCourseIndex = (url) => `${url}/outline`; - -// eslint-disable-next-line import/prefer-default-export -export { redirectToCourseIndex }; diff --git a/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx b/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx new file mode 100644 index 000000000..ca3e0b347 --- /dev/null +++ b/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx @@ -0,0 +1,31 @@ +import { RequestStatus } from '../../../data/constants'; +import { studioHomeMock } from '../../../studio-home/__mocks__'; + +export const courseId = 'course-v1:edX+DemoX+Demo_Course'; + +export const initialState = { + generic: { + createOrRerunCourse: { + courseData: {}, + courseRerunData: {}, + redirectUrlObj: {}, + postErrors: {}, + }, + loadingStatuses: { + organizationLoadingStatus: 'successful', courseRerunLoadingStatus: 'successful', + }, + organizations: ['krisEdx', 'krisEd', 'DeveloperInc', 'importMit', 'testX', 'edX', 'developerInb'], + savingStatus: '', + }, + studioHome: { + loadingStatuses: { + studioHomeLoadingStatus: RequestStatus.SUCCESSFUL, + courseNotificationLoadingStatus: RequestStatus.IN_PROGRESS, + }, + savingStatuses: { + courseCreatorSavingStatus: '', + deleteNotificationSavingStatus: '', + }, + studioHomeData: studioHomeMock, + }, +}; diff --git a/src/generic/create-or-rerun-course/hooks.jsx b/src/generic/create-or-rerun-course/hooks.jsx index 179e6f8d2..c328fdd44 100644 --- a/src/generic/create-or-rerun-course/hooks.jsx +++ b/src/generic/create-or-rerun-course/hooks.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { history } from '@edx/frontend-platform'; +import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useFormik } from 'formik'; import * as Yup from 'yup'; @@ -15,7 +15,6 @@ import { } from '../data/selectors'; import { updateSavingStatus, updatePostErrors } from '../data/slice'; import { fetchOrganizationsQuery } from '../data/thunks'; -import { redirectToCourseIndex } from './constants'; import messages from './messages'; const useCreateOrRerunCourse = (initialValues) => { @@ -88,9 +87,16 @@ const useCreateOrRerunCourse = (initialValues) => { useEffect(() => { if (createOrRerunCourseSavingStatus === RequestStatus.SUCCESSFUL) { dispatch(updateSavingStatus({ status: '' })); - const { url } = redirectUrlObj; + const { url, destinationCourseKey } = redirectUrlObj; + // New courses' url to the outline page is provided in the url. However, for course + // re-runs the url is /course/. The actual destination for the rer-run's outline + // is in the destionationCourseKey attribute from the api. if (url) { - history.push(redirectToCourseIndex(url)); + if (destinationCourseKey) { + window.location.assign(`${getConfig().STUDIO_BASE_URL}${url}${destinationCourseKey}`); + } else { + window.location.assign(`${getConfig().STUDIO_BASE_URL}${url}`); + } } } else if (createOrRerunCourseSavingStatus === RequestStatus.FAILED) { dispatch(updateSavingStatus({ status: '' })); diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index b78dbddb1..f67088aea 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -7,6 +7,7 @@ import { } from '@edx/paragon'; import { Add as AddIcon } from '@edx/paragon/icons/es5'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { getConfig } from '@edx/frontend-platform'; import Loading from '../generic/Loading'; import InternetConnectionAlert from '../generic/internet-connection-alert'; @@ -17,7 +18,6 @@ import HomeSidebar from './home-sidebar'; import TabsSection from './tabs-section'; import OrganizationSection from './organization-section'; import VerifyEmailLayout from './verify-email-layout'; -import ProcessingCourses from './processing-courses'; import CreateNewCourseForm from './create-new-course-form'; import messages from './messages'; import { useStudioHome } from './hooks'; @@ -40,12 +40,11 @@ const StudioHome = ({ intl }) => { userIsActive, studioShortName, studioRequestEmail, + libraryAuthoringMfeUrl, + redirectToLibraryAuthoringMfe, + splitStudioHome, } = studioHomeData; - if (isLoadingPage) { - return ; - } - function getHeaderButtons() { const headerButtons = []; @@ -69,6 +68,26 @@ const StudioHome = ({ intl }) => { ); } + let libraryHref = `${getConfig().STUDIO_BASE_URL}/home#libraries-tab`; + if (splitStudioHome) { + libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; + } + if (redirectToLibraryAuthoringMfe) { + libraryHref = `${libraryAuthoringMfeUrl}/create`; + } + headerButtons.push( + , + ); + return headerButtons; } @@ -77,7 +96,7 @@ const StudioHome = ({ intl }) => { return ( <>
- +
@@ -99,16 +118,22 @@ const StudioHome = ({ intl }) => { >
- {showNewCourseContainer && ( - setShowNewCourseContainer(false)} /> + {isLoadingPage ? ( + + ) : ( + <> + {showNewCourseContainer && ( + setShowNewCourseContainer(false)} /> + )} + {isShowOrganizationDropdown && } + setShowNewCourseContainer(true)} + isShowProcessing={isShowProcessing} + /> + )} - {isShowOrganizationDropdown && } - {isShowProcessing && } - setShowNewCourseContainer(true)} - />
diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index 865569815..74a47665a 100644 --- a/src/studio-home/StudioHome.test.jsx +++ b/src/studio-home/StudioHome.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { initializeMockApp } from '@edx/frontend-platform'; +import { getConfig, initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; import { AppProvider } from '@edx/frontend-platform/react'; @@ -105,6 +105,7 @@ describe('', async () => { it('shows the spinner before the query is complete', async () => { useSelector.mockReturnValue({ studioHomeLoadingStatus: RequestStatus.IN_PROGRESS, + userIsActive: true, }); await act(async () => { @@ -114,6 +115,42 @@ describe('', async () => { }); }); + describe('render new library button', () => { + it('href should include #libraries-tab', async () => { + useSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + }); + + const { getByTestId } = render(); + const createNewLibraryButton = getByTestId('new-library-button'); + expect(createNewLibraryButton.getAttribute('href')).toBe(`${getConfig().STUDIO_BASE_URL}/home#libraries-tab`); + }); + it('href should include home_library', async () => { + useSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + splitStudioHome: true, + }); + + const { getByTestId } = render(); + const createNewLibraryButton = getByTestId('new-library-button'); + expect(createNewLibraryButton.getAttribute('href')).toBe(`${getConfig().STUDIO_BASE_URL}/home_library`); + }); + it('href should include create', async () => { + useSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + redirectToLibraryAuthoringMfe: true, + }); + const libraryAuthoringMfeUrl = 'http://localhost:3001'; + + const { getByTestId } = render(); + const createNewLibraryButton = getByTestId('new-library-button'); + expect(createNewLibraryButton.getAttribute('href')).toBe(`${libraryAuthoringMfeUrl}/create`); + }); + }); + it('should render create new course container', async () => { useSelector.mockReturnValue({ ...studioHomeMock, diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index fa466e4cc..77824e564 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -60,7 +60,7 @@ module.exports = { }, ], librariesEnabled: true, - libraryAuthoringMfeUrl: 'http://somewhere', + libraryAuthoringMfeUrl: 'http://localhost:3001', optimizationEnabled: false, redirectToLibraryAuthoringMfe: false, requestCourseCreatorUrl: '/request_course_creator', diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx index 7591785de..53b45aff7 100644 --- a/src/studio-home/card-item/index.jsx +++ b/src/studio-home/card-item/index.jsx @@ -28,6 +28,7 @@ const CardItem = ({ return ( { - +

{description}

{actionTitle}
{[COURSE_CREATOR_STATES.denied, COURSE_CREATOR_STATES.pending].includes(state) ? (
- + {stateName} - {actionText} + {actionText}
) : ( dispatch(requestCourseCreatorQuery())} state={requestButtonCurrentState} {...requestButtonStates} diff --git a/src/studio-home/messages.js b/src/studio-home/messages.js index d39ba522c..a6a87f72f 100644 --- a/src/studio-home/messages.js +++ b/src/studio-home/messages.js @@ -9,6 +9,10 @@ const messages = defineMessages({ id: 'course-authoring.studio-home.add-new-course.btn.text', defaultMessage: 'New course', }, + addNewLibraryBtnText: { + id: 'course-authoring.studio-home.add-new-library.btn.text', + defaultMessage: 'New library', + }, emailStaffBtnText: { id: 'course-authoring.studio-home.email-staff.btn.text', defaultMessage: 'Email staff to create course', diff --git a/src/studio-home/organization-section/index.jsx b/src/studio-home/organization-section/index.jsx index c6711efe3..e077ed0c2 100644 --- a/src/studio-home/organization-section/index.jsx +++ b/src/studio-home/organization-section/index.jsx @@ -46,7 +46,7 @@ const OrganizationSection = ({ intl }) => { {intl.formatMessage(messages.organizationTitle)} - + {intl.formatMessage(messages.organizationLabel)} { {isInProgress && ( {displayName}

} subtitle={subtitle} actions={( - + - {intl.formatMessage(messages.itemInProgressActionText)} + {intl.formatMessage(messages.itemInProgressActionText)} )} /> - + {intl.formatMessage(messages.itemInProgressFooterText, { refresh: ( @@ -57,22 +58,23 @@ const CourseItem = ({ course }) => { {isFailed && ( {displayName}

} subtitle={subtitle} actions={( - - {intl.formatMessage(messages.itemIsFailedActionText)} + + {intl.formatMessage(messages.itemIsFailedActionText)} )} /> - + {intl.formatMessage(messages.itemFailedFooterText)}