Skip to content

Commit c33cb15

Browse files
authored
Merge pull request codeBelt#6 from codeBelt/feature/loading-indicator
Add Requesting and Error stores and selectors
2 parents 1c42241 + 1120e33 commit c33cb15

File tree

16 files changed

+286
-14
lines changed

16 files changed

+286
-14
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"husky": {
66
"hooks": {
7-
"pre-commit": "pretty-quick --staged",
7+
"pre-commit": "npm run lint && pretty-quick --staged",
88
"post-commit": "git update-index -g"
99
}
1010
},
@@ -40,6 +40,7 @@
4040
},
4141
"dependencies": {
4242
"axios": "0.19.0",
43+
"classnames": "2.2.6",
4344
"connected-react-router": "6.5.2",
4445
"dayjs": "1.8.16",
4546
"history": "4.9.0",
@@ -61,6 +62,7 @@
6162
},
6263
"devDependencies": {
6364
"@craco/craco": "5.4.0",
65+
"@types/classnames": "2.2.9",
6466
"@types/history": "4.7.3",
6567
"@types/jest": "24.0.18",
6668
"@types/node": "12.7.3",

src/models/IStore.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { RouterState } from 'connected-react-router';
22
import IShowState from '../stores/show/models/IShowState';
3+
import IRequestingState from '../stores/requesting/models/IRequestingState';
4+
import IErrorState from '../stores/error/models/IErrorState';
35

46
export default interface IStore {
7+
readonly error: IErrorState;
8+
readonly requesting: IRequestingState;
59
readonly router: RouterState;
610
readonly show: IShowState;
711
}

src/selectors/error/ErrorSelector.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createSelector, ParametricSelector } from 'reselect';
2+
import IErrorState from '../../stores/error/models/IErrorState';
3+
import IStore from '../../models/IStore';
4+
5+
export class ErrorSelector {
6+
public static selectRawErrors(errorState: IErrorState, actionTypes: string[]): IErrorState {
7+
return actionTypes.reduce((partialState: object, actionType: string) => {
8+
if (errorState[actionType]) {
9+
partialState[actionType] = errorState[actionType];
10+
}
11+
12+
return partialState;
13+
}, {});
14+
}
15+
16+
public static selectErrorText(errorState: IErrorState, actionTypes: string[]): string {
17+
const partialErrorState = ErrorSelector.selectRawErrors(errorState, actionTypes) as IErrorState;
18+
19+
const errorList: string[] = actionTypes.reduce((errorMessages: string[], actionType: string) => {
20+
if (partialErrorState[actionType]) {
21+
const { message, errors } = partialErrorState[actionType];
22+
const arrayOfErrors: string[] = errors.length ? errors : [message];
23+
24+
return errorMessages.concat(arrayOfErrors);
25+
}
26+
27+
return errorMessages;
28+
}, []);
29+
30+
return errorList.join(', ');
31+
}
32+
33+
public static hasErrors(errorState: IErrorState, actionTypes: string[]): boolean {
34+
return actionTypes.map((actionType: string) => errorState[actionType]).filter(Boolean).length > 0;
35+
}
36+
}
37+
38+
export const selectRawErrors: ParametricSelector<IStore, string[], IErrorState> = createSelector(
39+
(state: IStore) => state.error,
40+
(state: IStore, actionTypes: string[]) => actionTypes,
41+
ErrorSelector.selectRawErrors
42+
);
43+
44+
export const selectErrorText: ParametricSelector<IStore, string[], string> = createSelector(
45+
(state: IStore) => state.error,
46+
(state: IStore, actionTypes: string[]) => actionTypes,
47+
ErrorSelector.selectErrorText
48+
);
49+
50+
export const hasErrors: ParametricSelector<IStore, string[], boolean> = createSelector(
51+
(state: IStore) => state.error,
52+
(state: IStore, actionTypes: string[]) => actionTypes,
53+
ErrorSelector.hasErrors
54+
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createSelector, ParametricSelector } from 'reselect';
2+
import IRequestingState from '../../stores/requesting/models/IRequestingState';
3+
import IStore from '../../models/IStore';
4+
5+
export class RequestingSelector {
6+
public static selectRequesting(requestingState: IRequestingState, actionTypes: string[]): boolean {
7+
return actionTypes.some((actionType: string) => requestingState[actionType]);
8+
}
9+
}
10+
11+
export const selectRequesting: ParametricSelector<IStore, string[], boolean> = createSelector(
12+
(state: IStore) => state.requesting,
13+
(state: IStore, actionTypes: string[]) => actionTypes,
14+
RequestingSelector.selectRequesting
15+
);

