diff --git a/README.md b/README.md index 610c47af..e6eb4635 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,45 @@ Some guidelines for writing widgets: * You can load data from the redux store, but should not add or modify fields in that structure. * Network events should be managed in component hooks, though can use our `data/constants/requests:requestStates` for ease of tracking the request states. + ## License + +The code in this repository is licensed under the AGPLv3 unless otherwise +noted. + +Please see `LICENSE `_ for details. + +## Getting Help + +If you're having trouble, we have discussion forums at +https://discuss.openedx.org where you can connect with others in the community. + +Our real-time conversations are on Slack. You can request a `Slack +invitation`_, then join our `community Slack workspace`_. Because this is a +frontend repository, the best place to discuss it would be in the `#wg-frontend +channel`_. + +For anything non-trivial, the best path is to open an issue in this repository +with as many details about the issue you are facing as you can provide. + +https://github.com/openedx/frontend-app-learner-dashboard/issues + +For more information about these options, see the `Getting Help`_ page. + +.. _Slack invitation: https://openedx.org/slack +.. _community Slack workspace: https://openedx.slack.com/ +.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6 +.. _Getting Help: https://openedx.org/community/connect + ## Resources * [Learner Home project info at the Open edX Wiki](https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3575906333/Learner+Home) + +## The Open edX Code of Conduct + +All community members are expected to follow the `Open edX Code of Conduct`_. + +.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/ + +## Reporting Security Issues + +Please do not report security issues in public. Please email security@openedx.org. diff --git a/src/components/EmailLink.jsx b/src/components/EmailLink.jsx deleted file mode 100644 index d57ec677..00000000 --- a/src/components/EmailLink.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { MailtoLink } from '@edx/paragon'; - -export const EmailLink = ({ address }) => { - if (!address) { - return null; - } - return ( - {address} - ); -}; -EmailLink.defaultProps = { address: null }; -EmailLink.propTypes = { address: PropTypes.string }; - -export default EmailLink; diff --git a/src/components/EmailLink.test.jsx b/src/components/EmailLink.test.jsx deleted file mode 100644 index 2e1ab16e..00000000 --- a/src/components/EmailLink.test.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { shallow } from 'enzyme'; - -import EmailLink from './EmailLink'; - -describe('EmailLink', () => { - it('renders null when no address is provided', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.isEmptyRender()).toEqual(true); - }); - it('renders a MailtoLink when an address is provided', () => { - const wrapper = shallow(); - expect(wrapper.find('MailtoLink').length).toEqual(1); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/src/components/__snapshots__/EmailLink.test.jsx.snap b/src/components/__snapshots__/EmailLink.test.jsx.snap deleted file mode 100644 index 3515e1d2..00000000 --- a/src/components/__snapshots__/EmailLink.test.jsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EmailLink renders a MailtoLink when an address is provided 1`] = ` - - test@email.com - -`; - -exports[`EmailLink renders null when no address is provided 1`] = `""`; diff --git a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx index 1bae25fb..70083395 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx @@ -26,17 +26,14 @@ export const CertificateBanner = ({ cardId }) => { const { formatMessage } = useIntl(); const formatDate = useFormatDate(); - const emailLink = address => address && {address}; + const emailLink = address => {address}; if (certificate.isRestricted) { return ( - {formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })} + { supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)} {isVerified && ' '} - {isVerified && formatMessage( - messages.certRefundContactBilling, - { billingEmail: emailLink(billingEmail) }, - )} + {isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))} ); } diff --git a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx index cf8c20d6..68b8afcc 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx @@ -21,10 +21,6 @@ jest.mock('components/Banner', () => 'Banner'); describe('CertificateBanner', () => { const props = { cardId: 'cardId' }; - reduxHooks.usePlatformSettingsData.mockReturnValue({ - supportEmail: 'suport@email', - billingEmail: 'billing@email', - }); reduxHooks.useCardCourseRunData.mockReturnValue({ minPassingGrade: 0.8, progressUrl: 'progressUrl', @@ -42,16 +38,19 @@ describe('CertificateBanner', () => { }; const defaultCourseRun = { isArchived: false }; const defaultGrade = { isPassing: false }; + const defaultPlatformSettings = {}; const createWrapper = ({ certificate = {}, enrollment = {}, grade = {}, courseRun = {}, + platformSettings = {}, }) => { reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade }); reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate }); reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment }); reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun }); + reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings }); return shallow(); }; /** TODO: Update tests to validate snapshots **/ @@ -64,6 +63,28 @@ describe('CertificateBanner', () => { }); expect(wrapper).toMatchSnapshot(); }); + test('is restricted with support email', () => { + const wrapper = createWrapper({ + certificate: { + isRestricted: true, + }, + platformSettings: { + supportEmail: 'suport@email', + }, + }); + expect(wrapper).toMatchSnapshot(); + }); + test('is restricted with billing email', () => { + const wrapper = createWrapper({ + certificate: { + isRestricted: true, + }, + platformSettings: { + billingEmail: 'billing@email', + }, + }); + expect(wrapper).toMatchSnapshot(); + }); test('is restricted and verified', () => { const wrapper = createWrapper({ certificate: { @@ -75,6 +96,49 @@ describe('CertificateBanner', () => { }); expect(wrapper).toMatchSnapshot(); }); + test('is restricted and verified with support email', () => { + const wrapper = createWrapper({ + certificate: { + isRestricted: true, + }, + enrollment: { + isVerified: true, + }, + platformSettings: { + supportEmail: 'suport@email', + }, + }); + expect(wrapper).toMatchSnapshot(); + }); + test('is restricted and verified with billing email', () => { + const wrapper = createWrapper({ + certificate: { + isRestricted: true, + }, + enrollment: { + isVerified: true, + }, + platformSettings: { + billingEmail: 'billing@email', + }, + }); + expect(wrapper).toMatchSnapshot(); + }); + test('is restricted and verified with support and billing email', () => { + const wrapper = createWrapper({ + certificate: { + isRestricted: true, + }, + enrollment: { + isVerified: true, + }, + platformSettings: { + supportEmail: 'suport@email', + billingEmail: 'billing@email', + }, + }); + expect(wrapper).toMatchSnapshot(); + }); test('is passing and is downloadable', () => { const wrapper = createWrapper({ grade: { isPassing: true }, @@ -133,6 +197,10 @@ describe('CertificateBanner', () => { certificate: { isRestricted: true, }, + platformSettings: { + supportEmail: 'suport@email', + billingEmail: 'billing@email', + }, }); const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n'); expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage); @@ -146,6 +214,10 @@ describe('CertificateBanner', () => { enrollment: { isVerified: true, }, + platformSettings: { + supportEmail: 'suport@email', + billingEmail: 'billing@email', + }, }); const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n'); expect(bannerMessage).toContain(messages.certRestricted.defaultMessage); diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/__snapshots__/index.test.jsx.snap index 4096091a..96b1a629 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/__snapshots__/index.test.jsx.snap @@ -17,9 +17,11 @@ exports[`CreditBanner component render with error state snapshot 1`] = ` } values={ Object { - "supportEmailLink": , + "supportEmailLink": + test-support-email + , } } /> @@ -30,6 +32,21 @@ exports[`CreditBanner component render with error state snapshot 1`] = ` `; +exports[`CreditBanner component render with error state with no email snapshot 1`] = ` + +

