diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 7dcda59..c3f3c32 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -31,6 +31,7 @@ import PageLoading from './PageLoading'; import JumpNav from './JumpNav'; import DeleteAccount from './delete-account'; import EditableField from './EditableField'; +import EditableSelectField from './EditableSelectField'; import ResetPassword from './reset-password'; import NameChange from './name-change'; import ThirdPartyAuth from './third-party-auth'; @@ -609,7 +610,7 @@ class AccountSettingsPage extends React.Component { {(!getConfig().ENABLE_COPPA_COMPLIANCE) && ( - )} - {showState && ( - - option.value !== 'el') + : educationLevelOptions} label={this.props.intl.formatMessage(messages['account.settings.field.education'])} emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.education.empty'])} {...editableFieldProps} /> - - - - { type, value, userSuppliedValue, - options, saveState, error, confirmationMessageDefinition, @@ -45,10 +42,6 @@ const EditableField = (props) => { ...others } = props; const id = `field-${name}`; - let inputOptions = options; - if (getConfig().ENABLE_COPPA_COMPLIANCE && name === 'level_of_education' && options) { - inputOptions = options.filter(option => option.value !== 'el'); - } const handleSubmit = (e) => { e.preventDefault(); @@ -80,15 +73,6 @@ const EditableField = (props) => { } let finalValue = rawValue; - if (options) { - // Use == instead of === to prevent issues when HTML casts numbers as strings - // eslint-disable-next-line eqeqeq - const selectedOption = options.find(option => option.value == rawValue); - if (selectedOption) { - finalValue = selectedOption.label; - } - } - if (userSuppliedValue) { finalValue += `: ${userSuppliedValue}`; } @@ -112,25 +96,24 @@ const EditableField = (props) => { editing: ( <>
- - - {label} + - <>{others.children} - + {!!helpText && {helpText}} + {error != null && {error}} + {others.children} +

{ + const { + name, + label, + emptyLabel, + type, + value, + userSuppliedValue, + options, + saveState, + error, + confirmationMessageDefinition, + confirmationValue, + helpText, + onEdit, + onCancel, + onSubmit, + onChange, + isEditing, + isEditable, + isGrayedOut, + intl, + ...others + } = props; + const id = `field-${name}`; + + const handleSubmit = (e) => { + e.preventDefault(); + onSubmit(name, new FormData(e.target).get(name)); + }; + + const handleChange = (e) => { + onChange(name, e.target.value); + }; + + const handleEdit = () => { + onEdit(name); + }; + + const handleCancel = () => { + onCancel(name); + }; + + const renderEmptyLabel = () => { + if (isEditable) { + return ; + } + return {emptyLabel}; + }; + + const renderValue = (rawValue) => { + if (!rawValue) { + return renderEmptyLabel(); + } + let finalValue = rawValue; + + if (options) { + // Use == instead of === to prevent issues when HTML casts numbers as strings + // eslint-disable-next-line eqeqeq + const selectedOption = options.find(option => option.value == rawValue); + if (selectedOption) { + finalValue = selectedOption.label; + } + } + + if (userSuppliedValue) { + finalValue += `: ${userSuppliedValue}`; + } + + return finalValue; + }; + + const renderConfirmationMessage = () => { + if (!confirmationMessageDefinition || !confirmationValue) { + return null; + } + return intl.formatMessage(confirmationMessageDefinition, { + value: confirmationValue, + }); + }; + const selectOptions = options.map(option => ( + + )); + + return ( + + + + {label} + + {options.length > 0 && selectOptions} + + {!!helpText && {helpText}} + {error != null && {error}} + {others.children} + +

+ { + // 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={[]} + /> + +

+
+ {['name', 'verified_name'].includes(name) && } + + ), + default: ( +
+
+
{label}
+ {isEditable ? ( + + ) : null} +
+

{renderValue(value)}

+

{renderConfirmationMessage() || helpText}

+
+ ), + }} + /> + ); +}; + +EditableSelectField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]), + emptyLabel: PropTypes.node, + type: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + userSuppliedValue: PropTypes.string, + options: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + })), + saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']), + error: 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, +}; + +EditableSelectField.defaultProps = { + value: undefined, + options: [], + saveState: undefined, + label: undefined, + emptyLabel: undefined, + error: undefined, + confirmationMessageDefinition: undefined, + confirmationValue: undefined, + helpText: undefined, + isEditing: false, + isEditable: true, + isGrayedOut: false, + userSuppliedValue: undefined, +}; + +export default connect(editableFieldSelector, { + onEdit: openForm, + onCancel: closeForm, +})(injectIntl(EditableSelectField)); diff --git a/src/account-settings/EmailField.jsx b/src/account-settings/EmailField.jsx index 5922ecf..a0626ca 100644 --- a/src/account-settings/EmailField.jsx +++ b/src/account-settings/EmailField.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { - Button, StatefulButton, Input, ValidationFormGroup, + Button, StatefulButton, Form, } from '@edx/paragon'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; @@ -106,14 +106,12 @@ const EmailField = (props) => { cases={{ editing: (
- - - {label} + { value={value} onChange={handleChange} /> - + {!!helpText && {helpText}} + {error != null && {error}} +

( - ( - ( return props.saveSettings('phone_number', props.phone_number); }} /> - - { const { name } = e.target; @@ -64,9 +62,15 @@ const CoachingToggle = (props) => ( }; props.saveSettings(name, value); }} - /> - - + > + {props.intl.formatMessage(messages['account.settings.field.coaching_consent'])} + + {!!props.error && ( + + {props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])} + + )} + ); diff --git a/src/account-settings/coaching/test/__snapshots__/CoachingConsent.test.jsx.snap b/src/account-settings/coaching/test/__snapshots__/CoachingConsent.test.jsx.snap index 9c381f5..9e91a2e 100644 --- a/src/account-settings/coaching/test/__snapshots__/CoachingConsent.test.jsx.snap +++ b/src/account-settings/coaching/test/__snapshots__/CoachingConsent.test.jsx.snap @@ -52,14 +52,19 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = ` > Please confirm your name - +

+ +
Enter your mobile number - +
+ +
Please confirm your name - +
+ +
Enter your mobile number - +
+ +

- - - + - + {errorType !== null && ( + {intl.formatMessage(invalidMessage)} + )} +
)} buttons={[ diff --git a/src/account-settings/delete-account/__snapshots__/ConfirmationModal.test.jsx.snap b/src/account-settings/delete-account/__snapshots__/ConfirmationModal.test.jsx.snap index 42fc86c..742b12d 100644 --- a/src/account-settings/delete-account/__snapshots__/ConfirmationModal.test.jsx.snap +++ b/src/account-settings/delete-account/__snapshots__/ConfirmationModal.test.jsx.snap @@ -91,29 +91,27 @@ Array [
- - - Unable to delete account - + +
@@ -170,7 +168,7 @@ Array [ role="presentation" >

Are you sure?

@@ -285,29 +281,210 @@ Array [
- - + +
+
+ + + + + A password is required - +
+
+ + +
+ + +
+ + +
+
+ , +] +`; + +exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = ` +Array [ +
, +
+
+
+
+
+
+

+ Are you sure? +

+
+
+
+
+
+ +
+
+
+ You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted. +
+

+ If you proceed, you will be unable to use this account to take courses on localhost. +

+

+ You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion. +

+
+
+
+ +
+ +
@@ -352,162 +529,3 @@ Array [
, ] `; - -exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = ` -Array [ -
, -
-
-
-
-
-
-

- Are you sure? -

-
-
-
-
-
- -
-
-
- You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted. -
-

- If you proceed, you will be unable to use this account to take courses on localhost. -

-

- You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion. -

-
-
-
- - - - Unable to delete account - -
-
-
-
- - -
-
-
-
-
-
, -] -`; diff --git a/src/account-settings/demographics/Checkboxes.jsx b/src/account-settings/demographics/Checkboxes.jsx index c8cf396..97e45c3 100644 --- a/src/account-settings/demographics/Checkboxes.jsx +++ b/src/account-settings/demographics/Checkboxes.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { CheckBox } from '@edx/paragon'; +import { Form } from '@edx/paragon'; import { DECLINED } from '../data/constants'; const Checkboxes = (props) => { @@ -40,16 +40,17 @@ const Checkboxes = (props) => { const isChecked = selected.includes(option.value); return (
- handleToggle(value, option.value)} - /> + onChange={(event) => handleToggle(event.target.checked, option.value)} + > + {option.label} +
); }); diff --git a/src/account-settings/demographics/DemographicsSection.jsx b/src/account-settings/demographics/DemographicsSection.jsx index a4b406e..630549c 100644 --- a/src/account-settings/demographics/DemographicsSection.jsx +++ b/src/account-settings/demographics/DemographicsSection.jsx @@ -5,7 +5,7 @@ import { intlShape, } from '@edx/frontend-platform/i18n'; -import { Hyperlink, Input } from '@edx/paragon'; +import { Hyperlink, Form } from '@edx/paragon'; import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; @@ -13,7 +13,7 @@ import get from 'lodash.get'; import isEmpty from 'lodash.isempty'; import memoize from 'memoize-one'; import { demographicsSectionSelector } from '../data/selectors'; -import EditableField from '../EditableField'; +import EditableSelectField from '../EditableSelectField'; import Checkboxes from './Checkboxes'; import Alert from '../Alert'; import { saveMultipleSettings, updateDraft } from '../data/actions'; @@ -188,7 +188,7 @@ class DemographicsSection extends React.Component { */} {this.hasRetrievedDemographicsOptions() && (
- {showSelfDescribe && ( - )} - - + - + - - - - {showWorkStatusDescribe && ( - )} - - + - { {renderManagedProfileMessage()}
-