src/stores/error/ErrorAction.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import IAction from '../../models/IAction';
2+
import ActionUtility from '../../utilities/ActionUtility';
3+
4+
export default class ErrorAction {
5+
public static readonly CLEAR_ALL: string = 'ErrorAction.CLEAR_ALL';
6+
public static readonly REMOVE: string = 'ErrorAction.REMOVE';
7+
8+
public static removeById(id: string): IAction<string> {
9+
return ActionUtility.createAction(ErrorAction.REMOVE, id);
10+
}
11+
12+
public static clearAll(): IAction<undefined> {
13+
return ActionUtility.createAction(ErrorAction.CLEAR_ALL);
14+
}
15+
}

src/stores/error/ErrorReducer.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Note: This reducer breaks convention on how reducers should be setup.
3+
*/
4+
import IErrorState from './models/IErrorState';
5+
import IAction from '../../models/IAction';
6+
import ErrorAction from './ErrorAction';
7+
import HttpErrorResponseModel from '../../models/HttpErrorResponseModel';
8+
9+
export default class ErrorReducer {
10+
public static readonly initialState: IErrorState = {};
11+
12+
public static reducer(state: IErrorState = ErrorReducer.initialState, action: IAction<any>): IErrorState {
13+
const { type, error, payload } = action;
14+
15+
/*
16+
* Removes a specific error by it's id.
17+
*/
18+
if (type === ErrorAction.REMOVE) {
19+
return Object.entries(state).reduce((newState: object, [key, value]: [string, HttpErrorResponseModel]) => {
20+
if (value.id !== payload) {
21+
newState[key] = value;
22+
}
23+
24+
return newState;
25+
}, {});
26+
}
27+
28+
/*
29+
* Removes all the stored errors.
30+
*/
31+
if (type === ErrorAction.CLEAR_ALL) {
32+
return ErrorReducer.initialState;
33+
}
34+
35+
/*
36+
* If the action type has the key word 'REQUEST_' and not '_FINISHED' then we
37+
* want to remove the old error because there is a new request happening.
38+
*/
39+
const isStartRequestType: boolean = [type.includes('REQUEST_') === true, type.includes('_FINISHED') === false].every(Boolean);
40+
41+
if (isStartRequestType === true) {
42+
return Object.entries(state).reduce((newState: object, [key, value]: [string, HttpErrorResponseModel]) => {
43+
if (key !== `${type}_FINISHED`) {
44+
newState[key] = value;
45+
}
46+
47+
return newState;
48+
}, {});
49+
}
50+
51+
/*
52+
* If the action type has the key word '_FINISHED' we use the type as the key value
53+
* for the error.
54+
*/
55+
const isFinishedRequestType: boolean = type.includes('_FINISHED');
56+
const isError: boolean = [isFinishedRequestType, error].every(Boolean);
57+
58+
if (isError === false) {
59+
return state;
60+
}
61+
62+
return {
63+
...state,
64+
[type]: payload!,
65+
};
66+
}
67+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import HttpErrorResponseModel from '../../../models/HttpErrorResponseModel';
2+
3+
export default interface IErrorState {
4+
readonly [key: string]: HttpErrorResponseModel;
5+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Note: This reducer breaks convention on how reducers should be setup.
3+
*/
4+
import IRequestingState from './models/IRequestingState';
5+
import IAction from '../../models/IAction';
6+
7+
export default class RequestingReducer {
8+
public static readonly initialState: IRequestingState = {};
9+
10+
public static reducer(state: IRequestingState = RequestingReducer.initialState, action: IAction<any>): IRequestingState {
11+
// We only take actions that include 'REQUEST_' in the type.
12+
const isRequestType: boolean = action.type.includes('REQUEST_');
13+
14+
if (isRequestType === false) {
15+
return state;
16+
}
17+
18+
// Remove the string '_FINISHED' from the action type so we can use it as the key on the state.
19+
const [requestName] = action.type.split('_FINISHED');
20+
// If the action type includes '_FINISHED'. The boolean value will be false. Otherwise we
21+
// assume it is a starting request and will be set to true.
22+
const isFinishedRequestType: boolean = action.type.includes('_FINISHED');
23+
24+
return {
25+
...state,
26+
[requestName]: isFinishedRequestType === false,
27+
};
28+
}
29+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default interface IRequestingState {
2+
readonly [key: string]: boolean;
3+
}

src/stores/rootReducer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import { connectRouter } from 'connected-react-router';
33
import { History } from 'history';
44
import IStore from '../models/IStore';
55
import ShowReducer from './show/ShowReducer';
6+
import RequestingReducer from './requesting/RequestingReducer';
7+
import ErrorReducer from './error/ErrorReducer';
68

79
export default (history: History): Reducer<IStore> => {
810
const reducerMap: ReducersMapObject<IStore> = {
11+
error: ErrorReducer.reducer,
12+
requesting: RequestingReducer.reducer,
913
router: connectRouter(history) as any,
1014
show: ShowReducer.reducer,
1115
};

0 commit comments

Comments
 (0)