From 484b140a6f486c939fc0d3c51c509dc823645d2f Mon Sep 17 00:00:00 2001 From: David Curras Date: Mon, 27 Nov 2017 15:03:32 -0300 Subject: [PATCH] Redux modules refactor and integration with api helpers --- server/plugins/routes/default.js | 6 +- src/components/examples/Home/index.js | 7 +- .../examples/Profile/Profile.ios.js | 4 +- src/components/examples/Profile/index.js | 6 +- .../examples/Template/Template.ios.js | 4 +- src/components/examples/Todos/Todos.ios.js | 4 +- src/components/examples/Todos/Todos_test.js | 2 +- src/components/examples/Todos/index.js | 4 +- .../shared/Modal/Login/Login.index.js | 13 +- src/helpers/{api/index.js => api.js} | 0 .../{api/index_test.js => api_test.js} | 3 +- src/index.browser.js | 2 +- src/index.ios.js | 4 +- src/redux/epics/fetchProfile.js | 4 +- src/redux/modules/auth.js | 137 ------------------ src/redux/modules/auth/_test.js | 127 ++++++++++++++++ src/redux/modules/auth/actions.js | 35 +++++ .../api/auth.js => redux/modules/auth/api.js} | 15 +- src/redux/modules/auth/consts.js | 3 + src/redux/modules/auth/initialState.js | 11 ++ src/redux/modules/auth/mocks.js | 11 ++ src/redux/modules/auth/reducer.js | 69 +++++++++ src/redux/modules/auth/types.js | 12 ++ src/redux/modules/env/actions.js | 1 + src/redux/modules/env/consts.js | 1 + src/redux/modules/env/initialState.js | 4 + src/redux/modules/{env.js => env/reducer.js} | 11 +- src/redux/modules/env/types.js | 1 + src/redux/modules/examples/index.js | 10 +- src/redux/modules/examples/profile.js | 115 --------------- .../__snapshots__/_test.js.snap} | 0 .../{profile_test.js => profile/_test.js} | 4 +- src/redux/modules/examples/profile/actions.js | 21 +++ src/redux/modules/examples/profile/api.js | 1 + src/redux/modules/examples/profile/consts.js | 4 + .../modules/examples/profile/initialState.js | 21 +++ src/redux/modules/examples/profile/mocks.js | 1 + src/redux/modules/examples/profile/reducer.js | 44 ++++++ src/redux/modules/examples/profile/types.js | 20 +++ src/redux/modules/examples/todos.js | 117 --------------- src/redux/modules/examples/todos/_test.js | 123 ++++++++++++++++ src/redux/modules/examples/todos/actions.js | 28 ++++ src/redux/modules/examples/todos/api.js | 14 ++ src/redux/modules/examples/todos/consts.js | 10 ++ .../modules/examples/todos/initialState.js | 12 ++ .../modules/examples/todos/mocks.js} | 18 +-- src/redux/modules/examples/todos/reducer.js | 70 +++++++++ src/redux/modules/examples/todos/types.js | 14 ++ src/redux/modules/index.js | 19 ++- src/redux/modules/init.js | 61 -------- src/redux/modules/init/_test.js | 31 ++++ src/redux/modules/init/actions.js | 7 + src/redux/modules/init/api.js | 1 + src/redux/modules/init/consts.js | 3 + src/redux/modules/init/initialState.js | 7 + src/redux/modules/init/mocks.js | 1 + src/redux/modules/init/reducer.js | 31 ++++ src/redux/modules/init/types.js | 7 + src/redux/modules/init_test.js | 47 ------ src/redux/modules/request.js | 24 --- src/redux/modules/request/actions.js | 1 + src/redux/modules/request/consts.js | 1 + src/redux/modules/request/initialState.js | 4 + src/redux/modules/request/reducer.js | 16 ++ src/redux/modules/request/types.js | 4 + src/redux/modules/ui/actions.js | 9 ++ src/redux/modules/ui/consts.js | 6 + src/redux/modules/ui/initialState.js | 11 ++ src/redux/modules/{ui.js => ui/reducer.js} | 48 ++---- src/redux/modules/ui/types.js | 8 + src/selectors/getVisibleTodos.js | 2 +- 71 files changed, 875 insertions(+), 622 deletions(-) rename src/helpers/{api/index.js => api.js} (100%) rename src/helpers/{api/index_test.js => api_test.js} (97%) delete mode 100644 src/redux/modules/auth.js create mode 100644 src/redux/modules/auth/_test.js create mode 100644 src/redux/modules/auth/actions.js rename src/{helpers/api/auth.js => redux/modules/auth/api.js} (79%) create mode 100644 src/redux/modules/auth/consts.js create mode 100644 src/redux/modules/auth/initialState.js create mode 100644 src/redux/modules/auth/mocks.js create mode 100644 src/redux/modules/auth/reducer.js create mode 100644 src/redux/modules/auth/types.js create mode 100644 src/redux/modules/env/actions.js create mode 100644 src/redux/modules/env/consts.js create mode 100644 src/redux/modules/env/initialState.js rename src/redux/modules/{env.js => env/reducer.js} (55%) create mode 100644 src/redux/modules/env/types.js delete mode 100644 src/redux/modules/examples/profile.js rename src/redux/modules/examples/{__snapshots__/profile_test.js.snap => profile/__snapshots__/_test.js.snap} (100%) rename src/redux/modules/examples/{profile_test.js => profile/_test.js} (82%) create mode 100644 src/redux/modules/examples/profile/actions.js create mode 100644 src/redux/modules/examples/profile/api.js create mode 100644 src/redux/modules/examples/profile/consts.js create mode 100644 src/redux/modules/examples/profile/initialState.js create mode 100644 src/redux/modules/examples/profile/mocks.js create mode 100644 src/redux/modules/examples/profile/reducer.js create mode 100644 src/redux/modules/examples/profile/types.js delete mode 100644 src/redux/modules/examples/todos.js create mode 100644 src/redux/modules/examples/todos/_test.js create mode 100644 src/redux/modules/examples/todos/actions.js create mode 100644 src/redux/modules/examples/todos/api.js create mode 100644 src/redux/modules/examples/todos/consts.js create mode 100644 src/redux/modules/examples/todos/initialState.js rename src/{helpers/api/examples/todos.js => redux/modules/examples/todos/mocks.js} (61%) create mode 100644 src/redux/modules/examples/todos/reducer.js create mode 100644 src/redux/modules/examples/todos/types.js delete mode 100644 src/redux/modules/init.js create mode 100644 src/redux/modules/init/_test.js create mode 100644 src/redux/modules/init/actions.js create mode 100644 src/redux/modules/init/api.js create mode 100644 src/redux/modules/init/consts.js create mode 100644 src/redux/modules/init/initialState.js create mode 100644 src/redux/modules/init/mocks.js create mode 100644 src/redux/modules/init/reducer.js create mode 100644 src/redux/modules/init/types.js delete mode 100644 src/redux/modules/init_test.js delete mode 100644 src/redux/modules/request.js create mode 100644 src/redux/modules/request/actions.js create mode 100644 src/redux/modules/request/consts.js create mode 100644 src/redux/modules/request/initialState.js create mode 100644 src/redux/modules/request/reducer.js create mode 100644 src/redux/modules/request/types.js create mode 100644 src/redux/modules/ui/actions.js create mode 100644 src/redux/modules/ui/consts.js create mode 100644 src/redux/modules/ui/initialState.js rename src/redux/modules/{ui.js => ui/reducer.js} (50%) create mode 100644 src/redux/modules/ui/types.js diff --git a/server/plugins/routes/default.js b/server/plugins/routes/default.js index de0a5980..9530d852 100644 --- a/server/plugins/routes/default.js +++ b/server/plugins/routes/default.js @@ -7,11 +7,11 @@ import { StaticRouter } from 'react-router' import { Base64 } from 'js-base64' import { getAssets, getCss } from 'server/utils' import configureStore from 'src/redux/store' -import { initialState as authInitialState } from 'src/redux/modules/auth' +import authInitialState from 'src/redux/modules/auth/initialState' import Routes from 'src/routes' import env from 'config/env' import vars from 'config/variables' -import { getTodos } from 'src/redux/modules/examples/todos' +import { getTodos } from 'src/redux/modules/examples/todos/actions' // Material-UI import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' @@ -32,7 +32,7 @@ export default { module.hot.accept([ 'src/routes', 'src/redux/store', - 'src/redux/modules/auth', + 'src/redux/modules/auth/reducer', 'config/env', 'config/variables', 'server/utils', diff --git a/src/components/examples/Home/index.js b/src/components/examples/Home/index.js index 0e18bb64..4aee6bca 100644 --- a/src/components/examples/Home/index.js +++ b/src/components/examples/Home/index.js @@ -2,12 +2,10 @@ import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import { withRouter } from 'react-router' -import { logout } from 'src/redux/modules/auth' +import { logout } from 'src/redux/modules/auth/actions' import Home from './Home' - -type StateProps = { -} +type StateProps = {} const mapStateToProps = (): StateProps => ({}) @@ -19,6 +17,5 @@ type DispatchProps = { const mapDispatchToProps = (dispatch: GlobalDispatch<*>): DispatchProps => bindActionCreators({ logout }, dispatch) - export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Home)) export type ReduxProps = StateProps & DispatchProps diff --git a/src/components/examples/Profile/Profile.ios.js b/src/components/examples/Profile/Profile.ios.js index 9f3584f7..11b0abbe 100644 --- a/src/components/examples/Profile/Profile.ios.js +++ b/src/components/examples/Profile/Profile.ios.js @@ -4,14 +4,14 @@ * @flow */ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import { StyleSheet, Text, View, } from 'react-native' -class Home extends Component { +class Home extends PureComponent { render () { diff --git a/src/components/examples/Profile/index.js b/src/components/examples/Profile/index.js index 2087fbd4..425a1018 100644 --- a/src/components/examples/Profile/index.js +++ b/src/components/examples/Profile/index.js @@ -1,11 +1,10 @@ // @flow import { connect } from 'react-redux' -import { fetchProfile } from 'src/redux/modules/examples/profile' -import type { Profile as ProfileModel } from 'src/redux/modules/examples/profile' +import { fetchProfile } from 'src/redux/modules/examples/profile/actions' +import type { Profile as ProfileModel } from 'src/redux/modules/examples/profile/types' import type { RootReducerState } from 'src/redux/modules' import Profile from './Profile' - type StateProps = { me: ProfileModel, error: ?string, @@ -18,7 +17,6 @@ const mapStateToProps = ( error: profile.error, }) - type DispatchProps = { fetchProfile: Function, } diff --git a/src/components/examples/Template/Template.ios.js b/src/components/examples/Template/Template.ios.js index 9f3584f7..11b0abbe 100644 --- a/src/components/examples/Template/Template.ios.js +++ b/src/components/examples/Template/Template.ios.js @@ -4,14 +4,14 @@ * @flow */ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import { StyleSheet, Text, View, } from 'react-native' -class Home extends Component { +class Home extends PureComponent { render () { diff --git a/src/components/examples/Todos/Todos.ios.js b/src/components/examples/Todos/Todos.ios.js index 9f3584f7..11b0abbe 100644 --- a/src/components/examples/Todos/Todos.ios.js +++ b/src/components/examples/Todos/Todos.ios.js @@ -4,14 +4,14 @@ * @flow */ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import { StyleSheet, Text, View, } from 'react-native' -class Home extends Component { +class Home extends PureComponent { render () { diff --git a/src/components/examples/Todos/Todos_test.js b/src/components/examples/Todos/Todos_test.js index 0905d1ad..5896bccd 100644 --- a/src/components/examples/Todos/Todos_test.js +++ b/src/components/examples/Todos/Todos_test.js @@ -5,7 +5,7 @@ import React from 'react' import { shallow } from 'enzyme' -import { initialState } from 'src/redux/modules/examples/todos' +import initialState from 'src/redux/modules/examples/todos/initialState' import Todos from './Todos' const mockProps = { diff --git a/src/components/examples/Todos/index.js b/src/components/examples/Todos/index.js index 83b6625e..40e8aa68 100644 --- a/src/components/examples/Todos/index.js +++ b/src/components/examples/Todos/index.js @@ -5,9 +5,9 @@ import { getTodos, setFilterCurrent, setFilterDone, -} from 'src/redux/modules/examples/todos' +} from 'src/redux/modules/examples/todos/actions' import getVisibleTodos from 'src/selectors/getVisibleTodos' -import type { Todo } from 'src/redux/modules/examples/todos' +import type { Todo } from 'src/redux/modules/examples/todos/types' import type { RootReducerState } from 'src/redux/modules' import Todos from './Todos' diff --git a/src/components/shared/Modal/Login/Login.index.js b/src/components/shared/Modal/Login/Login.index.js index 05c16b4f..8f05b895 100644 --- a/src/components/shared/Modal/Login/Login.index.js +++ b/src/components/shared/Modal/Login/Login.index.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux' import { reduxForm } from 'redux-form' import { withRouter } from 'react-router' import { parse } from 'qs' -import { login } from 'src/redux/modules/auth' +import { login } from 'src/redux/modules/auth/actions' import type { RootReducerState } from 'src/redux/modules' import Login from './Login' @@ -29,11 +29,12 @@ const onSubmit = (values, dispatch, props): Promise<*> => { const { history } = props const search = parse(props.location.search.substr(1)) - return dispatch(login({ - username: usernameInput, - password: passwordInput, - })) - .then(() => history.push({ pathname: search.redirect })) + return dispatch( + login({ + username: usernameInput, + password: passwordInput, + }) + ).then(() => history.push({ pathname: search.redirect })) } diff --git a/src/helpers/api/index.js b/src/helpers/api.js similarity index 100% rename from src/helpers/api/index.js rename to src/helpers/api.js diff --git a/src/helpers/api/index_test.js b/src/helpers/api_test.js similarity index 97% rename from src/helpers/api/index_test.js rename to src/helpers/api_test.js index 4c2e1698..454bf20f 100644 --- a/src/helpers/api/index_test.js +++ b/src/helpers/api_test.js @@ -1,8 +1,7 @@ // @flow import Nock from 'nock' import _get from 'lodash/get' -import { get, post, patch, put, del, mock } from './' - +import { get, post, patch, put, del, mock } from './api' it('get fetches via GET', async () => { diff --git a/src/index.browser.js b/src/index.browser.js index d32cecf0..1672d4f3 100644 --- a/src/index.browser.js +++ b/src/index.browser.js @@ -9,7 +9,7 @@ import Rollbar from 'rollbar/dist/rollbar.umd.min' import { Provider } from 'react-redux' import { BrowserRouter as Router } from 'react-router-dom' import configureStore from 'src/redux/store' -import { loadSuccess } from 'src/redux/modules/init' +import { loadSuccess } from 'src/redux/modules/init/actions' import rollbarConfig from 'config/rollbar' import vars from 'config/variables' diff --git a/src/index.ios.js b/src/index.ios.js index 4ed11acd..57940f53 100644 --- a/src/index.ios.js +++ b/src/index.ios.js @@ -1,5 +1,5 @@ // @flow -import React, { Component } from 'react' +import React, { PureComponent } from 'react' // import { StatusBar } from 'react-native' import { Provider } from 'react-redux' import { NativeRouter } from 'react-router-native' @@ -28,7 +28,7 @@ const store = configureStore() // } /* END HTTP DEBUG */ -class ReactTemplate extends Component { +class ReactTemplate extends PureComponent { // componentDidMount () { diff --git a/src/redux/epics/fetchProfile.js b/src/redux/epics/fetchProfile.js index c30ea016..9112ca5c 100644 --- a/src/redux/epics/fetchProfile.js +++ b/src/redux/epics/fetchProfile.js @@ -6,11 +6,11 @@ import 'rxjs/add/operator/catch' import 'rxjs/add/operator/do' import 'rxjs/add/operator/map' import 'rxjs/add/operator/mergeMap' +import { GET_PROFILE } from 'src/redux/modules/examples/profile/consts' import { - GET_PROFILE, fetchProfileSuccess, fetchProfileError, -} from 'src/redux/modules/examples/profile' +} from 'src/redux/modules/examples/profile/actions' import API from 'src/helpers/observableApi' const api = new API() diff --git a/src/redux/modules/auth.js b/src/redux/modules/auth.js deleted file mode 100644 index 079deff1..00000000 --- a/src/redux/modules/auth.js +++ /dev/null @@ -1,137 +0,0 @@ -// @flow -// import Debug from 'debug' -import get from 'lodash/get' -import { removeAuthToken, setAuthToken } from 'src/helpers/auth' -import { - loginMock as loginAPI, - logoutMock as logoutAPI, -} from 'src/helpers/api/auth' - -// const log = Debug('my-app:redux:modules:auth') - -export const LOGIN = 'my-app/auth/LOGIN' -export const LOGOUT = 'my-app/auth/LOGOUT' - -// Flow type for this reducer's initial state -export type AuthState = { - token: ?string, - user: Object, - error: ?string, - isFetching: boolean, -} - -// Initial state with default values -export const initialState: AuthState = { - token: '', - user: {}, - error: '', - isFetching: false, -} - - -// REDUCER -function reducer (state: AuthState = initialState, action: GlobalFSA<*>) { - - switch (action.type) { - - case `${LOGIN}_PENDING`: - return { - ...state, - isFetching: true, - } - - case `${LOGIN}_FULFILLED`: - return { - ...state, - token: get(action, 'payload.data.account.jwt'), - error: '', - isFetching: false, - } - - case `${LOGIN}_REJECTED`: - return { - ...state, - token: '', - error: action.payload, - isFetching: false, - } - - case `${LOGOUT}_PENDING`: - return { - ...state, - // Optimistic - token: '', - isFetching: true, - } - - case `${LOGOUT}_FULFILLED`: - return { - ...state, - token: '', - error: '', - isFetching: false, - } - - case `${LOGOUT}_REJECTED`: - return { - ...state, - // Always logout! - token: '', - error: action.payload, - isFetching: false, - } - - default: - return state - - } - -} - - -// ACTION CREATORS -// Use redux-promise-middleware -// https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/chaining-actions.md -// Which, in turn, uses Flux Standard Action (FSA) notation -// https://github.com/acdlite/flux-standard-action -type LoginParams = { - username: string, - password: string, -} - -export const login = ( - { username, password }: LoginParams, -): GlobalThunkAction => - (dispatch: GlobalDispatch<*>) => dispatch({ - type: LOGIN, - payload: loginAPI({ username, password }) - .then(response => setAuthToken(response.data.account.jwt) - .then(() => response) - ), - }) - -export const logout = (history: Object, redirect: ?string): GlobalThunkAction => - (dispatch: GlobalDispatch<*>) => { - - // Always do optimistic logout - removeAuthToken() - redirect - ? history.push({ - pathname: redirect, - }) - : history.push({ - pathname: '/', - query: { - login: null, - }, - }) - - return dispatch({ - type: LOGOUT, - payload: logoutAPI(), - }) - - } - - -export default reducer diff --git a/src/redux/modules/auth/_test.js b/src/redux/modules/auth/_test.js new file mode 100644 index 00000000..22eedf02 --- /dev/null +++ b/src/redux/modules/auth/_test.js @@ -0,0 +1,127 @@ +// @flow +import userMock from './mocks' +import reducer from './reducer' +import initialState from './initialState' +import { LOGIN, LOGOUT } from './consts' + +describe('Auth Initial and Prev State', () => { + + const unknownAction = { + type: 'SOME_UNKNOWN_ACTION', + payload: {}, + } + + it('uses initial state when no prev state is given', () => { + + const prevState = undefined + const nextState = reducer(prevState, unknownAction) + expect(nextState).toEqual(initialState) + + }) + + it('returns prev state by default for unknown action', () => { + + const prevState = initialState + const nextState = reducer(prevState, unknownAction) + expect(nextState).toEqual(initialState) + + }) + +}) + +describe('User login', () => { + + const prevState = initialState + + it('returns isFetching while fetching', () => { + + const expectedState = { ...prevState, isFetching: true } + const action = { type: `${LOGIN}_PENDING`, payload: [] } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('returns user token, no isFetching and no error on fetch success', () => { + + const expectedState = { + ...prevState, + token: userMock.account.jwt, + isFetching: false, + error: '', + } + const action = { + type: `${LOGIN}_FULFILLED`, + payload: { data: userMock }, + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('returns no user token, no isFetching and error description on fetch failure', () => { + + const expectedState = { + ...prevState, + token: '', + isFetching: false, + error: 'Network Error', + } + const action = { + type: `${LOGIN}_REJECTED`, + payload: 'Network Error', + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + +}) + +describe('User logout', () => { + + const prevState = { + ...initialState, + token: userMock.account.jwt, + } + + it('returns isFetching and no token (optimistic) on loggin out', () => { + + const expectedState = { ...prevState, token: '', isFetching: true } + const action = { type: `${LOGOUT}_PENDING`, payload: {} } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('resets state to initialState on LOGOUT success', () => { + + const expectedState = initialState + + const action = { + type: `${LOGOUT}_FULFILLED`, + payload: initialState, + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('returns isFetching false, error description and no token (always logout) on logout failure', () => { + + const expectedState = { + ...prevState, + token: '', + isFetching: false, + error: 'Network Error', + } + const action = { + type: `${LOGOUT}_REJECTED`, + payload: 'Network Error', + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + +}) diff --git a/src/redux/modules/auth/actions.js b/src/redux/modules/auth/actions.js new file mode 100644 index 00000000..db8c460c --- /dev/null +++ b/src/redux/modules/auth/actions.js @@ -0,0 +1,35 @@ +// @flow +import { removeAuthToken, setAuthToken } from 'src/helpers/auth' +import { loginMock as loginAPI, logoutMock as logoutAPI } from './api' +import type { LoginParams } from './types' +import { LOGIN, LOGOUT } from './consts' + +// ACTION CREATORS +// Use redux-promise-middleware +// https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/chaining-actions.md +// Which, in turn, uses Flux Standard Action (FSA) notation +// https://github.com/acdlite/flux-standard-action + +export const login = ( + { username, password }: LoginParams, +): GlobalThunkAction => + (dispatch: GlobalDispatch<*>) => dispatch({ + type: LOGIN, + payload: loginAPI({ username, password }) + .then(response => setAuthToken(response.data.account.jwt) + .then(() => response) + ), + }) + +export const logout = (history: Object, redirect: ?string): GlobalThunkAction => + (dispatch: GlobalDispatch<*>) => { + + // Always do optimistic logout + removeAuthToken() + redirect + ? history.push({ pathname: redirect }) + : history.push({ pathname: '/', query: { login: null } }) + + return dispatch({ type: LOGOUT, payload: logoutAPI() }) + + } diff --git a/src/helpers/api/auth.js b/src/redux/modules/auth/api.js similarity index 79% rename from src/helpers/api/auth.js rename to src/redux/modules/auth/api.js index 74b22459..b8f5d511 100644 --- a/src/helpers/api/auth.js +++ b/src/redux/modules/auth/api.js @@ -1,21 +1,12 @@ // @flow import { post, mock } from 'src/helpers/api' import env from 'config/env' +import userMock from './mocks' +import type { LoginParams } from './types' const { API_URL, USE_MOCK_API } = env -type LoginParams = { - username: string, - password: string, -} - -export const loginMock = ({ username, password }: LoginParams) => mock({ - account: { - id: '1', - email: 'email@example.com', - jwt: 'tokenabc123', - }, -}) +export const loginMock = ({ username, password }: LoginParams) => mock(userMock) export const login = ({ username, password }: LoginParams) => ( USE_MOCK_API diff --git a/src/redux/modules/auth/consts.js b/src/redux/modules/auth/consts.js new file mode 100644 index 00000000..73aa9de4 --- /dev/null +++ b/src/redux/modules/auth/consts.js @@ -0,0 +1,3 @@ +// @flow +export const LOGIN = 'my-app/auth/LOGIN' +export const LOGOUT = 'my-app/auth/LOGOUT' diff --git a/src/redux/modules/auth/initialState.js b/src/redux/modules/auth/initialState.js new file mode 100644 index 00000000..f2d7b381 --- /dev/null +++ b/src/redux/modules/auth/initialState.js @@ -0,0 +1,11 @@ +// @flow +import type { AuthState } from './types' + +// Initial state with default values +const initialState: AuthState = { + token: '', + error: '', + isFetching: false, +} + +export default initialState diff --git a/src/redux/modules/auth/mocks.js b/src/redux/modules/auth/mocks.js new file mode 100644 index 00000000..5415cb91 --- /dev/null +++ b/src/redux/modules/auth/mocks.js @@ -0,0 +1,11 @@ +// @flow + +const user = { + account: { + id: '1', + email: 'email@example.com', + jwt: 'tokenabc123', + }, +} + +export default user diff --git a/src/redux/modules/auth/reducer.js b/src/redux/modules/auth/reducer.js new file mode 100644 index 00000000..c006692f --- /dev/null +++ b/src/redux/modules/auth/reducer.js @@ -0,0 +1,69 @@ +// @flow +// import Debug from 'debug' +import get from 'lodash/get' +import { LOGIN, LOGOUT } from './consts' +import initialState from './initialState' +import type { AuthState } from './types' + +// const log = Debug('my-app:redux:modules:auth') + +function reducer (state: AuthState = initialState, action: GlobalFSA) { + + switch (action.type) { + + case `${LOGIN}_PENDING`: + return { + ...state, + isFetching: true, + } + + case `${LOGIN}_FULFILLED`: + return { + ...state, + token: get(action, 'payload.data.account.jwt'), + error: '', + isFetching: false, + } + + case `${LOGIN}_REJECTED`: + return { + ...state, + token: '', + error: action.payload, + isFetching: false, + } + + case `${LOGOUT}_PENDING`: + return { + ...state, + // Optimistic + token: '', + isFetching: true, + } + + case `${LOGOUT}_FULFILLED`: + return { + ...state, + token: '', + error: '', + isFetching: false, + } + + case `${LOGOUT}_REJECTED`: + return { + ...state, + // Always logout! + token: '', + error: action.payload, + isFetching: false, + } + + default: + return state + + } + +} + + +export default reducer diff --git a/src/redux/modules/auth/types.js b/src/redux/modules/auth/types.js new file mode 100644 index 00000000..eb233f23 --- /dev/null +++ b/src/redux/modules/auth/types.js @@ -0,0 +1,12 @@ +// @flow +// Flow type for this reducer's initial state +export type AuthState = { + token: ?string, + error: ?string, + isFetching: boolean, +} + +export type LoginParams = { + username: string, + password: string, +} diff --git a/src/redux/modules/env/actions.js b/src/redux/modules/env/actions.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/env/actions.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/env/consts.js b/src/redux/modules/env/consts.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/env/consts.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/env/initialState.js b/src/redux/modules/env/initialState.js new file mode 100644 index 00000000..ebd650af --- /dev/null +++ b/src/redux/modules/env/initialState.js @@ -0,0 +1,4 @@ +// @flow +const initialState: Object = {} + +export default initialState diff --git a/src/redux/modules/env.js b/src/redux/modules/env/reducer.js similarity index 55% rename from src/redux/modules/env.js rename to src/redux/modules/env/reducer.js index 2c92f31f..7c5c287a 100644 --- a/src/redux/modules/env.js +++ b/src/redux/modules/env/reducer.js @@ -1,20 +1,13 @@ // @flow // Empty reducer for now since we probably don't want to change // it in the browser - import type { EnvState } from 'config/env' +import initialState from './initialState' -type EnvAction = Object - - -export const initialState: Object = { -} - -function reducer (state: Object = initialState, action: EnvAction): EnvState { +function reducer (state: Object = initialState, action: Object): EnvState { return state } - export default reducer diff --git a/src/redux/modules/env/types.js b/src/redux/modules/env/types.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/env/types.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/examples/index.js b/src/redux/modules/examples/index.js index 9b803e71..fb52bb07 100644 --- a/src/redux/modules/examples/index.js +++ b/src/redux/modules/examples/index.js @@ -1,12 +1,12 @@ // @flow -// + // Example nested reducer import { combineReducers } from 'redux' -import profile from './profile' -import todos from './todos' +import profile from './profile/reducer' +import todos from './todos/reducer' -import type { ProfileState } from './profile' -import type { TodosState } from './todos' +import type { ProfileState } from './profile/types' +import type { TodosState } from './todos/types' export type ExamplesState = { profile: ProfileState, diff --git a/src/redux/modules/examples/profile.js b/src/redux/modules/examples/profile.js deleted file mode 100644 index 2f6ace41..00000000 --- a/src/redux/modules/examples/profile.js +++ /dev/null @@ -1,115 +0,0 @@ -// @flow -// Non-shallow reducer state example needs nested reducers -// import Debug from 'debug' -import get from 'lodash/get' -import type { APIError } from 'src/helpers/api' - -// const log = Debug('my-app:redux:modules:profile') - - -// ACTION TYPES -export const GET_PROFILE = 'my-app/examples/profile/GET_PROFILE' -export const GET_PROFILE_FULFILLED = `${GET_PROFILE}_FULFILLED` -export const GET_PROFILE_REJECTED = `${GET_PROFILE}_REJECTED` - - -// MODEL -// Profile model with default values -export type Profile = { - id: number, - firstName: string, - lastName: string, - email: string, - city: string, - dob: string, - notifications: boolean, - picture: string, -} - -const profile = { - id: 1, - firstName: '', - lastName: '', - email: '', - city: 'nashville', - dob: '1970-01-01T00:00:00.000Z', - notifications: true, - picture: '', -} - - -// Flow type for this reducer's initial state -export type ProfileState = { - data: Profile, - isFetching: boolean, - error: ?string, -} - -// Initial state with default values -export const initialState: ProfileState = { - data: profile, - error: '', - isFetching: false, -} - - -// REDUCER -function reducer (state: ProfileState = initialState, action: GlobalFSA<*>) { - - switch (action.type) { - - case GET_PROFILE: - return { - ...state, - isFetching: true, - } - - case GET_PROFILE_FULFILLED: - return { - ...state, - data: get(action, 'payload.me'), - error: '', - isFetching: false, - } - - case GET_PROFILE_REJECTED: - return { - ...state, - data: initialState.data, - error: get(action, 'payload.statusCode'), - isFetching: false, - } - - default: - return state - - } - -} - - -// ACTION CREATORS -// Use Flux Standard Action (FSA) notation -// https://github.com/acdlite/flux-standard-action -export const fetchProfile = () => ({ - type: GET_PROFILE, -}) - - -export const fetchProfileSuccess = (me: Object) => ({ - type: GET_PROFILE_FULFILLED, - payload: { - me, - }, -}) - - -export const fetchProfileError = (error: APIError) => ({ - type: GET_PROFILE_REJECTED, - payload: error, - error: true, -}) -// Always append { error: true } for redux action error types - - -export default reducer diff --git a/src/redux/modules/examples/__snapshots__/profile_test.js.snap b/src/redux/modules/examples/profile/__snapshots__/_test.js.snap similarity index 100% rename from src/redux/modules/examples/__snapshots__/profile_test.js.snap rename to src/redux/modules/examples/profile/__snapshots__/_test.js.snap diff --git a/src/redux/modules/examples/profile_test.js b/src/redux/modules/examples/profile/_test.js similarity index 82% rename from src/redux/modules/examples/profile_test.js rename to src/redux/modules/examples/profile/_test.js index d21ff9f9..2533041d 100644 --- a/src/redux/modules/examples/profile_test.js +++ b/src/redux/modules/examples/profile/_test.js @@ -1,6 +1,6 @@ // @flow -import reducer, { initialState } from './profile' - +import reducer from './reducer' +import initialState from './initialState' it('reducer profile returns initialState by default', () => { diff --git a/src/redux/modules/examples/profile/actions.js b/src/redux/modules/examples/profile/actions.js new file mode 100644 index 00000000..07a9de4d --- /dev/null +++ b/src/redux/modules/examples/profile/actions.js @@ -0,0 +1,21 @@ +// @flow +import type { APIError } from 'src/helpers/api' +import { GET_PROFILE, GET_PROFILE_FULFILLED, GET_PROFILE_REJECTED } from './consts' + +// Use Flux Standard Action (FSA) notation +// https://github.com/acdlite/flux-standard-action +export const fetchProfile = () => ({ + type: GET_PROFILE, +}) + +export const fetchProfileSuccess = (me: Object) => ({ + type: GET_PROFILE_FULFILLED, + payload: { me }, +}) + +export const fetchProfileError = (error: APIError) => ({ + type: GET_PROFILE_REJECTED, + payload: error, + error: true, +}) +// Always append { error: true } for redux action error types diff --git a/src/redux/modules/examples/profile/api.js b/src/redux/modules/examples/profile/api.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/examples/profile/api.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/examples/profile/consts.js b/src/redux/modules/examples/profile/consts.js new file mode 100644 index 00000000..f622f572 --- /dev/null +++ b/src/redux/modules/examples/profile/consts.js @@ -0,0 +1,4 @@ +// @flow +export const GET_PROFILE = 'my-app/examples/profile/GET_PROFILE' +export const GET_PROFILE_FULFILLED = `${GET_PROFILE}_FULFILLED` +export const GET_PROFILE_REJECTED = `${GET_PROFILE}_REJECTED` diff --git a/src/redux/modules/examples/profile/initialState.js b/src/redux/modules/examples/profile/initialState.js new file mode 100644 index 00000000..de44a269 --- /dev/null +++ b/src/redux/modules/examples/profile/initialState.js @@ -0,0 +1,21 @@ +// @flow +import type { ProfileState } from './types' + +const profile = { + id: 1, + firstName: '', + lastName: '', + email: '', + city: 'nashville', + dob: '1970-01-01T00:00:00.000Z', + notifications: true, + picture: '', +} + +const initialState: ProfileState = { + data: profile, + error: '', + isFetching: false, +} + +export default initialState diff --git a/src/redux/modules/examples/profile/mocks.js b/src/redux/modules/examples/profile/mocks.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/examples/profile/mocks.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/examples/profile/reducer.js b/src/redux/modules/examples/profile/reducer.js new file mode 100644 index 00000000..1ada07cd --- /dev/null +++ b/src/redux/modules/examples/profile/reducer.js @@ -0,0 +1,44 @@ +// @flow +// Non-shallow reducer state example needs nested reducers +// import Debug from 'debug' +import get from 'lodash/get' +import { GET_PROFILE, GET_PROFILE_FULFILLED, GET_PROFILE_REJECTED } from './consts' +import type { ProfileState } from './types' +import initialState from './initialState' + +// const log = Debug('my-app:redux:modules:profile') + +function reducer (state: ProfileState = initialState, action: GlobalFSA) { + + switch (action.type) { + + case GET_PROFILE: + return { + ...state, + isFetching: true, + } + + case GET_PROFILE_FULFILLED: + return { + ...state, + data: get(action, 'payload.me'), + error: '', + isFetching: false, + } + + case GET_PROFILE_REJECTED: + return { + ...state, + data: initialState.data, + error: get(action, 'payload.statusCode'), + isFetching: false, + } + + default: + return state + + } + +} + +export default reducer diff --git a/src/redux/modules/examples/profile/types.js b/src/redux/modules/examples/profile/types.js new file mode 100644 index 00000000..1dd7e87d --- /dev/null +++ b/src/redux/modules/examples/profile/types.js @@ -0,0 +1,20 @@ +// @flow + +// Profile model with default values +export type Profile = { + id: number, + firstName: string, + lastName: string, + email: string, + city: string, + dob: string, + notifications: boolean, + picture: string, +} + +// Flow type for this reducer's initial state +export type ProfileState = { + data: Profile, + isFetching: boolean, + error: ?string, +} diff --git a/src/redux/modules/examples/todos.js b/src/redux/modules/examples/todos.js deleted file mode 100644 index 6de1954a..00000000 --- a/src/redux/modules/examples/todos.js +++ /dev/null @@ -1,117 +0,0 @@ -// @flow -// Non-shallow reducer state example needs Immutable -// Async actions need redux-observable epics -import { - getTodosMock as getTodosApi, -} from 'src/helpers/api/examples/todos' - - -// ACTION TYPES -// export const ADD_TODO = 'my-app/todo/ADD_TODO' -// export const COMPLETE_TODO = 'my-app/todo/COMPLETE_TODO' -export const GET_TODOS = 'my-app/examples/todos/GET_TODOS' -export const SET_FILTER = 'my-app/examples/todos/SET_FILTER' - -// FILTERS -export const FILTER_CURRENT = 'my-app/examples/todos/FILTER_CURRENT' -export const FILTER_DONE = 'my-app/examples/todos/FILTER_DONE' - - -// MODEL -// Todos model with default values -export type Todo = { - text: string, - date: string, - done: boolean, -} - -export type TodosState = { - +filter: string, - +list: Array, - +isFetching: boolean, - +error: ?string, -} - -export const initialState: TodosState = { - filter: FILTER_CURRENT, - list: [], - isFetching: false, - error: '', -} - - -// REDUCER -function reducer (state: TodosState = initialState, action: GlobalFSA<*>) { - - switch (action.type) { - - case SET_FILTER: - return { - ...state, - filter: action.payload.filter, - } - - case `${GET_TODOS}_PENDING`: - return { - ...state, - isFetching: true, - } - - case `${GET_TODOS}_FULFILLED`: - return { - ...state, - list: action.payload.data.data, - isFetching: false, - error: initialState.error, - } - - case `${GET_TODOS}_REJECTED`: - return { - ...state, - isFetching: false, - error: action.payload, - } - - default: - return state - - } - -} - - -// ACTION CREATORS -// Use redux-promise-middleware -// https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/chaining-actions.md -// Which, in turn, uses Flux Standard Action (FSA) notation -// https://github.com/acdlite/flux-standard-action -export const getTodos = () => ({ - type: GET_TODOS, - payload: getTodosApi(), -}) - - -// Plain actions uses Flux Standard Action (FSA) notation -// https://github.com/acdlite/flux-standard-action -export const setFilterDone = () => ({ - type: SET_FILTER, - payload: { - filter: FILTER_DONE, - }, -}) - - -export const setFilterCurrent = () => ({ - type: SET_FILTER, - payload: { - filter: FILTER_CURRENT, - }, -}) - - -// EPICS -// variable$ notation indicates an event stream -// https://redux-observable.js.org/docs/basics/Epics.html - - -export default reducer diff --git a/src/redux/modules/examples/todos/_test.js b/src/redux/modules/examples/todos/_test.js new file mode 100644 index 00000000..8197a608 --- /dev/null +++ b/src/redux/modules/examples/todos/_test.js @@ -0,0 +1,123 @@ +// @flow +import { todoList as todoListMock } from './mocks' +import reducer from './reducer' +import initialState from './initialState' +import { LOGOUT } from '../../auth/consts' +import { GET_TODOS } from './consts' + +describe('Todo Initial and Prev State', () => { + + const unknownAction = { + type: 'SOME_UNKNOWN_ACTION', + payload: {}, + } + + it('uses initial state when no prev state is given', () => { + + const prevState = undefined + const nextState = reducer(prevState, unknownAction) + expect(nextState).toEqual(initialState) + + }) + + it('returns prev state by default for unknown action', () => { + + const prevState = initialState + const nextState = reducer(prevState, unknownAction) + expect(nextState).toEqual(initialState) + + }) + +}) + +describe('Todos Fetch', () => { + + const prevState = initialState + + it('returns isFetching while fetching', () => { + + const expectedState = { ...prevState, isFetching: true } + const action = { type: `${GET_TODOS}_PENDING`, payload: [] } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('returns todo list, no isFetching and no error on fetch success', () => { + + const expectedState = { + ...prevState, + list: todoListMock.list, + isFetching: false, + error: '', + } + const action = { + type: `${GET_TODOS}_FULFILLED`, + payload: { data: todoListMock }, + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('returns prev todo list, no isFetching and error description on fetch failure', () => { + + const expectedState = { + ...prevState, + isFetching: false, + error: 'Network Error', + } + const action = { + type: `${GET_TODOS}_REJECTED`, + payload: 'Network Error', + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + +}) + +describe('User logout', () => { + + const prevState = initialState + + it('returns isFetching and initial state (optimistic) on loggin out', () => { + + const expectedState = { ...initialState, isFetching: true } + const action = { type: `${LOGOUT}_PENDING`, payload: {} } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('resets state to initialState on LOGOUT success', () => { + + const expectedState = { ...initialState, isFetching: false } + + const action = { + type: `${LOGOUT}_FULFILLED`, + payload: initialState, + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + + it('returns isFetching false, error description and initial state (always logout) on logout failure', () => { + + const expectedState = { + ...initialState, + isFetching: false, + error: 'Network Error', + } + const action = { + type: `${LOGOUT}_REJECTED`, + payload: 'Network Error', + } + const nextState = reducer(prevState, action) + expect(nextState).toEqual(expectedState) + + }) + +}) diff --git a/src/redux/modules/examples/todos/actions.js b/src/redux/modules/examples/todos/actions.js new file mode 100644 index 00000000..d919cc4f --- /dev/null +++ b/src/redux/modules/examples/todos/actions.js @@ -0,0 +1,28 @@ +// @flow +import { GET_TODOS, SET_FILTER, FILTER_DONE, FILTER_CURRENT } from './consts' +import { getTodosMock as getTodosApi } from './api' + +// Use redux-promise-middleware +// https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/chaining-actions.md +// Which, in turn, uses Flux Standard Action (FSA) notation +// https://github.com/acdlite/flux-standard-action +export const getTodos = () => ({ + type: GET_TODOS, + payload: getTodosApi(), +}) + +// Plain actions uses Flux Standard Action (FSA) notation +// https://github.com/acdlite/flux-standard-action +export const setFilterDone = () => ({ + type: SET_FILTER, + payload: { + filter: FILTER_DONE, + }, +}) + +export const setFilterCurrent = () => ({ + type: SET_FILTER, + payload: { + filter: FILTER_CURRENT, + }, +}) diff --git a/src/redux/modules/examples/todos/api.js b/src/redux/modules/examples/todos/api.js new file mode 100644 index 00000000..38bd6f4e --- /dev/null +++ b/src/redux/modules/examples/todos/api.js @@ -0,0 +1,14 @@ +// @flow +import { get, mock } from 'src/helpers/api' +import env from 'config/env' +import { todoList as todoListMock } from './mocks' + +const { API_URL, USE_MOCK_API } = env + +export const getTodosMock = () => mock(todoListMock) + +export const getTodos = () => ( + USE_MOCK_API + ? getTodosMock() + : get(`${API_URL}/todos`) +) diff --git a/src/redux/modules/examples/todos/consts.js b/src/redux/modules/examples/todos/consts.js new file mode 100644 index 00000000..3b765c06 --- /dev/null +++ b/src/redux/modules/examples/todos/consts.js @@ -0,0 +1,10 @@ +// @flow + +// export const ADD_TODO = 'my-app/todo/ADD_TODO' +// export const COMPLETE_TODO = 'my-app/todo/COMPLETE_TODO' +export const GET_TODOS = 'my-app/examples/todos/GET_TODOS' +export const SET_FILTER = 'my-app/examples/todos/SET_FILTER' + +// FILTERS +export const FILTER_CURRENT = 'my-app/examples/todos/FILTER_CURRENT' +export const FILTER_DONE = 'my-app/examples/todos/FILTER_DONE' diff --git a/src/redux/modules/examples/todos/initialState.js b/src/redux/modules/examples/todos/initialState.js new file mode 100644 index 00000000..567ea178 --- /dev/null +++ b/src/redux/modules/examples/todos/initialState.js @@ -0,0 +1,12 @@ +// @flow +import { FILTER_CURRENT } from './consts' +import type { TodosState } from './types' + +const initialState: TodosState = { + filter: FILTER_CURRENT, + list: [], + isFetching: false, + error: '', +} + +export default initialState diff --git a/src/helpers/api/examples/todos.js b/src/redux/modules/examples/todos/mocks.js similarity index 61% rename from src/helpers/api/examples/todos.js rename to src/redux/modules/examples/todos/mocks.js index 83147523..09a3866e 100644 --- a/src/helpers/api/examples/todos.js +++ b/src/redux/modules/examples/todos/mocks.js @@ -1,11 +1,13 @@ // @flow -import { get, mock } from 'src/helpers/api' -import env from 'config/env' -const { API_URL, USE_MOCK_API } = env +export const todo = { + text: 'This is a single todo', + date: '2017-11-11T11:11:11Z', + done: false, +} -export const getTodosMock = () => mock({ - data: [ +export const todoList = { + list: [ { text: 'This is the first todo', date: '2016-12-12T20:22:54Z', @@ -28,8 +30,4 @@ export const getTodosMock = () => mock({ done: true, }, ], -}) - -export const getTodos = () => (USE_MOCK_API - ? getTodosMock() - : get(`${API_URL}/todos`)) +} diff --git a/src/redux/modules/examples/todos/reducer.js b/src/redux/modules/examples/todos/reducer.js new file mode 100644 index 00000000..e25071d1 --- /dev/null +++ b/src/redux/modules/examples/todos/reducer.js @@ -0,0 +1,70 @@ +// @flow +// Non-shallow reducer state example needs Immutable +// Async actions need redux-observable epics +import { LOGOUT } from '../../auth/consts' +import { GET_TODOS, SET_FILTER } from './consts' +import type { TodosState } from './types' +import initialState from './initialState' + +// REDUCER +function reducer (state: TodosState = initialState, action: GlobalFSA) { + + switch (action.type) { + + case SET_FILTER: + return { + ...state, + filter: action.payload.filter, + } + + case `${GET_TODOS}_PENDING`: + return { + ...state, + isFetching: true, + } + + case `${GET_TODOS}_FULFILLED`: + return { + ...state, + list: action.payload.data.list, + isFetching: false, + error: initialState.error, + } + + case `${GET_TODOS}_REJECTED`: + return { + ...state, + isFetching: false, + error: action.payload, + } + + case `${LOGOUT}_PENDING`: + return { + ...initialState, + isFetching: true, + } + + case `${LOGOUT}_FULFILLED`: + return { + ...initialState, + isFetching: false, + } + + case `${LOGOUT}_REJECTED`: + return { + ...initialState, + error: action.payload, + } + + default: + return state + + } + +} + +// EPICS +// variable$ notation indicates an event stream +// https://redux-observable.js.org/docs/basics/Epics.html + +export default reducer diff --git a/src/redux/modules/examples/todos/types.js b/src/redux/modules/examples/todos/types.js new file mode 100644 index 00000000..077c1844 --- /dev/null +++ b/src/redux/modules/examples/todos/types.js @@ -0,0 +1,14 @@ +// @flow +// Todos model with default values +export type Todo = { + text: string, + date: string, + done: boolean, +} + +export type TodosState = { + +filter: string, + +list: Array, + +isFetching: boolean, + +error: ?string, +} diff --git a/src/redux/modules/index.js b/src/redux/modules/index.js index 654f7020..a7af74e6 100644 --- a/src/redux/modules/index.js +++ b/src/redux/modules/index.js @@ -4,24 +4,23 @@ import { reducer as form } from 'redux-form' // Types import type { EnvState } from 'config/env' -import type { RequestState } from 'src/redux/modules/request' -import type { InitState } from 'src/redux/modules/init' -import type { AuthState } from 'src/redux/modules/auth' -import type { UIState } from 'src/redux/modules/ui' +import type { RequestState } from 'src/redux/modules/request/types' +import type { InitState } from 'src/redux/modules/init/types' +import type { AuthState } from 'src/redux/modules/auth/types' +import type { UIState } from 'src/redux/modules/ui/types' // Examples import type { ExamplesState } from 'src/redux/modules/examples' -import env from './env' -import request from './request' -import init from './init' -import auth from './auth' -import ui from './ui' +import env from './env/reducer' +import request from './request/reducer' +import init from './init/reducer' +import auth from './auth/reducer' +import ui from './ui/reducer' // Examples import examples from './examples' - export type RootReducerState = { env: EnvState, request: RequestState, diff --git a/src/redux/modules/init.js b/src/redux/modules/init.js deleted file mode 100644 index ce660e0a..00000000 --- a/src/redux/modules/init.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow -// "Shallow" reducer state example doesn't need Immutable -export const LOAD = 'my-app/init/LOAD' -export const LOAD_SUCCESS = 'my-app/init/LOAD_SUCCESS' - - -export type InitState = { - isLoading: boolean, - loaded: boolean, -} - - -export const initialState = { - isLoading: true, - loaded: false, -} - - -function reducer (state: InitState = initialState, action: GlobalFSA<*>) { - - switch (action.type) { - - - case LOAD: - return { - ...state, - isLoading: true, - } - - case LOAD_SUCCESS: - return { - ...state, - isLoading: false, - loaded: true, - } - - - default: - return state - - } - -} - - -// Removed from implementation until the need arises -// For now, we assume that the app arrives in loading state -// export const load = () => { - -// return { -// type: LOAD, -// } - -// } - -export const loadSuccess = () => ({ - type: LOAD_SUCCESS, -}) - - -export default reducer diff --git a/src/redux/modules/init/_test.js b/src/redux/modules/init/_test.js new file mode 100644 index 00000000..0500ebeb --- /dev/null +++ b/src/redux/modules/init/_test.js @@ -0,0 +1,31 @@ +// @flow +import reducer from './reducer' +import initialState from './initialState' +import { LOAD, LOAD_SUCCESS } from './consts' + +it('reducer init returns initialState by default', () => { + + const expected = initialState + const action = { type: 'SOME_UNKNOWN_ACTION', payload: {} } + const actual = reducer(expected, action) + expect(actual).toEqual(expected) + +}) + +it('reducer init on LOAD sets isLoading true', () => { + + const expected = { isLoading: true, loaded: false } + const action = { type: LOAD, payload: {} } + const actual = reducer(initialState, action) + expect(actual).toEqual(expected) + +}) + +it('reducer init on LOAD_SUCCESS sets isLoading false, loaded true', () => { + + const expected = { loaded: true, isLoading: false } + const action = { type: LOAD_SUCCESS, payload: {} } + const actual = reducer(initialState, action) + expect(actual).toEqual(expected) + +}) diff --git a/src/redux/modules/init/actions.js b/src/redux/modules/init/actions.js new file mode 100644 index 00000000..65abe890 --- /dev/null +++ b/src/redux/modules/init/actions.js @@ -0,0 +1,7 @@ +// @flow +import { LOAD, LOAD_SUCCESS } from './consts' + +// Removed from implementation until the need arises +// For now, we assume that the app arrives in loading state +export const load = () => ({ type: LOAD }) +export const loadSuccess = () => ({ type: LOAD_SUCCESS }) diff --git a/src/redux/modules/init/api.js b/src/redux/modules/init/api.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/init/api.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/init/consts.js b/src/redux/modules/init/consts.js new file mode 100644 index 00000000..7d212ed5 --- /dev/null +++ b/src/redux/modules/init/consts.js @@ -0,0 +1,3 @@ +// @flow +export const LOAD = 'my-app/init/LOAD' +export const LOAD_SUCCESS = 'my-app/init/LOAD_SUCCESS' diff --git a/src/redux/modules/init/initialState.js b/src/redux/modules/init/initialState.js new file mode 100644 index 00000000..8c7a1e61 --- /dev/null +++ b/src/redux/modules/init/initialState.js @@ -0,0 +1,7 @@ +// @flow +const initialState = { + isLoading: true, + loaded: false, +} + +export default initialState diff --git a/src/redux/modules/init/mocks.js b/src/redux/modules/init/mocks.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/init/mocks.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/init/reducer.js b/src/redux/modules/init/reducer.js new file mode 100644 index 00000000..a3436570 --- /dev/null +++ b/src/redux/modules/init/reducer.js @@ -0,0 +1,31 @@ +// @flow +// "Shallow" reducer state example doesn't need Immutable +import { LOAD, LOAD_SUCCESS } from './consts' +import type { InitState, InitAction } from './types' +import initialState from './initialState' + +function reducer (state: InitState = initialState, action: InitAction) { + + switch (action.type) { + + case LOAD: + return { + ...state, + isLoading: true, + } + + case LOAD_SUCCESS: + return { + ...state, + isLoading: false, + loaded: true, + } + + default: + return state + + } + +} + +export default reducer diff --git a/src/redux/modules/init/types.js b/src/redux/modules/init/types.js new file mode 100644 index 00000000..f0243d83 --- /dev/null +++ b/src/redux/modules/init/types.js @@ -0,0 +1,7 @@ +// @flow +export type InitState = { + isLoading: boolean, + loaded: boolean, +} + +export type InitAction = GlobalFSA diff --git a/src/redux/modules/init_test.js b/src/redux/modules/init_test.js deleted file mode 100644 index 010e0abf..00000000 --- a/src/redux/modules/init_test.js +++ /dev/null @@ -1,47 +0,0 @@ -// @flow -import reducer, { initialState, LOAD, LOAD_SUCCESS } from './init' - - -it('reducer init returns initialState by default', () => { - - const expected = initialState - const actual = reducer(expected, { - type: 'SOME_UNKNOWN_ACTION', - payload: {}, - }) - - expect(actual).toEqual(expected) - -}) - - -it('reducer init on LOAD sets isLoading true', () => { - - const expected = { - isLoading: true, - loaded: false, - } - const actual = reducer(initialState, { - type: LOAD, - payload: {}, - }) - - expect(actual).toEqual(expected) - -}) - - -it('reducer init on LOAD_SUCCESS sets isLoading false, loaded true', () => { - - const expected = { - loaded: true, - isLoading: false, - } - const actual = reducer(initialState, { - type: LOAD_SUCCESS, - payload: {}, - }) - - expect(actual).toEqual(expected) - -}) diff --git a/src/redux/modules/request.js b/src/redux/modules/request.js deleted file mode 100644 index 67fc017b..00000000 --- a/src/redux/modules/request.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow -// Empty reducer for now since we probably don't want to change -// it in the browser - -type EnvAction = GlobalFSA<*> - -export type RequestState = { - userAgent: string, -} - -export const initialState = { - userAgent: '', -} - -function reducer ( - state: Object = initialState, action: EnvAction -): RequestState { - - return state - -} - - -export default reducer diff --git a/src/redux/modules/request/actions.js b/src/redux/modules/request/actions.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/request/actions.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/request/consts.js b/src/redux/modules/request/consts.js new file mode 100644 index 00000000..46e7f7c0 --- /dev/null +++ b/src/redux/modules/request/consts.js @@ -0,0 +1 @@ +// @flow diff --git a/src/redux/modules/request/initialState.js b/src/redux/modules/request/initialState.js new file mode 100644 index 00000000..c3c63671 --- /dev/null +++ b/src/redux/modules/request/initialState.js @@ -0,0 +1,4 @@ +// @flow +const initialState = { userAgent: '' } + +export default initialState diff --git a/src/redux/modules/request/reducer.js b/src/redux/modules/request/reducer.js new file mode 100644 index 00000000..320ae9f3 --- /dev/null +++ b/src/redux/modules/request/reducer.js @@ -0,0 +1,16 @@ +// @flow +// Empty reducer for now since we probably don't want to change +// it in the browser +import type { RequestAction, RequestState } from './types' +import initialState from './initialState' + +function reducer ( + state: Object = initialState, + action: RequestAction +): RequestState { + + return state + +} + +export default reducer diff --git a/src/redux/modules/request/types.js b/src/redux/modules/request/types.js new file mode 100644 index 00000000..941dd37c --- /dev/null +++ b/src/redux/modules/request/types.js @@ -0,0 +1,4 @@ +// @flow +export type RequestAction = GlobalFSA + +export type RequestState = { userAgent: string } diff --git a/src/redux/modules/ui/actions.js b/src/redux/modules/ui/actions.js new file mode 100644 index 00000000..33dffb53 --- /dev/null +++ b/src/redux/modules/ui/actions.js @@ -0,0 +1,9 @@ +// @flow +import { START_LOADING, STOP_LOADING } from './consts' + +// ACTION CREATORS +// Use Flux Standard Action (FSA) notation +// https://github.com/acdlite/flux-standard-action +export const startLoading = () => ({ type: START_LOADING }) + +export const stopLoading = () => ({ type: STOP_LOADING }) diff --git a/src/redux/modules/ui/consts.js b/src/redux/modules/ui/consts.js new file mode 100644 index 00000000..27beab08 --- /dev/null +++ b/src/redux/modules/ui/consts.js @@ -0,0 +1,6 @@ +// @flow +export const CLOSE_SIDEBAR = 'my-app/ui/CLOSE_SIDEBAR' +export const OPEN_SIDEBAR = 'my-app/ui/OPEN_SIDEBAR' + +export const START_LOADING = 'my-app/ui/START_LOADING' +export const STOP_LOADING = 'my-app/ui/STOP_LOADING' diff --git a/src/redux/modules/ui/initialState.js b/src/redux/modules/ui/initialState.js new file mode 100644 index 00000000..5e834caf --- /dev/null +++ b/src/redux/modules/ui/initialState.js @@ -0,0 +1,11 @@ +// @flow +import type { UIState } from './types' + +const initialState: UIState = { + sidebarOpen: true, + error: null, + // Manual option to show loading indicator if needed + showLoading: false, +} + +export default initialState diff --git a/src/redux/modules/ui.js b/src/redux/modules/ui/reducer.js similarity index 50% rename from src/redux/modules/ui.js rename to src/redux/modules/ui/reducer.js index 749cb750..c43854f7 100644 --- a/src/redux/modules/ui.js +++ b/src/redux/modules/ui/reducer.js @@ -1,33 +1,18 @@ // @flow import { DISPLAY_ERROR } from 'src/redux/middleware/errorDisplay' +import { + CLOSE_SIDEBAR, + OPEN_SIDEBAR, + START_LOADING, + STOP_LOADING, +} from './consts' +import type { UIState } from './types' +import initialState from './initialState' + // import Debug from 'debug' // const log = Debug('my-app:redux:modules:ui') - -// ACTION TYPES -export const CLOSE_SIDEBAR = 'my-app/ui/CLOSE_SIDEBAR' -export const OPEN_SIDEBAR = 'my-app/ui/OPEN_SIDEBAR' - -export const START_LOADING = 'my-app/ui/START_LOADING' -export const STOP_LOADING = 'my-app/ui/STOP_LOADING' - -// MODEL -export type UIState = { - sidebarOpen: boolean, - error: ?{ message: string }, - showLoading: boolean, -} - -export const initialState: UIState = { - sidebarOpen: true, - error: null, - // Manual option to show loading indicator if needed - showLoading: false, -} - - -// REDUCER -function reducer (state: UIState = initialState, action: GlobalFSA<*>) { +function reducer (state: UIState = initialState, action: GlobalFSA) { switch (action.type) { @@ -68,17 +53,4 @@ function reducer (state: UIState = initialState, action: GlobalFSA<*>) { } - -// ACTION CREATORS -// Use Flux Standard Action (FSA) notation -// https://github.com/acdlite/flux-standard-action -export const startLoading = () => ({ - type: START_LOADING, -}) - -export const stopLoading = () => ({ - type: STOP_LOADING, -}) - - export default reducer diff --git a/src/redux/modules/ui/types.js b/src/redux/modules/ui/types.js new file mode 100644 index 00000000..2af18259 --- /dev/null +++ b/src/redux/modules/ui/types.js @@ -0,0 +1,8 @@ +// @flow +export type ErrorMessage = { message: string } + +export type UIState = { + sidebarOpen: boolean, + error: ?ErrorMessage, + showLoading: boolean, +} diff --git a/src/selectors/getVisibleTodos.js b/src/selectors/getVisibleTodos.js index 98204923..d9b56bdf 100644 --- a/src/selectors/getVisibleTodos.js +++ b/src/selectors/getVisibleTodos.js @@ -1,6 +1,6 @@ // @flow import { createSelector } from 'reselect' -import { FILTER_CURRENT, FILTER_DONE } from 'src/redux/modules/examples/todos' +import { FILTER_CURRENT, FILTER_DONE } from 'src/redux/modules/examples/todos/consts' import type { RootReducerState } from 'src/redux/modules'