Skip to content

Commit 1cf3205

Browse files
authored
Merge pull request #10 from tb/refactor-actions
Refactor actions
2 parents 1251ab9 + abae6f5 commit 1cf3205

File tree

15 files changed

+160
-162
lines changed

15 files changed

+160
-162
lines changed

client/.eslintrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ globals:
2222

2323
rules:
2424
import/no-named-as-default: 0
25+
import/prefer-default-export: 0
2526
no-console: 0
2627
no-param-reassign: 0
2728
no-shadow: 0 # FIXME extract generic Edit component

client/src/api/client.js

Lines changed: 6 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,11 @@ import {
1111
zipObject,
1212
} from 'lodash';
1313

14-
import { denormalize, normalize } from './normalize';
14+
import { normalize } from './normalize';
1515

16-
export const GET_ONE = 'GET_ONE';
17-
export const GET_LIST = 'GET_LIST';
18-
export const GET_MANY = 'GET_MANY';
19-
export const CREATE = 'CREATE';
20-
export const UPDATE = 'UPDATE';
21-
export const DELETE = 'DELETE';
22-
export const AUTH_LOGIN = 'AUTH_LOGIN';
23-
export const AUTH_LOGOUT = 'AUTH_LOGOUT';
16+
export { denormalize } from './normalize';
2417

