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:
@@ -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));
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = () => ({
|
||||
|
||||
@@ -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));
|
||||
|
||||
13
src/account-settings/constants.js
Normal file
13
src/account-settings/constants.js
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user