diff --git a/.env b/.env index d5de96d5e..61c8cb9e2 100644 --- a/.env +++ b/.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='' diff --git a/.env.development b/.env.development index 2ea52ee9f..b8d837c53 100644 --- a/.env.development +++ b/.env.development @@ -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 diff --git a/.env.test b/.env.test index d92d12aef..cbdb4821c 100644 --- a/.env.test +++ b/.env.test @@ -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 diff --git a/src/index.jsx b/src/index.jsx index 7a9d89057..f717df4c0 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -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'); }, diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 9e7eb8ba8..0b2d5b39d 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -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} /> diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index 77824e564..0de8903c8 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -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, diff --git a/src/studio-home/card-item/CardItem.test.jsx b/src/studio-home/card-item/CardItem.test.jsx index 71c34f30f..20c4669ab 100644 --- a/src/studio-home/card-item/CardItem.test.jsx +++ b/src/studio-home/card-item/CardItem.test.jsx @@ -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('', () => { const { getByText } = render(); 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(); @@ -53,6 +54,19 @@ describe('', () => { 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(); + 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(); diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx index 4c0637e31..fbffd2072 100644 --- a/src/studio-home/card-item/index.jsx +++ b/src/studio-home/card-item/index.jsx @@ -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 && ( - - {isShowRerunLink && ( - - {intl.formatMessage(messages.btnReRunText)} + isPaginated ? ( + + + + {isShowRerunLink && ( + + {messages.btnReRunText.defaultMessage} + + )} + + {intl.formatMessage(messages.viewLiveBtnText)} + + + + ) : ( + + {isShowRerunLink && ( + + {intl.formatMessage(messages.btnReRunText)} + + )} + + {intl.formatMessage(messages.viewLiveBtnText)} - )} - - {intl.formatMessage(messages.viewLiveBtnText)} - - + + ) )} /> @@ -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); diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index 546a3d5b0..e94d5f3f6 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -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} - 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`); diff --git a/src/studio-home/data/api.test.js b/src/studio-home/data/api.test.js index 4c0cfff23..593a2730d 100644 --- a/src/studio-home/data/api.test.js +++ b/src/studio-home/data/api.test.js @@ -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()); diff --git a/src/studio-home/data/selectors.js b/src/studio-home/data/selectors.js index 62957e58a..186673eda 100644 --- a/src/studio-home/data/selectors.js +++ b/src/studio-home/data/selectors.js @@ -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; diff --git a/src/studio-home/data/slice.js b/src/studio-home/data/slice.js index 02c60dbea..b7e9f136c 100644 --- a/src/studio-home/data/slice.js +++ b/src/studio-home/data/slice.js @@ -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 { diff --git a/src/studio-home/data/slice.test.js b/src/studio-home/data/slice.test.js new file mode 100644 index 000000000..3e7ccda6a --- /dev/null +++ b/src/studio-home/data/slice.test.js @@ -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); + }); +}); diff --git a/src/studio-home/data/thunks.js b/src/studio-home/data/thunks.js index aca8f7ef1..495a726aa 100644 --- a/src/studio-home/data/thunks.js +++ b/src/studio-home/data/thunks.js @@ -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 })); diff --git a/src/studio-home/factories/mockApiResponses.jsx b/src/studio-home/factories/mockApiResponses.jsx index 9ef672ffd..b292c28a5 100644 --- a/src/studio-home/factories/mockApiResponses.jsx +++ b/src/studio-home/factories/mockApiResponses.jsx @@ -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: [ { diff --git a/src/studio-home/hooks.jsx b/src/studio-home/hooks.jsx index cfc45f972..c582f0ae1 100644 --- a/src/studio-home/hooks.jsx +++ b/src/studio-home/hooks.jsx @@ -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: '' })); diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index cbb29c40b..73b2205e8 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -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) => ( - + ); @@ -116,6 +125,36 @@ describe('', () => { expect(screen.getByText(tabMessages.courseTabErrorMessage.defaultMessage)).toBeVisible(); }); + + it('should render pagination when there are courses', async () => { + render(); + 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(); + 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', () => { diff --git a/src/studio-home/tabs-section/courses-tab/index.jsx b/src/studio-home/tabs-section/courses-tab/index.jsx index b84d1dff7..b76e88702 100644 --- a/src/studio-home/tabs-section/courses-tab/index.jsx +++ b/src/studio-home/tabs-section/courses-tab/index.jsx @@ -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 ( @@ -58,30 +73,55 @@ const CoursesTab = ({ ) : ( <> {isShowProcessing && } - {coursesDataItems?.length ? ( - sortAlphabeticallyArray(coursesDataItems).map( - ({ - courseKey, - displayName, - lmsLink, - org, - rerunLink, - number, - run, - url, - }) => ( - +

+ {intl.formatMessage(messages.coursesPaginationInfo, { + length: coursesDataItems.length, + total: coursesCount, + })} +

+ + )} + {hasCourses ? ( + <> + {sortAlphabeticallyArray(coursesDataItems).map( + ({ + courseKey, + displayName, + lmsLink, + org, + rerunLink, + number, + run, + url, + cmsLink, + }) => ( + + ), + )} + + {numPages > 1 && isEnabledPagination && ( + - ), - ) + )} + ) : (!optimizationEnabled && ( { 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} /> , ); @@ -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); diff --git a/src/studio-home/tabs-section/messages.js b/src/studio-home/tabs-section/messages.js index 75e945cef..05e8d0fce 100644 --- a/src/studio-home/tabs-section/messages.js +++ b/src/studio-home/tabs-section/messages.js @@ -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.',