From 1a51ac07a284b206ea071d6ab067950d74eb3c47 Mon Sep 17 00:00:00 2001 From: Mashal Malik <107556986+Mashal-m@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:12:14 +0500 Subject: [PATCH 1/4] refactor: updated README file to reflect template changes (#229) --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 610c47a..e6eb463 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. From 0cedeb08096fada0b755d7b10cbdf310a2c64674 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 13 Oct 2023 15:58:19 -0400 Subject: [PATCH 2/4] fix: default context for painted door experiment There are multiple places where attributes of the object provided by `usePaintedDoorExperimentContext()` are assumed to exist. This provides default (null) values for those attributes when creating the context. --- .../PaintedDoorExperimentContext.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext.jsx b/src/widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext.jsx index 51e2c06..a88e0f2 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({ From c2a20af9b8cc37460139b91b95e6253a6e9d4524 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 13 Oct 2023 16:34:01 -0400 Subject: [PATCH 3/4] fix: replace hardcoded strings and properly define i18n messages --- .../CourseFilterControls/CourseFilterControls.jsx | 2 +- src/containers/CourseFilterControls/messages.js | 5 +++-- .../ExpandedHeader/AuthenticatedUserDropdown.jsx | 4 ++-- src/containers/LearnerDashboardHeader/messages.js | 10 ++++++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/containers/CourseFilterControls/CourseFilterControls.jsx b/src/containers/CourseFilterControls/CourseFilterControls.jsx index c4a774a..03b420d 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 fa36829..af895dc 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 e84cb46..eaa7448 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 8ac4e04..08c71cd 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', From b83f128f813dff4e98d45be3c65254f910e005c6 Mon Sep 17 00:00:00 2001 From: Cindy Nguyen Date: Mon, 23 Oct 2023 10:44:20 -0400 Subject: [PATCH 4/4] fix: MailToLink to account for no emails --- src/components/EmailLink.jsx | 17 ---- src/components/EmailLink.test.jsx | 16 ---- .../__snapshots__/EmailLink.test.jsx.snap | 11 --- .../CourseCardBanners/CertificateBanner.jsx | 9 +- .../CertificateBanner.test.jsx | 80 +++++++++++++++- .../__snapshots__/index.test.jsx.snap | 23 ++++- .../CourseCardBanners/CreditBanner/index.jsx | 7 +- .../CreditBanner/index.test.jsx | 25 ++++- .../CreditBanner/messages.js | 5 + .../CertificateBanner.test.jsx.snap | 94 +++++++++++++++++-- .../components/CourseCardBanners/messages.js | 10 ++ 11 files changed, 226 insertions(+), 71 deletions(-) delete mode 100644 src/components/EmailLink.jsx delete mode 100644 src/components/EmailLink.test.jsx delete mode 100644 src/components/__snapshots__/EmailLink.test.jsx.snap diff --git a/src/components/EmailLink.jsx b/src/components/EmailLink.jsx deleted file mode 100644 index d57ec67..0000000 --- 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 2e1ab16..0000000 --- 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 3515e1d..0000000 --- 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 1bae25f..7008339 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 cf8c20d..68b8afc 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 4096091..96b1a62 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 f8fea3c..68f285b 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 bf0adaa..7c1d397 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 b337532..077fe8d 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 7a9b8a8..525f4b2 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',