diff --git a/src/account-settings/BetaLanguageBanner.jsx b/src/account-settings/BetaLanguageBanner.jsx index e309857..b73a820 100644 --- a/src/account-settings/BetaLanguageBanner.jsx +++ b/src/account-settings/BetaLanguageBanner.jsx @@ -1,7 +1,7 @@ -import React from 'react'; +import { useContext } from 'react'; import PropTypes from 'prop-types'; import { AppContext } from '@edx/frontend-platform/react'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { connect } from 'react-redux'; import { Button, Hyperlink } from '@openedx/paragon'; @@ -11,18 +11,11 @@ import { saveSettings } from './data/actions'; import { TRANSIFEX_LANGUAGE_BASE_URL } from './data/constants'; import Alert from './Alert'; -class BetaLanguageBanner extends React.Component { - getSiteLanguageEntry(languageCode) { - return this.props.siteLanguageList.filter(l => l.code === languageCode)[0]; - } +const BetaLanguageBanner = ({ siteLanguage = null, siteLanguageList }) => { + const intl = useIntl(); + const { locale } = useContext(AppContext); - /** - * Returns a link to the Transifex URL where contributors can provide translations. - * This code is tightly coupled to how Transifex chooses to design its URLs. - */ - getTransifexLink(languageCode) { - return TRANSIFEX_LANGUAGE_BASE_URL + this.getTransifexURLPath(languageCode); - } + const getSiteLanguageEntry = (languageCode) => siteLanguageList.filter(l => l.code === languageCode)[0]; /** * Returns the URL path that Transifex chooses to use for its language sub-pages. @@ -34,67 +27,70 @@ class BetaLanguageBanner extends React.Component { * For short language codes, it returns the code as is. * example: fr -> fr */ - getTransifexURLPath(languageCode) { + const getTransifexURLPath = (languageCode) => { const tokenizedCode = languageCode.split('-'); if (tokenizedCode.length > 1) { return `${tokenizedCode[0]}_${tokenizedCode[1].toUpperCase()}`; } return tokenizedCode[0]; - } - - handleRevertLanguage = () => { - const previousSiteLanguage = this.props.siteLanguage.previousValue; - this.props.saveSettings('siteLanguage', previousSiteLanguage); }; - render() { - const savedLanguage = this.getSiteLanguageEntry(this.context.locale); - if (!savedLanguage) { - return null; - } - const isSavedLanguageReleased = savedLanguage.released === true; - const noPreviousLanguageSet = this.props.siteLanguage.previousValue === null; - if (isSavedLanguageReleased || noPreviousLanguageSet) { - return null; - } + /** + * Returns a link to the Transifex URL where contributors can provide translations. + * This code is tightly coupled to how Transifex chooses to design its URLs. + */ + const getTransifexLink = (languageCode) => TRANSIFEX_LANGUAGE_BASE_URL + getTransifexURLPath(languageCode); - const previousLanguage = this.getSiteLanguageEntry(this.props.siteLanguage.previousValue); - return ( -
- -

- {this.props.intl.formatMessage(messages['account.settings.banner.beta.language'], { - beta_language: savedLanguage.name, - })} -

-
- - - {this.props.intl.formatMessage( - messages['account.settings.banner.beta.language.action.help.translate'], - { beta_language: savedLanguage.name }, - )} - -
-
-
- ); + const handleRevertLanguage = () => { + const previousSiteLanguage = siteLanguage.previousValue; + saveSettings('siteLanguage', previousSiteLanguage); + }; + + const savedLanguage = getSiteLanguageEntry(locale); + if (!savedLanguage) { + return null; } -} + const isSavedLanguageReleased = savedLanguage.released === true; + const noPreviousLanguageSet = siteLanguage.previousValue === null; + if (isSavedLanguageReleased || noPreviousLanguageSet) { + return null; + } + + const previousLanguage = getSiteLanguageEntry(siteLanguage.previousValue); + return ( +
+ +

+ {intl.formatMessage(messages['account.settings.banner.beta.language'], { + beta_language: savedLanguage.name, + })} +

+
+ + + {intl.formatMessage( + messages['account.settings.banner.beta.language.action.help.translate'], + { beta_language: savedLanguage.name }, + )} + +
+
+
+ ); +}; BetaLanguageBanner.contextType = AppContext; BetaLanguageBanner.propTypes = { - intl: intlShape.isRequired, siteLanguage: PropTypes.shape({ previousValue: PropTypes.string, draft: PropTypes.string, @@ -104,11 +100,6 @@ BetaLanguageBanner.propTypes = { code: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), released: PropTypes.bool, })).isRequired, - saveSettings: PropTypes.func.isRequired, -}; - -BetaLanguageBanner.defaultProps = { - siteLanguage: null, }; export default connect( @@ -116,4 +107,4 @@ export default connect( { saveSettings, }, -)(injectIntl(BetaLanguageBanner)); +)(BetaLanguageBanner); diff --git a/src/account-settings/delete-account/ConfirmationModal.jsx b/src/account-settings/delete-account/ConfirmationModal.jsx index 98f3ff5..2812244 100644 --- a/src/account-settings/delete-account/ConfirmationModal.jsx +++ b/src/account-settings/delete-account/ConfirmationModal.jsx @@ -1,11 +1,10 @@ -import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { AlertModal, Button, Form, ActionRow, } from '@openedx/paragon'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { getConfig } from '@edx/frontend-platform'; @@ -13,12 +12,20 @@ import messages from './messages'; import Alert from '../Alert'; import PrintingInstructions from './PrintingInstructions'; -export class ConfirmationModal extends Component { +export const ConfirmationModal = ({ + status = null, + errorType = null, + onCancel, + onChange, + onSubmit, + password, +}) => { + const intl = useIntl(); /** * @returns String The message id for a short description of the error, suitable for a header or * as the error message under an input field. */ - getShortErrorMessageId(reason) { + const getShortErrorMessageId = (reason) => { switch (reason) { case 'empty-password': return 'account.settings.delete.account.error.no.password'; @@ -27,15 +34,13 @@ export class ConfirmationModal extends Component { default: return 'account.settings.delete.account.error.unable.to.delete'; } - } - - renderError(reason) { - const { errorType, intl } = this.props; + }; + const renderError = (reason) => { if (errorType === null) { return null; } - const headerMessageId = this.getShortErrorMessageId(errorType); + const headerMessageId = getShortErrorMessageId(errorType); const detailsMessageId = reason === 'empty-password' ? null : 'account.settings.delete.account.error.unable.to.delete.details'; @@ -51,103 +56,86 @@ export class ConfirmationModal extends Component { ) : null} ); - } + }; - render() { - const { - status, - errorType, - intl, - onCancel, - onChange, - onSubmit, - password, - } = this.props; - const open = ['confirming', 'pending', 'failed'].includes(status); - const passwordFieldId = 'passwordFieldId'; - const invalidMessage = messages[this.getShortErrorMessageId(errorType)]; + const open = ['confirming', 'pending', 'failed'].includes(status); + const passwordFieldId = 'passwordFieldId'; + const invalidMessage = messages[getShortErrorMessageId(errorType)]; - // TODO: We lack a good way of providing custom language for a particular site. This is a hack - // to allow edx.org to fulfill its business requirements. - const deleteAccountModalText2MessageKey = getConfig().SITE_NAME === 'edX' - ? 'account.settings.delete.account.modal.text.2.edX' - : 'account.settings.delete.account.modal.text.2'; + // TODO: We lack a good way of providing custom language for a particular site. This is a hack + // to allow edx.org to fulfill its business requirements. + const deleteAccountModalText2MessageKey = getConfig().SITE_NAME === 'edX' + ? 'account.settings.delete.account.modal.text.2.edX' + : 'account.settings.delete.account.modal.text.2'; - return ( - - - - + return ( + + + + )} - > -
- {this.renderError()} - } - > -
- {intl.formatMessage( - messages['account.settings.delete.account.modal.text.1'], - { siteName: getConfig().SITE_NAME }, - )} -
-

- {intl.formatMessage( - messages[deleteAccountModalText2MessageKey], - { siteName: getConfig().SITE_NAME }, - )} -

-

- -

-
- - - {intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])} - - - {errorType !== null && ( - - {intl.formatMessage(invalidMessage)} - + > +
+ {renderError()} + } + > +
+ {intl.formatMessage( + messages['account.settings.delete.account.modal.text.1'], + { siteName: getConfig().SITE_NAME }, )} - -
+ +

+ {intl.formatMessage( + messages[deleteAccountModalText2MessageKey], + { siteName: getConfig().SITE_NAME }, + )} +

+

+ +

+ + + + {intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])} + + + {errorType !== null && ( + + {intl.formatMessage(invalidMessage)} + + )} + +
-
- ); - } -} +
+ ); +}; ConfirmationModal.propTypes = { status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']), errorType: PropTypes.oneOf(['empty-password', 'server']), - intl: intlShape.isRequired, onCancel: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, password: PropTypes.string.isRequired, }; -ConfirmationModal.defaultProps = { - status: null, - errorType: null, -}; - -export default injectIntl(ConfirmationModal); +export default ConfirmationModal; diff --git a/src/account-settings/delete-account/ConfirmationModal.test.jsx b/src/account-settings/delete-account/ConfirmationModal.test.jsx index 833e291..cad4137 100644 --- a/src/account-settings/delete-account/ConfirmationModal.test.jsx +++ b/src/account-settings/delete-account/ConfirmationModal.test.jsx @@ -1,6 +1,5 @@ -import React from 'react'; import renderer from 'react-test-renderer'; -import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; // Modal creates a portal. Overriding createPortal allows portals to be tested in jest. jest.mock('react-dom', () => ({ @@ -10,8 +9,6 @@ jest.mock('react-dom', () => ({ import { ConfirmationModal } from './ConfirmationModal'; // eslint-disable-line import/first -const IntlConfirmationModal = injectIntl(ConfirmationModal); - describe('ConfirmationModal', () => { let props = {}; @@ -30,7 +27,7 @@ describe('ConfirmationModal', () => { const tree = renderer .create(( - @@ -43,7 +40,7 @@ describe('ConfirmationModal', () => { const tree = renderer .create(( - @@ -57,7 +54,7 @@ describe('ConfirmationModal', () => { const tree = renderer .create(( - { + const intl = useIntl(); + const [password, setPassword] = useState(''); - this.state = { - password: '', - }; - } - - handleSubmit = () => { - if (this.state.password === '') { - this.props.deleteAccountFailure('empty-password'); + const handleSubmit = () => { + if (password === '') { + deleteAccountFailure('empty-password'); } else { - this.props.deleteAccount(this.state.password); + deleteAccount(password); } }; - handleCancel = () => { - this.setState({ password: '' }); - this.props.deleteAccountCancel(); + const handleCancel = () => { + setPassword(''); + deleteAccountCancel(); }; - handlePasswordChange = (e) => { - this.setState({ password: e.target.value.trim() }); - this.props.deleteAccountReset(); + const handlePasswordChange = (e) => { + setPassword(e.target.value.trim()); + deleteAccountReset(); }; - handleFinalClose = () => { + const handleFinalClose = () => { global.location = getConfig().LOGOUT_URL; }; - render() { - const { - hasLinkedTPA, isVerifiedAccount, status, errorType, intl, - } = this.props; - const canDelete = isVerifiedAccount && !hasLinkedTPA; - const supportArticleUrl = process.env.SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT; + const canDelete = isVerifiedAccount && !hasLinkedTPA; + const supportArticleUrl = process.env.SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT || ''; - // TODO: We lack a good way of providing custom language for a particular site. This is a hack - // to allow edx.org to fulfill its business requirements. - const deleteAccountText2MessageKey = getConfig().SITE_NAME === 'edX' - ? 'account.settings.delete.account.text.2.edX' - : 'account.settings.delete.account.text.2'; + // TODO: We lack a good way of providing custom language for a particular site. This is a hack + // to allow edx.org to fulfill its business requirements. + const deleteAccountText2MessageKey = getConfig().SITE_NAME === 'edX' + ? 'account.settings.delete.account.text.2.edX' + : 'account.settings.delete.account.text.2'; - const optInInstructionMessageId = getConfig().MARKETING_EMAILS_OPT_IN - ? 'account.settings.delete.account.please.confirm' - : 'account.settings.delete.account.please.activate'; + const optInInstructionMessageId = getConfig().MARKETING_EMAILS_OPT_IN + ? 'account.settings.delete.account.please.confirm' + : 'account.settings.delete.account.please.activate'; - return ( -
-

- {intl.formatMessage(messages['account.settings.delete.account.header'])} -

- { - this.props.canDeleteAccount ? ( + return ( +
+

+ {intl.formatMessage(messages['account.settings.delete.account.header'])} +

+ { + canDeleteAccount ? ( <>

{intl.formatMessage(messages['account.settings.delete.account.subheader'])}

@@ -109,7 +106,7 @@ export class DeleteAccount extends React.Component {

- ); - } -} +
+ ); +}; DeleteAccount.propTypes = { - deleteAccount: PropTypes.func.isRequired, - deleteAccountConfirmation: PropTypes.func.isRequired, - deleteAccountFailure: PropTypes.func.isRequired, - deleteAccountReset: PropTypes.func.isRequired, - deleteAccountCancel: PropTypes.func.isRequired, status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']), errorType: PropTypes.oneOf(['empty-password', 'server']), hasLinkedTPA: PropTypes.bool, isVerifiedAccount: PropTypes.bool, canDeleteAccount: PropTypes.bool, - intl: intlShape.isRequired, -}; - -DeleteAccount.defaultProps = { - hasLinkedTPA: false, - isVerifiedAccount: true, - status: null, - errorType: null, - canDeleteAccount: true, }; // Assume we're part of the accountSettings state. @@ -183,4 +162,4 @@ export default connect( deleteAccountReset, deleteAccountCancel, }, -)(injectIntl(DeleteAccount)); +)(DeleteAccount); diff --git a/src/account-settings/delete-account/DeleteAccount.test.jsx b/src/account-settings/delete-account/DeleteAccount.test.jsx index eb97f1d..15fcec7 100644 --- a/src/account-settings/delete-account/DeleteAccount.test.jsx +++ b/src/account-settings/delete-account/DeleteAccount.test.jsx @@ -1,7 +1,6 @@ /* eslint-disable react/jsx-no-useless-fragment */ -import React from 'react'; import renderer from 'react-test-renderer'; -import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; // Testing the modals separately, they just clutter up the snapshots if included here. jest.mock('./ConfirmationModal', () => function ConfirmationModalMock() { @@ -11,20 +10,21 @@ jest.mock('./SuccessModal', () => function SuccessModalMock() { return <>; }); -import { DeleteAccount } from './DeleteAccount'; // eslint-disable-line import/first +jest.mock('./data/actions', () => ({ + deleteAccount: jest.fn(), + deleteAccountConfirmation: jest.fn(), + deleteAccountFailure: jest.fn(), + deleteAccountReset: jest.fn(), + deleteAccountCancel: jest.fn(), +})); -const IntlDeleteAccount = injectIntl(DeleteAccount); +import { DeleteAccount } from './DeleteAccount'; // eslint-disable-line import/first describe('DeleteAccount', () => { let props = {}; beforeEach(() => { props = { - deleteAccount: jest.fn(), - deleteAccountConfirmation: jest.fn(), - deleteAccountFailure: jest.fn(), - deleteAccountReset: jest.fn(), - deleteAccountCancel: jest.fn(), status: null, errorType: null, hasLinkedTPA: false, @@ -36,7 +36,7 @@ describe('DeleteAccount', () => { const tree = renderer .create(( - @@ -50,7 +50,7 @@ describe('DeleteAccount', () => { const tree = renderer .create(( - @@ -64,7 +64,7 @@ describe('DeleteAccount', () => { const tree = renderer .create(( -