Merged in redux-update (pull request #35)

Redux update
This commit is contained in:
Ivan Diaz 2016-08-15 18:38:33 -03:00
commit c13e9a26c7
69 changed files with 1170 additions and 1177 deletions

3
client/.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"optional": ["es7.classProperties"]
}

View File

@ -38,7 +38,7 @@ function buildScript(file, watch) {
bundler.on('update', rebundle);
}
bundler.transform(babelify);
bundler.transform(babelify, {'optional': ['es7.classProperties']});
bundler.transform(debowerify);
function rebundle() {

View File

@ -17,6 +17,7 @@
},
"devDependencies": {
"babel-core": "^5.8.22",
"babel-plugin-transform-class-properties": "^6.11.5",
"babel-register": "^6.7.2",
"babelify": "^6.1.x",
"browser-sync": "^2.7.13",
@ -63,7 +64,10 @@
"react-dom": "^15.0.1",
"react-google-recaptcha": "^0.5.2",
"react-motion": "^0.3.0",
"react-redux": "^4.4.5",
"react-router": "^2.4.0",
"reflux": "^0.4.1"
"react-router-redux": "^4.0.5",
"redux": "^3.5.2",
"redux-promise-middleware": "^3.3.2"
}
}

View File

@ -1,5 +0,0 @@
export default {
changeLanguage: stub(),
logged: stub(),
loggedOut: stub()
};

View File

@ -0,0 +1,3 @@
export default {
changeLanguage: stub()
};

View File

@ -0,0 +1,5 @@
export default {
login: stub(),
logout: stub(),
initSession: stub()
};

View File

@ -1,7 +0,0 @@
export default {
checkLoginStatus: stub(),
sendRecoverPassword: stub(),
recoverPassword: stub(),
login: stub(),
logout: stub()
};

View File

@ -0,0 +1,64 @@
const sessionStoreMock = require('lib-app/__mocks__/session-store-mock');
const APICallMock = {
call: stub().returns('API_RESULT')
};
const ConfigActions = requireUnit('actions/config-actions', {
'lib-app/api-call': APICallMock,
'lib-app/session-store': sessionStoreMock
});
describe('Config Actions,', function () {
describe('init action', function () {
it('should return INIT_CONFIGS_FULFILLED with configs if it is already retrieved', function () {
sessionStoreMock.areConfigsStored.returns(true);
sessionStoreMock.getConfigs.returns({
config1: 'CONFIG_1',
config2: 'CONFIG_2'
});
expect(ConfigActions.init()).to.deep.equal({
type: 'INIT_CONFIGS_FULFILLED',
payload: {
data: {
config1: 'CONFIG_1',
config2: 'CONFIG_2'
}
}
})
});
it('should return INIT_CONFIGS with API_RESULT if it is not retrieved', function () {
APICallMock.call.reset();
sessionStoreMock.areConfigsStored.returns(false);
sessionStoreMock.getConfigs.returns({
config1: 'CONFIG_1',
config2: 'CONFIG_2'
});
expect(ConfigActions.init()).to.deep.equal({
type: 'INIT_CONFIGS',
payload: 'API_RESULT'
});
expect(APICallMock.call).to.have.been.calledWith({
path: '/system/get-configs',
data: {}
});
});
});
describe('changeLanguage action', function () {
it('should trigger CHANGE_LANGUAGE with new language', function () {
expect(ConfigActions.changeLanguage('es')).to.deep.equal({
type: 'CHANGE_LANGUAGE',
payload: 'es'
});
expect(ConfigActions.changeLanguage('de')).to.deep.equal({
type: 'CHANGE_LANGUAGE',
payload: 'de'
});
});
});
});

View File

@ -0,0 +1,144 @@
const sessionStoreMock = require('lib-app/__mocks__/session-store-mock');
const APICallMock = require('lib-app/__mocks__/api-call-mock');
const storeMock = {
dispatch: stub()
};
const SessionActions = requireUnit('actions/session-actions', {
'lib-app/api-call': APICallMock,
'lib-app/session-store': sessionStoreMock,
'app/store': storeMock
});
describe('Session Actions,', function () {
APICallMock.call.returns('API_RESULT');
describe('login action', function () {
it('should return LOGIN with with API_RESULT promise', function () {
APICallMock.call.reset();
let loginData = {
email: 'SOME_EMAIL',
password: 'SOME_PASSWORD',
remember: false
};
expect(SessionActions.login(loginData)).to.deep.equal({
type: 'LOGIN',
payload: 'API_RESULT'
});
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/login',
data: loginData
});
});
});
describe('autoLogin action', function () {
it('should return LOGIN_AUTO with remember data from sessionStore', function () {
APICallMock.call.reset();
sessionStoreMock.getRememberData.returns({
token: 'SOME_TOKEN',
userId: 'SOME_ID',
expiration: 'SOME_EXPIRATION'
});
expect(SessionActions.autoLogin()).to.deep.equal({
type: 'LOGIN_AUTO',
payload: 'API_RESULT'
});
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/login',
data: {
rememberToken: 'SOME_TOKEN',
userId: 'SOME_ID',
isAutomatic: true
}
});
});
});
describe('logout action', function () {
it('should return LOGOUT and call /user/logout', function () {
APICallMock.call.reset();
expect(SessionActions.logout()).to.deep.equal({
type: 'LOGOUT',
payload: 'API_RESULT'
});
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/logout',
data: {}
});
});
});
describe('initSession action', function () {
beforeEach(function () {
APICallMock.call.returns({
then: function (resolve) {
resolve({
data: {
sessionActive: false
}
});
}
});
APICallMock.call.reset();
storeMock.dispatch.reset();
});
after(function () {
APICallMock.call.returns(new Promise(function (resolve) {
resolve({
data: {
sessionActive: true
}
});
}));
});
it('should return CHECK_SESSION and dispatch SESSION_ACTIVE if session is active', function () {
APICallMock.call.returns({
then: function (resolve) {
resolve({
data: {
sessionActive: true
}
});
}
});
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
expect(storeMock.dispatch).to.have.been.calledWith({type: 'SESSION_CHECKED'});
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/check-session',
data: {}
});
});
it('should return CHECK_SESSION and dispatch LOGOUT_FULFILLED if session is not active and no remember data', function () {
sessionStoreMock.isRememberDataExpired.returns(true);
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
expect(storeMock.dispatch).to.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/check-session',
data: {}
});
});
it('should return CHECK_SESSION and dispatch LOGIN_AUTO if session is not active but remember data exists', function () {
sessionStoreMock.isRememberDataExpired.returns(false);
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
expect(storeMock.dispatch).to.not.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/check-session',
data: {}
});
expect(storeMock.dispatch).to.have.been.calledWith(SessionActions.autoLogin());
});
});
});

View File

@ -1,9 +0,0 @@
import Reflux from 'reflux';
let CommonActions = Reflux.createActions([
'changeLanguage',
'logged',
'loggedOut'
]);
export default CommonActions;

View File

@ -0,0 +1,30 @@
import API from 'lib-app/api-call';
import sessionStore from 'lib-app/session-store';
export default {
init() {
if (sessionStore.areConfigsStored()) {
return {
type: 'INIT_CONFIGS_FULFILLED',
payload: {
data: sessionStore.getConfigs()
}
};
} else {
return {
type: 'INIT_CONFIGS',
payload: API.call({
path: '/system/get-configs',
data: {}
})
};
}
},
changeLanguage(newLanguage) {
return {
type: 'CHANGE_LANGUAGE',
payload: newLanguage
};
}
};

View File

@ -0,0 +1,65 @@
import API from 'lib-app/api-call';
import sessionStore from 'lib-app/session-store';
import store from 'app/store';
export default {
login(loginData) {
return {
type: 'LOGIN',
payload: API.call({
path: '/user/login',
data: loginData
})
};
},
autoLogin() {
const rememberData = sessionStore.getRememberData();
return {
type: 'LOGIN_AUTO',
payload: API.call({
path: '/user/login',
data: {
userId: rememberData.userId,
rememberToken: rememberData.token,
isAutomatic: true
}
})
};
},
logout() {
return {
type: 'LOGOUT',
payload: API.call({
path: '/user/logout',
data: {}
})
};
},
initSession() {
return {
type: 'CHECK_SESSION',
payload: API.call({
path: '/user/check-session',
data: {}
}).then((result) => {
if (!result.data.sessionActive) {
if (sessionStore.isRememberDataExpired()) {
store.dispatch({
type: 'LOGOUT_FULFILLED'
});
} else {
store.dispatch(this.autoLogin());
}
} else {
store.dispatch({
type: 'SESSION_CHECKED'
});
}
})
}
}
};

View File

@ -1,12 +0,0 @@
import Reflux from 'reflux';
const UserActions = Reflux.createActions([
'checkLoginStatus',
'login',
'logout',
'signup',
'sendRecoverPassword',
'recoverPassword'
]);
export default UserActions;

View File

