refactor: remove references of ENABLE_HOME_PAGE_COURSE_API_V2 (#1611)
* refactor: remove references of ENABLE_HOME_PAGE_COURSE_API_V2 * fix: infinite requests when clearing filters * fix: some requests were being duplicated when changing filters * test: adapt tests to the latest changes * test: improve test coverage * refactor: drop tab for archived courses * test: filter reset functionality in CoursesTab component * refactor: revert deletion of isShowProcessing * test: update visibility check for pagination text in TabsSection tests * refactor: update dropdown and button accessibility in CardItem and CoursesTab components
This commit is contained in:
1
.env
1
.env
@@ -41,7 +41,6 @@ HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=false
|
||||
INVITE_STUDENTS_EMAIL_TO=''
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=''
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
# "Multi-level" blocks are unsupported in libraries
|
||||
|
||||
@@ -44,7 +44,6 @@ HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=true
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
# "Multi-level" blocks are unsupported in libraries
|
||||
|
||||
@@ -158,7 +158,6 @@ initialize({
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN || 'false',
|
||||
ENABLE_CERTIFICATE_PAGE: process.env.ENABLE_CERTIFICATE_PAGE || 'false',
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: process.env.ENABLE_TAGGING_TAXONOMY_PAGES || 'false',
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2: process.env.ENABLE_HOME_PAGE_COURSE_API_V2 === 'true',
|
||||
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS: process.env.ENABLE_GRADING_METHOD_IN_PROBLEMS === 'true',
|
||||
LIBRARY_UNSUPPORTED_BLOCKS: (process.env.LIBRARY_UNSUPPORTED_BLOCKS || 'conditional,step-builder,problem-builder').split(','),
|
||||
|
||||
@@ -31,7 +31,6 @@ const StudioHome = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isPaginationCoursesEnabled = getConfig().ENABLE_HOME_PAGE_COURSE_API_V2;
|
||||
const {
|
||||
isLoadingPage,
|
||||
isFailedLoadingPage,
|
||||
@@ -153,7 +152,6 @@ const StudioHome = () => {
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={() => setShowNewCourseContainer(true)}
|
||||
isShowProcessing={isShowProcessing && !isFiltered}
|
||||
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
|
||||
librariesV1Enabled={librariesV1Enabled}
|
||||
librariesV2Enabled={librariesV2Enabled}
|
||||
/>
|
||||
|
||||
@@ -29,18 +29,20 @@ describe('<CardItem />', () => {
|
||||
render(<CardItem {...props} />);
|
||||
const courseTitleLink = screen.getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
const dropDownMenu = screen.getByRole('button', { name: /course actions/i });
|
||||
fireEvent.click(dropDownMenu);
|
||||
const btnReRunCourse = screen.getByText(messages.btnReRunText.defaultMessage);
|
||||
expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
|
||||
expect(btnReRunCourse).toHaveAttribute('href', 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 />);
|
||||
render(<CardItem {...props} />);
|
||||
const courseTitleLink = screen.getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
const dropDownMenu = screen.getByTestId('toggle-dropdown');
|
||||
const dropDownMenu = screen.getByRole('button', { name: /course actions/i });
|
||||
fireEvent.click(dropDownMenu);
|
||||
const btnReRunCourse = screen.getByText(messages.btnReRunText.defaultMessage);
|
||||
expect(btnReRunCourse).toHaveAttribute('href', `/${trimSlashes(props.rerunLink)}`);
|
||||
|
||||
@@ -2,10 +2,8 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Card,
|
||||
Hyperlink,
|
||||
Dropdown,
|
||||
IconButton,
|
||||
ActionRow,
|
||||
} from '@openedx/paragon';
|
||||
import { MoreHoriz } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -16,7 +14,6 @@ import { getWaffleFlags } from '../../data/selectors';
|
||||
import { COURSE_CREATOR_STATES } from '../../constants';
|
||||
import { getStudioHomeData } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { trimSlashes } from './utils';
|
||||
|
||||
interface BaseProps {
|
||||
displayName: string;
|
||||
@@ -27,7 +24,6 @@ interface BaseProps {
|
||||
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' */
|
||||
@@ -51,7 +47,6 @@ const CardItem: React.FC<Props> = ({
|
||||
run = '',
|
||||
isLibraries = false,
|
||||
courseKey = '',
|
||||
isPaginated = false,
|
||||
path,
|
||||
url,
|
||||
}) => {
|
||||
@@ -92,48 +87,27 @@ const CardItem: React.FC<Props> = ({
|
||||
)}
|
||||
subtitle={subtitle}
|
||||
actions={showActions && (
|
||||
isPaginated ? (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
as={IconButton}
|
||||
iconAs={MoreHoriz}
|
||||
variant="primary"
|
||||
data-testid="toggle-dropdown"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
{isShowRerunLink && (
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
to={rerunLink ?? ''}
|
||||
>
|
||||
{messages.btnReRunText.defaultMessage}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={lmsLink}>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<ActionRow>
|
||||
{isShowRerunLink && (
|
||||
<Hyperlink
|
||||
className="small"
|
||||
destination={trimSlashes(rerunLink ?? '')}
|
||||
key={`action-row-rerunLink-${courseKey}`}
|
||||
>
|
||||
{intl.formatMessage(messages.btnReRunText)}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<Hyperlink
|
||||
className="small ml-3"
|
||||
destination={lmsLink ?? ''}
|
||||
key={`action-row-lmsLink-${courseKey}`}
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
as={IconButton}
|
||||
iconAs={MoreHoriz}
|
||||
variant="primary"
|
||||
aria-label={intl.formatMessage(messages.btnDropDownText)}
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
{isShowRerunLink && (
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
to={rerunLink ?? ''}
|
||||
>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
</Hyperlink>
|
||||
</ActionRow>
|
||||
)
|
||||
{messages.btnReRunText.defaultMessage}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={lmsLink}>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getStudioHomeLibraries,
|
||||
} from './api';
|
||||
import {
|
||||
generateGetStudioCoursesApiResponse,
|
||||
generateGetStudioCoursesApiResponseV2,
|
||||
generateGetStudioHomeDataApiResponse,
|
||||
generateGetStudioHomeLibrariesApiResponse,
|
||||
} from '../factories/mockApiResponses';
|
||||
@@ -50,9 +50,9 @@ describe('studio-home api calls', () => {
|
||||
|
||||
it('should get studio courses data', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponseV2());
|
||||
const result = await getStudioHomeCourses('');
|
||||
const expected = generateGetStudioCoursesApiResponse();
|
||||
const expected = generateGetStudioCoursesApiResponseV2();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(apiLink);
|
||||
expect(result).toEqual(expected);
|
||||
@@ -60,9 +60,9 @@ describe('studio-home api calls', () => {
|
||||
|
||||
it('should get studio courses data v2', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponseV2());
|
||||
const result = await getStudioHomeCoursesV2('');
|
||||
const expected = generateGetStudioCoursesApiResponse();
|
||||
const expected = generateGetStudioCoursesApiResponseV2();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(apiLink);
|
||||
expect(result).toEqual(expected);
|
||||
|
||||
@@ -3,6 +3,16 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
export const studioHomeCoursesRequestParamsDefault = {
|
||||
currentPage: 1,
|
||||
search: '',
|
||||
order: 'display_name',
|
||||
archivedOnly: undefined,
|
||||
activeOnly: undefined,
|
||||
isFiltered: false,
|
||||
cleanFilters: false,
|
||||
};
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'studioHome',
|
||||
initialState: {
|
||||
@@ -17,15 +27,7 @@ const slice = createSlice({
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
search: undefined,
|
||||
order: 'display_name',
|
||||
archivedOnly: undefined,
|
||||
activeOnly: undefined,
|
||||
isFiltered: false,
|
||||
cleanFilters: false,
|
||||
},
|
||||
studioHomeCoursesRequestParams: studioHomeCoursesRequestParamsDefault,
|
||||
},
|
||||
reducers: {
|
||||
updateLoadingStatuses: (state, { payload }) => {
|
||||
@@ -59,6 +61,9 @@ const slice = createSlice({
|
||||
updateStudioHomeCoursesCustomParams: (state, { payload }) => {
|
||||
Object.assign(state.studioHomeCoursesRequestParams, payload);
|
||||
},
|
||||
resetStudioHomeCoursesCustomParams: (state) => {
|
||||
state.studioHomeCoursesRequestParams = studioHomeCoursesRequestParamsDefault;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -70,6 +75,7 @@ export const {
|
||||
fetchCourseDataSuccessV2,
|
||||
fetchLibraryDataSuccess,
|
||||
updateStudioHomeCoursesCustomParams,
|
||||
resetStudioHomeCoursesCustomParams,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { reducer, updateStudioHomeCoursesCustomParams } from './slice';
|
||||
import { reducer, resetStudioHomeCoursesCustomParams, updateStudioHomeCoursesCustomParams } from './slice';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
search: undefined,
|
||||
search: '',
|
||||
order: 'display_name',
|
||||
archivedOnly: undefined,
|
||||
activeOnly: undefined,
|
||||
@@ -26,26 +26,9 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
|
||||
},
|
||||
};
|
||||
|
||||
it('should return the initial state', () => {
|
||||
const result = reducer(undefined, { type: undefined });
|
||||
expect(result).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should update the payload passed in studioHomeCoursesRequestParams', () => {
|
||||
const newState = {
|
||||
...initialState,
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 2,
|
||||
search: 'test',
|
||||
order: 'display_name',
|
||||
archivedOnly: true,
|
||||
activeOnly: true,
|
||||
isFiltered: true,
|
||||
cleanFilters: true,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = {
|
||||
const modifiedRequestParamsState = {
|
||||
...initialState,
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 2,
|
||||
search: 'test',
|
||||
order: 'display_name',
|
||||
@@ -53,9 +36,34 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
|
||||
activeOnly: true,
|
||||
isFiltered: true,
|
||||
cleanFilters: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const payload = {
|
||||
currentPage: 2,
|
||||
search: 'test',
|
||||
order: 'display_name',
|
||||
archivedOnly: true,
|
||||
activeOnly: true,
|
||||
isFiltered: true,
|
||||
cleanFilters: true,
|
||||
};
|
||||
|
||||
it('should return the initial state', () => {
|
||||
const result = reducer(undefined, { type: undefined });
|
||||
expect(result).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should update the payload passed in studioHomeCoursesRequestParams', () => {
|
||||
const result = reducer(initialState, updateStudioHomeCoursesCustomParams(payload));
|
||||
expect(result).toEqual(newState);
|
||||
expect(result).toEqual(modifiedRequestParamsState);
|
||||
});
|
||||
|
||||
it('should reset the studioHomeCoursesRequestParams state to the initial value', () => {
|
||||
const stateChanged = reducer(initialState, updateStudioHomeCoursesCustomParams(payload));
|
||||
expect(stateChanged).toEqual(modifiedRequestParamsState);
|
||||
|
||||
const stateReset = reducer(stateChanged, resetStudioHomeCoursesCustomParams());
|
||||
expect(stateReset.studioHomeCoursesRequestParams).toEqual(initialState.studioHomeCoursesRequestParams);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,13 +3,11 @@ import {
|
||||
getStudioHomeData,
|
||||
sendRequestForCourseCreator,
|
||||
handleCourseNotification,
|
||||
getStudioHomeCourses,
|
||||
getStudioHomeLibraries,
|
||||
getStudioHomeCoursesV2,
|
||||
} from './api';
|
||||
import {
|
||||
fetchStudioHomeDataSuccess,
|
||||
fetchCourseDataSuccess,
|
||||
updateLoadingStatuses,
|
||||
updateSavingStatuses,
|
||||
fetchLibraryDataSuccess,
|
||||
@@ -20,7 +18,6 @@ function fetchStudioHomeData(
|
||||
search,
|
||||
hasHomeData,
|
||||
requestParams = {},
|
||||
isPaginationEnabled = false,
|
||||
shouldFetchCourses = true,
|
||||
) {
|
||||
return async (dispatch) => {
|
||||
@@ -39,14 +36,8 @@ function fetchStudioHomeData(
|
||||
if (shouldFetchCourses) {
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
try {
|
||||
if (isPaginationEnabled) {
|
||||
const coursesData = await getStudioHomeCoursesV2(search || '', requestParams);
|
||||
dispatch(fetchCourseDataSuccessV2(coursesData));
|
||||
} else {
|
||||
const coursesData = await getStudioHomeCourses(search || '');
|
||||
dispatch(fetchCourseDataSuccess(coursesData));
|
||||
}
|
||||
|
||||
const coursesData = await getStudioHomeCoursesV2(search || '', requestParams);
|
||||
dispatch(fetchCourseDataSuccessV2(coursesData));
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.FAILED }));
|
||||
|
||||
@@ -48,34 +48,6 @@ export const generateGetStudioHomeDataApiResponse = () => ({
|
||||
allowToCreateNewOrg: false,
|
||||
});
|
||||
|
||||
/** Mock for the deprecated /api/contentstore/v1/home/courses endpoint. Note this endpoint is NOT paginated. */
|
||||
export const generateGetStudioCoursesApiResponse = () => ({
|
||||
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 = () => ({
|
||||
count: 5,
|
||||
next: null,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { COURSE_CREATOR_STATES } from '../constants';
|
||||
@@ -19,7 +18,6 @@ import { updateSavingStatuses } from './data/slice';
|
||||
const useStudioHome = () => {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const isPaginated = getConfig().ENABLE_HOME_PAGE_COURSE_API_V2;
|
||||
const studioHomeData = useSelector(getStudioHomeData);
|
||||
const studioHomeCoursesParams = useSelector(getStudioHomeCoursesParams);
|
||||
const { isFiltered } = studioHomeCoursesParams;
|
||||
@@ -35,18 +33,14 @@ const useStudioHome = () => {
|
||||
const isFailedLoadingPage = studioHomeLoadingStatus === RequestStatus.FAILED;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPaginated) {
|
||||
dispatch(fetchStudioHomeData(location.search ?? ''));
|
||||
setShowNewCourseContainer(false);
|
||||
}
|
||||
dispatch(fetchStudioHomeData(location.search ?? ''));
|
||||
setShowNewCourseContainer(false);
|
||||
dispatch(fetchWaffleFlags());
|
||||
}, [location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaginated) {
|
||||
const firstPage = 1;
|
||||
dispatch(fetchStudioHomeData(location.search ?? '', false, { page: firstPage, order: 'display_name' }, true));
|
||||
}
|
||||
const firstPage = 1;
|
||||
dispatch(fetchStudioHomeData(location.search ?? '', false, { page: firstPage, order: 'display_name' }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -45,6 +45,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.studio-home.btn.re-run.text',
|
||||
defaultMessage: 'Re-run course',
|
||||
},
|
||||
btnDropDownText: {
|
||||
id: 'course-authoring.studio-home.btn.dropdown.text',
|
||||
defaultMessage: 'Course actions',
|
||||
},
|
||||
viewLiveBtnText: {
|
||||
id: 'course-authoring.studio-home.btn.view-live.text',
|
||||
defaultMessage: 'View live',
|
||||
|
||||
@@ -8,7 +8,6 @@ import TabsSection from '.';
|
||||
import {
|
||||
initialState,
|
||||
generateGetStudioHomeDataApiResponse,
|
||||
generateGetStudioCoursesApiResponse,
|
||||
generateGetStudioCoursesApiResponseV2,
|
||||
generateGetStudioHomeLibrariesApiResponse,
|
||||
} from '../factories/mockApiResponses';
|
||||
@@ -28,7 +27,6 @@ const { studioShortName } = studioHomeMock;
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
const courseApiLinkV2 = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
|
||||
const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
|
||||
|
||||
@@ -37,7 +35,6 @@ const librariesBetaTabTitle = /Libraries Beta/;
|
||||
|
||||
const tabSectionComponent = (overrideProps) => (
|
||||
<TabsSection
|
||||
isPaginationCoursesEnabled={false}
|
||||
showNewCourseContainer={false}
|
||||
onClickNewCourse={() => {}}
|
||||
isShowProcessing
|
||||
@@ -84,16 +81,6 @@ describe('<TabsSection />', () => {
|
||||
|
||||
it('should render all tabs correctly', async () => {
|
||||
const data: any = 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();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
|
||||
@@ -104,8 +91,6 @@ describe('<TabsSection />', () => {
|
||||
expect(screen.getByRole('tab', { name: librariesBetaTabTitle })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('tab', { name: tabMessages.legacyLibrariesTabTitle.defaultMessage })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('tab', { name: tabMessages.archivedTabTitle.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render only 1 library tab when libraries-v2 disabled', async () => {
|
||||
@@ -143,9 +128,9 @@ describe('<TabsSection />', () => {
|
||||
describe('course tab', () => {
|
||||
it('should render specific course details', async () => {
|
||||
render();
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
const data = generateGetStudioCoursesApiResponseV2();
|
||||
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await axiosMock.onGet(courseApiLinkV2).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(studioHomeMock.courses[0].displayName)).toBeVisible();
|
||||
@@ -156,12 +141,12 @@ describe('<TabsSection />', () => {
|
||||
});
|
||||
|
||||
it('should render default sections when courses are empty', async () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.courses = [];
|
||||
const data = generateGetStudioCoursesApiResponseV2();
|
||||
data.results.courses = [];
|
||||
|
||||
render();
|
||||
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await axiosMock.onGet(courseApiLinkV2).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(`Are you staff on an existing ${studioShortName} course?`)).toBeInTheDocument();
|
||||
@@ -176,7 +161,7 @@ describe('<TabsSection />', () => {
|
||||
it('should render course fetch failure alert', async () => {
|
||||
render();
|
||||
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await axiosMock.onGet(courseApiLink).reply(404);
|
||||
await axiosMock.onGet(courseApiLinkV2).reply(404);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(tabMessages.courseTabErrorMessage.defaultMessage)).toBeVisible();
|
||||
@@ -186,7 +171,7 @@ describe('<TabsSection />', () => {
|
||||
render({ isPaginationCoursesEnabled: true });
|
||||
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await axiosMock.onGet(courseApiLinkV2).reply(200, generateGetStudioCoursesApiResponseV2());
|
||||
await executeThunk(fetchStudioHomeData('', true, {}, true), store.dispatch);
|
||||
await executeThunk(fetchStudioHomeData('', true, {}), store.dispatch);
|
||||
const data = generateGetStudioCoursesApiResponseV2();
|
||||
const coursesLength = data.results.courses.length;
|
||||
const totalItems = data.count;
|
||||
@@ -279,47 +264,9 @@ describe('<TabsSection />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('archived tab', () => {
|
||||
it('should switch to Archived tab and render specific archived course details', async () => {
|
||||
render();
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.archivedCourses = studioHomeMock.archivedCourses;
|
||||
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const archivedTab = await screen.findByText(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();
|
||||
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
await axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
await screen.findByRole('tab', { name: tabMessages.coursesTabTitle.defaultMessage });
|
||||
|
||||
await screen.findByRole('tab', { name: librariesBetaTabTitle });
|
||||
|
||||
await screen.findByRole('tab', { name: tabMessages.legacyLibrariesTabTitle.defaultMessage });
|
||||
|
||||
expect(screen.queryByRole('tab', { name: tabMessages.archivedTabTitle.defaultMessage })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('library tab', () => {
|
||||
beforeEach(async () => {
|
||||
await axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
await axiosMock.onGet(courseApiLinkV2).reply(200, generateGetStudioCoursesApiResponseV2());
|
||||
});
|
||||
it('should switch to Legacy Libraries tab and render specific v1 library details', async () => {
|
||||
render();
|
||||
@@ -395,7 +342,7 @@ describe('<TabsSection />', () => {
|
||||
expect(librariesTab).toHaveClass('active');
|
||||
|
||||
await screen.findByText('Showing 2 of 2');
|
||||
expect(screen.getByText('Page 1, Current Page, of 2')).toBeVisible();
|
||||
expect(screen.getAllByText('Page 1, Current Page, of 2')[0]).toBeVisible();
|
||||
|
||||
expect(screen.getByText(contentLibrariesListV2.results[0].title)).toBeVisible();
|
||||
expect(screen.getByText(
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Row } from '@openedx/paragon';
|
||||
import { Error } from '@openedx/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,
|
||||
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 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(
|
||||
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,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ArchivedTab);
|
||||
@@ -76,7 +76,7 @@ const CoursesFilters = ({
|
||||
const handleSearchCourses = (searchValueDebounced) => {
|
||||
const valueFormatted = searchValueDebounced.trim();
|
||||
const filterParams = {
|
||||
search: valueFormatted.length > 0 ? valueFormatted : undefined,
|
||||
search: valueFormatted.length > 0 ? valueFormatted : '',
|
||||
activeOnly,
|
||||
archivedOnly,
|
||||
order,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
@@ -10,6 +10,7 @@ import { studioHomeMock } from '../../__mocks__';
|
||||
import { initialState } from '../../factories/mockApiResponses';
|
||||
|
||||
import CoursesTab from '.';
|
||||
import { studioHomeCoursesRequestParamsDefault } from '../../data/slice';
|
||||
|
||||
const onClickNewCourse = jest.fn();
|
||||
const isShowProcessing = false;
|
||||
@@ -17,7 +18,6 @@ const isLoading = false;
|
||||
const isFailed = false;
|
||||
const numPages = 1;
|
||||
const coursesCount = studioHomeMock.courses.length;
|
||||
const isEnabledPagination = true;
|
||||
const showNewCourseContainer = true;
|
||||
|
||||
const renderComponent = (overrideProps = {}, studioHomeState = {}) => {
|
||||
@@ -33,24 +33,26 @@ const renderComponent = (overrideProps = {}, studioHomeState = {}) => {
|
||||
// Initialize the store with the custom initial state
|
||||
const store = initializeStore(customInitialState);
|
||||
|
||||
return render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CoursesTab
|
||||
coursesDataItems={studioHomeMock.courses}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
isShowProcessing={isShowProcessing}
|
||||
isLoading={isLoading}
|
||||
isFailed={isFailed}
|
||||
numPages={numPages}
|
||||
coursesCount={coursesCount}
|
||||
isEnabledPagination={isEnabledPagination}
|
||||
{...overrideProps}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
return {
|
||||
...render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CoursesTab
|
||||
coursesDataItems={studioHomeMock.courses}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
isShowProcessing={isShowProcessing}
|
||||
isLoading={isLoading}
|
||||
isFailed={isFailed}
|
||||
numPages={numPages}
|
||||
coursesCount={coursesCount}
|
||||
{...overrideProps}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
),
|
||||
store,
|
||||
};
|
||||
};
|
||||
|
||||
describe('<CoursesTab />', () => {
|
||||
@@ -77,18 +79,6 @@ describe('<CoursesTab />', () => {
|
||||
expect(coursesFilterSearchInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render pagination and filter elements when isEnabledPagination is false', () => {
|
||||
renderComponent({ isEnabledPagination: false });
|
||||
const coursesPaginationInfo = screen.queryByTestId('pagination-info');
|
||||
const coursesTypesMenu = screen.queryByTestId('dropdown-toggle-course-type-menu');
|
||||
const coursesOrderMenu = screen.queryByTestId('dropdown-toggle-courses-order-menu');
|
||||
const coursesFilterSearchInput = screen.queryByTestId('input-filter-courses-search');
|
||||
expect(coursesPaginationInfo).not.toBeInTheDocument();
|
||||
expect(coursesTypesMenu).not.toBeInTheDocument();
|
||||
expect(coursesOrderMenu).not.toBeInTheDocument();
|
||||
expect(coursesFilterSearchInput).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading spinner when isLoading is true and isFiltered is false', () => {
|
||||
const props = { isLoading: true, coursesDataItems: [] };
|
||||
const customStoreData = { studioHomeCoursesRequestParams: { currentPage: 1, isFiltered: true } };
|
||||
@@ -141,4 +131,17 @@ describe('<CoursesTab />', () => {
|
||||
const collapsibleStateWithAction = screen.queryByTestId('collapsible-state-with-action');
|
||||
expect(collapsibleStateWithAction).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should reset filters when in pressed the button to clean them', () => {
|
||||
const props = { isLoading: false, coursesDataItems: [] };
|
||||
const customStoreData = { studioHomeCoursesRequestParams: { isFiltered: true } };
|
||||
const { store } = renderComponent(props, customStoreData);
|
||||
const cleanFiltersButton = screen.getByRole('button', { name: /clear filters/i });
|
||||
expect(cleanFiltersButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(cleanFiltersButton!);
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.studioHome.studioHomeCoursesRequestParams).toStrictEqual(studioHomeCoursesRequestParamsDefault);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Error } from '@openedx/paragon/icons';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../../constants';
|
||||
import { getStudioHomeData, getStudioHomeCoursesParams } from '../../data/selectors';
|
||||
import { updateStudioHomeCoursesCustomParams } from '../../data/slice';
|
||||
import { resetStudioHomeCoursesCustomParams, updateStudioHomeCoursesCustomParams } from '../../data/slice';
|
||||
import { fetchStudioHomeData } from '../../data/thunks';
|
||||
import CardItem from '../../card-item';
|
||||
import CollapsibleStateWithAction from '../../collapsible-state-with-action';
|
||||
@@ -43,7 +43,6 @@ interface Props {
|
||||
isFailed: boolean;
|
||||
numPages: number;
|
||||
coursesCount: number;
|
||||
isEnabledPagination?: boolean;
|
||||
}
|
||||
|
||||
const CoursesTab: React.FC<Props> = ({
|
||||
@@ -55,7 +54,6 @@ const CoursesTab: React.FC<Props> = ({
|
||||
isFailed,
|
||||
numPages = 0,
|
||||
coursesCount = 0,
|
||||
isEnabledPagination = false,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
@@ -89,23 +87,13 @@ const CoursesTab: React.FC<Props> = ({
|
||||
activeOnly,
|
||||
};
|
||||
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page, ...customParams }, true));
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page, ...customParams }));
|
||||
dispatch(updateStudioHomeCoursesCustomParams({ currentPage: page, isFiltered: true }));
|
||||
};
|
||||
|
||||
const handleCleanFilters = () => {
|
||||
const customParams = {
|
||||
currentPage: 1,
|
||||
search: undefined,
|
||||
order: 'display_name',
|
||||
isFiltered: true,
|
||||
cleanFilters: true,
|
||||
archivedOnly: undefined,
|
||||
activeOnly: undefined,
|
||||
};
|
||||
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page: 1, order: 'display_name' }, true));
|
||||
dispatch(updateStudioHomeCoursesCustomParams(customParams));
|
||||
dispatch(resetStudioHomeCoursesCustomParams());
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page: 1, order: 'display_name' }));
|
||||
};
|
||||
|
||||
const isNotFilteringCourses = !isFiltered && !isLoading;
|
||||
@@ -132,18 +120,16 @@ const CoursesTab: React.FC<Props> = ({
|
||||
/>
|
||||
) : (
|
||||
<div className="courses-tab-container">
|
||||
{isShowProcessing && !isEnabledPagination && <ProcessingCourses />}
|
||||
{isEnabledPagination && (
|
||||
<div className="d-flex flex-row justify-content-between my-4">
|
||||
<CoursesFilters dispatch={dispatch} locationValue={locationValue} isLoading={isLoading} />
|
||||
<p data-testid="pagination-info">
|
||||
{intl.formatMessage(messages.coursesPaginationInfo, {
|
||||
length: coursesDataItems.length,
|
||||
total: coursesCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex flex-row align-items-center justify-content-between my-4">
|
||||
{isShowProcessing && <ProcessingCourses />}
|
||||
<CoursesFilters dispatch={dispatch} locationValue={locationValue} isLoading={isLoading} />
|
||||
<p data-testid="pagination-info" className="my-0">
|
||||
{intl.formatMessage(messages.coursesPaginationInfo, {
|
||||
length: coursesDataItems.length,
|
||||
total: coursesCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{hasCourses ? (
|
||||
<>
|
||||
{coursesDataItems.map(
|
||||
@@ -167,12 +153,11 @@ const CoursesTab: React.FC<Props> = ({
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
isPaginated={isEnabledPagination}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{numPages > 1 && isEnabledPagination && (
|
||||
{numPages > 1 && (
|
||||
<Pagination
|
||||
className="d-flex justify-content-center"
|
||||
paginationLabel="pagination navigation"
|
||||
|
||||
@@ -15,7 +15,6 @@ import { getLoadingStatuses, getStudioHomeData } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
import LibrariesTab from './libraries-tab';
|
||||
import LibrariesV2Tab from './libraries-v2-tab/index';
|
||||
import ArchivedTab from './archived-tab';
|
||||
import CoursesTab from './courses-tab';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { fetchLibraryData } from '../data/thunks';
|
||||
@@ -24,7 +23,6 @@ const TabsSection = ({
|
||||
showNewCourseContainer,
|
||||
onClickNewCourse,
|
||||
isShowProcessing,
|
||||
isPaginationCoursesEnabled,
|
||||
librariesV1Enabled,
|
||||
librariesV2Enabled,
|
||||
}) => {
|
||||
@@ -68,7 +66,7 @@ const TabsSection = ({
|
||||
}, [pathname]);
|
||||
|
||||
const {
|
||||
courses, libraries, archivedCourses,
|
||||
courses, libraries,
|
||||
numPages, coursesCount,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const {
|
||||
@@ -99,27 +97,10 @@ const TabsSection = ({
|
||||
isFailed={isFailedCoursesPage}
|
||||
numPages={numPages}
|
||||
coursesCount={coursesCount}
|
||||
isEnabledPagination={isPaginationCoursesEnabled}
|
||||
/>
|
||||
</Tab>,
|
||||
);
|
||||
|
||||
if (archivedCourses?.length) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
key={TABS_LIST.archived}
|
||||
eventKey={TABS_LIST.archived}
|
||||
title={intl.formatMessage(messages.archivedTabTitle)}
|
||||
>
|
||||
<ArchivedTab
|
||||
archivedCoursesData={archivedCourses}
|
||||
isLoading={isLoadingCourses}
|
||||
isFailed={isFailedCoursesPage}
|
||||
/>
|
||||
</Tab>,
|
||||
);
|
||||
}
|
||||
|
||||
if (librariesV2Enabled) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
@@ -168,7 +149,7 @@ const TabsSection = ({
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}, [archivedCourses, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]);
|
||||
}, [showNewCourseContainer, isLoadingCourses, isLoadingLibraries]);
|
||||
|
||||
const handleSelectTab = (tab) => {
|
||||
if (tab === TABS_LIST.courses) {
|
||||
@@ -196,15 +177,10 @@ const TabsSection = ({
|
||||
);
|
||||
};
|
||||
|
||||
TabsSection.defaultProps = {
|
||||
isPaginationCoursesEnabled: false,
|
||||
};
|
||||
|
||||
TabsSection.propTypes = {
|
||||
showNewCourseContainer: PropTypes.bool.isRequired,
|
||||
onClickNewCourse: PropTypes.func.isRequired,
|
||||
isShowProcessing: PropTypes.bool.isRequired,
|
||||
isPaginationCoursesEnabled: PropTypes.bool,
|
||||
librariesV1Enabled: PropTypes.bool,
|
||||
librariesV2Enabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user