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:
Brayan Cerón
2025-05-30 16:31:47 -05:00
committed by GitHub
parent f02347dd71
commit 5bb8a5d47c
19 changed files with 149 additions and 370 deletions

1
.env
View File

@@ -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

View File

@@ -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

View File

@@ -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(','),

View File

@@ -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}
/>

View File

@@ -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)}`);

View File

@@ -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>

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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);
});
});

View File

@@ -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 }));

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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',

View File

@@ -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(

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);
});
});

View File

@@ -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"

View File

@@ -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,
};