Merge remote-tracking branch 'remotes/dartandrevinsky/wip-customer' into wip-customer
This commit is contained in:
@@ -14,7 +14,7 @@ const PORT = process.env.PORT || 3000;
|
||||
const $ = gulpLoadPlugins({camelize: true});
|
||||
|
||||
|
||||
// Main tasks
|
||||
// MyAccounts tasks
|
||||
gulp.task('serve', () => runSequence('serve:clean', 'serve:index', 'serve:start'));
|
||||
gulp.task('dist', () => runSequence('dist:clean', 'dist:build', 'dist:index'));
|
||||
gulp.task('clean', ['dist:clean', 'serve:clean']);
|
||||
@@ -89,13 +89,21 @@ gulp.task('serve:start', ['serve:static'], () => {
|
||||
//}
|
||||
|
||||
proxy: {
|
||||
'/user*' : {
|
||||
target: 'http://localhost:8080'
|
||||
},
|
||||
'/login' : {
|
||||
target: 'http://localhost:8080'
|
||||
},
|
||||
'/customers' : {
|
||||
'/customers*' : {
|
||||
target: 'http://localhost:8080'
|
||||
},
|
||||
'/accounts*' : {
|
||||
target: 'http://localhost:8080'
|
||||
},
|
||||
'/transfers*' : {
|
||||
target: 'http://localhost:8080'
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
.listen(PORT, '0.0.0.0', (err) => {
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
"autoprefixer-loader": "^2.0.0",
|
||||
"babel-core": "6.1.4",
|
||||
"babel-loader": "6.1.0",
|
||||
"babel-plugin-add-module-exports": "^0.1.2",
|
||||
"babel-plugin-transform-runtime": "6.1.4",
|
||||
"babel-polyfill": "^6.1.4",
|
||||
"babel-preset-es2015": "6.1.4",
|
||||
"babel-preset-react": "6.1.4",
|
||||
"babel-preset-stage-0": "6.1.2",
|
||||
"babel-register": "6.1.4",
|
||||
"babel-runtime": "^6.0.14",
|
||||
"css-loader": "^0.14.4",
|
||||
"del": "^1.2.0",
|
||||
"extract-text-webpack-plugin": "^0.8.1",
|
||||
@@ -45,12 +48,14 @@
|
||||
"webpack-dev-server": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "6.1.4",
|
||||
"babel-runtime": "6.0.14",
|
||||
"classnames": "^2.2.3",
|
||||
"history": "1.17.0",
|
||||
"immutable": "^3.7.6",
|
||||
"invariant": "^2.1.1",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"js-cookie": "^2.1.0",
|
||||
"object-pick": "^0.1.1",
|
||||
"querystring": "^0.2.0",
|
||||
"react": "^0.14.7",
|
||||
"react-bootstrap": "^0.28.3",
|
||||
"react-dom": "^0.14.0",
|
||||
@@ -61,10 +66,11 @@
|
||||
"react-router-bootstrap": "^0.20.1",
|
||||
"react-router-redux": "^3.0.0",
|
||||
"react-select": "^0.9.1",
|
||||
"react-timeago": "^2.2.1",
|
||||
"redux": "^3.0.2",
|
||||
"redux-auth": "0.0.2",
|
||||
"redux-batched-subscribe": "^0.1.4",
|
||||
"redux-multi": "^0.1.9",
|
||||
"redux-logger": "^2.6.0",
|
||||
"redux-multi": "^0.1.91",
|
||||
"redux-router": "^1.0.0-beta7",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"uniloc": "^0.2.0"
|
||||
|
||||
@@ -3,44 +3,46 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Provider} from "react-redux";
|
||||
import { createStore, compose, applyMiddleware, combineReducers} from "redux";
|
||||
import { Provider, connect} from "react-redux";
|
||||
|
||||
import thunk from "redux-thunk";
|
||||
import createLogger from 'redux-logger';
|
||||
|
||||
import { Route, IndexRoute, Link, IndexLink } from "react-router";
|
||||
import { ReduxRouter} from "redux-router";
|
||||
|
||||
//import { Router, IndexRoute, Route, browserHistory } from 'react-router';
|
||||
//import { syncHistory, routeReducer } from 'react-router-redux';
|
||||
|
||||
import { Route, IndexRoute, Link, IndexLink} from "react-router";
|
||||
import { configure as reduxAuthConfigure, authStateReducer} from "redux-auth";
|
||||
import { AuthGlobals } from "redux-auth/bootstrap-theme";
|
||||
//import { configure as reduxAuthConfigure, authStateReducer } from "redux-auth";
|
||||
//import { authStateReducer } from "redux-auth";
|
||||
//import authStateReducer from './reducers/auth';
|
||||
//import appStateReducer from './reducers/data';
|
||||
import mainReducer from './reducers';
|
||||
|
||||
import { configure as reduxAuthConfigure } from './actions/configure';
|
||||
//import { AuthGlobals } from "redux-auth/bootstrap-theme";
|
||||
|
||||
import { createStore, compose, applyMiddleware} from "redux";
|
||||
import { createHistory, createHashHistory, createMemoryHistory } from "history";
|
||||
import { routerStateReducer, reduxReactRouter as clientRouter} from "redux-router";
|
||||
import { pushState, routerStateReducer, reduxReactRouter as clientRouter} from "redux-router";
|
||||
import { reduxReactRouter as serverRouter } from "redux-router/server";
|
||||
import { combineReducers} from "redux";
|
||||
|
||||
import thunk from "redux-thunk";
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { pushState } from 'redux-router';
|
||||
import { requireAuthentication } from './components/AuthComponent';
|
||||
|
||||
//import demoButtons from "./reducers/request-test-buttons";
|
||||
//import demoUi from "./reducers/demo-ui";
|
||||
import Container from "./components/partials/Container";
|
||||
import Main from "./views/Main";
|
||||
import MyAccounts from "./views/MyAccounts";
|
||||
import Account from "./views/Account";
|
||||
import SignIn from "./views/SignIn";
|
||||
import SignUp from "./views/SignUp";
|
||||
//import GlobalComponents from "./views/partials/GlobalComponents";
|
||||
|
||||
|
||||
// TODO: !!!!
|
||||
// <GlobalComponents />
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Container>
|
||||
<AuthGlobals />
|
||||
{this.props.children}
|
||||
</Container>
|
||||
);
|
||||
@@ -50,96 +52,29 @@ class App extends React.Component {
|
||||
export function initialize({cookies, isServer, currentLocation, userAgent} = {}) {
|
||||
|
||||
const reducer = combineReducers({
|
||||
auth: authStateReducer,
|
||||
app: mainReducer,
|
||||
router: routerStateReducer
|
||||
//demoButtons,
|
||||
//demoUi
|
||||
});
|
||||
|
||||
//let store;
|
||||
|
||||
//// access control method, used above in the "account" route
|
||||
//const requireAuth = (nextState, transition, cb) => {
|
||||
// // the setTimeout is necessary because of this bug:
|
||||
// // https://github.com/rackt/redux-router/pull/62
|
||||
// // this will result in a bunch of warnings, but it doesn't seem to be a serious problem
|
||||
// setTimeout(() => {
|
||||
// if (!store.getState().auth.getIn(["user", "isSignedIn"])) {
|
||||
// transition(null, "/login");
|
||||
// }
|
||||
// cb();
|
||||
// }, 0);
|
||||
//};
|
||||
|
||||
const requireAuthentication = (Component) => {
|
||||
class AuthenticatedComponent extends React.Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.checkAuth();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.checkAuth();
|
||||
}
|
||||
|
||||
checkAuth() {
|
||||
debugger;
|
||||
if (!this.props.isAuthenticated) {
|
||||
let redirectAfterLogin = this.props.location.pathname;
|
||||
this.props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
debugger;
|
||||
return (
|
||||
<div>
|
||||
{this.props.isAuthenticated === true
|
||||
? <Component {...this.props}/>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
token: state.auth.token,
|
||||
userName: state.auth.userName,
|
||||
isAuthenticated: state.auth.isAuthenticated
|
||||
});
|
||||
|
||||
return connect(mapStateToProps)(AuthenticatedComponent);
|
||||
};
|
||||
|
||||
// define app routes
|
||||
// <Route path="account" component={Account} onEnter={requireAuth} />
|
||||
|
||||
const routes = (
|
||||
<Route path="/" component={App}>
|
||||
<IndexRoute component={Main} />
|
||||
<IndexRoute component={requireAuthentication(MyAccounts)} />
|
||||
<Route path="signin" component={SignIn} />
|
||||
<Route path="register" component={SignUp} />
|
||||
<Route path="account" component={requireAuthentication(Account)} />
|
||||
<Route path="account/:accountId" component={requireAuthentication(Account)} />
|
||||
</Route>
|
||||
);
|
||||
|
||||
// these methods will differ from server to client
|
||||
var reduxReactRouter = clientRouter;
|
||||
var createHistoryMethod = createHashHistory;
|
||||
|
||||
if (isServer) {
|
||||
reduxReactRouter = serverRouter;
|
||||
createHistoryMethod = createMemoryHistory;
|
||||
}
|
||||
const reduxReactRouter = isServer ? serverRouter : clientRouter;
|
||||
const createHistoryMethod = isServer ? createMemoryHistory : createHashHistory;
|
||||
|
||||
// create the redux store
|
||||
const store = compose(
|
||||
applyMiddleware(thunk),
|
||||
applyMiddleware(thunk, createLogger()),
|
||||
reduxReactRouter({
|
||||
createHistory: createHistoryMethod,
|
||||
routes
|
||||
routes,
|
||||
createHistory: createHistoryMethod
|
||||
})
|
||||
)(createStore)(reducer);
|
||||
|
||||
@@ -150,35 +85,38 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
|
||||
return store.dispatch(reduxAuthConfigure([
|
||||
{
|
||||
default: {
|
||||
//apiUrl: __API_URL__
|
||||
apiUrl: '/',
|
||||
emailSignInPath: 'login',
|
||||
emailRegistrationPath: 'customers'
|
||||
//apiUrl: '/',
|
||||
emailSignInPath: '/login',
|
||||
customersPath: '/customers',
|
||||
currentUserPath: '/user',
|
||||
accountsPath: '/accounts',
|
||||
transfersPath: '/transfers'
|
||||
}
|
||||
}
|
||||
//, {
|
||||
// evilUser: {
|
||||
// //apiUrl: __API_URL__,
|
||||
// apiUrl: '/api',
|
||||
// signOutPath: "/mangs/sign_out",
|
||||
// emailSignInPath: "/mangs/sign_in",
|
||||
// emailRegistrationPath: "/mangs",
|
||||
// accountUpdatePath: "/mangs",
|
||||
// accountDeletePath: "/mangs",
|
||||
// passwordResetPath: "/mangs/password",
|
||||
// passwordUpdatePath: "/mangs/password",
|
||||
// tokenValidationPath: "/mangs/validate_token",
|
||||
// authProviderPaths: {
|
||||
// github: "/mangs/github",
|
||||
// facebook: "/mangs/facebook",
|
||||
// google: "/mangs/google_oauth2"
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
], {
|
||||
cookies,
|
||||
isServer,
|
||||
currentLocation
|
||||
currentLocation,
|
||||
storage: 'localStorage',
|
||||
tokenFormat: {
|
||||
"access-token": "{{ access-token }}"
|
||||
},
|
||||
handleLoginResponse: function(resp) {
|
||||
debugger;
|
||||
|
||||
return resp.data;
|
||||
},
|
||||
|
||||
handleAccountUpdateResponse: function(resp) {
|
||||
debugger;
|
||||
|
||||
return resp.data;
|
||||
},
|
||||
|
||||
handleTokenValidationResponse: function(resp) {
|
||||
debugger;
|
||||
return resp.data;
|
||||
}
|
||||
})).then(({ redirectPath, blank } = {}) => {
|
||||
// hack for material-ui server-side rendering.
|
||||
// see https://github.com/callemall/material-ui/pull/2007
|
||||
@@ -192,7 +130,7 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
|
||||
return <noscript />;
|
||||
}
|
||||
|
||||
console.log(`redirect path: ${redirectPath}`)
|
||||
console.log(`redirect path: ${redirectPath}`);
|
||||
|
||||
return ({
|
||||
blank,
|
||||
|
||||
27
js-frontend/src/actions/auth.js
Normal file
27
js-frontend/src/actions/auth.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
|
||||
export function configureStart({...props} = {}) {
|
||||
return {
|
||||
...props,
|
||||
type: T.AUTH.CONFIGURE_START
|
||||
};
|
||||
}
|
||||
export function configureComplete({config, ...props} = {}) {
|
||||
return {
|
||||
...props,
|
||||
type: T.AUTH.CONFIGURE_COMPLETE,
|
||||
config
|
||||
};
|
||||
}
|
||||
export function configureError({errors, ...props} = {}) {
|
||||
return {
|
||||
...props,
|
||||
type: T.AUTH.CONFIGURE_ERROR,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
86
js-frontend/src/actions/authenticate.js
Normal file
86
js-frontend/src/actions/authenticate.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import T from '../constants/ACTION_TYPES';
|
||||
|
||||
import {
|
||||
getCurrentSettings,
|
||||
setCurrentSettings,
|
||||
getInitialEndpointKey,
|
||||
setDefaultEndpointKey,
|
||||
setCurrentEndpoint,
|
||||
setCurrentEndpointKey,
|
||||
retrieveData,
|
||||
persistData,
|
||||
destroySession,
|
||||
persistUserData,
|
||||
retrieveUserData,
|
||||
retrieveHeaders
|
||||
} from "../utils/sessionStorage";
|
||||
|
||||
import {
|
||||
apiGetCurrentUser
|
||||
} from '../utils/api';
|
||||
|
||||
import {entityReceived } from './entities';
|
||||
|
||||
export function authenticateStart() {
|
||||
return { type: T.AUTH.AUTHENTICATE_START };
|
||||
}
|
||||
export function authenticateComplete(user) {
|
||||
return { type: T.AUTH.AUTHENTICATE_COMPLETE, user };
|
||||
}
|
||||
export function authenticateError(errors) {
|
||||
return { type: T.AUTH.AUTHENTICATE_ERROR, errors };
|
||||
}
|
||||
|
||||
|
||||
export function authenticate(forceReread) {
|
||||
return dispatch => {
|
||||
|
||||
dispatch(authenticateStart());
|
||||
|
||||
const savedUserPromise = new Promise((rs, rj) => {
|
||||
|
||||
const currentHeaders = retrieveHeaders();
|
||||
const accessToken = currentHeaders["access-token"];
|
||||
|
||||
if (!accessToken) {
|
||||
return rj({ reason: 'no token'});
|
||||
}
|
||||
|
||||
const savedUser = retrieveUserData();
|
||||
|
||||
if (savedUser && !forceReread) {
|
||||
return rs(savedUser);
|
||||
}
|
||||
|
||||
return apiGetCurrentUser().then((userData) => {
|
||||
persistUserData(userData);
|
||||
dispatch(entityReceived(userData.id, userData));
|
||||
rs(userData);
|
||||
}, (err) => {
|
||||
debugger;
|
||||
rj(err);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
return savedUserPromise
|
||||
.then(user => {
|
||||
|
||||
dispatch(authenticateComplete(user));
|
||||
|
||||
return user;
|
||||
})
|
||||
.catch(({reason} = {}) => {
|
||||
|
||||
dispatch(authenticateError([reason]));
|
||||
|
||||
return Promise.resolve({reason});
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
52
js-frontend/src/actions/configure.js
Normal file
52
js-frontend/src/actions/configure.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import * as C from "../utils/constants";
|
||||
import {
|
||||
authenticate,
|
||||
authenticateStart,
|
||||
authenticateComplete,
|
||||
authenticateError
|
||||
} from "./authenticate";
|
||||
|
||||
import {
|
||||
retrieveData,
|
||||
} from "../utils/sessionStorage";
|
||||
|
||||
|
||||
import {applyConfig} from "../utils/clientSettings";
|
||||
|
||||
//import {
|
||||
// showFirstTimeLoginSuccessModal,
|
||||
// showFirstTimeLoginErrorModal,
|
||||
// showPasswordResetSuccessModal,
|
||||
// showPasswordResetErrorModal
|
||||
//} from "./ui";
|
||||
|
||||
import getRedirectInfo from "../utils/parseUrl";
|
||||
import { pushState } from "redux-router";
|
||||
import root from '../utils/root';
|
||||
|
||||
export const SET_ENDPOINT_KEYS = "SET_ENDPOINT_KEYS";
|
||||
export const STORE_CURRENT_ENDPOINT_KEY = "STORE_CURRENT_ENDPOINT_KEY";
|
||||
|
||||
export function setEndpointKeys(endpoints, currentEndpointKey, defaultEndpointKey) {
|
||||
return { type: SET_ENDPOINT_KEYS, endpoints, currentEndpointKey, defaultEndpointKey };
|
||||
}
|
||||
|
||||
export function storeCurrentEndpointKey(currentEndpointKey) {
|
||||
return { type: STORE_CURRENT_ENDPOINT_KEY, currentEndpointKey };
|
||||
}
|
||||
|
||||
export function configure(endpoint={}, settings={}) {
|
||||
|
||||
return dispatch => {
|
||||
|
||||
|
||||
return applyConfig({ dispatch, endpoint, settings })
|
||||
.then(() => {
|
||||
return dispatch(authenticate());
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import T from '../constants/ACTION_TYPES'
|
||||
|
||||
|
||||
export function updateQuery(query) {
|
||||
return {
|
||||
type: T.DOCUMENT_LIST_VIEW.SET_QUERY,
|
||||
query,
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import uuid from '../utils/uuid'
|
||||
import documentValidator from '../validators/documentValidator'
|
||||
import T from '../constants/ACTION_TYPES'
|
||||
import * as navigation from './navigation'
|
||||
|
||||
|
||||
export function updateChanges(id, data) {
|
||||
return [
|
||||
{
|
||||
type: T.DOCUMENT_VIEW.UPDATE_DATA,
|
||||
id,
|
||||
data,
|
||||
},
|
||||
{
|
||||
type: T.DOCUMENT_VIEW.REMOVE_STALE_ERRORS,
|
||||
id,
|
||||
errors: documentValidator(data),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function clearChanges(id) {
|
||||
return {
|
||||
type: T.DOCUMENT_VIEW.CLEAR,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelChanges(id) {
|
||||
return [
|
||||
clearChanges(id),
|
||||
navigation.start('documentList'),
|
||||
]
|
||||
}
|
||||
|
||||
export function submitChanges(id) {
|
||||
return (dispatch, getState) => {
|
||||
const { view } = getState()
|
||||
const data = view.document.unsavedChanges[id]
|
||||
const errors = documentValidator(data)
|
||||
|
||||
if (errors) {
|
||||
dispatch({
|
||||
type: T.DOCUMENT_VIEW.SET_ERRORS,
|
||||
id,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
else {
|
||||
const newId = id == 'new' ? uuid() : id
|
||||
dispatch(navigation.start('documentEdit', {id: newId}))
|
||||
dispatch(clearChanges(id))
|
||||
dispatch({
|
||||
type: T.DOCUMENT_DATA.UPDATE,
|
||||
id: newId,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
237
js-frontend/src/actions/entities.js
Normal file
237
js-frontend/src/actions/entities.js
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Created by andrew on 27/02/16.
|
||||
*/
|
||||
import T from '../constants/ACTION_TYPES';
|
||||
import { makeActionCreator } from '../utils/actions';
|
||||
import * as api from '../utils/api';
|
||||
import { authenticate } from './authenticate';
|
||||
|
||||
export const entityRequested = makeActionCreator(T.ENTITIES.REQUESTED, 'id');
|
||||
export const entityReceived = makeActionCreator(T.ENTITIES.RECEIVED, 'id', 'entity');
|
||||
|
||||
export const accountsListRequested = makeActionCreator(T.ACCOUNTS.LIST_START);
|
||||
export const accountsListReceived = makeActionCreator(T.ACCOUNTS.LIST_COMPLETE, 'payload');
|
||||
export const accountsListError = makeActionCreator(T.ACCOUNTS.LIST_ERROR, 'error');
|
||||
|
||||
export const accountsRefListReceived = makeActionCreator(T.ACCOUNTS.LIST_REF_COMPLETE, 'payload');
|
||||
|
||||
export const accountCreateStart = makeActionCreator(T.ACCOUNTS.CREATE_START);
|
||||
export const accountCreateComplete = makeActionCreator(T.ACCOUNTS.CREATE_COMPLETE, 'payload');
|
||||
export const accountCreateError = makeActionCreator(T.ACCOUNTS.CREATE_ERROR, 'error');
|
||||
export const accountCreateFormUpdate = makeActionCreator(T.ACCOUNTS.CREATE_FORM_UPDATE, 'key', 'value');
|
||||
|
||||
export const accountRefCreateStart = makeActionCreator(T.ACCOUNTS.CREATE_REF_START);
|
||||
export const accountRefCreateComplete = makeActionCreator(T.ACCOUNTS.CREATE_REF_COMPLETE, 'payload');
|
||||
export const accountRefCreateError = makeActionCreator(T.ACCOUNTS.CREATE_REF_ERROR, 'error');
|
||||
export const accountRefCreateFormUpdate = makeActionCreator(T.ACCOUNTS.CREATE_REF_FORM_UPDATE, 'key', 'value');
|
||||
|
||||
export const accountRequested = makeActionCreator(T.ACCOUNT.SINGLE_START);
|
||||
export const accountComplete = makeActionCreator(T.ACCOUNT.SINGLE_COMPLETE, 'payload');
|
||||
export const accountError = makeActionCreator(T.ACCOUNT.SINGLE_ERROR, 'error');
|
||||
|
||||
|
||||
export function accountsList(userId) {
|
||||
return dispatch => {
|
||||
dispatch(accountsListRequested());
|
||||
return api.apiRetrieveAccounts(userId)
|
||||
.then(list => {
|
||||
dispatch(accountsListReceived(list));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(accountsListError(err));
|
||||
return Promise.resolve({ error: err });
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export function accountCreate(customerId, payload) {
|
||||
return dispatch => {
|
||||
dispatch(accountCreateStart());
|
||||
return api.apiCreateAccount(customerId, payload)
|
||||
.then(({ accountId }) => {
|
||||
dispatch(accountCreateComplete({
|
||||
id: accountId,
|
||||
...payload
|
||||
}));
|
||||
// dispatch(entityReceived(accountId, payload));
|
||||
dispatch(authenticate(true));
|
||||
return accountId;
|
||||
})
|
||||
.catch(err => {
|
||||
debugger;
|
||||
dispatch(accountCreateError(err));
|
||||
// return Promise.resolve({ error: err });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function accountRefCreate(customerId, payload) {
|
||||
return dispatch => {
|
||||
dispatch(accountRefCreateStart());
|
||||
return api.apiCreateRefAccount(customerId, payload)
|
||||
.then(({ id }) => {
|
||||
dispatch(accountRefCreateComplete({
|
||||
...payload,
|
||||
id
|
||||
}));
|
||||
dispatch(entityReceived(id, payload));
|
||||
return dispatch(authenticate(true));
|
||||
})
|
||||
.catch(err => {
|
||||
debugger;
|
||||
dispatch(accountRefCreateError(err));
|
||||
return Promise.resolve({ error: err });
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export function fetchOwnAccounts(customerId) {
|
||||
return dispatch => {
|
||||
//dispatch(accountsListRequested());
|
||||
return api.apiRetrieveAccounts(customerId)
|
||||
.then(data => {
|
||||
dispatch(accountsListReceived(data));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAccount(accountId) {
|
||||
return dispatch => {
|
||||
dispatch(accountRequested());
|
||||
return api.apiRetrieveAccount(accountId)
|
||||
.then(data => {
|
||||
dispatch(accountComplete(data));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(accountError(err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteAccountRequested = makeActionCreator(T.ACCOUNT.DELETE_START);
|
||||
export const deleteAccountComplete = makeActionCreator(T.ACCOUNT.DELETE_COMPLETE);
|
||||
export const deleteAccountError = makeActionCreator(T.ACCOUNT.DELETE_ERROR);
|
||||
|
||||
export function deleteAccount(customerId, accountId) {
|
||||
return dispatch => {
|
||||
dispatch(deleteAccountRequested());
|
||||
return api.apiDeleteAccount(accountId)
|
||||
.then(data => {
|
||||
//debugger;
|
||||
dispatch(deleteAccountComplete());
|
||||
return Promise.resolve('ok');
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(deleteAccountError());
|
||||
return Promise.reject(err);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export const errorMessageStart = makeActionCreator(T.ERROR.START, 'payload');
|
||||
export const errorMessageStop = makeActionCreator(T.ERROR.STOP);
|
||||
|
||||
export function errorMessageTimedOut(error, timeout) {
|
||||
return dispatch => {
|
||||
dispatch(errorMessageStart(error));
|
||||
setTimeout(() => {
|
||||
dispatch(errorMessageStop());
|
||||
}, timeout || 5000);
|
||||
};
|
||||
}
|
||||
|
||||
export const createRefOwnerLookupStart = makeActionCreator(T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_START, 'payload');
|
||||
export const createRefOwnerLookupComplete = makeActionCreator(T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_COMPLETE, 'payload');
|
||||
|
||||
export const createRefAccountLookupStart = makeActionCreator(T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_START, 'payload');
|
||||
export const createRefAccountLookupComplete = makeActionCreator(T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_COMPLETE, 'payload');
|
||||
|
||||
export const createRefOwnerLookup = lookup => {
|
||||
return dispatch => {
|
||||
dispatch(createRefOwnerLookupStart(lookup));
|
||||
return api.apiRetrieveUsers(lookup)
|
||||
.then(data => {
|
||||
|
||||
const { customers = [] } = data || {};
|
||||
|
||||
const arr = customers.map(c => {
|
||||
const { id, name, email } = c;
|
||||
const fullName = ([name.firstName, name.lastName]).filter(i => i).join(' ');
|
||||
const label = email ? `${ fullName } (${ email })` : fullName;
|
||||
return {
|
||||
value: id,
|
||||
label
|
||||
};
|
||||
});
|
||||
dispatch(createRefOwnerLookupComplete(arr));
|
||||
return { options: arr };
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(createRefOwnerLookupComplete([]));
|
||||
return { options: [] };
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const createRefAccountLookup = customerId => {
|
||||
return dispatch => {
|
||||
dispatch(createRefAccountLookupStart());
|
||||
return api.apiRetrieveAccounts(customerId)
|
||||
.then(data => {
|
||||
const arr = data.map(({ accountId, title }) => ({
|
||||
value: accountId,
|
||||
label: title
|
||||
}));
|
||||
dispatch(createRefAccountLookupComplete(arr));
|
||||
return { options: arr };
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(createRefAccountLookupComplete([]));
|
||||
return { options: [] };
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const makeTransferRequested = makeActionCreator(T.TRANSFERS.MAKE_START, 'payload');
|
||||
export const makeTransferComplete = makeActionCreator(T.TRANSFERS.MAKE_COMPLETE, 'payload');
|
||||
export const makeTransferError = makeActionCreator(T.TRANSFERS.MAKE_ERROR, 'error');
|
||||
export const makeTransferFormUpdate = makeActionCreator(T.TRANSFERS.MAKE_FORM_UPDATE, 'key', 'value');
|
||||
|
||||
export const makeTransfer = (accountId, payload) => {
|
||||
return dispatch => {
|
||||
dispatch(makeTransferRequested());
|
||||
return api.apiMakeTransfer(accountId, payload)
|
||||
.then(data => {
|
||||
const { moneyTransferId } = data;
|
||||
dispatch(makeTransferComplete(data));
|
||||
return moneyTransferId;
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(makeTransferError(err));
|
||||
return err;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getTransfersRequested = makeActionCreator(T.TRANSFERS.LIST_START);
|
||||
export const getTransfersComplete = makeActionCreator(T.TRANSFERS.LIST_COMPLETE, 'payload');
|
||||
export const getTransfersError = makeActionCreator(T.TRANSFERS.LIST_ERROR, 'error');
|
||||
|
||||
export const getTransfers = (accountId) => {
|
||||
return dispatch => {
|
||||
dispatch(getTransfersRequested());
|
||||
return api.apiRetrieveTransfers(accountId)
|
||||
.then(data => {
|
||||
dispatch(getTransfersComplete(data));
|
||||
return data;
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(getTransfersError(err));
|
||||
return err;
|
||||
});
|
||||
};
|
||||
};
|
||||
51
js-frontend/src/actions/signIn.js
Normal file
51
js-frontend/src/actions/signIn.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import {
|
||||
setCurrentEndpointKey,
|
||||
getCurrentEndpointKey,
|
||||
persistUserData
|
||||
} from "../utils/sessionStorage";
|
||||
|
||||
import { entityReceived } from './entities';
|
||||
import { storeCurrentEndpointKey } from "./configure";
|
||||
//import { parseResponse } from "../utils/handleFetchResponse";
|
||||
//import fetch from "../utils/fetch";
|
||||
|
||||
import { apiSignIn } from '../utils/api';
|
||||
import { makeActionCreator } from '../utils/actions';
|
||||
|
||||
import T from '../constants/ACTION_TYPES';
|
||||
|
||||
//import root from '../utils/root';
|
||||
|
||||
export const emailSignInFormUpdate = makeActionCreator(T.AUTH.SIGN_IN_FORM_UPDATE, 'key', 'value');
|
||||
export const emailSignInStart = makeActionCreator(T.AUTH.SIGN_IN_START);
|
||||
export const emailSignInComplete = makeActionCreator(T.AUTH.SIGN_IN_COMPLETE, 'user');
|
||||
export const emailSignInError = makeActionCreator(T.AUTH.SIGN_IN_ERROR, 'error');
|
||||
|
||||
export function emailSignIn(body) {
|
||||
return dispatch => {
|
||||
|
||||
dispatch(emailSignInStart());
|
||||
|
||||
return apiSignIn(body)
|
||||
.then(function(data = {}) {
|
||||
const { id } = data;
|
||||
if (id ) {
|
||||
dispatch(entityReceived(id, data));
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.then((user) => {
|
||||
persistUserData(user);
|
||||
dispatch(emailSignInComplete(user));
|
||||
})
|
||||
.catch((errors) => {
|
||||
// revert endpoint key to what it was before failed request
|
||||
//setCurrentEndpointKey(prevEndpointKey);
|
||||
//dispatch(storeCurrentEndpointKey(prevEndpointKey));
|
||||
return dispatch(emailSignInError(errors));
|
||||
});
|
||||
};
|
||||
}
|
||||
40
js-frontend/src/actions/signOut.js
Normal file
40
js-frontend/src/actions/signOut.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Created by andrew on 11/03/16.
|
||||
*/
|
||||
import {
|
||||
getEmailSignInUrl,
|
||||
setCurrentEndpointKey,
|
||||
getCurrentEndpointKey
|
||||
} from "../utils/sessionStorage";
|
||||
|
||||
import {destroySession} from "../utils/sessionStorage";
|
||||
|
||||
|
||||
import { entityReceived } from './entities';
|
||||
import { storeCurrentEndpointKey } from "./configure";
|
||||
import { parseResponse } from "../utils/handleFetchResponse";
|
||||
import fetch from "../utils/fetch";
|
||||
|
||||
import T from '../constants/ACTION_TYPES';
|
||||
|
||||
import root from '../utils/root';
|
||||
|
||||
export function signOutStart() {
|
||||
return { type: T.AUTH.SIGN_OUT_START };
|
||||
}
|
||||
|
||||
export function signOutComplete() {
|
||||
return { type: T.AUTH.SIGN_OUT_COMPLETE };
|
||||
}
|
||||
|
||||
export function signOut() {
|
||||
return dispatch => {
|
||||
|
||||
dispatch(signOutStart());
|
||||
|
||||
destroySession();
|
||||
|
||||
dispatch(signOutComplete());
|
||||
|
||||
};
|
||||
}
|
||||
48
js-frontend/src/actions/signUp.js
Normal file
48
js-frontend/src/actions/signUp.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Created by andrew on 11/03/16.
|
||||
*/
|
||||
import {
|
||||
getEmailSignUpUrl
|
||||
} from "../utils/sessionStorage";
|
||||
|
||||
|
||||
import { entityReceived } from './entities';
|
||||
import { storeCurrentEndpointKey } from "./configure";
|
||||
//import { parseResponse } from "../utils/handleFetchResponse";
|
||||
import { apiSignUp } from "../utils/api";
|
||||
import { emailSignInFormUpdate } from './signIn';
|
||||
import { push } from 'redux-router';
|
||||
|
||||
import T from '../constants/ACTION_TYPES';
|
||||
|
||||
export function emailSignUpFormUpdate(key, value) {
|
||||
return { type: T.AUTH.SIGN_UP_FORM_UPDATE, key, value };
|
||||
}
|
||||
|
||||
export function emailSignUpStart() {
|
||||
return { type: T.AUTH.SIGN_UP_START };
|
||||
}
|
||||
|
||||
export function emailSignUpComplete(user) {
|
||||
return { type: T.AUTH.SIGN_UP_COMPLETE, user };
|
||||
}
|
||||
|
||||
export function emailSignUpError(errors) {
|
||||
return { type: T.AUTH.SIGN_UP_ERROR, errors };
|
||||
}
|
||||
|
||||
export function emailSignUp(body) {
|
||||
return dispatch => {
|
||||
dispatch(emailSignUpStart());
|
||||
|
||||
return apiSignUp(body)
|
||||
.then(({data}) => {
|
||||
dispatch(emailSignUpComplete(data));
|
||||
const { email } = body;
|
||||
dispatch(emailSignInFormUpdate('email', email));
|
||||
dispatch(push('/signin'));
|
||||
})
|
||||
.catch(({errors}) => dispatch(emailSignUpError(errors)));
|
||||
|
||||
};
|
||||
}
|
||||
46
js-frontend/src/components/AccountInfo.js
Normal file
46
js-frontend/src/components/AccountInfo.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Created by andrew on 3/22/16.
|
||||
*/
|
||||
import React from "react";
|
||||
import { connect } from 'react-redux';
|
||||
import Spinner from "react-loader";
|
||||
import * as BS from "react-bootstrap";
|
||||
import * as A from '../actions/entities';
|
||||
|
||||
|
||||
// import { Money } from '../components/Money';
|
||||
|
||||
export class AccountInfo extends React.Component {
|
||||
componentWillMount() {
|
||||
this.ensureData(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.ensureData(nextProps);
|
||||
}
|
||||
|
||||
ensureData({ dispatch, entities, accountId }) {
|
||||
if (entities[accountId]) {
|
||||
return;
|
||||
}
|
||||
dispatch(A.fetchAccount(accountId));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { entities, accountId } = this.props;
|
||||
|
||||
const account = entities[accountId];
|
||||
|
||||
if (!account) {
|
||||
return (<div>{ accountId } <Spinner ref="spinner" loaded={false} /></div>)
|
||||
}
|
||||
|
||||
const { title } = account;
|
||||
|
||||
return (<div>{ title } </div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ app }) => ({
|
||||
entities: app.data.entities
|
||||
}))(AccountInfo);
|
||||
57
js-frontend/src/components/AuthComponent.js
Normal file
57
js-frontend/src/components/AuthComponent.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Created by andrew on 21/02/16.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { pushState } from 'redux-router';
|
||||
|
||||
import read from '../utils/readProp';
|
||||
|
||||
export function requireAuthentication(Component) {
|
||||
|
||||
class AuthComponent extends React.Component {
|
||||
|
||||
checkRedirect(props) {
|
||||
if (!props.isAuthenticated) {
|
||||
// redirect to login and add next param so we can redirect again after login
|
||||
const redirectAfterLogin = props.location.pathname;
|
||||
props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.checkRedirect(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.checkRedirect(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const { isAuthenticated = false } = this.props;
|
||||
|
||||
if (isAuthenticated) {
|
||||
// render the component that requires auth (passed to this wrapper)
|
||||
return (
|
||||
<Component {...this.props} />
|
||||
)
|
||||
}
|
||||
|
||||
return (<div className="panel">
|
||||
<h2 className="text-danger">No anonymous access!</h2>
|
||||
</div>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps =
|
||||
(state) => {
|
||||
return ({
|
||||
isAuthenticated: read(state, 'app.auth.user.isSignedIn', false)
|
||||
})
|
||||
};
|
||||
|
||||
return connect(mapStateToProps)(AuthComponent);
|
||||
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import './DocumentForm.less'
|
||||
|
||||
import React, {PropTypes} from 'react'
|
||||
import * as actions from '../actions/documentView'
|
||||
import { pacomoTransformer } from '../utils/pacomo'
|
||||
|
||||
|
||||
function updater(original, prop, fn) {
|
||||
return e => fn(Object.assign({}, original, {[prop]: e.target.value}))
|
||||
}
|
||||
|
||||
function preventDefault(fn) {
|
||||
return e => {
|
||||
e.preventDefault()
|
||||
fn && fn(e)
|
||||
}
|
||||
}
|
||||
|
||||
const errorMap = (error, i) => <li className='error' key={i}>{error}</li>
|
||||
|
||||
const DocumentForm = ({
|
||||
data,
|
||||
errors,
|
||||
onUpdate,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) =>
|
||||
<form
|
||||
onSubmit={preventDefault(onSubmit)}
|
||||
noValidate={true}
|
||||
>
|
||||
<ul className='errors'>
|
||||
{errors && Object.values(errors).map(errorMap)}
|
||||
</ul>
|
||||
<input
|
||||
type='text'
|
||||
className='title'
|
||||
placeholder='Title'
|
||||
onChange={updater(data, 'title', onUpdate)}
|
||||
value={data.title || ''}
|
||||
autoFocus
|
||||
/>
|
||||
<textarea
|
||||
type='text'
|
||||
className='content'
|
||||
onChange={updater(data, 'content', onUpdate)}
|
||||
value={data.content || ''}
|
||||
/>
|
||||
<footer className='buttons'>
|
||||
<button
|
||||
type='button'
|
||||
className='cancel'
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type='submit'
|
||||
className='submit'
|
||||
disabled={!onSubmit}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
DocumentForm.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default pacomoTransformer(DocumentForm)
|
||||
@@ -1,25 +0,0 @@
|
||||
.app-DocumentForm {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
|
||||
&-errors {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
&-error {
|
||||
color: red;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&-title,
|
||||
&-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&-cancel {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import './DocumentList.less'
|
||||
|
||||
import React, {PropTypes} from 'react'
|
||||
import * as actions from '../actions/documentListView'
|
||||
import { pacomoTransformer } from '../utils/pacomo'
|
||||
import Link from './Link'
|
||||
|
||||
|
||||
function mapValue(fn) {
|
||||
return e => fn(e.target.value)
|
||||
}
|
||||
|
||||
const DocumentList = ({
|
||||
id: activeId,
|
||||
query,
|
||||
documents,
|
||||
onChangeQuery,
|
||||
}) =>
|
||||
<div>
|
||||
<header className='header'>
|
||||
<input
|
||||
className='query'
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={query}
|
||||
onChange={mapValue(onChangeQuery)}
|
||||
/>
|
||||
</header>
|
||||
<ul className='list'>
|
||||
{documents.map(([id, data]) =>
|
||||
<li
|
||||
key={id}
|
||||
className={{
|
||||
'document-item': true,
|
||||
'document-item-active': activeId == id,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className='document-link'
|
||||
name='documentEdit'
|
||||
options={{id}}
|
||||
>
|
||||
{data.title}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
className={{
|
||||
'add-item': true,
|
||||
'add-item-active': activeId == 'new',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className='add-link'
|
||||
name='documentEdit'
|
||||
options={{id: 'new'}}
|
||||
>
|
||||
Add Document
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
DocumentList.propTypes = {
|
||||
id: PropTypes.string,
|
||||
query: PropTypes.string,
|
||||
documents: PropTypes.array.isRequired,
|
||||
onChangeQuery: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default pacomoTransformer(DocumentList)
|
||||
@@ -1,33 +0,0 @@
|
||||
.app-DocumentList {
|
||||
width: 100%;
|
||||
|
||||
&-header {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
&-query {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-document-item-active,
|
||||
&-add-item-active {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
&-document-link,
|
||||
&-add-link {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
js-frontend/src/components/HeaderLinks.js
Normal file
65
js-frontend/src/components/HeaderLinks.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Created by andrew on 11/03/16.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { LinkContainer } from "react-router-bootstrap";
|
||||
|
||||
import read from '../utils/readProp';
|
||||
|
||||
//import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
|
||||
import * as BS from "react-bootstrap";
|
||||
import { Link, IndexLink } from "react-router";
|
||||
|
||||
import { signOut } from '../actions/signOut';
|
||||
|
||||
|
||||
export class HeaderLinks extends React.Component {
|
||||
|
||||
signOut(evt, key) {
|
||||
this.props.dispatch(signOut());
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let buttonSet = null;
|
||||
const isSignedIn = read(this.props.auth, 'user.isSignedIn', false);
|
||||
const { location } = this.props.router;
|
||||
const isRegister = location.pathname == '/register';
|
||||
const isLogin = location.pathname == '/signin';
|
||||
|
||||
const condition = isSignedIn ? 2 : (isRegister ? 1 : 0);
|
||||
|
||||
switch (condition) {
|
||||
case 0:
|
||||
buttonSet = (<LinkContainer to="/register">
|
||||
<BS.NavItem>Register</BS.NavItem>
|
||||
</LinkContainer>);
|
||||
break;
|
||||
case 1:
|
||||
buttonSet = (<LinkContainer to="/signin">
|
||||
<BS.NavItem>Log In</BS.NavItem>
|
||||
</LinkContainer>);
|
||||
break;
|
||||
case 2:
|
||||
buttonSet = (<BS.NavItem onClick={this.signOut.bind(this)} eventKey="2">Sign Out</BS.NavItem>);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BS.Nav pullRight={true}>
|
||||
{ buttonSet }
|
||||
</BS.Nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({
|
||||
//dispatch,
|
||||
router,
|
||||
app
|
||||
}) => ({
|
||||
//dispatch,
|
||||
router,
|
||||
auth: app.auth
|
||||
}))(HeaderLinks);
|
||||
28
js-frontend/src/components/Money.js
Normal file
28
js-frontend/src/components/Money.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Created by andrew on 3/22/16.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
export const moneyText = (amount) => {
|
||||
|
||||
if (Number.isNaN(Number(amount))) {
|
||||
return '';
|
||||
}
|
||||
const absNum = Math.abs(Number(amount) / 100);
|
||||
if (absNum < 0) {
|
||||
return `$(${absNum.toFixed(2)})`;
|
||||
}
|
||||
return `$${absNum.toFixed(2)}`;
|
||||
};
|
||||
|
||||
export const Money = ({ amount }) => {
|
||||
|
||||
if (Number.isNaN(Number(amount))) {
|
||||
return (<span />);
|
||||
}
|
||||
const absNum = Math.abs(Number(amount) / 100);
|
||||
if (absNum < 0) {
|
||||
return (<span className="text-danger">($${ absNum.toFixed(2) })</span>)
|
||||
}
|
||||
return (<span>${ absNum.toFixed(2) }</span>);
|
||||
};
|
||||
61
js-frontend/src/components/TransfersTable.js
Normal file
61
js-frontend/src/components/TransfersTable.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Created by andrew on 3/22/16.
|
||||
*/
|
||||
import React from "react";
|
||||
import Spinner from "react-loader";
|
||||
import * as BS from "react-bootstrap";
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Money } from './Money';
|
||||
import AccountInfo from './AccountInfo';
|
||||
|
||||
export class TransfersTable extends React.Component {
|
||||
render() {
|
||||
const { loading, data, errors } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
|
||||
}
|
||||
if (Object.keys(errors).length) {
|
||||
return (<div className="text-danger">Errors..</div>);
|
||||
}
|
||||
|
||||
const transfers = data.length ? data.map(({
|
||||
amount,
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
transactionId,
|
||||
description = '',
|
||||
date = null,
|
||||
status = ''
|
||||
}, idx) => (<tr key={idx}>
|
||||
<td><TimeAgo date={date} /></td>
|
||||
<td><AccountInfo accountId={ fromAccountId } /></td>
|
||||
<td><AccountInfo accountId={ toAccountId } /></td>
|
||||
<td><Money amount={ amount } /></td>
|
||||
<td>{ description || 'N/a'}</td>
|
||||
<td>{ status || 'N/a' }</td>
|
||||
</tr>)) : (<tr>
|
||||
<td colSpan={6}>No transfers for this account just yet.</td>
|
||||
</tr>);
|
||||
|
||||
|
||||
return (
|
||||
<BS.Table striped bordered condensed hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>What</th>
|
||||
<th>Counter Account</th>
|
||||
<th>Amount</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ transfers }
|
||||
</tbody>
|
||||
</BS.Table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,16 @@
|
||||
* Created by andrew on 17/02/16.
|
||||
*/
|
||||
import React, { PropTypes } from "react";
|
||||
import { Grid, Navbar, NavItem, Nav, NavbarBrand } from "react-bootstrap";
|
||||
import { Grid, Col, Navbar, NavItem, Nav, NavbarBrand, Footer } from "react-bootstrap";
|
||||
import { LinkContainer } from "react-router-bootstrap";
|
||||
|
||||
import HeaderLinks from '../HeaderLinks';
|
||||
|
||||
//import { SignOutButton } from "redux-auth/bootstrap-theme";
|
||||
|
||||
//const SignOutButton = () => (<div>SignOutButton!</div>);
|
||||
|
||||
|
||||
//if (!global.__SERVER__ && !global.__TEST__) {
|
||||
// require("../../styles/main.scss");
|
||||
//}
|
||||
@@ -25,17 +32,25 @@ class Container extends React.Component {
|
||||
<LinkContainer to="/" onlyActiveOnIndex={true}>
|
||||
<NavItem eventKey={1}>Home</NavItem>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/account">
|
||||
<NavItem eventKey={2}>Account</NavItem>
|
||||
</LinkContainer>
|
||||
</Nav>
|
||||
<div>
|
||||
<HeaderLinks />
|
||||
</div>
|
||||
</Navbar>
|
||||
|
||||
<Grid className="content">
|
||||
{this.props.children}
|
||||
</Grid>
|
||||
|
||||
|
||||
<Navbar fixedBottom={true} className="footer-navigation">
|
||||
<Col xs={12} sm={6}>© 2016 Eventuate.io</Col>
|
||||
<Col xs={12} sm={6} className="text-right">
|
||||
<a href="#">Terms</a> |
|
||||
<a href="#">Policy</a> |
|
||||
<a href="#">Contact</a> |
|
||||
<a href="#">About</a>
|
||||
</Col>
|
||||
</Navbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,33 +4,78 @@ export default defineActionTypes({
|
||||
/*
|
||||
* View model
|
||||
*/
|
||||
|
||||
DOCUMENT_LIST_VIEW: `
|
||||
SET_QUERY
|
||||
AUTH: `
|
||||
CONFIGURE_START
|
||||
CONFIGURE_COMPLETE
|
||||
CONFIGURE_ERROR
|
||||
AUTHENTICATE_START
|
||||
AUTHENTICATE_COMPLETE
|
||||
AUTHENTICATE_ERROR
|
||||
SIGN_IN_START
|
||||
SIGN_IN_COMPLETE
|
||||
SIGN_IN_ERROR
|
||||
SIGN_IN_FORM_UPDATE
|
||||
SIGN_UP_START
|
||||
SIGN_UP_COMPLETE
|
||||
SIGN_UP_ERROR
|
||||
SIGN_UP_FORM_UPDATE
|
||||
SIGN_OUT_START
|
||||
SIGN_OUT_COMPLETE
|
||||
`,
|
||||
|
||||
DOCUMENT_VIEW: `
|
||||
UPDATE_DATA
|
||||
SET_ERRORS
|
||||
REMOVE_STALE_ERRORS
|
||||
CLEAR
|
||||
ENTITIES: `
|
||||
REQUESTED
|
||||
RECEIVED
|
||||
RECEIVED_LIST
|
||||
`,
|
||||
|
||||
|
||||
/*
|
||||
* Data model
|
||||
*/
|
||||
|
||||
DOCUMENT_DATA: `
|
||||
UPDATE
|
||||
ACCOUNTS: `
|
||||
LIST_START
|
||||
LIST_COMPLETE
|
||||
LIST_ERROR
|
||||
LIST_REF_START
|
||||
LIST_REF_COMPLETE
|
||||
LIST_REF_ERROR
|
||||
CREATE_START
|
||||
CREATE_COMPLETE
|
||||
CREATE_ERROR
|
||||
CREATE_FORM_UPDATE
|
||||
EDIT_START
|
||||
EDIT_COMPLETE
|
||||
EDIT_ERROR
|
||||
EDIT_FORM_UPDATE
|
||||
CREATE_REF_START
|
||||
CREATE_REF_COMPLETE
|
||||
CREATE_REF_ERROR
|
||||
CREATE_REF_FORM_UPDATE
|
||||
CREATE_REF_OWNER_LOOKUP_START
|
||||
CREATE_REF_OWNER_LOOKUP_COMPLETE
|
||||
CREATE_REF_ACCOUNT_LOOKUP_START
|
||||
CREATE_REF_ACCOUNT_LOOKUP_COMPLETE
|
||||
`,
|
||||
|
||||
/*
|
||||
* Application
|
||||
*/
|
||||
ACCOUNT: `
|
||||
SINGLE_START
|
||||
SINGLE_COMPLETE
|
||||
SINGLE_ERROR
|
||||
DELETE_START
|
||||
DELETE_COMPLETE
|
||||
DELETE_ERROR
|
||||
`,
|
||||
|
||||
TRANSFERS: `
|
||||
MAKE_START
|
||||
MAKE_COMPLETE
|
||||
MAKE_ERROR
|
||||
MAKE_FORM_UPDATE
|
||||
LIST_START
|
||||
LIST_COMPLETE
|
||||
LIST_ERROR
|
||||
`,
|
||||
|
||||
NAVIGATION: `
|
||||
ERROR: `
|
||||
START
|
||||
COMPLETE
|
||||
`,
|
||||
STOP
|
||||
`
|
||||
|
||||
})
|
||||
|
||||
53
js-frontend/src/controls/bootstrap/AuxErrorLabel.js
Normal file
53
js-frontend/src/controls/bootstrap/AuxErrorLabel.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Created by andrew on 15/02/16.
|
||||
*/
|
||||
import React, { PropTypes } from "react";
|
||||
import { Glyphicon } from "react-bootstrap";
|
||||
|
||||
|
||||
class AuxErrorLabel extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
label: PropTypes.string,
|
||||
errors: PropTypes.array
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
label: '',
|
||||
errors: []
|
||||
};
|
||||
|
||||
// <Input {...this.props}
|
||||
// bsStyle={(this.props.errors.length) ? "error" : null}
|
||||
// onChange={this.handleInput.bind(this)} />
|
||||
render () {
|
||||
const { errors } = this.props;
|
||||
|
||||
if (errors.length) {
|
||||
return (
|
||||
<div className='has-error'>
|
||||
{ errors.map((err, i) => {
|
||||
return (
|
||||
<p className="control-label inline-error-item"
|
||||
style={{paddingLeft: "20px", position: "relative", marginBottom: "28px"}}
|
||||
key={i}>
|
||||
|
||||
<Glyphicon glyph="exclamation-sign"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 2
|
||||
}}
|
||||
/> {this.props.label} {err}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <span />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AuxErrorLabel;
|
||||
@@ -2,12 +2,15 @@
|
||||
* Created by andrew on 15/02/16.
|
||||
*/
|
||||
import React, {PropTypes} from "react";
|
||||
import auth from "redux-auth";
|
||||
import { connect } from "react-redux";
|
||||
import read from '../../utils/readProp';
|
||||
|
||||
import * as BS from "react-bootstrap";
|
||||
import Input from "./Input";
|
||||
import ButtonLoader from "./ButtonLoader";
|
||||
import { emailSignInFormUpdate, emailSignIn } from "redux-auth";
|
||||
import { Glyphicon } from "react-bootstrap";
|
||||
import { connect } from "react-redux";
|
||||
import AuxErrorLabel from './AuxErrorLabel';
|
||||
|
||||
import { emailSignInFormUpdate, emailSignIn } from "../../actions/signIn";
|
||||
|
||||
/*
|
||||
<Input type="password"
|
||||
@@ -22,6 +25,7 @@ import { connect } from "react-redux";
|
||||
*/
|
||||
|
||||
class EmailSignInForm extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
endpoint: PropTypes.string,
|
||||
inputProps: PropTypes.shape({
|
||||
@@ -39,47 +43,56 @@ class EmailSignInForm extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
getEndpoint () {
|
||||
return (
|
||||
this.props.endpoint ||
|
||||
this.props.auth.getIn(["configure", "currentEndpointKey"]) ||
|
||||
this.props.auth.getIn(["configure", "defaultEndpointKey"])
|
||||
);
|
||||
}
|
||||
|
||||
handleInput (key, val) {
|
||||
this.props.dispatch(emailSignInFormUpdate(this.getEndpoint(), key, val));
|
||||
this.props.dispatch(emailSignInFormUpdate(key, val));
|
||||
}
|
||||
|
||||
handleSubmit (event) {
|
||||
event.preventDefault();
|
||||
let formData = this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "form"]).toJS();
|
||||
debugger;
|
||||
this.props.dispatch(emailSignIn(formData, this.getEndpoint()));
|
||||
let formData = { ...this.props.auth.signIn.form };
|
||||
this.props.dispatch(emailSignIn(formData));
|
||||
}
|
||||
|
||||
render () {
|
||||
let disabled = (
|
||||
this.props.auth.getIn(["user", "isSignedIn"]) ||
|
||||
this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "loading"])
|
||||
|
||||
try {
|
||||
const disabled = (
|
||||
this.props.auth.user.isSignedIn ||
|
||||
this.props.auth.signIn.loading
|
||||
);
|
||||
|
||||
return (
|
||||
//const error = read(this.props.auth, 'signIn.errors.email', null);
|
||||
//debugger;
|
||||
const formErrors = read(this.props.auth, 'signIn.errors.errors', '');
|
||||
|
||||
|
||||
return (
|
||||
<form className='redux-auth email-sign-in-form clearfix'
|
||||
onSubmit={this.handleSubmit.bind(this)}>
|
||||
|
||||
<div className="form-group" style={{
|
||||
display: formErrors ? 'block' : 'none'
|
||||
}}>
|
||||
<AuxErrorLabel
|
||||
label="Form:"
|
||||
errors={formErrors.length ? [formErrors] : [] }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input type="text"
|
||||
className="email-sign-in-email"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "form", "email"])}
|
||||
errors={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "errors", "email"])}
|
||||
value={read(this.props.auth, 'signIn.form.email', '')}
|
||||
errors={read(this.props.auth, 'signIn.errors.email', [])}
|
||||
onChange={this.handleInput.bind(this, "email")}
|
||||
{...this.props.inputProps.email} />
|
||||
|
||||
<ButtonLoader loading={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "loading"])}
|
||||
<ButtonLoader loading={read(this.props.auth, 'signIn.loading', false)}
|
||||
type="submit"
|
||||
icon={<Glyphicon glyph="log-in" />}
|
||||
icon={<BS.Glyphicon glyph="log-in" />}
|
||||
className='email-sign-in-submit pull-right'
|
||||
disabled={disabled}
|
||||
onClick={this.handleSubmit.bind(this)}
|
||||
@@ -88,7 +101,11 @@ class EmailSignInForm extends React.Component {
|
||||
</ButtonLoader>
|
||||
</form>
|
||||
);
|
||||
} catch (ex){
|
||||
console.error('Render exception: ', ex);
|
||||
return [' ERROR '];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({auth}) => ({auth}))(EmailSignInForm);
|
||||
export default connect(({app}) => ({auth: app.auth}))(EmailSignInForm);
|
||||
@@ -2,33 +2,22 @@
|
||||
* Created by andrew on 15/02/16.
|
||||
*/
|
||||
import React, {PropTypes} from "react";
|
||||
import auth from "redux-auth";
|
||||
//import auth from "redux-auth";
|
||||
import Input from "./Input";
|
||||
import ButtonLoader from "./ButtonLoader";
|
||||
import { emailSignUpFormUpdate, emailSignUp } from "redux-auth";
|
||||
//import { emailSignUpFormUpdate, emailSignUp } from "redux-auth";
|
||||
import IndexPanel from "./../../components/partials/IndexPanel";
|
||||
import { customerInfoMap } from '../../entities/formToPayloadMappers';
|
||||
|
||||
import read from '../../utils/readProp';
|
||||
|
||||
import { Glyphicon } from "react-bootstrap";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
class EmailSignUpForm extends React.Component {
|
||||
static propTypes = {
|
||||
endpoint: PropTypes.string,
|
||||
inputProps: PropTypes.shape({
|
||||
email: PropTypes.object,
|
||||
password: PropTypes.object,
|
||||
passwordConfirmation: PropTypes.object,
|
||||
submit: PropTypes.object
|
||||
})
|
||||
};
|
||||
import {emailSignUpFormUpdate, emailSignUp} from '../../actions/signUp';
|
||||
|
||||
static defaultProps = {
|
||||
inputProps: {
|
||||
email: {},
|
||||
password: {},
|
||||
submit: {}
|
||||
}
|
||||
};
|
||||
|
||||
class EmailSignUpForm extends React.Component {
|
||||
|
||||
getEndpoint () {
|
||||
return (
|
||||
@@ -39,57 +28,58 @@ class EmailSignUpForm extends React.Component {
|
||||
}
|
||||
|
||||
handleInput (key, val) {
|
||||
this.props.dispatch(emailSignUpFormUpdate(this.getEndpoint(), key, val));
|
||||
this.props.dispatch(emailSignUpFormUpdate(key, val));
|
||||
}
|
||||
|
||||
handleSubmit (event) {
|
||||
event.preventDefault();
|
||||
let formData = this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form"]).toJS();
|
||||
debugger;
|
||||
this.props.dispatch(emailSignUp(formData, this.getEndpoint()));
|
||||
|
||||
let formData = { ...this.props.auth.signUp.form };
|
||||
this.props.dispatch(emailSignUp(customerInfoMap(formData)));
|
||||
}
|
||||
|
||||
render () {
|
||||
let disabled = (
|
||||
this.props.auth.getIn(["user", "isSignedIn"]) ||
|
||||
this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "loading"])
|
||||
);
|
||||
|
||||
const disabled = (
|
||||
this.props.auth.user.isSignedIn ||
|
||||
this.props.auth.signUp.loading
|
||||
);
|
||||
|
||||
return (
|
||||
<form className='redux-auth email-sign-up-form clearfix'
|
||||
onSubmit={this.handleSubmit.bind(this)}>
|
||||
|
||||
<IndexPanel header="basic">
|
||||
|
||||
<Input type="text"
|
||||
label="First name"
|
||||
placeholder="First name"
|
||||
className="email-sign-up-email"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "fname"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "fname"])}
|
||||
value={read(this.props.auth, 'signUp.form.fname', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.fname', [])}
|
||||
onChange={this.handleInput.bind(this, "fname")}
|
||||
{...this.props.inputProps.fname} />
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="Last name"
|
||||
placeholder="Last name"
|
||||
className="email-sign-up-email"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "lname"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "lname"])}
|
||||
value={read(this.props.auth, 'signUp.form.lname', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.lname', [])}
|
||||
onChange={this.handleInput.bind(this, "lname")}
|
||||
{...this.props.inputProps.lname} />
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
className="email-sign-up-email"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "email"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "email"])}
|
||||
value={read(this.props.auth, 'signUp.form.email', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.email', [])}
|
||||
onChange={this.handleInput.bind(this, "email")}
|
||||
{...this.props.inputProps.email} />
|
||||
/>
|
||||
|
||||
|
||||
</IndexPanel>
|
||||
@@ -99,88 +89,96 @@ class EmailSignUpForm extends React.Component {
|
||||
label="SSN"
|
||||
placeholder="SSN"
|
||||
className="email-sign-up-email"
|
||||
bsSize="small"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "ssn"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "ssn"])}
|
||||
value={read(this.props.auth, 'signUp.form.ssn', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.ssn', [])}
|
||||
onChange={this.handleInput.bind(this, "ssn")}
|
||||
{...this.props.inputProps.ssn} />
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="Phone"
|
||||
placeholder="Phone"
|
||||
className="email-sign-up-email"
|
||||
bsSize="small"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "phone"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "phone"])}
|
||||
onChange={this.handleInput.bind(this, "phone")}
|
||||
{...this.props.inputProps.phone} />
|
||||
value={read(this.props.auth, 'signUp.form.phoneNumber', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.phoneNumber', [])}
|
||||
onChange={this.handleInput.bind(this, "phoneNumber")}
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="Address 1"
|
||||
placeholder="Address 1"
|
||||
className="email-sign-up-email"
|
||||
bsSize="small"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "address1"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "address1"])}
|
||||
value={read(this.props.auth, 'signUp.form.address1', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.address1', [])}
|
||||
onChange={this.handleInput.bind(this, "address1")}
|
||||
{...this.props.inputProps.address1} />
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="Address 2"
|
||||
placeholder="Address 2"
|
||||
className="email-sign-up-email"
|
||||
bsSize="small"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "address2"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "address2"])}
|
||||
value={read(this.props.auth, 'signUp.form.address2', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.address2', [])}
|
||||
onChange={this.handleInput.bind(this, "address2")}
|
||||
{...this.props.inputProps.address2} />
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="City"
|
||||
placeholder="City"
|
||||
className="email-sign-up-email"
|
||||
bsSize="small"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "city"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "city"])}
|
||||
value={read(this.props.auth, 'signUp.form.city', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.city', {})}
|
||||
onChange={this.handleInput.bind(this, "city")}
|
||||
{...this.props.inputProps.city} />
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="State"
|
||||
placeholder="State"
|
||||
className="email-sign-up-email"
|
||||
bsSize="small"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "state"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "state"])}
|
||||
value={read(this.props.auth, 'signUp.form.state', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.state', [])}
|
||||
onChange={this.handleInput.bind(this, "state")}
|
||||
{...this.props.inputProps.state} />
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
label="ZIP"
|
||||
placeholder="ZIP"
|
||||
className="email-sign-up-email"
|
||||
bsSize="small"
|
||||
disabled={disabled}
|
||||
value={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "form", "zip"])}
|
||||
errors={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "errors", "zip"])}
|
||||
value={read(this.props.auth, 'signUp.form.zip', '')}
|
||||
errors={read(this.props.auth, 'signUp.errors.zip', [])}
|
||||
onChange={this.handleInput.bind(this, "zip")}
|
||||
{...this.props.inputProps.zip} />
|
||||
/>
|
||||
|
||||
</IndexPanel>
|
||||
|
||||
|
||||
|
||||
<ButtonLoader loading={this.props.auth.getIn(["emailSignUp", this.getEndpoint(), "loading"])}
|
||||
<ButtonLoader loading={read(this.props.auth, 'signUp.loading', false)}
|
||||
type="submit"
|
||||
className="email-sign-up-submit pull-right"
|
||||
icon={<Glyphicon glyph="send" />}
|
||||
disabled={disabled}
|
||||
onClick={this.handleSubmit.bind(this)}
|
||||
{ ...this.props.inputProps.submit } >
|
||||
>
|
||||
Sign Up
|
||||
</ButtonLoader>
|
||||
</form>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({auth}) => ({auth}))(EmailSignUpForm);
|
||||
export default connect(({app}) => ({auth: app.auth}))(EmailSignUpForm);
|
||||
@@ -3,19 +3,18 @@
|
||||
*/
|
||||
import React, { PropTypes } from "react";
|
||||
import { Input, Glyphicon } from "react-bootstrap";
|
||||
import Immutable from "immutable";
|
||||
|
||||
class AuthInput extends React.Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
errors: PropTypes.object
|
||||
errors: PropTypes.array
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
label: "",
|
||||
label: '',
|
||||
value: null,
|
||||
errors: Immutable.fromJS([])
|
||||
errors: []
|
||||
};
|
||||
|
||||
handleInput (ev) {
|
||||
@@ -23,9 +22,10 @@ class AuthInput extends React.Component {
|
||||
}
|
||||
|
||||
renderErrorList () {
|
||||
if (this.props.errors.size) {
|
||||
|
||||
if (this.props.errors.length) {
|
||||
return (
|
||||
<div className='auth-error-message has-error'>
|
||||
<div className="auth-error-message has-error">
|
||||
{this.props.errors.map((err, i) => {
|
||||
return (
|
||||
<p className="control-label inline-error-item"
|
||||
@@ -53,7 +53,7 @@ class AuthInput extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<Input {...this.props}
|
||||
bsStyle={(this.props.errors.size) ? "error" : null}
|
||||
bsStyle={(this.props.errors.length) ? "error" : null}
|
||||
onChange={this.handleInput.bind(this)} />
|
||||
{this.renderErrorList()}
|
||||
</div>
|
||||
|
||||
30
js-frontend/src/entities/formToPayloadMappers.js
Normal file
30
js-frontend/src/entities/formToPayloadMappers.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Created by andrew on 21/02/16.
|
||||
*/
|
||||
export const customerInfoMap = ({
|
||||
ssn,
|
||||
address1,
|
||||
address2,
|
||||
city, //: "Moscow"
|
||||
email, //: "arevinsky@gmail.com"
|
||||
fname, //: "Andrew"
|
||||
lname, //: "Revinsky"
|
||||
phoneNumber, //: "+79031570864"
|
||||
state, //: "Kentucky"
|
||||
zip //: "125315"
|
||||
}) => ({
|
||||
"name": {
|
||||
"firstName": fname,
|
||||
"lastName": lname
|
||||
},
|
||||
email,
|
||||
ssn,
|
||||
"phoneNumber": phoneNumber,
|
||||
"address": {
|
||||
"street1": address1,
|
||||
"street2": address2,
|
||||
city,
|
||||
state,
|
||||
"zipCode": zip
|
||||
}
|
||||
});
|
||||
@@ -44,3 +44,29 @@ textarea {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 50px;
|
||||
/* height: 100%; */
|
||||
/* min-height: 100%; */
|
||||
height: auto;
|
||||
}
|
||||
.footer-navigation {
|
||||
height: 1px;
|
||||
& > .container {
|
||||
height: 100%;
|
||||
& > * {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding-bottom: 9px;
|
||||
margin: 0px 0 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
h1 {
|
||||
margin-top: .5em;
|
||||
}
|
||||
38
js-frontend/src/reducers/auth/authenticate.js
Normal file
38
js-frontend/src/reducers/auth/authenticate.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
valid: false,
|
||||
errors: null
|
||||
};
|
||||
|
||||
export const authReducer = (state = {...initialState}, action) => {
|
||||
switch(action.type) {
|
||||
case T.AUTH.AUTHENTICATE_START:
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
|
||||
case T.AUTH.AUTHENTICATE_COMPLETE:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: null,
|
||||
valid: true
|
||||
};
|
||||
|
||||
case T.AUTH.AUTHENTICATE_ERROR:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: "Invalid token",
|
||||
valid: false
|
||||
};
|
||||
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
56
js-frontend/src/reducers/auth/configure.js
Normal file
56
js-frontend/src/reducers/auth/configure.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
import createDataReducer from '../createDataReducer';
|
||||
|
||||
export const configReducer = createDataReducer([
|
||||
T.AUTH.CONFIGURE_START,
|
||||
T.AUTH.CONFIGURE_COMPLETE,
|
||||
T.AUTH.CONFIGURE_ERROR
|
||||
],
|
||||
'config',
|
||||
'config',
|
||||
(c = {}) => ({ ...c })
|
||||
);
|
||||
//
|
||||
// const configInitialState = {
|
||||
// loading: true,
|
||||
// errors: null,
|
||||
// config: null
|
||||
// };
|
||||
//
|
||||
// export const configReducer = (state = {...configInitialState}, action) => {
|
||||
// switch(action.type) {
|
||||
// case T.AUTH.CONFIGURE_START:
|
||||
// {
|
||||
// return {
|
||||
// ...state,
|
||||
// loading: true
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// case T.AUTH.CONFIGURE_COMPLETE: {
|
||||
// const { config } = action;
|
||||
// return {
|
||||
// ...state,
|
||||
// loading: false,
|
||||
// errors: null,
|
||||
// config
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// case T.AUTH.CONFIGURE_ERROR:
|
||||
// {
|
||||
// const { errors } = action;
|
||||
// return {
|
||||
// ...state,
|
||||
// loading: false,
|
||||
// errors
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// default:
|
||||
// return state;
|
||||
// }
|
||||
// };
|
||||
22
js-frontend/src/reducers/auth/index.js
Normal file
22
js-frontend/src/reducers/auth/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { configReducer } from './configure';
|
||||
import { authReducer } from './authenticate';
|
||||
import { signInReducer } from './signin';
|
||||
import { signUpReducer } from './signup';
|
||||
import { signOutReducer } from './signout';
|
||||
import { userReducer } from './user';
|
||||
|
||||
const authStateReducer = combineReducers({
|
||||
configure: configReducer,
|
||||
signIn: signInReducer,
|
||||
signUp: signUpReducer,
|
||||
signOut: signOutReducer,
|
||||
authentication: authReducer,
|
||||
user: userReducer
|
||||
});
|
||||
|
||||
export default authStateReducer;
|
||||
12
js-frontend/src/reducers/auth/signin.js
Normal file
12
js-frontend/src/reducers/auth/signin.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
import createFormReducer from '../createFormReducer';
|
||||
|
||||
export const signInReducer = createFormReducer([
|
||||
T.AUTH.SIGN_IN_START,
|
||||
T.AUTH.SIGN_IN_COMPLETE,
|
||||
T.AUTH.SIGN_IN_ERROR,
|
||||
T.AUTH.SIGN_IN_FORM_UPDATE
|
||||
]);
|
||||
26
js-frontend/src/reducers/auth/signout.js
Normal file
26
js-frontend/src/reducers/auth/signout.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
|
||||
const signOutInitialState = {
|
||||
loading: false,
|
||||
errors: null
|
||||
};
|
||||
|
||||
export const signOutReducer = (state = {...signOutInitialState}, action) => {
|
||||
switch(action.type) {
|
||||
case T.AUTH.SIGN_OUT_START:
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
case T.AUTH.SIGN_OUT_COMPLETE:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: null
|
||||
};
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
12
js-frontend/src/reducers/auth/signup.js
Normal file
12
js-frontend/src/reducers/auth/signup.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
import createFormReducer from '../createFormReducer';
|
||||
|
||||
export const signUpReducer = createFormReducer([
|
||||
T.AUTH.SIGN_UP_START,
|
||||
T.AUTH.SIGN_UP_COMPLETE,
|
||||
T.AUTH.SIGN_UP_ERROR,
|
||||
T.AUTH.SIGN_UP_FORM_UPDATE
|
||||
]);
|
||||
28
js-frontend/src/reducers/auth/user.js
Normal file
28
js-frontend/src/reducers/auth/user.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
|
||||
const initalState = {
|
||||
attributes: null,
|
||||
isSignedIn: false
|
||||
};
|
||||
|
||||
export const userReducer = (state = {...initalState}, action) => {
|
||||
switch(action.type) {
|
||||
case T.AUTH.AUTHENTICATE_COMPLETE:
|
||||
case T.AUTH.SIGN_IN_COMPLETE: {
|
||||
const { user } = action;
|
||||
return {...state,
|
||||
attributes: user,
|
||||
isSignedIn: !!user
|
||||
};
|
||||
}
|
||||
case T.AUTH.SIGN_OUT_COMPLETE:
|
||||
case T.AUTH.AUTHENTICATE_ERROR:
|
||||
return {
|
||||
...initalState
|
||||
};
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
43
js-frontend/src/reducers/createDataReducer.js
Normal file
43
js-frontend/src/reducers/createDataReducer.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Created by andrew on 3/22/16.
|
||||
*/
|
||||
const createDataReducer = ([KEY_REQUEST, KEY_SUCCESS, KEY_ERROR], payloadActionNameProp = 'payload', payloadStateNameProp = 'data', payloadAssignFn = (k = []) => [...k]) => {
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
errors: {},
|
||||
[payloadStateNameProp]: payloadAssignFn()
|
||||
};
|
||||
|
||||
return function formReducer(state = {...initialState}, action) {
|
||||
switch(action.type) {
|
||||
case KEY_REQUEST: {
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
case KEY_SUCCESS: {
|
||||
const payload = action[payloadActionNameProp];
|
||||
return {
|
||||
...initialState,
|
||||
[payloadStateNameProp]: payloadAssignFn(payload)
|
||||
};
|
||||
}
|
||||
case KEY_ERROR:
|
||||
{
|
||||
const {error} = action;
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: Object.isSealed(error) ? {aggregate: error} : {...error}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default createDataReducer;
|
||||
56
js-frontend/src/reducers/createFormReducer.js
Normal file
56
js-frontend/src/reducers/createFormReducer.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Created by andrew on 3/22/16.
|
||||
*/
|
||||
|
||||
const createFormReducer = ([KEY_REQUEST, KEY_SUCCESS, KEY_ERROR, KEY_UPDATE]) => {
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
form: {},
|
||||
errors: {}
|
||||
};
|
||||
|
||||
return function formReducer(state = {...initialState}, action) {
|
||||
switch(action.type) {
|
||||
case KEY_REQUEST: {
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
case KEY_ERROR: {
|
||||
const { error } = action;
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: error
|
||||
}
|
||||
}
|
||||
case KEY_SUCCESS: {
|
||||
return {
|
||||
...initialState
|
||||
}
|
||||
}
|
||||
case KEY_UPDATE: {
|
||||
const { key, value } = action;
|
||||
return {
|
||||
...state,
|
||||
form: {
|
||||
...state.form,
|
||||
[key]: value
|
||||
},
|
||||
errors: {
|
||||
...state.errors,
|
||||
aggregate: null,
|
||||
[key]: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default createFormReducer;
|
||||
69
js-frontend/src/reducers/data/accounts.js
Normal file
69
js-frontend/src/reducers/data/accounts.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
import { combineReducers } from 'redux';
|
||||
import createFormReducer from '../createFormReducer';
|
||||
|
||||
const ownAccountsReducer = (state = [], action ) => {
|
||||
switch (action.type) {
|
||||
case T.ACCOUNTS.LIST_COMPLETE: {
|
||||
const { payload = [] } = action;
|
||||
//const accounts = Object.keys(payload).map(key => payload[key]);
|
||||
return [
|
||||
...payload
|
||||
];
|
||||
}
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
const otherAccountsReducer = (state = [], action ) => {
|
||||
switch (action.type) {
|
||||
case T.AUTH.AUTHENTICATE_COMPLETE:
|
||||
case T.AUTH.SIGN_IN_COMPLETE: {
|
||||
const { user } = action;
|
||||
const { toAccounts = [] } = user;
|
||||
return otherAccountsReducer(state, {
|
||||
type: T.ACCOUNTS.LIST_REF_COMPLETE,
|
||||
payload: toAccounts
|
||||
});
|
||||
}
|
||||
|
||||
case T.ACCOUNTS.LIST_REF_COMPLETE: {
|
||||
const { payload = {} } = action;
|
||||
const accounts = Object.keys(payload).map(key => payload[key]);
|
||||
return [
|
||||
...accounts
|
||||
];
|
||||
}
|
||||
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const createAccountReducer = createFormReducer([
|
||||
T.ACCOUNTS.CREATE_START,
|
||||
T.ACCOUNTS.CREATE_COMPLETE,
|
||||
T.ACCOUNTS.CREATE_ERROR,
|
||||
T.ACCOUNTS.CREATE_FORM_UPDATE
|
||||
]);
|
||||
|
||||
const editAccountReducer = createFormReducer([
|
||||
T.ACCOUNTS.EDIT_START,
|
||||
T.ACCOUNTS.EDIT_COMPLETE,
|
||||
T.ACCOUNTS.EDIT_ERROR,
|
||||
T.ACCOUNTS.EDIT_FORM_UPDATE
|
||||
]);
|
||||
|
||||
export const accounts = combineReducers({
|
||||
own: ownAccountsReducer,
|
||||
other: otherAccountsReducer,
|
||||
create: createAccountReducer,
|
||||
edit: editAccountReducer
|
||||
});
|
||||
|
||||
119
js-frontend/src/reducers/data/bookmarkAccount.js
Normal file
119
js-frontend/src/reducers/data/bookmarkAccount.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Created by andrew on 18/03/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
|
||||
const optionsLoaderInitialState = {
|
||||
loading: false,
|
||||
options: [],
|
||||
value: ''
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
form: {},
|
||||
errors: {},
|
||||
accountsDisabled: true,
|
||||
|
||||
ownersLookup: {
|
||||
...optionsLoaderInitialState
|
||||
},
|
||||
accountsLookup: {
|
||||
...optionsLoaderInitialState
|
||||
}
|
||||
};
|
||||
|
||||
const optionsLoaderReducer = (state = {...optionsLoaderInitialState}, action) => {
|
||||
switch (action.type) {
|
||||
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_START:
|
||||
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_START: {
|
||||
const value = action.payload;
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_COMPLETE:
|
||||
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_COMPLETE: {
|
||||
const { payload } = action;
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
options: payload === null ? state.options : payload
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const bookmarkAccount = (state = {...initialState}, action) => {
|
||||
switch (action.type) {
|
||||
case T.ACCOUNTS.CREATE_REF_START: {
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
case T.ACCOUNTS.CREATE_REF_COMPLETE:{
|
||||
return {
|
||||
...initialState
|
||||
};
|
||||
}
|
||||
case T.ACCOUNTS.CREATE_REF_ERROR: {
|
||||
const { error } = action;
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: error
|
||||
};
|
||||
}
|
||||
case T.ACCOUNTS.CREATE_REF_FORM_UPDATE:{
|
||||
const { key, value } = action;
|
||||
const isOwnerSetBlank = ((key == 'owner') && !value);
|
||||
const isOwnerSelected = ((key == 'owner') && value);
|
||||
const nextAccountsDisabled = isOwnerSelected ? false : state.accountsDisabled;
|
||||
|
||||
const nextForm = isOwnerSetBlank ? {
|
||||
...state.form,
|
||||
account: null,
|
||||
[key]: value
|
||||
} : {
|
||||
...state.form,
|
||||
[key]: value
|
||||
};
|
||||
|
||||
const nextErrors = {
|
||||
...state.errors,
|
||||
[key]: null
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
accountsDisabled: nextAccountsDisabled,
|
||||
form: nextForm,
|
||||
errors: nextErrors
|
||||
};
|
||||
}
|
||||
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_START:
|
||||
case T.ACCOUNTS.CREATE_REF_OWNER_LOOKUP_COMPLETE: {
|
||||
return {
|
||||
...state,
|
||||
ownersLookup:
|
||||
optionsLoaderReducer(state.ownersLookup, action)
|
||||
};
|
||||
}
|
||||
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_START:
|
||||
case T.ACCOUNTS.CREATE_REF_ACCOUNT_LOOKUP_COMPLETE: {
|
||||
return {
|
||||
...state,
|
||||
accountsLookup:
|
||||
optionsLoaderReducer(state.accountsLookup, action)
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import typeReducers from '../../utils/typeReducers'
|
||||
import ACTION_TYPES from '../../constants/ACTION_TYPES'
|
||||
|
||||
|
||||
const defaultState = {}
|
||||
|
||||
|
||||
export default typeReducers(ACTION_TYPES.DOCUMENT_DATA, defaultState, {
|
||||
UPDATE: (state, {id, data}) => ({
|
||||
...state,
|
||||
[id]: { ...state[id], ...data },
|
||||
})
|
||||
})
|
||||
71
js-frontend/src/reducers/data/entities.js
Normal file
71
js-frontend/src/reducers/data/entities.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
|
||||
const initialState = {
|
||||
};
|
||||
|
||||
const nodeInitialState = {
|
||||
loading: false,
|
||||
data: {}
|
||||
};
|
||||
|
||||
export const entities = (state = {...initialState}, action) => {
|
||||
switch(action.type) {
|
||||
case T.ENTITIES.REQUESTED: {
|
||||
const { id } = action;
|
||||
return {
|
||||
...state,
|
||||
[id]: null
|
||||
}
|
||||
}
|
||||
case T.ENTITIES.RECEIVED: {
|
||||
const { id, entity = {} } = action;
|
||||
return {
|
||||
...state,
|
||||
[id]: {
|
||||
...entity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case T.AUTH.AUTHENTICATE_COMPLETE:
|
||||
case T.AUTH.SIGN_IN_COMPLETE:
|
||||
{
|
||||
const { user } = action;
|
||||
const { toAccounts = [] } = user;
|
||||
return {
|
||||
...state,
|
||||
...toAccounts
|
||||
};
|
||||
|
||||
}
|
||||
case T.ACCOUNTS.LIST_COMPLETE: {
|
||||
const { payload } = action;
|
||||
const hashMap = payload.reduce((memo, item) => {
|
||||
memo[item.accountId] = item;
|
||||
return memo;
|
||||
}, {});
|
||||
return {
|
||||
...state,
|
||||
...hashMap
|
||||
};
|
||||
}
|
||||
|
||||
case T.ACCOUNT.SINGLE_COMPLETE: {
|
||||
const { payload = {} } = action;
|
||||
const { accountId } = payload;
|
||||
if (!accountId) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
[accountId]: payload
|
||||
};
|
||||
}
|
||||
case T.ENTITIES.RECEIVED_LIST:
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
18
js-frontend/src/reducers/data/index.js
Normal file
18
js-frontend/src/reducers/data/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { accounts } from './accounts';
|
||||
import { transfers } from './transfers';
|
||||
import { entities } from './entities';
|
||||
import { bookmarkAccount } from './bookmarkAccount';
|
||||
|
||||
const dataReducer = combineReducers({
|
||||
transfers,
|
||||
entities,
|
||||
accounts,
|
||||
bookmarkAccount
|
||||
});
|
||||
|
||||
export default dataReducer;
|
||||
14
js-frontend/src/reducers/data/transfers.js
Normal file
14
js-frontend/src/reducers/data/transfers.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
import createListReducer from '../createDataReducer';
|
||||
|
||||
export const transfers = createListReducer([
|
||||
T.TRANSFERS.LIST_START,
|
||||
T.TRANSFERS.LIST_COMPLETE,
|
||||
T.TRANSFERS.LIST_ERROR
|
||||
]);
|
||||
@@ -1,21 +1,16 @@
|
||||
import { combineReducers } from 'redux'
|
||||
/**
|
||||
* Created by andrew on 18/03/16.
|
||||
*/
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import authStateReducer from './auth';
|
||||
import appStateReducer from './data'
|
||||
import uiReducer from './ui'
|
||||
|
||||
import navigation from './navigationReducer'
|
||||
const mainReducer = combineReducers({
|
||||
auth: authStateReducer,
|
||||
data: appStateReducer,
|
||||
ui: uiReducer
|
||||
});
|
||||
|
||||
import documentListView from './view/documentListViewReducer'
|
||||
import documentView from './view/documentViewReducer'
|
||||
|
||||
import documentData from './data/documentDataReducer'
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
navigation,
|
||||
view: combineReducers({
|
||||
documentList: documentListView,
|
||||
document: documentView,
|
||||
}),
|
||||
data: combineReducers({
|
||||
document: documentData,
|
||||
}),
|
||||
})
|
||||
export default mainReducer;
|
||||
@@ -1,19 +0,0 @@
|
||||
import typeReducers from '../utils/typeReducers'
|
||||
import ACTION_TYPES from '../constants/ACTION_TYPES'
|
||||
|
||||
|
||||
const defaultState = {
|
||||
transitioning: true,
|
||||
location: null,
|
||||
}
|
||||
|
||||
export default typeReducers(ACTION_TYPES.NAVIGATION, defaultState, {
|
||||
START: () => ({
|
||||
transitioning: true,
|
||||
}),
|
||||
|
||||
COMPLETE: (state, {location}) => ({
|
||||
transitioning: false,
|
||||
location,
|
||||
}),
|
||||
})
|
||||
42
js-frontend/src/reducers/ui/account.js
Normal file
42
js-frontend/src/reducers/ui/account.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
errors: []
|
||||
};
|
||||
|
||||
export const account = (state = { ...initialState }, action ) => {
|
||||
switch(action.type) {
|
||||
case T.ACCOUNT.SINGLE_START: {
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
case T.ACCOUNT.SINGLE_COMPLETE: {
|
||||
return {
|
||||
...initialState
|
||||
};
|
||||
}
|
||||
case T.ACCOUNT.SINGLE_ERROR: {
|
||||
const { error } = action;
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: [ error ]
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
9
js-frontend/src/reducers/ui/bookmarkAccount.js
Normal file
9
js-frontend/src/reducers/ui/bookmarkAccount.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Created by andrew on 18/03/16.
|
||||
*/
|
||||
export const bookmarkAccount = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
18
js-frontend/src/reducers/ui/errors.js
Normal file
18
js-frontend/src/reducers/ui/errors.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Created by andrew on 18/03/16.
|
||||
*/
|
||||
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
|
||||
export const error = (state = null, action ) => {
|
||||
switch (action.type) {
|
||||
case T.ERROR.STOP: {
|
||||
return null;
|
||||
}
|
||||
case T.ERROR.START:
|
||||
return action.payload;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
22
js-frontend/src/reducers/ui/index.js
Normal file
22
js-frontend/src/reducers/ui/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
/**
|
||||
* Created by andrew on 25/02/16.
|
||||
*/
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { account } from './account';
|
||||
import { error } from './errors';
|
||||
import { bookmarkAccount } from './bookmarkAccount';
|
||||
import { transfersMake } from './transfersMake';
|
||||
|
||||
|
||||
const uiReducer = combineReducers({
|
||||
account,
|
||||
error,
|
||||
bookmarkAccount,
|
||||
transfersMake
|
||||
});
|
||||
|
||||
export default uiReducer;
|
||||
15
js-frontend/src/reducers/ui/transfersMake.js
Normal file
15
js-frontend/src/reducers/ui/transfersMake.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
import T from '../../constants/ACTION_TYPES';
|
||||
import createFormReducer from '../createFormReducer';
|
||||
|
||||
export const transfersMake = createFormReducer([
|
||||
T.TRANSFERS.MAKE_START,
|
||||
T.TRANSFERS.MAKE_COMPLETE,
|
||||
T.TRANSFERS.MAKE_ERROR,
|
||||
T.TRANSFERS.MAKE_FORM_UPDATE
|
||||
]);
|
||||
@@ -1,10 +0,0 @@
|
||||
import typeReducers from '../../utils/typeReducers'
|
||||
import ACTION_TYPES from '../../constants/ACTION_TYPES'
|
||||
|
||||
|
||||
const defaultState = ''
|
||||
|
||||
|
||||
export default typeReducers(ACTION_TYPES.DOCUMENT_LIST_VIEW, defaultState, {
|
||||
SET_QUERY: (state, {query}) => query,
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import pick from 'object-pick'
|
||||
import typeReducers from '../../utils/typeReducers'
|
||||
import compact from '../../utils/compact'
|
||||
import ACTION_TYPES from '../../constants/ACTION_TYPES'
|
||||
|
||||
|
||||
const defaultState = {
|
||||
unsavedChanges: {},
|
||||
saveErrors: {},
|
||||
}
|
||||
|
||||
|
||||
export default typeReducers(ACTION_TYPES.DOCUMENT_VIEW, defaultState, {
|
||||
// Update the current document data
|
||||
UPDATE_DATA: (state, {id, data}) => ({
|
||||
...state,
|
||||
unsavedChanges: {
|
||||
...state.unsavedChanges,
|
||||
[id]: { ...state.unsavedChanges[id], ...data },
|
||||
},
|
||||
}),
|
||||
|
||||
// If there are fields marked as invalid which are now valid,
|
||||
// mark them as valid
|
||||
REMOVE_STALE_ERRORS: (state, {id, errors}) => ({
|
||||
...state,
|
||||
saveErrors: {
|
||||
...state.saveErrors,
|
||||
[id]: compact(pick(state.saveErrors[id], Object.keys(errors || {}))),
|
||||
},
|
||||
}),
|
||||
|
||||
// Set the errors to the passed in object
|
||||
SET_ERRORS: (state, {id, errors}) => ({
|
||||
...state,
|
||||
saveErrors: {
|
||||
...state.saveErrors,
|
||||
[id]: errors
|
||||
},
|
||||
}),
|
||||
|
||||
// Remove errors/data for an id
|
||||
CLEAR: (state, {id}) => ({
|
||||
unsavedChanges: {
|
||||
...state.unsavedChanges,
|
||||
[id]: null,
|
||||
},
|
||||
saveErrors: {
|
||||
...state.saveErrors,
|
||||
[id]: null,
|
||||
},
|
||||
}),
|
||||
})
|
||||
12
js-frontend/src/utils/actions.js
Normal file
12
js-frontend/src/utils/actions.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Created by andrew on 15/03/16.
|
||||
*/
|
||||
export function makeActionCreator(type, ...argNames) {
|
||||
return function(...args) {
|
||||
const action = { type };
|
||||
argNames.forEach((arg, index) => {
|
||||
action[argNames[index]] = args[index]
|
||||
});
|
||||
return action;
|
||||
};
|
||||
}
|
||||
173
js-frontend/src/utils/api.js
Normal file
173
js-frontend/src/utils/api.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Created by andrew on 12/03/16.
|
||||
*/
|
||||
import fetch from './fetch';
|
||||
import {
|
||||
getEmailSignInUrl,
|
||||
getEmailSignUpUrl,
|
||||
getCurrentUserUrl,
|
||||
getAccountsUrl,
|
||||
getCustomersUrl,
|
||||
getTransfersUrl
|
||||
} from "./sessionStorage";
|
||||
import root from './root';
|
||||
|
||||
|
||||
import { parseResponse } from "./handleFetchResponse";
|
||||
|
||||
function makeQuery(params) {
|
||||
return Object.keys(params).map(key => [encodeURIComponent(key), encodeURIComponent(params[key])].join('=')).join('&');
|
||||
}
|
||||
|
||||
export function apiSignIn(body) {
|
||||
return fetch(getEmailSignInUrl(), {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "post",
|
||||
body: root.JSON.stringify(body)
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiSignUp(body) {
|
||||
return fetch(getEmailSignUpUrl(), {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "post",
|
||||
body: root.JSON.stringify(body)
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiGetCurrentUser() {
|
||||
return fetch(getCurrentUserUrl(), {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "get"
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiCreateAccount(customerId, {
|
||||
title, balance: initialBalance, description }) {
|
||||
//{
|
||||
//"accountId": "0000015377cf131b-a250093f26850000"
|
||||
//}
|
||||
|
||||
return fetch(getAccountsUrl(), {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "post",
|
||||
body: root.JSON.stringify({
|
||||
customerId,
|
||||
title,
|
||||
initialBalance,
|
||||
description })
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiCreateRefAccount(customerId, {
|
||||
owner, account: accountId, title, description }) {
|
||||
|
||||
return fetch(`${getCustomersUrl()}/${customerId}/toaccounts`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "post",
|
||||
body: root.JSON.stringify({
|
||||
owner,
|
||||
id: accountId,
|
||||
title,
|
||||
description })
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiMakeTransfer(fromAccountId, {
|
||||
account, amount, description }) {
|
||||
|
||||
return fetch(getTransfersUrl(), {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "post",
|
||||
body: root.JSON.stringify({
|
||||
"amount": amount,
|
||||
"fromAccountId": fromAccountId,
|
||||
"toAccountId": account,
|
||||
description
|
||||
})
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiRetrieveAccounts(customerId) {
|
||||
|
||||
return fetch(`${getAccountsUrl()}?${makeQuery({ customerId })}`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "get"
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiRetrieveTransfers(accountId) {
|
||||
|
||||
return fetch(`${getAccountsUrl()}/${accountId}/history`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "get"
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiRetrieveAccount(accountId) {
|
||||
return fetch(`${getAccountsUrl()}/${accountId}`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "get"
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiDeleteAccount(accountId) {
|
||||
return Promise.reject({
|
||||
message: '\'Delete Account\' is not implemented.'
|
||||
});
|
||||
|
||||
return fetch(`${getAccountsUrl()}/${accountId}`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "delete"
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiRetrieveUsers(email) {
|
||||
return fetch(`${getCustomersUrl()}?${makeQuery({ email })}`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "get"
|
||||
}).then(parseResponse);
|
||||
}
|
||||
|
||||
export function apiRetrieveUser(customerId) {
|
||||
return fetch(`${getCustomersUrl()}/${ customerId }`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "get"
|
||||
}).then(parseResponse);
|
||||
}
|
||||
122
js-frontend/src/utils/clientSettings.js
Normal file
122
js-frontend/src/utils/clientSettings.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
|
||||
import * as C from "./constants";
|
||||
import fetch from "./fetch";
|
||||
|
||||
import parseEndpointConfig from "./parseEndpointConfig";
|
||||
import { setEndpointKeys } from "../actions/configure";
|
||||
|
||||
import {
|
||||
getCurrentSettings,
|
||||
setCurrentSettings,
|
||||
getInitialEndpointKey,
|
||||
setDefaultEndpointKey,
|
||||
setCurrentEndpoint,
|
||||
setCurrentEndpointKey,
|
||||
retrieveData,
|
||||
persistData,
|
||||
destroySession
|
||||
} from "./sessionStorage";
|
||||
|
||||
// can't use "window" with node app
|
||||
var root = Function("return this")() || (42, eval)("this");
|
||||
|
||||
const defaultSettings = {
|
||||
//proxyIf: function() { return false; },
|
||||
//proxyUrl: "/proxy",
|
||||
forceHardRedirect: false,
|
||||
storage: "cookies",
|
||||
cookieExpiry: 14,
|
||||
cookiePath: "/",
|
||||
initialCredentials: null,
|
||||
|
||||
passwordResetSuccessUrl: function() {
|
||||
return root.location.href;
|
||||
},
|
||||
|
||||
confirmationSuccessUrl: function() {
|
||||
return root.location.href;
|
||||
},
|
||||
|
||||
tokenFormat: {
|
||||
"access-token": "{{ access-token }}"
|
||||
//"token-type": "Bearer",
|
||||
//client: "{{ client }}",
|
||||
//expiry: "{{ expiry }}",
|
||||
//uid: "{{ uid }}"
|
||||
},
|
||||
|
||||
parseExpiry: function(headers){
|
||||
// convert from ruby time (seconds) to js time (millis)
|
||||
return (parseInt(headers["expiry"], 10) * 1000) || null;
|
||||
},
|
||||
|
||||
handleLoginResponse: function(resp) {
|
||||
return resp.data;
|
||||
},
|
||||
|
||||
handleAccountUpdateResponse: function(resp) {
|
||||
return resp.data;
|
||||
},
|
||||
|
||||
handleTokenValidationResponse: function(resp) {
|
||||
return resp.data;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// save session configuration
|
||||
export function applyConfig({ dispatch, endpoint={}, settings={}, reset=false } = {}) {
|
||||
|
||||
if (settings.currentLocation && settings.currentLocation.match(/blank=true/)) {
|
||||
return Promise.resolve({blank: true});
|
||||
}
|
||||
|
||||
let currentEndpointKey;
|
||||
|
||||
if (reset) {
|
||||
resetConfig();
|
||||
}
|
||||
|
||||
if (settings.initialCredentials) {
|
||||
currentEndpointKey = settings.initialCredentials.currentEndpointKey;
|
||||
}
|
||||
|
||||
setCurrentSettings({ ...defaultSettings, ...settings });
|
||||
|
||||
const currentHeaders = retrieveData(C.SAVED_CREDS_KEY) || {};
|
||||
|
||||
const accessToken = currentHeaders["access-token"];
|
||||
|
||||
//if (authRedirectHeaders && authRedirectHeaders["access-token"]) {
|
||||
if (!accessToken) {
|
||||
destroySession();
|
||||
}
|
||||
|
||||
|
||||
let { defaultEndpointKey, currentEndpoint } = parseEndpointConfig(
|
||||
endpoint, getInitialEndpointKey()
|
||||
);
|
||||
|
||||
if (!currentEndpointKey) {
|
||||
currentEndpointKey = defaultEndpointKey;
|
||||
}
|
||||
|
||||
// persist default config key with session storage
|
||||
setDefaultEndpointKey(defaultEndpointKey);
|
||||
setCurrentEndpoint(currentEndpoint);
|
||||
|
||||
dispatch(setEndpointKeys(
|
||||
Object.keys(currentEndpoint),
|
||||
currentEndpointKey,
|
||||
defaultEndpointKey));
|
||||
|
||||
setCurrentEndpointKey(currentEndpointKey);
|
||||
|
||||
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
}
|
||||
8
js-frontend/src/utils/constants.js
Normal file
8
js-frontend/src/utils/constants.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
export const INITIAL_CONFIG_KEY = "default";
|
||||
export const DEFAULT_CONFIG_KEY = "defaultConfigKey";
|
||||
export const SAVED_CONFIG_KEY = "currentConfigName";
|
||||
export const SAVED_CREDS_KEY = "authHeaders";
|
||||
export const SAVED_USER_INFO = "user-info";
|
||||
83
js-frontend/src/utils/fetch.js
Normal file
83
js-frontend/src/utils/fetch.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import originalFetch from "isomorphic-fetch";
|
||||
import * as C from "./constants";
|
||||
|
||||
import {
|
||||
retrieveData,
|
||||
persistData,
|
||||
getTokenFormat,
|
||||
isApiRequest
|
||||
} from "./sessionStorage";
|
||||
|
||||
|
||||
function getAuthHeaders(url) {
|
||||
if (isApiRequest(url)) {
|
||||
// fetch current auth headers from storage
|
||||
let currentHeaders = retrieveData(C.SAVED_CREDS_KEY) || {},
|
||||
nextHeaders = {};
|
||||
|
||||
if (currentHeaders === 'undefined') {
|
||||
currentHeaders = {};
|
||||
}
|
||||
// bust IE cache
|
||||
nextHeaders["If-Modified-Since"] = "Mon, 26 Jul 1997 05:00:00 GMT";
|
||||
|
||||
// set header for each key in `tokenFormat` config
|
||||
for (var key in getTokenFormat()) {
|
||||
if (key in currentHeaders) {
|
||||
nextHeaders[key] = currentHeaders[key];
|
||||
}
|
||||
}
|
||||
|
||||
return nextHeaders;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function updateAuthCredentials(resp) {
|
||||
|
||||
// check config apiUrl matches the current response url
|
||||
if (isApiRequest(resp.url)) {
|
||||
// set header for each key in `tokenFormat` config
|
||||
var newHeaders = {};
|
||||
|
||||
// set flag to ensure that we don't accidentally nuke the headers
|
||||
// if the response tokens aren't sent back from the API
|
||||
var blankHeaders = true;
|
||||
|
||||
// set header key + val for each key in `tokenFormat` config
|
||||
for (var key in getTokenFormat()) {
|
||||
newHeaders[key] = resp.headers.get(key);
|
||||
|
||||
if (newHeaders[key]) {
|
||||
blankHeaders = false;
|
||||
}
|
||||
}
|
||||
|
||||
// persist headers for next request
|
||||
if (!blankHeaders) {
|
||||
persistData(C.SAVED_CREDS_KEY, newHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
export default function (url, options = {}) {
|
||||
|
||||
if (!options.headers) {
|
||||
options.headers = {}
|
||||
}
|
||||
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
...getAuthHeaders(url)
|
||||
};
|
||||
|
||||
//extend(options.headers, getAuthHeaders(url));
|
||||
return originalFetch(url, options)
|
||||
.then(resp => updateAuthCredentials(resp));
|
||||
}
|
||||
33
js-frontend/src/utils/handleFetchResponse.js
Normal file
33
js-frontend/src/utils/handleFetchResponse.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
export function parseResponse (response) {
|
||||
let json = response.json();
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return json;
|
||||
} else {
|
||||
|
||||
//error: "Bad Request"
|
||||
//exception: "org.springframework.web.bind.MethodArgumentNotValidException"
|
||||
//message: "Validation failed for argument at index 0 in method: public rx.Observable<net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerResponse> net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.customers.CustomerController.createCustomer(net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo), with 3 error(s): [Field error in object 'customerInfo' on field 'ssn': rejected value [null]; codes [NotNull.customerInfo.ssn,NotNull.ssn,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.ssn,ssn]; arguments []; default message [ssn]]; default message [may not be null]] [Field error in object 'customerInfo' on field 'email': rejected value [null]; codes [NotNull.customerInfo.email,NotNull.email,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.email,email]; arguments []; default message [email]]; default message [may not be null]] [Field error in object 'customerInfo' on field 'phoneNumber': rejected value [null]; codes [NotNull.customerInfo.phoneNumber,NotNull.phoneNumber,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber]]; default message [may not be null]] "
|
||||
//path: "/customers"
|
||||
//status: 400
|
||||
//timestamp: 1458002123103
|
||||
|
||||
return json.then(({ message, ...rest }) => {
|
||||
if (!message) {
|
||||
return rest;
|
||||
}
|
||||
const jvmPattern = /\[Field error in object '\w+' on field '(\w+)'/gm;
|
||||
let errors = {};
|
||||
message.replace(jvmPattern, (m, name) => {
|
||||
errors[name] = ['Required'];
|
||||
});
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
return { errors };
|
||||
}
|
||||
return { errors: message };
|
||||
}).then(err => Promise.reject(err));
|
||||
}
|
||||
}
|
||||
65
js-frontend/src/utils/parseEndpointConfig.js
Normal file
65
js-frontend/src/utils/parseEndpointConfig.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import * as C from "./constants";
|
||||
|
||||
// base endpoint that other endpoints extend from
|
||||
const defaultEndpoint = {
|
||||
apiUrl: "/api",
|
||||
signOutPath: "/auth/sign_out",
|
||||
emailSignInPath: "/auth/sign_in",
|
||||
emailRegistrationPath: "/auth",
|
||||
accountUpdatePath: "/auth",
|
||||
accountDeletePath: "/auth",
|
||||
passwordResetPath: "/auth/password",
|
||||
passwordUpdatePath: "/auth/password",
|
||||
tokenValidationPath: "/auth/validate_token",
|
||||
|
||||
authProviderPaths: {
|
||||
github: "/auth/github",
|
||||
facebook: "/auth/facebook",
|
||||
google: "/auth/google_oauth2"
|
||||
}
|
||||
};
|
||||
|
||||
function getFirstObjectKey (obj) {
|
||||
for (var key in obj) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseEndpointConfig(endpoint, defaultEndpointKey = null) {
|
||||
// normalize so opts is always an array of objects
|
||||
if (endpoint.constructor !== Array) {
|
||||
// single config will always be called 'default' unless set
|
||||
// by previous session
|
||||
defaultEndpointKey = C.INITIAL_CONFIG_KEY;
|
||||
|
||||
// config should look like {default: {...}}
|
||||
var defaultConfig = {};
|
||||
defaultConfig[defaultEndpointKey] = endpoint;
|
||||
|
||||
// endpoint should look like [{default: {...}}]
|
||||
endpoint = [defaultConfig];
|
||||
}
|
||||
|
||||
let currentEndpoint = {};
|
||||
|
||||
// iterate over config items, extend each from defaults
|
||||
for (var i = 0; i < endpoint.length; i++) {
|
||||
var configName = getFirstObjectKey(endpoint[i]);
|
||||
|
||||
// set first as default config
|
||||
if (!defaultEndpointKey) {
|
||||
defaultEndpointKey = configName;
|
||||
}
|
||||
|
||||
// save config to `configs` hash
|
||||
currentEndpoint[configName] = {
|
||||
...defaultEndpoint,
|
||||
...endpoint[i][configName]
|
||||
};
|
||||
}
|
||||
|
||||
return { defaultEndpointKey, currentEndpoint };
|
||||
}
|
||||
134
js-frontend/src/utils/parseUrl.js
Normal file
134
js-frontend/src/utils/parseUrl.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import querystring from "querystring";
|
||||
|
||||
export function normalizeTokenKeys (params) {
|
||||
// normalize keys
|
||||
if (params.token) {
|
||||
params["access-token"] = params.token;
|
||||
delete params.token;
|
||||
}
|
||||
if (params.auth_token) {
|
||||
params["access-token"] = params.auth_token;
|
||||
delete params.auth_token;
|
||||
}
|
||||
if (params.client_id) {
|
||||
params.client = params.client_id;
|
||||
delete params.client_id;
|
||||
}
|
||||
if (params.config) {
|
||||
params.endpointKey = params.config;
|
||||
delete params.config;
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
const getAnchorSearch = function(location) {
|
||||
const rawAnchor = location.hash || "",
|
||||
arr = rawAnchor.split("?");
|
||||
return (arr.length > 1) ? arr[1] : null;
|
||||
};
|
||||
|
||||
const getSearchQs = function(location) {
|
||||
const rawQs = location.search || "",
|
||||
qs = rawQs.replace("?", ""),
|
||||
qsObj = (qs) ? querystring.parse(qs) : {};
|
||||
|
||||
return qsObj;
|
||||
};
|
||||
|
||||
const getAnchorQs = function(location) {
|
||||
const anchorQs = getAnchorSearch(location),
|
||||
anchorQsObj = (anchorQs) ? querystring.parse(anchorQs) : {};
|
||||
|
||||
return anchorQsObj;
|
||||
};
|
||||
|
||||
const stripKeys = function(obj, keys) {
|
||||
for (var q in keys) {
|
||||
delete obj[keys[q]];
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
export function getAllParams (location) {
|
||||
return {
|
||||
...getAnchorQs(location),
|
||||
...getSearchQs(location)
|
||||
};
|
||||
};
|
||||
|
||||
const buildCredentials = function(location, keys) {
|
||||
const params = getAllParams(location);
|
||||
let authHeaders = {};
|
||||
|
||||
for (var key of keys) {
|
||||
authHeaders[key] = params[key];
|
||||
}
|
||||
|
||||
return normalizeTokenKeys(authHeaders);
|
||||
};
|
||||
|
||||
|
||||
// this method is tricky. we want to reconstruct the current URL with the
|
||||
// following conditions:
|
||||
// 1. search contains none of the supplied keys
|
||||
// 2. anchor search (i.e. `#/?key=val`) contains none of the supplied keys
|
||||
// 3. all of the keys NOT supplied are presevered in their original form
|
||||
// 4. url protocol, host, and path are preserved
|
||||
const getLocationWithoutParams = function(currentLocation, keys) {
|
||||
// strip all values from both actual and anchor search params
|
||||
let newSearch = querystring.stringify(stripKeys(getSearchQs(currentLocation), keys)),
|
||||
newAnchorQs = querystring.stringify(stripKeys(getAnchorQs(currentLocation), keys)),
|
||||
newAnchor = (currentLocation.hash || "").split("?")[0];
|
||||
|
||||
if (newSearch) {
|
||||
newSearch = "?" + newSearch;
|
||||
}
|
||||
|
||||
if (newAnchorQs) {
|
||||
newAnchor += "?" + newAnchorQs;
|
||||
}
|
||||
|
||||
if (newAnchor && !newAnchor.match(/^#/)) {
|
||||
newAnchor = "#/" + newAnchor;
|
||||
}
|
||||
|
||||
// reconstruct location with stripped auth keys
|
||||
const newLocation = currentLocation.pathname + newSearch + newAnchor;
|
||||
|
||||
return newLocation;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default function getRedirectInfo(currentLocation) {
|
||||
if (!currentLocation) {
|
||||
return {};
|
||||
} else {
|
||||
let authKeys = [
|
||||
"access-token",
|
||||
"token",
|
||||
"auth_token",
|
||||
"config",
|
||||
"client",
|
||||
"client_id",
|
||||
"expiry",
|
||||
"uid",
|
||||
"reset_password",
|
||||
"account_confirmation_success"
|
||||
];
|
||||
|
||||
var authRedirectHeaders = buildCredentials(currentLocation, authKeys);
|
||||
var authRedirectPath = getLocationWithoutParams(currentLocation, authKeys);
|
||||
|
||||
if (authRedirectPath !== currentLocation) {
|
||||
return { authRedirectHeaders, authRedirectPath };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
16
js-frontend/src/utils/readProp.js
Normal file
16
js-frontend/src/utils/readProp.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Created by andrew on 11/03/16.
|
||||
*/
|
||||
export default function read(src, path = '', defaultVal = null) {
|
||||
const [pathItem = null, ...rest] = path.split('.');
|
||||
|
||||
if (pathItem === null ) {
|
||||
return src || defaultVal;
|
||||
} else if (rest.length === 0) {
|
||||
if (!src) { return defaultVal; }
|
||||
return src[pathItem] || defaultVal;
|
||||
}
|
||||
|
||||
if (!src) { return defaultVal; }
|
||||
return read(src[pathItem], rest.join('.'), defaultVal);
|
||||
}
|
||||
6
js-frontend/src/utils/root.js
Normal file
6
js-frontend/src/utils/root.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Created by andrew on 27/02/16.
|
||||
*/
|
||||
// even though this code shouldn't be used server-side, node will throw
|
||||
// errors if "window" is used
|
||||
export default Function("return this")() || (42, eval)("this");
|
||||
208
js-frontend/src/utils/sessionStorage.js
Normal file
208
js-frontend/src/utils/sessionStorage.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import Cookies from "js-cookie";
|
||||
import * as C from "./constants";
|
||||
|
||||
import root from './root';
|
||||
//import "babel-polyfill";
|
||||
|
||||
|
||||
// stateful variables that persist throughout session
|
||||
root.authState = {
|
||||
currentSettings: {},
|
||||
currentEndpoint: {},
|
||||
defaultEndpointKey: 'default'
|
||||
};
|
||||
|
||||
export function setCurrentSettings (s) {
|
||||
root.authState.currentSettings = s;
|
||||
}
|
||||
|
||||
export function getCurrentSettings () {
|
||||
return root.authState.currentSettings;
|
||||
}
|
||||
|
||||
export function setCurrentEndpoint (e) {
|
||||
root.authState.currentEndpoint = e;
|
||||
}
|
||||
|
||||
export function getCurrentEndpoint () {
|
||||
return root.authState.currentEndpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param k
|
||||
*/
|
||||
export function setCurrentEndpointKey (k) {
|
||||
persistData(C.SAVED_CONFIG_KEY, k || getDefaultEndpointKey());
|
||||
}
|
||||
|
||||
export function getCurrentEndpointKey () {
|
||||
return getDefaultEndpointKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param k
|
||||
*/
|
||||
export function setDefaultEndpointKey (k) {
|
||||
persistData(C.DEFAULT_CONFIG_KEY, k);
|
||||
}
|
||||
|
||||
export function getDefaultEndpointKey () {
|
||||
return retrieveData(C.DEFAULT_CONFIG_KEY);
|
||||
}
|
||||
|
||||
// reset stateful variables
|
||||
export function resetConfig () {
|
||||
root.authState = root.authState || {};
|
||||
root.authState.currentSettings = {};
|
||||
root.authState.currentEndpoint = {};
|
||||
destroySession();
|
||||
}
|
||||
|
||||
|
||||
export function destroySession () {
|
||||
var sessionKeys = [
|
||||
C.SAVED_CREDS_KEY,
|
||||
C.SAVED_CONFIG_KEY,
|
||||
C.SAVED_USER_INFO
|
||||
];
|
||||
|
||||
for (var key in sessionKeys) {
|
||||
key = sessionKeys[key];
|
||||
|
||||
// kill all local storage keys
|
||||
if (root.localStorage) {
|
||||
root.localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
// remove from base path in case config is not specified
|
||||
Cookies.remove(key, {
|
||||
path: root.authState.currentSettings.cookiePath || "/"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function unescapeQuotes (val) {
|
||||
return val && val.replace(/("|')/g, "");
|
||||
}
|
||||
|
||||
export function getInitialEndpointKey () {
|
||||
return unescapeQuotes(
|
||||
Cookies.get(C.SAVED_CONFIG_KEY) ||
|
||||
(root.localStorage && root.localStorage.getItem(C.SAVED_CONFIG_KEY))
|
||||
);
|
||||
}
|
||||
|
||||
export function isApiRequest(url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getSessionEndpointKey () {
|
||||
return getCurrentEndpointKey();
|
||||
}
|
||||
|
||||
export function getSessionEndpoint (k) {
|
||||
return getCurrentEndpoint()[getSessionEndpointKey()];
|
||||
}
|
||||
|
||||
|
||||
//// only should work for current session
|
||||
//export function getSignOutUrl (endpointKey) {
|
||||
// return `${getApiUrl(endpointKey)}${getSessionEndpoint(endpointKey).signOutPath}`
|
||||
//}
|
||||
|
||||
export function getEmailSignInUrl () {
|
||||
return `${getSessionEndpoint().emailSignInPath}`
|
||||
}
|
||||
|
||||
export function getEmailSignUpUrl () {
|
||||
return getCustomersUrl();
|
||||
}
|
||||
|
||||
export function getCurrentUserUrl () {
|
||||
return `${getSessionEndpoint().currentUserPath}`
|
||||
}
|
||||
|
||||
export function getAccountsUrl () {
|
||||
return `${getSessionEndpoint().accountsPath}`
|
||||
}
|
||||
|
||||
export function getCustomersUrl () {
|
||||
return `${getSessionEndpoint().customersPath}`
|
||||
}
|
||||
|
||||
export function getTransfersUrl () {
|
||||
return `${getSessionEndpoint().transfersPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param key
|
||||
* @returns {string|string}
|
||||
*/
|
||||
export function getApiUrl(key) {
|
||||
let configKey = getSessionEndpointKey(key);
|
||||
return root.authState.currentEndpoint[configKey].apiUrl;
|
||||
}
|
||||
|
||||
export function getTokenFormat() {
|
||||
return root.authState.currentSettings.tokenFormat;
|
||||
}
|
||||
|
||||
export function persistUserData(user) {
|
||||
persistData(C.SAVED_USER_INFO, user);
|
||||
}
|
||||
|
||||
export function retrieveUserData() {
|
||||
return retrieveData(C.SAVED_USER_INFO);
|
||||
}
|
||||
|
||||
export function retrieveHeaders() {
|
||||
return retrieveData(C.SAVED_CREDS_KEY) || {};
|
||||
}
|
||||
|
||||
export function persistData (key, val) {
|
||||
val = root.JSON.stringify(val);
|
||||
|
||||
switch (root.authState.currentSettings.storage) {
|
||||
case "localStorage":
|
||||
root.localStorage.setItem(key, val);
|
||||
break;
|
||||
|
||||
default:
|
||||
Cookies.set(key, val, {
|
||||
expires: root.authState.currentSettings.cookieExpiry,
|
||||
path: root.authState.currentSettings.cookiePath
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function retrieveData (key) {
|
||||
var val = null;
|
||||
|
||||
switch (root.authState.currentSettings.storage) {
|
||||
|
||||
case "localStorage":
|
||||
val = root.localStorage && root.localStorage.getItem(key);
|
||||
break;
|
||||
|
||||
default:
|
||||
val = Cookies.get(key);
|
||||
break;
|
||||
}
|
||||
|
||||
// if value is a simple string, the parser will fail. in that case, simply
|
||||
// unescape the quotes and return the string.
|
||||
try {
|
||||
// return parsed json response
|
||||
return JSON.parse(val);
|
||||
} catch (err) {
|
||||
// unescape quotes
|
||||
return unescapeQuotes(val);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import compact from '../utils/compact'
|
||||
|
||||
|
||||
export default function documentValidator(data) {
|
||||
return compact({
|
||||
titleExists:
|
||||
!data.title &&
|
||||
"You must specify a title for your document",
|
||||
})
|
||||
}
|
||||
@@ -2,18 +2,244 @@
|
||||
* Created by andrew on 12/02/16.
|
||||
*/
|
||||
import React from "react";
|
||||
import { PageHeader } from "react-bootstrap";
|
||||
//import { PageHeader } from "react-bootstrap";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
|
||||
import * as BS from "react-bootstrap";
|
||||
import Select from "react-select";
|
||||
import Spinner from "react-loader";
|
||||
import Input from "../controls/bootstrap/Input";
|
||||
import { Money, moneyText } from '../components/Money';
|
||||
import { TransfersTable } from '../components/TransfersTable';
|
||||
|
||||
import { Link, IndexLink} from "react-router";
|
||||
|
||||
|
||||
import IndexPanel from "./../components/partials/IndexPanel";
|
||||
import * as Modals from './modals';
|
||||
import * as A from '../actions/entities';
|
||||
import read from '../utils/readProp';
|
||||
|
||||
|
||||
|
||||
const resetModals = {
|
||||
showAccountModal: false
|
||||
};
|
||||
|
||||
export class Account extends React.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = { ...resetModals };
|
||||
}
|
||||
|
||||
loadAccountInfo() {
|
||||
const {
|
||||
id: customerId
|
||||
} = this.props.auth.user.attributes;
|
||||
this.props.dispatch(A.fetchOwnAccounts(customerId));
|
||||
|
||||
const { dispatch, params } = this.props;
|
||||
const { accountId } = params;
|
||||
dispatch(A.fetchAccount(accountId));
|
||||
dispatch(A.getTransfers(accountId));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadAccountInfo();
|
||||
}
|
||||
|
||||
createAccountModal() {
|
||||
this.setState({
|
||||
showAccountModal: true
|
||||
});
|
||||
}
|
||||
|
||||
createAccountModalConfirmed() {
|
||||
debugger;
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
this.setState({
|
||||
...resetModals
|
||||
});
|
||||
}
|
||||
|
||||
handleInput(key, value) {
|
||||
this.props.dispatch(A.makeTransferFormUpdate(key, value));
|
||||
}
|
||||
initiateTransfer(){
|
||||
const { dispatch, params, transfer } = this.props;
|
||||
const { accountId } = params;
|
||||
dispatch(A.makeTransfer(accountId, transfer.form ))
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
this.loadAccountInfo();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render () {
|
||||
|
||||
const { showAccountModal } = this.state;
|
||||
const { params } = this.props;
|
||||
const { loading, errors } = this.props.ui;
|
||||
const { entities, accounts } = this.props.data;
|
||||
const { accountId } = params;
|
||||
|
||||
const account = entities[accountId];
|
||||
|
||||
if (loading) {
|
||||
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
if (errors.length) {
|
||||
return (<h2>Error loading specified account</h2>);
|
||||
} else {
|
||||
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
|
||||
}
|
||||
}
|
||||
|
||||
const transferTo = [].concat(accounts.own.reduce((memo, item, idx) => {
|
||||
const { balance, title, accountId: itemAccountId } = item;
|
||||
|
||||
if (itemAccountId != accountId) {
|
||||
memo.push({
|
||||
value: itemAccountId ,
|
||||
label: `${title}: ${ moneyText(balance) }`
|
||||
});
|
||||
}
|
||||
return memo;
|
||||
}, []),
|
||||
accounts.other.reduce((memo, item, idx) => {
|
||||
if (!((item.id == accountId) || (item.accountId == accountId))) {
|
||||
memo.push({
|
||||
value: item.accountId || item.id,
|
||||
label: `${item.title}${ item.description ? ': ' + item.description.substr(0, 10): '' }`
|
||||
});
|
||||
}
|
||||
return memo;
|
||||
}, []));
|
||||
|
||||
const { title: titleRaw, description: descriptionRaw, balance } = account;
|
||||
|
||||
const title = titleRaw || '[No title]';
|
||||
const description = descriptionRaw || '[No description]';
|
||||
|
||||
const transferDisabled = this.props.transfer.loading;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader>Account page</PageHeader>
|
||||
<p>This page should only visible to authenticated users.</p>
|
||||
<PageHeader>
|
||||
Account
|
||||
<Nav pullRight={true}>
|
||||
<ButtonGroup>
|
||||
<Button bsStyle={"link"} onClick={this.createAccountModal.bind(this)}>Edit</Button>
|
||||
</ButtonGroup>
|
||||
</Nav>
|
||||
</PageHeader>
|
||||
|
||||
<Row>
|
||||
<IndexPanel header="Account Info:">
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>Title:</Col>
|
||||
<Col xs={8}><strong>{ title }</strong></Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>Balance:</Col>
|
||||
<Col xs={8}><strong><Money amount={balance} /></strong></Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>Description:</Col>
|
||||
<Col xs={8}><strong>{ description }</strong></Col>
|
||||
</Row>
|
||||
|
||||
</IndexPanel>
|
||||
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<h3>You can transfer money to accounts:</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col sm={4}>
|
||||
<label>Transfer To:</label>
|
||||
<Select
|
||||
value={read(this.props.transfer, 'form.account', '')}
|
||||
clearable={true}
|
||||
options={transferTo}
|
||||
disabled={transferDisabled}
|
||||
onChange={this.handleInput.bind(this, 'account')}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm={3}>
|
||||
<Input type="text"
|
||||
className=""
|
||||
label="Amount:"
|
||||
placeholder="Amount"
|
||||
name="amount"
|
||||
addonBefore={
|
||||
(<BS.Glyphicon glyph="usd" />)
|
||||
}
|
||||
addonAfter=".00"
|
||||
disabled={transferDisabled}
|
||||
value={read(this.props.transfer, 'form.amount', '')}
|
||||
errors={read(this.props.transfer, 'errors.amount', []) || []}
|
||||
onChange={this.handleInput.bind(this, 'amount')}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm={3}>
|
||||
<Input type="textarea"
|
||||
className=""
|
||||
label="Description:"
|
||||
placeholder="Description"
|
||||
name="description"
|
||||
disabled={transferDisabled}
|
||||
value={read(this.props.transfer, 'form.description', '') || ''}
|
||||
errors={read(this.props.transfer, 'errors.description', []) || []}
|
||||
onChange={this.handleInput.bind(this, 'description')}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm={2}>
|
||||
<br/>
|
||||
<Button bsStyle="primary"
|
||||
onClick={this.initiateTransfer.bind(this)}>Transfer</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<h3>Account History:</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<TransfersTable { ...this.props.transfers } />
|
||||
|
||||
<Modals.NewAccountModal show={showAccountModal}
|
||||
action={this.createAccountModalConfirmed.bind(this)}
|
||||
account={{ loading: true }}
|
||||
onHide={this.close.bind(this)}
|
||||
key={0} />
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({auth}) => ({auth}))(Account);
|
||||
export default connect(({
|
||||
app
|
||||
}) => ({
|
||||
auth: app.auth,
|
||||
data: app.data,
|
||||
transfers: app.data.transfers,
|
||||
ui: app.ui.account,
|
||||
transfer: app.ui.transfersMake
|
||||
}))(Account);
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Created by andrew on 17/02/16.
|
||||
*/
|
||||
import React from "react";
|
||||
import IndexPanel from "./../components/partials/IndexPanel";
|
||||
import { PageHeader, OverlayTrigger, Tooltip, Row, ButtonGroup, Table } from "react-bootstrap";
|
||||
import { Link, IndexLink} from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
import * as BSTheme from "redux-auth/bootstrap-theme";
|
||||
import * as DefaultTheme from "redux-auth";
|
||||
import Select from "react-select";
|
||||
|
||||
|
||||
class Main extends React.Component {
|
||||
updateTheme (theme) {
|
||||
//this.props.dispatch(updateDemoTheme(theme));
|
||||
}
|
||||
|
||||
updateEndpoint (endpoint) {
|
||||
//this.props.dispatch(updateDemoEndpoint(endpoint));
|
||||
}
|
||||
|
||||
render () {
|
||||
console.log("page endpoint", this.props.pageEndpoint);
|
||||
let Theme = BSTheme;
|
||||
let themePath = "/material-ui-theme";
|
||||
let endpointAttr = (this.props.pageEndpoint === "default")
|
||||
? ""
|
||||
: "endpoint=\"evilUser\"";
|
||||
|
||||
switch(this.props.theme) {
|
||||
case "default":
|
||||
Theme = DefaultTheme;
|
||||
themePath = "";
|
||||
break;
|
||||
case "bootstrap":
|
||||
Theme = BSTheme;
|
||||
themePath = "/bootstrap-theme";
|
||||
break;
|
||||
}
|
||||
|
||||
const deployTooltip = (<Tooltip>
|
||||
Create a new instance of this demo on your own Heroku server.
|
||||
</Tooltip>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader>
|
||||
Money Transfer Demo
|
||||
|
||||
<OverlayTrigger overlay={deployTooltip} placement="left">
|
||||
<a
|
||||
className="pull-right"
|
||||
href="https://heroku.com/deploy?template=https://github.com/lynndylanhurley/redux-auth-demo">
|
||||
<img src="https://www.herokucdn.com/deploy/button.svg" />
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</PageHeader>
|
||||
|
||||
<Row>
|
||||
<IndexPanel header="Current User">
|
||||
<label>current user provider</label>
|
||||
<p>{this.props.currentUserUid}</p>
|
||||
|
||||
<label>current user uid</label>
|
||||
<p>{this.props.currentUserProvider}</p>
|
||||
|
||||
<IndexLink to="/">Home</IndexLink><br/>
|
||||
<Link to="/signin">Login</Link><br/>
|
||||
<Link to="/register">Register</Link><br/>
|
||||
<Link to="/account">Account</Link><br/>
|
||||
|
||||
<label>currently selected theme</label>
|
||||
<Select
|
||||
value={this.props.theme}
|
||||
clearable={false}
|
||||
options={[
|
||||
{value: "default", label: "Default"},
|
||||
{value: "bootstrap", label: "Bootstrap"},
|
||||
{value: "materialUi", label: "Material UI"}
|
||||
]}
|
||||
onChange={this.updateTheme.bind(this)} />
|
||||
|
||||
<label>currently selected endpoint</label>
|
||||
<Select
|
||||
value={this.props.pageEndpoint}
|
||||
clearable={false}
|
||||
options={[
|
||||
{value: "default", label: "Default User Class"},
|
||||
{value: "evilUser", label: "Alternate User Class"}
|
||||
]}
|
||||
onChange={this.updateEndpoint.bind(this)} />
|
||||
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2}>
|
||||
ajax test
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Default user:</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alternate user class:</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group that includes both user classes:</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</IndexPanel>
|
||||
|
||||
<IndexPanel header="Other User"></IndexPanel>
|
||||
|
||||
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({auth, demoUi = new Map()}) => {
|
||||
return ({
|
||||
currentUserUid: auth.getIn(["user", "attributes", "provider"]) || "none",
|
||||
currentUserProvider: auth.getIn(["user", "attributes", "uid"]) || "none",
|
||||
currentUserEndpoint: auth.getIn(["user", "endpointKey"]) || "none",
|
||||
theme: demoUi.get("theme"),
|
||||
pageEndpoint: demoUi.get("endpoint")
|
||||
})
|
||||
})(Main);
|
||||
279
js-frontend/src/views/MyAccounts.js
Normal file
279
js-frontend/src/views/MyAccounts.js
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Created by andrew on 17/02/16.
|
||||
*/
|
||||
import React from "react";
|
||||
import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
|
||||
import * as BS from "react-bootstrap";
|
||||
import { Link, IndexLink} from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
//import * as DefaultTheme from "redux-auth";
|
||||
import Select from "react-select";
|
||||
import * as Modals from './modals';
|
||||
import IndexPanel from "./../components/partials/IndexPanel";
|
||||
|
||||
import * as A from '../actions/entities';
|
||||
import read from '../utils/readProp';
|
||||
import { Money } from '../components/Money';
|
||||
|
||||
|
||||
|
||||
const resetModals = {
|
||||
showAccountModal: false,
|
||||
show3rdPartyAccountModal: false,
|
||||
showDeleteAccountModal: false
|
||||
};
|
||||
|
||||
class MyAccounts extends React.Component {
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = { ...resetModals };
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {
|
||||
id: customerId
|
||||
} = this.props.auth.user.attributes;
|
||||
this.props.dispatch(A.fetchOwnAccounts(customerId));
|
||||
}
|
||||
|
||||
createAccountModal() {
|
||||
this.setState({
|
||||
showAccountModal: true
|
||||
});
|
||||
}
|
||||
|
||||
createAccountModalConfirmed(payload) {
|
||||
|
||||
const {
|
||||
id: customerId
|
||||
} = this.props.auth.user.attributes;
|
||||
|
||||
this.props.dispatch(A.accountCreate(customerId, payload))
|
||||
.then((accountId) => {
|
||||
this.close();
|
||||
return this.props.dispatch(A.fetchOwnAccounts(customerId));
|
||||
})
|
||||
.catch(err => {
|
||||
debugger;
|
||||
this.props.dispatch(A.accountCreateError(err));
|
||||
});
|
||||
}
|
||||
|
||||
create3rdPartyAccountModal() {
|
||||
this.setState({
|
||||
show3rdPartyAccountModal: true
|
||||
});
|
||||
}
|
||||
|
||||
create3rdPartyAccountModalConfirmed(payload) {
|
||||
const {
|
||||
id: customerId
|
||||
} = this.props.auth.user.attributes;
|
||||
|
||||
this.props.dispatch(A.accountRefCreate(customerId, payload))
|
||||
.then(() => {
|
||||
this.close();
|
||||
return this.props.dispatch(A.fetchOwnAccounts(customerId));
|
||||
})
|
||||
.catch(err => {
|
||||
debugger;
|
||||
this.props.dispatch(A.accountRefCreateError(err));
|
||||
});
|
||||
}
|
||||
|
||||
remove3rdPartyAccountModal(accountId, evt) {
|
||||
const account = this.props.app.entities[accountId];
|
||||
this.setState({
|
||||
accountToRemove: account,
|
||||
showDeleteAccountModal: true
|
||||
});
|
||||
}
|
||||
|
||||
remove3rdPartyAccountModalConfirmed(accountId) {
|
||||
const { customerId } = this.props;
|
||||
this.props.dispatch(A.deleteAccount(customerId, accountId))
|
||||
.then(() => {
|
||||
this.close();
|
||||
},
|
||||
err => {
|
||||
this.props.dispatch(A.errorMessageTimedOut(err && err.message || err));
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.setState({
|
||||
...resetModals
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
//const deployTooltip = (<Tooltip>
|
||||
// Create a new instance of this demo on your own Heroku server.
|
||||
//</Tooltip>);
|
||||
const user = this.props.auth.user.attributes;
|
||||
const {
|
||||
id: customerId,
|
||||
email = '',
|
||||
ssn = '',
|
||||
name = {},
|
||||
phoneNumber = '',
|
||||
address,
|
||||
toAccounts
|
||||
} = user;
|
||||
|
||||
const firstName = name.firstName || '';
|
||||
const lastName = name.lastName || '';
|
||||
|
||||
const {
|
||||
city,
|
||||
state,
|
||||
street1,
|
||||
street2,
|
||||
zipCode
|
||||
} = address;
|
||||
|
||||
const {
|
||||
showAccountModal,
|
||||
show3rdPartyAccountModal,
|
||||
showDeleteAccountModal } = this.state;
|
||||
|
||||
const { accountToRemove = null} = this.state;
|
||||
|
||||
const { error } = this.props;
|
||||
const errorLine = error ? (<BS.Panel bsStyle="danger"><strong>{ error }</strong></BS.Panel>) : [];
|
||||
|
||||
const ownAccountsData = this.props.app.accounts.own || [];
|
||||
|
||||
//accountId: "000001537c2cf075-a250093f26850000"
|
||||
//balance: 0
|
||||
//description: null
|
||||
//title: "Sample"
|
||||
|
||||
const ownAccounts = ownAccountsData.map(({
|
||||
accountId, balance, description = '', title
|
||||
}, idx) => (
|
||||
<tr key={`own_${idx}`}>
|
||||
<td key={0}><Link to={`/account/${accountId}`}>{ title }</Link>{
|
||||
(description) ? [
|
||||
(<br />),
|
||||
<span>{ description }</span>
|
||||
]: null
|
||||
}</td>
|
||||
<td key={1}><Money amount={balance} /></td>
|
||||
<td key={2}><BS.Button bsStyle={"link"} onClick={this.remove3rdPartyAccountModal.bind(this, accountId)}><BS.Glyphicon glyph="remove" /></BS.Button></td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
const refAccountsData = this.props.app.accounts.other || [];
|
||||
const refAccounts = refAccountsData.map(({
|
||||
title,
|
||||
description = '',
|
||||
id
|
||||
}, idx) => (
|
||||
<tr key={`ref_${idx}`}>
|
||||
<td key={0}><Link to={`/account/${id}`}>{ title }</Link>{
|
||||
(description) ? [
|
||||
(<br />),
|
||||
<span>{ description }</span>
|
||||
]: null
|
||||
}
|
||||
</td>
|
||||
<td key={1}></td>
|
||||
<td key={2}><Button pullRight={true} bsStyle={"link"} onClick={this.remove3rdPartyAccountModal.bind(this, id)}><BS.Glyphicon glyph="remove" /></Button>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
const accounts = (!!(ownAccounts.length + refAccounts.length)) ? [].concat(ownAccounts, refAccounts) : (<tr>
|
||||
<td colSpan={3}>No account exists: <Button bsStyle={"link"} onClick={this.createAccountModal.bind(this)}>create a new one</Button> or <Button bsStyle={"link"} onClick={this.create3rdPartyAccountModal.bind(this)}>add a recipient</Button></td>
|
||||
</tr>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader>
|
||||
My Accounts
|
||||
<Nav pullRight={true}>
|
||||
<ButtonGroup>
|
||||
<Button bsStyle={"link"} onClick={this.createAccountModal.bind(this)}>Create Account</Button>
|
||||
<Button bsStyle={"link"} onClick={this.create3rdPartyAccountModal.bind(this)}>Add 3rd Party Recipients</Button>
|
||||
</ButtonGroup>
|
||||
</Nav>
|
||||
</PageHeader>
|
||||
|
||||
{ errorLine }
|
||||
|
||||
<Row>
|
||||
<IndexPanel header="Personal Info:">
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>Customer:</Col>
|
||||
<Col xs={8}><strong>{ `${firstName} ${lastName}` }</strong></Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>Email:</Col>
|
||||
<Col xs={8}><strong>{ email }</strong></Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>Phone:</Col>
|
||||
<Col xs={8}><strong>{ phoneNumber }</strong></Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>SSN:</Col>
|
||||
<Col xs={8}><strong>{ ssn }</strong></Col>
|
||||
</Row>
|
||||
|
||||
|
||||
</IndexPanel>
|
||||
|
||||
</Row>
|
||||
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account Title</th>
|
||||
<th>Balance</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ accounts }
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
|
||||
<Modals.NewAccountModal show={showAccountModal}
|
||||
action={this.createAccountModalConfirmed.bind(this)}
|
||||
account={ this.props.app.accounts.create }
|
||||
onHide={this.close.bind(this)}
|
||||
key={0} />
|
||||
|
||||
<Modals.Add3rdPartyAccountModal show={show3rdPartyAccountModal}
|
||||
action={this.create3rdPartyAccountModalConfirmed.bind(this)}
|
||||
onHide={this.close.bind(this)}
|
||||
key={1} />
|
||||
|
||||
<Modals.RemoveAccountBookmarkModal show={showDeleteAccountModal}
|
||||
account={accountToRemove}
|
||||
action={this.remove3rdPartyAccountModalConfirmed.bind(this)}
|
||||
onHide={this.close.bind(this)}
|
||||
key={2} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ app }) => {
|
||||
return ({
|
||||
auth: app.auth,
|
||||
app: app.data,
|
||||
customerId: read(app, 'auth.user.isSignedIn', false) ? read(app, 'auth.user.attributes.id', null): null,
|
||||
error: app.ui.error
|
||||
})
|
||||
})(MyAccounts);
|
||||
@@ -6,8 +6,12 @@ import { PageHeader } from "react-bootstrap";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
//import ButtonLoader from "./ButtonLoader";
|
||||
import { Input } from "react-bootstrap";
|
||||
import ButtonLoader from "../controls/bootstrap/ButtonLoader";
|
||||
import * as BS from "react-bootstrap";
|
||||
//import ButtonLoader from "../controls/bootstrap/ButtonLoader";
|
||||
|
||||
|
||||
import {pushState} from "redux-router";
|
||||
|
||||
|
||||
//export {bootstrap, materialUi} from "./views";
|
||||
|
||||
@@ -16,9 +20,25 @@ import ButtonLoader from "../controls/bootstrap/ButtonLoader";
|
||||
//import { EmailSignInForm } from "redux-auth/bootstrap-theme";
|
||||
import EmailSignInForm from "../controls/bootstrap/EmailSignInForm";
|
||||
|
||||
|
||||
export class SignIn extends React.Component {
|
||||
|
||||
checkRedirect(props) {
|
||||
if (props.auth.user.isSignedIn) {
|
||||
props.dispatch(pushState(null, props.location.query.next));
|
||||
//// redirect to login and add next param so we can redirect again after login
|
||||
//const redirectAfterLogin = this.props.location.pathname;
|
||||
//this.props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`));
|
||||
}
|
||||
}
|
||||
componentWillMount() {
|
||||
this.checkRedirect(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.checkRedirect(nextProps);
|
||||
|
||||
}
|
||||
|
||||
render () {
|
||||
const signInProps = {
|
||||
inputProps: {
|
||||
@@ -31,13 +51,20 @@ export class SignIn extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
return <EmailSignInForm />;
|
||||
//return (
|
||||
// <div>
|
||||
// <PageHeader>Sign In First</PageHeader>
|
||||
// <p>Unauthenticated users can't access the account page.</p>
|
||||
// </div>
|
||||
//);
|
||||
return (
|
||||
<BS.Well>
|
||||
<PageHeader>Sign In</PageHeader>
|
||||
<EmailSignInForm {...this.props} />
|
||||
</BS.Well>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default connect(({routes}) => ({routes}))(SignIn);
|
||||
export default connect(({
|
||||
//dispatch,
|
||||
routes,
|
||||
app
|
||||
}) => ({
|
||||
//dispatch,
|
||||
routes,
|
||||
auth: app.auth
|
||||
}))(SignIn);
|
||||
|
||||
@@ -4,25 +4,51 @@
|
||||
import React from "react";
|
||||
//import { PageHeader } from "react-bootstrap";
|
||||
import { connect } from "react-redux";
|
||||
import { pushState } from 'redux-router';
|
||||
import read from '../utils/readProp';
|
||||
|
||||
import { PageHeader, OverlayTrigger, Tooltip, Row, ButtonGroup, Table } from "react-bootstrap";
|
||||
import * as BS from "react-bootstrap";
|
||||
import { Link, IndexLink} from "react-router";
|
||||
|
||||
//import { EmailSignUpForm } from "redux-auth/bootstrap-theme"
|
||||
import EmailSignUpForm from "../controls/bootstrap/EmailSignUpForm";
|
||||
|
||||
export class SignUp extends React.Component {
|
||||
|
||||
checkRedirect(props) {
|
||||
if (props.isAuthenticated) {
|
||||
// redirect to login and add next param so we can redirect again after login
|
||||
// const redirectAfterLogin = props.location.pathname;
|
||||
props.dispatch(pushState(null, `/`));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.checkRedirect(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.checkRedirect(nextProps);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader>
|
||||
Register
|
||||
</PageHeader>
|
||||
<Row>
|
||||
<EmailSignUpForm endpoint="default" />
|
||||
</Row>
|
||||
<BS.Well>
|
||||
<EmailSignUpForm />
|
||||
</BS.Well>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
export default connect(({routes}) => ({routes}))(SignUp);
|
||||
export default connect(({
|
||||
routes,
|
||||
app
|
||||
}) => ({routes,
|
||||
isAuthenticated: read(app, 'auth.user.isSignedIn', false)
|
||||
}))(SignUp);
|
||||
190
js-frontend/src/views/modals/Add3rdPartyAccountModal.js
Normal file
190
js-frontend/src/views/modals/Add3rdPartyAccountModal.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Created by andrew on 20/02/16.
|
||||
*/
|
||||
import React from "react";
|
||||
// import { PageHeader, OverlayTrigger, Modal, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
|
||||
import * as BS from "react-bootstrap";
|
||||
import ButtonLoader from "../../controls/bootstrap/ButtonLoader";
|
||||
import Input from "../../controls/bootstrap/Input";
|
||||
import AuxErrorLabel from "../../controls/bootstrap/AuxErrorLabel";
|
||||
import read from '../../utils/readProp';
|
||||
|
||||
import { Link, IndexLink} from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
import Select from "react-select";
|
||||
|
||||
import * as A from '../../actions/entities';
|
||||
|
||||
const formValidation = (payload) => ['owner', 'account', 'title', 'description'].reduce((memo, prop) => {
|
||||
let result = [];
|
||||
const value = (payload[prop] || '').replace(/(^\s+)|(\s+$)/g, '');
|
||||
|
||||
switch (prop) {
|
||||
case 'owner':
|
||||
case 'account':
|
||||
case 'title':
|
||||
if (/^$/.test(value)) {
|
||||
result.push('required');
|
||||
}
|
||||
}
|
||||
|
||||
switch (prop) {
|
||||
case 'description':
|
||||
if (value.length > 400) {
|
||||
result.push('need to less than 400 symbols long');
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length) {
|
||||
memo[prop] = result;
|
||||
memo.hasErrors = true;
|
||||
}
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
|
||||
export class Add3rdPartyAccountModal extends React.Component {
|
||||
|
||||
handleInput(key, value) {
|
||||
this.props.dispatch(A.accountRefCreateFormUpdate(key, value));
|
||||
switch(key) {
|
||||
case 'owner':
|
||||
if (value) {
|
||||
this.props.dispatch(A.createRefAccountLookup(value));
|
||||
} else {
|
||||
this.props.dispatch(A.createRefAccountLookupComplete([]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
const payload = { ...this.props.data.form };
|
||||
|
||||
const validationErrors = formValidation(payload);
|
||||
if (validationErrors.hasErrors) {
|
||||
this.props.dispatch(A.accountRefCreateError(validationErrors));
|
||||
return;
|
||||
}
|
||||
|
||||
const { action } = this.props;
|
||||
|
||||
if (action) {
|
||||
action(payload);
|
||||
}
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.props.dispatch(A.accountRefCreateComplete({}));
|
||||
if (this.props.onHide) {
|
||||
this.props.onHide();
|
||||
}
|
||||
}
|
||||
|
||||
getOwnersOptions(input) {
|
||||
if (!input) {
|
||||
return Promise.resolve({ options: [] });
|
||||
}
|
||||
return this.props.dispatch(A.createRefOwnerLookup(input));
|
||||
}
|
||||
|
||||
render() {
|
||||
const disabled = read(this.props.data, 'loading', false);
|
||||
|
||||
const ownersLoading = read(this.props.data, 'ownersLookup.loading', false);
|
||||
|
||||
const formErrors = read(this.props.data, 'errors.errors', '');
|
||||
|
||||
return (
|
||||
<BS.Modal show={this.props.show} onHide={this.onHide.bind(this)} key={1}>
|
||||
<BS.Modal.Header closeButton>
|
||||
<BS.Modal.Title>Add 3rd Party Account</BS.Modal.Title>
|
||||
</BS.Modal.Header>
|
||||
<BS.Modal.Body>
|
||||
<form>
|
||||
<div className="form-group" style={{
|
||||
display: formErrors ? 'block' : 'none'
|
||||
}}>
|
||||
<AuxErrorLabel
|
||||
label="Form:"
|
||||
errors={formErrors.length ? [formErrors] : [] }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label>Owner:</label>
|
||||
<div className="form-group">
|
||||
<Select
|
||||
name="owner"
|
||||
onBlurResetsInput={false}
|
||||
asyncOptions={this.getOwnersOptions.bind(this)}
|
||||
matchProp="label"
|
||||
onChange={this.handleInput.bind(this, 'owner')}
|
||||
value={read(this.props.data, 'form.owner', '')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<AuxErrorLabel
|
||||
label="Owner:"
|
||||
errors={read(this.props.data, 'errors.owner', [])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label>Account:</label>
|
||||
<div className="form-group">
|
||||
<Select
|
||||
name="account"
|
||||
value={read(this.props.data, 'form.account', '')}
|
||||
disabled={ownersLoading || disabled}
|
||||
clearable={false}
|
||||
searchable={false}
|
||||
options={read(this.props.data, 'accountsLookup.options', [])}
|
||||
onChange={this.handleInput.bind(this, 'account')} />
|
||||
<AuxErrorLabel
|
||||
label="Owner:"
|
||||
errors={read(this.props.data, 'errors.account', [])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input type="text"
|
||||
className="account-create-description"
|
||||
label="Title:"
|
||||
placeholder="Title"
|
||||
name="title"
|
||||
disabled={disabled}
|
||||
value={read(this.props.data, 'form.title', '')}
|
||||
errors={read(this.props.data, 'errors.title', [])}
|
||||
onChange={this.handleInput.bind(this, 'title')} />
|
||||
|
||||
<Input type="textarea"
|
||||
className="account-create-description"
|
||||
label="Description:"
|
||||
placeholder="Description"
|
||||
name="description"
|
||||
disabled={disabled}
|
||||
value={read(this.props.data, 'form.description', '')}
|
||||
errors={read(this.props.data, 'errors.description', [])}
|
||||
onChange={this.handleInput.bind(this, 'description')} />
|
||||
</form>
|
||||
</BS.Modal.Body>
|
||||
<BS.Modal.Footer>
|
||||
<BS.Button onClick={this.onHide.bind(this)}>Cancel</BS.Button>
|
||||
<ButtonLoader loading={read(this.props.data, 'loading', false)}
|
||||
type="submit"
|
||||
bsStyle="primary"
|
||||
icon={<BS.Glyphicon glyph="plus" />}
|
||||
disabled={disabled}
|
||||
onClick={this.handleSubmit.bind(this)}>
|
||||
Add
|
||||
</ButtonLoader>
|
||||
</BS.Modal.Footer>
|
||||
</BS.Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ app }) => ({
|
||||
ui: app.ui.bookmarkAccount,
|
||||
data: app.data.bookmarkAccount
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Add3rdPartyAccountModal);
|
||||
152
js-frontend/src/views/modals/NewAccountModal.js
Normal file
152
js-frontend/src/views/modals/NewAccountModal.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Created by andrew on 20/02/16.
|
||||
*/
|
||||
import React, { PropTypes } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as BS from "react-bootstrap";
|
||||
import Input from "../../controls/bootstrap/Input";
|
||||
import ButtonLoader from "../../controls/bootstrap/ButtonLoader";
|
||||
//import { PageHeader, OverlayTrigger, Modal, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
|
||||
|
||||
import { Link, IndexLink} from "react-router";
|
||||
import read from '../../utils/readProp';
|
||||
|
||||
import { accountCreateFormUpdate, accountCreateError } from '../../actions/entities';
|
||||
|
||||
const formValidation = (payload) => ['title', 'balance', 'description'].reduce((memo, prop) => {
|
||||
let result = [];
|
||||
const value = (payload[prop] || '').replace(/(^\s+)|(\s+$)/g, '');
|
||||
|
||||
switch (prop) {
|
||||
case 'title':
|
||||
case 'balance':
|
||||
if (/^$/.test(value)) {
|
||||
result.push('required');
|
||||
}
|
||||
}
|
||||
|
||||
switch (prop) {
|
||||
case 'balance':
|
||||
if (!/\d+/.test(value)) {
|
||||
result.push('need to be a number');
|
||||
}
|
||||
}
|
||||
|
||||
switch (prop) {
|
||||
case 'description':
|
||||
if (value.length > 400) {
|
||||
result.push('need to less than 400 symbols long');
|
||||
}
|
||||
|
||||
}
|
||||
if (result.length) {
|
||||
memo[prop] = result;
|
||||
memo.hasErrors = true;
|
||||
}
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
export class NewAccountModal extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
action: PropTypes.func,
|
||||
account: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const payload = { ...this.props.account.form };
|
||||
|
||||
const validationErrors = formValidation(payload);
|
||||
if (validationErrors.hasErrors) {
|
||||
this.props.dispatch(accountCreateError(validationErrors));
|
||||
return;
|
||||
}
|
||||
|
||||
const { action } = this.props;
|
||||
|
||||
if (action) {
|
||||
action(payload);
|
||||
}
|
||||
}
|
||||
|
||||
handleInput(key, val) {
|
||||
this.props.dispatch(accountCreateFormUpdate(key, val));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const disabled = (
|
||||
this.props.account.loading
|
||||
);
|
||||
|
||||
const actionLabel = 'Create';
|
||||
|
||||
return (<BS.Modal show={this.props.show} onHide={this.props.onHide} key={0}>
|
||||
<BS.Modal.Header closeButton>
|
||||
<BS.Modal.Title>New Account</BS.Modal.Title>
|
||||
</BS.Modal.Header>
|
||||
<BS.Modal.Body>
|
||||
<form className='account-create-form clearfix'
|
||||
onSubmit={this.handleSubmit.bind(this)}>
|
||||
|
||||
<Input type="text"
|
||||
className="account-create-title"
|
||||
label="Title"
|
||||
placeholder="Title"
|
||||
name="title"
|
||||
disabled={disabled}
|
||||
value={read(this.props.account, 'form.title', '')}
|
||||
errors={read(this.props.account, 'errors.title', [])}
|
||||
onChange={this.handleInput.bind(this, "title")}
|
||||
/>
|
||||
|
||||
<Input type="text"
|
||||
className="account-create-balance"
|
||||
label="Balance"
|
||||
placeholder="Balance"
|
||||
name="balance"
|
||||
addonBefore={
|
||||
(<BS.Glyphicon glyph="usd" />)
|
||||
}
|
||||
addonAfter=".00"
|
||||
disabled={disabled}
|
||||
value={read(this.props.account, 'form.balance', '')}
|
||||
errors={read(this.props.account, 'errors.balance', [])}
|
||||
onChange={this.handleInput.bind(this, 'balance')}
|
||||
/>
|
||||
|
||||
<Input type="textarea"
|
||||
className="account-create-description"
|
||||
label="Description"
|
||||
placeholder="Description"
|
||||
name="description"
|
||||
disabled={disabled}
|
||||
value={read(this.props.account, 'form.description', '') || ''}
|
||||
errors={read(this.props.account, 'errors.description', [])}
|
||||
onChange={this.handleInput.bind(this, 'description')}
|
||||
/>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
</BS.Modal.Body>
|
||||
<BS.Modal.Footer>
|
||||
<BS.Button onClick={this.props.onHide}>Cancel</BS.Button>
|
||||
<ButtonLoader loading={read(this.props.account, 'loading', false)}
|
||||
type="submit"
|
||||
bsStyle="primary"
|
||||
icon={<BS.Glyphicon glyph="plus" />}
|
||||
disabled={disabled}
|
||||
onClick={this.handleSubmit.bind(this)}
|
||||
>
|
||||
{actionLabel}
|
||||
</ButtonLoader>
|
||||
</BS.Modal.Footer>
|
||||
</BS.Modal>);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(NewAccountModal);
|
||||
68
js-frontend/src/views/modals/RemoveAccountModal.js
Normal file
68
js-frontend/src/views/modals/RemoveAccountModal.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Created by andrew on 20/02/16.
|
||||
*/
|
||||
import React, { PropTypes } from "react";
|
||||
import * as BS from 'react-bootstrap';
|
||||
import { PageHeader, OverlayTrigger, Modal, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
|
||||
import { Link, IndexLink} from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
import Select from "react-select";
|
||||
|
||||
export class RemoveAccountBookmarkModal extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
action: PropTypes.func,
|
||||
account: PropTypes.object
|
||||
};
|
||||
|
||||
handleAction(evt) {
|
||||
evt.preventDefault();
|
||||
const { action } = this.props;
|
||||
const { account } = this.props;
|
||||
const {
|
||||
id,
|
||||
accountId
|
||||
} = account || {};
|
||||
|
||||
if (action) {
|
||||
action(id || accountId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account } = this.props;
|
||||
|
||||
const { title: titleRaw,
|
||||
description: descriptionRaw,
|
||||
balance: balanceRaw,
|
||||
id,
|
||||
accountId } = account || {};
|
||||
|
||||
const entityId = id || accountId;
|
||||
|
||||
const title = titleRaw || '[No title]';
|
||||
const balance = ((balanceRaw > 0 && balanceRaw < 1) ? '$0' : '$') + Number(balanceRaw).toFixed(2);
|
||||
const description = descriptionRaw || '[No description]';
|
||||
|
||||
return (<Modal show={this.props.show} onHide={this.props.onHide} key={0}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Remove Account Bookmark</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
|
||||
<form className="form-horizontal">
|
||||
<BS.FormControls.Static label="Title" labelClassName="col-xs-2" wrapperClassName="col-xs-10">{ title }</BS.FormControls.Static>
|
||||
<BS.FormControls.Static label="Balance" labelClassName="col-xs-2" wrapperClassName="col-xs-10">{ balance }</BS.FormControls.Static>
|
||||
<BS.FormControls.Static label="Description" labelClassName="col-xs-2" wrapperClassName="col-xs-10">{ description }</BS.FormControls.Static>
|
||||
</form>
|
||||
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.props.onHide}>Cancel</Button>
|
||||
<Button bsStyle="danger" eventKey={ entityId } onClick={this.handleAction.bind(this)}>Remove</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>);
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoveAccountBookmarkModal;
|
||||
6
js-frontend/src/views/modals/index.js
Normal file
6
js-frontend/src/views/modals/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Created by andrew on 20/02/16.
|
||||
*/
|
||||
export { default as Add3rdPartyAccountModal } from './Add3rdPartyAccountModal';
|
||||
export { default as NewAccountModal } from './NewAccountModal';
|
||||
export { default as RemoveAccountBookmarkModal } from './RemoveAccountModal';
|
||||
@@ -9,7 +9,7 @@ export default (DEBUG, PATH, PORT=3000) => ({
|
||||
] : []).concat([
|
||||
'./src/main.less',
|
||||
'babel-polyfill',
|
||||
'./src/client',
|
||||
'./src/client'
|
||||
]),
|
||||
|
||||
output: {
|
||||
@@ -22,20 +22,22 @@ export default (DEBUG, PATH, PORT=3000) => ({
|
||||
debug: DEBUG,
|
||||
|
||||
// For options, see http://webpack.github.io/docs/configuration.html#devtool
|
||||
devtool: DEBUG && "eval",
|
||||
//devtool: DEBUG && "eval",
|
||||
devtool: DEBUG && "cheap-module-eval-source-map",
|
||||
|
||||
module: {
|
||||
loaders: [
|
||||
// Load ES6/JSX
|
||||
{ test: /\.jsx?$/,
|
||||
include: [
|
||||
path.resolve(__dirname, "src"),
|
||||
path.resolve(__dirname, "node_modules/redux-auth/src/views/bootstrap")
|
||||
path.resolve(__dirname, "src")
|
||||
//,
|
||||
//path.resolve(__dirname, "node_modules/redux-auth/src/views/bootstrap")
|
||||
],
|
||||
loader: "babel-loader",
|
||||
query: {
|
||||
plugins: ['transform-runtime'],
|
||||
presets: ['es2015', 'stage-0', 'react'],
|
||||
presets: ['es2015', 'react', 'stage-0']
|
||||
}
|
||||
},
|
||||
|
||||
@@ -65,7 +67,9 @@ export default (DEBUG, PATH, PORT=3000) => ({
|
||||
},
|
||||
|
||||
plugins: DEBUG
|
||||
? []
|
||||
? [
|
||||
//new
|
||||
]
|
||||
: [
|
||||
new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}),
|
||||
new ExtractTextPlugin("style.css", {allChunks: false}),
|
||||
@@ -75,11 +79,11 @@ export default (DEBUG, PATH, PORT=3000) => ({
|
||||
mangle: {screw_ie8: true, keep_fnames: true}
|
||||
}),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new webpack.optimize.AggressiveMergingPlugin(),
|
||||
new webpack.optimize.AggressiveMergingPlugin()
|
||||
],
|
||||
|
||||
resolveLoader: {
|
||||
root: path.join(__dirname, "node_modules"),
|
||||
root: path.join(__dirname, "node_modules")
|
||||
},
|
||||
|
||||
resolve: {
|
||||
@@ -94,6 +98,6 @@ export default (DEBUG, PATH, PORT=3000) => ({
|
||||
},
|
||||
|
||||
// Allow to omit extensions when requiring these files
|
||||
extensions: ["", ".js", ".jsx"],
|
||||
extensions: ["", ".js", ".jsx"]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div id="react-app"></div>
|
||||
|
||||
<!-- inject:app:js -->
|
||||
<script src="/main-9aec0a6fd78376cd52bb.js"></script>
|
||||
<script src="/main-6840e3c9a53c4afc34e9.js"></script>
|
||||
<!-- endinject -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
48
prebuilt-web-client/main-6840e3c9a53c4afc34e9.js
Normal file
48
prebuilt-web-client/main-6840e3c9a53c4afc34e9.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
@import url(http://fonts.googleapis.com/css?family=Roboto:300,400,500);*,:after,:before{box-sizing:border-box}#react-app,body,html,main{position:relative;height:100%;min-height:100%}*{margin:0}body,html,main{font-family:Roboto}body{-webkit-tap-highlight-color:transparent}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}
|
||||
@import url(http://fonts.googleapis.com/css?family=Roboto:300,400,500);*,:after,:before{box-sizing:border-box}#react-app,body,html,main{position:relative;height:100%;min-height:100%}*{margin:0}body,html,main{font-family:Roboto}body{-webkit-tap-highlight-color:transparent;padding-bottom:50px;height:auto}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}.footer-navigation{height:1px}.footer-navigation>.container{height:100%}.footer-navigation>.container>*{top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.page-header{padding-bottom:9px;margin:0 0 20px;border-bottom:1px solid #eee}h1{margin-top:.5em}
|
||||
Reference in New Issue
Block a user