feat: add certificate preference to name fields
Adds a checkbox under "Name" and "Verified Name" fields, signifying which name the user prefers to display on their certificates. When checking the checkbox, the user can save this choice along with their name. When unchecking the check- box, a modal appears, prompting the user to choose a name to display on their certificate.
This commit is contained in:
@@ -19,7 +19,12 @@ import {
|
||||
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
|
||||
import {
|
||||
fetchSettings,
|
||||
saveMultipleSettings,
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
} from './data/actions';
|
||||
import { accountSettingsPageSelector } from './data/selectors';
|
||||
import PageLoading from './PageLoading';
|
||||
import JumpNav from './JumpNav';
|
||||
@@ -162,6 +167,23 @@ class AccountSettingsPage extends React.Component {
|
||||
this.props.saveSettings(formId, values);
|
||||
}
|
||||
|
||||
handleSubmitName = (formId, values) => {
|
||||
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
|
||||
this.props.saveMultipleSettings([
|
||||
{
|
||||
formId,
|
||||
commitValues: values,
|
||||
},
|
||||
{
|
||||
formId: 'useVerifiedNameForCerts',
|
||||
commitValues: this.props.formValues.useVerifiedNameForCerts,
|
||||
},
|
||||
], formId);
|
||||
} else {
|
||||
this.props.saveSettings(formId, values);
|
||||
}
|
||||
};
|
||||
|
||||
isEditable(fieldName) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
@@ -224,6 +246,25 @@ class AccountSettingsPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderFullNameHelpText = (status) => {
|
||||
if (
|
||||
!this.props.formValues.verifiedNameHistory
|
||||
|| !this.props.formValues.verifiedNameHistory.verified_name_enabled
|
||||
) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted']);
|
||||
default:
|
||||
if (this.props.committedValues.useVerifiedNameForCerts) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.non.certificate']);
|
||||
}
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.certificate']);
|
||||
}
|
||||
}
|
||||
|
||||
renderVerifiedNameSuccessMessage = () => (
|
||||
<OneTimeDismissibleAlert
|
||||
id="dismissedVerifiedNameSuccessMessage"
|
||||
@@ -312,6 +353,9 @@ class AccountSettingsPage extends React.Component {
|
||||
renderVerifiedNameHelpText = (status) => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
if (this.props.committedValues.useVerifiedNameForCerts) {
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.certificate']));
|
||||
}
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.verified']));
|
||||
case 'submitted':
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.submitted']));
|
||||
@@ -320,15 +364,6 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderFullNameHelpText = (status) => {
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted']));
|
||||
default:
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']));
|
||||
}
|
||||
}
|
||||
|
||||
renderEmptyStaticFieldMessage() {
|
||||
if (this.isManagedProfile()) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], {
|
||||
@@ -440,7 +475,8 @@ class AccountSettingsPage extends React.Component {
|
||||
isGrayedOut={
|
||||
verifiedNameEnabled && verifiedName && !this.isEditable('verifiedName')
|
||||
}
|
||||
{...editableFieldProps}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitName}
|
||||
/>
|
||||
{verifiedNameEnabled && verifiedName
|
||||
&& (
|
||||
@@ -461,7 +497,8 @@ class AccountSettingsPage extends React.Component {
|
||||
helpText={this.renderVerifiedNameHelpText(verifiedName.status)}
|
||||
isEditable={this.isEditable('verifiedName')}
|
||||
isGrayedOut={!this.isEditable('verifiedName')}
|
||||
{...(this.isEditable('verifiedName') && editableFieldProps)}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitName}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -739,6 +776,7 @@ AccountSettingsPage.propTypes = {
|
||||
}),
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
useVerifiedNameForCerts: PropTypes.bool.isRequired,
|
||||
verifiedNameHistory: PropTypes.shape({
|
||||
verified_name_enabled: PropTypes.bool,
|
||||
use_verified_name_for_certs: PropTypes.bool,
|
||||
@@ -758,6 +796,10 @@ AccountSettingsPage.propTypes = {
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
committedValues: PropTypes.shape({
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
}),
|
||||
drafts: PropTypes.shape({}),
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
draft: PropTypes.string,
|
||||
@@ -781,6 +823,7 @@ AccountSettingsPage.propTypes = {
|
||||
})),
|
||||
fetchSiteLanguages: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.object),
|
||||
@@ -790,6 +833,10 @@ AccountSettingsPage.defaultProps = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
committedValues: {
|
||||
useVerifiedNameForCerts: false,
|
||||
},
|
||||
drafts: {},
|
||||
siteLanguage: null,
|
||||
siteLanguageOptions: [],
|
||||
timeZoneOptions: [],
|
||||
@@ -804,6 +851,7 @@ AccountSettingsPage.defaultProps = {
|
||||
export default connect(accountSettingsPageSelector, {
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
fetchSiteLanguages,
|
||||
})(injectIntl(AccountSettingsPage));
|
||||
|
||||
@@ -91,6 +91,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'The name that is used for ID verification and that appears on your certificates.',
|
||||
description: 'Help text for the account settings name field.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.non.certificate': {
|
||||
id: 'account.settings.field.full.name.help.text.non.certificate',
|
||||
defaultMessage: 'The name that appears on your public profile.',
|
||||
description: 'Help text for the account settings name field.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.certificate': {
|
||||
id: 'account.settings.field.full.name.help.text.certificate',
|
||||
defaultMessage: 'This name is selected to appear on your certificates and public-facing records.',
|
||||
description: 'Help text for the account settings name field.',
|
||||
},
|
||||
'account.settings.field.name.verified': {
|
||||
id: 'account.settings.field.name.verified',
|
||||
defaultMessage: 'Verified name',
|
||||
@@ -101,6 +111,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'This name has been verified by government ID.',
|
||||
description: 'Help text for the account settings verified name field when the name is verified.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.certificate': {
|
||||
id: 'account.settings.field.name.verified.help.text.certificate',
|
||||
defaultMessage: 'This name has been verified by government ID and selected to appear on your certificates and public-facing records.',
|
||||
description: 'Help text for the account settings verified name field when the name is selected for certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.submitted': {
|
||||
id: 'account.settings.field.name.verified.help.text.submitted',
|
||||
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.',
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
closeForm,
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
import CertificatePreference from './certificate-preference/CertificatePreference';
|
||||
|
||||
function EditableField(props) {
|
||||
const {
|
||||
@@ -103,54 +104,57 @@ function EditableField(props) {
|
||||
expression={isEditing ? 'editing' : 'default'}
|
||||
cases={{
|
||||
editing: (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={id}
|
||||
invalid={error != null}
|
||||
invalidMessage={error}
|
||||
helpText={helpText}
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
{...others}
|
||||
/>
|
||||
<>{others.children}</>
|
||||
</ValidationFormGroup>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
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.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={id}
|
||||
invalid={error != null}
|
||||
invalidMessage={error}
|
||||
helpText={helpText}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
{...others}
|
||||
/>
|
||||
<>{others.children}</>
|
||||
</ValidationFormGroup>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
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.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
{['name', 'verifiedName'].includes(name) && <CertificatePreference fieldName={name} />}
|
||||
</>
|
||||
),
|
||||
default: (
|
||||
<div className="form-group">
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ActionRow,
|
||||
Form,
|
||||
ModalDialog,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
closeForm,
|
||||
resetDrafts,
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
} from '../data/actions';
|
||||
import { certPreferenceSelector } from '../data/selectors';
|
||||
|
||||
import commonMessages from '../AccountSettingsPage.messages';
|
||||
import messages from './messages';
|
||||
|
||||
function CertificatePreference({
|
||||
intl,
|
||||
fieldName,
|
||||
originalFullName,
|
||||
originalVerifiedName,
|
||||
saveState,
|
||||
useVerifiedNameForCerts,
|
||||
verifiedNameEnabled,
|
||||
}) {
|
||||
if (!verifiedNameEnabled || !originalVerifiedName) {
|
||||
// If the user doesn't have an approved verified name, do not display this component
|
||||
return null;
|
||||
}
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const formId = 'useVerifiedNameForCerts';
|
||||
|
||||
function handleCheckboxChange() {
|
||||
if (!checked) {
|
||||
if (fieldName === 'verifiedName') {
|
||||
dispatch(updateDraft(formId, true));
|
||||
} else {
|
||||
dispatch(updateDraft(formId, false));
|
||||
}
|
||||
} else {
|
||||
setModalIsOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setModalIsOpen(false);
|
||||
dispatch(resetDrafts());
|
||||
}
|
||||
|
||||
function handleModalChange(e) {
|
||||
if (e.target.value === 'fullName') {
|
||||
dispatch(updateDraft(formId, false));
|
||||
} else {
|
||||
dispatch(updateDraft(formId, true));
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (saveState === 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(saveSettings(formId, useVerifiedNameForCerts));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (fieldName === 'verifiedName') {
|
||||
setChecked(useVerifiedNameForCerts);
|
||||
} else {
|
||||
setChecked(!useVerifiedNameForCerts);
|
||||
}
|
||||
}, [useVerifiedNameForCerts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalIsOpen && saveState === 'complete') {
|
||||
setModalIsOpen(false);
|
||||
dispatch(closeForm(fieldName));
|
||||
}
|
||||
}, [modalIsOpen, saveState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Checkbox className="mt-1 mb-4" checked={checked} onChange={handleCheckboxChange}>
|
||||
{intl.formatMessage(messages['account.settings.field.name.checkbox.certificate.select'])}
|
||||
</Form.Checkbox>
|
||||
|
||||
<ModalDialog
|
||||
title={intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
|
||||
isOpen={modalIsOpen}
|
||||
onClose={handleCancel}
|
||||
size="lg"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body>
|
||||
<Form.Group className="mb-4">
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.select'])}
|
||||
</Form.Label>
|
||||
<Form.RadioSet
|
||||
name={formId}
|
||||
value={useVerifiedNameForCerts ? 'verifiedName' : 'fullName'}
|
||||
onChange={handleModalChange}
|
||||
>
|
||||
<Form.Radio value="fullName">
|
||||
{originalFullName}{' '}
|
||||
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.full'])})
|
||||
</Form.Radio>
|
||||
<Form.Radio value="verifiedName">
|
||||
{originalVerifiedName}{' '}
|
||||
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.verified'])})
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</Form.Group>
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="outline-primary" disabled={saveState === 'pending'}>
|
||||
{intl.formatMessage(commonMessages['account.settings.editable.field.action.cancel'])}
|
||||
</ModalDialog.CloseButton>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.field.name.modal.certificate.button.choose']),
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</Form>
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CertificatePreference.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
originalFullName: PropTypes.string,
|
||||
originalVerifiedName: PropTypes.string,
|
||||
saveState: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
verifiedNameEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
CertificatePreference.defaultProps = {
|
||||
originalFullName: '',
|
||||
originalVerifiedName: '',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
|
||||
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));
|
||||
22
src/account-settings/certificate-preference/data/service.js
Normal file
22
src/account-settings/certificate-preference/data/service.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postVerifiedNameConfig(username, commitValues) {
|
||||
const requestConfig = { headers: { Accept: 'application/json' } };
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/config`;
|
||||
|
||||
const { useVerifiedNameForCerts } = commitValues;
|
||||
const postValues = {
|
||||
username,
|
||||
use_verified_name_for_certs: useVerifiedNameForCerts,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, postValues, requestConfig)
|
||||
.catch(error => handleRequestError(error));
|
||||
|
||||
return data;
|
||||
}
|
||||
36
src/account-settings/certificate-preference/messages.js
Normal file
36
src/account-settings/certificate-preference/messages.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.field.name.checkbox.certificate.select': {
|
||||
id: 'account.settings.field.name.certificate.select',
|
||||
defaultMessage: 'If checked, this name will appear on your certificates and public-facing records.',
|
||||
description: 'Label for checkbox describing that the selected name will appear on the user‘s certificates.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.title': {
|
||||
id: 'account.settings.field.name.modal.certificate.title',
|
||||
defaultMessage: 'Choose a preferred name for certificates and public-facing records',
|
||||
description: 'Title instructing the user to choose a preferred name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.select': {
|
||||
id: 'account.settings.field.name.modal.certificate.select',
|
||||
defaultMessage: 'Select a name',
|
||||
description: 'Label instructing the user to select a name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.option.full': {
|
||||
id: 'account.settings.field.name.modal.certificate.option.full',
|
||||
defaultMessage: 'Full Name',
|
||||
description: 'Option representing the user’s full name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.option.verified': {
|
||||
id: 'account.settings.field.name.modal.certificate.option.verified',
|
||||
defaultMessage: 'Verified Name',
|
||||
description: 'Option representing the user’s verified name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.button.choose': {
|
||||
id: 'account.settings.field.name.modal.certificate.button.choose',
|
||||
defaultMessage: 'Choose name',
|
||||
description: 'Button to confirm the user’s name choice.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlCertificatePreference = injectIntl(CertificatePreference);
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('NameChange', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
const formId = 'useVerifiedNameForCerts';
|
||||
const updateDraft = 'UPDATE_DRAFT';
|
||||
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
fieldName: 'name',
|
||||
originalFullName: 'Ed X',
|
||||
originalVerifiedName: 'edX Verified',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
verifiedNameEnabled: true,
|
||||
intl: {},
|
||||
};
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('does not render if there is no verified name', () => {
|
||||
props = {
|
||||
...props,
|
||||
originalVerifiedName: '',
|
||||
};
|
||||
|
||||
const wrapper = render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not trigger modal when checking empty checkbox, and updates draft immediately', () => {
|
||||
props = {
|
||||
...props,
|
||||
useVerifiedNameForCerts: true,
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(false);
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(screen.queryByRole('radiogroup')).toBeNull();
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { name: formId, value: false },
|
||||
type: updateDraft,
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers modal when attempting to uncheck checkbox', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(true);
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
|
||||
screen.getByRole('radiogroup');
|
||||
});
|
||||
|
||||
it('updates draft when changing radio value', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const fullNameOption = screen.getByLabelText('Ed X (Full Name)');
|
||||
const verifiedNameOption = screen.getByLabelText('edX Verified (Verified Name)');
|
||||
expect(fullNameOption.checked).toEqual(true);
|
||||
expect(verifiedNameOption.checked).toEqual(false);
|
||||
|
||||
fireEvent.click(verifiedNameOption);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { name: formId, value: true },
|
||||
type: updateDraft,
|
||||
});
|
||||
});
|
||||
|
||||
it('clears draft on cancel', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith({ type: 'RESET_DRAFTS' });
|
||||
expect(screen.queryByRole('radiogroup')).toBeNull();
|
||||
});
|
||||
|
||||
it('submits', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const submitButton = screen.getByText('Choose name');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { formId, commitValues: false },
|
||||
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NameChange does not render if there is no verified name 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
@@ -58,12 +58,22 @@ const valuesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
(accountSettings, mostRecentVerifiedNameValue, mostRecentApprovedVerifiedNameValue) => (
|
||||
{
|
||||
(accountSettings, mostRecentVerifiedNameValue, mostRecentApprovedVerifiedNameValue) => {
|
||||
let useVerifiedNameForCerts = (
|
||||
accountSettings.values.verifiedNameHistory?.use_verified_name_for_certs || false
|
||||
);
|
||||
|
||||
if (Object.keys(accountSettings.confirmationValues).includes('useVerifiedNameForCerts')) {
|
||||
useVerifiedNameForCerts = accountSettings.confirmationValues.useVerifiedNameForCerts;
|
||||
}
|
||||
|
||||
return {
|
||||
...accountSettings.values,
|
||||
verifiedName: mostRecentApprovedVerifiedNameValue,
|
||||
mostRecentVerifiedName: mostRecentVerifiedNameValue,
|
||||
}),
|
||||
useVerifiedNameForCerts,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const draftsSelector = createSelector(
|
||||
@@ -150,7 +160,11 @@ const formValuesSelector = createSelector(
|
||||
(values, drafts) => {
|
||||
const formValues = {};
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
if (typeof value === 'boolean') {
|
||||
formValues[name] = chooseFormValue(drafts[name], value);
|
||||
} else {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
}
|
||||
});
|
||||
return formValues;
|
||||
},
|
||||
@@ -195,6 +209,8 @@ export const accountSettingsPageSelector = createSelector(
|
||||
siteLanguageOptionsSelector,
|
||||
siteLanguageSelector,
|
||||
formValuesSelector,
|
||||
valuesSelector,
|
||||
draftsSelector,
|
||||
profileDataManagerSelector,
|
||||
staticFieldsSelector,
|
||||
timeZonesSelector,
|
||||
@@ -205,6 +221,8 @@ export const accountSettingsPageSelector = createSelector(
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
formValues,
|
||||
committedValues,
|
||||
drafts,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
timeZoneOptions,
|
||||
@@ -220,12 +238,36 @@ export const accountSettingsPageSelector = createSelector(
|
||||
countryTimeZoneOptions,
|
||||
isActive: activeAccount,
|
||||
formValues,
|
||||
committedValues,
|
||||
drafts,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
tpaProviders: accountSettings.thirdPartyAuth.providers,
|
||||
}),
|
||||
);
|
||||
|
||||
export const certPreferenceSelector = createSelector(
|
||||
valuesSelector,
|
||||
formValuesSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
saveStateSelector,
|
||||
errorSelector,
|
||||
(
|
||||
committedValues,
|
||||
formValues,
|
||||
mostRecentApprovedVerifiedNameValue,
|
||||
saveState,
|
||||
errors,
|
||||
) => ({
|
||||
originalFullName: committedValues?.name || '',
|
||||
originalVerifiedName: mostRecentApprovedVerifiedNameValue?.verified_name || '',
|
||||
useVerifiedNameForCerts: formValues.useVerifiedNameForCerts || false,
|
||||
saveState,
|
||||
verifiedNameEnabled: formValues.verifiedNameHistory?.verified_name_enabled || false,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const coachingConsentPageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
|
||||
@@ -7,6 +7,7 @@ import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
@@ -256,11 +257,19 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const certificateKeys = ['useVerifiedNameForCerts'];
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
|
||||
const accountCommitValues = omit(
|
||||
commitValues,
|
||||
preferenceKeys,
|
||||
coachingKeys,
|
||||
demographicsKeys,
|
||||
certificateKeys,
|
||||
);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const certCommitValues = pick(commitValues, certificateKeys);
|
||||
const patchRequests = [];
|
||||
|
||||
if (!isEmpty(accountCommitValues)) {
|
||||
@@ -275,6 +284,9 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
if (!isEmpty(certCommitValues)) {
|
||||
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
|
||||
}
|
||||
|
||||
const results = await Promise.all(patchRequests);
|
||||
// Assigns in order of requests. Preference keys
|
||||
|
||||
Reference in New Issue
Block a user