Skip to content

Commit

Permalink
Merge pull request #23374 from code-dot-org/staging
Browse files Browse the repository at this point in the history
DTT (Staging > Test) [robo-dtt]
  • Loading branch information
deploy-code-org committed Jun 27, 2018
2 parents 5c5d4d0 + 79b7c0f commit 0d44816
Show file tree
Hide file tree
Showing 72 changed files with 2,169 additions and 514 deletions.
13 changes: 13 additions & 0 deletions apps/i18n/common/en_us.json
Expand Up @@ -695,6 +695,18 @@
"makerViewProjectsTitle": "View your project list",
"makeYourOwnFlappy": "Make Your Own Flappy Game",
"manageAssets": "Manage Assets",
"manageLinkedAccounts": "Manage Linked Accounts",
"manageLinkedAccounts_actions": "Actions",
"manageLinkedAccounts_cannotDisconnectTooltip": "To make sure you can still sign in to your account, please add a password or another linked account first.",
"manageLinkedAccounts_clever": "Clever Account",
"manageLinkedAccounts_connect": "Connect",
"manageLinkedAccounts_disconnect": "Disconnect",
"manageLinkedAccounts_emailAddress": "Email Address",
"manageLinkedAccounts_facebook": "Facebook Account",
"manageLinkedAccounts_google_oauth2": "Google Account",
"manageLinkedAccounts_loginType": "Login Type",
"manageLinkedAccounts_microsoft": "Microsoft Account",
"manageLinkedAccounts_notConnected": "Not Connected",
"manageStudents": "Manage Students",
"manageStudentsNotificationFailure": "Something went wrong.",
"manageStudentsNotificationCannotAdd": "You could not add {numStudents, plural, one {1 student} other {# students}} to your section. Please try again or refresh the page.",
Expand Down Expand Up @@ -763,6 +775,7 @@
"parentsAndStudents": "Parents and Students",
"password": "Password",
"passwordConfirmation": "Password confirmation",
"passwordTooShort": "Password too short (minimum is 6 characters)",
"passwordsMustMatch": "Passwords must match",
"pause": "Break",
"percentCorrect": "% correct",
Expand Down
31 changes: 22 additions & 9 deletions apps/src/lib/ui/accounts/AddPasswordForm.jsx
Expand Up @@ -33,8 +33,10 @@ const styles = {
},
};

const MIN_PASSWORD_LENGTH = 6;
export const SAVING_STATE = i18n.saving();
export const SUCCESS_STATE = i18n.success();
export const PASSWORD_TOO_SHORT = i18n.passwordTooShort();
export const PASSWORDS_MUST_MATCH = i18n.passwordsMustMatch();

const DEFAULT_STATE = {
Expand Down Expand Up @@ -69,18 +71,28 @@ export default class AddPasswordForm extends React.Component {
});
};

