From 547d55a31f248fdc0e5e43d7b6e347291f3f33e4 Mon Sep 17 00:00:00 2001 From: Muhammad Abdullah Waheed <42172960+abdullahwaheed@users.noreply.github.com> Date: Tue, 8 Nov 2022 18:42:09 +0500 Subject: [PATCH] Paragon form component deprecations (#612) * refactor: removed deprecated paragon components from CoachingToggle and used alternatives * refactor: removed deprecated paragon components from ConfirmationModal and used alternatives * refactor: removed deprecations from EditableField and created separate component for SelectField * refactor: updated DemographicsSection to use new select component * refactor: removed deprecations from EmailField and used alternatives * refactor: removed deprecated Input from CoachingConsentForm * refactor: removed deprecated Input from DemographicsSection * refactor: removed deprecated Input from SummaryPanel component * refactor: removed deprecated CheckBox and used Form.CheckBox * refactor: fixed unit tests * refactor: changes based on PR reviews * fix: linting issue --- src/account-settings/AccountSettingsPage.jsx | 21 +- src/account-settings/EditableField.jsx | 42 +- src/account-settings/EditableSelectField.jsx | 232 ++++++++++ src/account-settings/EmailField.jsx | 18 +- .../coaching/CoachingConsentForm.jsx | 6 +- .../coaching/CoachingToggle.jsx | 24 +- .../CoachingConsent.test.jsx.snap | 80 ++-- .../delete-account/ConfirmationModal.jsx | 20 +- .../ConfirmationModal.test.jsx.snap | 410 +++++++++--------- .../demographics/Checkboxes.jsx | 11 +- .../demographics/DemographicsSection.jsx | 32 +- src/id-verification/panels/SummaryPanel.jsx | 4 +- 12 files changed, 579 insertions(+), 321 deletions(-) create mode 100644 src/account-settings/EditableSelectField.jsx 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()}
-