Compare commits
1 Commits
master
...
sajjad/VAN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d67a252998 |
@@ -52,6 +52,7 @@ import { fetchSiteLanguages } from './site-language';
|
||||
import DemographicsSection from './demographics/DemographicsSection';
|
||||
import { fetchCourseList } from '../notification-preferences/data/thunks';
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
import NameField from './NameField';
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -167,6 +168,34 @@ class AccountSettingsPage extends React.Component {
|
||||
this.props.saveSettings(formId, values, extendedProfileObject);
|
||||
};
|
||||
|
||||
handleSubmitFirstAndLastName = (formId, fullName, firstName, lastName) => {
|
||||
const settingsToBeSaved = [];
|
||||
|
||||
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
|
||||
settingsToBeSaved.push({
|
||||
formId: 'useVerifiedNameForCerts',
|
||||
commitValues: this.props.formValues.useVerifiedNameForCerts,
|
||||
});
|
||||
}
|
||||
|
||||
settingsToBeSaved.push({
|
||||
formId: 'first_name',
|
||||
commitValues: firstName,
|
||||
});
|
||||
|
||||
settingsToBeSaved.push({
|
||||
formId: 'last_name',
|
||||
commitValues: lastName,
|
||||
});
|
||||
|
||||
settingsToBeSaved.push({
|
||||
formId: 'name',
|
||||
commitValues: fullName,
|
||||
});
|
||||
|
||||
this.props.saveMultipleSettings(settingsToBeSaved, formId, false);
|
||||
};
|
||||
|
||||
handleSubmitProfileName = (formId, values) => {
|
||||
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
|
||||
this.props.saveMultipleSettings([
|
||||
@@ -552,37 +581,71 @@ class AccountSettingsPage extends React.Component {
|
||||
isEditable={false}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="name"
|
||||
type="text"
|
||||
value={
|
||||
{(this.props.formValues?.are_first_and_last_name_required_in_registration === true) ? (
|
||||
<NameField
|
||||
name="name"
|
||||
type="text"
|
||||
verifiedName={verifiedName}
|
||||
pendingNameChange={this.props.formValues.pending_name_change}
|
||||
fullNameValue={this.props.formValues.name}
|
||||
firstNameValue={this.props.formValues.first_name}
|
||||
lastNameValue={this.props.formValues.last_name}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
|
||||
emptyLabel={
|
||||
this.isEditable('name')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
helpText={
|
||||
verifiedName
|
||||
? this.renderFullNameHelpText(verifiedName.status, verifiedName.proctored_exam_attempt_id)
|
||||
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
|
||||
}
|
||||
isEditable={
|
||||
verifiedName
|
||||
? this.isEditable('verifiedName') && this.isEditable('name')
|
||||
: this.isEditable('name')
|
||||
}
|
||||
isGrayedOut={
|
||||
verifiedName && !this.isEditable('verifiedName')
|
||||
}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitFirstAndLastName}
|
||||
/>
|
||||
) : (
|
||||
<EditableField
|
||||
name="name"
|
||||
type="text"
|
||||
value={
|
||||
verifiedName?.status === 'submitted'
|
||||
&& this.props.formValues.pending_name_change
|
||||
? this.props.formValues.pending_name_change
|
||||
: this.props.formValues.name
|
||||
}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
|
||||
emptyLabel={
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
|
||||
emptyLabel={
|
||||
this.isEditable('name')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
helpText={
|
||||
helpText={
|
||||
verifiedName
|
||||
? this.renderFullNameHelpText(verifiedName.status, verifiedName.proctored_exam_attempt_id)
|
||||
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
|
||||
}
|
||||
isEditable={
|
||||
isEditable={
|
||||
verifiedName
|
||||
? this.isEditable('verifiedName') && this.isEditable('name')
|
||||
: this.isEditable('name')
|
||||
}
|
||||
isGrayedOut={
|
||||
isGrayedOut={
|
||||
verifiedName && !this.isEditable('verifiedName')
|
||||
}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitProfileName}
|
||||
/>
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitProfileName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{verifiedName
|
||||
&& (
|
||||
<EditableField
|
||||
@@ -872,10 +935,13 @@ AccountSettingsPage.propTypes = {
|
||||
formValues: PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
first_name: PropTypes.string,
|
||||
last_name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
secondary_email: PropTypes.string,
|
||||
secondary_email_enabled: PropTypes.bool,
|
||||
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
are_first_and_last_name_required_in_registration: PropTypes.bool,
|
||||
country: PropTypes.string,
|
||||
level_of_education: PropTypes.string,
|
||||
gender: PropTypes.string,
|
||||
|
||||
@@ -81,6 +81,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Full name',
|
||||
description: 'Label for account settings name field.',
|
||||
},
|
||||
'account.settings.field.first.name': {
|
||||
id: 'account.settings.field.first.name',
|
||||
defaultMessage: 'First name',
|
||||
description: 'Label for account settings first name field.',
|
||||
},
|
||||
'account.settings.field.last.name': {
|
||||
id: 'account.settings.field.last.name',
|
||||
defaultMessage: 'Last name',
|
||||
description: 'Label for account settings last name field.',
|
||||
},
|
||||
'account.settings.field.full.name.empty': {
|
||||
id: 'account.settings.field.full.name.empty',
|
||||
defaultMessage: 'Add name',
|
||||
|
||||
360
src/account-settings/NameField.jsx
Normal file
360
src/account-settings/NameField.jsx
Normal file
@@ -0,0 +1,360 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form, StatefulButton,
|
||||
} from '@openedx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import SwitchContent from './SwitchContent';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import {
|
||||
openForm,
|
||||
closeForm,
|
||||
} from './data/actions';
|
||||
import { nameFieldSelector } from './data/selectors';
|
||||
import CertificatePreference from './certificate-preference/CertificatePreference';
|
||||
|
||||
/**
|
||||
* This field shows concatenated user's first name and last name as their full name
|
||||
* and splits the name into first name and last name fields on edit.
|
||||
* @param props
|
||||
* @returns {Element}
|
||||
* @constructor
|
||||
*/
|
||||
const NameField = (props) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
emptyLabel,
|
||||
type,
|
||||
fullNameValue,
|
||||
firstNameValue,
|
||||
lastNameValue,
|
||||
verifiedName,
|
||||
pendingNameChange,
|
||||
userSuppliedValue,
|
||||
saveState,
|
||||
error,
|
||||
firstNameError,
|
||||
lastNameError,
|
||||
confirmationMessageDefinition,
|
||||
confirmationValue,
|
||||
helpText,
|
||||
onEdit,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
onChange,
|
||||
isEditing,
|
||||
isEditable,
|
||||
isGrayedOut,
|
||||
intl,
|
||||
...others
|
||||
} = props;
|
||||
|
||||
const id = `field-${name}`;
|
||||
|
||||
const firstNameFieldAttributes = {
|
||||
name: 'first_name',
|
||||
id: 'field-firstName',
|
||||
label: intl.formatMessage(messages['account.settings.field.first.name']),
|
||||
};
|
||||
|
||||
const lastNameFieldAttributes = {
|
||||
name: 'last_name',
|
||||
id: 'field-lastName',
|
||||
label: intl.formatMessage(messages['account.settings.field.last.name']),
|
||||
};
|
||||
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [fieldError, setFieldError] = useState('');
|
||||
|
||||
/**
|
||||
* Concatenates first and last name and generates full name.
|
||||
* @param first
|
||||
* @param last
|
||||
* @returns {`${string} ${string}`}
|
||||
*/
|
||||
const generateFullName = (first, last) => {
|
||||
if (first && last) {
|
||||
return `${first} ${last}`;
|
||||
}
|
||||
return first || last;
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits a full name into first name and last name such that the first word
|
||||
* is the firstName and rest of the name is last name.
|
||||
* - If the full name is "John Doe Hamilton", the splitting will be
|
||||
* e.g., fullName = John Doe => firstName = John, lastName = Doe Hamilton
|
||||
* @param {string} nameValue The full name to split.
|
||||
* @returns {object} An object containing the firstName and lastName.
|
||||
*/
|
||||
const splitFullName = (nameValue) => {
|
||||
const [first, ...lastNameArr] = nameValue.trim().split(' ');
|
||||
const last = lastNameArr.join(' ');
|
||||
return { first, last };
|
||||
};
|
||||
|
||||
/**
|
||||
* UseEffect for setting first and last name.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (firstNameValue || lastNameValue) {
|
||||
setFirstName(firstNameValue);
|
||||
setLastName(lastNameValue);
|
||||
} else {
|
||||
const { first, last } = splitFullName(fullNameValue);
|
||||
setFirstName(first);
|
||||
setLastName(last);
|
||||
}
|
||||
}, [firstNameValue, fullNameValue, lastNameValue]);
|
||||
|
||||
/**
|
||||
* UseEffect for setting full name.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (verifiedName?.status === 'submitted' && pendingNameChange) {
|
||||
setFullName(pendingNameChange);
|
||||
} else if (firstNameValue || lastNameValue) {
|
||||
setFullName(generateFullName(firstNameValue, lastNameValue));
|
||||
} else {
|
||||
setFullName(fullNameValue);
|
||||
}
|
||||
}, [firstNameValue, fullNameValue, lastNameValue, pendingNameChange, verifiedName?.status]);
|
||||
|
||||
/**
|
||||
* UseEffect for setting error
|
||||
*/
|
||||
useEffect(() => {
|
||||
setFieldError(error || firstNameError || lastNameError);
|
||||
}, [error, firstNameError, lastNameError]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const firstNameVal = formData.get(firstNameFieldAttributes.name).trim();
|
||||
const lastNameVal = formData.get(lastNameFieldAttributes.name).trim();
|
||||
const fullNameVal = generateFullName(firstName, lastName);
|
||||
|
||||
onSubmit(name, fullNameVal, firstNameVal, lastNameVal);
|
||||
};
|
||||
|
||||
const handleChange = (e, fieldName) => {
|
||||
onChange(fieldName, e.target.value);
|
||||
// Updating full name along with the updates to first and last name
|
||||
if (fieldName === firstNameFieldAttributes.name) {
|
||||
onChange(name, generateFullName(e.target.value.trim(), lastNameValue));
|
||||
} else if (fieldName === lastNameFieldAttributes.name) {
|
||||
onChange(name, generateFullName(firstNameValue, e.target.value.trim()));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit(name);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel(name);
|
||||
};
|
||||
|
||||
const renderEmptyLabel = () => {
|
||||
if (isEditable) {
|
||||
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
|
||||
}
|
||||
return <span className="text-muted">{emptyLabel}</span>;
|
||||
};
|
||||
|
||||
const renderValue = (rawValue) => {
|
||||
if (!rawValue) {
|
||||
return renderEmptyLabel();
|
||||
}
|
||||
let finalValue = rawValue;
|
||||
|
||||
if (userSuppliedValue) {
|
||||
finalValue += `: ${userSuppliedValue}`;
|
||||
}
|
||||
|
||||
return finalValue;
|
||||
};
|
||||
|
||||
const renderConfirmationMessage = () => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
return intl.formatMessage(confirmationMessageDefinition, {
|
||||
value: confirmationValue,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
expression={isEditing ? 'editing' : 'default'}
|
||||
cases={{
|
||||
editing: (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Form.Group
|
||||
controlId={id}
|
||||
isInvalid={fieldError != null}
|
||||
>
|
||||
<Form.Group
|
||||
controlId={firstNameFieldAttributes.id}
|
||||
isInvalid={firstNameError || error}
|
||||
className="d-inline-block mb-0"
|
||||
>
|
||||
<Form.Label size="sm" className="h6 d-block" htmlFor={firstNameFieldAttributes.id}>
|
||||
{firstNameFieldAttributes.label}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
data-hj-suppress
|
||||
name={firstNameFieldAttributes.name}
|
||||
id={firstNameFieldAttributes.id}
|
||||
type={type}
|
||||
value={firstName}
|
||||
onChange={(e) => { handleChange(e, firstNameFieldAttributes.name); }}
|
||||
{...others}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group
|
||||
controlId={lastNameFieldAttributes.id}
|
||||
isInvalid={lastNameError || error}
|
||||
className="d-inline-block mb-0"
|
||||
>
|
||||
<Form.Label size="sm" className="h6 d-block" htmlFor={lastNameFieldAttributes.id}>
|
||||
{lastNameFieldAttributes.label}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
data-hj-suppress
|
||||
name={lastNameFieldAttributes.name}
|
||||
id={lastNameFieldAttributes.id}
|
||||
type={type}
|
||||
value={lastName}
|
||||
onChange={(e) => { handleChange(e, lastNameFieldAttributes.name); }}
|
||||
{...others}
|
||||
/>
|
||||
</Form.Group>
|
||||
{!!helpText && <Form.Text>{helpText}</Form.Text>}
|
||||
{fieldError != null && <Form.Control.Feedback hasIcon={false}>{fieldError}</Form.Control.Feedback>}
|
||||
{others.children}
|
||||
</Form.Group>
|
||||
<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', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
|
||||
</>
|
||||
),
|
||||
default: (
|
||||
<div className="form-group">
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3">
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress className={classNames('text-truncate', { 'grayed-out': isGrayedOut })}>
|
||||
{renderValue(fullName)}
|
||||
</p>
|
||||
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
NameField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]),
|
||||
emptyLabel: PropTypes.node,
|
||||
type: PropTypes.string.isRequired,
|
||||
fullNameValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
firstNameValue: PropTypes.string,
|
||||
lastNameValue: PropTypes.string,
|
||||
pendingNameChange: PropTypes.string,
|
||||
verifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
proctored_exam_attempt_id: PropTypes.number,
|
||||
}),
|
||||
userSuppliedValue: PropTypes.string,
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
error: PropTypes.string,
|
||||
firstNameError: PropTypes.string,
|
||||
lastNameError: PropTypes.string,
|
||||
confirmationMessageDefinition: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
}),
|
||||
confirmationValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
helpText: PropTypes.node,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isGrayedOut: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
NameField.defaultProps = {
|
||||
fullNameValue: undefined,
|
||||
firstNameValue: undefined,
|
||||
lastNameValue: undefined,
|
||||
pendingNameChange: null,
|
||||
verifiedName: null,
|
||||
saveState: undefined,
|
||||
label: undefined,
|
||||
emptyLabel: undefined,
|
||||
error: null,
|
||||
firstNameError: null,
|
||||
lastNameError: null,
|
||||
confirmationMessageDefinition: undefined,
|
||||
confirmationValue: undefined,
|
||||
helpText: undefined,
|
||||
isEditing: false,
|
||||
isEditable: true,
|
||||
isGrayedOut: false,
|
||||
userSuppliedValue: undefined,
|
||||
};
|
||||
|
||||
export default connect(nameFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
})(injectIntl(NameField));
|
||||
@@ -105,9 +105,9 @@ export const savePreviousSiteLanguage = previousSiteLanguage => ({
|
||||
payload: { previousSiteLanguage },
|
||||
});
|
||||
|
||||
export const saveMultipleSettings = (settingsArray, form = null) => ({
|
||||
export const saveMultipleSettings = (settingsArray, form = null, saveInSeparateCalls = true) => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.BASE,
|
||||
payload: { settingsArray, form },
|
||||
payload: { settingsArray, form, saveInSeparateCalls },
|
||||
});
|
||||
|
||||
export const saveMultipleSettingsBegin = () => ({
|
||||
|
||||
@@ -124,14 +124,24 @@ export function* handleSaveMultipleSettings(action) {
|
||||
try {
|
||||
yield put(saveMultipleSettingsBegin());
|
||||
const { username, userId } = getAuthenticatedUser();
|
||||
const { settingsArray, form } = action.payload;
|
||||
for (let i = 0; i < settingsArray.length; i += 1) {
|
||||
const { formId, commitValues } = settingsArray[i];
|
||||
const { settingsArray, form, saveInSeparateCalls } = action.payload;
|
||||
if (saveInSeparateCalls) {
|
||||
for (let i = 0; i < settingsArray.length; i += 1) {
|
||||
const { formId, commitValues } = settingsArray[i];
|
||||
yield put(saveSettingsBegin());
|
||||
const commitData = { [formId]: commitValues };
|
||||
const savedSettings = yield call(patchSettings, username, commitData, userId);
|
||||
yield put(saveSettingsSuccess(savedSettings, commitData));
|
||||
}
|
||||
} else {
|
||||
const commitData = settingsArray.reduce((data, setting) => (
|
||||
{ ...data, [setting.formId]: setting.commitValues }
|
||||
), {});
|
||||
yield put(saveSettingsBegin());
|
||||
const commitData = { [formId]: commitValues };
|
||||
const savedSettings = yield call(patchSettings, username, commitData, userId);
|
||||
yield put(saveSettingsSuccess(savedSettings, commitData));
|
||||
}
|
||||
|
||||
yield put(saveMultipleSettingsSuccess(action));
|
||||
if (form) {
|
||||
yield delay(1000);
|
||||
|
||||
@@ -128,6 +128,16 @@ export const editableFieldSelector = createStructuredSelector({
|
||||
isEditing: isEditingSelector,
|
||||
});
|
||||
|
||||
export const nameFieldSelector = createSelector(
|
||||
editableFieldSelector,
|
||||
accountSettingsSelector,
|
||||
(editableFieldSettings, accountSettings) => ({
|
||||
...editableFieldSettings,
|
||||
firstNameError: accountSettings.errors?.first_name,
|
||||
lastNameError: accountSettings.errors?.last_name,
|
||||
}),
|
||||
);
|
||||
|
||||
export const profileDataManagerSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.profileDataManager,
|
||||
|
||||
@@ -62,7 +62,10 @@ const NameChangeModal = ({
|
||||
}));
|
||||
} else {
|
||||
const draftProfileName = targetFormId === 'name' ? formValues.name : null;
|
||||
dispatch(requestNameChange(username, draftProfileName, verifiedNameInput));
|
||||
const draftFirstName = targetFormId === 'name' ? formValues?.first_name : null;
|
||||
const draftLastName = targetFormId === 'name' ? formValues?.last_name : null;
|
||||
|
||||
dispatch(requestNameChange(username, draftProfileName, verifiedNameInput, draftFirstName, draftLastName));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -190,6 +193,8 @@ NameChangeModal.propTypes = {
|
||||
errors: PropTypes.shape({}).isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
first_name: PropTypes.string,
|
||||
last_name: PropTypes.string,
|
||||
verified_name: PropTypes.string,
|
||||
}).isRequired,
|
||||
saveState: PropTypes.string,
|
||||
|
||||
@@ -2,9 +2,11 @@ import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const REQUEST_NAME_CHANGE = new AsyncActionType('ACCOUNT_SETTINGS', 'REQUEST_NAME_CHANGE');
|
||||
|
||||
export const requestNameChange = (username, profileName, verifiedName) => ({
|
||||
export const requestNameChange = (username, profileName, verifiedName, firstName, lastName) => ({
|
||||
type: REQUEST_NAME_CHANGE.BASE,
|
||||
payload: { username, profileName, verifiedName },
|
||||
payload: {
|
||||
username, profileName, verifiedName, firstName, lastName,
|
||||
},
|
||||
});
|
||||
|
||||
export const requestNameChangeBegin = () => ({
|
||||
|
||||
@@ -17,7 +17,7 @@ export function* handleRequestNameChange(action) {
|
||||
try {
|
||||
yield put(requestNameChangeBegin());
|
||||
if (action.payload.profileName) {
|
||||
yield call(postNameChange, action.payload.profileName);
|
||||
yield call(postNameChange, action.payload.profileName, action.payload.firstName, action.payload.lastName);
|
||||
profileName = action.payload.profileName;
|
||||
}
|
||||
yield call(postVerifiedName, {
|
||||
|
||||
@@ -4,13 +4,19 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postNameChange(name) {
|
||||
export async function postNameChange(name, firstName, lastName) {
|
||||
// Requests a pending name change, rather than saving the account name immediately
|
||||
const requestConfig = { headers: { Accept: 'application/json' } };
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/name_change/`;
|
||||
|
||||
const nameChangePayload = { name };
|
||||
if (firstName && lastName) {
|
||||
nameChangePayload.first_name = firstName;
|
||||
nameChangePayload.last_name = lastName;
|
||||
}
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, { name }, requestConfig)
|
||||
.post(requestUrl, nameChangePayload, requestConfig)
|
||||
.catch(error => handleRequestError(error));
|
||||
|
||||
return data;
|
||||
|
||||
@@ -99,6 +99,8 @@ describe('NameChange', () => {
|
||||
const dispatchData = {
|
||||
payload: {
|
||||
profileName: null,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
username: 'edx',
|
||||
verifiedName: 'Verified Name',
|
||||
},
|
||||
@@ -167,4 +169,41 @@ describe('NameChange', () => {
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
expect(window.location.pathname).toEqual('/id-verification');
|
||||
});
|
||||
|
||||
it(
|
||||
'dispatches profileName with first and last name if first_name and last_name are available in settings',
|
||||
async () => {
|
||||
const dispatchData = {
|
||||
payload: {
|
||||
profileName: 'edx edx',
|
||||
username: 'edx',
|
||||
verifiedName: 'Verified Name',
|
||||
firstName: 'first',
|
||||
lastName: 'last',
|
||||
},
|
||||
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
|
||||
};
|
||||
const formProps = {
|
||||
...props,
|
||||
targetFormId: 'name',
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
first_name: 'first',
|
||||
last_name: 'last',
|
||||
},
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...formProps} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter the name on your photo ID');
|
||||
fireEvent.change(input, { target: { value: 'Verified Name' } });
|
||||
|
||||
const submitButton = screen.getByText('Continue');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(dispatchData);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import AccountSettingsPage from '../AccountSettingsPage';
|
||||
import mockData from './mockData';
|
||||
import { saveMultipleSettings, saveSettings } from '../data/actions';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -63,7 +64,6 @@ describe('AccountSettingsPage', () => {
|
||||
field_value: '',
|
||||
},
|
||||
],
|
||||
|
||||
},
|
||||
fetchSettings: jest.fn(),
|
||||
};
|
||||
@@ -98,4 +98,105 @@ describe('AccountSettingsPage', () => {
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
it(
|
||||
'renders NameField for full name if first_name and last_name are required in registration',
|
||||
async () => {
|
||||
const { getByText, rerender, getByLabelText } = render(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const fullNameText = getByText('Full name');
|
||||
const fullNameEditButton = fullNameText.parentElement.querySelector('button');
|
||||
|
||||
expect(fullNameEditButton).toBeInTheDocument();
|
||||
|
||||
store = mockStore({
|
||||
...mockData,
|
||||
accountSettings: {
|
||||
...mockData.accountSettings,
|
||||
openFormId: 'name',
|
||||
values: {
|
||||
...mockData.accountSettings.values,
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
are_first_and_last_name_required_in_registration: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
rerender(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const submitButton = screen.getByText('Save');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
const firstNameField = getByLabelText('First name');
|
||||
const lastNameField = getByLabelText('Last name');
|
||||
|
||||
// Use fireEvent.change to simulate changing the selected value
|
||||
fireEvent.change(firstNameField, { target: { value: 'John' } });
|
||||
fireEvent.change(lastNameField, { target: { value: 'Doe' } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(saveMultipleSettings(
|
||||
[
|
||||
{
|
||||
commitValues: 'John',
|
||||
formId: 'first_name',
|
||||
},
|
||||
{
|
||||
commitValues: 'Doe',
|
||||
formId: 'last_name',
|
||||
},
|
||||
{
|
||||
commitValues: 'John Doe',
|
||||
formId: 'name',
|
||||
},
|
||||
],
|
||||
'name',
|
||||
false,
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'renders EditableField for full name if first_name and last_name are not available in account settings',
|
||||
async () => {
|
||||
const { getByText, rerender, getByLabelText } = render(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const fullNameText = getByText('Full name');
|
||||
const fullNameEditButton = fullNameText.parentElement.querySelector('button');
|
||||
|
||||
expect(fullNameEditButton).toBeInTheDocument();
|
||||
|
||||
store = mockStore({
|
||||
...mockData,
|
||||
accountSettings: {
|
||||
...mockData.accountSettings,
|
||||
openFormId: 'name',
|
||||
values: {
|
||||
...mockData.accountSettings.values,
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
rerender(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const submitButton = screen.getByText('Save');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
const fullName = getByLabelText('Full name');
|
||||
|
||||
// Use fireEvent.change to simulate changing the selected value
|
||||
fireEvent.change(fullName, { target: { value: 'test_name' } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(saveSettings(
|
||||
'name',
|
||||
'test_name',
|
||||
));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
245
src/account-settings/test/NameField.test.jsx
Normal file
245
src/account-settings/test/NameField.test.jsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import NameField from '../NameField';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
|
||||
|
||||
const IntlNameField = injectIntl(NameField);
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('NameField', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
const onSubmit = jest.fn();
|
||||
const onChange = jest.fn();
|
||||
props = {
|
||||
name: 'name',
|
||||
label: 'Full name',
|
||||
emptyLabel: 'Add name',
|
||||
type: 'text',
|
||||
fullNameValue: 'Test Name',
|
||||
firstNameValue: '',
|
||||
lastNameValue: '',
|
||||
verifiedName: null,
|
||||
userSuppliedValue: '',
|
||||
pendingNameChange: '',
|
||||
error: '',
|
||||
firstNameError: '',
|
||||
lastNameError: '',
|
||||
saveState: 'default',
|
||||
confirmationValue: 'Confirmation Value',
|
||||
helpText: 'Helpful Text',
|
||||
isEditing: false,
|
||||
isEditable: true,
|
||||
isGrayedOut: false,
|
||||
onSubmit,
|
||||
onChange,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders NameField correctly with editing disabled', () => {
|
||||
const tree = renderer.create(reduxWrapper(<IntlNameField {...props} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders NameField correctly with editing enabled', () => {
|
||||
props = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
};
|
||||
|
||||
const tree = renderer.create(reduxWrapper(<IntlNameField {...props} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders NameField with an error on full name', () => {
|
||||
const errorProps = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
error: 'This value is invalid.',
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlNameField {...errorProps} />));
|
||||
|
||||
const errorMessage = getByText('This value is invalid.');
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders NameField with an error on first name', () => {
|
||||
const errorProps = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
firstNameError: 'This value is invalid.',
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlNameField {...errorProps} />));
|
||||
|
||||
const errorMessage = getByText('This value is invalid.');
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders NameField with an error on last name', () => {
|
||||
const errorProps = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
lastNameError: 'This value is invalid.',
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlNameField {...errorProps} />));
|
||||
|
||||
const errorMessage = getByText('This value is invalid.');
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('display pendingName in NameField if available when verifiedName submit state is submitted', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
pendingNameChange: 'Pending Name',
|
||||
verifiedName: {
|
||||
status: 'submitted',
|
||||
},
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
const pendingNameText = getByText('Pending Name');
|
||||
expect(pendingNameText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display pendingName when verifiedName submit state is not submitted', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
pendingNameChange: 'Pending Name',
|
||||
verifiedName: {
|
||||
status: 'pending',
|
||||
},
|
||||
};
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
expect(queryByText('Pending Name')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('display concatenated first and last name in full name if first or last name value is available', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
firstNameValue: 'John',
|
||||
lastNameValue: 'Doe',
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
const pendingNameText = getByText('John Doe');
|
||||
expect(pendingNameText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('display full name in NameField if first and last name value are not available', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
fullNameValue: 'John Doe',
|
||||
firstNameValue: '',
|
||||
lastNameValue: '',
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
const pendingNameText = getByText('John Doe');
|
||||
expect(pendingNameText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('split full name in first and last name on editing if first and last name value not available ', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
fullNameValue: '',
|
||||
firstNameValue: 'John',
|
||||
lastNameValue: 'Doe',
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
const firstNameField = getByLabelText('First name');
|
||||
const lastNameField = getByLabelText('Last name');
|
||||
|
||||
expect(firstNameField.value).toEqual('John');
|
||||
expect(lastNameField.value).toEqual('Doe');
|
||||
});
|
||||
|
||||
it('submit full name, first name and last name on save ', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
fullNameValue: '',
|
||||
firstNameValue: 'John',
|
||||
lastNameValue: 'Doe',
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
const submitButton = getByText('Save');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('name', 'John Doe', 'John', 'Doe');
|
||||
});
|
||||
|
||||
it('update both first name and full name on first name change ', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
fullNameValue: '',
|
||||
firstNameValue: '',
|
||||
lastNameValue: 'Doe',
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
const firstNameField = getByLabelText('First name');
|
||||
fireEvent.change(firstNameField, { target: { value: 'John' } });
|
||||
|
||||
expect(props.onChange).toHaveBeenCalledTimes(2);
|
||||
expect(props.onChange).toHaveBeenCalledWith('first_name', 'John');
|
||||
expect(props.onChange).toHaveBeenCalledWith('name', 'John Doe');
|
||||
});
|
||||
|
||||
it('update both last name and full name on last name change ', () => {
|
||||
const componentProps = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
fullNameValue: '',
|
||||
firstNameValue: 'John',
|
||||
lastNameValue: '',
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlNameField {...componentProps} />));
|
||||
|
||||
const lastNameField = getByLabelText('Last name');
|
||||
fireEvent.change(lastNameField, { target: { value: 'Doe' } });
|
||||
|
||||
expect(props.onChange).toHaveBeenCalledTimes(2);
|
||||
expect(props.onChange).toHaveBeenCalledWith('last_name', 'Doe');
|
||||
expect(props.onChange).toHaveBeenCalledWith('name', 'John Doe');
|
||||
});
|
||||
});
|
||||
189
src/account-settings/test/__snapshots__/NameField.test.jsx.snap
Normal file
189
src/account-settings/test/__snapshots__/NameField.test.jsx.snap
Normal file
@@ -0,0 +1,189 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NameField renders NameField correctly with editing disabled 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Full name
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className="text-truncate"
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Test Name
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
>
|
||||
Helpful Text
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`NameField renders NameField correctly with editing enabled 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="pgn__form-group"
|
||||
>
|
||||
<div
|
||||
className="pgn__form-group d-inline-block mb-0"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label h6 d-block"
|
||||
htmlFor="field-firstName"
|
||||
size="sm"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
data-hj-suppress={true}
|
||||
id="field-firstName"
|
||||
name="first_name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="Test"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn__form-group d-inline-block mb-0"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label h6 d-block"
|
||||
htmlFor="field-lastName"
|
||||
size="sm"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
data-hj-suppress={true}
|
||||
id="field-lastName"
|
||||
name="last_name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="Name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn__form-text pgn__form-text-default"
|
||||
>
|
||||
<div>
|
||||
Helpful Text
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn__form-control-description pgn__form-text pgn__form-text-invalid"
|
||||
id="field-name-3"
|
||||
>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-default mr-2 btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Reference in New Issue
Block a user