+ An error occurred with this transaction. +

+ +
+`; + exports[`CreditBanner component render with no error state snapshot 1`] = ` { if (hookData === null) { return null; } + const { ContentComponent, error, supportEmail } = hookData; - const supportEmailLink = (); + const supportEmailLink = ({supportEmail}); return ( {error && (

- {formatMessage(messages.error, { supportEmailLink })} + {supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}

)} diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/index.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/index.test.jsx index f8fea3c2..68f285bf 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/index.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/index.test.jsx @@ -2,15 +2,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { formatMessage } from 'testUtils'; - -import EmailLink from 'components/EmailLink'; +import { MailtoLink } from '@edx/paragon'; import hooks from './hooks'; import messages from './messages'; import CreditBanner from '.'; jest.mock('components/Banner', () => 'Banner'); -jest.mock('components/EmailLink', () => 'EmailLink'); jest.mock('./hooks', () => ({ useCreditBannerData: jest.fn(), @@ -54,7 +52,7 @@ describe('CreditBanner component', () => { it('includes credit-error-msg with support email link', () => { expect(el.find('.credit-error-msg').containsMatchingElement( formatMessage(messages.error, { - supportEmailLink: (), + supportEmailLink: ({supportEmail}), }), )).toEqual(true); }); @@ -62,6 +60,25 @@ describe('CreditBanner component', () => { expect(el.find('ContentComponent').props().cardId).toEqual(cardId); }); }); + + describe('with error state with no email', () => { + beforeEach(() => { + hooks.useCreditBannerData.mockReturnValue({ + error: true, + ContentComponent, + }); + el = shallow(); + }); + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + it('includes credit-error-msg without support email link', () => { + expect(el.find('.credit-error-msg').containsMatchingElement( + formatMessage(messages.errorNoEmail), + )).toEqual(true); + }); + }); + describe('with no error state', () => { beforeEach(() => { hooks.useCreditBannerData.mockReturnValue({ diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/messages.js b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/messages.js index bf0adaaa..7c1d3973 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/messages.js +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/messages.js @@ -6,6 +6,11 @@ export const messages = StrictDict({ description: '', defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.', }, + errorNoEmail: { + id: 'learner-dash.courseCard.banners.credit.errorNoEmail', + description: '', + defaultMessage: 'An error occurred with this transaction.', + }, }); export default messages; diff --git a/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap index b3375327..077fe8d4 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap @@ -21,20 +21,40 @@ exports[`CertificateBanner snapshot is restricted 1`] = ` + Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know. + +`; + +exports[`CertificateBanner snapshot is restricted and verified 1`] = ` + + Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know. + + If you would like a refund on your Certificate of Achievement, please contact us. + +`; + +exports[`CertificateBanner snapshot is restricted and verified with billing email 1`] = ` + + Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know. + - suport@email + billing@email , } } @@ -42,7 +62,7 @@ exports[`CertificateBanner snapshot is restricted 1`] = ` `; -exports[`CertificateBanner snapshot is restricted and verified 1`] = ` +exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = ` @@ -86,6 +106,66 @@ exports[`CertificateBanner snapshot is restricted and verified 1`] = ` `; +exports[`CertificateBanner snapshot is restricted and verified with support email 1`] = ` + + + suport@email + , + } + } + /> + + If you would like a refund on your Certificate of Achievement, please contact us. + +`; + +exports[`CertificateBanner snapshot is restricted with billing email 1`] = ` + + Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know. + +`; + +exports[`CertificateBanner snapshot is restricted with support email 1`] = ` + + + suport@email + , + } + } + /> + +`; + exports[`CertificateBanner snapshot not passing and audit 1`] = ` Grade required to pass the course: 0.8‏% diff --git a/src/containers/CourseCard/components/CourseCardBanners/messages.js b/src/containers/CourseCard/components/CourseCardBanners/messages.js index 7a9b8a82..525f4b24 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/messages.js +++ b/src/containers/CourseCard/components/CourseCardBanners/messages.js @@ -31,11 +31,21 @@ export const messages = StrictDict({ description: 'Restricted certificate warning message', defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.', }, + certRestrictedNoEmail: { + id: 'learner-dash.courseCard.banners.certificateRestrictedNoEmail', + description: 'Restricted certificate warning message', + defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.', + }, certRefundContactBilling: { id: 'learner-dash.courseCard.banners.certificateRefundContactBilling', description: 'Message to learners to contact billing for certificate refunds', defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}', }, + certRefundContactBillingNoEmail: { + id: 'learner-dash.courseCard.banners.certificateRefundContactBillingNoEmail', + description: 'Message to learners to contact billing for certificate refunds', + defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact us.', + }, passingGrade: { id: 'learner-dash.courseCard.banners.passingGrade', description: 'Message to learners with minimum passing grade for the course', diff --git a/src/containers/CourseFilterControls/CourseFilterControls.jsx b/src/containers/CourseFilterControls/CourseFilterControls.jsx index c4a774a4..03b420d4 100644 --- a/src/containers/CourseFilterControls/CourseFilterControls.jsx +++ b/src/containers/CourseFilterControls/CourseFilterControls.jsx @@ -68,7 +68,7 @@ export const CourseFilterControls = ({ onClose={close} >
- Refine + {formatMessage(messages.refine)}

diff --git a/src/containers/CourseFilterControls/messages.js b/src/containers/CourseFilterControls/messages.js index fa368292..af895dca 100644 --- a/src/containers/CourseFilterControls/messages.js +++ b/src/containers/CourseFilterControls/messages.js @@ -1,6 +1,6 @@ -import { StrictDict } from 'utils'; +import { defineMessages } from '@edx/frontend-platform/i18n'; -export const messages = StrictDict({ +const messages = defineMessages({ inProgress: { id: 'learner-dash.courseListFilters.inProgress', description: 'in-progress filter checkbox label for course list filters', @@ -52,4 +52,5 @@ export const messages = StrictDict({ defaultMessage: 'Refine', }, }); + export default messages; diff --git a/src/containers/LearnerDashboardHeader/ExpandedHeader/AuthenticatedUserDropdown.jsx b/src/containers/LearnerDashboardHeader/ExpandedHeader/AuthenticatedUserDropdown.jsx index e84cb468..eaa74486 100644 --- a/src/containers/LearnerDashboardHeader/ExpandedHeader/AuthenticatedUserDropdown.jsx +++ b/src/containers/LearnerDashboardHeader/ExpandedHeader/AuthenticatedUserDropdown.jsx @@ -29,9 +29,9 @@ export const AuthenticatedUserDropdown = () => { - SWITCH DASHBOARD + {formatMessage(messages.dashboardSwitch)} - Personal + {formatMessage(messages.dashboardPersonal)} {!!dashboard && ( diff --git a/src/containers/LearnerDashboardHeader/messages.js b/src/containers/LearnerDashboardHeader/messages.js index 8ac4e04a..08c71cd7 100644 --- a/src/containers/LearnerDashboardHeader/messages.js +++ b/src/containers/LearnerDashboardHeader/messages.js @@ -6,6 +6,16 @@ const messages = defineMessages({ defaultMessage: 'Dashboard', description: 'The text for the user menu Dashboard navigation link.', }, + dashboardPersonal: { + id: 'learnerVariantDashboard.menu.dashboardPersonal.label', + defaultMessage: 'Personal', + description: 'Link to personal dashboard in user menu', + }, + dashboardSwitch: { + id: 'learnerVariantDashboard.menu.dashboardSwitch.label', + defaultMessage: 'SWITCH DASHBOARD', + description: 'Switch Dashboard header in the user menu', + }, help: { id: 'learnerVariantDashboard.help.label', defaultMessage: 'Help', diff --git a/src/widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext.jsx b/src/widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext.jsx index 51e2c06d..a88e0f25 100644 --- a/src/widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext.jsx +++ b/src/widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext.jsx @@ -68,7 +68,13 @@ export const useIsEnterpriseUser = () => { return enterpriseUser; }; -export const PaintedDoorExperimentContext = React.createContext(); +export const PaintedDoorExperimentContext = React.createContext({ + experimentVariation: null, + isPaintedDoorNavbarBtnVariation: null, + isPaintedDoorWidgetBtnVariation: null, + isPaintedDoorControlVariation: null, + experimentLoading: null, +}); export const PaintedDoorExperimentProvider = ({ children }) => { const [experimentData, setExperimentData] = module.state.experimentData({