diff options
author | Sriram Viswanathan <sriramv@thoughtworks.com> | 2017-03-29 15:35:06 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-29 15:35:06 -0300 |
commit | 75eccfc3ea988a20919c95e892b314ccd816e7c2 (patch) | |
tree | 267a189c298597a47f9e24e09e563d5498ac0278 /web-ui | |
parent | 54ce5397e484fd81ee01d714c87c32ed4718fdb2 (diff) | |
parent | e2bb2aa79d4198f6bfdb59ac5e708bfebf787646 (diff) |
Merge pull request #1031 from pixelated/backup_account_error_feedback
[#931] Backup account error feedback
Diffstat (limited to 'web-ui')
9 files changed, 213 insertions, 27 deletions
diff --git a/web-ui/app/locales/en_US/translation.json b/web-ui/app/locales/en_US/translation.json index 22f533c0..22ac9f70 100644 --- a/web-ui/app/locales/en_US/translation.json +++ b/web-ui/app/locales/en_US/translation.json @@ -126,6 +126,9 @@ "paragraph": "Save this message, it is really important.", "button": "Got it! I'm ready!", "retry-button": "Hey, I didn't received it" + }, + "error": { + "submit-error": "There was an error. Please try again later." } }, "back-to-inbox": "Back to my inbox", diff --git a/web-ui/app/locales/pt_BR/translation.json b/web-ui/app/locales/pt_BR/translation.json index ccc0fe5a..d43487a2 100644 --- a/web-ui/app/locales/pt_BR/translation.json +++ b/web-ui/app/locales/pt_BR/translation.json @@ -103,6 +103,9 @@ "paragraph": "Salve esse e-mail, ele é bem importante.", "button": "Recebi! Pronto!", "retry-button": "Ei, eu não recebi" + }, + "error": { + "submit-error": "Ocorreu um erro. Por favor tente novamente mais tarde." } }, "back-to-inbox": "Voltar", diff --git a/web-ui/src/backup_account/backup_email/backup_email.js b/web-ui/src/backup_account/backup_email/backup_email.js index 09863950..7e6cb66c 100644 --- a/web-ui/src/backup_account/backup_email/backup_email.js +++ b/web-ui/src/backup_account/backup_email/backup_email.js @@ -40,7 +40,7 @@ export class BackupEmail extends React.Component { error: !emptyEmail && !validEmail ? t('backup-account.backup-email.error.invalid-email') : '', submitButtonDisabled: !validEmail || emptyEmail }); - } + }; submitHandler = (event) => { event.preventDefault(); @@ -54,8 +54,14 @@ export class BackupEmail extends React.Component { body: JSON.stringify({ csrftoken: [browser.getCookie('XSRF-TOKEN')] }) - }).then(() => this.props.onSubmit('success')); - } + }).then((response) => { + if (response.ok) { + this.props.onSubmit('success'); + } else { + this.props.onSubmit('error'); + } + }); + }; render() { const t = this.props.t; diff --git a/web-ui/src/backup_account/backup_email/backup_email.spec.js b/web-ui/src/backup_account/backup_email/backup_email.spec.js index 48199738..65fad608 100644 --- a/web-ui/src/backup_account/backup_email/backup_email.spec.js +++ b/web-ui/src/backup_account/backup_email/backup_email.spec.js @@ -87,39 +87,61 @@ describe('BackupEmail', () => { describe('Submit', () => { let preventDefaultSpy; - beforeEach((done) => { - mockOnSubmit = expect.createSpy().andCall(() => done()); + beforeEach(() => { preventDefaultSpy = expect.createSpy(); - expect.spyOn(browser, 'getCookie').andReturn('abc123'); + }); - backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />); + context('on success', () => { + beforeEach((done) => { + mockOnSubmit = expect.createSpy().andCall(() => done()); + expect.spyOn(browser, 'getCookie').andReturn('abc123'); - fetchMock.post('/backup-account', 204); - backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy }); - }); + fetchMock.post('/backup-account', 204); + backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />); + backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy }); + }); - it('posts backup email', () => { - expect(fetchMock.called('/backup-account')).toBe(true, 'Backup account POST was not called'); - }); + it('posts backup email', () => { + expect(fetchMock.called('/backup-account')).toBe(true, 'Backup account POST was not called'); + }); - it('sends csrftoken as content', () => { - expect(fetchMock.lastOptions('/backup-account').body).toContain('"csrftoken":["abc123"]'); - }); + it('sends csrftoken as content', () => { + expect(fetchMock.lastOptions('/backup-account').body).toContain('"csrftoken":["abc123"]'); + }); - it('sends content-type header', () => { - expect(fetchMock.lastOptions('/backup-account').headers['Content-Type']).toEqual('application/json'); - }); + it('sends content-type header', () => { + expect(fetchMock.lastOptions('/backup-account').headers['Content-Type']).toEqual('application/json'); + }); - it('sends same origin headers', () => { - expect(fetchMock.lastOptions('/backup-account').credentials).toEqual('same-origin'); - }); + it('sends same origin headers', () => { + expect(fetchMock.lastOptions('/backup-account').credentials).toEqual('same-origin'); + }); + + it('prevents default call to refresh page', () => { + expect(preventDefaultSpy).toHaveBeenCalled(); + }); - it('prevents default call to refresh page', () => { - expect(preventDefaultSpy).toHaveBeenCalled(); + it('calls onSubmit from props with success', () => { + expect(mockOnSubmit).toHaveBeenCalledWith('success'); + }); }); - it('calls onSubmit from props when success', () => { - expect(mockOnSubmit).toHaveBeenCalledWith('success'); + context('on error', () => { + beforeEach((done) => { + mockOnSubmit = expect.createSpy().andCall(() => done()); + + fetchMock.post('/backup-account', 500); + backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />); + backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy }); + }); + + it('calls onSubmit from props with error', () => { + expect(mockOnSubmit).toHaveBeenCalledWith('error'); + }); }); }); + + afterEach(() => { + fetchMock.restore(); + }); }); diff --git a/web-ui/src/backup_account/page.js b/web-ui/src/backup_account/page.js index 49e4b316..be3bae36 100644 --- a/web-ui/src/backup_account/page.js +++ b/web-ui/src/backup_account/page.js @@ -22,6 +22,7 @@ import Footer from 'src/common/footer/footer'; import Header from 'src/common/header/header'; import BackupEmail from 'src/backup_account/backup_email/backup_email'; import Confirmation from 'src/backup_account/confirmation/confirmation'; +import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification'; import 'font-awesome/scss/font-awesome.scss'; import './page.scss'; @@ -36,13 +37,20 @@ export class Page extends React.Component { saveBackupEmail = (status) => { this.setState({ status }); - } + }; mainContent = () => { if (this.state.status === 'success') return <Confirmation />; return <BackupEmail onSubmit={this.saveBackupEmail} />; }; + showSnackbarOnError = (t) => { + if (this.state.status === 'error') { + return <SnackbarNotification message={t('backup-account.error.submit-error')} isError />; + } + return undefined; // To satisfy eslint error - consistent-return + }; + render() { const t = this.props.t; return ( @@ -52,6 +60,7 @@ export class Page extends React.Component { <section> {this.mainContent()} </section> + {this.showSnackbarOnError(t)} <Footer /> </div> </DocumentTitle> diff --git a/web-ui/src/backup_account/page.spec.js b/web-ui/src/backup_account/page.spec.js index bd7bb884..2933a64e 100644 --- a/web-ui/src/backup_account/page.spec.js +++ b/web-ui/src/backup_account/page.spec.js @@ -4,6 +4,7 @@ import React from 'react'; import { Page } from 'src/backup_account/page'; import BackupEmail from 'src/backup_account/backup_email/backup_email'; import Confirmation from 'src/backup_account/confirmation/confirmation'; +import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification'; describe('BackupAccount', () => { let page; @@ -41,5 +42,29 @@ describe('BackupAccount', () => { pageInstance.saveBackupEmail('success'); expect(page.find(Confirmation).length).toEqual(1); }); + + context('on submit error', () => { + beforeEach(() => { + pageInstance.saveBackupEmail('error'); + }); + + it('returns snackbar component on error', () => { + const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t); + expect(snackbar).toEqual(<SnackbarNotification message='backup-account.error.submit-error' isError />); + }); + + it('returns nothing when there is no error', () => { + pageInstance.saveBackupEmail('success'); + const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t); + expect(snackbar).toEqual(undefined); + }); + + it('renders snackbar notification on error', () => { + const snackbar = page.find(SnackbarNotification); + expect(snackbar).toExist(); + expect(snackbar.props().message).toEqual('backup-account.error.submit-error'); + expect(snackbar.props().isError).toEqual(true); + }); + }); }); }); diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.js b/web-ui/src/common/snackbar_notification/snackbar_notification.js new file mode 100644 index 00000000..8a7e5094 --- /dev/null +++ b/web-ui/src/common/snackbar_notification/snackbar_notification.js @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2017 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +import React from 'react'; +import Snackbar from 'material-ui/Snackbar'; +import { red500, blue500 } from 'material-ui/styles/colors'; + +import './snackbar_notification.scss'; + +const notificationStyle = () => ({ + top: 0, + left: 'auto', + bottom: 'auto', + alignSelf: 'center', + transform: 'translate3d(0, 0px, 0)' +}); + +const contentStyle = { + textAlign: 'center' +}; + +const getStyleByType = (isError) => { + const style = { height: 'auto' }; + style.backgroundColor = isError ? red500 : blue500; + return style; +}; + +const SnackbarNotification = ({ message, isError = false, autoHideDuration = 5000 }) => ( + <Snackbar + id='snackbar' + open + bodyStyle={getStyleByType(isError)} + message={message} + autoHideDuration={autoHideDuration} + contentStyle={contentStyle} + style={notificationStyle()} + /> +); + +SnackbarNotification.propTypes = { + message: React.PropTypes.string.isRequired, + isError: React.PropTypes.bool, + autoHideDuration: React.PropTypes.number +}; + +SnackbarNotification.defaultProps = { + isError: false, + autoHideDuration: 5000 +}; + +export default SnackbarNotification; diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.scss b/web-ui/src/common/snackbar_notification/snackbar_notification.scss new file mode 100644 index 00000000..e37ba4ae --- /dev/null +++ b/web-ui/src/common/snackbar_notification/snackbar_notification.scss @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +//TODO: Refer to Issue - https://github.com/callemall/material-ui/issues/3860 +#snackbar > div { + line-height: 20px !important; + padding: 24px !important; +} diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js b/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js new file mode 100644 index 00000000..24b79e71 --- /dev/null +++ b/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js @@ -0,0 +1,31 @@ +import { shallow } from 'enzyme'; +import expect from 'expect'; +import React from 'react'; +import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification'; +import Snackbar from 'material-ui/Snackbar'; +import { red500 } from 'material-ui/styles/colors'; + +describe('SnackbarNotification', () => { + let snackbarNotification; + + beforeEach(() => { + snackbarNotification = shallow(<SnackbarNotification message={'Error Message'} isError />); + }); + + it('renders snackbar with error message', () => { + expect(snackbarNotification.find(Snackbar).props().message).toEqual('Error Message'); + }); + + it('renders snackbar with open as true', () => { + expect(snackbarNotification.find(Snackbar).props().open).toEqual(true); + }); + + it('renders snackbar with error body style', () => { + expect(snackbarNotification.find(Snackbar).props().bodyStyle) + .toEqual({ height: 'auto', backgroundColor: red500 }); + }); + + it('renders snackbar with default auto-hide duration', () => { + expect(snackbarNotification.find(Snackbar).props().autoHideDuration).toEqual(5000); + }); +}); |