Skip to content
31 changes: 18 additions & 13 deletions src/head/Head.jsx
Original file line number Diff line number Diff line change
@@ -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;
54 changes: 27 additions & 27 deletions src/profile/forms/elements/EditButton.jsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
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, style }) => {
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 = {
Expand Down
22 changes: 22 additions & 0 deletions src/profile/forms/elements/EditButton.test.jsx
Original file line number Diff line number Diff line change
@@ -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': { defaultMessage: '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();
});
});
32 changes: 17 additions & 15 deletions src/profile/forms/elements/FormControls.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
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';

import { VisibilitySelect } from './Visibility';
import { useIsVisibilityEnabled } from '../../data/hooks';

const FormControls = ({
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
cancelHandler,
changeHandler,
visibility,
visibilityId,
saveState,
}) => {
const intl = useIntl();
const buttonState = saveState === 'error' ? null : saveState;
const isVisibilityEnabled = useIsVisibilityEnabled();

Expand Down Expand Up @@ -42,18 +47,17 @@ 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.
Comment on lines -50 to -56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems to provide some helpful context. I don't think it should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took it back!

if (buttonState === 'pending') {
e.preventDefault();
}
Expand All @@ -66,16 +70,14 @@ const FormControls = ({
);
};

export default injectIntl(FormControls);
export default FormControls;

FormControls.propTypes = {
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
visibility: PropTypes.oneOf(['private', 'all_users']),
visibilityId: PropTypes.string.isRequired,
cancelHandler: PropTypes.func.isRequired,
changeHandler: PropTypes.func.isRequired,

intl: intlShape.isRequired,
};

FormControls.defaultProps = {
Expand Down
25 changes: 25 additions & 0 deletions src/profile/forms/elements/FormControls.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import FormControls from './FormControls';
import messages from './FormControls.messages';

describe('FormControls', () => {
it('renders and triggers cancelHandler', () => {
const cancelHandler = jest.fn();
const changeHandler = jest.fn();
const { getByText } = render(
<IntlProvider locale="en" messages={messages}>
<FormControls
cancelHandler={cancelHandler}
changeHandler={changeHandler}
visibilityId="test-visibility"
/>
</IntlProvider>,
);
// Use the actual label from the messages file
const cancelLabel = messages['profile.formcontrols.button.cancel'].defaultMessage;
fireEvent.click(getByText(cancelLabel));
expect(cancelHandler).toHaveBeenCalled();
});
});
28 changes: 12 additions & 16 deletions src/profile/forms/elements/Visibility.jsx
Original file line number Diff line number Diff line change
@@ -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 }) => {
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">
Expand All @@ -22,14 +25,13 @@ 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 VisibilitySelect = ({ className, ...props }) => {
const intl = useIntl();
const { value } = props;
const icon = value === 'private' ? faEyeSlash : faEye;

Expand All @@ -43,7 +45,9 @@ const VisibilitySelect = ({ intl, className, ...props }) => {
{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>
Expand All @@ -56,8 +60,6 @@ VisibilitySelect.propTypes = {
name: PropTypes.string,
value: PropTypes.oneOf(['private', 'all_users']),
onChange: PropTypes.func,

intl: intlShape.isRequired,
};
VisibilitySelect.defaultProps = {
id: null,
Expand All @@ -67,10 +69,4 @@ VisibilitySelect.defaultProps = {
onChange: null,
};

const intlVisibility = injectIntl(Visibility);
const intlVisibilitySelect = injectIntl(VisibilitySelect);

export {
intlVisibility as Visibility,
intlVisibilitySelect as VisibilitySelect,
};
export { Visibility, VisibilitySelect };
43 changes: 43 additions & 0 deletions src/profile/forms/elements/Visibility.test.jsx
Original file line number Diff line number Diff line change
@@ -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': { defaultMessage: 'Just me' },
'profile.visibility.who.everyone': { defaultMessage: '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);
});
});