From 3dd27fbb3e1cd09421bbf78cc6f0a8487cf6dd67 Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 1 Aug 2016 11:34:08 -0300 Subject: [PATCH 1/6] Ivan - Update i18n, add recover-password path logic [skip ci] --- client/src/actions/user-actions.js | 5 +- client/src/app/App.js | 7 +- client/src/app/Routes.js | 2 + .../main-home/main-home-page-login-widget.js | 34 +++---- .../src/app/main/main-home/main-home-page.js | 10 +- client/src/app/main/main-layout-header.js | 9 +- .../main-recover-password-page.js | 91 +++++++++++++++++++ .../main-recover-password-page.scss | 19 ++++ client/src/core-components/drop-down.js | 3 +- client/src/data/fixtures/user-fixtures.js | 19 ++++ client/src/data/i18n-data.js | 16 +++- client/src/data/languages/en.js | 12 ++- client/src/data/languages/es.js | 7 -- .../lib-app/validations/email-validator.js | 2 +- .../lib-app/validations/length-validator.js | 16 ++++ .../validations/repeat-password-validator.js | 10 ++ .../validations/validations-factory.js | 10 +- client/src/lib-core/react-dfs.js | 13 ++- client/src/stores/user-store.js | 13 +++ 19 files changed, 245 insertions(+), 53 deletions(-) create mode 100644 client/src/app/main/main-recover-password/main-recover-password-page.js create mode 100644 client/src/app/main/main-recover-password/main-recover-password-page.scss delete mode 100644 client/src/data/languages/es.js create mode 100644 client/src/lib-app/validations/length-validator.js create mode 100644 client/src/lib-app/validations/repeat-password-validator.js diff --git a/client/src/actions/user-actions.js b/client/src/actions/user-actions.js index ef6b8ae9..3fb1714c 100644 --- a/client/src/actions/user-actions.js +++ b/client/src/actions/user-actions.js @@ -1,9 +1,10 @@ import Reflux from 'reflux'; -let UserActions = Reflux.createActions([ +const UserActions = Reflux.createActions([ 'checkLoginStatus', 'login', - 'logout' + 'logout', + 'recoverPassword' ]); export default UserActions; \ No newline at end of file diff --git a/client/src/app/App.js b/client/src/app/App.js index 91b2ce24..2eee2843 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -3,10 +3,11 @@ import Reflux from 'reflux'; import CommonStore from 'stores/common-store'; -let App = React.createClass({ +const App = React.createClass({ contextTypes: { - router: React.PropTypes.object + router: React.PropTypes.object, + location: React.PropTypes.object }, mixins: [Reflux.listenTo(CommonStore, 'onCommonStoreChanged')], @@ -21,7 +22,7 @@ let App = React.createClass({ onCommonStoreChanged(change) { let handle = { - 'i18n': () => {this.forceUpdate()}, + 'i18n': () => {this.context.router.push(this.context.location.pathname)}, 'logged': () => {this.context.router.push('/app/dashboard')}, 'loggedOut': () => {this.context.router.push('/app')} }; diff --git a/client/src/app/Routes.js b/client/src/app/Routes.js index 80a61cb9..71a755fa 100644 --- a/client/src/app/Routes.js +++ b/client/src/app/Routes.js @@ -7,6 +7,7 @@ const DemoPage = require('app/demo/components-demo-page'); const MainLayout = require('app/main/main-layout'); const MainHomePage = require('app/main/main-home/main-home-page'); const MainSignUpPage = require('app/main/main-signup/main-signup-page'); +const MainRecoverPasswordPage = require('app/main/main-recover-password/main-recover-password-page'); const DashboardLayout = require('app/main/dashboard/dashboard-layout'); @@ -25,6 +26,7 @@ export default ( + diff --git a/client/src/app/main/main-home/main-home-page-login-widget.js b/client/src/app/main/main-home/main-home-page-login-widget.js index e6a72c43..37884eb8 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.js +++ b/client/src/app/main/main-home/main-home-page-login-widget.js @@ -1,19 +1,19 @@ -const React = require('react'); -const ReactDOM = require('react-dom'); -const Reflux = require('reflux'); -const _ = require('lodash'); -const classNames = require('classnames'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import Reflux from 'reflux'; +import classNames from 'classnames'; -const UserActions = require('actions/user-actions'); -const UserStore = require('stores/user-store'); -const focus = require('lib-core/focus'); +import UserActions from 'actions/user-actions'; +import UserStore from 'stores/user-store'; +import focus from 'lib-core/focus'; +import i18n from 'lib-app/i18n'; -const Button = require('core-components/button'); -const Form = require('core-components/form'); -const Input = require('core-components/input'); -const Checkbox = require('core-components/checkbox'); -const Widget = require('core-components/widget'); -const WidgetTransition = require('core-components/widget-transition'); +import Button from 'core-components/button'; +import Form from 'core-components/form'; +import Input from 'core-components/input'; +import Checkbox from 'core-components/checkbox'; +import Widget from 'core-components/widget'; +import WidgetTransition from 'core-components/widget-transition'; let MainHomePageLoginWidget = React.createClass({ @@ -49,7 +49,7 @@ let MainHomePageLoginWidget = React.createClass({ ); @@ -57,7 +57,7 @@ let MainHomePageLoginWidget = React.createClass({ renderPasswordRecovery() { return ( - +
@@ -103,7 +103,7 @@ let MainHomePageLoginWidget = React.createClass({ if (event === 'LOGIN_FAIL') { this.setState({ loginFormErrors: { - password: 'Password does not match' + password: i18n('ERROR_PASSWORD') } }, function () { this.refs.loginForm.refs.password.focus() diff --git a/client/src/app/main/main-home/main-home-page.js b/client/src/app/main/main-home/main-home-page.js index eb3218cf..57c4bc7b 100644 --- a/client/src/app/main/main-home/main-home-page.js +++ b/client/src/app/main/main-home/main-home-page.js @@ -1,10 +1,10 @@ -const React = require( 'react'); +import React from 'react'; -const MainHomePageLoginWidget = require('app/main/main-home/main-home-page-login-widget'); -const MainHomePagePortal = require('app/main/main-home/main-home-page-portal'); +import MainHomePageLoginWidget from 'app/main/main-home/main-home-page-login-widget'; +import MainHomePagePortal from 'app/main/main-home/main-home-page-portal'; -const CommonActions = require('actions/common-actions'); -const UserStore = require('stores/user-store'); +import CommonActions from 'actions/common-actions'; +import UserStore from 'stores/user-store'; const MainHomePage = React.createClass({ diff --git a/client/src/app/main/main-layout-header.js b/client/src/app/main/main-layout-header.js index b1c33fdc..d8a52883 100644 --- a/client/src/app/main/main-layout-header.js +++ b/client/src/app/main/main-layout-header.js @@ -7,14 +7,13 @@ import UserStore from 'stores/user-store'; import Button from 'core-components/button'; import DropDown from 'core-components/drop-down'; -import Icon from 'core-components/icon'; -let languageList = ['English', 'Spanish', 'Portuguese', 'German', 'Turkish', 'Indian']; let codeLanguages = { 'English': 'us', 'Spanish': 'es', - 'Portuguese': 'pt', 'German': 'de', + 'French': 'fr', + 'Chinese': 'cn', 'Turkish': 'tr', 'Indian': 'in' }; @@ -52,7 +51,7 @@ let MainLayoutHeader = React.createClass({ }, getLanguageList() { - return languageList.map((language) => { + return Object.keys(codeLanguages).map((language) => { return { content: language, icon: codeLanguages[language] @@ -61,7 +60,7 @@ let MainLayoutHeader = React.createClass({ }, changeLanguage(event) { - let language = languageList[event.index]; + let language = Object.keys(codeLanguages)[event.index]; CommonActions.changeLanguage(codeLanguages[language]); }, diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.js b/client/src/app/main/main-recover-password/main-recover-password-page.js new file mode 100644 index 00000000..33ce5c7c --- /dev/null +++ b/client/src/app/main/main-recover-password/main-recover-password-page.js @@ -0,0 +1,91 @@ +import React from 'react'; +import Reflux from 'reflux'; +import _ from 'lodash'; + +import CommonActions from 'actions/common-actions'; +import UserActions from 'actions/user-actions'; +import UserStore from 'stores/user-store'; +import i18n from 'lib-app/i18n'; + +import Widget from 'core-components/widget'; +import Form from 'core-components/form'; +import Input from 'core-components/input'; +import Button from 'core-components/button'; + +const MainRecoverPasswordPage = React.createClass({ + + mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')], + + propTypes: { + location: React.PropTypes.object, + router: React.PropTypes.object + }, + + componentWillMount() { + if (UserStore.isLoggedIn()) { + CommonActions.logged(); + } + + if (!this.props.location.query.token || !this.props.location.query.email) { + CommonActions.loggedOut(); + } + }, + + getInitialState() { + return { + recoverStatus: 'waiting' + }; + }, + + render() { + return ( +
+ + +
+ + +
+
+ +
+ {this.renderRecoverStatus()} + +
+
+ ); + }, + + renderRecoverStatus() { + switch (this.state.recoverStatus) { + case 'valid': + return
{i18n('VALID_RECOVER')}
; + case 'invalid': + return
{i18n('INVALID_RECOVER')}
; + case 'waiting': + return null; + } + }, + + handleRecoverPasswordSubmit(formState) { + let recoverData = _.clone(formState); + recoverData.token = this.props.location.query.token; + recoverData.email = this.props.location.query.email; + + UserActions.recoverPassword(formState); + }, + + onUserStoreChanged(event) { + if (event === 'VALID_RECOVER') { + this.setState({ + recoverStatus: 'valid' + }); + } else { + this.setState({ + recoverStatus: 'invalid' + }); + } + } +}); + +export default MainRecoverPasswordPage; \ No newline at end of file diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.scss b/client/src/app/main/main-recover-password/main-recover-password-page.scss new file mode 100644 index 00000000..9b720e83 --- /dev/null +++ b/client/src/app/main/main-recover-password/main-recover-password-page.scss @@ -0,0 +1,19 @@ +.recover-password { + &__inputs { + display: inline-block; + margin: 0 auto 20px; + text-align: left; + } + + &__input { + margin: 10px 0 0 0; + } + + &__text_valid { + color: green; + } + + &__text_invalid { + color: red; + } +} \ No newline at end of file diff --git a/client/src/core-components/drop-down.js b/client/src/core-components/drop-down.js index c608b498..be3b1b80 100644 --- a/client/src/core-components/drop-down.js +++ b/client/src/core-components/drop-down.js @@ -63,7 +63,8 @@ const DropDown = React.createClass({ let menuProps = { items: this.props.items, onItemClick: this.handleItemClick, - onMouseDown: this.handleListMouseDown + onMouseDown: this.handleListMouseDown, + selectedIndex: this.state.selectedIndex }; return ( diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js index fe8be0c0..f0f8128e 100644 --- a/client/src/data/fixtures/user-fixtures.js +++ b/client/src/data/fixtures/user-fixtures.js @@ -46,5 +46,24 @@ module.exports = [ } }; } + }, + { + path: 'user/recover-password', + time: 100, + response: function (data) { + + if (data.password.length > 6) { + return { + status: 'success', + data: {} + }; + } else { + return { + status: 'fail', + message: 'Invalid token', + data: {} + }; + } + } } ]; diff --git a/client/src/data/i18n-data.js b/client/src/data/i18n-data.js index eafa26dd..aa15af21 100644 --- a/client/src/data/i18n-data.js +++ b/client/src/data/i18n-data.js @@ -1,9 +1,19 @@ -const englishLanguage = require('data/languages/en'); -const spanishLanguage = require('data/languages/es'); +import englishLanguage from 'data/languages/en'; +import spanishLanguage from 'data/languages/en'; +import germanLanguage from 'data/languages/en'; +import frenchLanguage from 'data/languages/en'; +import chineseLanguage from 'data/languages/en'; +import turkishLanguage from 'data/languages/en'; +import indianLanguage from 'data/languages/en'; const languages = { 'us': englishLanguage, - 'es': spanishLanguage + 'es': spanishLanguage, + 'de': germanLanguage, + 'fr': frenchLanguage, + 'cn': chineseLanguage, + 'tr': turkishLanguage, + 'in': indianLanguage }; const i18nData = function (key, lang) { diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index c7d9b592..f7cb3111 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -2,6 +2,16 @@ export default { 'SUBMIT': 'Submit', 'LOG_IN': 'Log in', 'SIGN_UP': 'Sign up', + 'FORGOT_PASSWORD': 'Forgot your password?', + 'RECOVER_PASSWORD': 'Recover Password', + 'NEW_PASSWORD': 'New password', + 'REPEAT_NEW_PASSWORD': 'Repeat new password', + 'VALID_RECOVER': 'Password recovered successfully', + 'INVALID_RECOVER': 'Invalid recover data', + + //ERRORS 'ERROR_EMPTY': 'Invalid value', - 'ERROR_EMAIL': 'Invalid email' + 'ERROR_PASSWORD': 'Invalid password', + 'ERROR_EMAIL': 'Invalid email', + 'PASSWORD_NOT_MATCH': 'Password does not match' }; \ No newline at end of file diff --git a/client/src/data/languages/es.js b/client/src/data/languages/es.js deleted file mode 100644 index 8f565699..00000000 --- a/client/src/data/languages/es.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - 'SUBMIT': 'Enviar', - 'LOG_IN': 'Ingresar', - 'SIGN_UP': 'Registrarse', - 'ERROR_EMPTY': 'Valor invalido', - 'ERROR_EMAIL': 'Email invalido' -}; \ No newline at end of file diff --git a/client/src/lib-app/validations/email-validator.js b/client/src/lib-app/validations/email-validator.js index e1b7cb52..ef4b7e45 100644 --- a/client/src/lib-app/validations/email-validator.js +++ b/client/src/lib-app/validations/email-validator.js @@ -1,4 +1,4 @@ -const Validator = require('lib-app/validations/validator'); +import Validator from 'lib-app/validations/validator'; class EmailValidator extends Validator { diff --git a/client/src/lib-app/validations/length-validator.js b/client/src/lib-app/validations/length-validator.js new file mode 100644 index 00000000..2d68dae5 --- /dev/null +++ b/client/src/lib-app/validations/length-validator.js @@ -0,0 +1,16 @@ +import Validator from 'lib-app/validations/validator'; + +class LengthValidator extends Validator { + constructor(length, errorKey = 'INVALID_VALUE') { + super(); + + this.minlength = length; + this.errorKey = errorKey; + } + + validate(value, form) { + if (value.length < this.minlength) return this.getError(this.errorKey); + } +} + +export default LengthValidator; \ No newline at end of file diff --git a/client/src/lib-app/validations/repeat-password-validator.js b/client/src/lib-app/validations/repeat-password-validator.js new file mode 100644 index 00000000..ca88f64c --- /dev/null +++ b/client/src/lib-app/validations/repeat-password-validator.js @@ -0,0 +1,10 @@ +import Validator from 'lib-app/validations/validator'; + +class RepeatPasswordValidator extends Validator { + + validate(value, form) { + if (value !== form.password) return this.getError('PASSWORD_NOT_MATCH'); + } +} + +export default RepeatPasswordValidator; \ No newline at end of file diff --git a/client/src/lib-app/validations/validations-factory.js b/client/src/lib-app/validations/validations-factory.js index 77c98652..355077c9 100644 --- a/client/src/lib-app/validations/validations-factory.js +++ b/client/src/lib-app/validations/validations-factory.js @@ -1,9 +1,13 @@ -const Validator = require('lib-app/validations/validator'); -const EmailValidator = require('lib-app/validations/email-validator'); +import Validator from 'lib-app/validations/validator'; +import EmailValidator from 'lib-app/validations/email-validator'; +import RepeatPasswordValidator from 'lib-app/validations/repeat-password-validator'; +import LengthValidator from 'lib-app/validations/length-validator'; let validators = { 'DEFAULT': new Validator(), - 'EMAIL': new EmailValidator() + 'EMAIL': new EmailValidator(), + 'PASSWORD': new LengthValidator(6, 'ERROR_PASSWORD'), + 'REPEAT_PASSWORD': new RepeatPasswordValidator() }; class ValidatorFactory { diff --git a/client/src/lib-core/react-dfs.js b/client/src/lib-core/react-dfs.js index 4cfb6c38..bdd4177b 100644 --- a/client/src/lib-core/react-dfs.js +++ b/client/src/lib-core/react-dfs.js @@ -11,11 +11,14 @@ let reactDFS = function (children, visitFunction) { let element = stack.pop(); let tempChilds = []; - if(element.props && element.props.children) { - React.Children.forEach(element.props.children, child => tempChilds.push(child)); + if (element) { + if (element.props && element.props.children) { + React.Children.forEach(element.props.children, child => tempChilds.push(child)); + } + + visitFunction(element); } - visitFunction(element); stack = stack.concat(tempChilds.reverse()); } }; @@ -26,12 +29,12 @@ let renderChildrenWithProps = function(children, mapFunction) { } return React.Children.map(children, function (child) { - let props = mapFunction(child); - if (typeof child !== 'object' || child === null) { return child; } + let props = mapFunction(child); + if (!_.isEmpty(props)) { return React.cloneElement(child, props, child.props && child.props.children); } else { diff --git a/client/src/stores/user-store.js b/client/src/stores/user-store.js index 213123cc..0f14b49b 100644 --- a/client/src/stores/user-store.js +++ b/client/src/stores/user-store.js @@ -13,6 +13,7 @@ const UserStore = Reflux.createStore({ this.listenTo(UserActions.checkLoginStatus, this.checkLoginStatus); this.listenTo(UserActions.login, this.loginUser); this.listenTo(UserActions.logout, this.logoutUser); + this.listenTo(UserActions.recoverPassword, this.recoverPassword); }, initSession() { @@ -53,6 +54,18 @@ const UserStore = Reflux.createStore({ }); }, + recoverPassword(recoverData) { + return API.call({ + path: 'user/recover-password', + data: recoverData + }).then(() => { + this.trigger('VALID_RECOVER'); + setTimeout(CommonActions.loggedOut, 2000); + }, () => { + this.trigger('INVALID_RECOVER') + }); + }, + isLoggedIn() { return sessionStore.isLoggedIn(); }, From 8ec2fb73e55eb9f4f7877b12cf5cc189c07d0747 Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 1 Aug 2016 12:46:59 -0300 Subject: [PATCH 2/6] Ivan - Create message component and implement in password recover page [skip ci] --- .../main-recover-password-page.js | 5 +- .../main-recover-password-page.scss | 4 + client/src/core-components/icon.js | 4 +- client/src/core-components/message.js | 79 +++++++++++++++++ client/src/core-components/message.scss | 86 +++++++++++++++++++ client/src/stores/user-store.js | 2 +- 6 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 client/src/core-components/message.js create mode 100644 client/src/core-components/message.scss diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.js b/client/src/app/main/main-recover-password/main-recover-password-page.js index 33ce5c7c..afd70229 100644 --- a/client/src/app/main/main-recover-password/main-recover-password-page.js +++ b/client/src/app/main/main-recover-password/main-recover-password-page.js @@ -11,6 +11,7 @@ import Widget from 'core-components/widget'; import Form from 'core-components/form'; import Input from 'core-components/input'; import Button from 'core-components/button'; +import Message from 'core-components/message'; const MainRecoverPasswordPage = React.createClass({ @@ -59,9 +60,9 @@ const MainRecoverPasswordPage = React.createClass({ renderRecoverStatus() { switch (this.state.recoverStatus) { case 'valid': - return
{i18n('VALID_RECOVER')}
; + return {i18n('VALID_RECOVER')}; case 'invalid': - return
{i18n('INVALID_RECOVER')}
; + return {i18n('INVALID_RECOVER')}; case 'waiting': return null; } diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.scss b/client/src/app/main/main-recover-password/main-recover-password-page.scss index 9b720e83..59cae9b5 100644 --- a/client/src/app/main/main-recover-password/main-recover-password-page.scss +++ b/client/src/app/main/main-recover-password/main-recover-password-page.scss @@ -9,6 +9,10 @@ margin: 10px 0 0 0; } + &__submit-button { + margin-bottom: 40px; + } + &__text_valid { color: green; } diff --git a/client/src/core-components/icon.js b/client/src/core-components/icon.js index 8d6770f5..69788693 100644 --- a/client/src/core-components/icon.js +++ b/client/src/core-components/icon.js @@ -5,12 +5,12 @@ const Icon = React.createClass({ propTypes: { name: React.PropTypes.string.isRequired, - size: React.PropTypes.number + size: React.PropTypes.string }, getDefaultProps() { return { - size: 0 + size: 'lg' }; }, diff --git a/client/src/core-components/message.js b/client/src/core-components/message.js new file mode 100644 index 00000000..5c00c5a7 --- /dev/null +++ b/client/src/core-components/message.js @@ -0,0 +1,79 @@ +import React from 'react'; +import classNames from 'classnames'; + +import {Motion, spring} from 'react-motion'; +import Icon from 'core-components/icon'; + +const Message = React.createClass({ + + propTypes: { + title: React.PropTypes.string, + children: React.PropTypes.node, + type: React.PropTypes.oneOf(['success', 'error', 'info']) + }, + + getDefaultProps() { + return { + type: 'info' + }; + }, + + render() { + return ( + + {this.renderMessage} + + ); + }, + + getAnimationProps() { + return { + defaultStyle: { + opacity: spring(0, [100, 30]) + }, + style: { + opacity: spring(1, [100, 30]) + } + }; + }, + + renderMessage(style) { + return ( +
+ +
{this.props.title}
+
{this.props.children}
+
+ ) + }, + + getClass() { + let classes = { + 'message': true, + 'message_success': (this.props.type === 'success'), + 'message_error': (this.props.type === 'error'), + 'message_info': (this.props.type === 'info'), + 'message_with-title': (this.props.title), + + [this.props.className]: (this.props.className) + }; + + return classNames(classes); + }, + + getIconName() { + let iconNames = { + 'success': 'check-circle', + 'error': 'exclamation-circle', + 'info': 'info-circle' + }; + + return iconNames[this.props.type]; + }, + + getIconSize() { + return (this.props.title) ? '2x' : 'lg'; + } +}); + +export default Message; diff --git a/client/src/core-components/message.scss b/client/src/core-components/message.scss new file mode 100644 index 00000000..010f7034 --- /dev/null +++ b/client/src/core-components/message.scss @@ -0,0 +1,86 @@ +@import "../scss/vars"; + +.message { + padding: 10px; + text-align: center; + position: relative; + overflow: hidden; + + &__icon { + position: absolute; + top: 13px; + left: 13px; + } + + &__title { + + } + + &__content { + color: white; + } + + &_success { + background-color: #d8f7b3; + + .message__icon { + color: #189e1e; + } + + .message__title { + color: $primary-blue; + } + + .message__content { + color: $primary-blue; + } + } + + &_error { + background-color: #ffb4b4; + + .message__icon { + color: red; + } + + .message__title { + color: #bb4242; + } + + .message__content { + color: white; + } + } + + &_info { + background-color: #a2e1ff; + + .message__icon { + color: #4c80ff; + } + + .message__title { + color: $primary-blue; + } + + .message__content { + color: $primary-blue; + } + } + + &_with-title { + text-align: left; + + .message__title { + font-size: $font-size--md; + font-weight: bold; + } + + .message__icon { + position: initial; + float: left; + margin-top: 7px; + margin-right: 20px; + } + } +} \ No newline at end of file diff --git a/client/src/stores/user-store.js b/client/src/stores/user-store.js index 0f14b49b..d9e020da 100644 --- a/client/src/stores/user-store.js +++ b/client/src/stores/user-store.js @@ -60,7 +60,7 @@ const UserStore = Reflux.createStore({ data: recoverData }).then(() => { this.trigger('VALID_RECOVER'); - setTimeout(CommonActions.loggedOut, 2000); + //setTimeout(CommonActions.loggedOut, 2000); }, () => { this.trigger('INVALID_RECOVER') }); From ad1e45c301caf6b0d442d61a8315e3d8c9707722 Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 1 Aug 2016 13:18:03 -0300 Subject: [PATCH 3/6] Ivan - Implement message component in login recover widget [skip ci] --- .../main-home/main-home-page-login-widget.js | 53 +++++++++++++------ .../main-home-page-login-widget.scss | 16 ++++-- client/src/core-components/message.js | 1 + client/src/core-components/message.scss | 22 ++++++-- client/src/data/languages/en.js | 1 + 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/client/src/app/main/main-home/main-home-page-login-widget.js b/client/src/app/main/main-home/main-home-page-login-widget.js index 37884eb8..fb2a0eb1 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.js +++ b/client/src/app/main/main-home/main-home-page-login-widget.js @@ -14,6 +14,7 @@ import Input from 'core-components/input'; import Checkbox from 'core-components/checkbox'; import Widget from 'core-components/widget'; import WidgetTransition from 'core-components/widget-transition'; +import Message from 'core-components/message'; let MainHomePageLoginWidget = React.createClass({ @@ -22,13 +23,14 @@ let MainHomePageLoginWidget = React.createClass({ getInitialState() { return { sideToShow: 'front', - loginFormErrors: {} + loginFormErrors: {}, + recoverSent: false }; }, render() { return ( - + {this.renderLogin()} {this.renderPasswordRecovery()} @@ -37,18 +39,18 @@ let MainHomePageLoginWidget = React.createClass({ renderLogin() { return ( - -
-
- - - + + +
+ + +
-
+
- @@ -57,28 +59,45 @@ let MainHomePageLoginWidget = React.createClass({ renderPasswordRecovery() { return ( - -
-
- + + +
+
-
+
- + {this.renderRecoverStatus()} ); }, + renderRecoverStatus() { + let status = null; + + if (this.state.recoverSent) { + status = ( + + {i18n('RECOVER_SENT')} + + ); + } + + return status; + }, + handleLoginFormSubmit(formState) { UserActions.login(formState); }, handleForgotPasswordSubmit() { - + this.setState({ + recoverSent: true + }); }, handleLoginFormErrorsValidation(errors) { diff --git a/client/src/app/main/main-home/main-home-page-login-widget.scss b/client/src/app/main/main-home/main-home-page-login-widget.scss index e62067a2..248ae7f2 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.scss +++ b/client/src/app/main/main-home/main-home-page-login-widget.scss @@ -1,20 +1,28 @@ .login-widget { - &--container { + &__container { height: 361px; } - &--input { + &__input { margin: 10px 0; } - &--inputs { + &__inputs { display: inline-block; margin: 0 auto 20px; text-align: left; } - &--forgot-password { + &__forgot-password { margin-top: 20px; } + + &__message { + margin-top: 18px; + } + + &_password { + width: 324px; + } } \ No newline at end of file diff --git a/client/src/core-components/message.js b/client/src/core-components/message.js index 5c00c5a7..413f65ef 100644 --- a/client/src/core-components/message.js +++ b/client/src/core-components/message.js @@ -54,6 +54,7 @@ const Message = React.createClass({ 'message_error': (this.props.type === 'error'), 'message_info': (this.props.type === 'info'), 'message_with-title': (this.props.title), + 'message_left-aligned': (this.props.leftAligned), [this.props.className]: (this.props.className) }; diff --git a/client/src/core-components/message.scss b/client/src/core-components/message.scss index 010f7034..c8b8c980 100644 --- a/client/src/core-components/message.scss +++ b/client/src/core-components/message.scss @@ -71,16 +71,28 @@ &_with-title { text-align: left; - .message__title { - font-size: $font-size--md; - font-weight: bold; - } - .message__icon { position: initial; float: left; margin-top: 7px; margin-right: 20px; } + + .message__title { + font-size: $font-size--md; + font-weight: bold; + } + + .message__content { + font-size: $font-size--sm; + overflow: auto; + } + } + + &_left-aligned { + .message__content { + text-align: left; + padding-left: 28px; + } } } \ No newline at end of file diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index f7cb3111..ea2f2ce1 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -4,6 +4,7 @@ export default { 'SIGN_UP': 'Sign up', 'FORGOT_PASSWORD': 'Forgot your password?', 'RECOVER_PASSWORD': 'Recover Password', + 'RECOVER_SENT': 'An email with password recover instructions has been sent.', 'NEW_PASSWORD': 'New password', 'REPEAT_NEW_PASSWORD': 'Repeat new password', 'VALID_RECOVER': 'Password recovered successfully', From 96fe51d08ae896855a6793fad2264cbd6e46b42f Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 1 Aug 2016 13:38:15 -0300 Subject: [PATCH 4/6] Ivan - Add send-recover-password api call logic [skip ci] --- client/src/actions/user-actions.js | 1 + .../main-home/main-home-page-login-widget.js | 40 ++++++++++++++----- client/src/data/fixtures/user-fixtures.js | 19 +++++++++ client/src/data/languages/en.js | 4 +- .../lib-app/validations/email-validator.js | 2 +- client/src/stores/user-store.js | 14 ++++++- 6 files changed, 68 insertions(+), 12 deletions(-) diff --git a/client/src/actions/user-actions.js b/client/src/actions/user-actions.js index 3fb1714c..dc9cd0b6 100644 --- a/client/src/actions/user-actions.js +++ b/client/src/actions/user-actions.js @@ -4,6 +4,7 @@ const UserActions = Reflux.createActions([ 'checkLoginStatus', 'login', 'logout', + 'sendRecover', 'recoverPassword' ]); diff --git a/client/src/app/main/main-home/main-home-page-login-widget.js b/client/src/app/main/main-home/main-home-page-login-widget.js index fb2a0eb1..ff8cf267 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.js +++ b/client/src/app/main/main-home/main-home-page-login-widget.js @@ -24,6 +24,7 @@ let MainHomePageLoginWidget = React.createClass({ return { sideToShow: 'front', loginFormErrors: {}, + recoverFormErrors: {}, recoverSent: false }; }, @@ -60,16 +61,16 @@ let MainHomePageLoginWidget = React.createClass({ renderPasswordRecovery() { return ( -
+
- +
- +
{this.renderRecoverStatus()}
@@ -94,10 +95,8 @@ let MainHomePageLoginWidget = React.createClass({ UserActions.login(formState); }, - handleForgotPasswordSubmit() { - this.setState({ - recoverSent: true - }); + handleForgotPasswordSubmit(formState) { + UserActions.sendRecover(formState); }, handleLoginFormErrorsValidation(errors) { @@ -106,6 +105,12 @@ let MainHomePageLoginWidget = React.createClass({ }); }, + handleRecoverFormErrorsValidation(errors) { + this.setState({ + recoverFormErrors: errors + }); + }, + handleForgotPasswordClick() { this.setState({ sideToShow: 'back' @@ -125,9 +130,26 @@ let MainHomePageLoginWidget = React.createClass({ password: i18n('ERROR_PASSWORD') } }, function () { - this.refs.loginForm.refs.password.focus() + this.refs.loginForm.refs.password.focus(); }.bind(this)); } + + if (event === 'SEND_RECOVER_FAIL') { + this.setState({ + recoverFormErrors: { + email: i18n('EMAIL_NOT_EXIST') + } + }, function () { + this.refs.recoverForm.refs.email.focus(); + }.bind(this)); + + } + + if (event === 'SEND_RECOVER_SUCCESS') { + this.setState({ + recoverSent: true + }); + } }, moveFocusToCurrentSide() { diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js index f0f8128e..b17b4842 100644 --- a/client/src/data/fixtures/user-fixtures.js +++ b/client/src/data/fixtures/user-fixtures.js @@ -47,6 +47,25 @@ module.exports = [ }; } }, + { + path: 'user/send-recover-password', + time: 100, + response: function (data) { + + if (data.email.length > 10) { + return { + status: 'success', + data: {} + }; + } else { + return { + status: 'fail', + message: 'Email not exists', + data: {} + }; + } + } + }, { path: 'user/recover-password', time: 100, diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index ea2f2ce1..41d85727 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -4,11 +4,13 @@ export default { 'SIGN_UP': 'Sign up', 'FORGOT_PASSWORD': 'Forgot your password?', 'RECOVER_PASSWORD': 'Recover Password', - 'RECOVER_SENT': 'An email with password recover instructions has been sent.', + 'RECOVER_SENT': 'An email with recover instructions has been sent.', 'NEW_PASSWORD': 'New password', 'REPEAT_NEW_PASSWORD': 'Repeat new password', 'VALID_RECOVER': 'Password recovered successfully', 'INVALID_RECOVER': 'Invalid recover data', + 'BACK_LOGIN_FORM': 'Back to login form', + 'EMAIL_NOT_EXIST': 'Email does not exist', //ERRORS 'ERROR_EMPTY': 'Invalid value', diff --git a/client/src/lib-app/validations/email-validator.js b/client/src/lib-app/validations/email-validator.js index ef4b7e45..9cba77b8 100644 --- a/client/src/lib-app/validations/email-validator.js +++ b/client/src/lib-app/validations/email-validator.js @@ -3,7 +3,7 @@ import Validator from 'lib-app/validations/validator'; class EmailValidator extends Validator { validate(value, form) { - if (!value.length) return this.getError('ERROR_EMPTY'); + if (value.length < 6) return this.getError('ERROR_EMAIL'); if (value.indexOf('@') === -1) return this.getError('ERROR_EMAIL'); } } diff --git a/client/src/stores/user-store.js b/client/src/stores/user-store.js index d9e020da..07ae890a 100644 --- a/client/src/stores/user-store.js +++ b/client/src/stores/user-store.js @@ -14,6 +14,7 @@ const UserStore = Reflux.createStore({ this.listenTo(UserActions.login, this.loginUser); this.listenTo(UserActions.logout, this.logoutUser); this.listenTo(UserActions.recoverPassword, this.recoverPassword); + this.listenTo(UserActions.sendRecover, this.sendRecoverPassword); }, initSession() { @@ -54,13 +55,24 @@ const UserStore = Reflux.createStore({ }); }, + sendRecoverPassword(recoverData) { + return API.call({ + path: 'user/send-recover-password', + data: recoverData + }).then(() => { + this.trigger('SEND_RECOVER_SUCCESS'); + }, () => { + this.trigger('SEND_RECOVER_FAIL') + }); + }, + recoverPassword(recoverData) { return API.call({ path: 'user/recover-password', data: recoverData }).then(() => { this.trigger('VALID_RECOVER'); - //setTimeout(CommonActions.loggedOut, 2000); + setTimeout(CommonActions.loggedOut, 1000); }, () => { this.trigger('INVALID_RECOVER') }); From 8952c1b7e1cbc2f5e7527bd0fff29747925db544 Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 1 Aug 2016 13:48:55 -0300 Subject: [PATCH 5/6] Ivan - Fix mocha unit testing [skip ci] --- client/src/app/__tests__/App-test.js | 7 ++++++- .../__tests__/main-home-page-login-widget-test.js | 2 +- client/src/core-components/message.scss | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/app/__tests__/App-test.js b/client/src/app/__tests__/App-test.js index 6bc02936..fd136dd0 100644 --- a/client/src/app/__tests__/App-test.js +++ b/client/src/app/__tests__/App-test.js @@ -16,6 +16,10 @@ describe('App component', function () { app.context = { router: { push: stub() + }, + + location: { + pathname: 'MOCK_PATH' } }; @@ -23,9 +27,10 @@ describe('App component', function () { }); it('should update with i18n', function () { + app.context.router.push.reset(); app.forceUpdate.reset(); app.onCommonStoreChanged('i18n'); - expect(app.forceUpdate).to.have.been.called; + expect(app.context.router.push).to.have.been.calledWith('MOCK_PATH'); }); it('should redirect when logged in', function () { diff --git a/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js b/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js index e34d5965..9e1a6b1c 100644 --- a/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js +++ b/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js @@ -62,7 +62,7 @@ describe('Login/Recover Widget', function () { it('should add error if login fails', function () { component.refs.loginForm.refs.password.focus.reset(); component.onUserStoreChanged('LOGIN_FAIL'); - expect(loginForm.props.errors).to.deep.equal({password: 'Password does not match'}); + expect(loginForm.props.errors).to.deep.equal({password: 'Invalid password'}); expect(component.refs.loginForm.refs.password.focus).to.have.been.called; }); diff --git a/client/src/core-components/message.scss b/client/src/core-components/message.scss index c8b8c980..aef7e563 100644 --- a/client/src/core-components/message.scss +++ b/client/src/core-components/message.scss @@ -53,7 +53,7 @@ } &_info { - background-color: #a2e1ff; + background-color: #ceefff; .message__icon { color: #4c80ff; From db32a2a6ab7e9fcd4df1ffa4e1fee9f6aef01c33 Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 1 Aug 2016 14:48:08 -0300 Subject: [PATCH 6/6] Ivan - Update user store tests [skip ci] --- client/src/core-components/message.js | 1 + .../src/stores/__tests__/user-store-test.js | 91 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/client/src/core-components/message.js b/client/src/core-components/message.js index 413f65ef..72a39b63 100644 --- a/client/src/core-components/message.js +++ b/client/src/core-components/message.js @@ -9,6 +9,7 @@ const Message = React.createClass({ propTypes: { title: React.PropTypes.string, children: React.PropTypes.node, + leftAligned: React.PropTypes.bool, type: React.PropTypes.oneOf(['success', 'error', 'info']) }, diff --git a/client/src/stores/__tests__/user-store-test.js b/client/src/stores/__tests__/user-store-test.js index f6fb225c..d3af00b2 100644 --- a/client/src/stores/__tests__/user-store-test.js +++ b/client/src/stores/__tests__/user-store-test.js @@ -156,7 +156,7 @@ describe('UserStore', function () { }); describe('and no session is active', function () { - beforeEach(function() { + beforeEach(function () { let mockSuccessData = { status: 'success', data: { @@ -198,5 +198,92 @@ describe('UserStore', function () { }); }); }); - }}) + }}); + + describe('when recovering password', function () { + beforeEach(function () { + let mockSuccessData = { + status: 'success', + data: {} + }; + API.call = stub().returns({ + then: (resolve) => {resolve(mockSuccessData)} + }); + spy(UserStore, 'trigger'); + }); + + afterEach(function () { + UserStore.trigger.restore(); + }); + + it('should send recover password', function () { + UserStore.sendRecoverPassword({ + email: 'SOME_EMAIL' + }); + + expect(API.call).to.have.been.calledWithMatch({ + path: 'user/send-recover-password', + data: { + email: 'SOME_EMAIL' + } + }); + expect(UserStore.trigger).to.have.been.calledWith('SEND_RECOVER_SUCCESS'); + }); + + it('should trigger fail if send recover fails', function () { + API.call = stub().returns({ + then: (resolve, reject) => {reject({ status: 'fail'})} + }); + UserStore.sendRecoverPassword({ + email: 'SOME_EMAIL' + }); + + expect(API.call).to.have.been.calledWithMatch({ + path: 'user/send-recover-password', + data: { + email: 'SOME_EMAIL' + } + }); + expect(UserStore.trigger).to.have.been.calledWith('SEND_RECOVER_FAIL'); + }); + + it('should recover password', function () { + UserStore.recoverPassword({ + email: 'SOME_EMAIL', + token: 'SOME_TOKEN', + password: 'SOME_PASSWORD' + }); + + expect(API.call).to.have.been.calledWithMatch({ + path: 'user/recover-password', + data: { + email: 'SOME_EMAIL', + token: 'SOME_TOKEN', + password: 'SOME_PASSWORD' + } + }); + expect(UserStore.trigger).to.have.been.calledWith('VALID_RECOVER'); + }); + + it('should trigger fail if recover password fails', function () { + API.call = stub().returns({ + then: (resolve, reject) => {reject({ status: 'fail'})} + }); + UserStore.recoverPassword({ + email: 'SOME_EMAIL', + token: 'SOME_TOKEN', + password: 'SOME_PASSWORD' + }); + + expect(API.call).to.have.been.calledWithMatch({ + path: 'user/recover-password', + data: { + email: 'SOME_EMAIL', + token: 'SOME_TOKEN', + password: 'SOME_PASSWORD' + } + }); + expect(UserStore.trigger).to.have.been.calledWith('INVALID_RECOVER'); + }); + }); });