From 56c86980e710ae5ddcb1a9129b9833634072dc77 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Fri, 22 Sep 2023 15:29:57 -0700 Subject: [PATCH 01/15] fix: page padding --- src/course-rerun/index.jsx | 2 +- src/studio-home/StudioHome.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/course-rerun/index.jsx b/src/course-rerun/index.jsx index 32e9988fb6..f0fff9a3bc 100644 --- a/src/course-rerun/index.jsx +++ b/src/course-rerun/index.jsx @@ -42,7 +42,7 @@ const CourseRerun = ({ courseId }) => { return ( <>
- +
diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index b78dbddb16..17900ed36e 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -77,7 +77,7 @@ const StudioHome = ({ intl }) => { return ( <>
- +
From a83fc59c0fbec414238d1281a295b852eef75ec1 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Fri, 22 Sep 2023 15:31:18 -0700 Subject: [PATCH 02/15] fix: creator and re-run status colors --- src/studio-home/card-item/index.jsx | 1 + .../collapsible-state-with-action/index.jsx | 11 ++++++----- .../processing-courses/course-item/index.jsx | 16 +++++++++------- src/studio-home/processing-courses/index.jsx | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx index 7591785dec..53b45aff77 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/processing-courses/course-item/index.jsx b/src/studio-home/processing-courses/course-item/index.jsx index 32f76239ce..f94cdb3d40 100644 --- a/src/studio-home/processing-courses/course-item/index.jsx +++ b/src/studio-home/processing-courses/course-item/index.jsx @@ -31,18 +31,19 @@ const CourseItem = ({ course }) => { {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)}
diff --git a/src/studio-home/processing-courses/index.jsx b/src/studio-home/processing-courses/index.jsx index f7d9205129..85fd1011e2 100644 --- a/src/studio-home/processing-courses/index.jsx +++ b/src/studio-home/processing-courses/index.jsx @@ -13,11 +13,11 @@ const ProcessingCourses = () => { return ( <> -

+

{intl.formatMessage(messages.processingTitle)} -

+

- + {inProcessCourseActions.map((course) => ( { const { courseCreatorStatus, @@ -27,6 +29,7 @@ const CoursesTab = ({ return ( <> + {isShowProcessing && } {coursesDataItems?.length ? ( sortAlphabeticallyArray(coursesDataItems).map( ({ @@ -84,6 +87,7 @@ CoursesTab.propTypes = { ).isRequired, showNewCourseContainer: PropTypes.bool.isRequired, onClickNewCourse: PropTypes.func.isRequired, + isShowProcessing: PropTypes.bool.isRequired, }; export default CoursesTab; diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index ea18f82af6..cd9d763bc6 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -11,7 +11,7 @@ import ArchivedTab from './archived-tab'; import CoursesTab from './courses-tab'; const TabsSection = ({ - intl, tabsData, showNewCourseContainer, onClickNewCourse, + intl, tabsData, showNewCourseContainer, onClickNewCourse, isShowProcessing, }) => { const TABS_LIST = { courses: 'courses', @@ -40,6 +40,7 @@ const TabsSection = ({ coursesDataItems={courses} showNewCourseContainer={showNewCourseContainer} onClickNewCourse={onClickNewCourse} + isShowProcessing={isShowProcessing} /> , ); @@ -123,6 +124,7 @@ TabsSection.propTypes = { }).isRequired, showNewCourseContainer: PropTypes.bool.isRequired, onClickNewCourse: PropTypes.func.isRequired, + isShowProcessing: PropTypes.bool.isRequired, }; export default injectIntl(TabsSection); From dd810dd0c3a9741c714c6dc535839fa12a39f110 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Mon, 25 Sep 2023 10:51:10 -0700 Subject: [PATCH 04/15] fix: organization dropdown alignment --- src/assets/scss/_form.scss | 11 ----------- src/studio-home/organization-section/index.jsx | 2 +- src/studio-home/scss/StudioHome.scss | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/assets/scss/_form.scss b/src/assets/scss/_form.scss index 326bd7a5a5..622513d14c 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/studio-home/organization-section/index.jsx b/src/studio-home/organization-section/index.jsx index c6711efe37..e077ed0c2b 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)} Date: Mon, 25 Sep 2023 12:31:06 -0700 Subject: [PATCH 05/15] feat: add new library button --- src/studio-home/StudioHome.jsx | 23 +++++++++++++++++++++++ src/studio-home/messages.js | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 8babe8921b..9f8f5a6323 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'; @@ -39,6 +40,9 @@ const StudioHome = ({ intl }) => { userIsActive, studioShortName, studioRequestEmail, + libraryAuthoringMfeUrl, + redirectToLibraryAuthoringMfe, + splitStudioHome, } = studioHomeData; if (isLoadingPage) { @@ -68,6 +72,25 @@ 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; } diff --git a/src/studio-home/messages.js b/src/studio-home/messages.js index d39ba522c0..a6a87f72f2 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', From 12fd4fc2c1bf6e57f5f97458af1cbc0b5fe61adb Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Mon, 25 Sep 2023 12:36:38 -0700 Subject: [PATCH 06/15] fix: re-run start date to use localized time --- .../course-rerun-sidebar/index.jsx | 25 ++++++++++++++++--- .../course-rerun-sidebar/messages.js | 2 +- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/course-rerun/course-rerun-sidebar/index.jsx b/src/course-rerun/course-rerun-sidebar/index.jsx index db5fa6deec..32437b13a6 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 && ( Date: Mon, 25 Sep 2023 12:41:17 -0700 Subject: [PATCH 07/15] fix: create form error font size --- src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx index 01cc8f5373..d11e55f7cb 100644 --- a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx @@ -246,7 +246,7 @@ const CreateOrRerunCourseForm = ({ type="invalid" hasIcon={false} > - {errors[field.name]} + {errors[field.name]} )} From a960421d04f9bea6d29e6891f53d0f7d9585efb8 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Mon, 25 Sep 2023 13:29:57 -0700 Subject: [PATCH 08/15] fix: course rerun double title --- .../CourseRerunSidebar.test.jsx | 2 +- src/course-rerun/index.jsx | 14 +++++++------- src/course-rerun/messages.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx b/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx index ba70ee5f98..cb6ecb76b8 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} December 31, 2029 at 4:00 PM`)).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/index.jsx b/src/course-rerun/index.jsx index f0fff9a3bc..9df8626d64 100644 --- a/src/course-rerun/index.jsx +++ b/src/course-rerun/index.jsx @@ -47,19 +47,19 @@ const CourseRerun = ({ courseId }) => {
-

{intl.formatMessage(messages.rerunTitle)}

+ +

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

+ {originalCourseData} +
-

- -

{originalCourseData}

-

{displayName}

-
-
Date: Tue, 26 Sep 2023 10:25:23 -0700 Subject: [PATCH 09/15] fix: broken test --- .../course-rerun-sidebar/CourseRerunSidebar.test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx b/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx index cb6ecb76b8..d497d873f1 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} December 31, 2029 at 4:00 PM`)).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(); From 03204f657e45825602df6ed2a0c1e4f4723f8505 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Tue, 26 Sep 2023 12:55:23 -0700 Subject: [PATCH 10/15] chore: add tests --- src/studio-home/StudioHome.jsx | 29 +++++++++------- src/studio-home/StudioHome.test.jsx | 38 ++++++++++++++++++++- src/studio-home/__mocks__/studioHomeMock.js | 2 +- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 9f8f5a6323..f67088aead 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -45,10 +45,6 @@ const StudioHome = ({ intl }) => { splitStudioHome, } = studioHomeData; - if (isLoadingPage) { - return ; - } - function getHeaderButtons() { const headerButtons = []; @@ -86,6 +82,7 @@ const StudioHome = ({ intl }) => { size="sm" disabled={showNewCourseContainer} href={libraryHref} + data-testid="new-library-button" > {intl.formatMessage(messages.addNewLibraryBtnText)} , @@ -121,16 +118,22 @@ const StudioHome = ({ intl }) => { >
- {showNewCourseContainer && ( - setShowNewCourseContainer(false)} /> + {isLoadingPage ? ( + + ) : ( + <> + {showNewCourseContainer && ( + setShowNewCourseContainer(false)} /> + )} + {isShowOrganizationDropdown && } + setShowNewCourseContainer(true)} + isShowProcessing={isShowProcessing} + /> + )} - {isShowOrganizationDropdown && } - setShowNewCourseContainer(true)} - isShowProcessing={isShowProcessing} - />
diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index 8655698157..a8140ab035 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,41 @@ 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, + redirectToLibraryAuthoringMfe: true, + }); + const libraryAuthoringMfeUrl = 'http://localhost:3001'; + + const { getByTestId } = render(); + const createNewLibraryButton = getByTestId('new-library-button'); + expect(createNewLibraryButton.getAttribute('href')).toBe(`${libraryAuthoringMfeUrl}/create`); + }); + it('href should include create', 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('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 fa466e4cc4..77824e5641 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', From a2fcd7b12285b284dbcd6b255da3465362babf17 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Tue, 26 Sep 2023 13:52:56 -0700 Subject: [PATCH 11/15] fix: page redirect on course creation --- src/export-page/export-modal-error/ExportModalError.jsx | 5 ++--- src/generic/create-or-rerun-course/constants.js | 4 ---- src/generic/create-or-rerun-course/hooks.jsx | 5 ++--- 3 files changed, 4 insertions(+), 10 deletions(-) delete mode 100644 src/generic/create-or-rerun-course/constants.js diff --git a/src/export-page/export-modal-error/ExportModalError.jsx b/src/export-page/export-modal-error/ExportModalError.jsx index bff597db12..e975191e49 100644 --- a/src/export-page/export-modal-error/ExportModalError.jsx +++ b/src/export-page/export-modal-error/ExportModalError.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useDispatch, useSelector } from 'react-redux'; -import { history } from '@edx/frontend-platform'; +import { getConfig } from '@edx/frontend-platform'; import PropTypes from 'prop-types'; import ModalError from '../../generic/modal-error/ModalError'; @@ -18,8 +18,7 @@ const ExportModalError = ({ const { msg: errorMessage, unitUrl: unitErrorUrl } = useSelector(getError); const handleUnitRedirect = () => { window.location.href = unitErrorUrl; }; - const handleRedirectCourseHome = () => history.push(`/course/${courseId}/outline`); - + const handleRedirectCourseHome = () => { window.location.href = `${getConfig().STUDIO_BASE_URL}/course/${courseId}`; }; return ( `${url}/outline`; - -// eslint-disable-next-line import/prefer-default-export -export { redirectToCourseIndex }; diff --git a/src/generic/create-or-rerun-course/hooks.jsx b/src/generic/create-or-rerun-course/hooks.jsx index 179e6f8d23..54c3e7d830 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) => { @@ -90,7 +89,7 @@ const useCreateOrRerunCourse = (initialValues) => { dispatch(updateSavingStatus({ status: '' })); const { url } = redirectUrlObj; if (url) { - history.push(redirectToCourseIndex(url)); + window.location.href = `${getConfig().STUDIO_BASE_URL}${url}`; } } else if (createOrRerunCourseSavingStatus === RequestStatus.FAILED) { dispatch(updateSavingStatus({ status: '' })); From c66a50b907da5dd4b6cb56dc10df6d3245644c98 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Tue, 26 Sep 2023 14:55:07 -0700 Subject: [PATCH 12/15] chore: update tests --- src/studio-home/StudioHome.test.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index a8140ab035..74a47665ac 100644 --- a/src/studio-home/StudioHome.test.jsx +++ b/src/studio-home/StudioHome.test.jsx @@ -130,23 +130,24 @@ describe('', async () => { useSelector.mockReturnValue({ ...studioHomeMock, courseCreatorStatus: COURSE_CREATOR_STATES.granted, - redirectToLibraryAuthoringMfe: true, + splitStudioHome: true, }); - const libraryAuthoringMfeUrl = 'http://localhost:3001'; const { getByTestId } = render(); const createNewLibraryButton = getByTestId('new-library-button'); - expect(createNewLibraryButton.getAttribute('href')).toBe(`${libraryAuthoringMfeUrl}/create`); + 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(`${getConfig().STUDIO_BASE_URL}/home#libraries-tab`); + expect(createNewLibraryButton.getAttribute('href')).toBe(`${libraryAuthoringMfeUrl}/create`); }); }); From 96da31fa437c2709bed7afbf769da2c7d8c89e9f Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Thu, 28 Sep 2023 11:34:42 -0700 Subject: [PATCH 13/15] fix: page redirect for course re-run --- src/generic/create-or-rerun-course/hooks.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/generic/create-or-rerun-course/hooks.jsx b/src/generic/create-or-rerun-course/hooks.jsx index 54c3e7d830..7a29dae25c 100644 --- a/src/generic/create-or-rerun-course/hooks.jsx +++ b/src/generic/create-or-rerun-course/hooks.jsx @@ -87,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) { - window.location.href = `${getConfig().STUDIO_BASE_URL}${url}`; + if (destinationCourseKey) { + window.location.href = `${getConfig().STUDIO_BASE_URL}${url}${destinationCourseKey}`; + } else { + window.location.href = `${getConfig().STUDIO_BASE_URL}${url}`; + } } } else if (createOrRerunCourseSavingStatus === RequestStatus.FAILED) { dispatch(updateSavingStatus({ status: '' })); From bd397c8058d3e05867b8f57fa14c83a4204ed87a Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Thu, 28 Sep 2023 14:25:20 -0700 Subject: [PATCH 14/15] chore: increase code coverage --- .../export-modal-error/ExportModalError.jsx | 4 +- .../CreateOrRerunCourseForm.test.jsx | 259 ++++++++++-------- .../factories/mockApiResponses.jsx | 31 +++ src/generic/create-or-rerun-course/hooks.jsx | 4 +- src/studio-home/tabs-section/index.jsx | 2 +- 5 files changed, 187 insertions(+), 113 deletions(-) create mode 100644 src/generic/create-or-rerun-course/factories/mockApiResponses.jsx diff --git a/src/export-page/export-modal-error/ExportModalError.jsx b/src/export-page/export-modal-error/ExportModalError.jsx index e975191e49..56921a84c4 100644 --- a/src/export-page/export-modal-error/ExportModalError.jsx +++ b/src/export-page/export-modal-error/ExportModalError.jsx @@ -17,8 +17,8 @@ const ExportModalError = ({ const isErrorModalOpen = useSelector(getIsErrorModalOpen); const { msg: errorMessage, unitUrl: unitErrorUrl } = useSelector(getError); - const handleUnitRedirect = () => { window.location.href = unitErrorUrl; }; - const handleRedirectCourseHome = () => { window.location.href = `${getConfig().STUDIO_BASE_URL}/course/${courseId}`; }; + const handleUnitRedirect = () => { window.location.assign(unitErrorUrl); }; + const handleRedirectCourseHome = () => { window.location.assign(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`); }; return ( ({ ...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,15 @@ const props = { onClickCancel: onClickCancelMock, }; -describe('', async () => { +const mockStore = async () => { + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); + // axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { url: '/course' }); + + await executeThunk(fetchStudioHomeData, store.dispatch); + // await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch); +}; + +describe('', () => { afterEach(() => jest.clearAllMocks()); beforeEach(async () => { initializeMockApp({ @@ -74,116 +80,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 +221,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 typeahead dropdown with allowed to create org permissions', async () => { + const updatedStudioData = { ...studioHomeMock, allowToCreateNewOrg: true }; + store = initializeStore({ + ...initialState, + studioHome: { + ...initialState.studioHome, + studioHomeData: updatedStudioData, + }, + }); + render(); + await mockStore(); - it('shows button pending state', () => { - useSelector.mockReturnValue(RequestStatus.PENDING); - const { getByRole } = render(); - expect(getByRole('button', { name: messages.creatingButton.defaultMessage })).toBeInTheDocument(); + expect(screen.getByPlaceholderText(messages.courseOrgPlaceholder.defaultMessage)); }); - it('shows alert error if postErrors presents', () => { - useSelector.mockReturnValue({ - errMsg: 'aaa', - orgErrMsg: 'bbb', - courseErrMsg: 'ccc', + + it('shows button pending state', async () => { + store = initializeStore({ + ...initialState, + generic: { + ...initialState.generic, + savingStatus: RequestStatus.PENDING, + }, }); - const { getByText } = render(); - expect(getByText('aaa')).toBeInTheDocument(); + render(); + await mockStore(); + expect(screen.getByRole('button', { name: messages.creatingButton.defaultMessage })).toBeInTheDocument(); + }); + + 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', () => { - const { getByPlaceholderText, getByText } = render(); - const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + it('shows error on field', async () => { + render(); + await mockStore(); + const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); - act(() => { + 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/factories/mockApiResponses.jsx b/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx new file mode 100644 index 0000000000..ca3e0b3476 --- /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 7a29dae25c..c328fdd440 100644 --- a/src/generic/create-or-rerun-course/hooks.jsx +++ b/src/generic/create-or-rerun-course/hooks.jsx @@ -93,9 +93,9 @@ const useCreateOrRerunCourse = (initialValues) => { // is in the destionationCourseKey attribute from the api. if (url) { if (destinationCourseKey) { - window.location.href = `${getConfig().STUDIO_BASE_URL}${url}${destinationCourseKey}`; + window.location.assign(`${getConfig().STUDIO_BASE_URL}${url}${destinationCourseKey}`); } else { - window.location.href = `${getConfig().STUDIO_BASE_URL}${url}`; + window.location.assign(`${getConfig().STUDIO_BASE_URL}${url}`); } } } else if (createOrRerunCourseSavingStatus === RequestStatus.FAILED) { diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index cd9d763bc6..15279afa53 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -74,7 +74,7 @@ const TabsSection = ({ const handleSelectTab = (tab) => { if (tab === TABS_LIST.libraries && redirectToLibraryAuthoringMfe) { - window.location.href = libraryAuthoringMfeUrl; + window.location.assign(libraryAuthoringMfeUrl); } }; From a9c239c3f56e7a3ffaa8b7c2bb4040d1412f1d30 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Thu, 28 Sep 2023 14:26:43 -0700 Subject: [PATCH 15/15] fix: remove commented out code --- .../create-or-rerun-course/CreateOrRerunCourseForm.test.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx index f353cb91a8..b1b22c12b6 100644 --- a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx @@ -62,10 +62,8 @@ const props = { const mockStore = async () => { axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); - // axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { url: '/course' }); await executeThunk(fetchStudioHomeData, store.dispatch); - // await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch); }; describe('', () => {