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