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
This commit is contained in:
Adam Butterworth
2019-05-28 12:55:23 -06:00
committed by GitHub
parent 747fe550c7
commit 71f007b9df
7 changed files with 219 additions and 34 deletions

View File

@@ -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 {
<EmailField
name="secondary_email"
label={this.props.intl.formatMessage(messages['account.settings.field.secondary.email'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.secondary.email.empty'])}
value={this.props.formValues.secondary_email}
confirmationMessageDefinition={messages['account.settings.field.secondary.email.confirmation']}
{...editableFieldProps}
@@ -178,17 +210,27 @@ class AccountSettingsPage extends React.Component {
type="text"
value={this.props.formValues.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={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
isEditable={!this.props.staticFields.includes('name')}
isEditable={this.isEditable('name')}
{...editableFieldProps}
/>
<EmailField
name="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
emptyLabel={
this.isEditable('email') ?
this.props.intl.formatMessage(messages['account.settings.field.email.empty']) :
this.renderEmptyStaticFieldMessage()
}
value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
isEditable={!this.props.staticFields.includes('email')}
isEditable={this.isEditable('email')}
{...editableFieldProps}
/>
{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}
/>
<EditableField
name="country"
type="select"
value={this.props.formValues.country}
options={this.props.countryOptions}
options={this.countryOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
isEditable={!this.props.staticFields.includes('country')}
emptyLabel={
this.isEditable('country') ?
this.props.intl.formatMessage(messages['account.settings.field.country.empty']) :
this.renderEmptyStaticFieldMessage()
}
isEditable={this.isEditable('country')}
{...editableFieldProps}
/>
</section>
@@ -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}
/>
<EditableField
@@ -231,14 +280,16 @@ class AccountSettingsPage extends React.Component {
value={this.props.formValues.gender}
options={this.genderOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.gender'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
{...editableFieldProps}
/>
<EditableField
name="language_proficiencies"
type="select"
value={this.props.formValues.language_proficiencies}
options={this.props.languageProficiencyOptions}
options={this.languageProficiencyOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps}
/>
</section>
@@ -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}
/>
<EditableField
@@ -261,6 +313,7 @@ class AccountSettingsPage extends React.Component {
type="text"
value={this.props.formValues.social_link_facebook}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.facebook'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.facebook.empty'])}
{...editableFieldProps}
/>
<EditableField
@@ -268,6 +321,7 @@ class AccountSettingsPage extends React.Component {
type="text"
value={this.props.formValues.social_link_twitter}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter.empty'])}
{...editableFieldProps}
/>
</section>
@@ -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,

View File

@@ -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: 'Weve 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: 'Weve 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 browsers 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;

View File

@@ -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 <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
}
return <span className="text-muted">{emptyLabel}</span>;
};
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) {
</Button>
) : null}
</div>
<p>{getValue(value)}</p>
<p>{renderValue(value)}</p>
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
</div>
),
@@ -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,

View File

@@ -21,6 +21,7 @@ function EmailField(props) {
const {
name,
label,
emptyLabel,
value,
saveState,
error,
@@ -82,6 +83,18 @@ function EmailField(props) {
</span>
);
const renderEmptyLabel = () => {
if (isEditable) {
return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
}
return <span className="text-muted">{emptyLabel}</span>;
};
const renderValue = () => {
if (confirmationValue) return renderConfirmationValue();
return value || renderEmptyLabel();
};
return (
<SwitchContent
expression={isEditing ? 'editing' : 'default'}
@@ -143,7 +156,7 @@ function EmailField(props) {
</Button>
) : null}
</div>
<p>{confirmationValue ? renderConfirmationValue() : value}</p>
<p>{renderValue()}</p>
{renderConfirmationMessage() || <p className="small text-muted mt-n2">{helpText}</p>}
</div>
),
@@ -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,

View File

@@ -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',

View File

@@ -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;
},

View File

@@ -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;
}