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
This commit is contained in:
Adam Butterworth
2019-04-25 14:44:29 -04:00
committed by GitHub
parent 53aaba4f13
commit d4fd7acbd6
8 changed files with 78 additions and 86 deletions

View File

@@ -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 (
<div key={this.props.intl.formatMessage(sectionHeading)}>
<h2>{this.props.intl.formatMessage(sectionHeading)}</h2>
<p>{this.props.intl.formatMessage(sectionDescription)}</p>
{fields.map(field => (
<EditableField
{...field}
label={this.props.intl.formatMessage(field.label)}
key={field.name}
isEditing={this.props.openFormId === field.name}
/>
), this)}
</div>
);
}
renderContent() {
return (
<div>
<div className="row">
<div className="col-md-8 col-lg-6">
{this.props.fieldSections.map(this.renderSection, this)}
<h2>{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
<EditableField
name="username"
type="text"
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
isEditable={false}
/>
<EditableField
name="name"
type="text"
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
/>
<EditableField
name="email"
type="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
/>
<EditableField
name="year_of_birth"
type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
options={yearOfBirthOptions}
defaultValue={yearOfBirthDefault}
/>
</div>
</div>
</div>
@@ -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));

View File

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

View File

@@ -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 = () => ({

View File

@@ -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 (
<SwitchContent
expression={isEditing ? 'editing' : 'default'}
@@ -113,7 +121,7 @@ function EditableField(props) {
) : null}
</div>
<p className="m-0">{value}</p>
<p className="small text-muted">{helpText}</p>
<p className="small text-muted">{renderConfirmationMessage() || helpText}</p>
</div>
),
}}
@@ -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));

View File

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

View File

@@ -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 {

View File

@@ -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) {

View File

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