refactor: Replace of injectIntl with useIntl()

This commit is contained in:
diana-villalvazo-wgu
2025-07-21 14:20:18 -06:00
parent f6babc2db9
commit 5f42857332
5 changed files with 204 additions and 249 deletions

View File

@@ -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 (
<div>
<Alert className="beta_language_alert alert alert-warning" role="alert">
<p>
{this.props.intl.formatMessage(messages['account.settings.banner.beta.language'], {
beta_language: savedLanguage.name,
})}
</p>
<div>
<Button onClick={this.handleRevertLanguage} className="mr-2">
{this.props.intl.formatMessage(
messages['account.settings.banner.beta.language.action.switch.back'],
{ previous_language: previousLanguage.name },
)}
</Button>
<Hyperlink
destination={this.getTransifexLink(savedLanguage.code)}
className="btn btn-outline-secondary"
target="_blank"
>
{this.props.intl.formatMessage(
messages['account.settings.banner.beta.language.action.help.translate'],
{ beta_language: savedLanguage.name },
)}
</Hyperlink>
</div>
</Alert>
</div>
);
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 (
<div>
<Alert className="beta_language_alert alert alert-warning" role="alert">
<p>
{intl.formatMessage(messages['account.settings.banner.beta.language'], {
beta_language: savedLanguage.name,
})}
</p>
<div>
<Button onClick={handleRevertLanguage} className="mr-2">
{intl.formatMessage(
messages['account.settings.banner.beta.language.action.switch.back'],
{ previous_language: previousLanguage.name },
)}
</Button>
<Hyperlink
destination={getTransifexLink(savedLanguage.code)}
className="btn btn-outline-secondary"
target="_blank"
>
{intl.formatMessage(
messages['account.settings.banner.beta.language.action.help.translate'],
{ beta_language: savedLanguage.name },
)}
</Hyperlink>
</div>
</Alert>
</div>
);
};
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);

View File

@@ -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}
</Alert>
);
}
};
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 (
<AlertModal
isOpen={open}
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
onClose={onCancel}
isOverflowVisible
footerNode={(
<ActionRow>
<Button variant="link" onClick={onCancel}>Cancel</Button>
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
</ActionRow>
return (
<AlertModal
isOpen={open}
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
onClose={onCancel}
isOverflowVisible
footerNode={(
<ActionRow>
<Button variant="link" onClick={onCancel}>Cancel</Button>
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
</ActionRow>
)}
>
<div className="p-3">
{this.renderError()}
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<h6>
{intl.formatMessage(
messages['account.settings.delete.account.modal.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{intl.formatMessage(
messages[deleteAccountModalText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
</Alert>
<Form.Group
for={passwordFieldId}
isInvalid={errorType !== null}
>
<Form.Label className="d-block" htmlFor={passwordFieldId}>
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
</Form.Label>
<Form.Control
name="password"
id={passwordFieldId}
type="password"
value={password}
onChange={onChange}
/>
{errorType !== null && (
<Form.Control.Feedback type="invalid" feedback-for={passwordFieldId}>
{intl.formatMessage(invalidMessage)}
</Form.Control.Feedback>
>
<div className="p-3">
{renderError()}
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<h6>
{intl.formatMessage(
messages['account.settings.delete.account.modal.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</Form.Group>
</div>
</h6>
<p>
{intl.formatMessage(
messages[deleteAccountModalText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
</Alert>
<Form.Group
for={passwordFieldId}
isInvalid={errorType !== null}
>
<Form.Label className="d-block" htmlFor={passwordFieldId}>
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
</Form.Label>
<Form.Control
name="password"
id={passwordFieldId}
type="password"
value={password}
onChange={onChange}
/>
{errorType !== null && (
<Form.Control.Feedback type="invalid" feedback-for={passwordFieldId}>
{intl.formatMessage(invalidMessage)}
</Form.Control.Feedback>
)}
</Form.Group>
</div>
</AlertModal>
);
}
}
</AlertModal>
);
};
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;

View File

@@ -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((
<IntlProvider locale="en">
<IntlConfirmationModal
<ConfirmationModal
{...props}
/>
</IntlProvider>
@@ -43,7 +40,7 @@ describe('ConfirmationModal', () => {
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlConfirmationModal
<ConfirmationModal
{...props}
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
/>
@@ -57,7 +54,7 @@ describe('ConfirmationModal', () => {
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlConfirmationModal
<ConfirmationModal
{...props}
errorType="empty-password"
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink } from '@openedx/paragon';
// Actions
@@ -23,61 +23,58 @@ import PrintingInstructions from './PrintingInstructions';
import ConnectedSuccessModal from './SuccessModal';
import BeforeProceedingBanner from './BeforeProceedingBanner';
export class DeleteAccount extends React.Component {
constructor(props) {
super(props);
export const DeleteAccount = ({
hasLinkedTPA = false,
isVerifiedAccount = true,
status = null,
errorType = null,
canDeleteAccount = true,
}) => {
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 (
<div>
<h2 className="section-heading h4 mb-3">
{intl.formatMessage(messages['account.settings.delete.account.header'])}
</h2>
{
this.props.canDeleteAccount ? (
return (
<div>
<h2 className="section-heading h4 mb-3">
{intl.formatMessage(messages['account.settings.delete.account.header'])}
</h2>
{
canDeleteAccount ? (
<>
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
<p>
@@ -109,7 +106,7 @@ export class DeleteAccount extends React.Component {
<p>
<Button
variant="outline-danger"
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
onClick={canDelete ? deleteAccountConfirmation : null}
disabled={!canDelete}
>
{intl.formatMessage(messages['account.settings.delete.account.button'])}
@@ -127,48 +124,30 @@ export class DeleteAccount extends React.Component {
supportArticleUrl={supportArticleUrl}
/>
) : null}
<ConnectedConfirmationModal
status={status}
errorType={errorType}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
onChange={this.handlePasswordChange}
password={this.state.password}
onSubmit={handleSubmit}
onCancel={handleCancel}
onChange={handlePasswordChange}
password={password}
/>
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
<ConnectedSuccessModal status={status} onClose={handleFinalClose} />
</>
) : (
<p>{intl.formatMessage(messages['account.settings.cannot.delete.account.text'])}</p>
)
}
</div>
);
}
}
</div>
);
};
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);

View File

@@ -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((
<IntlProvider locale="en">
<IntlDeleteAccount
<DeleteAccount
{...props}
/>
</IntlProvider>
@@ -50,7 +50,7 @@ describe('DeleteAccount', () => {
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlDeleteAccount
<DeleteAccount
{...props}
isVerifiedAccount={false}
/>
@@ -64,7 +64,7 @@ describe('DeleteAccount', () => {
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlDeleteAccount
<DeleteAccount
{...props}
hasLinkedTPA
/>