From 71f007b9df87fbff7b66497708717e2597bc700e Mon Sep 17 00:00:00 2001 From: Adam Butterworth Date: Tue, 28 May 2019 12:55:23 -0600 Subject: [PATCH] Add empty states and ability to delete values from select fields (#67) * fix: add empty states and ability to delete values from select fields * refactor: change name of isEditable method * refactor: make managed profile conditions clearer * refactor: be positive --- src/account-settings/AccountSettingsPage.jsx | 75 +++++++++++-- .../AccountSettingsPage.messages.jsx | 102 ++++++++++++++++-- .../components/EditableField.jsx | 35 ++++-- .../components/EmailField.jsx | 17 ++- src/account-settings/constants/index.js | 4 +- src/account-settings/selectors.js | 2 +- src/account-settings/service.js | 18 +++- 7 files changed, 219 insertions(+), 34 deletions(-) diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 46127b9..269378d 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -36,12 +36,24 @@ class AccountSettingsPage extends React.Component { super(props); this.educationLevels = EDUCATION_LEVELS.map(key => ({ value: key, - label: props.intl.formatMessage(messages[`account.settings.field.education.levels.${key}`]), + label: props.intl.formatMessage(messages[`account.settings.field.education.levels.${key || 'empty'}`]), })); this.genderOptions = GENDER_OPTIONS.map(key => ({ value: key, - label: props.intl.formatMessage(messages[`account.settings.field.gender.options.${key}`]), + label: props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]), })); + this.languageProficiencyOptions = [{ + value: '', + label: props.intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']), + }].concat(props.languageProficiencyOptions); + this.yearOfBirthOptions = [{ + value: '', + label: props.intl.formatMessage(messages['account.settings.field.year_of_birth.options.empty']), + }].concat(YEAR_OF_BIRTH_OPTIONS); + this.countryOptions = [{ + value: '', + label: props.intl.formatMessage(messages['account.settings.field.country.options.empty']), + }].concat(props.countryOptions); } componentDidMount() { @@ -67,6 +79,16 @@ class AccountSettingsPage extends React.Component { return concatTimeZoneOptions; }); + isEditable(fieldName) { + return !this.props.staticFields.includes(fieldName); + } + + isManagedProfile() { + // Enterprise customer profiles are managed by their organizations. We determine whether + // a profile is managed or not by the presence of the profileDataManager prop. + return Boolean(this.props.profileDataManager); + } + handleEditableFieldChange = (name, value) => { this.props.updateDraft(name, value); }; @@ -97,7 +119,7 @@ class AccountSettingsPage extends React.Component { } renderManagedProfileMessage() { - if (!this.props.profileDataManager) { + if (!this.isManagedProfile()) { return null; } @@ -126,6 +148,15 @@ class AccountSettingsPage extends React.Component { ); } + renderEmptyStaticFieldMessage() { + if (this.isManagedProfile()) { + return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], { + enterprise: this.props.profileDataManager, + }); + } + return this.props.intl.formatMessage(messages['account.settings.static.field.empty.no.admin']); + } + renderSecondaryEmailField(editableFieldProps) { if (this.props.hiddenFields.includes('secondary_email')) { return null; @@ -135,6 +166,7 @@ class AccountSettingsPage extends React.Component { {this.renderSecondaryEmailField(editableFieldProps)} @@ -197,17 +239,23 @@ class AccountSettingsPage extends React.Component { name="year_of_birth" type="select" label={this.props.intl.formatMessage(messages['account.settings.field.dob'])} + emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.dob.empty'])} value={this.props.formValues.year_of_birth} - options={YEAR_OF_BIRTH_OPTIONS} + options={this.yearOfBirthOptions} {...editableFieldProps} /> @@ -223,6 +271,7 @@ class AccountSettingsPage extends React.Component { value={this.props.formValues.level_of_education} options={this.educationLevels} label={this.props.intl.formatMessage(messages['account.settings.field.education'])} + emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.education.empty'])} {...editableFieldProps} /> @@ -254,6 +305,7 @@ class AccountSettingsPage extends React.Component { type="text" value={this.props.formValues.social_link_linkedin} label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.linkedin'])} + emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.linkedin.empty'])} {...editableFieldProps} /> @@ -293,6 +347,7 @@ class AccountSettingsPage extends React.Component { value={this.props.formValues.time_zone || ''} options={timeZoneOptions} label={this.props.intl.formatMessage(messages['account.settings.field.time.zone'])} + emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.time.zone.empty'])} helpText={this.props.intl.formatMessage(messages['account.settings.field.time.zone.description'])} {...editableFieldProps} onSubmit={(formId, value) => { @@ -378,7 +433,7 @@ AccountSettingsPage.propTypes = { name: PropTypes.string, email: PropTypes.string, secondary_email: PropTypes.string, - year_of_birth: PropTypes.number, + year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), country: PropTypes.string, level_of_education: PropTypes.string, gender: PropTypes.string, diff --git a/src/account-settings/AccountSettingsPage.messages.jsx b/src/account-settings/AccountSettingsPage.messages.jsx index 396a66c..0ceb832 100644 --- a/src/account-settings/AccountSettingsPage.messages.jsx +++ b/src/account-settings/AccountSettingsPage.messages.jsx @@ -76,6 +76,11 @@ const messages = defineMessages({ defaultMessage: 'Full name', description: 'Label for account settings name field.', }, + 'account.settings.field.full.name.empty': { + id: 'account.settings.field.full.name.empty', + defaultMessage: 'Add name', + description: 'Placeholder for empty account settings name field.', + }, 'account.settings.field.full.name.help.text': { id: 'account.settings.field.full.name.help.text', defaultMessage: 'The name that is used for ID verification and that appears on your certificates.', @@ -86,6 +91,11 @@ const messages = defineMessages({ defaultMessage: 'Email address (Sign in)', description: 'Label for account settings email field.', }, + 'account.settings.field.email.empty': { + id: 'account.settings.field.email.empty', + defaultMessage: 'Add email address', + description: 'Placeholder for empty account settings email field.', + }, 'account.settings.field.email.confirmation': { id: 'account.settings.field.email.confirmation', defaultMessage: 'We’ve sent a confirmation message to {value}. Click the link in the message to update your email address.', @@ -101,6 +111,11 @@ const messages = defineMessages({ defaultMessage: 'Recovery email address', description: 'Label for account settings recovery email field.', }, + 'account.settings.field.secondary.email.empty': { + id: 'account.settings.field.secondary.email.empty', + defaultMessage: 'Add a recovery email address', + description: 'Placeholder for empty account settings recovery email field.', + }, 'account.settings.field.secondary.email.confirmation': { id: 'account.settings.field.secondary.email.confirmation', defaultMessage: 'We’ve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.', @@ -116,14 +131,34 @@ const messages = defineMessages({ defaultMessage: 'Year of birth', description: 'Label for account settings year of birth field.', }, + 'account.settings.field.dob.empty': { + id: 'account.settings.field.dob.empty', + defaultMessage: 'Add year of birth', + description: 'Placeholder for empty account settings year of birth field.', + }, + 'account.settings.field.year_of_birth.options.empty': { + id: 'account.settings.field.year_of_birth.options.empty', + defaultMessage: 'Select a year of birth', + description: 'Option for empty value on account settings year of birth field.', + }, 'account.settings.field.country': { id: 'account.settings.field.country', defaultMessage: 'Country', description: 'Label for account settings country field.', }, + 'account.settings.field.country.empty': { + id: 'account.settings.field.country.empty', + defaultMessage: 'Add country', + description: 'Placeholder for empty account settings country field.', + }, + 'account.settings.field.country.options.empty': { + id: 'account.settings.field.country.options.empty', + defaultMessage: 'Select a Country', + description: 'Option for empty value on account settings country field.', + }, 'account.settings.field.site.language': { id: 'account.settings.field.site.language', - defaultMessage: 'Site Language', + defaultMessage: 'Site language', description: 'Label for account settings site language field.', }, 'account.settings.field.site.language.help.text': { @@ -136,8 +171,13 @@ const messages = defineMessages({ defaultMessage: 'Education', description: 'Label for account settings education field.', }, - 'account.settings.field.education.levels.null': { - id: 'account.settings.field.education.levels.null', + 'account.settings.field.education.empty': { + id: 'account.settings.field.education.empty', + defaultMessage: 'Add level of education', + description: 'Placeholder for empty account settings education field.', + }, + 'account.settings.field.education.levels.empty': { + id: 'account.settings.field.education.levels.empty', defaultMessage: 'Select a level of education', description: 'Placeholder for the education levels dropdown.', }, @@ -192,8 +232,13 @@ const messages = defineMessages({ defaultMessage: 'Gender', description: 'Label for account settings gender field.', }, - 'account.settings.field.gender.options.null': { - id: 'account.settings.field.gender.options.null', + 'account.settings.field.gender.empty': { + id: 'account.settings.field.gender.empty', + defaultMessage: 'Add gender', + description: 'Placeholder for empty account settings gender field.', + }, + 'account.settings.field.gender.options.empty': { + id: 'account.settings.field.gender.options.empty', defaultMessage: 'Select a gender', description: 'Placeholder for the gender options dropdown.', }, @@ -214,14 +259,29 @@ const messages = defineMessages({ }, 'account.settings.field.language.proficiencies': { id: 'account.settings.field.language.proficiencies', - defaultMessage: 'Spoken Languages', + defaultMessage: 'Spoken languages', description: 'Label for account settings spoken languages field.', }, + 'account.settings.field.language.proficiencies.empty': { + id: 'account.settings.field.language.proficiencies.empty', + defaultMessage: 'Add a spoken language', + description: 'Placeholder for empty account settings spoken languages field.', + }, + 'account.settings.field.language_proficiencies.options.empty': { + id: 'account.settings.field.language_proficiencies.options.empty', + defaultMessage: 'Select a Language', + description: 'Option for an empty value on account settings spoken languages field.', + }, 'account.settings.field.time.zone': { id: 'account.settings.field.time.zone', - defaultMessage: 'Time Zone', + defaultMessage: 'Time zone', description: 'Label for time zone settings field.', }, + 'account.settings.field.time.zone.empty': { + id: 'account.settings.field.time.zone.empty', + defaultMessage: 'Set time zone', + description: 'Placeholder for empty for time zone settings field.', + }, 'account.settings.field.time.zone.description': { id: 'account.settings.field.time.zone.description', defaultMessage: 'Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browser’s local time zone.', @@ -258,16 +318,34 @@ const messages = defineMessages({ defaultMessage: 'LinkedIn', description: 'Label for LinkedIn', }, + 'account.settings.field.social.platform.name.linkedin.empty': { + id: 'account.settings.field.social.platform.name.linkedin.empty', + defaultMessage: 'Add LinkedIn profile', + description: 'Placeholder for an empty LinkedIn field', + }, + 'account.settings.field.social.platform.name.twitter': { id: 'account.settings.field.social.platform.name.twitter', defaultMessage: 'Twitter', description: 'Label for Twitter', }, + 'account.settings.field.social.platform.name.twitter.empty': { + id: 'account.settings.field.social.platform.name.twitter.empty', + defaultMessage: 'Add Twitter profile', + description: 'Placeholder for an empty Twitter field', + }, + 'account.settings.field.social.platform.name.facebook': { id: 'account.settings.field.social.platform.name.facebook', defaultMessage: 'Facebook', description: 'Label for Facebook', }, + 'account.settings.field.social.platform.name.facebook.empty': { + id: 'account.settings.field.social.platform.name.facebook.empty', + defaultMessage: 'Add Facebook profile', + description: 'Placeholder for an empty Facebook field', + }, + 'account.settings.delete.account.header': { id: 'account.settings.delete.account.header', @@ -396,6 +474,16 @@ const messages = defineMessages({ defaultMessage: 'Edit', description: 'The edit button on an editable field', }, + 'account.settings.static.field.empty': { + id: 'account.settings.static.field.empty', + defaultMessage: 'No value set. Contact your {enterprise} administrator to make changes.', + description: 'The placeholder for an empty but uneditable field', + }, + 'account.settings.static.field.empty.no.admin': { + id: 'account.settings.static.field.empty.no.admin', + defaultMessage: 'No value set.', + description: 'The placeholder for an empty but uneditable field when there is no administrator', + }, }); export default messages; diff --git a/src/account-settings/components/EditableField.jsx b/src/account-settings/components/EditableField.jsx index 24b34ff..a6b3405 100644 --- a/src/account-settings/components/EditableField.jsx +++ b/src/account-settings/components/EditableField.jsx @@ -20,6 +20,7 @@ function EditableField(props) { const { name, label, + emptyLabel, type, value, options, @@ -39,16 +40,6 @@ function EditableField(props) { } = props; const id = `field-${name}`; - const getValue = (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) return selectedOption.label; - } - return rawValue; - }; - const handleSubmit = (e) => { e.preventDefault(); onSubmit(name, new FormData(e.target).get(name)); @@ -66,6 +57,26 @@ function EditableField(props) { onCancel(name); }; + const renderEmptyLabel = () => { + if (isEditable) { + return ; + } + return {emptyLabel}; + }; + + const renderValue = (rawValue) => { + if (!rawValue) return renderEmptyLabel(); + + 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) return selectedOption.label; + } + + return rawValue; + }; + const renderConfirmationMessage = () => { if (!confirmationMessageDefinition || !confirmationValue) return null; return intl.formatMessage(confirmationMessageDefinition, { @@ -135,7 +146,7 @@ function EditableField(props) { ) : null} -

{getValue(value)}

+

{renderValue(value)}

{renderConfirmationMessage() || helpText}

), @@ -148,6 +159,7 @@ function EditableField(props) { EditableField.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + emptyLabel: PropTypes.node, type: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), options: PropTypes.arrayOf(PropTypes.shape({ @@ -177,6 +189,7 @@ EditableField.defaultProps = { options: undefined, saveState: undefined, label: undefined, + emptyLabel: undefined, error: undefined, confirmationMessageDefinition: undefined, confirmationValue: undefined, diff --git a/src/account-settings/components/EmailField.jsx b/src/account-settings/components/EmailField.jsx index 427f87f..f27df09 100644 --- a/src/account-settings/components/EmailField.jsx +++ b/src/account-settings/components/EmailField.jsx @@ -21,6 +21,7 @@ function EmailField(props) { const { name, label, + emptyLabel, value, saveState, error, @@ -82,6 +83,18 @@ function EmailField(props) { ); + const renderEmptyLabel = () => { + if (isEditable) { + return ; + } + return {emptyLabel}; + }; + + const renderValue = () => { + if (confirmationValue) return renderConfirmationValue(); + return value || renderEmptyLabel(); + }; + return ( ) : null} -

{confirmationValue ? renderConfirmationValue() : value}

+

{renderValue()}

{renderConfirmationMessage() ||

{helpText}

} ), @@ -156,6 +169,7 @@ function EmailField(props) { EmailField.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + emptyLabel: PropTypes.node, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']), error: PropTypes.string, @@ -179,6 +193,7 @@ EmailField.defaultProps = { value: undefined, saveState: undefined, label: undefined, + emptyLabel: undefined, error: undefined, confirmationMessageDefinition: undefined, confirmationValue: undefined, diff --git a/src/account-settings/constants/index.js b/src/account-settings/constants/index.js index 1bbfa16..c6e2552 100644 --- a/src/account-settings/constants/index.js +++ b/src/account-settings/constants/index.js @@ -12,7 +12,7 @@ export const YEAR_OF_BIRTH_OPTIONS = (() => { })(); export const EDUCATION_LEVELS = [ - null, + '', 'p', 'm', 'b', @@ -25,7 +25,7 @@ export const EDUCATION_LEVELS = [ ]; export const GENDER_OPTIONS = [ - null, + '', 'f', 'm', 'o', diff --git a/src/account-settings/selectors.js b/src/account-settings/selectors.js index 80f6b92..e09f1f3 100644 --- a/src/account-settings/selectors.js +++ b/src/account-settings/selectors.js @@ -111,7 +111,7 @@ const formValuesSelector = createSelector( (values, drafts) => { const formValues = {}; Object.entries(values).forEach(([name, value]) => { - formValues[name] = chooseFormValue(drafts[name], value); + formValues[name] = chooseFormValue(drafts[name], value) || ''; }); return formValues; }, diff --git a/src/account-settings/service.js b/src/account-settings/service.js index 31385a4..cea805d 100644 --- a/src/account-settings/service.js +++ b/src/account-settings/service.js @@ -74,8 +74,22 @@ function packAccountCommitData(commitData) { delete packedData[key]; }); - if (commitData.language_proficiencies) { - packedData.language_proficiencies = [{ code: commitData.language_proficiencies }]; + if (commitData.language_proficiencies !== undefined) { + if (commitData.language_proficiencies) { + packedData.language_proficiencies = [{ code: commitData.language_proficiencies }]; + } else { + // An empty string should be sent as an array. + packedData.language_proficiencies = []; + } + } + + if (commitData.year_of_birth !== undefined) { + if (commitData.year_of_birth) { + packedData.year_of_birth = commitData.year_of_birth; + } else { + // An empty string should be sent as null. + packedData.year_of_birth = null; + } } return packedData; }