Skip to content

Commit

Permalink
Merge pull request #33976 from code-dot-org/parent-info-dialog
Browse files Browse the repository at this point in the history
Adding the AddParentEmailModal
  • Loading branch information
bethanyaconnor committed Apr 6, 2020
2 parents 4fc6a14 + 39f29cf commit 689680a
Show file tree
Hide file tree
Showing 4 changed files with 479 additions and 0 deletions.
8 changes: 8 additions & 0 deletions apps/i18n/common/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
"actions": "Actions",
"add": "Add",
"addNewSection": "Add New Section",
"addParentEmailModal_parentEmail_invalid": "The email address you provided is not valid.",
"addParentEmailModal_parentEmail_isRequired": "An email address is required.",
"addParentEmailModal_parentEmail_label": "New parent/guardian email address",
"addParentEmailModal_parentEmail_mustBeDifferent": "New email address must not match old email address.",
"addParentEmailModal_emailOptIn_description": "Can we email you with occasional updates on your child’s progress and projects, and updates about their course and computer science?",
"addParentEmailModal_emailOptIn_isRequired": "This field is required.",
"addParentEmailModal_save": "Update",
"addParentEmailModal_title": "Update parent/guardian email address",
"addPartners": "Add Partners",
"addPassword": "Add a password",
"addPasswordHint": "By creating a password below, you will be able to sign in with your username or email address and password. It will also help keep your account safer because then we will prompt you for the password before any updates are made.",
Expand Down
233 changes: 233 additions & 0 deletions apps/src/lib/ui/accounts/AddParentEmailModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import PropTypes from 'prop-types';
import React from 'react';
import i18n from '@cdo/locale';
import BaseDialog from '../../../templates/BaseDialog';
import color from '../../../util/color';
import {isEmail} from '../../../util/formatValidation';
import {Field, Header, ConfirmCancelFooter} from '../SystemDialog/SystemDialog';
import {pegasus} from '../../util/urlHelpers';

const STATE_INITIAL = 'initial';
const STATE_SAVING = 'saving';
const STATE_UNKNOWN_ERROR = 'unknown-error';

