feat: break up load api to tab specific (#740)
This commit is contained in:
@@ -2,10 +2,12 @@ import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Icon,
|
||||
Layout,
|
||||
MailtoLink,
|
||||
Row,
|
||||
} from '@edx/paragon';
|
||||
import { Add as AddIcon } from '@edx/paragon/icons/es5';
|
||||
import { Add as AddIcon, Error } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
@@ -21,10 +23,12 @@ import VerifyEmailLayout from './verify-email-layout';
|
||||
import CreateNewCourseForm from './create-new-course-form';
|
||||
import messages from './messages';
|
||||
import { useStudioHome } from './hooks';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
|
||||
const StudioHome = ({ intl }) => {
|
||||
const {
|
||||
isLoadingPage,
|
||||
isFailedLoadingPage,
|
||||
studioHomeData,
|
||||
isShowProcessing,
|
||||
anyQueryIsFailed,
|
||||
@@ -34,6 +38,7 @@ const StudioHome = ({ intl }) => {
|
||||
isShowOrganizationDropdown,
|
||||
hasAbilityToCreateNewCourse,
|
||||
setShowNewCourseContainer,
|
||||
dispatch,
|
||||
} = useStudioHome();
|
||||
|
||||
const {
|
||||
@@ -47,6 +52,10 @@ const StudioHome = ({ intl }) => {
|
||||
function getHeaderButtons() {
|
||||
const headerButtons = [];
|
||||
|
||||
if (isFailedLoadingPage || !userIsActive) {
|
||||
return headerButtons;
|
||||
}
|
||||
|
||||
if (isShowEmailStaff) {
|
||||
headerButtons.push(
|
||||
<MailtoLink to={studioRequestEmail}>{intl.formatMessage(messages.emailStaffBtnText)}</MailtoLink>,
|
||||
@@ -93,6 +102,53 @@ const StudioHome = ({ intl }) => {
|
||||
return (<Loading />);
|
||||
}
|
||||
|
||||
const getMainBody = () => {
|
||||
if (isFailedLoadingPage) {
|
||||
return (
|
||||
<AlertMessage
|
||||
variant="danger"
|
||||
description={(
|
||||
<Row className="m-0 align-items-center">
|
||||
<Icon src={Error} className="text-danger-500 mr-1" />
|
||||
<span>{intl.formatMessage(messages.homePageLoadFailedMessage)}</span>
|
||||
</Row>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!userIsActive) {
|
||||
return <VerifyEmailLayout />;
|
||||
}
|
||||
return (
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
sm={[{ span: 9 }, { span: 3 }]}
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
<section>
|
||||
{showNewCourseContainer && (
|
||||
<CreateNewCourseForm handleOnClickCancel={() => setShowNewCourseContainer(false)} />
|
||||
)}
|
||||
{isShowOrganizationDropdown && <OrganizationSection />}
|
||||
<TabsSection
|
||||
tabsData={studioHomeData}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={() => setShowNewCourseContainer(true)}
|
||||
isShowProcessing={isShowProcessing}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</section>
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<HomeSidebar />
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header isHiddenMainMenu />
|
||||
@@ -101,40 +157,12 @@ const StudioHome = ({ intl }) => {
|
||||
<article className="studio-home-sub-header">
|
||||
<section>
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle, { studioShortName })}
|
||||
title={intl.formatMessage(messages.headingTitle, { studioShortName: studioShortName || 'Studio' })}
|
||||
headerActions={headerButtons}
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
{!userIsActive ? (
|
||||
<VerifyEmailLayout />
|
||||
) : (
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
sm={[{ span: 9 }, { span: 3 }]}
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
<section>
|
||||
{showNewCourseContainer && (
|
||||
<CreateNewCourseForm handleOnClickCancel={() => setShowNewCourseContainer(false)} />
|
||||
)}
|
||||
{isShowOrganizationDropdown && <OrganizationSection />}
|
||||
<TabsSection
|
||||
tabsData={studioHomeData}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={() => setShowNewCourseContainer(true)}
|
||||
isShowProcessing={isShowProcessing}
|
||||
/>
|
||||
</section>
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<HomeSidebar />
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
)}
|
||||
{getMainBody()}
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
|
||||
@@ -50,174 +50,204 @@ const RootWrapper = () => (
|
||||
);
|
||||
|
||||
describe('<StudioHome />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
|
||||
it('should render page and page title correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(`${studioShortName} home`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render email staff header button', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.disallowedForThisSite,
|
||||
describe('api fetch fails', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(404);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
useSelector.mockReturnValue({ studioHomeLoadingStatus: RequestStatus.FAILED });
|
||||
});
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
expect(getByRole('link', { name: messages.emailStaffBtnText.defaultMessage }))
|
||||
.toHaveAttribute('href', `mailto:${studioRequestEmail}`);
|
||||
});
|
||||
|
||||
it('should render create new course button', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
it('should render fetch error', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.homePageLoadFailedMessage.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
expect(getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage })).toBeInTheDocument();
|
||||
it('should render Studio home title', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Studio home')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show verify email layout if user inactive', () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
userIsActive: false,
|
||||
describe('api fetch succeeds', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Thanks for signing up, abc123!', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
userIsActive: true,
|
||||
it('should render page and page title correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(`${studioShortName} home`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
it('should render email staff header button', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.disallowedForThisSite,
|
||||
});
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
expect(getByRole('link', { name: messages.emailStaffBtnText.defaultMessage }))
|
||||
.toHaveAttribute('href', `mailto:${studioRequestEmail}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('render new library button', () => {
|
||||
it('href should include home_library', async () => {
|
||||
it('should render create new course button', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
const studioBaseUrl = 'http://localhost:18010';
|
||||
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
const createNewLibraryButton = getByTestId('new-library-button');
|
||||
expect(createNewLibraryButton.getAttribute('href')).toBe(`${studioBaseUrl}/home_library`);
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
expect(getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
it('href should include create', async () => {
|
||||
|
||||
it('should show verify email layout if user inactive', () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
userIsActive: false,
|
||||
});
|
||||
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Thanks for signing up, abc123!', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
userIsActive: true,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('render new library button', () => {
|
||||
it('href should include home_library', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
const studioBaseUrl = 'http://localhost:18010';
|
||||
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
const createNewLibraryButton = getByTestId('new-library-button');
|
||||
expect(createNewLibraryButton.getAttribute('href')).toBe(`${studioBaseUrl}/home_library`);
|
||||
});
|
||||
it('href should include create', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
splitStudioHome: true,
|
||||
redirectToLibraryAuthoringMfe: true,
|
||||
});
|
||||
const libraryAuthoringMfeUrl = 'http://localhost:3001';
|
||||
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
const createNewLibraryButton = getByTestId('new-library-button');
|
||||
expect(createNewLibraryButton.getAttribute('href')).toBe(`${libraryAuthoringMfeUrl}/create`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render create new course container', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
splitStudioHome: true,
|
||||
redirectToLibraryAuthoringMfe: true,
|
||||
});
|
||||
const libraryAuthoringMfeUrl = 'http://localhost:3001';
|
||||
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
const createNewLibraryButton = getByTestId('new-library-button');
|
||||
expect(createNewLibraryButton.getAttribute('href')).toBe(`${libraryAuthoringMfeUrl}/create`);
|
||||
});
|
||||
});
|
||||
const { getByRole, getByText } = render(<RootWrapper />);
|
||||
const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage });
|
||||
|
||||
it('should render create new course container', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
|
||||
const { getByRole, getByText } = render(<RootWrapper />);
|
||||
const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage });
|
||||
|
||||
await act(() => fireEvent.click(createNewCourseButton));
|
||||
expect(getByText(createNewCourseMessages.createNewCourse.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide create new course container', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
|
||||
const { getByRole, queryByText, getByText } = render(<RootWrapper />);
|
||||
const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage });
|
||||
|
||||
fireEvent.click(createNewCourseButton);
|
||||
waitFor(() => {
|
||||
await act(() => fireEvent.click(createNewCourseButton));
|
||||
expect(getByText(createNewCourseMessages.createNewCourse.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cancelButton = getByRole('button', { name: createOrRerunCourseMessages.cancelButton.defaultMessage });
|
||||
fireEvent.click(cancelButton);
|
||||
waitFor(() => {
|
||||
expect(queryByText(createNewCourseMessages.createNewCourse.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('contact administrator card', () => {
|
||||
it('should show contact administrator card with no add course buttons', () => {
|
||||
it('should hide create new course container', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courses: null,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.pending,
|
||||
});
|
||||
const { getByText, queryByText } = render(<RootWrapper />);
|
||||
const defaultTitleMessage = messages.defaultSection_1_Title.defaultMessage;
|
||||
const titleWithStudioName = defaultTitleMessage.replace('{studioShortName}', 'Studio');
|
||||
const administratorCardTitle = getByText(titleWithStudioName);
|
||||
|
||||
expect(administratorCardTitle).toBeVisible();
|
||||
|
||||
const addCourseButton = queryByText(messages.btnAddNewCourseText.defaultMessage);
|
||||
expect(addCourseButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should show contact administrator card with add course buttons', () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courses: null,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
const { getByText, getByTestId } = render(<RootWrapper />);
|
||||
const defaultTitleMessage = messages.defaultSection_1_Title.defaultMessage;
|
||||
const titleWithStudioName = defaultTitleMessage.replace('{studioShortName}', 'Studio');
|
||||
const administratorCardTitle = getByText(titleWithStudioName);
|
||||
|
||||
expect(administratorCardTitle).toBeVisible();
|
||||
const { getByRole, queryByText, getByText } = render(<RootWrapper />);
|
||||
const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage });
|
||||
|
||||
const addCourseButton = getByTestId('contact-admin-create-course');
|
||||
expect(addCourseButton).toBeVisible();
|
||||
fireEvent.click(createNewCourseButton);
|
||||
waitFor(() => {
|
||||
expect(getByText(createNewCourseMessages.createNewCourse.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(addCourseButton);
|
||||
expect(getByTestId('create-course-form')).toBeVisible();
|
||||
const cancelButton = getByRole('button', { name: createOrRerunCourseMessages.cancelButton.defaultMessage });
|
||||
fireEvent.click(cancelButton);
|
||||
waitFor(() => {
|
||||
expect(queryByText(createNewCourseMessages.createNewCourse.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('contact administrator card', () => {
|
||||
it('should show contact administrator card with no add course buttons', () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courses: null,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.pending,
|
||||
});
|
||||
const { getByText, queryByText } = render(<RootWrapper />);
|
||||
const defaultTitleMessage = messages.defaultSection_1_Title.defaultMessage;
|
||||
const titleWithStudioName = defaultTitleMessage.replace('{studioShortName}', 'Studio');
|
||||
const administratorCardTitle = getByText(titleWithStudioName);
|
||||
|
||||
expect(administratorCardTitle).toBeVisible();
|
||||
|
||||
const addCourseButton = queryByText(messages.btnAddNewCourseText.defaultMessage);
|
||||
expect(addCourseButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should show contact administrator card with add course buttons', () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courses: null,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
const { getByText, getByTestId } = render(<RootWrapper />);
|
||||
const defaultTitleMessage = messages.defaultSection_1_Title.defaultMessage;
|
||||
const titleWithStudioName = defaultTitleMessage.replace('{studioShortName}', 'Studio');
|
||||
const administratorCardTitle = getByText(titleWithStudioName);
|
||||
|
||||
expect(administratorCardTitle).toBeVisible();
|
||||
|
||||
const addCourseButton = getByTestId('contact-admin-create-course');
|
||||
expect(addCourseButton).toBeVisible();
|
||||
|
||||
fireEvent.click(addCourseButton);
|
||||
expect(getByTestId('create-course-form')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show footer', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
|
||||
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show footer', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
|
||||
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getStudioHomeApiUrl = (search) => new URL(`api/contentstore/v1/home${search}`, getApiBaseUrl()).href;
|
||||
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getStudioHomeApiUrl = () => new URL('api/contentstore/v1/home', getApiBaseUrl()).href;
|
||||
export const getRequestCourseCreatorUrl = () => new URL('request_course_creator', getApiBaseUrl()).href;
|
||||
export const getCourseNotificationUrl = (url) => new URL(url, getApiBaseUrl()).href;
|
||||
|
||||
@@ -11,8 +11,18 @@ export const getCourseNotificationUrl = (url) => new URL(url, getApiBaseUrl()).h
|
||||
* @param {string} search
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getStudioHomeData(search) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getStudioHomeApiUrl(search));
|
||||
export async function getStudioHomeData() {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getStudioHomeApiUrl());
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
export async function getStudioHomeCourses(search) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
export async function getStudioHomeLibraries() {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/libraries`);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import {
|
||||
getStudioHomeApiUrl,
|
||||
getRequestCourseCreatorUrl,
|
||||
@@ -10,7 +9,11 @@ import {
|
||||
getStudioHomeData,
|
||||
handleCourseNotification,
|
||||
sendRequestForCourseCreator,
|
||||
getApiBaseUrl,
|
||||
getStudioHomeCourses,
|
||||
getStudioHomeLibraries,
|
||||
} from './api';
|
||||
import { generateGetStudioCoursesApiResponse, generateGetStudioHomeDataApiResponse, generateGetStuioHomeLibrariesApiResponse } from '../factories/mockApiResponses';
|
||||
|
||||
let axiosMock;
|
||||
|
||||
@@ -32,11 +35,32 @@ describe('studio-home api calls', () => {
|
||||
});
|
||||
|
||||
it('should get studio home data', async () => {
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
const result = await getStudioHomeData();
|
||||
const expected = generateGetStudioHomeDataApiResponse();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(getStudioHomeApiUrl());
|
||||
expect(result).toEqual(studioHomeMock);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
fit('should get studio courses data', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
const result = await getStudioHomeCourses('');
|
||||
const expected = generateGetStudioCoursesApiResponse();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(apiLink);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should get studio libraries data', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStuioHomeLibrariesApiResponse());
|
||||
const result = await getStudioHomeLibraries();
|
||||
const expected = generateGetStuioHomeLibrariesApiResponse();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(apiLink);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle course notification request', async () => {
|
||||
|
||||
@@ -9,6 +9,8 @@ const slice = createSlice({
|
||||
loadingStatuses: {
|
||||
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseNotificationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
libraryLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
savingStatuses: {
|
||||
courseCreatorSavingStatus: '',
|
||||
@@ -26,6 +28,16 @@ const slice = createSlice({
|
||||
fetchStudioHomeDataSuccess: (state, { payload }) => {
|
||||
Object.assign(state.studioHomeData, payload);
|
||||
},
|
||||
fetchCourseDataSuccess: (state, { payload }) => {
|
||||
const { courses, archivedCourses, inProcessCourseActions } = payload;
|
||||
state.studioHomeData.courses = courses;
|
||||
state.studioHomeData.archivedCourses = archivedCourses;
|
||||
state.studioHomeData.inProcessCourseActions = inProcessCourseActions;
|
||||
},
|
||||
fetchLibraryDataSuccess: (state, { payload }) => {
|
||||
const { libraries } = payload;
|
||||
state.studioHomeData.libraries = libraries;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,6 +45,8 @@ export const {
|
||||
updateSavingStatuses,
|
||||
updateLoadingStatuses,
|
||||
fetchStudioHomeDataSuccess,
|
||||
fetchCourseDataSuccess,
|
||||
fetchLibraryDataSuccess,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
@@ -1,21 +1,54 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { getStudioHomeData, sendRequestForCourseCreator, handleCourseNotification } from './api';
|
||||
import {
|
||||
getStudioHomeData,
|
||||
sendRequestForCourseCreator,
|
||||
handleCourseNotification,
|
||||
getStudioHomeCourses,
|
||||
getStudioHomeLibraries,
|
||||
} from './api';
|
||||
import {
|
||||
fetchStudioHomeDataSuccess,
|
||||
fetchCourseDataSuccess,
|
||||
updateLoadingStatuses,
|
||||
updateSavingStatuses,
|
||||
fetchLibraryDataSuccess,
|
||||
} from './slice';
|
||||
|
||||
function fetchStudioHomeData(search) {
|
||||
function fetchStudioHomeData(search, hasHomeData) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
if (!hasHomeData) {
|
||||
try {
|
||||
const studioHomeData = await getStudioHomeData();
|
||||
dispatch(fetchStudioHomeDataSuccess(studioHomeData));
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.FAILED }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const coursesData = await getStudioHomeCourses(search || '');
|
||||
dispatch(fetchCourseDataSuccess(coursesData));
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fetchLibraryData() {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatuses({ libraryLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const studioHomeData = await getStudioHomeData(search || '');
|
||||
dispatch(fetchStudioHomeDataSuccess(studioHomeData));
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
const libraryData = await getStudioHomeLibraries();
|
||||
dispatch(fetchLibraryDataSuccess(libraryData));
|
||||
dispatch(updateLoadingStatuses({ libraryLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.FAILED }));
|
||||
dispatch(updateLoadingStatuses({ libraryLoadingStatus: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -50,6 +83,7 @@ function requestCourseCreatorQuery() {
|
||||
|
||||
export {
|
||||
fetchStudioHomeData,
|
||||
fetchLibraryData,
|
||||
requestCourseCreatorQuery,
|
||||
handleDeleteNotificationQuery,
|
||||
};
|
||||
|
||||
114
src/studio-home/factories/mockApiResponses.jsx
Normal file
114
src/studio-home/factories/mockApiResponses.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
export const courseId = 'course';
|
||||
|
||||
export const initialState = {
|
||||
studioHome: {
|
||||
loadingStatuses: {
|
||||
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseNotificationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
libraryLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
savingStatuses: {
|
||||
courseCreatorSavingStatus: '',
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const generateGetStudioHomeDataApiResponse = () => ({
|
||||
activeTab: 'courses',
|
||||
allowCourseReruns: true,
|
||||
allowedOrganizations: ['edx', 'org'],
|
||||
archivedCourses: [],
|
||||
canCreateOrganizations: true,
|
||||
courseCreatorStatus: 'granted',
|
||||
courses: [],
|
||||
inProcessCourseActions: [],
|
||||
libraries: [],
|
||||
librariesEnabled: true,
|
||||
libraryAuthoringMfeUrl: 'http://localhost:3001',
|
||||
optimizationEnabled: false,
|
||||
redirectToLibraryAuthoringMfe: false,
|
||||
requestCourseCreatorUrl: '/request_course_creator',
|
||||
rerunCreatorStatus: true,
|
||||
showNewLibraryButton: true,
|
||||
splitStudioHome: false,
|
||||
studioName: 'Studio',
|
||||
studioShortName: 'Studio',
|
||||
studioRequestEmail: 'request@email.com',
|
||||
techSupportEmail: 'technical@example.com',
|
||||
platformName: 'Your Platform Name Here',
|
||||
userIsActive: true,
|
||||
allowToCreateNewOrg: false,
|
||||
});
|
||||
|
||||
export const generateGetStudioCoursesApiResponse = () => ({
|
||||
archivedCourses: [
|
||||
{
|
||||
courseKey: 'course-v1:MachineLearning+123+2023',
|
||||
displayName: 'Machine Learning',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:MachineLearning+123+2023/jump_to/block-v1:MachineLearning+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'LSE',
|
||||
rerunLink: '/course_rerun/course-v1:MachineLearning+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:MachineLearning+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'course-v1:Design+123+e.g.2025',
|
||||
displayName: 'Design',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:Design+123+e.g.2025/jump_to/block-v1:Design+123+e.g.2025+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'University of Cape Town',
|
||||
rerunLink: '/course_rerun/course-v1:Design+123+e.g.2025',
|
||||
run: 'e.g.2025',
|
||||
url: '/course/course-v1:Design+123+e.g.2025',
|
||||
},
|
||||
],
|
||||
courses: [
|
||||
{
|
||||
courseKey: 'course-v1:HarvardX+123+2023',
|
||||
displayName: 'Managing Risk in the Information Age',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:HarvardX+123+2023/jump_to/block-v1:HarvardX+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'HarvardX',
|
||||
rerunLink: '/course_rerun/course-v1:HarvardX+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:HarvardX+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'org.0/course_0/Run_0',
|
||||
displayName: 'Run 0',
|
||||
lmsLink: null,
|
||||
number: 'course_0',
|
||||
org: 'org.0',
|
||||
rerunLink: null,
|
||||
run: 'Run_0',
|
||||
url: null,
|
||||
},
|
||||
],
|
||||
inProcessCourseActions: [],
|
||||
});
|
||||
|
||||
export const generateGetStuioHomeLibrariesApiResponse = () => ({
|
||||
libraries: [
|
||||
{
|
||||
displayName: 'MBA',
|
||||
libraryKey: 'library-v1:MBA+123',
|
||||
url: '/library/library-v1:MDA+123',
|
||||
org: 'Cambridge',
|
||||
number: '123',
|
||||
canEdit: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const generateNewVideoApiResponse = () => ({
|
||||
files: [{
|
||||
edx_video_id: 'mOckID4',
|
||||
upload_url: 'http://testing.org',
|
||||
}],
|
||||
});
|
||||
@@ -26,6 +26,7 @@ const useStudioHome = () => {
|
||||
} = useSelector(getSavingStatuses);
|
||||
const [showNewCourseContainer, setShowNewCourseContainer] = useState(false);
|
||||
const isLoadingPage = studioHomeLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isFailedLoadingPage = studioHomeLoadingStatus === RequestStatus.FAILED;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStudioHomeData(location.search ?? ''));
|
||||
@@ -59,7 +60,7 @@ const useStudioHome = () => {
|
||||
|
||||
const isShowOrganizationDropdown = optimizationEnabled && courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const isShowEmailStaff = courseCreatorStatus === COURSE_CREATOR_STATES.disallowedForThisSite && !!studioRequestEmail;
|
||||
const isShowProcessing = allowCourseReruns && rerunCreatorStatus && inProcessCourseActions.length > 0;
|
||||
const isShowProcessing = allowCourseReruns && rerunCreatorStatus && inProcessCourseActions?.length > 0;
|
||||
const hasAbilityToCreateNewCourse = courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const anyQueryIsPending = [deleteNotificationSavingStatus, courseCreatorSavingStatus, savingCreateRerunStatus]
|
||||
.includes(RequestStatus.PENDING);
|
||||
@@ -68,6 +69,7 @@ const useStudioHome = () => {
|
||||
|
||||
return {
|
||||
isLoadingPage,
|
||||
isFailedLoadingPage,
|
||||
newCourseData,
|
||||
studioHomeData,
|
||||
isShowProcessing,
|
||||
@@ -78,7 +80,6 @@ const useStudioHome = () => {
|
||||
courseCreatorSavingStatus,
|
||||
isShowOrganizationDropdown,
|
||||
hasAbilityToCreateNewCourse,
|
||||
deleteNotificationSavingStatus,
|
||||
dispatch,
|
||||
setShowNewCourseContainer,
|
||||
};
|
||||
|
||||
@@ -13,22 +13,14 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.studio-home.add-new-library.btn.text',
|
||||
defaultMessage: 'New library',
|
||||
},
|
||||
homePageLoadFailedMessage: {
|
||||
id: 'course-authoring.studio-home.page-load.failed.message',
|
||||
defaultMessage: 'Failed to load Studio home. Please try again later.',
|
||||
},
|
||||
emailStaffBtnText: {
|
||||
id: 'course-authoring.studio-home.email-staff.btn.text',
|
||||
defaultMessage: 'Email staff to create course',
|
||||
},
|
||||
coursesTabTitle: {
|
||||
id: 'course-authoring.studio-home.courses.tab.title',
|
||||
defaultMessage: 'Courses',
|
||||
},
|
||||
librariesTabTitle: {
|
||||
id: 'course-authoring.studio-home.libraries.tab.title',
|
||||
defaultMessage: 'Libraries',
|
||||
},
|
||||
archivedTabTitle: {
|
||||
id: 'course-authoring.studio-home.archived.tab.title',
|
||||
defaultMessage: 'Archived courses',
|
||||
},
|
||||
defaultSection_1_Title: {
|
||||
id: 'course-authoring.studio-home.default-section-1.title',
|
||||
defaultMessage: 'Are you staff on an existing {studioShortName} course?',
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { waitFor, render, fireEvent } from '@testing-library/react';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
waitFor, render, fireEvent, screen, act,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from '../messages';
|
||||
import tabMessages from './messages';
|
||||
import TabsSection from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
import {
|
||||
initialState,
|
||||
generateGetStudioHomeDataApiResponse,
|
||||
generateGetStudioCoursesApiResponse,
|
||||
generateGetStuioHomeLibrariesApiResponse,
|
||||
} from '../factories/mockApiResponses';
|
||||
import { getApiBaseUrl, getStudioHomeApiUrl } from '../data/api';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks';
|
||||
|
||||
const { studioShortName } = studioHomeMock;
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TabsSection intl={{ formatMessage: jest.fn() }} tabsData={studioHomeMock} />
|
||||
<TabsSection intl={{ formatMessage: jest.fn() }} dispatch={jest.fn()} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
@@ -37,68 +48,173 @@ describe('<TabsSection />', () => {
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
it('should render all tabs correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.archivedTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
it('should render all tabs correctly', async () => {
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
data.archivedCourses = [{
|
||||
courseKey: 'course-v1:MachineLearning+123+2023',
|
||||
displayName: 'Machine Learning',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:MachineLearning+123+2023/jump_to/block-v1:MachineLearning+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'LSE',
|
||||
rerunLink: '/course_rerun/course-v1:MachineLearning+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:MachineLearning+123+2023',
|
||||
}];
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(tabMessages.archivedTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('should render specific course details', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(studioHomeMock.courses[0].displayName)).toBeVisible();
|
||||
expect(getByText(
|
||||
`${studioHomeMock.courses[0].org} / ${studioHomeMock.courses[0].number} / ${studioHomeMock.courses[0].run}`,
|
||||
)).toBeVisible();
|
||||
|
||||
describe('course tab', () => {
|
||||
it('should render specific course details', async () => {
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(studioHomeMock.courses[0].displayName)).toBeVisible();
|
||||
|
||||
expect(screen.getByText(
|
||||
`${studioHomeMock.courses[0].org} / ${studioHomeMock.courses[0].number} / ${studioHomeMock.courses[0].run}`,
|
||||
)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render default sections when courses are empty', async () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.courses = [];
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(`Are you staff on an existing ${studioShortName} course?`)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(messages.defaultSection_1_Description.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('button', { name: messages.defaultSection_2_Title.defaultMessage })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(messages.defaultSection_2_Description.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render course fetch failure alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(404);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(tabMessages.courseTabErrorMessage.defaultMessage)).toBeVisible();
|
||||
});
|
||||
});
|
||||
it('should switch to Libraries tab and render specific library details', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const librariesTab = getByText(messages.librariesTabTitle.defaultMessage);
|
||||
fireEvent.click(librariesTab);
|
||||
expect(getByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
|
||||
expect(getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible();
|
||||
|
||||
describe('archived tab', () => {
|
||||
it('should switch to Archived tab and render specific archived course details', async () => {
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const archivedTab = screen.getByText(tabMessages.archivedTabTitle.defaultMessage);
|
||||
fireEvent.click(archivedTab);
|
||||
|
||||
expect(screen.getByText(studioHomeMock.archivedCourses[0].displayName)).toBeVisible();
|
||||
|
||||
expect(screen.getByText(
|
||||
`${studioHomeMock.archivedCourses[0].org} / ${studioHomeMock.archivedCourses[0].number} / ${studioHomeMock.archivedCourses[0].run}`,
|
||||
)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should hide Archived tab when archived courses are empty', async () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.archivedCourses = [];
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText(tabMessages.archivedTabTitle.defaultMessage)).toBeNull();
|
||||
});
|
||||
});
|
||||
it('should switch to Archived tab and render specific archived course details', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const archivedTab = getByText(messages.archivedTabTitle.defaultMessage);
|
||||
fireEvent.click(archivedTab);
|
||||
expect(getByText(studioHomeMock.archivedCourses[0].displayName)).toBeVisible();
|
||||
expect(getByText(
|
||||
`${studioHomeMock.archivedCourses[0].org} / ${studioHomeMock.archivedCourses[0].number} / ${studioHomeMock.archivedCourses[0].run}`,
|
||||
)).toBeVisible();
|
||||
});
|
||||
it('should hide Libraries tab when libraries are disabled', () => {
|
||||
studioHomeMock.librariesEnabled = false;
|
||||
const { queryByText, getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.librariesTabTitle.defaultMessage)).toBeNull();
|
||||
expect(getByText(messages.archivedTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('should hide Archived tab when archived courses are empty', () => {
|
||||
studioHomeMock.librariesEnabled = true;
|
||||
studioHomeMock.archivedCourses = [];
|
||||
const { queryByText, getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.archivedTabTitle.defaultMessage)).toBeNull();
|
||||
});
|
||||
it('should render default sections when courses are empty', () => {
|
||||
studioHomeMock.courses = [];
|
||||
const { getByText, getByRole } = render(<RootWrapper />);
|
||||
expect(getByText(`Are you staff on an existing ${studioShortName} course?`)).toBeInTheDocument();
|
||||
expect(getByText(messages.defaultSection_1_Description.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.defaultSection_2_Title.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByText(messages.defaultSection_2_Description.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('should redirect to library authoring mfe', () => {
|
||||
studioHomeMock.redirectToLibraryAuthoringMfe = true;
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const librariesTab = getByText(messages.librariesTabTitle.defaultMessage);
|
||||
fireEvent.click(librariesTab);
|
||||
waitFor(() => {
|
||||
expect(window.location.href).toBe(studioHomeMock.libraryAuthoringMfeUrl);
|
||||
|
||||
describe('library tab', () => {
|
||||
it('should switch to Libraries tab and render specific library details', async () => {
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(libraryApiLink).reply(200, generateGetStuioHomeLibrariesApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
await executeThunk(fetchLibraryData(), store.dispatch);
|
||||
|
||||
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
expect(screen.getByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
|
||||
|
||||
expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should hide Libraries tab when libraries are disabled', async () => {
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
data.librariesEnabled = false;
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText(tabMessages.librariesTabTitle.defaultMessage)).toBeNull();
|
||||
});
|
||||
|
||||
it('should redirect to library authoring mfe', async () => {
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
data.redirectToLibraryAuthoringMfe = true;
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
|
||||
fireEvent.click(librariesTab);
|
||||
|
||||
waitFor(() => {
|
||||
expect(window.location.href).toBe(data.libraryAuthoringMfeUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render libraries fetch failure alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(libraryApiLink).reply(404);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
await executeThunk(fetchLibraryData(), store.dispatch);
|
||||
|
||||
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
expect(screen.getByText(tabMessages.librariesTabErrorMessage.defaultMessage)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Row } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
|
||||
import { LoadingSpinner } from '../../../generic/Loading';
|
||||
import CardItem from '../../card-item';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
import AlertMessage from '../../../generic/alert-message';
|
||||
import messages from '../messages';
|
||||
|
||||
const ArchivedTab = ({ archivedCoursesData }) => (
|
||||
<div className="courses-tab">
|
||||
{sortAlphabeticallyArray(archivedCoursesData).map(({
|
||||
courseKey, displayName, lmsLink, org, rerunLink, number, run, url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
const ArchivedTab = ({
|
||||
archivedCoursesData,
|
||||
isLoading,
|
||||
isFailed,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
<LoadingSpinner />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return (
|
||||
isFailed ? (
|
||||
<AlertMessage
|
||||
variant="danger"
|
||||
description={(
|
||||
<Row className="m-0 align-items-center">
|
||||
<Icon src={Error} className="text-danger-500 mr-1" />
|
||||
<span>{intl.formatMessage(messages.archiveTabErrorMessage)}</span>
|
||||
</Row>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
) : (
|
||||
<div className="courses-tab">
|
||||
{sortAlphabeticallyArray(archivedCoursesData).map(({
|
||||
courseKey, displayName, lmsLink, org, rerunLink, number, run, url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ArchivedTab.propTypes = {
|
||||
archivedCoursesData: PropTypes.arrayOf(
|
||||
@@ -36,6 +69,10 @@ ArchivedTab.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default ArchivedTab;
|
||||
export default injectIntl(ArchivedTab);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Row } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../../constants';
|
||||
import { getStudioHomeData } from '../../data/selectors';
|
||||
@@ -9,13 +12,19 @@ import CollapsibleStateWithAction from '../../collapsible-state-with-action';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
import ContactAdministrator from './contact-administrator';
|
||||
import ProcessingCourses from '../../processing-courses';
|
||||
import { LoadingSpinner } from '../../../generic/Loading';
|
||||
import AlertMessage from '../../../generic/alert-message';
|
||||
import messages from '../messages';
|
||||
|
||||
const CoursesTab = ({
|
||||
coursesDataItems,
|
||||
showNewCourseContainer,
|
||||
onClickNewCourse,
|
||||
isShowProcessing,
|
||||
isLoading,
|
||||
isFailed,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseCreatorStatus,
|
||||
optimizationEnabled,
|
||||
@@ -27,48 +36,68 @@ const CoursesTab = ({
|
||||
COURSE_CREATOR_STATES.unrequested,
|
||||
].includes(courseCreatorStatus);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
<LoadingSpinner />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isShowProcessing && <ProcessingCourses />}
|
||||
{coursesDataItems?.length ? (
|
||||
sortAlphabeticallyArray(coursesDataItems).map(
|
||||
({
|
||||
courseKey,
|
||||
displayName,
|
||||
lmsLink,
|
||||
org,
|
||||
rerunLink,
|
||||
number,
|
||||
run,
|
||||
url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
/>
|
||||
),
|
||||
isFailed ? (
|
||||
<AlertMessage
|
||||
variant="danger"
|
||||
description={(
|
||||
<Row className="m-0 align-items-center">
|
||||
<Icon src={Error} className="text-danger-500 mr-1" />
|
||||
<span>{intl.formatMessage(messages.courseTabErrorMessage)}</span>
|
||||
</Row>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isShowProcessing && <ProcessingCourses />}
|
||||
{coursesDataItems?.length ? (
|
||||
sortAlphabeticallyArray(coursesDataItems).map(
|
||||
({
|
||||
courseKey,
|
||||
displayName,
|
||||
lmsLink,
|
||||
org,
|
||||
rerunLink,
|
||||
number,
|
||||
run,
|
||||
url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (!optimizationEnabled && (
|
||||
<ContactAdministrator
|
||||
hasAbilityToCreateCourse={hasAbilityToCreateCourse}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
/>
|
||||
)
|
||||
) : (!optimizationEnabled && (
|
||||
<ContactAdministrator
|
||||
hasAbilityToCreateCourse={hasAbilityToCreateCourse}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{showCollapsible && (
|
||||
<CollapsibleStateWithAction
|
||||
state={courseCreatorStatus}
|
||||
className="mt-3"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showCollapsible && (
|
||||
<CollapsibleStateWithAction
|
||||
state={courseCreatorStatus}
|
||||
className="mt-3"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,6 +117,8 @@ CoursesTab.propTypes = {
|
||||
showNewCourseContainer: PropTypes.bool.isRequired,
|
||||
onClickNewCourse: PropTypes.func.isRequired,
|
||||
isShowProcessing: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default CoursesTab;
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tab, Tabs } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { getStudioHomeData } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { getLoadingStatuses, getStudioHomeData } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
import LibrariesTab from './libraries-tab';
|
||||
import ArchivedTab from './archived-tab';
|
||||
import CoursesTab from './courses-tab';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { fetchLibraryData } from '../data/thunks';
|
||||
|
||||
const TabsSection = ({
|
||||
intl, tabsData, showNewCourseContainer, onClickNewCourse, isShowProcessing,
|
||||
intl, showNewCourseContainer, onClickNewCourse, isShowProcessing, dispatch,
|
||||
}) => {
|
||||
const TABS_LIST = {
|
||||
courses: 'courses',
|
||||
libraries: 'libraries',
|
||||
archived: 'archived',
|
||||
};
|
||||
const [tabKey, setTabKey] = useState(TABS_LIST.courses);
|
||||
const {
|
||||
libraryAuthoringMfeUrl,
|
||||
redirectToLibraryAuthoringMfe,
|
||||
courses, librariesEnabled, libraries, archivedCourses,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const {
|
||||
activeTab, courses, librariesEnabled, libraries, archivedCourses,
|
||||
} = tabsData;
|
||||
courseLoadingStatus,
|
||||
libraryLoadingStatus,
|
||||
} = useSelector(getLoadingStatuses);
|
||||
const isLoadingCourses = courseLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isFailedCoursesPage = courseLoadingStatus === RequestStatus.FAILED;
|
||||
const isLoadingLibraries = libraryLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isFailedLibrariesPage = libraryLoadingStatus === RequestStatus.FAILED;
|
||||
|
||||
// Controlling the visibility of tabs when using conditional rendering is necessary for
|
||||
// the correct operation of iterating over child elements inside the Paragon Tabs component.
|
||||
@@ -41,6 +50,8 @@ const TabsSection = ({
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
isShowProcessing={isShowProcessing}
|
||||
isLoading={isLoadingCourses}
|
||||
isFailed={isFailedCoursesPage}
|
||||
/>
|
||||
</Tab>,
|
||||
);
|
||||
@@ -52,7 +63,11 @@ const TabsSection = ({
|
||||
eventKey={TABS_LIST.archived}
|
||||
title={intl.formatMessage(messages.archivedTabTitle)}
|
||||
>
|
||||
<ArchivedTab archivedCoursesData={archivedCourses} />
|
||||
<ArchivedTab
|
||||
archivedCoursesData={archivedCourses}
|
||||
isLoading={isLoadingCourses}
|
||||
isFailed={isFailedCoursesPage}
|
||||
/>
|
||||
</Tab>,
|
||||
);
|
||||
}
|
||||
@@ -64,25 +79,34 @@ const TabsSection = ({
|
||||
eventKey={TABS_LIST.libraries}
|
||||
title={intl.formatMessage(messages.librariesTabTitle)}
|
||||
>
|
||||
{!redirectToLibraryAuthoringMfe && <LibrariesTab libraries={libraries} />}
|
||||
{!redirectToLibraryAuthoringMfe && (
|
||||
<LibrariesTab
|
||||
libraries={libraries}
|
||||
isLoading={isLoadingLibraries}
|
||||
isFailed={isFailedLibrariesPage}
|
||||
/>
|
||||
)}
|
||||
</Tab>,
|
||||
);
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}, [archivedCourses, librariesEnabled, showNewCourseContainer]);
|
||||
}, [archivedCourses, librariesEnabled, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]);
|
||||
|
||||
const handleSelectTab = (tab) => {
|
||||
if (tab === TABS_LIST.libraries && redirectToLibraryAuthoringMfe) {
|
||||
window.location.assign(libraryAuthoringMfeUrl);
|
||||
} else if (tab === TABS_LIST.libraries && !redirectToLibraryAuthoringMfe) {
|
||||
dispatch(fetchLibraryData());
|
||||
}
|
||||
setTabKey(tab);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className="studio-home-tabs"
|
||||
variant="tabs"
|
||||
defaultActiveKey={activeTab}
|
||||
activeKey={tabKey}
|
||||
onSelect={handleSelectTab}
|
||||
>
|
||||
{visibleTabs}
|
||||
@@ -90,41 +114,12 @@ const TabsSection = ({
|
||||
);
|
||||
};
|
||||
|
||||
const courseDataStructure = {
|
||||
courseKey: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
lmsLink: PropTypes.string.isRequired,
|
||||
number: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
rerunLink: PropTypes.string.isRequired,
|
||||
run: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
TabsSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
tabsData: PropTypes.shape({
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
archivedCourses: PropTypes.arrayOf(
|
||||
PropTypes.shape(courseDataStructure),
|
||||
).isRequired,
|
||||
courses: PropTypes.arrayOf(
|
||||
PropTypes.shape(courseDataStructure),
|
||||
).isRequired,
|
||||
libraries: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
libraryKey: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
number: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
librariesEnabled: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
showNewCourseContainer: PropTypes.bool.isRequired,
|
||||
onClickNewCourse: PropTypes.func.isRequired,
|
||||
isShowProcessing: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TabsSection);
|
||||
|
||||
@@ -1,26 +1,58 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Row } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
|
||||
import { LoadingSpinner } from '../../../generic/Loading';
|
||||
import CardItem from '../../card-item';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
import AlertMessage from '../../../generic/alert-message';
|
||||
import messages from '../messages';
|
||||
|
||||
const LibrariesTab = ({ libraries }) => (
|
||||
<div className="courses-tab">
|
||||
{sortAlphabeticallyArray(libraries).map(({
|
||||
displayName, org, number, url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={number}
|
||||
isLibraries
|
||||
displayName={displayName}
|
||||
org={org}
|
||||
number={number}
|
||||
url={url}
|
||||
const LibrariesTab = ({
|
||||
libraries,
|
||||
isLoading,
|
||||
isFailed,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
<LoadingSpinner />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return (
|
||||
isFailed ? (
|
||||
<AlertMessage
|
||||
variant="danger"
|
||||
description={(
|
||||
<Row className="m-0 align-items-center">
|
||||
<Icon src={Error} className="text-danger-500 mr-1" />
|
||||
<span>{intl.formatMessage(messages.librariesTabErrorMessage)}</span>
|
||||
</Row>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
) : (
|
||||
<div className="courses-tab">
|
||||
{sortAlphabeticallyArray(libraries).map(({
|
||||
displayName, org, number, url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={`${org}+${number}`}
|
||||
isLibraries
|
||||
displayName={displayName}
|
||||
org={org}
|
||||
number={number}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
LibrariesTab.propTypes = {
|
||||
libraries: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
@@ -31,6 +63,10 @@ LibrariesTab.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default LibrariesTab;
|
||||
export default injectIntl(LibrariesTab);
|
||||
|
||||
30
src/studio-home/tabs-section/messages.js
Normal file
30
src/studio-home/tabs-section/messages.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
coursesTabTitle: {
|
||||
id: 'course-authoring.studio-home.courses.tab.title',
|
||||
defaultMessage: 'Courses',
|
||||
},
|
||||
courseTabErrorMessage: {
|
||||
id: 'course-authoring.studio-home.courses.tab.error.message',
|
||||
defaultMessage: 'Failed to fetch courses. Please try again later.',
|
||||
},
|
||||
librariesTabErrorMessage: {
|
||||
id: 'course-authoring.studio-home.libraries.tab.error.message',
|
||||
defaultMessage: 'Failed to fetch libraries. Please try again later.',
|
||||
},
|
||||
librariesTabTitle: {
|
||||
id: 'course-authoring.studio-home.libraries.tab.title',
|
||||
defaultMessage: 'Libraries',
|
||||
},
|
||||
archivedTabTitle: {
|
||||
id: 'course-authoring.studio-home.archived.tab.title',
|
||||
defaultMessage: 'Archived courses',
|
||||
},
|
||||
archiveTabErrorMessage: {
|
||||
id: 'course-authoring.studio-home.archived.tab.error.message',
|
||||
defaultMessage: 'Failed to fetch archived courses. Please try again later.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
Reference in New Issue
Block a user