From d4fd7acbd65ea665fe84913bb2fa12944d12017b Mon Sep 17 00:00:00 2001 From: Adam Butterworth Date: Thu, 25 Apr 2019 14:44:29 -0400 Subject: [PATCH] feat: refactor and add email confirmation message (#6) * refactor: delete example service * fix: properly send errors through to ui * feat: add editable options to fields * fix: make button display as a link * fix: remove unnecessary Object.create for error * feat: add email confirmation message and refactor to support the pattern * refactor: move isEditing prop to form selector --- src/account-settings/AccountSettingsPage.jsx | 108 +++++------------- .../AccountSettingsPage.messages.jsx | 5 + src/account-settings/actions.js | 4 +- .../components/EditableField.jsx | 23 +++- src/account-settings/constants.js | 13 +++ src/account-settings/reducers.js | 6 + src/account-settings/sagas.js | 3 +- src/account-settings/selectors.js | 2 + 8 files changed, 78 insertions(+), 86 deletions(-) create mode 100644 src/account-settings/constants.js diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index e55952a..5e3f9e7 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -5,43 +5,50 @@ import { injectIntl, intlShape } from 'react-intl'; import messages from './AccountSettingsPage.messages'; -import { fetchAccount, openForm, closeForm, saveAccount } from './actions'; +import { fetchAccount } from './actions'; import { pageSelector } from './selectors'; import { PageLoading } from '../common'; import EditableField from './components/EditableField'; - +import { yearOfBirthOptions, yearOfBirthDefault } from './constants'; class AccountSettingsPage extends React.Component { componentDidMount() { this.props.fetchAccount(); } - renderSection({ - sectionHeading, sectionDescription, fields, - }) { - return ( -
-

{this.props.intl.formatMessage(sectionHeading)}

-

{this.props.intl.formatMessage(sectionDescription)}

- {fields.map(field => ( - - ), this)} -
- ); - } - renderContent() { return (
- {this.props.fieldSections.map(this.renderSection, this)} +

{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}

+

{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}

+ + + + +
@@ -89,73 +96,16 @@ AccountSettingsPage.propTypes = { loading: PropTypes.bool, loaded: PropTypes.bool, loadingError: PropTypes.string, - openFormId: PropTypes.string, - fieldSections: PropTypes.arrayOf(PropTypes.shape({ - sectionHeading: PropTypes.object, - sectionDescription: PropTypes.object, - fields: PropTypes.array, - })), - fetchAccount: PropTypes.func.isRequired, - openForm: PropTypes.func.isRequired, - closeForm: PropTypes.func.isRequired, - saveAccount: PropTypes.func.isRequired, }; AccountSettingsPage.defaultProps = { loading: false, loaded: false, loadingError: null, - openFormId: null, - fieldSections: [ - { - sectionHeading: messages['account.settings.section.account.information'], - sectionDescription: messages['account.settings.section.account.information.description'], - fields: [ - { - name: 'username', - isEditable: false, - label: messages['account.settings.field.username'], - type: 'text', - }, - { - name: 'name', - isEditable: true, - label: messages['account.settings.field.full.name'], - type: 'text', - }, - { - name: 'email', - isEditable: true, - label: messages['account.settings.field.email'], - type: 'email', - }, - { - name: 'year_of_birth', - isEditable: true, - label: messages['account.settings.field.dob'], - type: 'select', - options: (() => { - const currentYear = new Date().getFullYear(); - const years = []; - let startYear = currentYear - 120; - while (startYear < currentYear) { - startYear += 1; - years.push({ value: startYear, label: startYear }); - } - return years.reverse(); - })(), - defaultValue: new Date().getFullYear() - 35, - }, - ], - }, - ], }; export default connect(pageSelector, { fetchAccount, - openForm, - closeForm, - saveAccount, })(injectIntl(AccountSettingsPage)); diff --git a/src/account-settings/AccountSettingsPage.messages.jsx b/src/account-settings/AccountSettingsPage.messages.jsx index 4660b22..a70c2aa 100644 --- a/src/account-settings/AccountSettingsPage.messages.jsx +++ b/src/account-settings/AccountSettingsPage.messages.jsx @@ -31,6 +31,11 @@ const messages = defineMessages({ defaultMessage: 'Email address (Sign in)', description: 'Label for 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.', + description: 'Confirmation message for saving the account settings email field.', + }, 'account.settings.field.dob': { id: 'account.settings.field.dob', defaultMessage: 'Year of birth', diff --git a/src/account-settings/actions.js b/src/account-settings/actions.js index f189905..ae5d585 100644 --- a/src/account-settings/actions.js +++ b/src/account-settings/actions.js @@ -69,9 +69,9 @@ export const saveAccountBegin = () => ({ type: SAVE_ACCOUNT.BEGIN, }); -export const saveAccountSuccess = values => ({ +export const saveAccountSuccess = (values, confirmationValues) => ({ type: SAVE_ACCOUNT.SUCCESS, - payload: { values }, + payload: { values, confirmationValues }, }); export const saveAccountReset = () => ({ diff --git a/src/account-settings/components/EditableField.jsx b/src/account-settings/components/EditableField.jsx index 3928dd0..7076a74 100644 --- a/src/account-settings/components/EditableField.jsx +++ b/src/account-settings/components/EditableField.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import { Button } from '@edx/paragon'; import Input from './temp/Input'; @@ -25,6 +25,8 @@ function EditableField(props) { type, value, error, + confirmationMessageDefinition, + confirmationValue, helpText, onEdit, onCancel, @@ -32,6 +34,7 @@ function EditableField(props) { onChange, isEditing, isEditable, + intl, ...others } = props; const id = `field-${name}`; @@ -55,6 +58,11 @@ function EditableField(props) { onCancel(name); }; + const renderConfirmationMessage = () => { + if (!confirmationMessageDefinition || !confirmationValue) return null; + return intl.formatMessage(confirmationMessageDefinition, { value: confirmationValue }); + }; + return (

{value}

-

{helpText}

+

{renderConfirmationMessage() || helpText}

), }} @@ -128,6 +136,12 @@ EditableField.propTypes = { type: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 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, @@ -135,11 +149,14 @@ EditableField.propTypes = { onChange: PropTypes.func.isRequired, isEditing: PropTypes.bool, isEditable: PropTypes.bool, + intl: intlShape.isRequired, }; EditableField.defaultProps = { value: undefined, error: undefined, + confirmationMessageDefinition: undefined, + confirmationValue: undefined, helpText: undefined, isEditing: false, isEditable: true, @@ -151,4 +168,4 @@ export default connect(formSelector, { onCancel: closeForm, onChange: updateDraft, onSubmit: saveAccount, -})(EditableField); +})(injectIntl(EditableField)); diff --git a/src/account-settings/constants.js b/src/account-settings/constants.js new file mode 100644 index 0000000..79085d9 --- /dev/null +++ b/src/account-settings/constants.js @@ -0,0 +1,13 @@ + +export const yearOfBirthOptions = (() => { + const currentYear = new Date().getFullYear(); + const years = []; + let startYear = currentYear - 120; + while (startYear < currentYear) { + startYear += 1; + years.push({ value: startYear, label: startYear }); + } + return years.reverse(); +})(); + +export const yearOfBirthDefault = new Date().getFullYear() - 35; diff --git a/src/account-settings/reducers.js b/src/account-settings/reducers.js index cc8e671..f0de4e5 100644 --- a/src/account-settings/reducers.js +++ b/src/account-settings/reducers.js @@ -14,6 +14,7 @@ export const defaultState = { data: null, values: {}, errors: {}, + confirmationValues: {}, drafts: {}, saveState: null, }; @@ -95,6 +96,11 @@ const accountSettingsReducer = (state = defaultState, action) => { saveState: 'complete', values: Object.assign({}, state.values, action.payload.values), errors: {}, + confirmationValues: Object.assign( + {}, + state.confirmationValues, + action.payload.confirmationValues, + ), }; case SAVE_ACCOUNT.FAILURE: return { diff --git a/src/account-settings/sagas.js b/src/account-settings/sagas.js index c7f312a..380213a 100644 --- a/src/account-settings/sagas.js +++ b/src/account-settings/sagas.js @@ -41,8 +41,7 @@ export function* handleSaveAccount(action) { const username = yield select(getUsername); const { commitValues } = action.payload; const savedValues = yield call(ApiService.patchAccount, username, commitValues); - - yield put(saveAccountSuccess(savedValues)); + yield put(saveAccountSuccess(savedValues, commitValues)); yield put(closeForm(action.payload.formId)); } catch (e) { if (e.fieldErrors) { diff --git a/src/account-settings/selectors.js b/src/account-settings/selectors.js index 269bd65..40d8d7a 100644 --- a/src/account-settings/selectors.js +++ b/src/account-settings/selectors.js @@ -12,5 +12,7 @@ export const formSelector = (state, props) => { return { value, error: state[storeName].errors[props.name], + confirmationValue: state[storeName].confirmationValues[props.name], + isEditing: state[storeName].openFormId === props.name, }; };