@ -1,16 +1,21 @@
import React from 'react';
import Reflux from 'reflux';
import _ from 'lodash';
import { connect } from 'react-redux'
import { browserHistory } from 'react-router';
import CommonStore from 'stores/common-store';
const App = React.createClass({
contextTypes: {
class App extends React.Component {
static contextTypes = {
router: React.PropTypes.object,
location: React.PropTypes.object
},
};
mixins: [Reflux.listenTo(CommonStore, 'onCommonStoreChanged')],
componentWillMount() {
this.redirectIfPathIsNotValid(this.props);
}
componentWillReceiveProps(nextProps) {
this.redirectIfPathIsNotValid(nextProps);
}
render() {
return (
@ -18,19 +23,33 @@ const App = React.createClass({
{React.cloneElement(this.props.children, {})}
</div>
);
},
}
onCommonStoreChanged(change) {
let handle = {
'i18n': () => {this.context.router.push(this.context.location.pathname)},
'logged': () => {this.context.router.push('/app/dashboard')},
'loggedOut': () => {this.context.router.push('/app')}
redirectIfPathIsNotValid(props) {
const validations = {
languageChanged: props.config.language !== this.props.config.language,
loggedIn: !_.includes(props.location.pathname, '/app/dashboard') && props.session.logged,
loggedOut: _.includes(props.location.pathname, '/app/dashboard') && !props.session.logged
};
if (handle[change]) {
handle[change]();
if (validations.languageChanged) {
browserHistory.push(props.location.pathname);
}
if (validations.loggedOut) {
browserHistory.push('/app');
}
if (validations.loggedIn) {
browserHistory.push('/app/dashboard');
}
}
});
}
export default App;
export default connect((store) => {
return {
config: store.config,
session: store.session,
routing: store.routing
};
})(App);

View File

@ -1,27 +1,32 @@
const React = require('react');
const {Router, Route, IndexRoute, browserHistory} = require('react-router');
import React from 'react';
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
const App = require('app/App');
const DemoPage = require('app/demo/components-demo-page');
import store from 'app/store';
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');
import App from 'app/App';
import DemoPage from 'app/demo/components-demo-page';
const DashboardLayout = require('app/main/dashboard/dashboard-layout');
import MainLayout from 'app/main/main-layout';
import MainHomePage from 'app/main/main-home/main-home-page';
import MainSignUpPage from 'app/main/main-signup/main-signup-page';
import MainRecoverPasswordPage from 'app/main/main-recover-password/main-recover-password-page';
const DashboardListTicketsPage = require('app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page');
const DashboardListArticlesPage = require('app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page');
import DashboardLayout from 'app/main/dashboard/dashboard-layout';
const DashboardCreateTicketPage = require('app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page');
const DashboardEditProfilePage = require('app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page');
import DashboardListTicketsPage from 'app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page';
import DashboardListArticlesPage from 'app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page';
const DashboardArticlePage = require('app/main/dashboard/dashboard-article/dashboard-article-page');
const DashboardTicketPage = require('app/main/dashboard/dashboard-ticket/dashboard-ticket-page');
import DashboardCreateTicketPage from 'app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page';
import DashboardEditProfilePage from 'app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page';
import DashboardArticlePage from 'app/main/dashboard/dashboard-article/dashboard-article-page';
import DashboardTicketPage from 'app/main/dashboard/dashboard-ticket/dashboard-ticket-page';
const history = syncHistoryWithStore(browserHistory, store);
export default (
<Router history={browserHistory}>
<Router history={history}>
<Route component={App} path='/'>
<Route path='/app' component={MainLayout}>
<IndexRoute component={MainHomePage} />

View File

@ -0,0 +1,8 @@
export default {
dispatch: stub(),
getState: stub().returns({
config: {},
session: {},
routing: {}
})
};

View File

@ -1,48 +0,0 @@
const CommonStore = require('stores/__mocks__/common-store-mock');
const App = requireUnit('app/App', {
'store/common-store': CommonStore
});
describe('App component', function () {
describe('when reacting to CommonStore', function () {
let app;
beforeEach(function () {
app = TestUtils.renderIntoDocument(
<App><span>MOCK_CHILD</span></App>
);
app.context = {
router: {
push: stub()
},
location: {
pathname: 'MOCK_PATH'
}
};
spy(app, 'forceUpdate');
});
it('should update with i18n', function () {
app.context.router.push.reset();
app.forceUpdate.reset();
app.onCommonStoreChanged('i18n');
expect(app.context.router.push).to.have.been.calledWith('MOCK_PATH');
});
it('should redirect when logged in', function () {
app.context.router.push.reset();
app.onCommonStoreChanged('logged');
expect(app.context.router.push).to.have.been.calledWith('/app/dashboard');
});
it('should redirect when logged out', function () {
app.context.router.push.reset();
app.onCommonStoreChanged('loggedOut');
expect(app.context.router.push).to.have.been.calledWith('/app');
});
});
});

View File

@ -1,9 +1,11 @@
import React from 'react';
import {render} from 'react-dom'
import Router from 'react-router';
import UserStore from 'stores/user-store';
import { Provider } from 'react-redux';
import SessionActions from 'actions/session-actions';
import ConfigActions from 'actions/config-actions';
import routes from './Routes';
import store from './store';
if ( process.env.NODE_ENV !== 'production' ) {
// Enable React devtools
@ -14,9 +16,18 @@ if (noFixtures === 'disabled') {
require('lib-app/fixtures-loader');
}
let onSessionInit = function () {
render(routes, document.getElementById('app'));
let renderApplication = function () {
render(<Provider store={store}>{routes}</Provider>, document.getElementById('app'));
};
window.store = store;
store.dispatch(ConfigActions.init());
store.dispatch(SessionActions.initSession());
UserStore.initSession().then(onSessionInit, onSessionInit);
let unsubscribe = store.subscribe(() => {
console.log(store.getState());
if (store.getState().session.initDone) {
unsubscribe();
renderApplication();
}
});

View File

@ -1,32 +0,0 @@
const CommonActions = require('actions/__mocks__/common-actions-mock');
const UserStore = require('stores/__mocks__/user-store-mock');
const DashboardLayout = requireUnit('app/main/dashboard/dashboard-layout', {
'actions/common-actions': CommonActions,
'stores/user-store': UserStore,
'app/main/dashboard/dashboard-menu': ReactMock()
});
describe('Dashboard page', function () {
afterEach(function () {
UserStore.isLoggedIn.returns(false);
});
it('should trigger common action if user is not logged', function () {
CommonActions.loggedOut.reset();
UserStore.isLoggedIn.returns(false);
TestUtils.renderIntoDocument(<DashboardLayout />);
expect(CommonActions.loggedOut).to.have.been.called;
});
it('should not trigger common action user if is logged', function () {
CommonActions.loggedOut.reset();
UserStore.isLoggedIn.returns(true);
TestUtils.renderIntoDocument(<DashboardLayout />);
expect(CommonActions.loggedOut).to.not.have.been.called;
});
});

View File

@ -1,6 +1,6 @@
import React from 'react';
const DashboardArticlePage = React.createClass({
class DashboardArticlePage extends React.Component {
render() {
return (
@ -9,6 +9,6 @@ const DashboardArticlePage = React.createClass({
</div>
);
}
});
}
export default DashboardArticlePage;

View File

@ -1,6 +1,6 @@
import React from 'react';
const DashboardCreateTicketPage = React.createClass({
class DashboardCreateTicketPage extends React.Component {
render() {
return (
@ -9,6 +9,6 @@ const DashboardCreateTicketPage = React.createClass({
</div>
);
}
});
}
export default DashboardCreateTicketPage;

View File

@ -1,6 +1,6 @@
import React from 'react';
const DashboardEditProfilePage = React.createClass({
class DashboardEditProfilePage extends React.Component {
render() {
return (
@ -9,6 +9,6 @@ const DashboardEditProfilePage = React.createClass({
</div>
);
}
});
}
export default DashboardEditProfilePage;

View File

@ -1,26 +1,22 @@
import React from 'react';
import UserStore from 'stores/user-store';
import CommonActions from 'actions/common-actions';
import {connect} from 'react-redux';
import DashboardMenu from 'app/main/dashboard/dashboard-menu';
const DashboardLayout = React.createClass({
componentWillMount() {
if (!UserStore.isLoggedIn()) {
CommonActions.loggedOut();
}
},
class DashboardLayout extends React.Component {
render() {
return (UserStore.isLoggedIn()) ? (
return (this.props.session.logged) ? (
<div>
<div><DashboardMenu location={this.props.location} /></div>
<div>{this.props.children}</div>
</div>
) : null;
}
});
}
export default DashboardLayout;
export default connect((store) => {
return {
session: store.session
};
})(DashboardLayout);

View File

@ -1,6 +1,6 @@
import React from 'react';
const DashboardListArticlesPage = React.createClass({
class DashboardListArticlesPage extends React.Component {
render() {
return (
@ -9,6 +9,6 @@ const DashboardListArticlesPage = React.createClass({
</div>
);
}
});
}
export default DashboardListArticlesPage;

View File

@ -1,6 +1,6 @@
import React from 'react';
const DashboardListTicketsPage = React.createClass({
class DashboardListTicketsPage extends React.Component {
render() {
return (
@ -9,6 +9,6 @@ const DashboardListTicketsPage = React.createClass({
</div>
);
}
});
}
export default DashboardListTicketsPage;

View File

@ -10,48 +10,48 @@ let dashboardRoutes = [
{ path: '/app/dashboard/edit-profile', text: 'Edit Profile' }
];
const DashboardMenu = React.createClass({
contextTypes: {
class DashboardMenu extends React.Component {
static contextTypes = {
router: React.PropTypes.object
},
};
propTypes: {
static propTypes = {
location: React.PropTypes.object
},
};
render() {
return (
<Menu {...this.getProps()} />
);
},
}
getProps() {
return {
items: this.getMenuItems(),
selectedIndex: this.getSelectedIndex(),
onItemClick: this.goToPathByIndex
onItemClick: this.goToPathByIndex.bind(this)
};
},
}
getMenuItems: function () {
return dashboardRoutes.map(this.getMenuItem);
},
getMenuItems() {
return dashboardRoutes.map(this.getMenuItem.bind(this));
}
getMenuItem(item) {
return {
content: item.text
};
},
}
getSelectedIndex() {
let pathname = this.props.location.pathname;
return _.findIndex(dashboardRoutes, {path: pathname});
},
}
goToPathByIndex(itemIndex) {
this.context.router.push(dashboardRoutes[itemIndex].path);
}
});
}
export default DashboardMenu;

View File

@ -1,6 +1,6 @@
import React from 'react';
const DashboardTicketPage = React.createClass({
class DashboardTicketPage extends React.Component {
render() {
return (
@ -9,6 +9,6 @@ const DashboardTicketPage = React.createClass({
</div>
);
}
});
}
export default DashboardTicketPage;

View File

@ -1,5 +1,5 @@
const UserActions = require('actions/__mocks__/user-actions-mock');
const UserStore = require('stores/__mocks__/user-store-mock');
const SessionActionsMock = require('actions/__mocks__/session-actions-mock');
const APICallMock = require('lib-app/__mocks__/api-call-mock');
const SubmitButton = ReactMock();
const Button = ReactMock();
@ -11,6 +11,9 @@ const Widget = ReactMock();
const WidgetTransition = ReactMock();
const MainHomePageLoginWidget = requireUnit('app/main/main-home/main-home-page-login-widget', {
'react-redux': ReduxMock,
'actions/session-actions': SessionActionsMock,
'lib-app/api-call': APICallMock,
'core-components/submit-button': SubmitButton,
'core-components/button': Button,
'core-components/input': Input,
@ -18,9 +21,7 @@ const MainHomePageLoginWidget = requireUnit('app/main/main-home/main-home-page-l
'core-components/checkbox': Checkbox,
'core-components/message': Message,
'core-components/widget': Widget,
'core-components/widget-transition': WidgetTransition,
'actions/user-actions': UserActions,
'stores/user-store': UserStore
'core-components/widget-transition': WidgetTransition
});
@ -29,9 +30,12 @@ describe('Login/Recover Widget', function () {
let loginWidget, loginForm, widgetTransition, inputs, checkbox, component,
forgotPasswordButton, submitButton;
beforeEach(function () {
component = TestUtils.renderIntoDocument(
<MainHomePageLoginWidget />
let dispatch = stub();
function renderComponent(props = {session: {pending: false, failed: false}}) {
component = reRenderIntoDocument(
<MainHomePageLoginWidget dispatch={dispatch} {...props}/>
);
widgetTransition = TestUtils.scryRenderedComponentsWithType(component, WidgetTransition)[0];
loginWidget = TestUtils.scryRenderedComponentsWithType(component, Widget)[0];
@ -48,7 +52,9 @@ describe('Login/Recover Widget', function () {
}
}
};
});
}
beforeEach(renderComponent);
it('should control form errors by prop', function () {
expect(loginForm.props.errors).to.deep.equal({});
@ -58,25 +64,38 @@ describe('Login/Recover Widget', function () {
it('should trigger login action when submitted', function () {
let mockSubmitData = {email: 'MOCK_VALUE', password: 'MOCK_VALUE'};
let actionMock = {};
SessionActionsMock.login.returns(actionMock);
dispatch.reset();
UserActions.login.reset();
loginForm.props.onSubmit(mockSubmitData);
expect(UserActions.login).to.have.been.calledWith(mockSubmitData);
expect(SessionActionsMock.login).to.have.been.calledWith(mockSubmitData);
expect(dispatch).to.have.been.calledWith(actionMock);
});
it('should set loading true in the form when submitted', function () {
let mockSubmitData = {email: 'MOCK_VALUE', password: 'MOCK_VALUE'};
loginForm.props.onSubmit(mockSubmitData);
it('should set loading true if session login is pending', function () {
expect(loginForm.props.loading).to.equal(false);
renderComponent({
session: {
pending: true,
failed: false
}
});
expect(loginForm.props.loading).to.equal(true);
});
it('should add error and stop loading if login fails', function () {
component.refs.loginForm.refs.password.focus.reset();
component.onUserStoreChanged('LOGIN_FAIL');
component.setState({
loginFormErrors: {}
});
renderComponent({
session: {
pending: false,
failed: true
}
});
expect(loginForm.props.errors).to.deep.equal({password: 'Invalid password'});
expect(loginForm.props.loading).to.equal(false);
expect(component.refs.loginForm.refs.password.focus).to.have.been.called;
});
it('should show back side if \'Forgot your password?\' link is clicked', function () {
@ -90,9 +109,11 @@ describe('Login/Recover Widget', function () {
let recoverWidget, recoverForm, widgetTransition, emailInput, component,
backToLoginButton, submitButton;
let dispatch = stub();
beforeEach(function () {
component = TestUtils.renderIntoDocument(
<MainHomePageLoginWidget />
<MainHomePageLoginWidget dispatch={dispatch} session={{pending: false, failed: false}} />
);
widgetTransition = TestUtils.scryRenderedComponentsWithType(component, WidgetTransition)[0];
recoverWidget = TestUtils.scryRenderedComponentsWithType(component, Widget)[1];
@ -116,12 +137,15 @@ describe('Login/Recover Widget', function () {
expect(recoverForm.props.errors).to.deep.equal({email: 'MOCK_ERROR'});
});
it('should trigger sendRecoverPassword action when submitted', function () {
it('should call sendRecoverPassword when submitted', function () {
let mockSubmitData = {email: 'MOCK_VALUE'};
APICallMock.call.reset();
UserActions.sendRecoverPassword.reset();
recoverForm.props.onSubmit(mockSubmitData);
expect(UserActions.sendRecoverPassword).to.have.been.calledWith(mockSubmitData);
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/send-recover-password',
data: mockSubmitData
});
});
it('should set loading true in the form when submitted', function () {
@ -133,7 +157,8 @@ describe('Login/Recover Widget', function () {
it('should add error and stop loading when send recover fails', function () {
component.refs.recoverForm.refs.email.focus.reset();
component.onUserStoreChanged('SEND_RECOVER_FAIL');
component.onRecoverPasswordFail();
expect(recoverForm.props.errors).to.deep.equal({email: 'Email does not exist'});
expect(recoverForm.props.loading).to.equal(false);
expect(component.refs.recoverForm.refs.email.focus).to.have.been.called;
@ -143,7 +168,7 @@ describe('Login/Recover Widget', function () {
let message = TestUtils.scryRenderedComponentsWithType(component, Message)[0];
expect(message).to.equal(undefined);
component.onUserStoreChanged('SEND_RECOVER_SUCCESS');
component.onRecoverPasswordSent();
message = TestUtils.scryRenderedComponentsWithType(component, Message)[0];
expect(recoverForm.props.loading).to.equal(false);

View File

@ -1,31 +0,0 @@
const CommonActions = require('actions/__mocks__/common-actions-mock');
const UserStore = require('stores/__mocks__/user-store-mock');
const MainHomePage = requireUnit('app/main/main-home/main-home-page', {
'actions/common-actions': CommonActions,
'stores/user-store': UserStore
});
describe('Main home page', function () {
afterEach(function () {
UserStore.isLoggedIn.returns(false);
});
it('should trigger common action if user is currently logged', function () {
CommonActions.logged.reset();
UserStore.isLoggedIn.returns(true);
TestUtils.renderIntoDocument(<MainHomePage />);
expect(CommonActions.logged).to.have.been.called;
});
it('should not trigger common action user if is not logged', function () {
CommonActions.logged.reset();
UserStore.isLoggedIn.returns(false);
TestUtils.renderIntoDocument(<MainHomePage />);
expect(CommonActions.logged).to.not.have.been.called;
});
});

View File

@ -1,10 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Reflux from 'reflux';
import {connect} from 'react-redux';
import classNames from 'classnames';
import _ from 'lodash';
import UserActions from 'actions/user-actions';
import UserStore from 'stores/user-store';
import SessionActions from 'actions/session-actions';
import API from 'lib-app/api-call';
import focus from 'lib-core/focus';
import i18n from 'lib-app/i18n';
@ -17,12 +18,12 @@ import Widget from 'core-components/widget';
import WidgetTransition from 'core-components/widget-transition';
import Message from 'core-components/message';
let MainHomePageLoginWidget = React.createClass({
mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')],
class MainHomePageLoginWidget extends React.Component {
getInitialState() {
return {
constructor(props) {
super(props);
this.state = {
sideToShow: 'front',
loginFormErrors: {},
recoverFormErrors: {},
@ -30,7 +31,13 @@ let MainHomePageLoginWidget = React.createClass({
loadingLogin: false,
loadingRecover: false
};
},
}
componentDidUpdate(prevProps) {
if (!prevProps.session.failed && this.props.session.failed) {
this.refs.loginForm.refs.password.focus();
}
}
render() {
return (
@ -39,7 +46,7 @@ let MainHomePageLoginWidget = React.createClass({
{this.renderPasswordRecovery()}
</WidgetTransition>
);
},
}
renderLogin() {
return (
@ -54,12 +61,12 @@ let MainHomePageLoginWidget = React.createClass({
<SubmitButton type="primary">LOG IN</SubmitButton>
</div>
</Form>
<Button className="login-widget__forgot-password" type="link" onClick={this.handleForgotPasswordClick} onMouseDown={(event) => {event.preventDefault()}}>
<Button className="login-widget__forgot-password" type="link" onClick={this.onForgotPasswordClick.bind(this)} onMouseDown={(event) => {event.preventDefault()}}>
{i18n('FORGOT_PASSWORD')}
</Button>
</Widget>
);
},
}
renderPasswordRecovery() {
return (
@ -72,13 +79,13 @@ let MainHomePageLoginWidget = React.createClass({
<SubmitButton type="primary">{i18n('RECOVER_PASSWORD')}</SubmitButton>
</div>
</Form>
<Button className="login-widget__forgot-password" type="link" onClick={this.handleBackToLoginClick} onMouseDown={(event) => {event.preventDefault()}}>
<Button className="login-widget__forgot-password" type="link" onClick={this.onBackToLoginClick.bind(this)} onMouseDown={(event) => {event.preventDefault()}}>
{i18n('BACK_LOGIN_FORM')}
</Button>
{this.renderRecoverStatus()}
</Widget>
);
},
}
renderRecoverStatus() {
let status = null;
@ -92,102 +99,98 @@ let MainHomePageLoginWidget = React.createClass({
}
return status;
},
}
getLoginFormProps() {
return {
loading: this.state.loadingLogin,
loading: this.props.session.pending,
className: 'login-widget__form',
ref: 'loginForm',
onSubmit:this.handleLoginFormSubmit,
errors: this.state.loginFormErrors,
onValidateErrors: this.handleLoginFormErrorsValidation
onSubmit: this.onLoginFormSubmit.bind(this),
errors: this.getLoginFormErrors(),
onValidateErrors: this.onLoginFormErrorsValidation.bind(this)
};
},
}
getRecoverFormProps() {
return {
loading: this.state.loadingRecover,
className: 'login-widget__form',
ref: 'recoverForm',
onSubmit:this.handleForgotPasswordSubmit,
onSubmit: this.onForgotPasswordSubmit.bind(this),
errors: this.state.recoverFormErrors,
onValidateErrors: this.handleRecoverFormErrorsValidation
onValidateErrors: this.onRecoverFormErrorsValidation.bind(this)
};
},
}
handleLoginFormSubmit(formState) {
UserActions.login(formState);
getLoginFormErrors() {
let errors = _.extend({}, this.state.loginFormErrors);
if (this.props.session.failed) {
errors.password = i18n('ERROR_PASSWORD');
}
return errors;
}
onLoginFormSubmit(formState) {
this.props.dispatch(SessionActions.login(formState));
}
onForgotPasswordSubmit(formState) {
this.setState({
loadingLogin: true
loadingRecover: true,
recoverSent: false
});
},
handleForgotPasswordSubmit(formState) {
UserActions.sendRecoverPassword(formState);
API.call({
path: '/user/send-recover-password',
data: formState
}).then(this.onRecoverPasswordSent.bind(this)).catch(this.onRecoverPasswordFail.bind(this));
}
this.setState({
loadingRecover: true
});
},
handleLoginFormErrorsValidation(errors) {
onLoginFormErrorsValidation(errors) {
this.setState({
loginFormErrors: errors
});
},
}
handleRecoverFormErrorsValidation(errors) {
onRecoverFormErrorsValidation(errors) {
this.setState({
recoverFormErrors: errors
});
},
}
handleForgotPasswordClick() {
onForgotPasswordClick() {
this.setState({
sideToShow: 'back'
}, this.moveFocusToCurrentSide);
},
}
handleBackToLoginClick() {
onBackToLoginClick() {
this.setState({
sideToShow: 'front',
recoverSent: false
}, this.moveFocusToCurrentSide);
},
onUserStoreChanged(event) {
if (event === 'LOGIN_FAIL') {
this.setState({
loadingLogin: false,
loginFormErrors: {
password: i18n('ERROR_PASSWORD')
}
}, function () {
this.refs.loginForm.refs.password.focus();
}.bind(this));
}
}
if (event === 'SEND_RECOVER_FAIL') {
this.setState({
loadingRecover: false,
recoverFormErrors: {
email: i18n('EMAIL_NOT_EXIST')
}
}, function () {
this.refs.recoverForm.refs.email.focus();
}.bind(this));
onRecoverPasswordSent() {
this.setState({
loadingRecover: false,
recoverSent: true
});
}
}
if (event === 'SEND_RECOVER_SUCCESS') {
this.setState({
loadingRecover: false,
recoverSent: true
});
}
},
onRecoverPasswordFail() {
this.setState({
loadingRecover: false,
recoverFormErrors: {
email: i18n('EMAIL_NOT_EXIST')
}
}, function () {
this.refs.recoverForm.refs.email.focus();
}.bind(this));
}
moveFocusToCurrentSide() {
let currentWidget;
@ -205,6 +208,11 @@ let MainHomePageLoginWidget = React.createClass({
focus.focusFirstInput(currentWidget);
}
}
});
}
export default MainHomePageLoginWidget;
export default connect((store) => {
return {
session: store.session
};
})(MainHomePageLoginWidget);

View File

@ -1,9 +1,9 @@
const React = require('react');
const classNames = require('classnames');
import React from 'react';
import classNames from 'classnames';
const Widget = require('core-components/widget');
import Widget from 'core-components/widget';
const MainHomePagePortal = React.createClass({
class MainHomePagePortal extends React.Component {
render() {
return (
<Widget className={classNames('main-home-page-portal', this.props.className)}>
@ -11,6 +11,6 @@ const MainHomePagePortal = React.createClass({
</Widget>
);
}
});
}
export default MainHomePagePortal;

View File

@ -3,16 +3,7 @@ import React from 'react';
import MainHomePageLoginWidget from 'app/main/main-home/main-home-page-login-widget';
import MainHomePagePortal from 'app/main/main-home/main-home-page-portal';
import CommonActions from 'actions/common-actions';
import UserStore from 'stores/user-store';
const MainHomePage = React.createClass({
componentWillMount() {
if (UserStore.isLoggedIn()) {
CommonActions.logged();
}
},
class MainHomePage extends React.Component {
render() {
return (
@ -22,6 +13,6 @@ const MainHomePage = React.createClass({
</div>
);
}
});
}
export default MainHomePage;

View File

@ -1,6 +1,6 @@
import React from 'react';
let MainLayoutFooter = React.createClass({
class MainLayoutFooter extends React.Component {
render() {
return (
@ -11,6 +11,6 @@ let MainLayoutFooter = React.createClass({
</div>
);
}
});
}
export default MainLayoutFooter;

View File

@ -1,9 +1,9 @@
import React from 'react';
import { connect } from 'react-redux'
import i18n from 'lib-app/i18n';
import CommonActions from 'actions/common-actions';
import UserActions from 'actions/user-actions';
import UserStore from 'stores/user-store';
import SessionActions from 'actions/session-actions';
import ConfigActions from 'actions/config-actions';
import Button from 'core-components/button';
import DropDown from 'core-components/drop-down';
@ -18,24 +18,25 @@ let codeLanguages = {
'Indian': 'in'
};
let MainLayoutHeader = React.createClass({
class MainLayoutHeader extends React.Component {
render() {
return (
<div className="main-layout-header">
{this.renderAccessLinks()}
<DropDown className="main-layout-header--languages" items={this.getLanguageList()} onChange={this.changeLanguage}/>
<DropDown {...this.getLanguageSelectorProps()}/>
</div>
);
},
}
renderAccessLinks() {
let result;
if (UserStore.isLoggedIn()) {
if (this.props.session.logged) {
result = (
<div className="main-layout-header--login-links">
Welcome, pepito
<Button type="clean" onClick={this.logout}>(Close Session)</Button>
Welcome, John
<Button type="clean" onClick={this.logout.bind(this)}>(Close Session)</Button>
</div>
);
} else {
@ -48,7 +49,16 @@ let MainLayoutHeader = React.createClass({
}
return result;
},
}
getLanguageSelectorProps() {
return {
className: 'main-layout-header--languages',
items: this.getLanguageList(),
selectedIndex: Object.values(codeLanguages).indexOf(this.props.config.language),
onChange: this.changeLanguage.bind(this)
};
}
getLanguageList() {
return Object.keys(codeLanguages).map((language) => {
@ -57,17 +67,22 @@ let MainLayoutHeader = React.createClass({
icon: codeLanguages[language]
};
});
},
}
changeLanguage(event) {
let language = Object.keys(codeLanguages)[event.index];
CommonActions.changeLanguage(codeLanguages[language]);
},
this.props.dispatch(ConfigActions.changeLanguage(codeLanguages[language]));
}
logout() {
UserActions.logout();
this.props.dispatch(SessionActions.logout());
}
});
}
export default MainLayoutHeader;
export default connect((store) => {
return {
session: store.session,
config: store.config
};
})(MainLayoutHeader);

View File

@ -3,22 +3,19 @@ import React from 'react';
import MainHeader from 'app/main/main-layout-header';
import MainFooter from 'app/main/main-layout-footer';
let MainLayout = React.createClass({
class MainLayout extends React.Component {
render() {
return (
<div className="main-layout">
<MainHeader />
<div className="main-layout--content">
{this.props.children}
</div>
<MainFooter />
</div>
);
}
});
}
export default MainLayout;

View File

@ -1,6 +1,4 @@
const CommonActions = require('actions/__mocks__/common-actions-mock');
const UserActions = require('actions/__mocks__/user-actions-mock');
const UserStore = require('stores/__mocks__/user-store-mock');
const APICallMock = require('lib-app/__mocks__/api-call-mock');
const SubmitButton = ReactMock();
const Button = ReactMock();
@ -10,15 +8,13 @@ const Message = ReactMock();
const Widget = ReactMock();
const MainRecoverPasswordPage = requireUnit('app/main/main-recover-password/main-recover-password-page', {
'lib-app/api-call': APICallMock,
'core-components/submit-button': SubmitButton,
'core-components/button': Button,
'core-components/input': Input,
'core-components/form': Form,
'core-components/message': Message,
'core-components/widget': Widget,
'actions/common-actions': CommonActions,
'actions/user-actions': UserActions,
'stores/user-store': UserStore
'core-components/widget': Widget
});
describe('Recover Password form', function () {
@ -38,12 +34,16 @@ describe('Recover Password form', function () {
});
it('should trigger recoverPassword action when submitted', function () {
UserActions.sendRecoverPassword.reset();
APICallMock.call.reset();
recoverForm.props.onSubmit({password: 'MOCK_VALUE'});
expect(UserActions.recoverPassword).to.have.been.calledWith({
password: 'MOCK_VALUE',
token: 'SOME_TOKEN',
email: 'SOME_EMAIL'
expect(APICallMock.call).to.have.been.calledWith({
path: '/user/recover-password',
data: {
password: 'MOCK_VALUE',
token: 'SOME_TOKEN',
email: 'SOME_EMAIL'
}
});
});
@ -53,7 +53,7 @@ describe('Recover Password form', function () {
});
it('should show message when recover fails', function () {
component.onUserStoreChanged('INVALID_RECOVER');
component.onPasswordRecoverFail();
expect(recoverForm.props.loading).to.equal(false);
let message = TestUtils.scryRenderedComponentsWithType(component, Message)[0];
@ -63,7 +63,7 @@ describe('Recover Password form', function () {
});
it('should show message when recover success', function () {
component.onUserStoreChanged('VALID_RECOVER');
component.onPasswordRecovered();
expect(recoverForm.props.loading).to.equal(false);
let message = TestUtils.scryRenderedComponentsWithType(component, Message)[0];

View File

@ -1,11 +1,8 @@
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 API from 'lib-app/api-call';
import Widget from 'core-components/widget';
import Form from 'core-components/form';
@ -13,37 +10,27 @@ import Input from 'core-components/input';
import SubmitButton from 'core-components/submit-button';
import Message from 'core-components/message';
const MainRecoverPasswordPage = React.createClass({
class MainRecoverPasswordPage extends React.Component {
mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')],
propTypes: {
static propTypes = {
location: React.PropTypes.object,
router: React.PropTypes.object
},
};
componentWillMount() {
if (UserStore.isLoggedIn()) {
CommonActions.logged();
}
constructor(props) {
super(props);
if (!this.props.location.query.token || !this.props.location.query.email) {
CommonActions.loggedOut();
}
},
getInitialState() {
return {
this.state = {
recoverStatus: 'waiting',
loading: false
};
},
}
}
render() {
return (
<div className="main-recover-password-page">
<Widget title={i18n('RECOVER_PASSWORD')} className="col-md-4 col-md-offset-4">
<Form className="recover-password__form" onSubmit={this.handleRecoverPasswordSubmit} loading={this.state.loading}>
<Form className="recover-password__form" onSubmit={this.onRecoverPasswordSubmit.bind(this)} loading={this.state.loading}>
<div className="recover-password__inputs">
<Input placeholder={i18n('NEW_PASSWORD')} name="password" className="recover-password__input" validation="PASSWORD" password required/>
<Input placeholder={i18n('REPEAT_NEW_PASSWORD')} name="password-repeat" className="recover-password__input" validation="REPEAT_PASSWORD" password required/>
@ -56,7 +43,7 @@ const MainRecoverPasswordPage = React.createClass({
</Widget>
</div>
);
},
}
renderRecoverStatus() {
switch (this.state.recoverStatus) {
@ -67,33 +54,39 @@ const MainRecoverPasswordPage = React.createClass({
case 'waiting':
return null;
}
},
}
handleRecoverPasswordSubmit(formState) {
onRecoverPasswordSubmit(formState) {
let recoverData = _.clone(formState);
recoverData.token = this.props.location.query.token;
recoverData.email = this.props.location.query.email;
UserActions.recoverPassword(recoverData);
this.setState({
loading: true
});
},
onUserStoreChanged(event) {
if (event === 'VALID_RECOVER') {
setTimeout(CommonActions.loggedOut, 2000);
this.setState({
recoverStatus: 'valid',
loading: false
});
} else {
this.setState({
recoverStatus: 'invalid',
loading: false
});
}
}, this.callRecoverPassword.bind(this, recoverData));
}
});
callRecoverPassword(recoverData) {
API.call({
path: '/user/recover-password',
data: recoverData
}).then(this.onPasswordRecovered.bind(this)).catch(this.onPasswordRecoverFail.bind(this));
}
onPasswordRecovered() {
setTimeout(() => {this.props.history.push('/app')}, 2000);
this.setState({
recoverStatus: 'valid',
loading: false
});
}
onPasswordRecoverFail() {
this.setState({
recoverStatus: 'invalid',
loading: false
});
}
}
export default MainRecoverPasswordPage;

View File

@ -1,32 +0,0 @@
const CommonActions = require('actions/__mocks__/common-actions-mock');
const UserStore = require('stores/__mocks__/user-store-mock');
const MainSignupPage = requireUnit('app/main/main-signup/main-signup-page', {
'actions/common-actions': CommonActions,
'stores/user-store': UserStore,
'react-google-recaptcha': ReactMock()
});
describe('Signup page', function () {
afterEach(function () {
UserStore.isLoggedIn.returns(false);
});
it('should trigger common action if user is currently logged', function () {
CommonActions.logged.reset();
UserStore.isLoggedIn.returns(true);
TestUtils.renderIntoDocument(<MainSignupPage />);
expect(CommonActions.logged).to.have.been.called;
});
it('should not trigger common action user if is not logged', function () {
CommonActions.logged.reset();
UserStore.isLoggedIn.returns(false);
TestUtils.renderIntoDocument(<MainSignupPage />);
expect(CommonActions.logged).to.not.have.been.called;
});
});

View File

@ -1,11 +1,8 @@
import React from 'react';
import Reflux from 'reflux';
import ReCAPTCHA from 'react-google-recaptcha';
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 API from 'lib-app/api-call';
import SubmitButton from 'core-components/submit-button';
import Message from 'core-components/message';
@ -14,22 +11,16 @@ import Input from 'core-components/input';
import Widget from 'core-components/widget';
let MainSignUpPageWidget = React.createClass({
mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')],
class MainSignUpPageWidget extends React.Component {
componentDidMount() {
if (UserStore.isLoggedIn()) {
CommonActions.logged();
}
},
constructor(props) {
super(props);
getInitialState() {
return {
this.state = {
loading: false,
email: null
};
},
}
render() {
return (
@ -52,7 +43,7 @@ let MainSignUpPageWidget = React.createClass({
</Widget>
</div>
);
},
}
renderMessage() {
switch (this.state.message) {
@ -63,37 +54,47 @@ let MainSignUpPageWidget = React.createClass({
default:
return null;
}
},
}
getFormProps() {
return {
loading: this.state.loading,
className: 'signup-widget__form',
onSubmit: this.handleLoginFormSubmit
onSubmit: this.onLoginFormSubmit.bind(this)
};
},
}
getInputProps() {
return {
inputType: 'secondary',
className: 'signup-widget__input'
};
},
}
handleLoginFormSubmit(formState) {
onLoginFormSubmit(formState) {
this.setState({
loading: true
});
UserActions.signup(formState);
},
onUserStoreChanged(event) {
API.call({
path: '/user/signup',
data: formState
}).then(this.onSignupSuccess.bind(this)).catch(this.onSignupFail.bind(this));
}
onSignupSuccess() {
this.setState({
loading: false,
message: (event === 'SIGNUP_FAIL') ? 'fail': 'success'
message: 'success'
});
}
});
onSignupFail() {
this.setState({
loading: false,
message: 'fail'
});
}
}
export default MainSignUpPageWidget;

5
client/src/app/store.js Normal file
View File

@ -0,0 +1,5 @@
import { createStore, applyMiddleware } from 'redux';
import promise from 'redux-promise-middleware';
import reducers from 'reducers/_reducers';
export default createStore(reducers, applyMiddleware(promise()));

View File

@ -6,13 +6,13 @@ import classNames from 'classnames';
// CORE LIBS
import callback from 'lib-core/callback';
const Button = React.createClass({
class Button extends React.Component {
contextTypes: {
static contextTypes = {
router: React.PropTypes.object
},
};
propTypes: {
static propTypes = {
children: React.PropTypes.node,
type: React.PropTypes.oneOf([
'primary',
@ -24,13 +24,11 @@ const Button = React.createClass({
params: React.PropTypes.object,
query: React.PropTypes.query
})
},
};
getDefaultProps() {
return {
type: 'primary'
};
},
static defaultProps = {
type: 'primary'
};
render() {
return (
@ -38,19 +36,19 @@ const Button = React.createClass({
{this.props.children}
</button>
);
},
}
getProps() {
let props = _.clone(this.props);
props.onClick = callback(this.handleClick, this.props.onClick);
props.onClick = callback(this.handleClick.bind(this), this.props.onClick);
props.className = this.getClass();
delete props.route;
delete props.type;
return props;
},
}
getClass() {
let classes = {
@ -62,13 +60,13 @@ const Button = React.createClass({
classes[this.props.className] = (this.props.className);
return classNames(classes);
},
}
handleClick() {
if (this.props.route) {
this.context.router.push(this.props.route.to);
}
}
});
}
export default Button;

View File

@ -5,25 +5,25 @@ import _ from 'lodash';
import callback from 'lib-core/callback';
import getIcon from 'lib-core/get-icon';
let CheckBox = React.createClass({
class CheckBox extends React.Component {
propTypes: {
static propTypes = {
alignment: React.PropTypes.string,
label: React.PropTypes.string,
value: React.PropTypes.bool
},
};
getDefaultProps() {
return {
alignment: 'right'
};
},
static defaultProps = {
alignment: 'right'
};
getInitialState() {
return {
constructor(props) {
super(props);
this.state = {
checked: false
};
},
}
render() {
return (
@ -35,7 +35,7 @@ let CheckBox = React.createClass({
<input {...this.getProps()}/>
</label>
);
},
}
getProps() {
let props = _.clone(this.props);
@ -45,7 +45,7 @@ let CheckBox = React.createClass({
props['aria-hidden'] = true;
props.className = 'checkbox--box';
props.checked = this.getValue();
props.onChange = callback(this.handleChange, this.props.onChange);
props.onChange = callback(this.handleChange.bind(this), this.props.onChange);
delete props.alignment;
delete props.error;
@ -53,7 +53,7 @@ let CheckBox = React.createClass({
delete props.value;
return props;
},
}
getClass() {
let classes = {
@ -64,39 +64,39 @@ let CheckBox = React.createClass({
};
return classNames(classes);
},
}
getIconProps() {
return {
'aria-checked': this.getValue(),
className: 'checkbox--icon',
onKeyDown: callback(this.handleIconKeyDown, this.props.onKeyDown),
onKeyDown: callback(this.handleIconKeyDown.bind(this), this.props.onKeyDown),
role: "checkbox",
tabIndex: 0
};
},
}
getValue() {
return (this.props.value === undefined) ? this.state.checked : this.props.value;
},
}
handleChange: function (event) {
handleChange(event) {
this.setState({
checked: event.target.checked
});
},
}
handleIconKeyDown: function (event) {
handleIconKeyDown(event) {
if (event.keyCode == 32) {
event.preventDefault();
callback(this.handleChange, this.props.onChange)({
callback(this.handleChange.bind(this), this.props.onChange)({
target: {
checked: !this.state.checked
}
});
}
}
});
}
export default CheckBox;

View File

@ -1,32 +1,30 @@
const React = require('react');
const classNames = require('classnames');
const _ = require('lodash');
const {Motion, spring} = require('react-motion');
const callback = require('lib-core/callback');
import React from 'react';
import classNames from 'classnames';
import {Motion, spring} from 'react-motion';
const Menu = require('core-components/menu');
const Icon = require('core-components/icon');
import Menu from 'core-components/menu';
import Icon from 'core-components/icon';
const DropDown = React.createClass({
class DropDown extends React.Component {
propTypes: {
static propTypes = {
defaultSelectedIndex: React.PropTypes.number,
selectedIndex: React.PropTypes.number,
items: Menu.propTypes.items
},
};
getDefaultProps() {
return {
defaultSelectedIndex: 0
};
},
static defaultProps = {
defaultSelectedIndex: 2
};
getInitialState() {
return {
constructor(props) {
super(props);
this.state = {
selectedIndex: 0,
opened: false
};
},
}
getAnimationStyles() {
let closedStyle = {
@ -42,7 +40,7 @@ const DropDown = React.createClass({
defaultStyle: closedStyle,
style: (this.state.opened) ? openedStyle : closedStyle
};
},
}
render() {
let animation = this.getAnimationStyles();
@ -52,19 +50,19 @@ const DropDown = React.createClass({
<div className={this.getClass()}>
{this.renderCurrentItem(selectedItem)}
<Motion defaultStyle={animation.defaultStyle} style={animation.style}>
{this.renderList}
{this.renderList.bind(this)}
</Motion>
</div>
);
},
}
renderList({opacity, translateY}) {
let style = { opacity: opacity, transform: `translateY(${translateY}px)`};
let menuProps = {
items: this.props.items,
onItemClick: this.handleItemClick,
onMouseDown: this.handleListMouseDown,
selectedIndex: this.state.selectedIndex
onItemClick: this.handleItemClick.bind(this),
onMouseDown: this.handleListMouseDown.bind(this),
selectedIndex: this.getSelectedIndex()
};
return (
@ -72,7 +70,7 @@ const DropDown = React.createClass({
<Menu {...menuProps} />
</div>
);
},
}
renderCurrentItem(item) {
var iconNode = null;
@ -82,11 +80,11 @@ const DropDown = React.createClass({
}
return (
<div className="drop-down--current-item" onBlur={this.handleBlur} onClick={this.handleClick} tabIndex="0">
<div className="drop-down--current-item" onBlur={this.handleBlur.bind(this)} onClick={this.handleClick.bind(this)} tabIndex="0">
{iconNode}{item.content}
</div>
);
},
}
getClass() {
let classes = {
@ -97,19 +95,19 @@ const DropDown = React.createClass({
};
return classNames(classes);
},
}
handleBlur() {
this.setState({
opened: false
});
},
}
handleClick() {
this.setState({
opened: !this.state.opened
});
},
}
handleItemClick(index) {
this.setState({
@ -122,15 +120,15 @@ const DropDown = React.createClass({
index
});
}
},
}
handleListMouseDown(event) {
event.preventDefault();
},
}
getSelectedIndex() {
return (this.props.selectedIndex !== undefined) ? this.props.selectedIndex : this.state.selectedIndex;
}
});
}
export default DropDown;

View File

@ -1,64 +1,66 @@
const React = require('react');
const _ = require('lodash');
const classNames = require('classnames');
import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
const {reactDFS, renderChildrenWithProps} = require('lib-core/react-dfs');
const ValidationFactory = require('lib-app/validations/validations-factory');
import {reactDFS, renderChildrenWithProps} from 'lib-core/react-dfs';
import ValidationFactory from 'lib-app/validations/validations-factory';
const Input = require('core-components/input');
const Checkbox = require('core-components/checkbox');
import Input from 'core-components/input';
import Checkbox from 'core-components/checkbox';
const Form = React.createClass({
class Form extends React.Component {
propTypes: {
static propTypes = {
loading: React.PropTypes.bool,
errors: React.PropTypes.object,
onValidateErrors: React.PropTypes.func,
onSubmit: React.PropTypes.func
},
};
childContextTypes: {
static childContextTypes = {
loading: React.PropTypes.bool
},
};
constructor(props) {
super(props);
this.state = {
form: {},
validations: {},
errors: {}
};
}
getChildContext() {
return {
loading: this.props.loading
};
},
getInitialState() {
return {
form: {},
validations: {},
errors: {}
};
},
}
componentDidMount() {
this.setState(this.getInitialFormAndValidations());
},
}
render() {
return (
<form {...this.getProps()}>
{renderChildrenWithProps(this.props.children, this.getFieldProps)}
{renderChildrenWithProps(this.props.children, this.getFieldProps.bind(this))}
</form>
);
},
}
getProps() {
let props = _.clone(this.props);
props.className = this.getClass();
props.onSubmit = this.handleSubmit;
props.onSubmit = this.handleSubmit.bind(this);
delete props.errors;
delete props.loading;
delete props.onValidateErrors;
return props;
},
}
getClass() {
let classes = {
@ -68,7 +70,7 @@ const Form = React.createClass({
classes[this.props.className] = (this.props.className);
return classNames(classes);
},
}
getFieldProps({props, type}) {
let additionalProps = {};
@ -86,7 +88,7 @@ const Form = React.createClass({
}
return additionalProps;
},
}
getFieldError(fieldName) {
let error = this.state.errors[fieldName];
@ -95,7 +97,7 @@ const Form = React.createClass({
error = this.props.errors[fieldName]
}
return error;
},
}
getFirstErrorField() {
let fieldName = _.findKey(this.state.errors);
@ -106,7 +108,7 @@ const Form = React.createClass({
}
return fieldNode;
},
}
getAllFieldErrors() {
let form = this.state.form;
@ -118,7 +120,7 @@ const Form = React.createClass({
});
return errors;
},
}
getErrorsWithValidatedField(fieldName, form = this.state.form, errors = this.state.errors) {
let newErrors = _.clone(errors);
@ -128,7 +130,7 @@ const Form = React.createClass({
}
return newErrors;
},
}
getInitialFormAndValidations() {
let form = {};
@ -154,17 +156,17 @@ const Form = React.createClass({
form: form,
validations: validations
}
},
}
handleSubmit(event) {
event.preventDefault();
if (this.hasFormErrors()) {
this.updateErrors(this.getAllFieldErrors(), this.focusFirstErrorField);
this.updateErrors(this.getAllFieldErrors(), this.focusFirstErrorField.bind(this));
} else if (this.props.onSubmit) {
this.props.onSubmit(this.state.form);
}
},
}
handleFieldChange(fieldName, type, event) {
let form = _.clone(this.state.form);
@ -178,19 +180,19 @@ const Form = React.createClass({
this.setState({
form: form
});
},
}
isValidFieldType(child) {
return child.type === Input || child.type === Checkbox;
},
}
hasFormErrors() {
return _.some(this.getAllFieldErrors());
},
}
validateField(fieldName) {
this.updateErrors(this.getErrorsWithValidatedField(fieldName));
},
}
updateErrors(errors, callback) {
this.setState({
@ -200,7 +202,7 @@ const Form = React.createClass({
if (this.props.onValidateErrors) {
this.props.onValidateErrors(errors);
}
},
}
focusFirstErrorField() {
let firstErrorField = this.getFirstErrorField();
@ -209,6 +211,6 @@ const Form = React.createClass({
firstErrorField.focus();
}
}
});
}
export default Form;

View File

@ -1,34 +1,32 @@
const React = require('react');
const classNames = require('classnames');
import React from 'react';
import classNames from 'classnames';
const Icon = React.createClass({
class Icon extends React.Component {
propTypes: {
static propTypes = {
name: React.PropTypes.string.isRequired,
size: React.PropTypes.string
},
};
getDefaultProps() {
return {
size: 'lg'
};
},
static defaultProps = {
size: 'lg'
};
render() {
return (this.props.name.length > 2) ? this.renderFontIcon() : this.renderFlag();
},
}
renderFontIcon() {
return (
<span className={this.getFontIconClass()} aria-hidden="true" />
);
},
}
renderFlag() {
return (
<img className={this.props.className} src={`/images/icons/${this.props.name}.png`} aria-hidden="true" />
);
},
}
getFontIconClass() {
let classes = {
@ -40,6 +38,6 @@ const Icon = React.createClass({
return classNames(classes);
}
});
}
export default Icon;

View File

@ -1,16 +1,16 @@
const React = require('react');
const classNames = require('classnames');
const _ = require('lodash');
import React from 'react';
import classNames from 'classnames';
import _ from 'lodash';
const Icon = require('core-components/icon');
import Icon from 'core-components/icon';
const Input = React.createClass({
class Input extends React.Component {
contextTypes: {
static contextTypes = {
loading: React.PropTypes.bool
},
};
propTypes: {
static propTypes = {
value: React.PropTypes.string,
validation: React.PropTypes.string,
onChange: React.PropTypes.func,
@ -19,13 +19,11 @@ const Input = React.createClass({
required: React.PropTypes.bool,
icon: React.PropTypes.string,
error: React.PropTypes.string
},
};
getDefaultProps() {
return {
static defaultProps = {
inputType: 'primary'
};
},
};
render() {
return (
@ -36,7 +34,7 @@ const Input = React.createClass({
{this.renderError()}
</label>
);
},
}
renderError() {
let error = null;
@ -46,7 +44,7 @@ const Input = React.createClass({
}
return error;
},
}
renderIcon() {
let icon = null;
@ -56,7 +54,7 @@ const Input = React.createClass({
}
return icon;
},
}
getInputProps() {
let props = _.clone(this.props);
@ -73,7 +71,7 @@ const Input = React.createClass({
delete props.password;
return props;
},
}
getClass() {
let classes = {
@ -86,13 +84,13 @@ const Input = React.createClass({
};
return classNames(classes);
},
}
focus() {
if (this.refs.nativeInput) {
this.refs.nativeInput.focus();
}
}
});
}
export default Input;

View File

@ -1,34 +1,32 @@
const React = require('react');
const _ = require('lodash');
const classNames = require('classnames');
import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
const Icon = require('core-components/icon');
import Icon from 'core-components/icon';
const Menu = React.createClass({
class Menu extends React.Component {
propTypes: {
static propTypes = {
type: React.PropTypes.oneOf(['primary', 'secondary']),
items: React.PropTypes.arrayOf(React.PropTypes.shape({
content: React.PropTypes.string.isRequired,
icon: React.PropTypes.string
})).isRequired,
selectedIndex: React.PropTypes.number
},
};
getDefaultProps() {
return {
type: 'primary',
selectedIndex: 0
};
},
static defaultProps = {
type: 'primary',
selectedIndex: 0
};
render() {
return (
<ul {...this.getProps()}>
{this.props.items.map(this.renderListItem)}
{this.props.items.map(this.renderListItem.bind(this))}
</ul>
)
},
}
renderListItem(item, index) {
let iconNode = null;
@ -42,7 +40,7 @@ const Menu = React.createClass({
{iconNode}{item.content}
</li>
);
},
}
getProps() {
var props = _.clone(this.props);
@ -55,7 +53,7 @@ const Menu = React.createClass({
delete props.type;
return props;
},
}
getClass() {
let classes = {
@ -66,7 +64,7 @@ const Menu = React.createClass({
classes[this.props.className] = true;
return classNames(classes);
},
}
getItemProps(index) {
return {
@ -74,7 +72,7 @@ const Menu = React.createClass({
onClick: this.handleItemClick.bind(this, index),
key: index
};
},
}
getItemClass(index) {
let classes = {
@ -83,13 +81,13 @@ const Menu = React.createClass({
};
return classNames(classes);
},
}
handleItemClick(index) {
if (this.props.onItemClick) {
this.props.onItemClick(index);
}
}
});
}
export default Menu;

View File

@ -4,29 +4,27 @@ import classNames from 'classnames';
import {Motion, spring} from 'react-motion';
import Icon from 'core-components/icon';
const Message = React.createClass({
class Message extends React.Component {
propTypes: {
static propTypes = {
title: React.PropTypes.string,
children: React.PropTypes.node,
leftAligned: React.PropTypes.bool,
type: React.PropTypes.oneOf(['success', 'error', 'info'])
},
};
getDefaultProps() {
return {
type: 'info',
leftAligned: false
};
},
static defaultProps = {
type: 'info',
leftAligned: false
};
render() {
return (
<Motion {...this.getAnimationProps()}>
{this.renderMessage}
{this.renderMessage.bind(this)}
</Motion>
);
},
}
getAnimationProps() {
return {
@ -37,7 +35,7 @@ const Message = React.createClass({
opacity: spring(1, [100, 30])
}
};
},
}
renderMessage(style) {
return (
@ -47,7 +45,7 @@ const Message = React.createClass({
<div className="message__content">{this.props.children}</div>
</div>
)
},
}
getClass() {
let classes = {
@ -62,7 +60,7 @@ const Message = React.createClass({
};
return classNames(classes);
},
}
getIconName() {
let iconNames = {
@ -72,11 +70,11 @@ const Message = React.createClass({
};
return iconNames[this.props.type];
},
}
getIconSize() {
return (this.props.title) ? '2x' : 'lg';
}
});
}
export default Message;

View File

@ -6,21 +6,19 @@ import classNames from 'classnames';
// CORE LIBS
import Button from 'core-components/button';
const SubmitButton = React.createClass({
class SubmitButton extends React.Component {
contextTypes: {
static contextTypes = {
loading: React.PropTypes.bool
},
};
propTypes: {
static propTypes = {
children: React.PropTypes.node
},
};
getDefaultProps() {
return {
type: 'primary'
};
},
static defaultProps = {
type: 'primary'
};
render() {
return (
@ -28,20 +26,20 @@ const SubmitButton = React.createClass({
{(this.context.loading) ? this.renderLoading() : this.props.children}
</Button>
);
},
}
renderLoading() {
return (
<div className="submit-button__loader"></div>
);
},
}
getProps() {
return _.extend({}, this.props, {
disabled: this.context.loading,
className: this.getClass()
});
},
}
getClass() {
let classes = {
@ -53,6 +51,6 @@ const SubmitButton = React.createClass({
return classNames(classes);
}
});
}
export default SubmitButton;

View File

@ -3,35 +3,31 @@ import classNames from 'classnames';
import _ from 'lodash';
import {Motion, spring} from 'react-motion';
import Widget from 'core-components/widget';
class WidgetTransition extends React.Component {
let WidgetTransition = React.createClass({
propTypes: {
static propTypes = {
sideToShow: React.PropTypes.string
},
};
getDefaultProps() {
return {
sideToShow: 'front'
};
},
static defaultProps = {
sideToShow: 'front'
};
getDefaultAnimation() {
return {
rotateY: -90
};
},
}
render() {
return (
<Motion defaultStyle={this.getDefaultAnimation()} style={this.getAnimation()}>
{this.renderChildren}
{this.renderChildren.bind(this)}
</Motion>
);
},
}
renderChildren: function (animation) {
renderChildren(animation) {
return (
<div className={this.getClass()}>
{React.Children.map(this.props.children, function (child, index) {
@ -57,7 +53,7 @@ let WidgetTransition = React.createClass({
})}
</div>
)
},
}
getClass() {
let classes = {
@ -66,13 +62,13 @@ let WidgetTransition = React.createClass({
};
return classNames(classes);
},
}
getAnimation() {
return {
rotateY: (this.props.sideToShow === 'front') ? spring(0, [100, 20]) : spring(180, [100, 20])
};
}
});
}
export default WidgetTransition;

View File

@ -1,17 +1,15 @@
import React from 'react';
import classNames from 'classnames';
let Widget = React.createClass({
propTypes: {
class Widget extends React.Component {
static propTypes = {
title: React.PropTypes.string,
children: React.PropTypes.node.isRequired
},
};
getDefaultProps() {
return {
title: ''
};
},
static defaultProps = {
title: ''
};
render() {
return (
@ -20,7 +18,7 @@ let Widget = React.createClass({
{this.props.children}
</div>
);
},
}
renderTitle() {
let titleNode = null;
@ -30,7 +28,7 @@ let Widget = React.createClass({
}
return titleNode;
},
}
getClass() {
let classes = {
@ -41,6 +39,6 @@ let Widget = React.createClass({
return classNames(classes);
}
});
}
export default Widget;

View File

@ -0,0 +1,15 @@
module.exports = [
{
path: '/system/get-configs',
time: 1000,
response: function () {
return {
status: 'success',
data: {
'language': 'us',
'reCaptchaKey': '6LfM5CYTAAAAAGLz6ctpf-hchX2_l0Ge-Bn-n8wS'
}
};
}
}
];

View File

@ -1,18 +1,18 @@
module.exports = [
{
path: 'user/login',
path: '/user/login',
time: 1000,
response: function (data) {
let response;
if (data.password === 'valid' || (data.rememberToken === 'aa41efe0a1b3eeb9bf303e4561ff8392' && data.userId === 12)) {
if (data.password === 'valid' || (data.rememberToken === 'aa41efe0a1b3eeb9bf303e4561ff8392' && data.userId == 12)) {
response = {
status: 'success',
data: {
'userId': 12,
'token': 'cc6b4921e6733d6aafe284ec0d7be57e',
'rememberToken': (data.remember) ? 'aa41efe0a1b3eeb9bf303e4561ff8392' : null,
'rememberExpiration': (data.remember) ? 2018 : 0
'rememberExpiration': (data.remember) ? 20180806 : 0
}
};
} else {
@ -26,7 +26,7 @@ module.exports = [
}
},
{
path: 'user/logout',
path: '/user/logout',
time: 100,
response: function () {
return {
@ -36,19 +36,19 @@ module.exports = [
}
},
{
path: 'user/check-session',
path: '/user/check-session',
time: 100,
response: function () {
return {
status: 'success',
data: {
sessionActive: true
sessionActive: false
}
};
}
},
{
path: 'user/send-recover-password',
path: '/user/send-recover-password',
time: 2000,
response: function (data) {
@ -67,7 +67,7 @@ module.exports = [
}
},
{
path: 'user/recover-password',
path: '/user/recover-password',
time: 1000,
response: function (data) {
@ -86,7 +86,7 @@ module.exports = [
}
},
{
path: 'user/signup',
path: '/user/signup',
time: 1000,
response: function (data) {

View File

@ -6,5 +6,8 @@ export default {
getRememberData: stub(),
isRememberDataExpired: stub().returns(false),
isLoggedIn: stub().returns(false),
setConfigs: stub().returns(false),
getConfigs: stub().returns({}),
areConfigsStored: stub().returns(false),
closeSession: stub()
};

View File

@ -2,7 +2,7 @@ const _ = require('lodash');
const APIUtils = require('lib-core/APIUtils');
const SessionStore = require('lib-app/session-store');
const root = 'http://localhost:3000/api/';
const root = 'http://localhost:3000/api';
function processData (data) {
return _.extend(SessionStore.getSessionData(), data);

View File

@ -17,11 +17,12 @@ let fixtures = (function () {
// FIXTURES
fixtures.add(require('data/fixtures/user-fixtures'));
fixtures.add(require('data/fixtures/system-fixtures'));
_.each(fixtures.getAll(), function (fixture) {
mockjax({
contentType: 'application/json',
url: 'http://localhost:3000/api/' + fixture.path,
url: 'http://localhost:3000/api' + fixture.path,
responseTime: fixture.time || 500,
response: function (settings) {
this.responseText = fixture.response(settings.data);

View File

@ -1,12 +1,12 @@
import MessageFormat from 'messageformat';
import CommonStore from 'stores/common-store';
import store from 'app/store';
import i18nData from 'data/i18n-data';
let mf = new MessageFormat('en');
let i18n = function (key, params = null) {
let i18nKey = i18nData(key, CommonStore.language);
let i18nKey = i18nData(key, store.getState().config.language);
let message = mf.compile(i18nKey);
return message(params);

View File

@ -6,7 +6,7 @@ class SessionStore {
this.storage = LocalStorage;
if (!this.getItem('language')) {
this.setItem('language', 'english');
this.setItem('language', 'us');
}
}
@ -37,6 +37,22 @@ class SessionStore {
this.setItem('rememberData-expiration', expiration);
}
storeConfigs(configs) {
this.setItem('language', configs.language);
this.setItem('reCaptchaKey', configs.reCaptchaKey);
}
getConfigs() {
return {
language: this.getItem('language'),
reCaptchaKey: this.getItem('reCaptchaKey')
};
}
areConfigsStored() {
return !!this.getItem('reCaptchaKey');
}
isRememberDataExpired() {
let rememberData = this.getRememberData();

View File

@ -1,3 +1,5 @@
'use strict';
var jsdom = require('jsdom').jsdom;
global.document = jsdom('<html><body></body></html>');
@ -30,3 +32,6 @@ global.reRenderIntoDocument = (function () {
return ReactDOM.render(jsx, div);
}
})();
global.ReduxMock = {
connect: stub().returns(stub().returnsArg(0))
};

View File

@ -0,0 +1,56 @@
const Reducer = require('reducers/reducer');
class SomeReducer extends Reducer {
getTypeHandlers() {
return {
'ACTION_1': this.handleAction1
};
}
getInitialState() {
return {
prop1: 0,
prop2: '',
prop3: false
};
}
handleAction1(state, payload) {
return {
state: state,
payload: payload,
prop1: 5,
prop2: 'hello',
prop3: true
};
}
}
describe('Reducer class', function () {
let reducer;
before(function () {
reducer = SomeReducer.getInstance();
});
it('should call correct handlers for each type', function () {
let result = reducer(undefined, {});
expect(result).to.deep.equal({
prop1: 0,
prop2: '',
prop3: false
});
result = reducer({prop4: true}, {type: 'ACTION_1', payload: 'PAYLOAD'});
expect(result).to.deep.equal({
state: {
prop4: true
},
payload: 'PAYLOAD',
prop1: 5,
prop2: 'hello',
prop3: true
});
});
});

View File

@ -0,0 +1,11 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import sessionReducer from 'reducers/session-reducer';
import configReducer from 'reducers/config-reducer';
export default combineReducers({
session: sessionReducer,
config: configReducer,
routing: routerReducer
});

View File

@ -0,0 +1,36 @@
import _ from 'lodash';
import Reducer from 'reducers/reducer';
import sessionStore from 'lib-app/session-store';
class ConfigReducer extends Reducer {
getInitialState() {
return {
language: sessionStore.getItem('language')
};
}
getTypeHandlers() {
return {
'CHANGE_LANGUAGE': this.onLanguageChange,
'INIT_CONFIGS_FULFILLED': this.onInitConfigs
};
}
onLanguageChange(state, payload) {
sessionStore.setItem('language', payload);
return _.extend({}, state, {
language: payload
});
}
onInitConfigs(state, payload) {
sessionStore.storeConfigs(payload.data);
return _.extend({}, state, payload.data);
}
}
export default ConfigReducer.getInstance();

View File

@ -0,0 +1,13 @@
class Reducer {
static getInstance() {
let reducer = new this();
return (state = reducer.getInitialState(), action) => {
const actionHandler = reducer.getTypeHandlers()[action.type];
return (actionHandler) ? actionHandler(state, action.payload) : state;
};
}
}
export default Reducer;

View File

@ -0,0 +1,100 @@
import _ from 'lodash';
import Reducer from 'reducers/reducer';
import sessionStore from 'lib-app/session-store';
class SessionReducer extends Reducer {
getInitialState() {
return {
initDone: false,
logged: false,
pending: false,
failed: false
};
}
getTypeHandlers() {
return {
'LOGIN_PENDING': this.onLoginPending,
'LOGIN_FULFILLED': this.onLoginCompleted.bind(this),
'LOGIN_REJECTED': this.onLoginFailed,
'LOGOUT_FULFILLED': this.onLogout,
'CHECK_SESSION_REJECTED': (state) => { return _.extend({}, state, {initDone: true})},
'SESSION_CHECKED': (state) => { return _.extend({}, state, {initDone: true, logged: true})},
'LOGIN_AUTO_FULFILLED': this.onAutoLogin.bind(this),
'LOGIN_AUTO_REJECTED': this.onAutoLoginFail
};
}
onLoginPending(state) {
return _.extend({}, state, {
logged: false,
pending: true,
failed: false
});
}
onLoginCompleted(state, payload) {
this.storeLoginResultData(payload.data);
return _.extend({}, state, {
logged: true,
pending: false,
failed: false
});
}
onLoginFailed(state) {
return _.extend({}, state, {
logged: false,
pending: false,
failed: true
});
}
onLogout(state) {
sessionStore.closeSession();
sessionStore.clearRememberData();
return _.extend({}, state, {
initDone: true,
logged: false,
pending: false,
failed: false
});
}
onAutoLogin(state, payload) {
this.storeLoginResultData(payload.data);
return _.extend({}, state, {
initDone: true,
logged: true,
pending: false,
failed: false
});
}
onAutoLoginFail(state) {
sessionStore.closeSession();
sessionStore.clearRememberData();
return _.extend({}, state, {
initDone: true
});
}
storeLoginResultData(resultData) {
if (resultData.rememberToken) {
sessionStore.storeRememberData({
token: resultData.rememberToken,
userId: resultData.userId,
expiration: resultData.rememberExpiration
});
}
sessionStore.createSession(resultData.userId, resultData.token);
}
}
export default SessionReducer.getInstance();

View File

@ -1,6 +0,0 @@
export default {
changeLanguage: stub(),
logged: stub(),
loggedOut: stub(),
listen: stub()
};

View File

@ -1,6 +0,0 @@
export default {
loginUser: stub(),
logoutUser: stub(),
isLoggedIn: stub().returns(false),
listen: stub()
};

View File

@ -1,289 +0,0 @@
// MOCKS
const CommonActions = require('actions/__mocks__/common-actions-mock');
const SessionStore = require('lib-app/__mocks__/session-store-mock');
const API = require('lib-app/__mocks__/api-call-mock');
const UserActions = {
checkLoginStatus: {listen: stub()},
login: {listen: stub()},
logout: {listen: stub()}
};
const UserStore = requireUnit('stores/user-store', {
'actions/user-actions': UserActions,
'actions/common-actions': CommonActions,
'lib-app/session-store': SessionStore,
'lib-app/api-call': API
});
describe('UserStore', function () {
it ('should inform is the user is logged based on SessionStores\' info', function () {
SessionStore.isLoggedIn.returns(true);
expect(UserStore.isLoggedIn()).to.equal(true);
SessionStore.isLoggedIn.returns(false);
expect(UserStore.isLoggedIn()).to.equal(false);
});
describe('when login user', function () {
it('should call /user/login api path', function () {
let mockLoginData = {email: 'mock', password: 'mock'};
UserStore.loginUser(mockLoginData);
expect(API.call).to.have.been.calledWith({
path: 'user/login',
data: mockLoginData
});
});
it('should create session, trigger success event and inform common action when having a successful login', function () {
let mockLoginData = {email: 'mock', password: 'mock'};
let mockSuccessData = {
status: 'success',
data: {
userId: 12,
token: 'RANDOM_TOKEN'
}
};
spy(UserStore, 'trigger');
CommonActions.logged.reset();
SessionStore.createSession.reset();
API.call.returns({
then: (resolve) => {resolve(mockSuccessData)}
});
UserStore.loginUser(mockLoginData);
expect(SessionStore.storeRememberData).to.have.not.been.called;
expect(SessionStore.createSession).to.have.been.calledWith(12, 'RANDOM_TOKEN');
expect(UserStore.trigger).to.have.been.calledWith('LOGIN_SUCCESS');
expect(CommonActions.logged).to.have.been.called;
UserStore.trigger.restore();
});
it('should trigger fail event if login fails', function () {
let mockLoginData = {email: 'mock', password: 'mock'};
spy(UserStore, 'trigger');
API.call.returns({
then: (resolve, reject) => {reject()}
});
UserStore.loginUser(mockLoginData);
expect(UserStore.trigger).to.have.been.calledWith('LOGIN_FAIL');
UserStore.trigger.restore();
});
it('should store remember data if remember is true', function () {
let mockLoginData = {email: 'mock', password: 'mock', remember: true};
let mockSuccessData = {
status: 'success',
data: {
userId: 12,
token: 'RANDOM_TOKEN',
rememberToken: 'RANDOM_TOKEN_2',
rememberExpiration: 20150822
}
};
spy(UserStore, 'trigger');
CommonActions.logged.reset();
SessionStore.createSession.reset();
API.call.returns({
then: (resolve) => {resolve(mockSuccessData)}
});
UserStore.loginUser(mockLoginData);
expect(SessionStore.storeRememberData).to.have.been.calledWith({
token: 'RANDOM_TOKEN_2',
userId: 12,
expiration: 20150822
});
expect(SessionStore.createSession).to.have.been.calledWith(12, 'RANDOM_TOKEN');
expect(UserStore.trigger).to.have.been.calledWith('LOGIN_SUCCESS');
expect(CommonActions.logged).to.have.been.called;
UserStore.trigger.restore();
});
});
describe('when login out', function () {
it('should call /user/logout api path', function () {
API.call = stub().returns({
then: (resolve) => {resolve()}
});
UserStore.logoutUser();
expect(API.call).to.have.been.calledWith({
path: 'user/logout'
});
});
it('should delete session, trigger LOGOUT event and inform common action of logout', function () {
API.call = stub().returns({
then: (resolve) => {resolve()}
});
spy(UserStore, 'trigger');
UserStore.logoutUser();
expect(SessionStore.closeSession).to.have.been.called;
expect(UserStore.trigger).to.have.been.calledWith('LOGOUT');
expect(CommonActions.loggedOut).to.have.been.called;
UserStore.trigger.restore()
})
});
describe('when calling initSession', function () {{
it('should check if session is active in the API', function () {
let mockSuccessData = {
status: 'success',
data: {
sessionActive: true
}
};
API.call = stub().returns({
then: (resolve) => {resolve(mockSuccessData)}
});
UserStore.initSession();
expect(API.call).to.have.been.calledWith({
path: 'user/check-session',
data: {}
});
});
describe('and no session is active', function () {
beforeEach(function () {
let mockSuccessData = {
status: 'success',
data: {
sessionActive: false
}
};
API.call = stub().returns({
then: (resolve) => {resolve(mockSuccessData)}
});
});
it('should log out and delete remember data if expired', function () {
SessionStore.isRememberDataExpired.returns(true);
SessionStore.clearRememberData.reset();
UserStore.initSession();
expect(SessionStore.clearRememberData).to.have.been.called;
expect(SessionStore.closeSession).to.have.been.called;
expect(CommonActions.loggedOut).to.have.been.called;
});
it('should login with remember data', function () {
SessionStore.isRememberDataExpired.returns(false);
SessionStore.getRememberData.returns({
userId: 'REMEMBER_USER_ID',
token: 'REMEMBER_TOKEN',
expiration: 20160721
});
UserStore.initSession();
expect(API.call).to.have.been.calledWithMatch({
path: 'user/login',
data: {
userId: 'REMEMBER_USER_ID',
rememberToken: 'REMEMBER_TOKEN'
}
});
});
});
}});
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');
});
});
});

View File

@ -1,29 +0,0 @@
import Reflux from 'reflux';
import CommonActions from 'actions/common-actions';
let CommonStore = Reflux.createStore({
init() {
this.language = 'us';
this.listenTo(CommonActions.changeLanguage, this.changeLanguage);
this.listenTo(CommonActions.logged, this.logged);
this.listenTo(CommonActions.loggedOut, this.loggedOut);
},
changeLanguage(lang) {
this.language = lang;
this.trigger('i18n');
},
logged() {
this.trigger('logged');
},
loggedOut() {
this.trigger('loggedOut');
}
});
export default CommonStore;

View File

@ -1,131 +0,0 @@
const Reflux = require('reflux');
const API = require('lib-app/api-call');
const sessionStore = require('lib-app/session-store');
const UserActions = require('actions/user-actions');
const CommonActions = require('actions/common-actions');
const UserStore = Reflux.createStore({
init() {
this.user = null;
this.listenTo(UserActions.checkLoginStatus, this.checkLoginStatus);
this.listenTo(UserActions.login, this.loginUser);
this.listenTo(UserActions.signup, this.signupUser);
this.listenTo(UserActions.logout, this.logoutUser);
this.listenTo(UserActions.recoverPassword, this.recoverPassword);
this.listenTo(UserActions.sendRecoverPassword, this.sendRecoverPassword);
},
initSession() {
return API.call({
path: 'user/check-session',
data: {}
}).then(this.tryLoginIfSessionIsInactive);
},
tryLoginIfSessionIsInactive(result) {
if (!result.data.sessionActive) {
if (sessionStore.isRememberDataExpired()) {
return this.logoutUser();
} else {
return this.loginWithRememberData();
}
}
},
signupUser(signupData) {
return API.call({
path: 'user/signup',
data: signupData
}).then(this.handleSignupSuccess, this.handleSignupFail);
},
loginUser(loginData) {
let onSuccessLogin = (loginData.remember) ? this.handleLoginSuccessWithRemember : this.handleLoginSuccess;
let onFailedLogin = (loginData.isAutomatic) ? null : this.handleLoginFail;
return API.call({
path: 'user/login',
data: loginData
}).then(onSuccessLogin, onFailedLogin);
},
logoutUser() {
return API.call({
path: 'user/logout'
}).then(() => {
sessionStore.closeSession();
sessionStore.clearRememberData();
CommonActions.loggedOut();
this.trigger('LOGOUT');
});
},
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');
}, () => {
this.trigger('INVALID_RECOVER')
});
},
isLoggedIn() {
return sessionStore.isLoggedIn();
},
loginWithRememberData() {
let rememberData = sessionStore.getRememberData();
return this.loginUser({
userId: rememberData.userId,
rememberToken: rememberData.token,
isAutomatic: true
});
},
handleLoginSuccessWithRemember(result) {
sessionStore.storeRememberData({
token: result.data.rememberToken,
userId: result.data.userId,
expiration: result.data.rememberExpiration
});
this.handleLoginSuccess(result)
},
handleLoginSuccess(result) {
sessionStore.createSession(result.data.userId, result.data.token);
CommonActions.logged();
this.trigger('LOGIN_SUCCESS');
},
handleLoginFail() {
this.trigger('LOGIN_FAIL');
},
handleSignupSuccess() {
this.trigger('SIGNUP_SUCCESS');
},
handleSignupFail() {
this.trigger('SIGNUP_FAIL');
}
});
export default UserStore;