Merge branch 'master' into KristinAoki/TNL-8511
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1383,9 +1383,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-lib-special-exams": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-1.8.1.tgz",
|
||||
"integrity": "sha512-6vQrwlE9+PCq8NsAUqY6EOpQtG3KiwKrvdj1lvYOdQMtww3gIHQRh6ZOukAoFlDTDEO4PC6VVx7RFFEj1nTDjg==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-1.9.0.tgz",
|
||||
"integrity": "sha512-dG9iW/vru/6Rh9+1kkqDu3gdiSVF49/9JvdjPb+7H1mdZPs4/FdP43HicPgaJ4r6PKfd9Ssam+3rq1NhxpWzvw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.34",
|
||||
"@fortawesome/free-brands-svg-icons": "5.11.2",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.1.5",
|
||||
"@edx/frontend-enterprise": "4.2.3",
|
||||
"@edx/frontend-lib-special-exams": "1.8.1",
|
||||
"@edx/frontend-lib-special-exams": "1.9.0",
|
||||
"@edx/frontend-platform": "1.11.0",
|
||||
"@edx/paragon": "15.2.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.34",
|
||||
|
||||
@@ -2,4 +2,4 @@ import './courseHomeMetadata.factory';
|
||||
import './datesTabData.factory';
|
||||
import './outlineTabData.factory';
|
||||
import './progressTabData.factory';
|
||||
import './upgradeCardData.factory';
|
||||
import './upgradeNotificationData.factory';
|
||||
|
||||
@@ -29,6 +29,7 @@ Factory.define('outlineTabData')
|
||||
upgrade_url: `${host}/dashboard`,
|
||||
}))
|
||||
.attrs({
|
||||
has_scheduled_content: null,
|
||||
access_expiration: null,
|
||||
can_show_upgrade_sock: false,
|
||||
cert_data: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
|
||||
Factory.define('upgradeCardData')
|
||||
Factory.define('upgradeNotificationData')
|
||||
.option('host', 'http://localhost:18000')
|
||||
.option('dateBlocks', [])
|
||||
.option('offer', null)
|
||||
@@ -6,6 +6,7 @@ Object {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"courseStatus": "loaded",
|
||||
"gradesFeatureIsLocked": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
@@ -302,6 +303,7 @@ Object {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"courseStatus": "loaded",
|
||||
"gradesFeatureIsLocked": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
@@ -380,6 +382,7 @@ Object {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
|
||||
"effortActivities": undefined,
|
||||
"effortTime": undefined,
|
||||
"hasScheduledContent": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"sectionIds": Array [
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
@@ -448,6 +451,7 @@ Object {
|
||||
},
|
||||
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
||||
"hasEnded": undefined,
|
||||
"hasScheduledContent": null,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"offer": null,
|
||||
"resumeCourse": Object {
|
||||
@@ -479,6 +483,7 @@ Object {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"courseStatus": "loaded",
|
||||
"gradesFeatureIsLocked": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
|
||||
@@ -116,6 +116,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
||||
id: courseId,
|
||||
title: block.display_name,
|
||||
sectionIds: block.children || [],
|
||||
hasScheduledContent: block.has_scheduled_content,
|
||||
};
|
||||
break;
|
||||
|
||||
@@ -211,8 +212,15 @@ export async function getDatesTabData(courseId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProgressTabData(courseId) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`;
|
||||
export async function getProgressTabData(courseId, targetUserId) {
|
||||
let url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`;
|
||||
|
||||
// If targetUserId is passed in, we will get the progress page data
|
||||
// for the user with the provided id, rather than the requesting user.
|
||||
if (targetUserId) {
|
||||
url += `/${targetUserId}/`;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
const camelCasedData = camelCaseObject(data);
|
||||
@@ -316,6 +324,7 @@ export async function getOutlineTabData(courseId) {
|
||||
const datesWidget = camelCaseObject(data.dates_widget);
|
||||
const enrollAlert = camelCaseObject(data.enroll_alert);
|
||||
const handoutsHtml = data.handouts_html;
|
||||
const hasScheduledContent = data.has_scheduled_content;
|
||||
const hasEnded = data.has_ended;
|
||||
const offer = camelCaseObject(data.offer);
|
||||
const resumeCourse = camelCaseObject(data.resume_course);
|
||||
@@ -334,6 +343,7 @@ export async function getOutlineTabData(courseId) {
|
||||
datesWidget,
|
||||
enrollAlert,
|
||||
handoutsHtml,
|
||||
hasScheduledContent,
|
||||
hasEnded,
|
||||
offer,
|
||||
resumeCourse,
|
||||
|
||||
@@ -115,6 +115,20 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('Should handle the url including a targetUserId', async () => {
|
||||
const progressTabData = Factory.build('progressTabData', { courseId });
|
||||
const targetUserId = 2;
|
||||
const progressUrl = `${progressBaseUrl}/${courseId}/${targetUserId}/`;
|
||||
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
|
||||
axiosMock.onGet(progressUrl).reply(200, progressTabData);
|
||||
|
||||
await executeThunk(thunks.fetchProgressTab(courseId, 2), store.dispatch);
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.targetUserId).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test saveCourseGoal', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ const slice = createSlice({
|
||||
},
|
||||
fetchTabSuccess: (state, { payload }) => {
|
||||
state.courseId = payload.courseId;
|
||||
state.targetUserId = payload.targetUserId;
|
||||
state.courseStatus = LOADED;
|
||||
},
|
||||
fetchTabFailure: (state, { payload }) => {
|
||||
|
||||
@@ -27,12 +27,12 @@ const eventTypes = {
|
||||
POST_EVENT: 'post_event',
|
||||
};
|
||||
|
||||
export function fetchTab(courseId, tab, getTabData) {
|
||||
export function fetchTab(courseId, tab, getTabData, targetUserId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(fetchTabRequest({ courseId }));
|
||||
Promise.allSettled([
|
||||
getCourseHomeCourseMetadata(courseId),
|
||||
getTabData(courseId),
|
||||
getTabData(courseId, targetUserId),
|
||||
]).then(([courseHomeCourseMetadataResult, tabDataResult]) => {
|
||||
const fetchedCourseHomeCourseMetadata = courseHomeCourseMetadataResult.status === 'fulfilled';
|
||||
const fetchedTabData = tabDataResult.status === 'fulfilled';
|
||||
@@ -62,7 +62,7 @@ export function fetchTab(courseId, tab, getTabData) {
|
||||
}
|
||||
|
||||
if (fetchedCourseHomeCourseMetadata && fetchedTabData) {
|
||||
dispatch(fetchTabSuccess({ courseId }));
|
||||
dispatch(fetchTabSuccess({ courseId, targetUserId }));
|
||||
} else {
|
||||
dispatch(fetchTabFailure({ courseId }));
|
||||
}
|
||||
@@ -74,8 +74,8 @@ export function fetchDatesTab(courseId) {
|
||||
return fetchTab(courseId, 'dates', getDatesTabData);
|
||||
}
|
||||
|
||||
export function fetchProgressTab(courseId) {
|
||||
return fetchTab(courseId, 'progress', getProgressTabData);
|
||||
export function fetchProgressTab(courseId, targetUserId) {
|
||||
return fetchTab(courseId, 'progress', getProgressTabData, parseInt(targetUserId, 10) || targetUserId);
|
||||
}
|
||||
|
||||
export function fetchOutlineTab(courseId) {
|
||||
|
||||
@@ -16,13 +16,14 @@ import messages from './messages';
|
||||
import Section from './Section';
|
||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||
import UpdateGoalSelector from './widgets/UpdateGoalSelector';
|
||||
import UpgradeCard from '../../generic/upgrade-card/UpgradeCard';
|
||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
||||
import { useAccessExpirationAlertMasquerade } from '../../alerts/access-expiration-alert';
|
||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
||||
import useCourseEndAlert from './alerts/course-end-alert';
|
||||
import useCourseStartAlert from './alerts/course-start-alert';
|
||||
import usePrivateCourseAlert from './alerts/private-course-alert';
|
||||
import useScheduledContentAlert from './alerts/scheduled-content-alert';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import WelcomeMessage from './widgets/WelcomeMessage';
|
||||
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
||||
@@ -90,6 +91,7 @@ function OutlineTab({ intl }) {
|
||||
const courseEndAlert = useCourseEndAlert(courseId);
|
||||
const certificateAvailableAlert = useCertificateAvailableAlert(courseId);
|
||||
const privateCourseAlert = usePrivateCourseAlert(courseId);
|
||||
const scheduledContentAlert = useScheduledContentAlert(courseId);
|
||||
|
||||
const rootCourseId = courses && Object.keys(courses)[0];
|
||||
|
||||
@@ -152,6 +154,7 @@ function OutlineTab({ intl }) {
|
||||
...certificateAvailableAlert,
|
||||
...courseEndAlert,
|
||||
...courseStartAlert,
|
||||
...scheduledContentAlert,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -216,7 +219,7 @@ function OutlineTab({ intl }) {
|
||||
{ MMP2P.state.isEnabled
|
||||
? <MMP2PFlyover isStatic options={MMP2P} />
|
||||
: (
|
||||
<UpgradeCard
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
|
||||
@@ -694,6 +694,47 @@ describe('Outline Tab', () => {
|
||||
expect(screen.queryByText('Verify your identity to earn a certificate!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scheduled Content Alert', () => {
|
||||
it('appears correctly', async () => {
|
||||
const now = new Date();
|
||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { hasScheduledContent: true });
|
||||
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||
setMetadata({ is_enrolled: true });
|
||||
setTabData({
|
||||
course_blocks: { blocks: courseBlocks.blocks },
|
||||
date_blocks: [
|
||||
{
|
||||
date_type: 'course-end-date',
|
||||
date: tomorrow.toISOString(),
|
||||
title: 'End',
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByText('More content is coming soon!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('Scheduled Content Alert not present without courseBlocks', () => {
|
||||
it('appears correctly', async () => {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||
setMetadata({ is_enrolled: true });
|
||||
setTabData({
|
||||
course_blocks: null,
|
||||
date_blocks: [
|
||||
{
|
||||
date_type: 'course-end-date',
|
||||
date: tomorrow.toISOString(),
|
||||
title: 'End',
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('More content is coming soon!')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate (web) Complete Alert', () => {
|
||||
@@ -722,6 +763,33 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Requesting Certificate Alert', () => {
|
||||
it('appears', async () => {
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
||||
setMetadata({ is_enrolled: true });
|
||||
setTabData({
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
||||
cert_web_view_url: null,
|
||||
certificate_available_date: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
{
|
||||
date_type: 'course-end-date',
|
||||
date: yesterday.toISOString(),
|
||||
title: 'End',
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByText('Congratulations! Your certificate is ready.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Request certificate')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate (pdf) Complete Alert', () => {
|
||||
it('appears', async () => {
|
||||
const now = new Date();
|
||||
|
||||
@@ -7,23 +7,29 @@ import {
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCheckCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import certMessages from './messages';
|
||||
import certStatusMessages from '../../../progress-tab/certificate-status/messages';
|
||||
import { requestCert } from '../../../data/thunks';
|
||||
|
||||
export const CERT_STATUS_TYPE = {
|
||||
EARNED_NOT_AVAILABLE: 'earned_but_not_available',
|
||||
DOWNLOADABLE: 'downloadable',
|
||||
REQUESTING: 'requesting',
|
||||
UNVERIFIED: 'unverified',
|
||||
};
|
||||
|
||||
function CertificateStatusAlert({ intl, payload }) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
certificateAvailableDate,
|
||||
certStatusType,
|
||||
certStatus,
|
||||
courseEndDate,
|
||||
courseId,
|
||||
certURL,
|
||||
isWebCert,
|
||||
userTimezone,
|
||||
@@ -38,7 +44,7 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
icon: faCheckCircle,
|
||||
iconClassName: 'alert-icon text-success-500',
|
||||
};
|
||||
if (certStatusType === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
|
||||
if (certStatus === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
const certificateAvailableDateFormatted = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
|
||||
const courseEndDateFormatted = <FormattedDate value={courseEndDate} day="numeric" month="long" year="numeric" />;
|
||||
@@ -57,7 +63,7 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else if (certStatusType === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
||||
} else if (certStatus === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
||||
alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
|
||||
if (isWebCert) {
|
||||
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
|
||||
@@ -66,6 +72,12 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
}
|
||||
alertProps.buttonVisible = true;
|
||||
alertProps.buttonLink = certURL;
|
||||
} else if (certStatus === CERT_STATUS_TYPE.REQUESTING) {
|
||||
alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
|
||||
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.requestableButton);
|
||||
alertProps.buttonVisible = true;
|
||||
alertProps.buttonLink = '';
|
||||
alertProps.buttonAction = () => { dispatch(requestCert(courseId)); };
|
||||
}
|
||||
return alertProps;
|
||||
};
|
||||
@@ -86,9 +98,10 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
};
|
||||
|
||||
let alertProps = {};
|
||||
switch (certStatusType) {
|
||||
switch (certStatus) {
|
||||
case CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE:
|
||||
case CERT_STATUS_TYPE.DOWNLOADABLE:
|
||||
case CERT_STATUS_TYPE.REQUESTING:
|
||||
alertProps = renderCertAwardedStatus();
|
||||
break;
|
||||
case CERT_STATUS_TYPE.UNVERIFIED:
|
||||
@@ -106,22 +119,26 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
iconClassName,
|
||||
icon,
|
||||
header,
|
||||
buttonLink,
|
||||
body,
|
||||
buttonAction,
|
||||
buttonLink,
|
||||
buttonMessage,
|
||||
}) => (
|
||||
<Alert variant={variant}>
|
||||
<div className="row justify-content-between align-items-center">
|
||||
<div className="d-flex flex-column flex-lg-row justify-content-between align-items-center">
|
||||
<div className={buttonVisible ? 'col-lg-8' : 'col-auto'}>
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
<Alert.Heading>{header}</Alert.Heading>
|
||||
{body}
|
||||
</div>
|
||||
{buttonVisible && (
|
||||
<div className="m-auto m-lg-0 pr-lg-3">
|
||||
<div className="flex-grow-0 pt-3 pt-lg-0">
|
||||
<Button
|
||||
variant="primary"
|
||||
href={buttonLink}
|
||||
onClick={() => {
|
||||
if (buttonAction) { buttonAction(); }
|
||||
}}
|
||||
>
|
||||
{buttonMessage}
|
||||
</Button>
|
||||
@@ -139,8 +156,9 @@ CertificateStatusAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
certificateAvailableDate: PropTypes.string,
|
||||
certStatusType: PropTypes.string,
|
||||
certStatus: PropTypes.string,
|
||||
courseEndDate: PropTypes.string,
|
||||
courseId: PropTypes.string,
|
||||
certURL: PropTypes.string,
|
||||
isWebCert: PropTypes.bool,
|
||||
userTimezone: PropTypes.string,
|
||||
|
||||
@@ -3,29 +3,28 @@ import React, { useMemo } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useAlert } from '../../../../generic/user-messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import { CERT_STATUS_TYPE } from './CertificateStatusAlert';
|
||||
|
||||
const CertificateStatusAlert = React.lazy(() => import('./CertificateStatusAlert'));
|
||||
|
||||
function verifyCertStatusType(status) {
|
||||
// This method will only return cert statuses when we want to alert on them.
|
||||
// It should be modified when we want to alert on a new status type.
|
||||
if (status === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
||||
return CERT_STATUS_TYPE.DOWNLOADABLE;
|
||||
switch (status) {
|
||||
case CERT_STATUS_TYPE.DOWNLOADABLE:
|
||||
case CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE:
|
||||
case CERT_STATUS_TYPE.REQUESTING:
|
||||
case CERT_STATUS_TYPE.UNVERIFIED:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (status === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
|
||||
return CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE;
|
||||
}
|
||||
if (status === CERT_STATUS_TYPE.UNVERIFIED) {
|
||||
return CERT_STATUS_TYPE.UNVERIFIED;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function useCertificateStatusAlert(courseId) {
|
||||
const {
|
||||
isEnrolled,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const {
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
@@ -41,7 +40,6 @@ function useCertificateStatusAlert(courseId) {
|
||||
downloadUrl,
|
||||
} = certData || {};
|
||||
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
|
||||
const certStatusType = verifyCertStatusType(certStatus);
|
||||
const isWebCert = downloadUrl === null;
|
||||
|
||||
let certURL = '';
|
||||
@@ -51,14 +49,15 @@ function useCertificateStatusAlert(courseId) {
|
||||
// PDF Certificate
|
||||
certURL = downloadUrl;
|
||||
}
|
||||
const hasCertStatus = certStatusType !== '';
|
||||
const hasAlertingCertStatus = verifyCertStatusType(certStatus);
|
||||
|
||||
// Only show if there is a known cert status that we want provide status on.
|
||||
const isVisible = isEnrolled && hasCertStatus;
|
||||
const isVisible = isEnrolled && hasAlertingCertStatus;
|
||||
const payload = {
|
||||
certificateAvailableDate,
|
||||
certURL,
|
||||
certStatusType,
|
||||
certStatus,
|
||||
courseId,
|
||||
courseEndDate: endBlock && endBlock.date,
|
||||
userTimezone,
|
||||
isWebCert,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ScheduledContentAlert({ payload }) {
|
||||
const {
|
||||
datesTabLink,
|
||||
} = payload;
|
||||
|
||||
return (
|
||||
<Alert variant="info">
|
||||
<div className="d-flex flex-column flex-lg-row justify-content-between align-items-center">
|
||||
<div className="col-lg-7">
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="learning.outline.alert.scheduled-content.heading"
|
||||
defaultMessage="More content is coming soon!"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="learning.outline.alert.scheduled-content.body"
|
||||
defaultMessage="This course will have more content released at a future date. Look out for email updates or check back on this course for updates."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow-0 pt-3 pt-lg-0">
|
||||
{datesTabLink && (
|
||||
<Button
|
||||
href={datesTabLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="learning.outline.alert.scheduled-content.button"
|
||||
defaultMessage="View Course Schedule"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduledContentAlert.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
datesTabLink: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ScheduledContentAlert;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useAlert } from '../../../../generic/user-messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
const ScheduledContentAlert = React.lazy(() => import('./ScheduledCotentAlert'));
|
||||
|
||||
const useScheduledContentAlert = (courseId) => {
|
||||
const {
|
||||
courseBlocks: {
|
||||
courses,
|
||||
},
|
||||
datesWidget: {
|
||||
datesTabLink,
|
||||
},
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const hasScheduledContent = (
|
||||
!!courses
|
||||
&& !!Object.values(courses).find(course => course.hasScheduledContent === true)
|
||||
);
|
||||
const { isEnrolled } = useModel('courseHomeMeta', courseId);
|
||||
const payload = {
|
||||
datesTabLink,
|
||||
};
|
||||
useAlert(hasScheduledContent && isEnrolled, {
|
||||
code: 'ScheduledContentAlert',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
topic: 'outline-course-alerts',
|
||||
});
|
||||
|
||||
return { ScheduledContentAlert };
|
||||
};
|
||||
|
||||
export default useScheduledContentAlert;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './hooks';
|
||||
@@ -12,16 +12,23 @@ import messages from './messages';
|
||||
function ProgressHeader({ intl }) {
|
||||
const {
|
||||
courseId,
|
||||
targetUserId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
const { administrator, userId } = getAuthenticatedUser();
|
||||
|
||||
const { studioUrl } = useModel('progress', courseId);
|
||||
const { studioUrl, username } = useModel('progress', courseId);
|
||||
|
||||
const viewingOtherStudentsProgressPage = (targetUserId && targetUserId !== userId);
|
||||
|
||||
const pageTitle = viewingOtherStudentsProgressPage
|
||||
? intl.formatMessage(messages.progressHeaderForTargetUser, { username })
|
||||
: intl.formatMessage(messages.progressHeader);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row w-100 m-0 mt-3 mb-4 justify-content-between">
|
||||
<h1>{intl.formatMessage(messages.progressHeader)}</h1>
|
||||
<h1>{pageTitle}</h1>
|
||||
{administrator && studioUrl && (
|
||||
<Button variant="outline-primary" size="sm" className="align-self-center" href={studioUrl}>
|
||||
{intl.formatMessage(messages.studioLink)}
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('Progress Tab', () => {
|
||||
const courseId = 'course-v1:edX+Test+run';
|
||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`;
|
||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||
const progressUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`;
|
||||
const progressUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/*`);
|
||||
|
||||
const store = initializeStore();
|
||||
const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId });
|
||||
@@ -1034,4 +1034,16 @@ describe('Progress Tab', () => {
|
||||
expect(screen.queryByTestId('certificate-status-component')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Viewing progress page of other students by changing url', () => {
|
||||
it('Changing the url changes the header', async () => {
|
||||
setMetadata({ is_enrolled: true });
|
||||
setTabData({ username: 'otherstudent' });
|
||||
|
||||
await executeThunk(thunks.fetchProgressTab(courseId, 10), store.dispatch);
|
||||
await act(async () => render(<ProgressTab />, { store }));
|
||||
|
||||
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,11 @@ const messages = defineMessages({
|
||||
id: 'progress.header',
|
||||
defaultMessage: 'Your progress',
|
||||
},
|
||||
progressHeaderForTargetUser: {
|
||||
id: 'progress.header.targetUser',
|
||||
defaultMessage: 'Course progress for {username}',
|
||||
description: 'Header when displaying the progress for a different user',
|
||||
},
|
||||
studioLink: {
|
||||
id: 'progress.link.studio',
|
||||
defaultMessage: 'View grading in Studio',
|
||||
|
||||
@@ -59,9 +59,11 @@ const checkUnitToSequenceUnitRedirect = memoize((courseStatus, courseId, sequenc
|
||||
}
|
||||
});
|
||||
|
||||
const checkSpecialExamRedirect = memoize((sequenceStatus, sequence) => {
|
||||
const checkSpecialExamRedirect = memoize((sequenceStatus, sequence, specialExamsEnabled, proctoredExamsEnabled) => {
|
||||
if (sequenceStatus === 'loaded') {
|
||||
if (sequence.isTimeLimited && sequence.legacyWebUrl !== undefined) {
|
||||
const shouldRedirectTimeLimited = sequence.isTimeLimited && !specialExamsEnabled;
|
||||
const shouldRedirectProctored = sequence.isProctored && !proctoredExamsEnabled;
|
||||
if ((shouldRedirectTimeLimited || shouldRedirectProctored) && sequence.legacyWebUrl !== undefined) {
|
||||
global.location.assign(sequence.legacyWebUrl);
|
||||
}
|
||||
}
|
||||
@@ -181,11 +183,7 @@ class CoursewareContainer extends Component {
|
||||
// Check special exam redirect:
|
||||
// /course/:courseId/:sequenceId(/:unitId) -> :legacyWebUrl
|
||||
// because special exams are currently still served in the legacy LMS frontend.
|
||||
const shouldRedirectProctoredExams = specialExamsEnabledWaffleFlag && sequence.isProctored
|
||||
&& !proctoredExamsEnabledWaffleFlag;
|
||||
if (!specialExamsEnabledWaffleFlag || shouldRedirectProctoredExams) {
|
||||
checkSpecialExamRedirect(sequenceStatus, sequence);
|
||||
}
|
||||
checkSpecialExamRedirect(sequenceStatus, sequence, specialExamsEnabledWaffleFlag, proctoredExamsEnabledWaffleFlag);
|
||||
|
||||
// Check to sequence to sequence-unit redirect:
|
||||
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
|
||||
|
||||
@@ -14,7 +14,7 @@ import Sequence from './sequence';
|
||||
import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
|
||||
import ContentTools from './content-tools';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
import SidebarNotificationButton from './SidebarNotificationButton';
|
||||
import NotificationTrigger from './NotificationTrigger';
|
||||
|
||||
import CourseSock from '../../generic/course-sock';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
@@ -66,12 +66,12 @@ function Course({
|
||||
// the browser console and refresh: document.cookie = 'value_prop_cookie=true';
|
||||
const isValuePropCookieSet = Cookies.get('value_prop_cookie') === 'true';
|
||||
|
||||
const shouldDisplaySidebarButton = useWindowSize().width >= responsiveBreakpoints.small.minWidth;
|
||||
const shouldDisplayNotificationTrigger = useWindowSize().width >= responsiveBreakpoints.small.minWidth;
|
||||
|
||||
const [sidebarVisible, setSidebar] = useState(true);
|
||||
const isSidebarVisible = () => sidebarVisible && setSidebar;
|
||||
const toggleSidebar = () => {
|
||||
if (sidebarVisible) { setSidebar(false); } else { setSidebar(true); }
|
||||
const [notificationTrayVisible, setNotificationTray] = verifiedMode ? useState(true) : useState(false);
|
||||
const isNotificationTrayVisible = () => notificationTrayVisible && setNotificationTray;
|
||||
const toggleNotificationTray = () => {
|
||||
if (notificationTrayVisible) { setNotificationTray(false); } else { setNotificationTray(true); }
|
||||
};
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
@@ -102,10 +102,10 @@ function Course({
|
||||
mmp2p={MMP2P}
|
||||
/>
|
||||
|
||||
{ isValuePropCookieSet && shouldDisplaySidebarButton ? (
|
||||
<SidebarNotificationButton
|
||||
toggleSidebar={toggleSidebar}
|
||||
isSidebarVisible={isSidebarVisible}
|
||||
{ isValuePropCookieSet && shouldDisplayNotificationTrigger ? (
|
||||
<NotificationTrigger
|
||||
toggleNotificationTray={toggleNotificationTray}
|
||||
isNotificationTrayVisible={isNotificationTrayVisible}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -118,9 +118,9 @@ function Course({
|
||||
unitNavigationHandler={unitNavigationHandler}
|
||||
nextSequenceHandler={nextSequenceHandler}
|
||||
previousSequenceHandler={previousSequenceHandler}
|
||||
toggleSidebar={toggleSidebar}
|
||||
isSidebarVisible={isSidebarVisible}
|
||||
sidebarVisible={sidebarVisible}
|
||||
toggleNotificationTray={toggleNotificationTray}
|
||||
isNotificationTrayVisible={isNotificationTrayVisible}
|
||||
notificationTrayVisible={notificationTrayVisible}
|
||||
isValuePropCookieSet={isValuePropCookieSet}
|
||||
//* * [MM-P2P] Experiment */
|
||||
mmp2p={MMP2P}
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Course', () => {
|
||||
nextSequenceHandler: () => {},
|
||||
previousSequenceHandler: () => {},
|
||||
unitNavigationHandler: () => {},
|
||||
toggleSidebar: () => {},
|
||||
toggleNotificationTray: () => {},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -87,9 +87,9 @@ describe('Course', () => {
|
||||
expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays sidebar notification button', async () => {
|
||||
const toggleSidebar = jest.fn();
|
||||
const isSidebarVisible = jest.fn();
|
||||
it('displays notification trigger', async () => {
|
||||
const toggleNotificationTray = jest.fn();
|
||||
const isNotificationTrayVisible = jest.fn();
|
||||
|
||||
// REV-2297 TODO: remove cookie related code once temporary value prop cookie code is removed.
|
||||
const cookieName = 'value_prop_cookie';
|
||||
@@ -101,15 +101,15 @@ describe('Course', () => {
|
||||
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
|
||||
const testData = {
|
||||
...mockData,
|
||||
toggleSidebar,
|
||||
isSidebarVisible,
|
||||
toggleNotificationTray,
|
||||
isNotificationTrayVisible,
|
||||
};
|
||||
render(<Course {...testData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
|
||||
const sidebarOpenButton = screen.getByRole('button', { name: /Show sidebar notification/i });
|
||||
const notificationOpenButton = screen.getByRole('button', { name: /Show notification tray/i });
|
||||
|
||||
expect(getSpy).toBeCalledWith(cookieName);
|
||||
expect(sidebarOpenButton).toBeInTheDocument();
|
||||
expect(notificationOpenButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays offer and expiration alert', async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import messages from './messages';
|
||||
function NotificationIcon({ intl, status, notificationColor }) {
|
||||
return (
|
||||
<div className="icon-container">
|
||||
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openSidebarButton)} />
|
||||
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openNotificationTrigger)} />
|
||||
{status === 'active'
|
||||
? <span className={classNames(notificationColor, 'notification-dot')} data-testid="notification-dot" />
|
||||
: null}
|
||||
|
||||
@@ -9,10 +9,10 @@ import { ArrowBackIos, Close } from '@edx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize';
|
||||
import UpgradeCard from '../../generic/upgrade-card/UpgradeCard';
|
||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
||||
|
||||
function Sidebar({
|
||||
intl, toggleSidebar,
|
||||
function NotificationTray({
|
||||
intl, toggleNotificationTray,
|
||||
}) {
|
||||
const {
|
||||
courseId,
|
||||
@@ -33,23 +33,23 @@ function Sidebar({
|
||||
const shouldDisplayFullScreen = useWindowSize().width < responsiveBreakpoints.large.minWidth;
|
||||
|
||||
return (
|
||||
<section className={classNames('sidebar-container ml-0 ml-lg-4', { 'no-notification': !verifiedMode && !shouldDisplayFullScreen })} aria-label={intl.formatMessage(messages.sidebarNotification)}>
|
||||
<section className={classNames('notification-tray-container ml-0 ml-lg-4', { 'no-notification': !verifiedMode && !shouldDisplayFullScreen })} aria-label={intl.formatMessage(messages.notificationTray)}>
|
||||
{shouldDisplayFullScreen ? (
|
||||
<div className="mobile-close-container" onClick={() => { toggleSidebar(); }} onKeyDown={() => { toggleSidebar(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.responsiveCloseSidebar)}>
|
||||
<div className="mobile-close-container" onClick={() => { toggleNotificationTray(); }} onKeyDown={() => { toggleNotificationTray(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.responsiveCloseNotificationTray)}>
|
||||
<Icon src={ArrowBackIos} />
|
||||
<span className="mobile-close">{intl.formatMessage(messages.responsiveCloseSidebar)}</span>
|
||||
<span className="mobile-close">{intl.formatMessage(messages.responsiveCloseNotificationTray)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="sidebar-header px-3">
|
||||
<div className="notification-tray-header px-3">
|
||||
<span>{intl.formatMessage(messages.notificationTitle)}</span>
|
||||
{shouldDisplayFullScreen
|
||||
? null
|
||||
: <Icon src={Close} className="close-btn" onClick={() => { toggleSidebar(); }} onKeyDown={() => { toggleSidebar(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.closeSidebarButton)} />}
|
||||
: <Icon src={Close} className="close-btn" onClick={() => { toggleNotificationTray(); }} onKeyDown={() => { toggleNotificationTray(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.closeNotificationTrigger)} />}
|
||||
</div>
|
||||
<div className="sidebar-divider" />
|
||||
<div className="notification-tray-divider" />
|
||||
<div>{verifiedMode
|
||||
? (
|
||||
<UpgradeCard
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
@@ -60,19 +60,19 @@ function Sidebar({
|
||||
org={org}
|
||||
shouldDisplayBorder={false}
|
||||
/>
|
||||
) : <p className="sidebar-content">{intl.formatMessage(messages.noNotificationsMessage)}</p>}
|
||||
) : <p className="notification-tray-content">{intl.formatMessage(messages.noNotificationsMessage)}</p>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Sidebar.propTypes = {
|
||||
NotificationTray.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
toggleSidebar: PropTypes.func,
|
||||
toggleNotificationTray: PropTypes.func,
|
||||
};
|
||||
|
||||
Sidebar.defaultProps = {
|
||||
toggleSidebar: null,
|
||||
NotificationTray.defaultProps = {
|
||||
toggleNotificationTray: null,
|
||||
};
|
||||
|
||||
export default injectIntl(Sidebar);
|
||||
export default injectIntl(NotificationTray);
|
||||
@@ -1,4 +1,4 @@
|
||||
.sidebar-container {
|
||||
.notification-tray-container {
|
||||
border: 1px solid $light-400;
|
||||
border-radius: 4px;
|
||||
width: 31rem;
|
||||
@@ -23,7 +23,7 @@
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
.notification-tray-header {
|
||||
padding: 0.625rem 0;
|
||||
|
||||
span {
|
||||
@@ -35,7 +35,7 @@
|
||||
float: right;
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
.notification-tray-divider {
|
||||
width: 100.5%;
|
||||
height: 0.5rem;
|
||||
background: $gray-100;
|
||||
@@ -43,7 +43,7 @@
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
.notification-tray-content {
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
} from '../../setupTest';
|
||||
import initializeStore from '../../store';
|
||||
import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils';
|
||||
import Sidebar from './Sidebar';
|
||||
import NotificationTray from './NotificationTray';
|
||||
import useWindowSize from '../../generic/tabs/useWindowSize';
|
||||
|
||||
initializeMockApp();
|
||||
jest.mock('../../generic/tabs/useWindowSize');
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
describe('Sidebar', () => {
|
||||
describe('NotificationTray', () => {
|
||||
let mockData;
|
||||
let axiosMock;
|
||||
let store;
|
||||
@@ -43,45 +43,45 @@ describe('Sidebar', () => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||
mockData = {
|
||||
toggleSidebar: () => {},
|
||||
toggleNotificationTray: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
it('renders sidebar', async () => {
|
||||
it('renders notification tray', async () => {
|
||||
useWindowSize.mockReturnValue({ width: 1200, height: 422 });
|
||||
await fetchAndRender(<Sidebar />);
|
||||
await fetchAndRender(<NotificationTray />);
|
||||
expect(screen.getByText('Notifications')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Back to course')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders upgrade card', async () => {
|
||||
await fetchAndRender(<Sidebar />);
|
||||
const upgradeCard = document.querySelector('.upgrade-card');
|
||||
await fetchAndRender(<NotificationTray />);
|
||||
const UpgradeNotification = document.querySelector('.upgrade-notification');
|
||||
|
||||
expect(upgradeCard).toBeInTheDocument();
|
||||
expect(UpgradeNotification).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('You have no new notifications at this time.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no notifications message if no verified mode', async () => {
|
||||
setMetadata({ verified_mode: null });
|
||||
await fetchAndRender(<Sidebar />);
|
||||
await fetchAndRender(<NotificationTray />);
|
||||
expect(screen.queryByText('You have no new notifications at this time.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders sidebar with full screen "Back to course" at response width', async () => {
|
||||
it('renders notification tray with full screen "Back to course" at response width', async () => {
|
||||
useWindowSize.mockReturnValue({ width: 991, height: 422 });
|
||||
const toggleSidebar = jest.fn();
|
||||
const toggleNotificationTray = jest.fn();
|
||||
const testData = {
|
||||
...mockData,
|
||||
toggleSidebar,
|
||||
toggleNotificationTray,
|
||||
};
|
||||
await fetchAndRender(<Sidebar {...testData} />);
|
||||
await fetchAndRender(<NotificationTray {...testData} />);
|
||||
|
||||
const responsiveCloseButton = screen.getByRole('button', { name: 'Back to course' });
|
||||
await waitFor(() => expect(responsiveCloseButton).toBeInTheDocument());
|
||||
|
||||
fireEvent.click(responsiveCloseButton);
|
||||
expect(toggleSidebar).toHaveBeenCalledTimes(1);
|
||||
expect(toggleNotificationTray).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -6,13 +6,13 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import NotificationIcon from './NotificationIcon';
|
||||
import messages from './messages';
|
||||
|
||||
function SidebarNotificationButton({ intl, toggleSidebar, isSidebarVisible }) {
|
||||
function NotificationTrigger({ intl, toggleNotificationTray, isNotificationTrayVisible }) {
|
||||
return (
|
||||
<button
|
||||
className={classNames('sidebar-notification-btn', { active: isSidebarVisible() })}
|
||||
className={classNames('notification-trigger-btn', { active: isNotificationTrayVisible() })}
|
||||
type="button"
|
||||
onClick={() => { toggleSidebar(); }}
|
||||
aria-label={intl.formatMessage(messages.openSidebarButton)}
|
||||
onClick={() => { toggleNotificationTray(); }}
|
||||
aria-label={intl.formatMessage(messages.openNotificationTrigger)}
|
||||
>
|
||||
{/* REV-2297 TODO: add logic for status "active" if red dot should display */}
|
||||
<NotificationIcon status="inactive" notificationColor="bg-danger-500" />
|
||||
@@ -20,10 +20,10 @@ function SidebarNotificationButton({ intl, toggleSidebar, isSidebarVisible }) {
|
||||
);
|
||||
}
|
||||
|
||||
SidebarNotificationButton.propTypes = {
|
||||
NotificationTrigger.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
toggleSidebar: PropTypes.func.isRequired,
|
||||
isSidebarVisible: PropTypes.func.isRequired,
|
||||
toggleNotificationTray: PropTypes.func.isRequired,
|
||||
isNotificationTrayVisible: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SidebarNotificationButton);
|
||||
export default injectIntl(NotificationTrigger);
|
||||
@@ -1,4 +1,4 @@
|
||||
.sidebar-notification-btn {
|
||||
.notification-trigger-btn {
|
||||
border: 1px solid $light-400;
|
||||
background: none;
|
||||
margin-top: 0.625rem;
|
||||
43
src/courseware/course/NotificationTrigger.test.jsx
Normal file
43
src/courseware/course/NotificationTrigger.test.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import {
|
||||
render, initializeTestStore, screen, fireEvent,
|
||||
} from '../../setupTest';
|
||||
import NotificationTrigger from './NotificationTrigger';
|
||||
|
||||
describe('Notification Trigger', () => {
|
||||
let mockData;
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeTestStore({ courseMetadata, excludeFetchCourse: true, excludeFetchSequence: true });
|
||||
mockData = {
|
||||
toggleNotificationTray: () => {},
|
||||
isNotificationTrayVisible: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
it('renders notification trigger with icon', async () => {
|
||||
const { container } = render(<NotificationTrigger {...mockData} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
const buttonIcon = container.querySelectorAll('svg');
|
||||
expect(buttonIcon).toHaveLength(1);
|
||||
|
||||
// REV-2297 TODO: update below test once the status=active or inactive is implemented
|
||||
// expect(screen.getByTestId('notification-dot')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles onClick event toggling the notification tray', async () => {
|
||||
const toggleNotificationTray = jest.fn();
|
||||
const testData = {
|
||||
...mockData,
|
||||
toggleNotificationTray,
|
||||
};
|
||||
render(<NotificationTrigger {...testData} />);
|
||||
|
||||
const notificationOpenButton = screen.getByRole('button', { name: /Show notification tray/i });
|
||||
expect(notificationOpenButton).toBeInTheDocument();
|
||||
fireEvent.click(notificationOpenButton);
|
||||
expect(toggleNotificationTray).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import {
|
||||
render, initializeTestStore, screen, fireEvent,
|
||||
} from '../../setupTest';
|
||||
import SidebarNotificationButton from './SidebarNotificationButton';
|
||||
|
||||
describe('Sidebar Notification Button', () => {
|
||||
let mockData;
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeTestStore({ courseMetadata, excludeFetchCourse: true, excludeFetchSequence: true });
|
||||
mockData = {
|
||||
toggleSidebar: () => {},
|
||||
isSidebarVisible: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
it('renders sidebar notification button with icon', async () => {
|
||||
const { container } = render(<SidebarNotificationButton {...mockData} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
const buttonIcon = container.querySelectorAll('svg');
|
||||
expect(buttonIcon).toHaveLength(1);
|
||||
|
||||
// REV-2297 TODO: update below test once the status=active or inactive is implemented
|
||||
// expect(screen.getByTestId('notification-dot')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles onClick event toggling the sidebar', async () => {
|
||||
const toggleSidebar = jest.fn();
|
||||
const testData = {
|
||||
...mockData,
|
||||
toggleSidebar,
|
||||
};
|
||||
render(<SidebarNotificationButton {...testData} />);
|
||||
|
||||
const sidebarOpenButton = screen.getByRole('button', { name: /Show sidebar notification/i });
|
||||
expect(sidebarOpenButton).toBeInTheDocument();
|
||||
fireEvent.click(sidebarOpenButton);
|
||||
expect(toggleSidebar).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +1,33 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
sidebarNotification: {
|
||||
id: 'sidebar.notification.container',
|
||||
defaultMessage: 'Sidebar notification',
|
||||
description: 'Sidebar notification section container',
|
||||
notificationTray: {
|
||||
id: 'notification.tray.container',
|
||||
defaultMessage: 'Notification tray',
|
||||
description: 'Notification tray container',
|
||||
},
|
||||
openSidebarButton: {
|
||||
id: 'sidebar.open.button',
|
||||
defaultMessage: 'Show sidebar notification',
|
||||
description: 'Button to open the sidebar and show notifications',
|
||||
openNotificationTrigger: {
|
||||
id: 'notification.open.button',
|
||||
defaultMessage: 'Show notification tray',
|
||||
description: 'Button to open the notification tray and show notifications',
|
||||
},
|
||||
closeSidebarButton: {
|
||||
id: 'sidebar.close.button',
|
||||
closeNotificationTrigger: {
|
||||
id: 'notification.close.button',
|
||||
defaultMessage: 'Close sidebar notification',
|
||||
description: 'Button for the learner to close the sidebar',
|
||||
},
|
||||
responsiveCloseSidebar: {
|
||||
id: 'sidebar.responsive.close.button',
|
||||
responsiveCloseNotificationTray: {
|
||||
id: 'responsive.close.notification',
|
||||
defaultMessage: 'Back to course',
|
||||
description: 'Responsive button for the learner to go back to course and close the sidebar',
|
||||
description: 'Responsive button to go back to course and close the notification tray',
|
||||
},
|
||||
notificationTitle: {
|
||||
id: 'sidebar.notification.title',
|
||||
id: 'notification.tray.title',
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Title text displayed for sidebar notifications',
|
||||
description: 'Title text displayed for the notification tray',
|
||||
},
|
||||
noNotificationsMessage: {
|
||||
id: 'sidebar.notification.no.message',
|
||||
id: 'notification.tray.no.message',
|
||||
defaultMessage: 'You have no new notifications at this time.',
|
||||
description: 'Text displayed when the learner has no notifications',
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ import CourseLicense from '../course-license';
|
||||
import messages from './messages';
|
||||
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
|
||||
import SequenceContent from './SequenceContent';
|
||||
import Sidebar from '../Sidebar';
|
||||
import SidebarNotificationButton from '../SidebarNotificationButton';
|
||||
import NotificationTray from '../NotificationTray';
|
||||
import NotificationTrigger from '../NotificationTrigger';
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
import { isMobile } from '../../../experiments/mm-p2p/utils';
|
||||
@@ -37,9 +37,9 @@ function Sequence({
|
||||
nextSequenceHandler,
|
||||
previousSequenceHandler,
|
||||
intl,
|
||||
toggleSidebar,
|
||||
sidebarVisible,
|
||||
isSidebarVisible,
|
||||
toggleNotificationTray,
|
||||
notificationTrayVisible,
|
||||
isNotificationTrayVisible,
|
||||
isValuePropCookieSet,
|
||||
mmp2p,
|
||||
}) {
|
||||
@@ -49,7 +49,7 @@ function Sequence({
|
||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||
const specialExamsEnabledWaffleFlag = useSelector(state => state.courseware.specialExamsEnabledWaffleFlag);
|
||||
const proctoredExamsEnabledWaffleFlag = useSelector(state => state.courseware.proctoredExamsEnabledWaffleFlag);
|
||||
const shouldDisplaySidebarButton = useWindowSize().width < responsiveBreakpoints.small.minWidth;
|
||||
const shouldDisplayNotificationTrigger = useWindowSize().width < responsiveBreakpoints.small.minWidth;
|
||||
|
||||
const handleNext = () => {
|
||||
const nextIndex = sequence.unitIds.indexOf(unitId) + 1;
|
||||
@@ -167,7 +167,7 @@ function Sequence({
|
||||
|
||||
const defaultContent = (
|
||||
<div className="sequence-container" style={{ display: 'inline-flex', flexDirection: 'row' }}>
|
||||
<div className={classNames('sequence', { 'position-relative': shouldDisplaySidebarButton })} style={{ width: '100%' }}>
|
||||
<div className={classNames('sequence', { 'position-relative': shouldDisplayNotificationTrigger })} style={{ width: '100%' }}>
|
||||
<SequenceNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
@@ -192,10 +192,10 @@ function Sequence({
|
||||
isValuePropCookieSet={isValuePropCookieSet}
|
||||
/>
|
||||
|
||||
{isValuePropCookieSet && shouldDisplaySidebarButton ? (
|
||||
<SidebarNotificationButton
|
||||
toggleSidebar={toggleSidebar}
|
||||
isSidebarVisible={isSidebarVisible}
|
||||
{isValuePropCookieSet && shouldDisplayNotificationTrigger ? (
|
||||
<NotificationTrigger
|
||||
toggleNotificationTray={toggleNotificationTray}
|
||||
isNotificationTrayVisible={isNotificationTrayVisible}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -226,10 +226,10 @@ function Sequence({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isValuePropCookieSet && sidebarVisible ? (
|
||||
<Sidebar
|
||||
toggleSidebar={toggleSidebar}
|
||||
sidebarVisible={sidebarVisible}
|
||||
{isValuePropCookieSet && notificationTrayVisible ? (
|
||||
<NotificationTray
|
||||
toggleNotificationTray={toggleNotificationTray}
|
||||
notificationTrayVisible={notificationTrayVisible}
|
||||
/>
|
||||
) : null }
|
||||
|
||||
@@ -269,9 +269,9 @@ Sequence.propTypes = {
|
||||
nextSequenceHandler: PropTypes.func.isRequired,
|
||||
previousSequenceHandler: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
toggleSidebar: PropTypes.func,
|
||||
sidebarVisible: PropTypes.bool,
|
||||
isSidebarVisible: PropTypes.func,
|
||||
toggleNotificationTray: PropTypes.func,
|
||||
notificationTrayVisible: PropTypes.bool,
|
||||
isNotificationTrayVisible: PropTypes.func,
|
||||
isValuePropCookieSet: PropTypes.bool,
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
@@ -291,9 +291,9 @@ Sequence.propTypes = {
|
||||
Sequence.defaultProps = {
|
||||
sequenceId: null,
|
||||
unitId: null,
|
||||
toggleSidebar: null,
|
||||
sidebarVisible: null,
|
||||
isSidebarVisible: null,
|
||||
toggleNotificationTray: null,
|
||||
notificationTrayVisible: null,
|
||||
isNotificationTrayVisible: null,
|
||||
isValuePropCookieSet: null,
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('Sequence', () => {
|
||||
unitNavigationHandler: () => {},
|
||||
nextSequenceHandler: () => {},
|
||||
previousSequenceHandler: () => {},
|
||||
sidebarVisible: false,
|
||||
notificationTrayVisible: false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -130,10 +130,10 @@ describe('Sequence', () => {
|
||||
expect(screen.getAllByRole('button', { name: /previous|next/i }).length).toEqual(4);
|
||||
});
|
||||
|
||||
it('renders sidebar in sequence', async () => {
|
||||
it('renders notification tray in sequence', async () => {
|
||||
const testData = {
|
||||
...mockData,
|
||||
sidebarVisible: true,
|
||||
notificationTrayVisible: true,
|
||||
isValuePropCookieSet: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ function SequenceNavigation({
|
||||
sequence.gatedContent !== undefined && sequence.gatedContent.gated
|
||||
) : undefined;
|
||||
|
||||
const shouldDisplaySidebarButton = useWindowSize().width < responsiveBreakpoints.small.minWidth;
|
||||
const shouldDisplayNotificationTrigger = useWindowSize().width < responsiveBreakpoints.small.minWidth;
|
||||
|
||||
const renderUnitButtons = () => {
|
||||
if (isLocked) {
|
||||
@@ -70,15 +70,15 @@ function SequenceNavigation({
|
||||
const disabled = isLastUnit && !exitActive;
|
||||
return (
|
||||
<Button variant="link" className="next-btn" onClick={buttonOnClick} disabled={disabled} iconAfter={ChevronRight}>
|
||||
{isValuePropCookieSet && shouldDisplaySidebarButton ? null : buttonText}
|
||||
{isValuePropCookieSet && shouldDisplayNotificationTrigger ? null : buttonText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return sequenceStatus === LOADED && (
|
||||
<nav className={classNames('sequence-navigation', className)} style={{ width: isValuePropCookieSet && shouldDisplaySidebarButton ? '90%' : null }}>
|
||||
<nav className={classNames('sequence-navigation', className)} style={{ width: isValuePropCookieSet && shouldDisplayNotificationTrigger ? '90%' : null }}>
|
||||
<Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit} iconBefore={ChevronLeft}>
|
||||
{isValuePropCookieSet && shouldDisplaySidebarButton ? null : intl.formatMessage(messages.previousButton)}
|
||||
{isValuePropCookieSet && shouldDisplayNotificationTrigger ? null : intl.formatMessage(messages.previousButton)}
|
||||
</Button>
|
||||
{renderUnitButtons()}
|
||||
{renderNextButton()}
|
||||
|
||||
@@ -36,8 +36,8 @@ export default function Tabs({ children, className, ...attrs }) {
|
||||
ref={overflowElementRef}
|
||||
key="overflow"
|
||||
>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="link" className="nav-link font-weight-normal">
|
||||
<Dropdown className="h-100">
|
||||
<Dropdown.Toggle variant="link" className="nav-link h-100">
|
||||
<FormattedMessage
|
||||
id="learn.course.tabs.navigation.overflow.menu"
|
||||
description="The title of the overflow menu for course tabs"
|
||||
|
||||
@@ -14,26 +14,26 @@ function UpsellNoFBECardContent() {
|
||||
const verifiedCertLink = (
|
||||
<a className="inline-link-underline font-weight-bold" rel="noopener noreferrer" target="_blank" href={`${getConfig().MARKETING_SITE_BASE_URL}/verified-certificate`}>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.verifiedCertLink"
|
||||
id="learning.generic.upgradeNotification.verifiedCertLink"
|
||||
defaultMessage="verified certificate"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className="fa-ul upgrade-card-ul pt-0">
|
||||
<ul className="fa-ul upgrade-notification-ul pt-0">
|
||||
<li>
|
||||
<span className="fa-li upgrade-card-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<span className="fa-li upgrade-notification-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.verifiedCertMessage"
|
||||
id="learning.generic.upgradeNotification.verifiedCertMessage"
|
||||
defaultMessage="Earn a {verifiedCertLink} of completion to showcase on your resume"
|
||||
values={{ verifiedCertLink }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li upgrade-card-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<span className="fa-li upgrade-notification-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.nonProfitMission"
|
||||
id="learning.generic.upgradeNotification.noFBE.nonProfitMission"
|
||||
defaultMessage="Support our {nonProfitMission} at edX"
|
||||
values={{
|
||||
nonProfitMission: (
|
||||
@@ -50,7 +50,7 @@ function UpsellFBEFarAwayCardContent() {
|
||||
const verifiedCertLink = (
|
||||
<a className="inline-link-underline font-weight-bold" rel="noopener noreferrer" target="_blank" href={`${getConfig().MARKETING_SITE_BASE_URL}/verified-certificate`}>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.verifiedCertLink"
|
||||
id="learning.generic.upgradeNotification.verifiedCertLink"
|
||||
defaultMessage="verified certificate"
|
||||
/>
|
||||
</a>
|
||||
@@ -59,7 +59,7 @@ function UpsellFBEFarAwayCardContent() {
|
||||
const gradedAssignments = (
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.gradedAssignments"
|
||||
id="learning.generic.upgradeNotification.gradedAssignments"
|
||||
defaultMessage="graded assignments"
|
||||
/>
|
||||
</span>
|
||||
@@ -68,7 +68,7 @@ function UpsellFBEFarAwayCardContent() {
|
||||
const fullAccess = (
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.upgradeCard.verifiedCertLink"
|
||||
id="learning.generic.upgradeNotification.verifiedCertLink.fullAccess"
|
||||
defaultMessage="Full access"
|
||||
/>
|
||||
</span>
|
||||
@@ -77,42 +77,42 @@ function UpsellFBEFarAwayCardContent() {
|
||||
const nonProfitMission = (
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.upgradeCard.nonProfitMission"
|
||||
id="learning.generic.upgradeNotification.FBE.nonProfitMission"
|
||||
defaultMessage="non-profit mission"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className="fa-ul upgrade-card-ul">
|
||||
<ul className="fa-ul upgrade-notification-ul">
|
||||
<li>
|
||||
<span className="fa-li upgrade-card-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<span className="fa-li upgrade-notification-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.verifiedCertMessage"
|
||||
id="learning.generic.upgradeNotification.verifiedCertMessage"
|
||||
defaultMessage="Earn a {verifiedCertLink} of completion to showcase on your resume"
|
||||
values={{ verifiedCertLink }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li upgrade-card-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<span className="fa-li upgrade-notification-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.unlockGraded"
|
||||
id="learning.generic.upgradeNotification.unlockGraded"
|
||||
defaultMessage="Unlock your access to all course activities, including {gradedAssignments}"
|
||||
values={{ gradedAssignments }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li upgrade-card-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<span className="fa-li upgrade-notification-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.fullAccess"
|
||||
id="learning.generic.upgradeNotification.fullAccess"
|
||||
defaultMessage="{fullAccess} to course content and materials, even after the course ends"
|
||||
values={{ fullAccess }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li upgrade-card-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<span className="fa-li upgrade-notification-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.nonProfitMission"
|
||||
id="learning.generic.upgradeNotification.nonProfitMission"
|
||||
defaultMessage="Support our {nonProfitMission} at edX"
|
||||
values={{ nonProfitMission }}
|
||||
/>
|
||||
@@ -125,7 +125,7 @@ function UpsellFBESoonCardContent({ accessExpirationDate, timezoneFormatArgs })
|
||||
const includingAnyProgress = (
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.upgradeCard.expirationAccessLoss.progress"
|
||||
id="learning.generic.upgradeNotification.expirationAccessLoss.progress"
|
||||
defaultMessage="including any progress"
|
||||
/>
|
||||
</span>
|
||||
@@ -144,17 +144,17 @@ function UpsellFBESoonCardContent({ accessExpirationDate, timezoneFormatArgs })
|
||||
const benefitsOfUpgrading = (
|
||||
<a className="inline-link-underline font-weight-bold" rel="noopener noreferrer" target="_blank" href="https://support.edx.org/hc/en-us/articles/360013426573-What-are-the-differences-between-audit-free-and-verified-paid-courses-">
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.expirationVerifiedCert.benefits"
|
||||
id="learning.generic.upgradeNotification.expirationVerifiedCert.benefits"
|
||||
defaultMessage="benefits of upgrading"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="upgrade-card-text">
|
||||
<div className="upgrade-notification-text">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.expirationAccessLoss"
|
||||
id="learning.generic.upgradeNotification.expirationAccessLoss"
|
||||
defaultMessage="You will lose all access to this course, {includingAnyProgress}, on {date}."
|
||||
values={{
|
||||
includingAnyProgress,
|
||||
@@ -164,7 +164,7 @@ function UpsellFBESoonCardContent({ accessExpirationDate, timezoneFormatArgs })
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.expirationVerifiedCert"
|
||||
id="learning.generic.upgradeNotification.expirationVerifiedCert"
|
||||
defaultMessage="Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}."
|
||||
values={{ benefitsOfUpgrading }}
|
||||
/>
|
||||
@@ -190,7 +190,7 @@ function ExpirationCountdown({ hoursToExpiration }) {
|
||||
if (hoursToExpiration >= 24) {
|
||||
expirationText = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.expirationDays"
|
||||
id="learning.generic.upgradeNotification.expirationDays"
|
||||
defaultMessage={`{dayCount, number} {dayCount, plural,
|
||||
one {day}
|
||||
other {days}} left`}
|
||||
@@ -202,7 +202,7 @@ function ExpirationCountdown({ hoursToExpiration }) {
|
||||
} else if (hoursToExpiration >= 1) {
|
||||
expirationText = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.expirationHours"
|
||||
id="learning.generic.upgradeNotification.expirationHours"
|
||||
defaultMessage={`{hourCount, number} {hourCount, plural,
|
||||
one {hour}
|
||||
other {hours}} left`}
|
||||
@@ -214,7 +214,7 @@ function ExpirationCountdown({ hoursToExpiration }) {
|
||||
} else {
|
||||
expirationText = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.expirationMinutes"
|
||||
id="learning.generic.upgradeNotification.expirationMinutes"
|
||||
defaultMessage="Less than 1 hour left"
|
||||
/>
|
||||
);
|
||||
@@ -230,7 +230,7 @@ function AccessExpirationDateBanner({ accessExpirationDate, timezoneFormatArgs }
|
||||
return (
|
||||
<div className="upsell-warning-light">
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.expiration"
|
||||
id="learning.generic.upgradeNotification.expiration"
|
||||
defaultMessage="Course access will expire {date}"
|
||||
values={{
|
||||
date: (
|
||||
@@ -259,7 +259,7 @@ AccessExpirationDateBanner.defaultProps = {
|
||||
timezoneFormatArgs: {},
|
||||
};
|
||||
|
||||
function UpgradeCard({
|
||||
function UpgradeNotification({
|
||||
accessExpiration,
|
||||
contentTypeGatingEnabled,
|
||||
courseId,
|
||||
@@ -313,12 +313,12 @@ function UpgradeCard({
|
||||
|
||||
/*
|
||||
There are 4 parts that change in the upgrade card:
|
||||
upgradeCardHeaderText
|
||||
upgradeNotificationHeaderText
|
||||
expirationBanner
|
||||
upsellMessage
|
||||
offerCode
|
||||
*/
|
||||
let upgradeCardHeaderText;
|
||||
let upgradeNotificationHeaderText;
|
||||
let expirationBanner;
|
||||
let upsellMessage;
|
||||
let offerCode;
|
||||
@@ -331,7 +331,7 @@ function UpgradeCard({
|
||||
offerCode = (
|
||||
<div className="text-center discount-info">
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.code"
|
||||
id="learning.generic.upgradeNotification.code"
|
||||
defaultMessage="Use code {code} at checkout"
|
||||
values={{
|
||||
code: (<span className="font-weight-bold">{offer.code}</span>),
|
||||
@@ -344,9 +344,9 @@ function UpgradeCard({
|
||||
if (hoursToAccessExpiration >= (7 * 24)) {
|
||||
if (offer) { // countdown to the first purchase discount if there is one
|
||||
const hoursToDiscountExpiration = Math.floor((new Date(offer.expirationDate) - correctedTime) / 1000 / 60 / 60);
|
||||
upgradeCardHeaderText = (
|
||||
upgradeNotificationHeaderText = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.firstTimeLearnerDiscount"
|
||||
id="learning.generic.upgradeNotification.firstTimeLearnerDiscount"
|
||||
defaultMessage="{percentage}% First-Time Learner Discount"
|
||||
values={{
|
||||
percentage: (offer.percentage),
|
||||
@@ -355,9 +355,9 @@ function UpgradeCard({
|
||||
);
|
||||
expirationBanner = <ExpirationCountdown hoursToExpiration={hoursToDiscountExpiration} />;
|
||||
} else {
|
||||
upgradeCardHeaderText = (
|
||||
upgradeNotificationHeaderText = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.accessExpiration"
|
||||
id="learning.generic.upgradeNotification.accessExpiration"
|
||||
defaultMessage="Upgrade your course today"
|
||||
/>
|
||||
);
|
||||
@@ -370,9 +370,9 @@ function UpgradeCard({
|
||||
}
|
||||
upsellMessage = <UpsellFBEFarAwayCardContent />;
|
||||
} else { // more urgent messaging if there's less than 7 days left to access expiration
|
||||
upgradeCardHeaderText = (
|
||||
upgradeNotificationHeaderText = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.accessExpirationUrgent"
|
||||
id="learning.generic.upgradeNotification.accessExpirationUrgent"
|
||||
defaultMessage="Course Access Expiration"
|
||||
/>
|
||||
);
|
||||
@@ -385,9 +385,9 @@ function UpgradeCard({
|
||||
);
|
||||
}
|
||||
} else { // FBE is turned off
|
||||
upgradeCardHeaderText = (
|
||||
upgradeNotificationHeaderText = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.widgets.upgradeCard.pursueAverifiedCertificate"
|
||||
id="learning.generic.upgradeNotification.pursueAverifiedCertificate"
|
||||
defaultMessage="Pursue a verified certificate"
|
||||
/>
|
||||
);
|
||||
@@ -395,26 +395,26 @@ function UpgradeCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classNames('upgrade-card small', { 'card mb-4': shouldDisplayBorder })}>
|
||||
<h2 className="h5 upgrade-card-header" id="outline-sidebar-upgrade-header">
|
||||
{upgradeCardHeaderText}
|
||||
<section className={classNames('upgrade-notification small', { 'card mb-4': shouldDisplayBorder })}>
|
||||
<h2 className="h5 upgrade-notification-header" id="outline-sidebar-upgrade-header">
|
||||
{upgradeNotificationHeaderText}
|
||||
</h2>
|
||||
{expirationBanner}
|
||||
<div className="upgrade-card-message">
|
||||
<div className="upgrade-notification-message">
|
||||
{upsellMessage}
|
||||
</div>
|
||||
<UpgradeButton
|
||||
offer={offer}
|
||||
onClick={logClick}
|
||||
verifiedMode={verifiedMode}
|
||||
className="upgrade-card-button"
|
||||
className="upgrade-notification-button"
|
||||
/>
|
||||
{offerCode}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
UpgradeCard.propTypes = {
|
||||
UpgradeNotification.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
accessExpiration: PropTypes.shape({
|
||||
@@ -436,7 +436,7 @@ UpgradeCard.propTypes = {
|
||||
shouldDisplayBorder: PropTypes.bool,
|
||||
};
|
||||
|
||||
UpgradeCard.defaultProps = {
|
||||
UpgradeNotification.defaultProps = {
|
||||
accessExpiration: null,
|
||||
contentTypeGatingEnabled: false,
|
||||
offer: null,
|
||||
@@ -446,4 +446,4 @@ UpgradeCard.defaultProps = {
|
||||
shouldDisplayBorder: null,
|
||||
};
|
||||
|
||||
export default injectIntl(UpgradeCard);
|
||||
export default injectIntl(UpgradeNotification);
|
||||
@@ -1,8 +1,8 @@
|
||||
.upgrade-card {
|
||||
.upgrade-notification {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.upgrade-card-header {
|
||||
.upgrade-notification-header {
|
||||
margin: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -18,22 +18,22 @@
|
||||
padding: 0.5rem 1.25rem;
|
||||
}
|
||||
|
||||
.upgrade-card-ul {
|
||||
.upgrade-notification-ul {
|
||||
margin-left: 3rem;
|
||||
padding-top: 0.875rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.upgrade-card-li {
|
||||
.upgrade-notification-li {
|
||||
left: -2.125rem;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
.upgrade-card-text {
|
||||
.upgrade-notification-text {
|
||||
padding: 0.875rem 1.25rem 0 1.25rem;
|
||||
}
|
||||
|
||||
.upgrade-card-button {
|
||||
.upgrade-notification-button {
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.25rem;
|
||||
@@ -49,6 +49,6 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.upgrade-card .upgrade-card-message a {
|
||||
.upgrade-notification .upgrade-notification-message a {
|
||||
color: $primary-500;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp, render, screen } from '../../setupTest';
|
||||
import UpgradeCard from './UpgradeCard';
|
||||
import UpgradeNotification from './UpgradeNotification';
|
||||
|
||||
initializeMockApp();
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
@@ -11,10 +11,10 @@ jest
|
||||
.spyOn(global.Date, 'now')
|
||||
.mockImplementation(() => dateNow.valueOf());
|
||||
|
||||
describe('Upgrade Card', () => {
|
||||
describe('Upgrade Notification', () => {
|
||||
function buildAndRender(attributes) {
|
||||
const upgradeCardData = Factory.build('upgradeCardData', { ...attributes });
|
||||
render(<UpgradeCard {...upgradeCardData} />);
|
||||
const upgradeNotificationData = Factory.build('upgradeNotificationData', { ...attributes });
|
||||
render(<UpgradeNotification {...upgradeNotificationData} />);
|
||||
}
|
||||
|
||||
it('does not render when there is no verified mode', async () => {
|
||||
@@ -42,6 +42,9 @@
|
||||
"alert.enroll": "يجب أن تكون مسجلا في المساق لمشاهدة المحتوى.",
|
||||
"learning.privateCourse.signInOrRegister": "{signIn} أو {register} ثم التحق بهذا المساق.",
|
||||
"learning.privateCourse.canEnroll": "{enrollNow}لتصفح كامل المساق.",
|
||||
"learning.outline.alert.scheduled-content.heading": "More content is coming soon!",
|
||||
"learning.outline.alert.scheduled-content.body": "This course will have more content released at a future date. Look out for email updates or check back on this course for updates.",
|
||||
"learning.outline.alert.scheduled-content.button": "View Course Schedule",
|
||||
"learning.outline.dates.all": "عرض جميع تواريخ المساق",
|
||||
"learning.outline.collapseAll": "Collapse all",
|
||||
"learning.outline.completedAssignment": "اكتمل",
|
||||
@@ -159,6 +162,7 @@
|
||||
"progress.weightedGrade": "Weighted grade",
|
||||
"progress.weightedGradeSummary": "Your current weighted grade summary",
|
||||
"progress.header": "Your progress",
|
||||
"progress.header.targetUser": "Course progress for {username}",
|
||||
"progress.link.studio": "View grading in Studio",
|
||||
"progress.relatedLinks.datesCard.description": "A schedule view of your course due dates and upcoming assignments.",
|
||||
"progress.relatedLinks.datesCard.link": "Dates",
|
||||
@@ -283,12 +287,12 @@
|
||||
"learn.course.license.creativeCommons.terms.zero": "لا توجد عناصر",
|
||||
"learn.course.license.creativeCommons.text": "بعض الحقوق محفوظة",
|
||||
"learn.breadcrumb.navigation.course.home": "المساق ",
|
||||
"sidebar.notification.container": "Sidebar notification",
|
||||
"sidebar.open.button": "Show sidebar notification",
|
||||
"sidebar.close.button": "Close sidebar notification",
|
||||
"sidebar.responsive.close.button": "Back to course",
|
||||
"sidebar.notification.title": "Notifications",
|
||||
"sidebar.notification.no.message": "You have no new notifications at this time.",
|
||||
"notification.tray.container": "Notification tray",
|
||||
"notification.open.button": "Show notification tray",
|
||||
"notification.close.button": "Close sidebar notification",
|
||||
"responsive.close.notification": "Back to course",
|
||||
"notification.tray.title": "Notifications",
|
||||
"notification.tray.no.message": "You have no new notifications at this time.",
|
||||
"learn.contentLock.content.locked": "محتوى مغلق",
|
||||
"learn.contentLock.complete.prerequisite": "يجب استيفاء المتطلبات الأساسية: '{priceqSectionName}' للوصول إلى هذا المحتوى.",
|
||||
"learn.contentLock.goToSection": "انتقل إلى قسم المتطلبات الأساسية",
|
||||
@@ -347,27 +351,28 @@
|
||||
"learning.upgradeButton.screenReaderInlinePrices": "السعر الأصلي: {originalPrice}",
|
||||
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||
"learning.upgradeNowButton.buttonText": "Upgrade now for {pricing}",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertLink": "verified certificate",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertMessage": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||
"learning.outline.widgets.upgradeCard.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.outline.widgets.upgradeCard.gradedAssignments": "graded assignments",
|
||||
"learning.upgradeCard.verifiedCertLink": "Full access",
|
||||
"learning.upgradeCard.nonProfitMission": "non-profit mission",
|
||||
"learning.outline.widgets.upgradeCard.unlockGraded": "Unlock your access to all course activities, including {gradedAssignments}",
|
||||
"learning.outline.widgets.upgradeCard.fullAccess": "{fullAccess} to course content and materials, even after the course ends",
|
||||
"learning.upgradeCard.expirationAccessLoss.progress": "including any progress",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||
"learning.outline.widgets.upgradeCard.expirationAccessLoss": "You will lose all access to this course, {includingAnyProgress}, on {date}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||
"learning.outline.widgets.upgradeCard.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||
"learning.outline.widgets.upgradeCard.expirationMinutes": "Less than 1 hour left",
|
||||
"learning.outline.widgets.upgradeCard.expiration": "Course access will expire {date}",
|
||||
"learning.outline.widgets.upgradeCard.code": "Use code {code} at checkout",
|
||||
"learning.outline.widgets.upgradeCard.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||
"learning.outline.widgets.upgradeCard.accessExpiration": "Upgrade your course today",
|
||||
"learning.outline.widgets.upgradeCard.accessExpirationUrgent": "Course Access Expiration",
|
||||
"learning.outline.widgets.upgradeCard.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink": "verified certificate",
|
||||
"learning.generic.upgradeNotification.verifiedCertMessage": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||
"learning.generic.upgradeNotification.noFBE.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.generic.upgradeNotification.gradedAssignments": "graded assignments",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink.fullAccess": "Full access",
|
||||
"learning.generic.upgradeNotification.FBE.nonProfitMission": "non-profit mission",
|
||||
"learning.generic.upgradeNotification.unlockGraded": "Unlock your access to all course activities, including {gradedAssignments}",
|
||||
"learning.generic.upgradeNotification.fullAccess": "{fullAccess} to course content and materials, even after the course ends",
|
||||
"learning.generic.upgradeNotification.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "including any progress",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss": "You will lose all access to this course, {includingAnyProgress}, on {date}.",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||
"learning.generic.upgradeNotification.expirationMinutes": "Less than 1 hour left",
|
||||
"learning.generic.upgradeNotification.expiration": "Course access will expire {date}",
|
||||
"learning.generic.upgradeNotification.code": "Use code {code} at checkout",
|
||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||
"learning.generic.upgradeNotification.accessExpiration": "Upgrade your course today",
|
||||
"learning.generic.upgradeNotification.accessExpirationUrgent": "Course Access Expiration",
|
||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"masquerade-widget.userName.error.generic": "حدث خطأ؛ يرجى المحاولة مرة أخرى.",
|
||||
"masquerade-widget.userName.input.placeholder": "اسم المستخدم أو البريد الإلكتروني",
|
||||
"masquerade-widget.userName.input.label": "عرف كهذا المستخدم",
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
"alert.enroll": "You must be enrolled in the course to see course content.",
|
||||
"learning.privateCourse.signInOrRegister": "{sign} o {register} y después inscríbete en este curso.",
|
||||
"learning.privateCourse.canEnroll": "{enrollNow} para acceder a la totalidad del curso.",
|
||||
"learning.outline.alert.scheduled-content.heading": "More content is coming soon!",
|
||||
"learning.outline.alert.scheduled-content.body": "This course will have more content released at a future date. Look out for email updates or check back on this course for updates.",
|
||||
"learning.outline.alert.scheduled-content.button": "View Course Schedule",
|
||||
"learning.outline.dates.all": "Ver todas las fechas del curso",
|
||||
"learning.outline.collapseAll": "Colapsar todo",
|
||||
"learning.outline.completedAssignment": "Completed",
|
||||
@@ -159,6 +162,7 @@
|
||||
"progress.weightedGrade": "Weighted grade",
|
||||
"progress.weightedGradeSummary": "Your current weighted grade summary",
|
||||
"progress.header": "Your progress",
|
||||
"progress.header.targetUser": "Course progress for {username}",
|
||||
"progress.link.studio": "View grading in Studio",
|
||||
"progress.relatedLinks.datesCard.description": "A schedule view of your course due dates and upcoming assignments.",
|
||||
"progress.relatedLinks.datesCard.link": "Dates",
|
||||
@@ -283,12 +287,12 @@
|
||||
"learn.course.license.creativeCommons.terms.zero": "Sin condiciones",
|
||||
"learn.course.license.creativeCommons.text": "Algunos Derechos Reservados",
|
||||
"learn.breadcrumb.navigation.course.home": "Curso",
|
||||
"sidebar.notification.container": "Notificación de la barra lateral",
|
||||
"sidebar.open.button": "Mostrar la notificación de la barra lateral",
|
||||
"sidebar.close.button": "Cerrar la notificación de la barra lateral",
|
||||
"sidebar.responsive.close.button": "Regresar al curso",
|
||||
"sidebar.notification.title": "Notificaciones",
|
||||
"sidebar.notification.no.message": "No tienes notificaciones nuevas en este momento.",
|
||||
"notification.tray.container": "Notificación de la barra lateral",
|
||||
"notification.open.button": "Mostrar la notificación de la barra lateral",
|
||||
"notification.close.button": "Cerrar la notificación de la barra lateral",
|
||||
"responsive.close.notification": "Regresar al curso",
|
||||
"notification.tray.title": "Notificaciones",
|
||||
"notification.tray.no.message": "No tienes notificaciones nuevas en este momento.",
|
||||
"learn.contentLock.content.locked": "Contenido Bloqueado",
|
||||
"learn.contentLock.complete.prerequisite": "Debe completar el prerrequisito: '{prereqSectionName}'para acceder a este contenido.",
|
||||
"learn.contentLock.goToSection": "Ir a la Sección de Prerrequisitos",
|
||||
@@ -347,27 +351,28 @@
|
||||
"learning.upgradeButton.screenReaderInlinePrices": "Precio original: {originalPrice}",
|
||||
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||
"learning.upgradeNowButton.buttonText": "Cambia a la opción paga por {pricing}",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertLink": "certificado verificado",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertMessage": "Obtén un {verifiedCertLink} de finalización para compartirlo en tu currículum",
|
||||
"learning.outline.widgets.upgradeCard.nonProfitMission": "Apoya nuestra {nonProfitMission} en edX",
|
||||
"learning.outline.widgets.upgradeCard.gradedAssignments": "tareas calificadas",
|
||||
"learning.upgradeCard.verifiedCertLink": "Acceso completo",
|
||||
"learning.upgradeCard.nonProfitMission": "misión sin fines de lucro",
|
||||
"learning.outline.widgets.upgradeCard.unlockGraded": "Desbloquea el acceso a todas las actividades del curso, incluidas las {gradedAssignments}",
|
||||
"learning.outline.widgets.upgradeCard.fullAccess": "{fullAccess} al contenido y los materiales del curso, incluso después de que finalice el curso",
|
||||
"learning.upgradeCard.expirationAccessLoss.progress": "incluido cualquier progreso",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert.benefits": "beneficios del cambio",
|
||||
"learning.outline.widgets.upgradeCard.expirationAccessLoss": "Perderás todo el acceso a este curso, {includingAnyProgress}, el {date}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert": "Cambiarte a la opción verificada permite obtener un certificado verificado y obtener acceso a numerosas funciones. Obtén más información sobre los {benefitsOfUpgrading}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationDays": "Quedan {dayCount, number} {dayCount, plural, \none {day}\nother {days}}",
|
||||
"learning.outline.widgets.upgradeCard.expirationHours": "Quedan {hourCount, number} {hourCount, plural,\none {hour}\nother {hours}}",
|
||||
"learning.outline.widgets.upgradeCard.expirationMinutes": "Queda menos de 1 hora",
|
||||
"learning.outline.widgets.upgradeCard.expiration": "El acceso al curso expirará {date}",
|
||||
"learning.outline.widgets.upgradeCard.code": "Usa el código {code} al finalizar la compra",
|
||||
"learning.outline.widgets.upgradeCard.firstTimeLearnerDiscount": "{percentage}% de descuento de bienvenida para estudiantes nuevos",
|
||||
"learning.outline.widgets.upgradeCard.accessExpiration": "Cámbiate a la opción verificada",
|
||||
"learning.outline.widgets.upgradeCard.accessExpirationUrgent": "Vencimiento del acceso al curso",
|
||||
"learning.outline.widgets.upgradeCard.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink": "certificado verificado",
|
||||
"learning.generic.upgradeNotification.verifiedCertMessage": "Obtén un {verifiedCertLink} de finalización para compartirlo en tu currículum",
|
||||
"learning.generic.upgradeNotification.noFBE.nonProfitMission": "Apoya nuestra {nonProfitMission} en edX",
|
||||
"learning.generic.upgradeNotification.gradedAssignments": "tareas calificadas",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink.fullAccess": "Acceso completo",
|
||||
"learning.generic.upgradeNotification.FBE.nonProfitMission": "misión sin fines de lucro",
|
||||
"learning.generic.upgradeNotification.unlockGraded": "Desbloquea el acceso a todas las actividades del curso, incluidas las {gradedAssignments}",
|
||||
"learning.generic.upgradeNotification.fullAccess": "{fullAccess} al contenido y los materiales del curso, incluso después de que finalice el curso",
|
||||
"learning.generic.upgradeNotification.nonProfitMission": "Apoya nuestra {nonProfitMission} en edX",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "incluido cualquier progreso",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "beneficios del cambio",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss": "Perderás todo el acceso a este curso, {includingAnyProgress}, el {date}.",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "Cambiarte a la opción verificada permite obtener un certificado verificado y obtener acceso a numerosas funciones. Obtén más información sobre los {benefitsOfUpgrading}.",
|
||||
"learning.generic.upgradeNotification.expirationDays": "Quedan {dayCount, number} {dayCount, plural, \none {day}\nother {days}}",
|
||||
"learning.generic.upgradeNotification.expirationHours": "Quedan {hourCount, number} {hourCount, plural,\none {hour}\nother {hours}}",
|
||||
"learning.generic.upgradeNotification.expirationMinutes": "Queda menos de 1 hora",
|
||||
"learning.generic.upgradeNotification.expiration": "El acceso al curso expirará el {date}",
|
||||
"learning.generic.upgradeNotification.code": "Usa el código {code} al finalizar la compra",
|
||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% de descuento de bienvenida para estudiantes nuevos",
|
||||
"learning.generic.upgradeNotification.accessExpiration": "Cámbiate a la opción verificada",
|
||||
"learning.generic.upgradeNotification.accessExpirationUrgent": "Vencimiento del acceso al curso",
|
||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Obtenga un certificado verificado",
|
||||
"masquerade-widget.userName.error.generic": "Se ha producido un error. Inténtalo de nuevo.",
|
||||
"masquerade-widget.userName.input.placeholder": "Nombre de usuario o correo electrónico",
|
||||
"masquerade-widget.userName.input.label": "Hazte pasar por este usuario",
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
"alert.enroll": "You must be enrolled in the course to see course content.",
|
||||
"learning.privateCourse.signInOrRegister": "{signIn} or {register} and then enroll in this course.",
|
||||
"learning.privateCourse.canEnroll": "{enrollNow} to access the full course.",
|
||||
"learning.outline.alert.scheduled-content.heading": "More content is coming soon!",
|
||||
"learning.outline.alert.scheduled-content.body": "This course will have more content released at a future date. Look out for email updates or check back on this course for updates.",
|
||||
"learning.outline.alert.scheduled-content.button": "View Course Schedule",
|
||||
"learning.outline.dates.all": "View all course dates",
|
||||
"learning.outline.collapseAll": "Collapse all",
|
||||
"learning.outline.completedAssignment": "Completed",
|
||||
@@ -159,6 +162,7 @@
|
||||
"progress.weightedGrade": "Weighted grade",
|
||||
"progress.weightedGradeSummary": "Your current weighted grade summary",
|
||||
"progress.header": "Your progress",
|
||||
"progress.header.targetUser": "Course progress for {username}",
|
||||
"progress.link.studio": "View grading in Studio",
|
||||
"progress.relatedLinks.datesCard.description": "A schedule view of your course due dates and upcoming assignments.",
|
||||
"progress.relatedLinks.datesCard.link": "Dates",
|
||||
@@ -283,12 +287,12 @@
|
||||
"learn.course.license.creativeCommons.terms.zero": "No terms",
|
||||
"learn.course.license.creativeCommons.text": "Some Rights Reserved",
|
||||
"learn.breadcrumb.navigation.course.home": "Course",
|
||||
"sidebar.notification.container": "Sidebar notification",
|
||||
"sidebar.open.button": "Show sidebar notification",
|
||||
"sidebar.close.button": "Close sidebar notification",
|
||||
"sidebar.responsive.close.button": "Back to course",
|
||||
"sidebar.notification.title": "Notifications",
|
||||
"sidebar.notification.no.message": "You have no new notifications at this time.",
|
||||
"notification.tray.container": "Notification tray",
|
||||
"notification.open.button": "Show notification tray",
|
||||
"notification.close.button": "Close sidebar notification",
|
||||
"responsive.close.notification": "Back to course",
|
||||
"notification.tray.title": "Notifications",
|
||||
"notification.tray.no.message": "You have no new notifications at this time.",
|
||||
"learn.contentLock.content.locked": "Content Locked",
|
||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
||||
@@ -347,27 +351,28 @@
|
||||
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
||||
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||
"learning.upgradeNowButton.buttonText": "Upgrade now for {pricing}",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertLink": "verified certificate",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertMessage": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||
"learning.outline.widgets.upgradeCard.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.outline.widgets.upgradeCard.gradedAssignments": "graded assignments",
|
||||
"learning.upgradeCard.verifiedCertLink": "Full access",
|
||||
"learning.upgradeCard.nonProfitMission": "non-profit mission",
|
||||
"learning.outline.widgets.upgradeCard.unlockGraded": "Unlock your access to all course activities, including {gradedAssignments}",
|
||||
"learning.outline.widgets.upgradeCard.fullAccess": "{fullAccess} to course content and materials, even after the course ends",
|
||||
"learning.upgradeCard.expirationAccessLoss.progress": "including any progress",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||
"learning.outline.widgets.upgradeCard.expirationAccessLoss": "You will lose all access to this course, {includingAnyProgress}, on {date}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||
"learning.outline.widgets.upgradeCard.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||
"learning.outline.widgets.upgradeCard.expirationMinutes": "Less than 1 hour left",
|
||||
"learning.outline.widgets.upgradeCard.expiration": "Course access will expire {date}",
|
||||
"learning.outline.widgets.upgradeCard.code": "Use code {code} at checkout",
|
||||
"learning.outline.widgets.upgradeCard.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||
"learning.outline.widgets.upgradeCard.accessExpiration": "Upgrade your course today",
|
||||
"learning.outline.widgets.upgradeCard.accessExpirationUrgent": "Course Access Expiration",
|
||||
"learning.outline.widgets.upgradeCard.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink": "verified certificate",
|
||||
"learning.generic.upgradeNotification.verifiedCertMessage": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||
"learning.generic.upgradeNotification.noFBE.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.generic.upgradeNotification.gradedAssignments": "graded assignments",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink.fullAccess": "Full access",
|
||||
"learning.generic.upgradeNotification.FBE.nonProfitMission": "non-profit mission",
|
||||
"learning.generic.upgradeNotification.unlockGraded": "Unlock your access to all course activities, including {gradedAssignments}",
|
||||
"learning.generic.upgradeNotification.fullAccess": "{fullAccess} to course content and materials, even after the course ends",
|
||||
"learning.generic.upgradeNotification.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "including any progress",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss": "You will lose all access to this course, {includingAnyProgress}, on {date}.",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||
"learning.generic.upgradeNotification.expirationMinutes": "Less than 1 hour left",
|
||||
"learning.generic.upgradeNotification.expiration": "Course access will expire {date}",
|
||||
"learning.generic.upgradeNotification.code": "Use code {code} at checkout",
|
||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||
"learning.generic.upgradeNotification.accessExpiration": "Upgrade your course today",
|
||||
"learning.generic.upgradeNotification.accessExpirationUrgent": "Course Access Expiration",
|
||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
||||
"masquerade-widget.userName.input.placeholder": "Username or email",
|
||||
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
"alert.enroll": "You must be enrolled in the course to see course content.",
|
||||
"learning.privateCourse.signInOrRegister": "{signIn} or {register} and then enroll in this course.",
|
||||
"learning.privateCourse.canEnroll": "{enrollNow} to access the full course.",
|
||||
"learning.outline.alert.scheduled-content.heading": "More content is coming soon!",
|
||||
"learning.outline.alert.scheduled-content.body": "This course will have more content released at a future date. Look out for email updates or check back on this course for updates.",
|
||||
"learning.outline.alert.scheduled-content.button": "View Course Schedule",
|
||||
"learning.outline.dates.all": "View all course dates",
|
||||
"learning.outline.collapseAll": "Collapse all",
|
||||
"learning.outline.completedAssignment": "Completed",
|
||||
@@ -159,6 +162,7 @@
|
||||
"progress.weightedGrade": "Weighted grade",
|
||||
"progress.weightedGradeSummary": "Your current weighted grade summary",
|
||||
"progress.header": "Your progress",
|
||||
"progress.header.targetUser": "Course progress for {username}",
|
||||
"progress.link.studio": "View grading in Studio",
|
||||
"progress.relatedLinks.datesCard.description": "A schedule view of your course due dates and upcoming assignments.",
|
||||
"progress.relatedLinks.datesCard.link": "Dates",
|
||||
@@ -283,12 +287,12 @@
|
||||
"learn.course.license.creativeCommons.terms.zero": "No terms",
|
||||
"learn.course.license.creativeCommons.text": "Some Rights Reserved",
|
||||
"learn.breadcrumb.navigation.course.home": "Course",
|
||||
"sidebar.notification.container": "Sidebar notification",
|
||||
"sidebar.open.button": "Show sidebar notification",
|
||||
"sidebar.close.button": "Close sidebar notification",
|
||||
"sidebar.responsive.close.button": "Back to course",
|
||||
"sidebar.notification.title": "Notifications",
|
||||
"sidebar.notification.no.message": "You have no new notifications at this time.",
|
||||
"notification.tray.container": "Notification tray",
|
||||
"notification.open.button": "Show notification tray",
|
||||
"notification.close.button": "Close sidebar notification",
|
||||
"responsive.close.notification": "Back to course",
|
||||
"notification.tray.title": "Notifications",
|
||||
"notification.tray.no.message": "You have no new notifications at this time.",
|
||||
"learn.contentLock.content.locked": "Content Locked",
|
||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
||||
@@ -347,27 +351,28 @@
|
||||
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
||||
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||
"learning.upgradeNowButton.buttonText": "Upgrade now for {pricing}",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertLink": "verified certificate",
|
||||
"learning.outline.widgets.upgradeCard.verifiedCertMessage": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||
"learning.outline.widgets.upgradeCard.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.outline.widgets.upgradeCard.gradedAssignments": "graded assignments",
|
||||
"learning.upgradeCard.verifiedCertLink": "Full access",
|
||||
"learning.upgradeCard.nonProfitMission": "non-profit mission",
|
||||
"learning.outline.widgets.upgradeCard.unlockGraded": "Unlock your access to all course activities, including {gradedAssignments}",
|
||||
"learning.outline.widgets.upgradeCard.fullAccess": "{fullAccess} to course content and materials, even after the course ends",
|
||||
"learning.upgradeCard.expirationAccessLoss.progress": "including any progress",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||
"learning.outline.widgets.upgradeCard.expirationAccessLoss": "You will lose all access to this course, {includingAnyProgress}, on {date}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||
"learning.outline.widgets.upgradeCard.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||
"learning.outline.widgets.upgradeCard.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||
"learning.outline.widgets.upgradeCard.expirationMinutes": "Less than 1 hour left",
|
||||
"learning.outline.widgets.upgradeCard.expiration": "Course access will expire {date}",
|
||||
"learning.outline.widgets.upgradeCard.code": "Use code {code} at checkout",
|
||||
"learning.outline.widgets.upgradeCard.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||
"learning.outline.widgets.upgradeCard.accessExpiration": "Upgrade your course today",
|
||||
"learning.outline.widgets.upgradeCard.accessExpirationUrgent": "Course Access Expiration",
|
||||
"learning.outline.widgets.upgradeCard.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink": "verified certificate",
|
||||
"learning.generic.upgradeNotification.verifiedCertMessage": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||
"learning.generic.upgradeNotification.noFBE.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.generic.upgradeNotification.gradedAssignments": "graded assignments",
|
||||
"learning.generic.upgradeNotification.verifiedCertLink.fullAccess": "Full access",
|
||||
"learning.generic.upgradeNotification.FBE.nonProfitMission": "non-profit mission",
|
||||
"learning.generic.upgradeNotification.unlockGraded": "Unlock your access to all course activities, including {gradedAssignments}",
|
||||
"learning.generic.upgradeNotification.fullAccess": "{fullAccess} to course content and materials, even after the course ends",
|
||||
"learning.generic.upgradeNotification.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "including any progress",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss": "You will lose all access to this course, {includingAnyProgress}, on {date}.",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||
"learning.generic.upgradeNotification.expirationMinutes": "Less than 1 hour left",
|
||||
"learning.generic.upgradeNotification.expiration": "Course access will expire {date}",
|
||||
"learning.generic.upgradeNotification.code": "Use code {code} at checkout",
|
||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||
"learning.generic.upgradeNotification.accessExpiration": "Upgrade your course today",
|
||||
"learning.generic.upgradeNotification.accessExpirationUrgent": "Course Access Expiration",
|
||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
||||
"masquerade-widget.userName.input.placeholder": "Username or email",
|
||||
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
||||
|
||||
@@ -44,11 +44,21 @@ subscribe(APP_READY, () => {
|
||||
<DatesTab />
|
||||
</TabContainer>
|
||||
</PageRoute>
|
||||
<PageRoute path="/c/:courseId/progress">
|
||||
<TabContainer tab="progress" fetch={fetchProgressTab} slice="courseHome">
|
||||
<ProgressTab />
|
||||
</TabContainer>
|
||||
</PageRoute>
|
||||
<PageRoute
|
||||
path={[
|
||||
'/c/:courseId/progress/:targetUserId/',
|
||||
'/c/:courseId/progress',
|
||||
]}
|
||||
render={({ match }) => (
|
||||
<TabContainer
|
||||
tab="progress"
|
||||
fetch={(courseId) => fetchProgressTab(courseId, match.params.targetUserId)}
|
||||
slice="courseHome"
|
||||
>
|
||||
<ProgressTab />
|
||||
</TabContainer>
|
||||
)}
|
||||
/>
|
||||
<PageRoute path="/c/:courseId/course-end">
|
||||
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
|
||||
<CourseExit />
|
||||
|
||||
@@ -372,14 +372,14 @@
|
||||
|
||||
// Import component-specific sass files
|
||||
@import 'courseware/course/celebration/CelebrationModal.scss';
|
||||
@import 'courseware/course/Sidebar.scss';
|
||||
@import 'courseware/course/SidebarNotificationButton.scss';
|
||||
@import 'courseware/course/NotificationTray.scss';
|
||||
@import 'courseware/course/NotificationTrigger.scss';
|
||||
@import 'courseware/course/NotificationIcon.scss';
|
||||
@import 'shared/streak-celebration/StreakCelebrationModal.scss';
|
||||
@import 'courseware/course/content-tools/calculator/calculator.scss';
|
||||
@import 'courseware/course/content-tools/contentTools.scss';
|
||||
@import 'course-home/dates-tab/timeline/Day.scss';
|
||||
@import 'generic/upgrade-card/UpgradeCard.scss';
|
||||
@import 'generic/upgrade-notification/UpgradeNotification.scss';
|
||||
@import 'course-home/outline-tab/widgets/ProctoringInfoPanel.scss';
|
||||
@import 'course-home/progress-tab/course-completion/CompletionDonutChart.scss';
|
||||
@import 'course-home/progress-tab/grades/course-grade/GradeBar.scss';
|
||||
|
||||
@@ -124,7 +124,12 @@ export function buildMinimalCourseBlocks(courseId, title, options = {}) {
|
||||
)];
|
||||
const courseBlock = options.courseBlock || Factory.build(
|
||||
'block',
|
||||
{ type: 'course', display_name: title, children: sectionBlocks.map(block => block.id) },
|
||||
{
|
||||
type: 'course',
|
||||
display_name: title,
|
||||
has_scheduled_content: options.hasScheduledContent || false,
|
||||
children: sectionBlocks.map(block => block.id),
|
||||
},
|
||||
{ courseId },
|
||||
);
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { OuterExamTimer } from '@edx/frontend-lib-special-exams';
|
||||
|
||||
import TabPage from './TabPage';
|
||||
|
||||
@@ -34,6 +35,7 @@ export default function TabContainer(props) {
|
||||
courseStatus={courseStatus}
|
||||
metadataModel={`${slice}Meta`}
|
||||
>
|
||||
{courseId && <OuterExamTimer courseId={courseId} />}
|
||||
{children}
|
||||
</TabPage>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { initializeTestStore, render, screen } from '../setupTest';
|
||||
import { TabContainer } from './index';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
const mockFetch = jest.fn().mockImplementation((x) => x);
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
@@ -13,15 +13,18 @@ jest.mock('react-redux', () => ({
|
||||
jest.mock('./TabPage', () => () => <div data-testid="TabPage" />);
|
||||
|
||||
describe('Tab Container', () => {
|
||||
const mockData = {
|
||||
children: [],
|
||||
fetch: mockFetch,
|
||||
tab: 'dummy',
|
||||
slice: 'courseware',
|
||||
};
|
||||
let courseId;
|
||||
let mockFetch;
|
||||
let mockData;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
mockFetch = jest.fn().mockImplementation((x) => x);
|
||||
mockData = {
|
||||
children: [],
|
||||
fetch: mockFetch,
|
||||
tab: 'dummy',
|
||||
slice: 'courseware',
|
||||
};
|
||||
const store = await initializeTestStore({ excludeFetchSequence: true });
|
||||
courseId = store.getState().courseware.courseId;
|
||||
});
|
||||
@@ -42,4 +45,26 @@ describe('Tab Container', () => {
|
||||
.toHaveBeenCalledWith(courseId);
|
||||
expect(screen.getByTestId('TabPage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should handle passing in a targetUserId', () => {
|
||||
const targetUserId = '1';
|
||||
history.push(`/course/${courseId}/progress/${targetUserId}/`);
|
||||
|
||||
render(
|
||||
<Route
|
||||
path="/course/:courseId/progress/:targetUserId/"
|
||||
render={({ match }) => (
|
||||
<TabContainer
|
||||
fetch={() => mockFetch(match.params.courseId, match.params.targetUserId)}
|
||||
slice="courseHome"
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(mockFetch)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith(courseId, targetUserId);
|
||||
expect(screen.getByTestId('TabPage')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user