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(
+
+ {intl.formatMessage(messages.addNewLibraryBtnText)}
+ ,
+ );
+
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)}
dispatch(handleDeleteNotificationQuery(dismissLink))}
iconBefore={CloseIcon}
- variant="outline-danger"
+ variant="tertiary"
size="sm"
>
{intl.formatMessage(messages.itemFailedFooterButton)}
diff --git a/src/studio-home/processing-courses/index.jsx b/src/studio-home/processing-courses/index.jsx
index b41786fd0..85fd1011e 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 ea18f82af..15279afa5 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}
/>
,
);
@@ -73,7 +74,7 @@ const TabsSection = ({
const handleSelectTab = (tab) => {
if (tab === TABS_LIST.libraries && redirectToLibraryAuthoringMfe) {
- window.location.href = libraryAuthoringMfeUrl;
+ window.location.assign(libraryAuthoringMfeUrl);
}
};
@@ -123,6 +124,7 @@ TabsSection.propTypes = {
}).isRequired,
showNewCourseContainer: PropTypes.bool.isRequired,
onClickNewCourse: PropTypes.func.isRequired,
+ isShowProcessing: PropTypes.bool.isRequired,
};
export default injectIntl(TabsSection);