From f31cae24ea25ccf19f5a84bfa52dfbdfb7170aaf Mon Sep 17 00:00:00 2001 From: Jhon Vente Date: Fri, 23 Feb 2024 07:26:27 -0500 Subject: [PATCH] feat: adding feature for pagination --- .env | 1 + .env.development | 1 + .env.test | 1 + src/index.jsx | 1 + src/studio-home/StudioHome.jsx | 3 + src/studio-home/card-item/CardItem.test.jsx | 14 +++- src/studio-home/card-item/index.jsx | 64 +++++++++++++------ src/studio-home/data/api.js | 6 +- src/studio-home/data/api.test.js | 13 +++- src/studio-home/data/slice.js | 7 ++ src/studio-home/data/thunks.js | 14 +++- .../factories/mockApiResponses.jsx | 48 ++++++++++++++ src/studio-home/hooks.jsx | 2 +- .../tabs-section/TabsSection.test.jsx | 27 +++++--- .../tabs-section/courses-tab/index.jsx | 17 +++-- src/studio-home/tabs-section/index.jsx | 13 +++- 16 files changed, 192 insertions(+), 40 deletions(-) diff --git a/.env b/.env index d0bb7ec0d..c99c6c43d 100644 --- a/.env +++ b/.env @@ -40,3 +40,4 @@ HOTJAR_VERSION=6 HOTJAR_DEBUG=false INVITE_STUDENTS_EMAIL_TO='' AI_TRANSLATIONS_BASE_URL='' +ENABLE_PAGINATION_COURSES_STUDIO_HOME='true' diff --git a/.env.development b/.env.development index 045c52f2d..e62534fe7 100644 --- a/.env.development +++ b/.env.development @@ -42,3 +42,4 @@ HOTJAR_VERSION=6 HOTJAR_DEBUG=true INVITE_STUDENTS_EMAIL_TO="someone@domain.com" AI_TRANSLATIONS_BASE_URL='http://localhost:18760' +ENABLE_PAGINATION_COURSES_STUDIO_HOME='true' diff --git a/.env.test b/.env.test index 67ad2994b..efec9adae 100644 --- a/.env.test +++ b/.env.test @@ -34,3 +34,4 @@ 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_PAGINATION_COURSES_STUDIO_HOME='true' diff --git a/src/index.jsx b/src/index.jsx index 39c682cb6..525b5e2c7 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -119,6 +119,7 @@ initialize({ ENABLE_UNIT_PAGE: process.env.ENABLE_UNIT_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_PAGINATION_COURSES_STUDIO_HOME: process.env.ENABLE_PAGINATION_COURSES_STUDIO_HOME || 'false', }, 'CourseAuthoringConfig'); }, }, diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 9e7eb8ba8..a06309ddd 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -49,6 +49,8 @@ const StudioHome = ({ intl }) => { redirectToLibraryAuthoringMfe, } = studioHomeData; + const isPaginationCoursesEnabled = getConfig().ENABLE_PAGINATION_COURSES_STUDIO_HOME === 'true'; + function getHeaderButtons() { const headerButtons = []; @@ -139,6 +141,7 @@ const StudioHome = ({ intl }) => { onClickNewCourse={() => setShowNewCourseContainer(true)} isShowProcessing={isShowProcessing} dispatch={dispatch} + isPaginationCoursesEnabled={isPaginationCoursesEnabled} /> diff --git a/src/studio-home/card-item/CardItem.test.jsx b/src/studio-home/card-item/CardItem.test.jsx index feb7f39e9..73dc73815 100644 --- a/src/studio-home/card-item/CardItem.test.jsx +++ b/src/studio-home/card-item/CardItem.test.jsx @@ -43,9 +43,21 @@ 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, getByTestId } = render(); + const { getByText } = render(); + const courseTitleLink = getByText(props.displayName); + expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`); + const btnReRunCourse = getByText(messages.btnReRunText.defaultMessage); + expect(btnReRunCourse).toHaveAttribute('href', props.rerunLink); + const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage); + expect(viewLiveLink).toHaveAttribute('href', props.lmsLink); + }); + + it('should render correct links for non-library course pagination', () => { + const props = studioHomeMock.archivedCourses[0]; + const { getByText, getByTestId } = render(); const courseTitleLink = getByText(props.displayName); expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`); const dropDownMenu = getByTestId('toggle-dropdown'); diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx index 0ef2199bc..5588ade31 100644 --- a/src/studio-home/card-item/index.jsx +++ b/src/studio-home/card-item/index.jsx @@ -6,6 +6,7 @@ import { Hyperlink, Dropdown, IconButton, + ActionRow, } from '@openedx/paragon'; import { MoreHoriz } from '@openedx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -24,6 +25,8 @@ const CardItem = ({ number, run, isLibraries, + courseKey, + isPaginationEnabled, url, cmsLink, }) => { @@ -56,27 +59,48 @@ const CardItem = ({ )} subtitle={subtitle} actions={showActions && ( - - - - {isShowRerunLink && ( - - {messages.btnReRunText.defaultMessage} + isPaginationEnabled ? ( + + + + {isShowRerunLink && ( + + {messages.btnReRunText.defaultMessage} + + )} + + {intl.formatMessage(messages.viewLiveBtnText)} + + {intl.formatMessage(messages.editStudioBtnText)} + + + + ) : ( + + {isShowRerunLink && ( + + {intl.formatMessage(messages.btnReRunText)} + )} - + {intl.formatMessage(messages.viewLiveBtnText)} - - - {intl.formatMessage(messages.editStudioBtnText)} - - - + + + ) )} /> @@ -85,6 +109,8 @@ const CardItem = ({ CardItem.defaultProps = { isLibraries: false, + isPaginationEnabled: false, + courseKey: '', rerunLink: '', lmsLink: '', run: '', @@ -102,6 +128,8 @@ CardItem.propTypes = { number: PropTypes.string.isRequired, url: PropTypes.string.isRequired, isLibraries: PropTypes.bool, + courseKey: PropTypes.string, + isPaginationEnabled: PropTypes.bool, }; export default injectIntl(CardItem); diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index e5903690e..e94d5f3f6 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -16,6 +16,10 @@ export async function getStudioHomeData() { return camelCaseObject(data); } +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. @@ -25,7 +29,7 @@ export async function getStudioHomeData() { * 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 getStudioHomeCourses(search, customParams) { +export async function getStudioHomeCoursesV2(search, customParams) { const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParams }); return camelCaseObject(data); } diff --git a/src/studio-home/data/api.test.js b/src/studio-home/data/api.test.js index 3e8edfcee..7e34ccc22 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'; @@ -44,7 +45,7 @@ describe('studio-home api calls', () => { }); fit('should get studio courses data', async () => { - const apiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`; + const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`; axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse()); const result = await getStudioHomeCourses(''); const expected = generateGetStudioCoursesApiResponse(); @@ -53,6 +54,16 @@ describe('studio-home api calls', () => { expect(result).toEqual(expected); }); + fit('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/slice.js b/src/studio-home/data/slice.js index 64f8851ea..6d7944756 100644 --- a/src/studio-home/data/slice.js +++ b/src/studio-home/data/slice.js @@ -32,6 +32,12 @@ const slice = createSlice({ Object.assign(state.studioHomeData, payload); }, fetchCourseDataSuccess: (state, { payload }) => { + const { courses, archivedCourses, inProcessCourseActions } = payload; + state.studioHomeData.courses = courses; + 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; @@ -56,6 +62,7 @@ export const { updateLoadingStatuses, fetchStudioHomeDataSuccess, fetchCourseDataSuccess, + fetchCourseDataSuccessV2, fetchLibraryDataSuccess, updateStudioHomeCoursesCustomParams, } = slice.actions; diff --git a/src/studio-home/data/thunks.js b/src/studio-home/data/thunks.js index 81c5e2a9a..76ae08fdc 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, customParams = {}) { +function fetchStudioHomeData(search, hasHomeData, customParams = {}, 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, customParams = {}) { } } try { - const coursesData = await getStudioHomeCourses(search || '', customParams); - dispatch(fetchCourseDataSuccess(coursesData)); + if (isPaginationEnabled) { + const coursesData = await getStudioHomeCoursesV2(search || '', customParams); + 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 7f5176d66..e92e00f66 100644 --- a/src/studio-home/factories/mockApiResponses.jsx +++ b/src/studio-home/factories/mockApiResponses.jsx @@ -49,6 +49,54 @@ export const generateGetStudioHomeDataApiResponse = () => ({ }); export const generateGetStudioCoursesApiResponse = () => ({ + 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', + }, + { + courseKey: 'course-v1:Design+123+e.g.2025', + displayName: 'Design', + lmsLink: '//localhost:18000/courses/course-v1:Design+123+e.g.2025/jump_to/block-v1:Design+123+e.g.2025+type@course+block@course', + number: '123', + org: 'University of Cape Town', + rerunLink: '/course_rerun/course-v1:Design+123+e.g.2025', + run: 'e.g.2025', + url: '/course/course-v1:Design+123+e.g.2025', + }, + ], + 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, previous: null, diff --git a/src/studio-home/hooks.jsx b/src/studio-home/hooks.jsx index b3dec7a53..a12bc4782 100644 --- a/src/studio-home/hooks.jsx +++ b/src/studio-home/hooks.jsx @@ -37,7 +37,7 @@ const useStudioHome = () => { useEffect(() => { const { currentPage } = studioHomeCoursesParams; - dispatch(fetchStudioHomeData(location.search ?? '', false, { page: currentPage })); + dispatch(fetchStudioHomeData(location.search ?? '', false, { page: currentPage }, true)); }, [studioHomeCoursesParams.currentPage]); useEffect(() => { diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index 142dd9e8f..affcbea06 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'; @@ -27,15 +28,21 @@ const { studioShortName } = studioHomeMock; let axiosMock; let store; -const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`; +const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`; +const courseApiLinkV2 = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`; const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`; const mockDispatch = jest.fn(); -const RootWrapper = () => ( +const RootWrapper = (overrideProps) => ( - + ); @@ -94,7 +101,7 @@ describe('', () => { it('should render default sections when courses are empty', async () => { const data = generateGetStudioCoursesApiResponse(); - data.results.courses = []; + data.courses = []; render(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); @@ -120,11 +127,11 @@ describe('', () => { }); it('should render pagination when there are courses', async () => { - render(); + render(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); - axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse()); - await executeThunk(fetchStudioHomeData(), store.dispatch); - const data = generateGetStudioCoursesApiResponse(); + 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}`; @@ -138,11 +145,11 @@ describe('', () => { }); it('should not render pagination when there are not courses', async () => { - const data = generateGetStudioCoursesApiResponse(); + const data = generateGetStudioCoursesApiResponseV2(); data.results.courses = []; render(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); - axiosMock.onGet(courseApiLink).reply(200, data); + axiosMock.onGet(courseApiLinkV2).reply(200, data); await executeThunk(fetchStudioHomeData(), store.dispatch); const pagination = screen.queryByRole('navigation'); diff --git a/src/studio-home/tabs-section/courses-tab/index.jsx b/src/studio-home/tabs-section/courses-tab/index.jsx index 04c16656b..582e39f95 100644 --- a/src/studio-home/tabs-section/courses-tab/index.jsx +++ b/src/studio-home/tabs-section/courses-tab/index.jsx @@ -27,6 +27,7 @@ const CoursesTab = ({ dispatch, numPages, coursesCount, + isEnabledPagination, }) => { const intl = useIntl(); const { @@ -66,7 +67,7 @@ const CoursesTab = ({ ) : ( <> {isShowProcessing && } - {hasCourses && ( + {hasCourses && isEnabledPagination && (

{intl.formatMessage(messages.coursesPaginationInfo, { @@ -100,11 +101,12 @@ const CoursesTab = ({ run={run} url={url} cmsLink={cmsLink} + isPaginated={isEnabledPagination} /> ), )} - {numPages > 1 && ( + {numPages > 1 && isEnabledPagination && ( { const TABS_LIST = { courses: 'courses', @@ -56,6 +61,7 @@ const TabsSection = ({ dispatch={dispatch} numPages={numPages} coursesCount={coursesCount} + isEnabledPagination={isPaginationCoursesEnabled} /> , ); @@ -118,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);