feat: pagination studio home for courses
This commit is contained in:
@@ -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';
|
||||
@@ -45,13 +45,17 @@ describe('<CardItem />', () => {
|
||||
});
|
||||
it('should render correct links for non-library course', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
const { getByText, getByTestId } = render(<RootWrapper {...props} />);
|
||||
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);
|
||||
const editInStudioLink = getByText(messages.editStudioBtnText.defaultMessage);
|
||||
expect(editInStudioLink).toHaveAttribute('href', props.cmsLink);
|
||||
});
|
||||
it('should render course details for library course', () => {
|
||||
const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, Card, Hyperlink } from '@edx/paragon';
|
||||
import {
|
||||
Card,
|
||||
Hyperlink,
|
||||
Dropdown,
|
||||
IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { MoreHoriz } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
@@ -10,7 +16,16 @@ 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,
|
||||
url,
|
||||
cmsLink,
|
||||
}) => {
|
||||
const {
|
||||
allowCourseReruns,
|
||||
@@ -41,16 +56,29 @@ const CardItem = ({
|
||||
)}
|
||||
subtitle={subtitle}
|
||||
actions={showActions && (
|
||||
<ActionRow>
|
||||
{isShowRerunLink && (
|
||||
<Hyperlink className="small" destination={rerunLink}>
|
||||
{intl.formatMessage(messages.btnReRunText)}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<Hyperlink className="small ml-3" destination={lmsLink}>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
</Hyperlink>
|
||||
</ActionRow>
|
||||
<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.Item href={cmsLink}>
|
||||
{intl.formatMessage(messages.editStudioBtnText)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
@@ -62,12 +90,14 @@ CardItem.defaultProps = {
|
||||
rerunLink: '',
|
||||
lmsLink: '',
|
||||
run: '',
|
||||
cmsLink: '',
|
||||
};
|
||||
|
||||
CardItem.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
lmsLink: PropTypes.string,
|
||||
cmsLink: PropTypes.string,
|
||||
rerunLink: PropTypes.string,
|
||||
org: PropTypes.string.isRequired,
|
||||
run: PropTypes.string,
|
||||
|
||||
@@ -16,8 +16,8 @@ export async function getStudioHomeData() {
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
export async function getStudioHomeCourses(search) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`);
|
||||
export async function getStudioHomeCourses(search, customParams) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParams });
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('studio-home api calls', () => {
|
||||
});
|
||||
|
||||
fit('should get studio courses data', async () => {
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
|
||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
const result = await getStudioHomeCourses('');
|
||||
const expected = generateGetStudioCoursesApiResponse();
|
||||
|
||||
@@ -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.studioHomeCoursesCustomParams;
|
||||
|
||||
@@ -17,6 +17,9 @@ const slice = createSlice({
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesCustomParams: {
|
||||
currentPage: 1,
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
updateLoadingStatuses: (state, { payload }) => {
|
||||
@@ -29,15 +32,21 @@ const slice = createSlice({
|
||||
Object.assign(state.studioHomeData, payload);
|
||||
},
|
||||
fetchCourseDataSuccess: (state, { payload }) => {
|
||||
const { courses, archivedCourses, inProcessCourseActions } = 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 }) => {
|
||||
state.studioHomeCoursesCustomParams = { ...state.studioHomeCoursesCustomParams, ...payload };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,6 +56,7 @@ export const {
|
||||
fetchStudioHomeDataSuccess,
|
||||
fetchCourseDataSuccess,
|
||||
fetchLibraryDataSuccess,
|
||||
updateStudioHomeCoursesCustomParams,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
fetchLibraryDataSuccess,
|
||||
} from './slice';
|
||||
|
||||
function fetchStudioHomeData(search, hasHomeData) {
|
||||
function fetchStudioHomeData(search, hasHomeData, customParams = {}) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
@@ -30,7 +30,7 @@ function fetchStudioHomeData(search, hasHomeData) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
const coursesData = await getStudioHomeCourses(search || '');
|
||||
const coursesData = await getStudioHomeCourses(search || '', customParams);
|
||||
dispatch(fetchCourseDataSuccess(coursesData));
|
||||
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
|
||||
@@ -15,6 +15,9 @@ export const initialState = {
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesCustomParams: {
|
||||
currentPage: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,51 +49,58 @@ 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: [],
|
||||
count: 5,
|
||||
next: null,
|
||||
previous: null,
|
||||
numPages: 2,
|
||||
results: {
|
||||
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 generateGetStuioHomeLibrariesApiResponse = () => ({
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getLoadingStatuses,
|
||||
getSavingStatuses,
|
||||
getStudioHomeData,
|
||||
getStudioHomeCoursesParams,
|
||||
} from './data/selectors';
|
||||
import { updateSavingStatuses } from './data/slice';
|
||||
|
||||
@@ -17,6 +18,7 @@ const useStudioHome = () => {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const studioHomeData = useSelector(getStudioHomeData);
|
||||
const studioHomeCoursesParams = useSelector(getStudioHomeCoursesParams);
|
||||
const newCourseData = useSelector(getCourseData);
|
||||
const { studioHomeLoadingStatus } = useSelector(getLoadingStatuses);
|
||||
const savingCreateRerunStatus = useSelector(getSavingStatus);
|
||||
@@ -33,6 +35,11 @@ const useStudioHome = () => {
|
||||
setShowNewCourseContainer(false);
|
||||
}, [location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
const { currentPage } = studioHomeCoursesParams;
|
||||
dispatch(fetchStudioHomeData(location.search ?? '', false, { page: currentPage }));
|
||||
}, [studioHomeCoursesParams.currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (courseCreatorSavingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: '' }));
|
||||
|
||||
@@ -49,6 +49,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.studio-home.btn.view-live.text',
|
||||
defaultMessage: 'View live',
|
||||
},
|
||||
editStudioBtnText: {
|
||||
id: 'course-authoring.studio-home.btn.edit.studio.text',
|
||||
defaultMessage: 'Edit in Studio',
|
||||
},
|
||||
organizationTitle: {
|
||||
id: 'course-authoring.studio-home.organization.title',
|
||||
defaultMessage: 'Organization and library settings',
|
||||
|
||||
@@ -27,13 +27,15 @@ const { studioShortName } = studioHomeMock;
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||
const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
|
||||
const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TabsSection intl={{ formatMessage: jest.fn() }} dispatch={jest.fn()} />
|
||||
<TabsSection intl={{ formatMessage: jest.fn() }} dispatch={mockDispatch} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
@@ -92,7 +94,7 @@ describe('<TabsSection />', () => {
|
||||
|
||||
it('should render default sections when courses are empty', async () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.courses = [];
|
||||
data.results.courses = [];
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
@@ -116,6 +118,36 @@ describe('<TabsSection />', () => {
|
||||
|
||||
expect(screen.getByText(tabMessages.courseTabErrorMessage.defaultMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render pagination when there are courses', async () => {
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
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 = generateGetStudioCoursesApiResponse();
|
||||
data.results.courses = [];
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const pagination = screen.queryByRole('navigation');
|
||||
expect(pagination).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('archived tab', () => {
|
||||
@@ -137,7 +169,7 @@ describe('<TabsSection />', () => {
|
||||
|
||||
it('should hide Archived tab when archived courses are empty', async () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.archivedCourses = [];
|
||||
data.results.archivedCourses = [];
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
|
||||
@@ -2,11 +2,12 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Row } from '@edx/paragon';
|
||||
import { Icon, Row, Pagination } from '@edx/paragon';
|
||||
import { Error } from '@edx/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 CardItem from '../../card-item';
|
||||
import CollapsibleStateWithAction from '../../collapsible-state-with-action';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
@@ -23,12 +24,16 @@ const CoursesTab = ({
|
||||
isShowProcessing,
|
||||
isLoading,
|
||||
isFailed,
|
||||
dispatch,
|
||||
numPages,
|
||||
coursesCount,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseCreatorStatus,
|
||||
optimizationEnabled,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const { currentPage } = useSelector(getStudioHomeCoursesParams);
|
||||
const hasAbilityToCreateCourse = courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const showCollapsible = [
|
||||
COURSE_CREATOR_STATES.denied,
|
||||
@@ -36,6 +41,9 @@ const CoursesTab = ({
|
||||
COURSE_CREATOR_STATES.unrequested,
|
||||
].includes(courseCreatorStatus);
|
||||
|
||||
const handlePageSelected = (page) => dispatch(updateStudioHomeCoursesCustomParams({ currentPage: page }));
|
||||
const hasCourses = coursesDataItems?.length;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
@@ -58,30 +66,54 @@ 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 && (
|
||||
<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}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{numPages > 1 && (
|
||||
<Pagination
|
||||
className="d-flex justify-content-center"
|
||||
paginationLabel="pagination navigation"
|
||||
pageCount={numPages}
|
||||
currentPage={currentPage}
|
||||
onPageSelect={handlePageSelected}
|
||||
/>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (!optimizationEnabled && (
|
||||
<ContactAdministrator
|
||||
hasAbilityToCreateCourse={hasAbilityToCreateCourse}
|
||||
@@ -119,6 +151,9 @@ CoursesTab.propTypes = {
|
||||
isShowProcessing: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
numPages: PropTypes.number.isRequired,
|
||||
coursesCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default CoursesTab;
|
||||
|
||||
@@ -25,6 +25,7 @@ const TabsSection = ({
|
||||
libraryAuthoringMfeUrl,
|
||||
redirectToLibraryAuthoringMfe,
|
||||
courses, librariesEnabled, libraries, archivedCourses,
|
||||
numPages, coursesCount,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const {
|
||||
courseLoadingStatus,
|
||||
@@ -52,6 +53,9 @@ const TabsSection = ({
|
||||
isShowProcessing={isShowProcessing}
|
||||
isLoading={isLoadingCourses}
|
||||
isFailed={isFailedCoursesPage}
|
||||
dispatch={dispatch}
|
||||
numPages={numPages}
|
||||
coursesCount={coursesCount}
|
||||
/>
|
||||
</Tab>,
|
||||
);
|
||||
|
||||
@@ -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