fix: Use soft nav when clicking a library from studio home (#1306)
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const AlertMessage = ({ title, description, ...props }) => (
|
||||
<Alert {...props}>
|
||||
<Alert.Heading>{title}</Alert.Heading>
|
||||
<span>{description}</span>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
AlertMessage.propTypes = {
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
};
|
||||
|
||||
AlertMessage.defaultProps = {
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
export default AlertMessage;
|
||||
16
src/generic/alert-message/index.tsx
Normal file
16
src/generic/alert-message/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
interface Props extends React.ComponentPropsWithoutRef<typeof Alert> {
|
||||
title?: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
}
|
||||
|
||||
const AlertMessage: React.FC<Props> = ({ title, description, ...props }) => (
|
||||
<Alert {...props}>
|
||||
<Alert.Heading>{title}</Alert.Heading>
|
||||
<span>{description}</span>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
export default AlertMessage;
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Row,
|
||||
} from '@openedx/paragon';
|
||||
import { Add as AddIcon, Error } from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
@@ -28,7 +28,8 @@ import messages from './messages';
|
||||
import { useStudioHome } from './hooks';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
|
||||
const StudioHome = ({ intl }) => {
|
||||
const StudioHome = () => {
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -46,7 +47,6 @@ const StudioHome = ({ intl }) => {
|
||||
hasAbilityToCreateNewCourse,
|
||||
isFiltered,
|
||||
setShowNewCourseContainer,
|
||||
dispatch,
|
||||
} = useStudioHome(isPaginationCoursesEnabled);
|
||||
|
||||
const libMode = getConfig().LIBRARY_MODE;
|
||||
@@ -64,7 +64,7 @@ const StudioHome = ({ intl }) => {
|
||||
} = studioHomeData;
|
||||
|
||||
const getHeaderButtons = useCallback(() => {
|
||||
const headerButtons = [];
|
||||
const headerButtons: JSX.Element[] = [];
|
||||
|
||||
if (isFailedLoadingPage || !userIsActive) {
|
||||
return headerButtons;
|
||||
@@ -160,11 +160,9 @@ const StudioHome = ({ intl }) => {
|
||||
)}
|
||||
{isShowOrganizationDropdown && <OrganizationSection />}
|
||||
<TabsSection
|
||||
tabsData={studioHomeData}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={() => setShowNewCourseContainer(true)}
|
||||
isShowProcessing={isShowProcessing && !isFiltered}
|
||||
dispatch={dispatch}
|
||||
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
|
||||
/>
|
||||
</section>
|
||||
@@ -203,8 +201,4 @@ const StudioHome = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StudioHome.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StudioHome);
|
||||
export default StudioHome;
|
||||
@@ -1,107 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from '../messages';
|
||||
import initializeStore from '../../store';
|
||||
import { trimSlashes } from './utils';
|
||||
import CardItem from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
let store;
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CardItem intl={{ formatMessage: jest.fn() }} {...props} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<CardItem />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
it('should render course details for non-library course', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
expect(getByText(`${props.org} / ${props.number} / ${props.run}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct links for non-library course', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
const courseTitleLink = getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
const btnReRunCourse = getByText(messages.btnReRunText.defaultMessage);
|
||||
expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
|
||||
const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage);
|
||||
expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
|
||||
});
|
||||
|
||||
it('should render correct links for non-library course pagination', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
const { getByText, getByTestId } = render(<RootWrapper {...props} isPaginated />);
|
||||
const courseTitleLink = getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
const dropDownMenu = getByTestId('toggle-dropdown');
|
||||
fireEvent.click(dropDownMenu);
|
||||
const btnReRunCourse = getByText(messages.btnReRunText.defaultMessage);
|
||||
expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
|
||||
const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage);
|
||||
expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
|
||||
});
|
||||
it('should render course details for library course', () => {
|
||||
const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
const courseTitleLink = getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
expect(getByText(`${props.org} / ${props.number}`)).toBeInTheDocument();
|
||||
});
|
||||
it('should hide rerun button if disallowed', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
useSelector.mockReturnValue({ ...studioHomeMock, allowCourseReruns: false });
|
||||
const { queryByText } = render(<RootWrapper {...props} />);
|
||||
expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
it('should be read only course if old mongo course', () => {
|
||||
const props = studioHomeMock.courses[1];
|
||||
const { queryByText } = render(<RootWrapper {...props} />);
|
||||
expect(queryByText(props.displayName)).not.toHaveAttribute('href');
|
||||
expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(queryByText(messages.viewLiveBtnText.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render course key if displayname is empty', () => {
|
||||
const props = studioHomeMock.courses[1];
|
||||
const courseKeyTest = 'course-key';
|
||||
const { getByText } = render(
|
||||
<RootWrapper
|
||||
{...props}
|
||||
displayName=""
|
||||
courseKey={courseKeyTest}
|
||||
lmsLink="lmsLink"
|
||||
rerunLink="returnLink"
|
||||
url="url"
|
||||
/>,
|
||||
);
|
||||
expect(getByText(courseKeyTest)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
89
src/studio-home/card-item/CardItem.test.tsx
Normal file
89
src/studio-home/card-item/CardItem.test.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import * as reactRedux from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from '../messages';
|
||||
import { trimSlashes } from './utils';
|
||||
import {
|
||||
fireEvent,
|
||||
initializeMocks,
|
||||
render,
|
||||
screen,
|
||||
} from '../../testUtils';
|
||||
import CardItem from '.';
|
||||
|
||||
jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => studioHomeMock);
|
||||
|
||||
describe('<CardItem />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
});
|
||||
it('should render course details for non-library course', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
render(<CardItem {...props} />);
|
||||
expect(screen.getByText(`${props.org} / ${props.number} / ${props.run}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct links for non-library course', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
render(<CardItem {...props} />);
|
||||
const courseTitleLink = screen.getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
const btnReRunCourse = screen.getByText(messages.btnReRunText.defaultMessage);
|
||||
expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
|
||||
const viewLiveLink = screen.getByText(messages.viewLiveBtnText.defaultMessage);
|
||||
expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
|
||||
});
|
||||
|
||||
it('should render correct links for non-library course pagination', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
render(<CardItem {...props} isPaginated />);
|
||||
const courseTitleLink = screen.getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
const dropDownMenu = screen.getByTestId('toggle-dropdown');
|
||||
fireEvent.click(dropDownMenu);
|
||||
const btnReRunCourse = screen.getByText(messages.btnReRunText.defaultMessage);
|
||||
expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
|
||||
const viewLiveLink = screen.getByText(messages.viewLiveBtnText.defaultMessage);
|
||||
expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
|
||||
});
|
||||
it('should render course details for library course', () => {
|
||||
const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
|
||||
render(<CardItem {...props} />);
|
||||
const courseTitleLink = screen.getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
expect(screen.getByText(`${props.org} / ${props.number}`)).toBeInTheDocument();
|
||||
});
|
||||
it('should hide rerun button if disallowed', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
// Update our mocked redux data:
|
||||
jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => (
|
||||
{ ...studioHomeMock, allowCourseReruns: false }
|
||||
));
|
||||
const { queryByText } = render(<CardItem {...props} />);
|
||||
expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
it('should be read only course if old mongo course', () => {
|
||||
const props = studioHomeMock.courses[1];
|
||||
render(<CardItem {...props} />);
|
||||
expect(screen.queryByText(props.displayName)).not.toHaveAttribute('href');
|
||||
expect(screen.queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.viewLiveBtnText.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render course key if displayname is empty', () => {
|
||||
const props = studioHomeMock.courses[1];
|
||||
const courseKeyTest = 'course-key';
|
||||
render(
|
||||
<CardItem
|
||||
{...props}
|
||||
displayName=""
|
||||
courseKey={courseKeyTest}
|
||||
lmsLink="lmsLink"
|
||||
rerunLink="returnLink"
|
||||
url="url"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(courseKeyTest)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Card,
|
||||
Hyperlink,
|
||||
@@ -9,16 +8,40 @@ import {
|
||||
ActionRow,
|
||||
} from '@openedx/paragon';
|
||||
import { MoreHoriz } from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../constants';
|
||||
import { getStudioHomeData } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { trimSlashes } from './utils';
|
||||
|
||||
const CardItem = ({
|
||||
intl,
|
||||
interface BaseProps {
|
||||
displayName: string;
|
||||
org: string;
|
||||
number: string;
|
||||
run?: string;
|
||||
lmsLink?: string | null;
|
||||
rerunLink?: string | null;
|
||||
courseKey?: string;
|
||||
isLibraries?: boolean;
|
||||
isPaginated?: boolean;
|
||||
}
|
||||
type Props = BaseProps & (
|
||||
/** If we should open this course/library in this MFE, this is the path to the edit page, e.g. '/course/foo' */
|
||||
{ path: string, url?: never } |
|
||||
/**
|
||||
* If we might be redirecting to the legacy Studio view, this is the URL to redirect to.
|
||||
* URLs starting with '/' are assumed to be relative to the legacy Studio root.
|
||||
*/
|
||||
{ url: string, path?: never }
|
||||
);
|
||||
|
||||
/**
|
||||
* A card on the Studio home page that represents a Course or a Library
|
||||
*/
|
||||
const CardItem: React.FC<Props> = ({
|
||||
displayName,
|
||||
lmsLink = '',
|
||||
rerunLink = '',
|
||||
@@ -28,14 +51,16 @@ const CardItem = ({
|
||||
isLibraries = false,
|
||||
courseKey = '',
|
||||
isPaginated = false,
|
||||
path,
|
||||
url,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
allowCourseReruns,
|
||||
courseCreatorStatus,
|
||||
rerunCreatorStatus,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const destinationUrl = () => new URL(url, getConfig().STUDIO_BASE_URL);
|
||||
const destinationUrl: string = path ?? new URL(url, getConfig().STUDIO_BASE_URL).toString();
|
||||
const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`;
|
||||
const readOnlyItem = !(lmsLink || rerunLink || url);
|
||||
const showActions = !(readOnlyItem || isLibraries);
|
||||
@@ -49,12 +74,12 @@ const CardItem = ({
|
||||
<Card.Header
|
||||
size="sm"
|
||||
title={!readOnlyItem ? (
|
||||
<Hyperlink
|
||||
<Link
|
||||
className="card-item-title"
|
||||
destination={destinationUrl().toString()}
|
||||
to={destinationUrl}
|
||||
>
|
||||
{hasDisplayName}
|
||||
</Hyperlink>
|
||||
</Link>
|
||||
) : (
|
||||
<span className="card-item-title">{displayName}</span>
|
||||
)}
|
||||
@@ -70,7 +95,7 @@ const CardItem = ({
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
{isShowRerunLink && (
|
||||
<Dropdown.Item href={trimSlashes(rerunLink)}>
|
||||
<Dropdown.Item href={trimSlashes(rerunLink ?? '')}>
|
||||
{messages.btnReRunText.defaultMessage}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
@@ -84,7 +109,7 @@ const CardItem = ({
|
||||
{isShowRerunLink && (
|
||||
<Hyperlink
|
||||
className="small"
|
||||
destination={trimSlashes(rerunLink)}
|
||||
destination={trimSlashes(rerunLink ?? '')}
|
||||
key={`action-row-rerunLink-${courseKey}`}
|
||||
>
|
||||
{intl.formatMessage(messages.btnReRunText)}
|
||||
@@ -92,7 +117,7 @@ const CardItem = ({
|
||||
)}
|
||||
<Hyperlink
|
||||
className="small ml-3"
|
||||
destination={lmsLink}
|
||||
destination={lmsLink ?? ''}
|
||||
key={`action-row-lmsLink-${courseKey}`}
|
||||
>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
@@ -105,18 +130,4 @@ const CardItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
CardItem.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
lmsLink: PropTypes.string,
|
||||
rerunLink: PropTypes.string,
|
||||
org: PropTypes.string.isRequired,
|
||||
run: PropTypes.string,
|
||||
number: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
isLibraries: PropTypes.bool,
|
||||
courseKey: PropTypes.string,
|
||||
isPaginated: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(CardItem);
|
||||
export default CardItem;
|
||||
@@ -4,4 +4,4 @@
|
||||
* @returns {string} The trimmed string.
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const trimSlashes = (str) => str.replace(/^\/|\/$/g, '');
|
||||
export const trimSlashes = (str: string): string => str.replace(/^\/|\/$/g, '');
|
||||
@@ -16,6 +16,7 @@ export async function getStudioHomeData() {
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/** Get list of courses from the deprecated non-paginated API */
|
||||
export async function getStudioHomeCourses(search) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`);
|
||||
return camelCaseObject(data);
|
||||
|
||||
@@ -48,36 +48,32 @@ export const generateGetStudioHomeDataApiResponse = () => ({
|
||||
allowToCreateNewOrg: false,
|
||||
});
|
||||
|
||||
/** Mock for the deprecated /api/contentstore/v1/home/courses endpoint. Note this endpoint is NOT paginated. */
|
||||
export const generateGetStudioCoursesApiResponse = () => ({
|
||||
count: 5,
|
||||
next: null,
|
||||
previous: null,
|
||||
numPages: 2,
|
||||
results: {
|
||||
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: [],
|
||||
},
|
||||
archivedCourses: /** @type {any[]} */([]),
|
||||
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 generateGetStudioCoursesApiResponseV2 = () => ({
|
||||
|
||||
@@ -93,7 +93,6 @@ const useStudioHome = (isPaginated = false) => {
|
||||
isShowOrganizationDropdown,
|
||||
hasAbilityToCreateNewCourse,
|
||||
isFiltered,
|
||||
dispatch,
|
||||
setShowNewCourseContainer,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { getConfig, initializeMockApp, setConfig } from '@edx/frontend-platform';
|
||||
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 { Routes, Route } from 'react-router-dom';
|
||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from '../messages';
|
||||
import tabMessages from './messages';
|
||||
@@ -27,6 +17,13 @@ import { executeThunk } from '../../utils';
|
||||
import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks';
|
||||
import { getContentLibraryV2ListApiUrl } from '../../library-authoring/data/api';
|
||||
import contentLibrariesListV2 from '../../library-authoring/__mocks__/contentLibrariesListV2';
|
||||
import {
|
||||
initializeMocks,
|
||||
render as baseRender,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '../../testUtils';
|
||||
|
||||
const { studioShortName } = studioHomeMock;
|
||||
|
||||
@@ -36,56 +33,39 @@ const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
const courseApiLinkV2 = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
|
||||
const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const tabSectionComponent = (overrideProps) => (
|
||||
<TabsSection
|
||||
intl={{ formatMessage: jest.fn() }}
|
||||
dispatch={mockDispatch}
|
||||
isPaginationCoursesEnabled={false}
|
||||
showNewCourseContainer={false}
|
||||
onClickNewCourse={() => {}}
|
||||
isShowProcessing
|
||||
{...overrideProps}
|
||||
/>
|
||||
);
|
||||
|
||||
const RootWrapper = (overrideProps) => (
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={['/home']}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/home"
|
||||
element={tabSectionComponent(overrideProps)}
|
||||
/>
|
||||
<Route
|
||||
path="/libraries"
|
||||
element={tabSectionComponent(overrideProps)}
|
||||
/>
|
||||
<Route
|
||||
path="/libraries-v1"
|
||||
element={tabSectionComponent(overrideProps)}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
const render = (overrideProps = {}) => baseRender(
|
||||
<Routes>
|
||||
<Route
|
||||
path="/home"
|
||||
element={tabSectionComponent(overrideProps)}
|
||||
/>
|
||||
<Route
|
||||
path="/libraries"
|
||||
element={tabSectionComponent(overrideProps)}
|
||||
/>
|
||||
<Route
|
||||
path="/libraries-v1"
|
||||
element={tabSectionComponent(overrideProps)}
|
||||
/>
|
||||
</Routes>,
|
||||
{ routerProps: { initialEntries: ['/home'] } },
|
||||
);
|
||||
|
||||
describe('<TabsSection />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const newMocks = initializeMocks({ initialState });
|
||||
store = newMocks.reduxStore;
|
||||
axiosMock = newMocks.axiosMock;
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
LIBRARY_MODE: 'mixed',
|
||||
@@ -94,7 +74,7 @@ describe('<TabsSection />', () => {
|
||||
});
|
||||
|
||||
it('should render all tabs correctly', async () => {
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
const data: any = generateGetStudioHomeDataApiResponse();
|
||||
data.archivedCourses = [{
|
||||
courseKey: 'course-v1:MachineLearning+123+2023',
|
||||
displayName: 'Machine Learning',
|
||||
@@ -106,7 +86,7 @@ describe('<TabsSection />', () => {
|
||||
url: '/course/course-v1:MachineLearning+123+2023',
|
||||
}];
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
@@ -127,7 +107,7 @@ describe('<TabsSection />', () => {
|
||||
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
@@ -148,7 +128,7 @@ describe('<TabsSection />', () => {
|
||||
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
@@ -163,8 +143,8 @@ describe('<TabsSection />', () => {
|
||||
|
||||
describe('course tab', () => {
|
||||
it('should render specific course details', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { results: data } = generateGetStudioCoursesApiResponse();
|
||||
render();
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
@@ -178,9 +158,9 @@ describe('<TabsSection />', () => {
|
||||
|
||||
it('should render default sections when courses are empty', async () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.results.courses = [];
|
||||
data.courses = [];
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
@@ -195,7 +175,7 @@ describe('<TabsSection />', () => {
|
||||
});
|
||||
|
||||
it('should render course fetch failure alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(404);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
@@ -204,7 +184,7 @@ describe('<TabsSection />', () => {
|
||||
});
|
||||
|
||||
it('should render pagination when there are courses', async () => {
|
||||
render(<RootWrapper isPaginationCoursesEnabled />);
|
||||
render({ isPaginationCoursesEnabled: true });
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLinkV2).reply(200, generateGetStudioCoursesApiResponseV2());
|
||||
await executeThunk(fetchStudioHomeData('', true, {}, true), store.dispatch);
|
||||
@@ -224,7 +204,7 @@ describe('<TabsSection />', () => {
|
||||
it('should not render pagination when there are not courses', async () => {
|
||||
const data = generateGetStudioCoursesApiResponseV2();
|
||||
data.results.courses = [];
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLinkV2).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
@@ -236,7 +216,7 @@ describe('<TabsSection />', () => {
|
||||
it('should set the url path to "/home" when switching away then back to courses tab', async () => {
|
||||
const data = generateGetStudioCoursesApiResponseV2();
|
||||
data.results.courses = [];
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLinkV2).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
@@ -250,9 +230,7 @@ describe('<TabsSection />', () => {
|
||||
axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
|
||||
await executeThunk(fetchLibraryData(), store.dispatch);
|
||||
const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
fireEvent.click(librariesTab);
|
||||
|
||||
// confirm that the url path has changed
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
@@ -262,9 +240,7 @@ describe('<TabsSection />', () => {
|
||||
|
||||
// switch back to courses tab
|
||||
const coursesTab = screen.getByText(tabMessages.coursesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(coursesTab);
|
||||
});
|
||||
fireEvent.click(coursesTab);
|
||||
|
||||
// confirm that the url path is /home
|
||||
expect(coursesTab).toHaveClass('active');
|
||||
@@ -281,7 +257,7 @@ describe('<TabsSection />', () => {
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
@@ -295,7 +271,7 @@ describe('<TabsSection />', () => {
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
@@ -310,8 +286,8 @@ describe('<TabsSection />', () => {
|
||||
|
||||
describe('archived tab', () => {
|
||||
it('should switch to Archived tab and render specific archived course details', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { results: data } = generateGetStudioCoursesApiResponse();
|
||||
render();
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.archivedCourses = studioHomeMock.archivedCourses;
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
@@ -331,7 +307,7 @@ describe('<TabsSection />', () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.archivedCourses = [];
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
@@ -347,34 +323,33 @@ describe('<TabsSection />', () => {
|
||||
});
|
||||
|
||||
describe('library tab', () => {
|
||||
beforeEach(() => {
|
||||
axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
});
|
||||
it('should switch to Legacy Libraries tab and render specific v1 library details', async () => {
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
await executeThunk(fetchLibraryData(), store.dispatch);
|
||||
|
||||
const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
fireEvent.click(librariesTab);
|
||||
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
expect(screen.getByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
|
||||
expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
|
||||
|
||||
expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should switch to Libraries tab and render specific v2 library details', async () => {
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
fireEvent.click(librariesTab);
|
||||
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
@@ -397,20 +372,18 @@ describe('<TabsSection />', () => {
|
||||
LIBRARY_MODE: 'v1 only',
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
await executeThunk(fetchLibraryData(), store.dispatch);
|
||||
|
||||
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
fireEvent.click(librariesTab);
|
||||
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
expect(screen.getByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
|
||||
expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
|
||||
|
||||
expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible();
|
||||
});
|
||||
@@ -421,14 +394,12 @@ describe('<TabsSection />', () => {
|
||||
LIBRARY_MODE: 'v2 only',
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
fireEvent.click(librariesTab);
|
||||
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
@@ -449,7 +420,7 @@ describe('<TabsSection />', () => {
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
data.librariesEnabled = false;
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
@@ -461,7 +432,7 @@ describe('<TabsSection />', () => {
|
||||
const data = generateGetStudioHomeDataApiResponse();
|
||||
data.redirectToLibraryAuthoringMfe = true;
|
||||
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
@@ -474,20 +445,18 @@ describe('<TabsSection />', () => {
|
||||
});
|
||||
|
||||
it('should render libraries fetch failure alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
render();
|
||||
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.legacyLibrariesTabTitle.defaultMessage);
|
||||
await act(async () => {
|
||||
fireEvent.click(librariesTab);
|
||||
});
|
||||
fireEvent.click(librariesTab);
|
||||
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
expect(screen.getByText(tabMessages.librariesTabErrorMessage.defaultMessage)).toBeVisible();
|
||||
expect(await screen.findByText(tabMessages.librariesTabErrorMessage.defaultMessage)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,8 +10,6 @@ import { initialState } from '../../factories/mockApiResponses';
|
||||
|
||||
import CoursesTab from '.';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const onClickNewCourse = jest.fn();
|
||||
const isShowProcessing = false;
|
||||
const isLoading = false;
|
||||
@@ -23,7 +21,7 @@ const showNewCourseContainer = true;
|
||||
|
||||
const renderComponent = (overrideProps = {}, studioHomeState = {}) => {
|
||||
// Generate a custom initial state based on studioHomeCoursesRequestParams
|
||||
const customInitialState = {
|
||||
const customInitialState: any = { // TODO: remove 'any' once our redux state has proper types
|
||||
...initialState,
|
||||
studioHome: {
|
||||
...initialState.studioHome,
|
||||
@@ -38,7 +36,6 @@ const renderComponent = (overrideProps = {}, studioHomeState = {}) => {
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CoursesTab
|
||||
dispatch={mockDispatch}
|
||||
coursesDataItems={studioHomeMock.courses}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon,
|
||||
@@ -26,18 +25,39 @@ import AlertMessage from '../../../generic/alert-message';
|
||||
import messages from '../messages';
|
||||
import './index.scss';
|
||||
|
||||
const CoursesTab = ({
|
||||
interface Props {
|
||||
coursesDataItems: {
|
||||
courseKey: string;
|
||||
displayName: string;
|
||||
lmsLink: string | null;
|
||||
number: string;
|
||||
org: string;
|
||||
rerunLink: string | null;
|
||||
run: string;
|
||||
url: string;
|
||||
}[];
|
||||
showNewCourseContainer: boolean;
|
||||
onClickNewCourse: () => void;
|
||||
isShowProcessing: boolean;
|
||||
isLoading: boolean;
|
||||
isFailed: boolean;
|
||||
numPages: number;
|
||||
coursesCount: number;
|
||||
isEnabledPagination?: boolean;
|
||||
}
|
||||
|
||||
const CoursesTab: React.FC<Props> = ({
|
||||
coursesDataItems,
|
||||
showNewCourseContainer,
|
||||
onClickNewCourse,
|
||||
isShowProcessing,
|
||||
isLoading,
|
||||
isFailed,
|
||||
dispatch,
|
||||
numPages,
|
||||
coursesCount,
|
||||
isEnabledPagination,
|
||||
numPages = 0,
|
||||
coursesCount = 0,
|
||||
isEnabledPagination = false,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const {
|
||||
@@ -136,7 +156,6 @@ const CoursesTab = ({
|
||||
number,
|
||||
run,
|
||||
url,
|
||||
cmsLink,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
@@ -148,7 +167,6 @@ const CoursesTab = ({
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
cmsLink={cmsLink}
|
||||
isPaginated={isEnabledPagination}
|
||||
/>
|
||||
),
|
||||
@@ -197,34 +215,4 @@ const CoursesTab = ({
|
||||
);
|
||||
};
|
||||
|
||||
CoursesTab.defaultProps = {
|
||||
numPages: 0,
|
||||
coursesCount: 0,
|
||||
isEnabledPagination: false,
|
||||
};
|
||||
|
||||
CoursesTab.propTypes = {
|
||||
coursesDataItems: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
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,
|
||||
}),
|
||||
).isRequired,
|
||||
showNewCourseContainer: PropTypes.bool.isRequired,
|
||||
onClickNewCourse: PropTypes.func.isRequired,
|
||||
isShowProcessing: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
numPages: PropTypes.number,
|
||||
coursesCount: PropTypes.number,
|
||||
isEnabledPagination: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CoursesTab;
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tab, Tabs } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { getLoadingStatuses, getStudioHomeData } from '../data/selectors';
|
||||
@@ -17,13 +17,13 @@ import { fetchLibraryData } from '../data/thunks';
|
||||
import { isMixedOrV1LibrariesMode, isMixedOrV2LibrariesMode } from './utils';
|
||||
|
||||
const TabsSection = ({
|
||||
intl,
|
||||
showNewCourseContainer,
|
||||
onClickNewCourse,
|
||||
isShowProcessing,
|
||||
dispatch,
|
||||
isPaginationCoursesEnabled,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const libMode = getConfig().LIBRARY_MODE;
|
||||
@@ -33,7 +33,7 @@ const TabsSection = ({
|
||||
legacyLibraries: 'legacyLibraries',
|
||||
archived: 'archived',
|
||||
taxonomies: 'taxonomies',
|
||||
};
|
||||
} as const;
|
||||
|
||||
const initTabKeyState = (pname) => {
|
||||
if (pname.includes('/libraries-v1')) {
|
||||
@@ -80,7 +80,7 @@ const TabsSection = ({
|
||||
// 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.
|
||||
const visibleTabs = useMemo(() => {
|
||||
const tabs = [];
|
||||
const tabs: JSX.Element[] = [];
|
||||
tabs.push(
|
||||
<Tab
|
||||
key={TABS_LIST.courses}
|
||||
@@ -94,7 +94,6 @@ const TabsSection = ({
|
||||
isShowProcessing={isShowProcessing}
|
||||
isLoading={isLoadingCourses}
|
||||
isFailed={isFailedCoursesPage}
|
||||
dispatch={dispatch}
|
||||
numPages={numPages}
|
||||
coursesCount={coursesCount}
|
||||
isEnabledPagination={isPaginationCoursesEnabled}
|
||||
@@ -199,12 +198,10 @@ TabsSection.defaultProps = {
|
||||
};
|
||||
|
||||
TabsSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
showNewCourseContainer: PropTypes.bool.isRequired,
|
||||
onClickNewCourse: PropTypes.func.isRequired,
|
||||
isShowProcessing: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
isPaginationCoursesEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(TabsSection);
|
||||
export default TabsSection;
|
||||
@@ -150,11 +150,14 @@ const defaultUser = {
|
||||
*
|
||||
* Returns the new `axiosMock` in case you need to mock out axios requests.
|
||||
*/
|
||||
export function initializeMocks({ user = defaultUser } = {}) {
|
||||
export function initializeMocks({ user = defaultUser, initialState = undefined }: {
|
||||
user?: { userId: number, username: string },
|
||||
initialState?: Record<string, any>, // TODO: proper typing for our redux state
|
||||
} = {}) {
|
||||
initializeMockApp({
|
||||
authenticatedUser: user,
|
||||
});
|
||||
reduxStore = initializeReduxStore();
|
||||
reduxStore = initializeReduxStore(initialState as any);
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
Reference in New Issue
Block a user