refactor: replaced injectIntl with useIntl (#1239)
* refactor: replaced injectIntl with useIntl * test: update tests for useIntl hook implementation * fix: add missing trailing comma * test: fix failing component tests and remove deprecated defaultProps - Fix SwitchContent component defaultProps warning with default parameters - Fix Visibility component formatMessage errors and remove defaultProps - Fix FormControls component intl provider issues with useIntl mock - Fix EditButton component defaultProps and message formatting - Update EditableItemHeader, EmptyContent components - Replace React defaultProps with ES6 default parameters across components - Update test mocking to properly handle useIntl hook - All 82 tests now pass (previously 4 failed, 78 passed) Resolves React deprecation warnings and modernizes component patterns. * fix: add missing trailing comma
This commit is contained in:
@@ -1,21 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const Head = ({ intl }) => (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['profile.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
Head.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
const Head = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['profile.page.title'], {
|
||||
siteName: getConfig().SITE_NAME,
|
||||
})}
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href={getConfig().FAVICON_URL}
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(Head);
|
||||
export default Head;
|
||||
|
||||
@@ -1,46 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EditOutline } from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon';
|
||||
import messages from './EditButton.messages';
|
||||
|
||||
const EditButton = ({
|
||||
onClick, className, style, intl,
|
||||
}) => (
|
||||
<OverlayTrigger
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip variant="light" id="tooltip-top">
|
||||
<p className="h5 font-weight-normal m-0 p-0">
|
||||
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
||||
</p>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
const EditButton = ({ onClick, className = null, style = null }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip variant="light" id="tooltip-top">
|
||||
<p className="h5 font-weight-normal m-0 p-0">
|
||||
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
||||
</p>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<EditOutline className="text-gray-700" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
<EditOutline className="text-gray-700" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(EditButton);
|
||||
export default EditButton;
|
||||
|
||||
EditButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object, // eslint-disable-line
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
EditButton.defaultProps = {
|
||||
className: null,
|
||||
style: null,
|
||||
};
|
||||
|
||||
22
src/profile/forms/elements/EditButton.test.jsx
Normal file
22
src/profile/forms/elements/EditButton.test.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import EditButton from './EditButton';
|
||||
|
||||
const messages = {
|
||||
'profile.editbutton.edit': 'Edit',
|
||||
};
|
||||
|
||||
describe('EditButton', () => {
|
||||
it('renders and calls onClick when clicked', () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByRole } = render(
|
||||
<IntlProvider locale="en" messages={messages}>
|
||||
<EditButton onClick={onClick} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const button = getByRole('button');
|
||||
fireEvent.click(button);
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -7,12 +7,12 @@ import { Visibility } from './Visibility';
|
||||
import { useIsOnMobileScreen } from '../../data/hooks';
|
||||
|
||||
const EditableItemHeader = ({
|
||||
content,
|
||||
showVisibility,
|
||||
visibility,
|
||||
showEditButton,
|
||||
onClickEdit,
|
||||
headingId,
|
||||
content = '',
|
||||
showVisibility = false,
|
||||
visibility = 'private',
|
||||
showEditButton = false,
|
||||
onClickEdit = () => {},
|
||||
headingId = null,
|
||||
}) => {
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
return (
|
||||
@@ -57,13 +57,3 @@ EditableItemHeader.propTypes = {
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
headingId: PropTypes.string,
|
||||
};
|
||||
|
||||
EditableItemHeader.defaultProps = {
|
||||
onClickEdit: () => {
|
||||
},
|
||||
showVisibility: false,
|
||||
showEditButton: false,
|
||||
content: '',
|
||||
visibility: 'private',
|
||||
headingId: null,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const EmptyContent = ({ children, onClick, showPlusIcon }) => (
|
||||
const EmptyContent = ({ children = null, onClick = null, showPlusIcon = true }) => (
|
||||
<div className="p-0 m-0">
|
||||
{onClick ? (
|
||||
<button
|
||||
@@ -27,9 +27,3 @@ EmptyContent.propTypes = {
|
||||
children: PropTypes.node,
|
||||
showPlusIcon: PropTypes.bool,
|
||||
};
|
||||
|
||||
EmptyContent.defaultProps = {
|
||||
onClick: null,
|
||||
children: null,
|
||||
showPlusIcon: true,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, StatefulButton } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './FormControls.messages';
|
||||
|
||||
@@ -9,8 +9,13 @@ import { VisibilitySelect } from './Visibility';
|
||||
import { useIsVisibilityEnabled } from '../../data/hooks';
|
||||
|
||||
const FormControls = ({
|
||||
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
|
||||
cancelHandler,
|
||||
changeHandler,
|
||||
visibility = 'private',
|
||||
visibilityId,
|
||||
saveState = null,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const buttonState = saveState === 'error' ? null : saveState;
|
||||
const isVisibilityEnabled = useIsVisibilityEnabled();
|
||||
|
||||
@@ -42,18 +47,24 @@ const FormControls = ({
|
||||
type="submit"
|
||||
state={buttonState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['profile.formcontrols.button.save']),
|
||||
pending: intl.formatMessage(messages['profile.formcontrols.button.saving']),
|
||||
complete: intl.formatMessage(messages['profile.formcontrols.button.saved']),
|
||||
default: intl.formatMessage(
|
||||
messages['profile.formcontrols.button.save'],
|
||||
),
|
||||
pending: intl.formatMessage(
|
||||
messages['profile.formcontrols.button.saving'],
|
||||
),
|
||||
complete: intl.formatMessage(
|
||||
messages['profile.formcontrols.button.saved'],
|
||||
),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Swallow clicks if the state is pending.
|
||||
// We do this instead of disabling the button to prevent
|
||||
// it from losing focus (disabled elements cannot have focus).
|
||||
// Disabling it would causes upstream issues in focus management.
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
// Swallow clicks if the state is pending.
|
||||
// We do this instead of disabling the button to prevent
|
||||
// it from losing focus (disabled elements cannot have focus).
|
||||
// Disabling it would causes upstream issues in focus management.
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
if (buttonState === 'pending') {
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -66,7 +77,7 @@ const FormControls = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(FormControls);
|
||||
export default FormControls;
|
||||
|
||||
FormControls.propTypes = {
|
||||
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
@@ -74,11 +85,4 @@ FormControls.propTypes = {
|
||||
visibilityId: PropTypes.string.isRequired,
|
||||
cancelHandler: PropTypes.func.isRequired,
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
FormControls.defaultProps = {
|
||||
visibility: 'private',
|
||||
saveState: null,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,9 @@ jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const actual = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
return {
|
||||
...actual,
|
||||
useIntl: () => ({
|
||||
formatMessage: (msg) => msg.id, // returns id so we can assert on it
|
||||
}),
|
||||
injectIntl: (Component) => (props) => (
|
||||
<Component
|
||||
{...props}
|
||||
@@ -27,6 +30,10 @@ jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../data/hooks', () => ({
|
||||
useIsVisibilityEnabled: () => true,
|
||||
}));
|
||||
|
||||
describe('FormControls', () => {
|
||||
it('renders Save button label when saveState is null', () => {
|
||||
render(<FormControls {...defaultProps} />);
|
||||
|
||||
@@ -17,7 +17,7 @@ const onChildExit = (htmlNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
const SwitchContent = ({ expression, cases, className }) => {
|
||||
const SwitchContent = ({ expression = null, cases, className = null }) => {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
if (typeof cases[caseKey] === 'string') {
|
||||
@@ -51,9 +51,4 @@ SwitchContent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
SwitchContent.defaultProps = {
|
||||
expression: null,
|
||||
className: null,
|
||||
};
|
||||
|
||||
export default SwitchContent;
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
import messages from './Visibility.messages';
|
||||
|
||||
const Visibility = ({ to, intl }) => {
|
||||
const Visibility = ({ to = 'private' }) => {
|
||||
const intl = useIntl();
|
||||
const icon = to === 'private' ? faEyeSlash : faEye;
|
||||
const label = to === 'private'
|
||||
? intl.formatMessage(messages['profile.visibility.who.just.me'])
|
||||
: intl.formatMessage(messages['profile.visibility.who.everyone'], { siteName: getConfig().SITE_NAME });
|
||||
: intl.formatMessage(messages['profile.visibility.who.everyone'], {
|
||||
siteName: getConfig().SITE_NAME,
|
||||
});
|
||||
|
||||
return (
|
||||
<span className="ml-auto small text-muted">
|
||||
@@ -22,15 +25,17 @@ const Visibility = ({ to, intl }) => {
|
||||
|
||||
Visibility.propTypes = {
|
||||
to: PropTypes.oneOf(['private', 'all_users']),
|
||||
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
Visibility.defaultProps = {
|
||||
to: 'private',
|
||||
};
|
||||
|
||||
const VisibilitySelect = ({ intl, className, ...props }) => {
|
||||
const { value } = props;
|
||||
const VisibilitySelect = ({
|
||||
id = null,
|
||||
className = null,
|
||||
name = 'visibility',
|
||||
value = null,
|
||||
onChange = null,
|
||||
...props
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const icon = value === 'private' ? faEyeSlash : faEye;
|
||||
|
||||
return (
|
||||
@@ -38,12 +43,21 @@ const VisibilitySelect = ({ intl, className, ...props }) => {
|
||||
<span className="d-inline-block ml-1 mr-2 width-24px">
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
</span>
|
||||
<select className="d-inline-block form-control" {...props}>
|
||||
<select
|
||||
className="d-inline-block form-control"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
>
|
||||
<option key="private" value="private">
|
||||
{intl.formatMessage(messages['profile.visibility.who.just.me'])}
|
||||
</option>
|
||||
<option key="all_users" value="all_users">
|
||||
{intl.formatMessage(messages['profile.visibility.who.everyone'], { siteName: getConfig().SITE_NAME })}
|
||||
{intl.formatMessage(messages['profile.visibility.who.everyone'], {
|
||||
siteName: getConfig().SITE_NAME,
|
||||
})}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
@@ -56,21 +70,6 @@ VisibilitySelect.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.oneOf(['private', 'all_users']),
|
||||
onChange: PropTypes.func,
|
||||
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
VisibilitySelect.defaultProps = {
|
||||
id: null,
|
||||
className: null,
|
||||
name: 'visibility',
|
||||
value: null,
|
||||
onChange: null,
|
||||
};
|
||||
|
||||
const intlVisibility = injectIntl(Visibility);
|
||||
const intlVisibilitySelect = injectIntl(VisibilitySelect);
|
||||
|
||||
export {
|
||||
intlVisibility as Visibility,
|
||||
intlVisibilitySelect as VisibilitySelect,
|
||||
};
|
||||
export { Visibility, VisibilitySelect };
|
||||
|
||||
43
src/profile/forms/elements/Visibility.test.jsx
Normal file
43
src/profile/forms/elements/Visibility.test.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Visibility, VisibilitySelect } from './Visibility';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const messages = {
|
||||
'profile.visibility.who.just.me': 'Just me',
|
||||
'profile.visibility.who.everyone': 'Everyone',
|
||||
};
|
||||
|
||||
describe('Visibility', () => {
|
||||
it('shows the correct icon and label for private', () => {
|
||||
const { getByText } = render(
|
||||
<IntlProvider locale="en" messages={messages}>
|
||||
<Visibility to="private" />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(getByText(/just me/i)).toBeInTheDocument();
|
||||
});
|
||||
it('shows the correct icon and label for all_users', () => {
|
||||
const { getByText } = render(
|
||||
<IntlProvider locale="en" messages={messages}>
|
||||
<Visibility to="all_users" />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(getByText(/everyone/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('VisibilitySelect', () => {
|
||||
it('renders both options', () => {
|
||||
const { getByRole, getAllByRole } = render(
|
||||
<IntlProvider locale="en" messages={messages}>
|
||||
<VisibilitySelect value="private" onChange={() => {}} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const select = getByRole('combobox');
|
||||
const options = getAllByRole('option');
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(options.length).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user