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:
Jhon Vente
2024-04-03 10:38:05 -05:00
committed by GitHub
parent 5247ec5022
commit fde3872e2e
20 changed files with 365 additions and 48 deletions

1
.env
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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