export default class AddParentEmailModal extends React.Component {
static propTypes = {
/**
* @type {function({parentEmail: string, emailOptIn: string}):Promise}
*/
handleSubmit: PropTypes.func.isRequired,
/**
* @type {function()}
*/
handleCancel: PropTypes.func.isRequired,
currentParentEmail: PropTypes.string
};

constructor(props) {
super(props);
const displayedParentEmail = props.currentParentEmail
? props.currentParentEmail
: '';
this.state = {
saveState: STATE_INITIAL,
values: {
parentEmail: displayedParentEmail,
emailOptIn: ''
},
errors: {
parentEmail: '',
emailOptIn: ''
}
};
}

focusOnError() {
const {errors} = this.state;
if (errors.parentEmail) {
this.parentEmailInput.focus();
}
}

save = () => {
// No-op if we know the form is invalid, client-side.
// This blocks return-key submission when the form is invalid.
if (!this.isFormValid(this.getValidationErrors())) {
return;
}

const {values} = this.state;
this.setState({saveState: STATE_SAVING});
this.props.handleSubmit(values).catch(this.onSubmitFailure);
};

cancel = () => this.props.handleCancel();

onSubmitFailure = error => {
if (error && error.hasOwnProperty('serverErrors')) {
this.setState(
{
saveState: STATE_INITIAL,
errors: error.serverErrors
},
() => this.focusOnError()
);
} else {
this.setState({saveState: STATE_UNKNOWN_ERROR});
}
};

isFormValid(errors) {
return Object.keys(errors).every(key => !errors[key]);
}

getValidationErrors() {
const {errors} = this.state;
return {
parentEmail: errors.parentEmail || this.getNewEmailValidationError(),
emailOptIn: errors.emailOptIn || this.getEmailOptInValidationError()
};
}

getNewEmailValidationError = () => {
const {parentEmail} = this.state.values;
if (parentEmail.trim().length === 0) {
return i18n.addParentEmailModal_parentEmail_isRequired();
}
if (!isEmail(parentEmail.trim())) {
return i18n.addParentEmailModal_parentEmail_invalid();
}
if (parentEmail.trim() === this.props.currentParentEmail) {
return i18n.addParentEmailModal_parentEmail_mustBeDifferent();
}
return null;
};

getEmailOptInValidationError = () => {
const {emailOptIn} = this.state.values;
if (emailOptIn.length === 0) {
return i18n.addParentEmailModal_emailOptIn_isRequired();
}
return null;
};

onParentEmailChange = event => {
const {values, errors} = this.state;
values['parentEmail'] = event.target.value;
errors['parentEmail'] = '';
this.setState({values, errors});
};

onEmailOptInChange = event => {
const {values, errors} = this.state;
values['emailOptIn'] = event.target.value;
errors['emailOptIn'] = '';
this.setState({values, errors});
};

render = () => {
const {saveState, values} = this.state;
const validationErrors = this.getValidationErrors();
const isFormValid = this.isFormValid(validationErrors);
const saving = saveState === STATE_SAVING;
return (
<BaseDialog
useUpdatedStyles
isOpen
handleClose={this.cancel}
uncloseable={STATE_SAVING === saveState}
>
<div style={styles.container}>
<Header text={i18n.addParentEmailModal_title()} />
<Field
label={i18n.addParentEmailModal_parentEmail_label()}
error={validationErrors.parentEmail}
>
<input
type="email"
value={values.parentEmail}
disabled={saving}
tabIndex="1"
onKeyDown={this.onKeyDown}
onChange={this.onParentEmailChange}
autoComplete="off"
maxLength="255"
size="255"
style={styles.input}
ref={el => (this.parentEmailInput = el)}
/>
</Field>
<Field error={validationErrors.emailOptIn}>
<div style={styles.emailOptIn}>
<label style={styles.label}>
{i18n.addParentEmailModal_emailOptIn_description()}{' '}
<a href={pegasus('/privacy')}>
{i18n.changeEmailModal_emailOptIn_privacyPolicy()}
</a>
</label>
<div>
<div style={styles.radioButton}>
<input
type="radio"
id="yes"
value={'yes'}
disabled={saving}
checked={values['emailOptIn'] === 'yes'}
onChange={this.onEmailOptInChange}
/>
<label htmlFor="yes" style={styles.label}>
{i18n.yes()}
</label>
</div>
<div style={styles.radioButton}>
<input
type="radio"
id="no"
value={'no'}
disabled={saving}
checked={values['emailOptIn'] === 'no'}
onChange={this.onEmailOptInChange}
/>
<label htmlFor="no" style={styles.label}>
{i18n.no()}
</label>
</div>
</div>
</div>
</Field>
<ConfirmCancelFooter
confirmText={i18n.addParentEmailModal_save()}
onConfirm={this.save}
onCancel={this.cancel}
disableConfirm={saving || !isFormValid}
disableCancel={saving}
tabIndex="2"
>
{saving && <em>{i18n.saving()}</em>}
{STATE_UNKNOWN_ERROR === saveState && (
<em>{i18n.changeEmailModal_unexpectedError()}</em>
)}
</ConfirmCancelFooter>
</div>
</BaseDialog>
);
};
}

const styles = {
container: {
margin: 20,
color: color.charcoal
},
emailOptIn: {
display: 'flex',
flexDirection: 'row'
},
radioButton: {
display: 'flex',
justifyContent: 'space-between'
},
label: {
margin: 'auto'
}
};
20 changes: 20 additions & 0 deletions apps/src/lib/ui/accounts/AddParentEmailModal.story.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import AddParentEmailModal from './AddParentEmailModal';
import {action} from '@storybook/addon-actions';

export default storybook =>
storybook
.storiesOf('AddParentEmailModal', module)
.add('view with no current parent email', () => (
<AddParentEmailModal
handleSubmit={action('handleSubmit callback')}
handleCancel={action('handleCancel callback')}
/>
))
.add('view with current parent email', () => (
<AddParentEmailModal
handleSubmit={action('handleSubmit callback')}
handleCancel={action('handleCancel callback')}
currentParentEmail={'minerva@hogwarts.edu'}
/>
));

0 comments on commit 689680a

Please sign in to comment.