25-
const client = axios.create({
18+
export const client = axios.create({
2619
baseURL: '/',
2720
headers: {
2821
Accept: 'application/vnd.api+json',
@@ -56,9 +49,9 @@ client.interceptors.request.use(
5649

5750
const stringifyParams = params => qs.stringify(params, { format: 'RFC1738', arrayFormat: 'brackets' });
5851

59-
const withParams = (url, params) => `${url}?${stringifyParams(params)}`;
52+
export const withParams = (url, params) => `${url}?${stringifyParams(params)}`;
6053

61-
const normalizeResponse = (response) => {
54+
export const normalizeResponse = (response) => {
6255
const { data = [], included = [] } = response.data;
6356
const dataByType = groupBy(castArray(data).concat(included), 'type');
6457

@@ -73,71 +66,11 @@ const normalizeResponse = (response) => {
7366
}));
7467
};
7568

76-
const normalizeErrors = (response) => {
69+
export const normalizeErrors = (response) => {
7770
throw get(response, 'response.data.errors')
7871
.reduce((errors, error) => {
7972
const attribute = /\/data\/[a-z]*\/(.*)$/.exec(get(error, 'source.pointer'))[1];
8073
set(errors, attribute.split('/'), error.title);
8174
return errors;
8275
}, {});
8376
};
84-
85-
export default (requestType, payload, meta) => {
86-
const {
87-
url = `${meta.key}`,
88-
include,
89-
} = meta;
90-
91-
const params = payload;
92-
93-
switch (requestType) {
94-
case GET_ONE:
95-
return client({
96-
url: withParams(`${url}/${payload.id}`, params),
97-
method: 'GET',
98-
data: JSON.stringify(payload),
99-
}).then(normalizeResponse);
100-
case GET_MANY:
101-
case GET_LIST:
102-
return client({
103-
url: withParams(`${url}`, params),
104-
method: 'GET',
105-
data: JSON.stringify(payload),
106-
}).then(normalizeResponse).then(res => ({ ...res, params }));
107-
case CREATE:
108-
return client({
109-
url: withParams(url, { include }),
110-
method: 'POST',
111-
data: denormalize(meta.key, payload),
112-
}).then(normalizeResponse).catch(normalizeErrors);
113-
case UPDATE: {
114-
return client({
115-
url: withParams(`${url}/${payload.id}`, { include }),
116-
method: 'PUT',
117-
data: denormalize(meta.key, payload),
118-
}).then(normalizeResponse).catch(normalizeErrors);
119-
}
120-
case DELETE:
121-
return client({
122-
url: withParams(`${url}/${payload.id}`),
123-
method: 'DELETE',
124-
}).then(() => ({ data: payload }));
125-
case AUTH_LOGIN:
126-
return client({
127-
url: 'auth/sign_in',
128-
method: 'POST',
129-
data: payload,
130-
}).then(response => ({
131-
...response.data.data,
132-
...pick(response.headers, ['access-token', 'client']),
133-
}));
134-
case AUTH_LOGOUT:
135-
return client({
136-
url: 'auth/sign_out',
137-
method: 'DELETE',
138-
data: payload,
139-
});
140-
default:
141-
throw new Error(`No client handler for ${requestType}`);
142-
}
143-
};

client/src/api/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from './client';
2-
export client from './client';

client/src/components/App.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
33
import { isEmpty } from 'lodash';
44
import { Collapse, Container, Navbar, NavbarToggler, Nav, NavItem, NavLink } from 'reactstrap';
55

6-
import { getUser, logout } from '../store/api';
6+
import { getUser, logout } from '../store/auth';
77

88
export class App extends Component {
99
state = {

client/src/components/Auth/Login.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { SubmissionError } from 'redux-form';
55

66
import { CardSingle } from '../UI';
77
import LoginForm from './LoginForm';
8-
import { login } from '../../store/api';
8+
import { login } from '../../store/auth';
99

1010
export class Login extends Component {
1111
onSubmit = values => this.props.login(values)

client/src/components/Routes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { PureComponent, PropTypes } from 'react';
22
import { Router, Route, IndexRoute } from 'react-router';
33
import { UserAuthWrapper } from 'redux-auth-wrapper';
44

5-
import { getUser } from '../store/api';
5+
import { getUser } from '../store/auth';
66
import App from './App';
77
import Dashboard from './Dashboard';
88
import { PostList, PostEdit } from './Posts';

client/src/store/api/actions.js

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,51 @@
11
import {
2-
GET_ONE,
3-
GET_LIST,
4-
GET_MANY,
5-
CREATE,
6-
UPDATE,
7-
DELETE,
8-
AUTH_LOGIN,
9-
AUTH_LOGOUT,
102
client,
3+
withParams,
4+
normalizeResponse,
5+
normalizeErrors,
6+
denormalize,
117
} from '../../api';
128

13-
export const STARTED = 'STARTED';
14-
export const SUCCESS = 'SUCCESS';
15-
export const FAILED = 'FAILED';
9+
import {
10+
createAsyncActionType,
11+
createAsyncAction,
12+
} from '../utils';
13+
14+
export const GET_ONE = createAsyncActionType('GET_ONE');
15+
export const GET_LIST = createAsyncActionType('GET_LIST');
16+
export const GET_MANY = createAsyncActionType('GET_MANY');
17+
export const CREATE = createAsyncActionType('CREATE');
18+
export const UPDATE = createAsyncActionType('UPDATE');
19+
export const DELETE = createAsyncActionType('DELETE');
1620

17-
export const actionType = (request, status) => `@@api/${request}/${status}`;
21+
export const fetchOne = createAsyncAction(GET_ONE, (payload, meta) => client({
22+
url: withParams(meta.url, { include: meta.include, ...payload }),
23+
method: 'GET',
24+
data: JSON.stringify(payload),
25+
}).then(normalizeResponse));
1826

19-
const createAction = (request, status) => (key, payload, meta = {}) => ({
20-
type: actionType(request, status),
21-
payload,
22-
meta: { ...meta, status },
23-
error: status === FAILED ? true : undefined,
27+
export const fetchList = createAsyncAction(GET_LIST, (payload, meta) => {
28+
const params = { include: meta.include, ...payload };
29+
return client({
30+
url: withParams(meta.url, params),
31+
method: 'GET',
32+
data: JSON.stringify(payload),
33+
}).then(normalizeResponse).then(res => ({ ...res, params }));
2434
});
2535

26-
const createAsyncAction = request => (key, payload = {}, _meta = {}) => (dispatch) => {
27-
const meta = { ..._meta, key, request };
28-
dispatch(createAction(request, STARTED)(key, payload, meta));
29-
return client(request, payload, meta)
30-
.then((response) => {
31-
dispatch(createAction(request, SUCCESS)(key, response, meta));
32-
return response;
33-
})
34-
.catch((error) => {
35-
dispatch(createAction(request, FAILED)(key, error, meta));
36-
throw error;
37-
});
38-
};
36+
export const createResource = createAsyncAction(CREATE, (payload, meta) => client({
37+
url: withParams(meta.url, { include: meta.include }),
38+
method: 'POST',
39+
data: denormalize(meta.key, payload),
40+
}).then(normalizeResponse).catch(normalizeErrors));
41+
42+
export const updateResource = createAsyncAction(UPDATE, (payload, meta) => client({
43+
url: withParams(`${meta.url}/${payload.id}`, { include: meta.include }),
44+
method: 'PUT',
45+
data: denormalize(meta.key, payload),
46+
}).then(normalizeResponse).catch(normalizeErrors));
3947

40-
export const fetchOne = createAsyncAction(GET_ONE);
41-
export const fetchList = createAsyncAction(GET_LIST);
42-
export const fetchMany = createAsyncAction(GET_MANY);
43-
export const createResource = createAsyncAction(CREATE);
44-
export const updateResource = createAsyncAction(UPDATE);
45-
export const deleteResource = createAsyncAction(DELETE);
46-
export const login = createAsyncAction(AUTH_LOGIN);
47-
export const logout = createAsyncAction(AUTH_LOGOUT);
48+
export const deleteResource = createAsyncAction(DELETE, (payload, meta) => client({
49+
url: withParams(`${meta.url}/${payload.id}`),
50+
method: 'DELETE',
51+
}).then(() => ({ data: payload })));

client/src/store/api/reducer.js

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import imm from 'object-path-immutable';
22
import {
33
get,
4-
map,
5-
keys,
6-
keyBy,
74
isEmpty,
5+
keyBy,
6+
keys,
7+
map,
8+
pick,
89
without,
910
} from 'lodash';
1011

@@ -15,21 +16,8 @@ import {
1516
CREATE,
1617
UPDATE,
1718
DELETE,
18-
AUTH_LOGIN,
19-
AUTH_LOGOUT,
20-
} from '../../api';
21-
22-
import {
23-
STARTED,
24-
SUCCESS,
25-
FAILED,
26-
actionType,
2719
} from './actions';
2820

29-
const initialState = {
30-
user: JSON.parse(localStorage.getItem('user') || '{}'),
31-
};
32-
3321
const addNormalized = (newState, payload) => {
3422
keys(payload.normalized).forEach((key) => {
3523
payload.normalized[key].forEach((item) => {
@@ -39,57 +27,47 @@ const addNormalized = (newState, payload) => {
3927
return newState;
4028
};
4129

42-
export default (state = initialState, action) => {
43-
const { type, payload, meta } = action;
30+
const initialState = {};
31+
32+
export default (state = initialState, { type, payload, meta }) => {
4433
const { key, list = 'list' } = meta || {};
4534
let newState = state;
4635

4736
switch (type) {
48-
case actionType(GET_ONE, SUCCESS): {
37+
case GET_ONE.SUCCESS: {
4938
return addNormalized(newState, payload);
5039
}
51-
case actionType(GET_LIST, STARTED): {
40+
case GET_LIST.STARTED: {
5241
return imm.set(newState, [key, list, 'loading'], true);
5342
}
54-
case actionType(GET_LIST, SUCCESS): {
43+
case GET_LIST.SUCCESS: {
5544
newState = addNormalized(newState, payload);
56-
newState = imm.set(newState, [key, list, 'ids'], map(payload.data, 'id'));
57-
newState = imm.set(newState, [key, list, 'params'], payload.params);
58-
newState = imm.set(newState, [key, list, 'links'], payload.links);
59-
newState = imm.set(newState, [key, list, 'meta'], payload.meta);
60-
newState = imm.set(newState, [key, list, 'loading'], false);
61-
return newState;
45+
return imm.set(newState, [key, list], {
46+
ids: map(payload.data, 'id'),
47+
loading: false,
48+
...pick(payload, ['params', 'links', 'meta']),
49+
});
6250
}
63-
case actionType(GET_MANY, SUCCESS): {
51+
case GET_MANY.SUCCESS: {
6452
return addNormalized(newState, payload);
6553
}
66-
case actionType(CREATE, SUCCESS): {
54+
case CREATE.SUCCESS: {
6755
newState = addNormalized(newState, payload);
6856
if (list) {
6957
newState = imm.push(newState, [key, list, 'ids'], payload.data.id);
7058
}
7159
return newState;
7260
}
73-
case actionType(UPDATE, SUCCESS): {
61+
case UPDATE.SUCCESS: {
7462
return addNormalized(newState, payload);
7563
}
76-
case actionType(DELETE, SUCCESS): {
64+
case DELETE.SUCCESS: {
7765
newState = imm.del(newState, [key, 'byId', payload.data.id]);
7866
newState = imm.set(newState, [key, list, 'ids'],
7967
without(get(newState, [key, list, 'ids']), payload.data.id),
8068
);
8169
return newState;
8270
}
83-
case actionType(AUTH_LOGIN, SUCCESS): {
84-
localStorage.setItem('user', JSON.stringify(payload));
85-
newState = imm.set(newState, ['user'], payload);
86-
return newState;
87-
}
88-
case actionType(AUTH_LOGOUT, SUCCESS): {
89-
localStorage.removeItem('user');
90-
newState = imm.set(newState, ['user'], {});
91-
return newState;
92-
}
9371
default:
9472
return state;
9573
}

client/src/store/api/selectors.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import { get, isEmpty } from 'lodash';
2-
3-
export const getUser = state =>
4-
get(state, ['api', 'user']) || {};
1+
import { compact, get, isEmpty } from 'lodash';
52

63
export const getOne = (state, resourceName, id) =>
74
get(state, ['api', resourceName, 'byId', id]) || {};
@@ -16,10 +13,20 @@ export const getMany = (state, resourceName, ids) => {
1613
: (ids || Object.keys(byId)).map(id => byId[id]);
1714
};
1815

16+
const emptyList = {
17+
data: [],
18+
ids: [],
19+
links: {},
20+
meta: {},
21+
params: { page: {}, filter: {} },
22+
loading: true,
23+
};
24+
1925
export const getList = (state, resourceName, listName = 'list') => {
2026
const byId = get(state, ['api', resourceName, 'byId']) || {};
2127
const list = get(state, ['api', resourceName, listName]) || {};
28+
2229
return !list.ids
23-
? { data: [], ids: [], links: {}, params: { page: {}, filter: {} }, loading: true, empty: true }
24-
: { ...list, empty: false, data: list.ids.map(id => byId[id]) };
30+
? { ...emptyList, empty: true }
31+
: { ...emptyList, empty: false, ...list, data: compact(list.ids.map(id => byId[id])) };
2532
};

client/src/store/auth/actions.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { pick } from 'lodash';
2+
3+
import { client } from '../../api';
4+
import { createAsyncActionType, createAsyncAction } from '../utils';
5+
6+
export const AUTH_LOGIN = createAsyncActionType('AUTH_LOGIN');
7+
export const AUTH_LOGOUT = createAsyncActionType('AUTH_LOGOUT');
8+
9+
export const login = createAsyncAction(AUTH_LOGIN, payload => client({
10+
url: 'auth/sign_in',
11+
method: 'POST',
12+
data: payload,
13+
}).then(response => ({
14+
...response.data.data,
15+
...pick(response.headers, ['access-token', 'client']),
16+
})));
17+
18+
export const logout = createAsyncAction(AUTH_LOGOUT, payload => client({
19+
url: 'auth/sign_out',
20+
method: 'DELETE',
21+
data: payload,
22+
}));

0 commit comments

Comments
 (0)