passwordFieldsHaveContent = () => {
passwordsHaveMinimumContent = () => {
const {password, passwordConfirmation} = this.state;
return password.length > 0 && passwordConfirmation.length > 0;
return password.length >= MIN_PASSWORD_LENGTH && passwordConfirmation.length >= MIN_PASSWORD_LENGTH;
};

isFormValid = () => {
passwordsMatch = () => {
const {password, passwordConfirmation} = this.state;
return this.passwordFieldsHaveContent() && (password === passwordConfirmation);
return password === passwordConfirmation;
};

isFormValid = () => {
return this.passwordsHaveMinimumContent() && this.passwordsMatch();
};

minimumLengthError = (value) => {
if (value.length > 0 && value.length < MIN_PASSWORD_LENGTH) {
return PASSWORD_TOO_SHORT;
}
};

mismatchedPasswordsError = () => {
if (this.passwordFieldsHaveContent() && !this.isFormValid()) {
if (this.passwordsHaveMinimumContent() && !this.passwordsMatch()) {
return PASSWORDS_MUST_MATCH;
}
};
Expand Down Expand Up @@ -116,7 +128,7 @@ export default class AddPasswordForm extends React.Component {
};

render() {
const {submissionState} = this.state;
const {password, passwordConfirmation, submissionState} = this.state;
let statusTextStyles = styles.statusText;
statusTextStyles = submissionState.isError ? {...statusTextStyles, ...styles.errorText} : statusTextStyles;

Expand All @@ -131,13 +143,14 @@ export default class AddPasswordForm extends React.Component {
</div>
<PasswordField
label={i18n.password()}
value={this.state.password}
error={this.minimumLengthError(password)}
value={password}
onChange={this.onPasswordChange}
/>
<PasswordField
label={i18n.passwordConfirmation()}
error={this.mismatchedPasswordsError()}
value={this.state.passwordConfirmation}
error={this.minimumLengthError(passwordConfirmation) || this.mismatchedPasswordsError()}
value={passwordConfirmation}
onChange={this.onPasswordConfirmationChange}
/>
<div style={styles.buttonContainer}>
Expand Down
159 changes: 159 additions & 0 deletions apps/src/lib/ui/accounts/ManageLinkedAccounts.jsx
@@ -0,0 +1,159 @@
import React, {PropTypes} from 'react';
import ReactTooltip from 'react-tooltip';
import _ from 'lodash';
import i18n from '@cdo/locale';
import color from '@cdo/apps/util/color';
import {tableLayoutStyles} from "@cdo/apps/templates/tables/tableConstants";
import BootstrapButton from './BootstrapButton';

const OAUTH_PROVIDERS = {
GOOGLE: 'google_oauth2',
FACEBOOK: 'facebook',
CLEVER: 'clever',
MICROSOFT: 'microsoft',
};

export default class ManageLinkedAccounts extends React.Component {
render() {
return (
<div style={styles.container}>
<hr/>
<h2 style={styles.header}>{i18n.manageLinkedAccounts()}</h2>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.headerCell}>{i18n.manageLinkedAccounts_loginType()}</th>
<th style={styles.headerCell}>{i18n.manageLinkedAccounts_emailAddress()}</th>
<th style={styles.headerCell}>{i18n.manageLinkedAccounts_actions()}</th>
</tr>
</thead>
<tbody>
<OauthConnection
type={OAUTH_PROVIDERS.GOOGLE}
displayName={i18n.manageLinkedAccounts_google_oauth2()}
email={'brad@code.org'}
onClick={() => {}}
/>
<OauthConnection
type={OAUTH_PROVIDERS.MICROSOFT}
displayName={i18n.manageLinkedAccounts_microsoft()}
email={'*** encrypted ***'}
onClick={() => {}}
/>
<OauthConnection
type={OAUTH_PROVIDERS.CLEVER}
displayName={i18n.manageLinkedAccounts_clever()}
email={undefined}
onClick={() => {}}
/>
<OauthConnection
type={OAUTH_PROVIDERS.FACEBOOK}
displayName={i18n.manageLinkedAccounts_facebook()}
email={'brad@code.org'}
onClick={() => {}}
cannotDisconnect
/>
</tbody>
</table>
</div>
);
}
}

class OauthConnection extends React.Component {
static propTypes = {
type: PropTypes.oneOf(Object.values(OAUTH_PROVIDERS)).isRequired,
displayName: PropTypes.string.isRequired,
email: PropTypes.string,
onClick: PropTypes.func.isRequired,
cannotDisconnect: PropTypes.bool
};

render() {
const {displayName, email, onClick, cannotDisconnect} = this.props;
const emailStyles = !!email ? styles.cell : {...styles.cell, ...styles.emptyEmailCell};
const buttonText = !!email ?
i18n.manageLinkedAccounts_disconnect() :
i18n.manageLinkedAccounts_connect();
const tooltipId = _.uniqueId();

return (
<tr>
<td style={styles.cell}>
{displayName}
</td>
<td style={emailStyles}>
{email || i18n.manageLinkedAccounts_notConnected()}
</td>
<td style={styles.cell}>
<span
data-for={tooltipId}
data-tip
>
{/* This button intentionally uses BootstrapButton to match other account page buttons */}
<BootstrapButton
style={styles.button}
text={buttonText}
onClick={onClick}
disabled={cannotDisconnect}
/>
{cannotDisconnect &&
<ReactTooltip
id={tooltipId}
offset={styles.tooltipOffset}
role="tooltip"
effect="solid"
>
<div style={styles.tooltip}>
{i18n.manageLinkedAccounts_cannotDisconnectTooltip()}
</div>
</ReactTooltip>
}
</span>
</td>
</tr>
);
}
}

const GUTTER = 20;
const BUTTON_WIDTH = 105;
const styles = {
container: {
paddingTop: GUTTER,
},
header: {
fontSize: 22,
},
table: {
...tableLayoutStyles.table,
marginTop: GUTTER,
},
headerCell: {
...tableLayoutStyles.headerCell,
paddingLeft: GUTTER,
paddingRight: GUTTER,
fontWeight: 'normal',
},
cell: {
...tableLayoutStyles.cell,
paddingLeft: GUTTER,
paddingRight: GUTTER,
},
emptyEmailCell: {
color: color.light_gray,
fontStyle: 'italic',
},
button: {
width: BUTTON_WIDTH,
fontFamily: '"Gotham 5r", sans-serif',
color: color.charcoal,
padding: 8,
},
tooltipOffset: {
left: -(BUTTON_WIDTH / 2)
},
tooltip: {
width: BUTTON_WIDTH * 2
},
};
6 changes: 6 additions & 0 deletions apps/src/lib/ui/accounts/ManageLinkedAccounts.story.jsx
@@ -0,0 +1,6 @@
import React from 'react';
import ManageLinkedAccounts from './ManageLinkedAccounts';

export default storybook => storybook
.storiesOf('ManageLinkedAccounts', module)
.add('default table', () => (<ManageLinkedAccounts />));
12 changes: 12 additions & 0 deletions apps/src/lib/ui/accounts/ManageLinkedAccountsController.js
@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ManageLinkedAccounts from './ManageLinkedAccounts';

export default class ManageLinkedAccountsController {
constructor(mountPoint) {
ReactDOM.render(
<ManageLinkedAccounts/>,
mountPoint
);
}
}
15 changes: 11 additions & 4 deletions apps/src/sites/studio/pages/devise/registrations/edit.js
Expand Up @@ -2,6 +2,7 @@ import $ from 'jquery';
import ChangeEmailController from '@cdo/apps/lib/ui/accounts/ChangeEmailController';
import AddPasswordController from '@cdo/apps/lib/ui/accounts/AddPasswordController';
import ChangeUserTypeController from '@cdo/apps/lib/ui/accounts/ChangeUserTypeController';
import ManageLinkedAccountsController from '@cdo/apps/lib/ui/accounts/ManageLinkedAccountsController';
import getScriptData from '@cdo/apps/util/getScriptData';

// Values loaded from scriptData are always initial values, not the latest
Expand All @@ -25,10 +26,16 @@ $(document).ready(() => {
userType,
);

new AddPasswordController(
$('#add-password-form'),
document.getElementById('add-password-fields'),
);
const addPasswordMountPoint = document.getElementById('add-password-fields');
if (addPasswordMountPoint) {
new AddPasswordController($('#add-password-form'), addPasswordMountPoint);
}


const manageLinkedAccountsMountPoint = document.getElementById('manage-linked-accounts');
if (manageLinkedAccountsMountPoint) {
new ManageLinkedAccountsController(manageLinkedAccountsMountPoint);
}

initializeCreatePersonalAccountControls();
});
Expand Down
28 changes: 26 additions & 2 deletions apps/test/unit/lib/ui/accounts/AddPasswordFormTest.jsx
Expand Up @@ -2,7 +2,12 @@ import React from 'react';
import {mount} from 'enzyme';
import sinon from 'sinon';
import {expect} from '../../../../util/configuredChai';
import AddPasswordForm, {SAVING_STATE, SUCCESS_STATE, PASSWORDS_MUST_MATCH} from '@cdo/apps/lib/ui/accounts/AddPasswordForm';
import AddPasswordForm, {
SAVING_STATE,
SUCCESS_STATE,
PASSWORD_TOO_SHORT,
PASSWORDS_MUST_MATCH
} from '@cdo/apps/lib/ui/accounts/AddPasswordForm';

describe('AddPasswordForm', () => {
let wrapper, handleSubmit;
Expand All @@ -16,7 +21,7 @@ describe('AddPasswordForm', () => {
);
});

it('enables form submission if passwords are present and match', () => {
it('enables form submission if passwords have minimum length and match', () => {
wrapper.setState({
password: 'mypassword',
passwordConfirmation: 'mypassword'
Expand All @@ -33,6 +38,14 @@ describe('AddPasswordForm', () => {
expect(wrapper.find('button')).to.have.attr('disabled');
});

it('disables form submission if passwords are too short', () => {
wrapper.setState({
password: 'short',
passwordConfirmation: 'short'
});
expect(wrapper.find('button')).to.have.attr('disabled');
});

it('disables form submission if passwords do not match', () => {
wrapper.setState({
password: 'newpassword',
Expand All @@ -41,6 +54,17 @@ describe('AddPasswordForm', () => {
expect(wrapper.find('button')).to.have.attr('disabled');
});

it('renders password length validation errors if passwords are too short', () => {
wrapper.setState({
password: 'short',
passwordConfirmation: 'short'
});
const fieldErrors = wrapper.find('FieldError');
expect(fieldErrors).to.have.length(2);
expect(fieldErrors.at(0)).to.have.text(PASSWORD_TOO_SHORT);
expect(fieldErrors.at(1)).to.have.text(PASSWORD_TOO_SHORT);
});

it('renders a password mismatch validation error if passwords do not match', () => {
wrapper.setState({
password: 'newpassword',
Expand Down

0 comments on commit 0d44816

Please sign in to comment.