From 49488f9386c821bc1ebdf271d0290ddfe37011fb Mon Sep 17 00:00:00 2001 From: Adam Butterworth Date: Fri, 26 Apr 2019 13:38:50 -0400 Subject: [PATCH] feat: account Information Country, Education, Gender, Spoken Lang (#8) * feat: add country select. improve handling of select inputs * feat: add education field * feat: add gender field * fix: injectIntl shim should pass extra arguments * feat: add language proficiencies select Includes extra functionality for EditableField --- src/account-settings/AccountSettingsPage.jsx | 59 +++++++++- .../AccountSettingsPage.messages.jsx | 109 ++++++++++++++++-- .../components/EditableField.jsx | 85 +++++++++++--- .../components/temp/Input.jsx | 20 ++-- src/account-settings/constants.js | 23 +++- src/account-settings/reducers.js | 2 + src/account-settings/selectors.js | 1 + src/i18n/injectIntlWithShim.jsx | 4 +- 8 files changed, 263 insertions(+), 40 deletions(-) diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 03f5399..adac380 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -1,7 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { injectIntl, intlShape } from 'react-intl'; +import { + injectIntl, + intlShape, + getLocale, + getCountryList, + getLanguageList, +} from '@edx/frontend-i18n'; // eslint-disable-line import messages from './AccountSettingsPage.messages'; @@ -11,10 +17,30 @@ import { pageSelector } from './selectors'; import { PageLoading } from '../common'; import EditableField from './components/EditableField'; import PasswordReset from './components/PasswordReset'; -import { yearOfBirthOptions, yearOfBirthDefault } from './constants'; +import { + YEAR_OF_BIRTH_OPTIONS, + EDUCATION_LEVELS, + GENDER_OPTIONS, +} from './constants'; class AccountSettingsPage extends React.Component { + constructor(props) { + super(props); + this.countryOptions = getCountryList(getLocale()) + .map(({ code, name }) => ({ value: code, label: name })); + this.languageProficiencyOptions = getLanguageList(getLocale()) + .map(({ code, name }) => ({ value: code, label: name })); + this.educationLevels = EDUCATION_LEVELS.map(key => ({ + value: key, + label: props.intl.formatMessage(messages[`account.settings.field.education.levels.${key}`]), + })); + this.genderOptions = GENDER_OPTIONS.map(key => ({ + value: key, + label: props.intl.formatMessage(messages[`account.settings.field.gender.options.${key}`]), + })); + } + componentDidMount() { this.props.fetchAccount(); } @@ -48,10 +74,35 @@ class AccountSettingsPage extends React.Component { name="year_of_birth" type="select" label={this.props.intl.formatMessage(messages['account.settings.field.dob'])} - options={yearOfBirthOptions} - defaultValue={yearOfBirthDefault} + options={YEAR_OF_BIRTH_OPTIONS} /> + + + + (v.length ? v[0].code : null)} + reverseTransform={v => ([{ code: v }])} + label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])} + /> diff --git a/src/account-settings/AccountSettingsPage.messages.jsx b/src/account-settings/AccountSettingsPage.messages.jsx index a70c2aa..88e2d21 100644 --- a/src/account-settings/AccountSettingsPage.messages.jsx +++ b/src/account-settings/AccountSettingsPage.messages.jsx @@ -16,6 +16,16 @@ const messages = defineMessages({ defaultMessage: 'Error: {error}', description: 'Message when data failed to load', }, + 'account.settings.section.account.information': { + id: 'account.settings.section.account.information', + defaultMessage: 'Account Information', + description: 'The basic account information section heading.', + }, + 'account.settings.section.account.information.description': { + id: 'account.settings.section.account.information.description', + defaultMessage: 'These settings include basic information about your account.', + description: 'The basic account information section heading description.', + }, 'account.settings.field.username': { id: 'account.settings.field.username', defaultMessage: 'Username', @@ -41,15 +51,98 @@ const messages = defineMessages({ defaultMessage: 'Year of birth', description: 'Label for account settings year of birth field.', }, - 'account.settings.section.account.information': { - id: 'account.settings.section.account.information', - defaultMessage: 'Account Information', - description: 'The basic account information section heading.', + 'account.settings.field.country': { + id: 'account.settings.field.country', + defaultMessage: 'Country', + description: 'Label for account settings country field.', }, - 'account.settings.section.account.information.description': { - id: 'account.settings.section.account.information.description', - defaultMessage: 'These settings include basic information about your account.', - description: 'The basic account information section heading description.', + + 'account.settings.field.education': { + id: 'account.settings.field.education', + defaultMessage: 'Education', + description: 'Label for account settings education field.', + }, + 'account.settings.field.education.levels.null': { + id: 'account.settings.field.education.levels.null', + defaultMessage: 'Select a level of education', + description: 'Placeholder for the education levels dropdown.', + }, + 'account.settings.field.education.levels.p': { + id: 'account.settings.field.education.levels.p', + defaultMessage: 'Doctorate', + description: 'Selected by the user if their highest level of education is a doctorate degree.', + }, + 'account.settings.field.education.levels.m': { + id: 'account.settings.field.education.levels.m', + defaultMessage: "Master's or professional degree", + description: "Selected by the user if their highest level of education is a master's or professional degree from a college or university.", + }, + 'account.settings.field.education.levels.b': { + id: 'account.settings.field.education.levels.b', + defaultMessage: "Bachelor's Degree", + description: "Selected by the user if their highest level of education is a four year college or university bachelor's degree.", + }, + 'account.settings.field.education.levels.a': { + id: 'account.settings.field.education.levels.a', + defaultMessage: "Associate's degree", + description: "Selected by the user if their highest level of education is an associate's degree. 1-2 years of college or university.", + }, + 'account.settings.field.education.levels.hs': { + id: 'account.settings.field.education.levels.hs', + defaultMessage: 'Secondary/high school', + description: 'Selected by the user if their highest level of education is secondary or high school. 9-12 years of education.', + }, + 'account.settings.field.education.levels.jhs': { + id: 'account.settings.field.education.levels.jhs', + defaultMessage: 'Junior secondary/junior high/middle school', + description: 'Selected by the user if their highest level of education is junior or middle school. 6-8 years of education.', + }, + 'account.settings.field.education.levels.el': { + id: 'account.settings.field.education.levels.el', + defaultMessage: 'Elementary/primary school', + description: 'Selected by the user if their highest level of education is elementary or primary school. 1-5 years of education.', + }, + 'account.settings.field.education.levels.none': { + id: 'account.settings.field.education.levels.none', + defaultMessage: 'No formal education', + description: 'Selected by the user to describe their education.', + }, + 'account.settings.field.education.levels.o': { + id: 'account.settings.field.education.levels.o', + defaultMessage: 'Other education', + description: 'Selected by the user if they have a type of education not described by the other choices.', + }, + + 'account.settings.field.gender': { + id: 'account.settings.field.gender', + defaultMessage: 'Gender', + description: 'Label for account settings gender field.', + }, + 'account.settings.field.gender.options.null': { + id: 'account.settings.field.gender.options.null', + defaultMessage: 'Select a gender', + description: 'Placeholder for the gender options dropdown.', + }, + 'account.settings.field.gender.options.f': { + id: 'account.settings.field.gender.options.f', + defaultMessage: 'Female', + description: 'The label for the female gender option.', + }, + 'account.settings.field.gender.options.m': { + id: 'account.settings.field.gender.options.m', + defaultMessage: 'Male', + description: 'The label for the male gender option.', + }, + 'account.settings.field.gender.options.o': { + id: 'account.settings.field.gender.options.o', + defaultMessage: 'Other', + description: 'The label for catch-all gender option.', + }, + + 'account.settings.field.language.proficiencies': { + id: 'account.settings.field.language.proficiencies', + defaultMessage: 'Spoken Languages', + description: 'Label for account settings spoken languages field.', }, }); diff --git a/src/account-settings/components/EditableField.jsx b/src/account-settings/components/EditableField.jsx index 5d2b70b..e41eb96 100644 --- a/src/account-settings/components/EditableField.jsx +++ b/src/account-settings/components/EditableField.jsx @@ -1,8 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; -import { Button } from '@edx/paragon'; +import { FormattedMessage } from 'react-intl'; +import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line +import { Button, StatefulButton } from '@edx/paragon'; import Input from './temp/Input'; import ValidationFormGroup from './temp/ValidationFormGroup'; @@ -23,7 +24,9 @@ function EditableField(props) { name, label, type, - value, + value: propValue, + options, + saveState, error, confirmationMessageDefinition, confirmationValue, @@ -35,19 +38,36 @@ function EditableField(props) { isEditing, isEditable, intl, + transformValue, + reverseTransform, ...others } = props; const id = `field-${name}`; + const value = transformValue(propValue); + + const getValue = (rawValue) => { + if (options) { + if (Array.isArray(rawValue)) { + return rawValue.map(getValue).join(', '); + } + // 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(); - const data = {}; - new FormData(e.target).forEach((v, k) => { data[k] = v; }); + const data = { + [name]: reverseTransform(new FormData(e.target).get(name)), + }; onSubmit(name, data); }; const handleChange = (e) => { - onChange(name, e.target.value); + onChange(name, reverseTransform(e.target.value)); }; const handleEdit = () => { @@ -60,7 +80,9 @@ function EditableField(props) { const renderConfirmationMessage = () => { if (!confirmationMessageDefinition || !confirmationValue) return null; - return intl.formatMessage(confirmationMessageDefinition, { value: confirmationValue }); + return intl.formatMessage(confirmationMessageDefinition, { + value: transformValue(confirmationValue), + }); }; return ( @@ -82,17 +104,36 @@ function EditableField(props) { type={type} value={value} onChange={handleChange} + options={options} {...others} />

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