feat: pagination studio home for courses (#825)
This PR adds pagination for the studio home view and makes minor changes to each course card. NOTE: This needs to be activated by the environment variable ENABLE_HOME_PAGE_COURSE_API_V2 otherwise, it will continue using the old course list enable this feature flag new_studio_mfe.use_new_home_page * feat: pagination studio home for courses * chore: addressing some comments * refactor: addressing pr comments * test: adding test for studio home slice * chore: deleting unnecessary blank line * feat: adding feature for pagination * refactor: change customParams to requestParams * fix: linter problems * fix: course home number of 0 courses * chore: update feature name for pagination * fix: pagination enabled request and test for tab section added again * chore: removing cms link in course card items * chore: addresing some comments * fix: array dependency for pagination
This commit is contained in:
1
.env
1
.env
@@ -41,4 +41,5 @@ HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=false
|
||||
INVITE_STUDENTS_EMAIL_TO=''
|
||||
AI_TRANSLATIONS_BASE_URL=''
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=''
|
||||
|
||||
@@ -43,4 +43,5 @@ HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=true
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
|
||||
@@ -35,4 +35,5 @@ ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
|
||||
@@ -125,6 +125,7 @@ initialize({
|
||||
ENABLE_ASSETS_PAGE: process.env.ENABLE_ASSETS_PAGE || 'false',
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN || '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',
|
||||
}, 'CourseAuthoringConfig');
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useStudioHome } from './hooks';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
|
||||
const StudioHome = ({ intl }) => {
|
||||
const isPaginationCoursesEnabled = getConfig().ENABLE_HOME_PAGE_COURSE_API_V2;
|
||||
const {
|
||||
isLoadingPage,
|
||||
isFailedLoadingPage,
|
||||
@@ -39,7 +40,7 @@ const StudioHome = ({ intl }) => {
|
||||
hasAbilityToCreateNewCourse,
|
||||
setShowNewCourseContainer,
|
||||
dispatch,
|
||||
} = useStudioHome();
|
||||
} = useStudioHome(isPaginationCoursesEnabled);
|
||||
|
||||
const {
|
||||
userIsActive,
|
||||
@@ -139,6 +140,7 @@ const StudioHome = ({ intl }) => {
|
||||
onClickNewCourse={() => setShowNewCourseContainer(true)}
|
||||
isShowProcessing={isShowProcessing}
|
||||
dispatch={dispatch}
|
||||
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
|
||||
/>
|
||||
</section>
|
||||
</Layout.Element>
|
||||
|
||||
@@ -12,6 +12,7 @@ module.exports = {
|
||||
rerunLink: '/course_rerun/course-v1:MachineLearning+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:MachineLearning+123+2023',
|
||||
cmsLink: '//localhost:18010/courses/course-v1:MachineLearning+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'course-v1:Design+123+e.g.2025',
|
||||
@@ -22,6 +23,7 @@ module.exports = {
|
||||
rerunLink: '/course_rerun/course-v1:Design+123+e.g.2025',
|
||||
run: 'e.g.2025',
|
||||
url: '/course/course-v1:Design+123+e.g.2025',
|
||||
cmsLink: '//localhost:18010/courses/course-v1:Design+123+e.g.2025',
|
||||
},
|
||||
],
|
||||
canCreateOrganizations: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
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';
|
||||
@@ -43,6 +43,7 @@ describe('<CardItem />', () => {
|
||||
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} />);
|
||||
@@ -53,6 +54,19 @@ describe('<CardItem />', () => {
|
||||
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', 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} />);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, Card, Hyperlink } from '@openedx/paragon';
|
||||
import {
|
||||
Card,
|
||||
Hyperlink,
|
||||
Dropdown,
|
||||
IconButton,
|
||||
ActionRow,
|
||||
} from '@openedx/paragon';
|
||||
import { MoreHoriz } from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
@@ -10,7 +17,17 @@ import { getStudioHomeData } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
|
||||
const CardItem = ({
|
||||
intl, displayName, lmsLink, rerunLink, org, number, run, isLibraries, url,
|
||||
intl,
|
||||
displayName,
|
||||
lmsLink,
|
||||
rerunLink,
|
||||
org,
|
||||
number,
|
||||
run,
|
||||
isLibraries,
|
||||
courseKey,
|
||||
isPaginated,
|
||||
url,
|
||||
}) => {
|
||||
const {
|
||||
allowCourseReruns,
|
||||
@@ -41,16 +58,45 @@ const CardItem = ({
|
||||
)}
|
||||
subtitle={subtitle}
|
||||
actions={showActions && (
|
||||
<ActionRow>
|
||||
{isShowRerunLink && (
|
||||
<Hyperlink className="small" destination={rerunLink}>
|
||||
{intl.formatMessage(messages.btnReRunText)}
|
||||
isPaginated ? (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
as={IconButton}
|
||||
iconAs={MoreHoriz}
|
||||
variant="primary"
|
||||
data-testid="toggle-dropdown"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
{isShowRerunLink && (
|
||||
<Dropdown.Item href={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={rerunLink}
|
||||
key={`action-row-rerunLink-${courseKey}`}
|
||||
>
|
||||
{intl.formatMessage(messages.btnReRunText)}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<Hyperlink
|
||||
className="small ml-3"
|
||||
destination={lmsLink}
|
||||
key={`action-row-lmsLink-${courseKey}`}
|
||||
>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<Hyperlink className="small ml-3" destination={lmsLink}>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
</Hyperlink>
|
||||
</ActionRow>
|
||||
</ActionRow>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
@@ -59,6 +105,8 @@ const CardItem = ({
|
||||
|
||||
CardItem.defaultProps = {
|
||||
isLibraries: false,
|
||||
isPaginated: false,
|
||||
courseKey: '',
|
||||
rerunLink: '',
|
||||
lmsLink: '',
|
||||
run: '',
|
||||
@@ -74,6 +122,8 @@ CardItem.propTypes = {
|
||||
number: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
isLibraries: PropTypes.bool,
|
||||
courseKey: PropTypes.string,
|
||||
isPaginated: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(CardItem);
|
||||
|
||||
@@ -20,6 +20,19 @@ export async function getStudioHomeCourses(search) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
/**
|
||||
* Get's studio home courses.
|
||||
* @param {string} search - Query string parameters for filtering the courses.
|
||||
* @param {object} customParams - Additional custom parameters for the API request.
|
||||
* @returns {Promise<Object>} - A Promise that resolves to the response data containing the studio home courses.
|
||||
* Note: We are changing /api/contentstore/v1 to /api/contentstore/v2 due to upcoming breaking changes.
|
||||
* Features such as pagination, filtering, and ordering are better handled in the new version.
|
||||
* Please refer to this PR for further details: https://github.com/openedx/edx-platform/pull/34173
|
||||
*/
|
||||
export async function getStudioHomeCoursesV2(search, customParams) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParams });
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
export async function getStudioHomeLibraries() {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/libraries`);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sendRequestForCourseCreator,
|
||||
getApiBaseUrl,
|
||||
getStudioHomeCourses,
|
||||
getStudioHomeCoursesV2,
|
||||
getStudioHomeLibraries,
|
||||
} from './api';
|
||||
import { generateGetStudioCoursesApiResponse, generateGetStudioHomeDataApiResponse, generateGetStuioHomeLibrariesApiResponse } from '../factories/mockApiResponses';
|
||||
@@ -43,7 +44,7 @@ describe('studio-home api calls', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
fit('should get studio courses data', async () => {
|
||||
it('should get studio courses data', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
const result = await getStudioHomeCourses('');
|
||||
@@ -53,6 +54,16 @@ describe('studio-home api calls', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should get studio courses data v2', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
const result = await getStudioHomeCoursesV2('');
|
||||
const expected = generateGetStudioCoursesApiResponse();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(apiLink);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should get studio libraries data', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStuioHomeLibrariesApiResponse());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export const getStudioHomeData = state => state.studioHome.studioHomeData;
|
||||
export const getLoadingStatuses = (state) => state.studioHome.loadingStatuses;
|
||||
export const getSavingStatuses = (state) => state.studioHome.savingStatuses;
|
||||
export const getStudioHomeCoursesParams = (state) => state.studioHome.studioHomeCoursesRequestParams;
|
||||
|
||||
@@ -17,6 +17,9 @@ const slice = createSlice({
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
updateLoadingStatuses: (state, { payload }) => {
|
||||
@@ -34,10 +37,23 @@ const slice = createSlice({
|
||||
state.studioHomeData.archivedCourses = archivedCourses;
|
||||
state.studioHomeData.inProcessCourseActions = inProcessCourseActions;
|
||||
},
|
||||
fetchCourseDataSuccessV2: (state, { payload }) => {
|
||||
const { courses, archivedCourses = [], inProcessCourseActions } = payload.results;
|
||||
const { numPages, count } = payload;
|
||||
state.studioHomeData.courses = courses;
|
||||
state.studioHomeData.archivedCourses = archivedCourses;
|
||||
state.studioHomeData.inProcessCourseActions = inProcessCourseActions;
|
||||
state.studioHomeData.numPages = numPages;
|
||||
state.studioHomeData.coursesCount = count;
|
||||
},
|
||||
fetchLibraryDataSuccess: (state, { payload }) => {
|
||||
const { libraries } = payload;
|
||||
state.studioHomeData.libraries = libraries;
|
||||
},
|
||||
updateStudioHomeCoursesCustomParams: (state, { payload }) => {
|
||||
const { currentPage } = payload;
|
||||
state.studioHomeCoursesRequestParams.currentPage = currentPage;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,7 +62,9 @@ export const {
|
||||
updateLoadingStatuses,
|
||||
fetchStudioHomeDataSuccess,
|
||||
fetchCourseDataSuccess,
|
||||
fetchCourseDataSuccessV2,
|
||||
fetchLibraryDataSuccess,
|
||||
updateStudioHomeCoursesCustomParams,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
42
src/studio-home/data/slice.test.js
Normal file
42
src/studio-home/data/slice.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { reducer, updateStudioHomeCoursesCustomParams } from './slice'; // Assuming the file is named slice.js
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
describe('updateStudioHomeCoursesCustomParams action', () => {
|
||||
const initialState = {
|
||||
loadingStatuses: {
|
||||
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseNotificationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
libraryLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
savingStatuses: {
|
||||
courseCreatorSavingStatus: '',
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
},
|
||||
};
|
||||
|
||||
it('should return the initial state', () => {
|
||||
const result = reducer(undefined, { type: undefined });
|
||||
expect(result).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should update the currentPage in studioHomeCoursesRequestParams', () => {
|
||||
const newState = {
|
||||
...initialState,
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 2,
|
||||
},
|
||||
};
|
||||
const payload = {
|
||||
currentPage: 2,
|
||||
};
|
||||
|
||||
const result = reducer(initialState, updateStudioHomeCoursesCustomParams(payload));
|
||||
expect(result).toEqual(newState);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
handleCourseNotification,
|
||||
getStudioHomeCourses,
|
||||
getStudioHomeLibraries,
|
||||
getStudioHomeCoursesV2,
|
||||
} from './api';
|
||||
import {
|
||||
fetchStudioHomeDataSuccess,
|
||||
@@ -12,9 +13,10 @@ import {
|
||||
updateLoadingStatuses,
|
||||
updateSavingStatuses,
|
||||
fetchLibraryDataSuccess,
|
||||
fetchCourseDataSuccessV2,
|
||||
} from './slice';
|
||||
|
||||
function fetchStudioHomeData(search, hasHomeData) {
|
||||
function fetchStudioHomeData(search, hasHomeData, requestParams = {}, isPaginationEnabled = false) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
@@ -30,8 +32,14 @@ function fetchStudioHomeData(search, hasHomeData) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
const coursesData = await getStudioHomeCourses(search || '');
|
||||
dispatch(fetchCourseDataSuccess(coursesData));
|
||||
if (isPaginationEnabled) {
|
||||
const coursesData = await getStudioHomeCoursesV2(search || '', requestParams);
|
||||
dispatch(fetchCourseDataSuccessV2(coursesData));
|
||||
} else {
|
||||
const coursesData = await getStudioHomeCourses(search || '');
|
||||
dispatch(fetchCourseDataSuccess(coursesData));
|
||||
}
|
||||
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.FAILED }));
|
||||
|
||||
@@ -15,6 +15,9 @@ export const initialState = {
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -93,6 +96,38 @@ export const generateGetStudioCoursesApiResponse = () => ({
|
||||
inProcessCourseActions: [],
|
||||
});
|
||||
|
||||
export const generateGetStudioCoursesApiResponseV2 = () => ({
|
||||
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: [],
|
||||
},
|
||||
});
|
||||
|
||||
export const generateGetStuioHomeLibrariesApiResponse = () => ({
|
||||
libraries: [
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from './data/selectors';
|
||||
import { updateSavingStatuses } from './data/slice';
|
||||
|
||||
const useStudioHome = () => {
|
||||
const useStudioHome = (isPaginated = false) => {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const studioHomeData = useSelector(getStudioHomeData);
|
||||
@@ -29,10 +29,19 @@ const useStudioHome = () => {
|
||||
const isFailedLoadingPage = studioHomeLoadingStatus === RequestStatus.FAILED;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStudioHomeData(location.search ?? ''));
|
||||
setShowNewCourseContainer(false);
|
||||
if (!isPaginated) {
|
||||
dispatch(fetchStudioHomeData(location.search ?? ''));
|
||||
setShowNewCourseContainer(false);
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaginated) {
|
||||
const firstPage = 1;
|
||||
dispatch(fetchStudioHomeData(location.search ?? '', false, { page: firstPage }, true));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (courseCreatorSavingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: '' }));
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
initialState,
|
||||
generateGetStudioHomeDataApiResponse,
|
||||
generateGetStudioCoursesApiResponse,
|
||||
generateGetStudioCoursesApiResponseV2,
|
||||
generateGetStuioHomeLibrariesApiResponse,
|
||||
} from '../factories/mockApiResponses';
|
||||
import { getApiBaseUrl, getStudioHomeApiUrl } from '../data/api';
|
||||
@@ -28,12 +29,20 @@ 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`;
|
||||
|
||||
const RootWrapper = () => (
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const RootWrapper = (overrideProps) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TabsSection intl={{ formatMessage: jest.fn() }} dispatch={jest.fn()} />
|
||||
<TabsSection
|
||||
intl={{ formatMessage: jest.fn() }}
|
||||
dispatch={mockDispatch}
|
||||
isPaginationCoursesEnabled={false}
|
||||
{...overrideProps}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
@@ -116,6 +125,36 @@ describe('<TabsSection />', () => {
|
||||
|
||||
expect(screen.getByText(tabMessages.courseTabErrorMessage.defaultMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render pagination when there are courses', async () => {
|
||||
render(<RootWrapper isPaginationCoursesEnabled />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLinkV2).reply(200, generateGetStudioCoursesApiResponseV2());
|
||||
await executeThunk(fetchStudioHomeData('', true, {}, true), store.dispatch);
|
||||
const data = generateGetStudioCoursesApiResponseV2();
|
||||
const coursesLength = data.results.courses.length;
|
||||
const totalItems = data.count;
|
||||
const paginationInfoText = `Showing ${coursesLength} of ${totalItems}`;
|
||||
|
||||
expect(screen.getByText(studioHomeMock.courses[0].displayName)).toBeVisible();
|
||||
|
||||
const pagination = screen.getByRole('navigation');
|
||||
const paginationInfo = screen.getByTestId('pagination-info');
|
||||
expect(paginationInfo.textContent).toContain(paginationInfoText);
|
||||
expect(pagination).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not render pagination when there are not courses', async () => {
|
||||
const data = generateGetStudioCoursesApiResponseV2();
|
||||
data.results.courses = [];
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLinkV2).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const pagination = screen.queryByRole('navigation');
|
||||
expect(pagination).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('archived tab', () => {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Row } from '@openedx/paragon';
|
||||
import { Icon, Row, Pagination } from '@openedx/paragon';
|
||||
import { Error } from '@openedx/paragon/icons';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../../constants';
|
||||
import { getStudioHomeData } from '../../data/selectors';
|
||||
import { getStudioHomeData, getStudioHomeCoursesParams } from '../../data/selectors';
|
||||
import { updateStudioHomeCoursesCustomParams } from '../../data/slice';
|
||||
import { fetchStudioHomeData } from '../../data/thunks';
|
||||
import CardItem from '../../card-item';
|
||||
import CollapsibleStateWithAction from '../../collapsible-state-with-action';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
@@ -23,12 +26,18 @@ const CoursesTab = ({
|
||||
isShowProcessing,
|
||||
isLoading,
|
||||
isFailed,
|
||||
dispatch,
|
||||
numPages,
|
||||
coursesCount,
|
||||
isEnabledPagination,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const {
|
||||
courseCreatorStatus,
|
||||
optimizationEnabled,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const { currentPage } = useSelector(getStudioHomeCoursesParams);
|
||||
const hasAbilityToCreateCourse = courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const showCollapsible = [
|
||||
COURSE_CREATOR_STATES.denied,
|
||||
@@ -36,6 +45,12 @@ const CoursesTab = ({
|
||||
COURSE_CREATOR_STATES.unrequested,
|
||||
].includes(courseCreatorStatus);
|
||||
|
||||
const handlePageSelected = (page) => {
|
||||
dispatch(fetchStudioHomeData(location.search ?? '', false, { page }, true));
|
||||
dispatch(updateStudioHomeCoursesCustomParams({ currentPage: page }));
|
||||
};
|
||||
const hasCourses = coursesDataItems?.length > 0;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
@@ -58,30 +73,55 @@ const CoursesTab = ({
|
||||
) : (
|
||||
<>
|
||||
{isShowProcessing && <ProcessingCourses />}
|
||||
{coursesDataItems?.length ? (
|
||||
sortAlphabeticallyArray(coursesDataItems).map(
|
||||
({
|
||||
courseKey,
|
||||
displayName,
|
||||
lmsLink,
|
||||
org,
|
||||
rerunLink,
|
||||
number,
|
||||
run,
|
||||
url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
{hasCourses && isEnabledPagination && (
|
||||
<div className="d-flex justify-content-end">
|
||||
<p data-testid="pagination-info">
|
||||
{intl.formatMessage(messages.coursesPaginationInfo, {
|
||||
length: coursesDataItems.length,
|
||||
total: coursesCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{hasCourses ? (
|
||||
<>
|
||||
{sortAlphabeticallyArray(coursesDataItems).map(
|
||||
({
|
||||
courseKey,
|
||||
displayName,
|
||||
lmsLink,
|
||||
org,
|
||||
rerunLink,
|
||||
number,
|
||||
run,
|
||||
url,
|
||||
cmsLink,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
cmsLink={cmsLink}
|
||||
isPaginated={isEnabledPagination}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{numPages > 1 && isEnabledPagination && (
|
||||
<Pagination
|
||||
className="d-flex justify-content-center"
|
||||
paginationLabel="pagination navigation"
|
||||
pageCount={numPages}
|
||||
currentPage={currentPage}
|
||||
onPageSelect={handlePageSelected}
|
||||
/>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (!optimizationEnabled && (
|
||||
<ContactAdministrator
|
||||
hasAbilityToCreateCourse={hasAbilityToCreateCourse}
|
||||
@@ -101,6 +141,12 @@ const CoursesTab = ({
|
||||
);
|
||||
};
|
||||
|
||||
CoursesTab.defaultProps = {
|
||||
numPages: 0,
|
||||
coursesCount: 0,
|
||||
isEnabledPagination: false,
|
||||
};
|
||||
|
||||
CoursesTab.propTypes = {
|
||||
coursesDataItems: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
@@ -119,6 +165,10 @@ CoursesTab.propTypes = {
|
||||
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;
|
||||
|
||||
@@ -13,7 +13,12 @@ import { RequestStatus } from '../../data/constants';
|
||||
import { fetchLibraryData } from '../data/thunks';
|
||||
|
||||
const TabsSection = ({
|
||||
intl, showNewCourseContainer, onClickNewCourse, isShowProcessing, dispatch,
|
||||
intl,
|
||||
showNewCourseContainer,
|
||||
onClickNewCourse,
|
||||
isShowProcessing,
|
||||
dispatch,
|
||||
isPaginationCoursesEnabled,
|
||||
}) => {
|
||||
const TABS_LIST = {
|
||||
courses: 'courses',
|
||||
@@ -25,6 +30,7 @@ const TabsSection = ({
|
||||
libraryAuthoringMfeUrl,
|
||||
redirectToLibraryAuthoringMfe,
|
||||
courses, librariesEnabled, libraries, archivedCourses,
|
||||
numPages, coursesCount,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const {
|
||||
courseLoadingStatus,
|
||||
@@ -52,6 +58,10 @@ const TabsSection = ({
|
||||
isShowProcessing={isShowProcessing}
|
||||
isLoading={isLoadingCourses}
|
||||
isFailed={isFailedCoursesPage}
|
||||
dispatch={dispatch}
|
||||
numPages={numPages}
|
||||
coursesCount={coursesCount}
|
||||
isEnabledPagination={isPaginationCoursesEnabled}
|
||||
/>
|
||||
</Tab>,
|
||||
);
|
||||
@@ -114,12 +124,17 @@ const TabsSection = ({
|
||||
);
|
||||
};
|
||||
|
||||
TabsSection.defaultProps = {
|
||||
isPaginationCoursesEnabled: false,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -9,6 +9,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.studio-home.courses.tab.error.message',
|
||||
defaultMessage: 'Failed to fetch courses. Please try again later.',
|
||||
},
|
||||
coursesPaginationInfo: {
|
||||
id: 'course-authoring.studio-home.courses.pagination.info',
|
||||
defaultMessage: 'Showing {length} of {total}',
|
||||
},
|
||||
librariesTabErrorMessage: {
|
||||
id: 'course-authoring.studio-home.libraries.tab.error.message',
|
||||
defaultMessage: 'Failed to fetch libraries. Please try again later.',
|
||||
|
||||
Reference in New Issue
Block a user