Commit da3d378b authored by Florent Chehab's avatar Florent Chehab

refactor(frontend api interactions)

* Complete redesign of the api actions / reducers,
* Now makes use of Axios,
* No more black magic,
* Doc updated,

BREAKING: All `...Specific` reducers results are no under `...One` and `readSpecific` moved to `readOne`

Also,
* Fixed bug regarding opposite user_can_moderate value returned by backend

Fixes #98
parent 3a615383
Pipeline #37648 passed with stages
in 4 minutes and 18 seconds
......@@ -162,7 +162,7 @@ class EssentialModuleSerializer(BaseModelSerializer):
return {
"user_can_edit": user_can_edit,
"user_can_moderate": is_moderation_required(
"user_can_moderate": not is_moderation_required(
self.Meta.model, obj, self.get_user_from_request()
),
}
......
......@@ -188,7 +188,7 @@ That's all! :confetti_ball:
## Redux and the backend API
!> :warning: This section if probably the most important if you are already familiar with redux. Because we have tons of backend API endpoints, we have implemented a generic way to use them with redux. :warning:
!> :warning: This section if probably the most important one if you are already familiar with redux. Because we have tons of backend API endpoints, we have implemented a generic way to use them with redux. :warning:
### Dynamic actions and reducers
......@@ -211,9 +211,6 @@ const mapDispatchToProps = (dispatch) => {
return {
api: {
universities: () => dispatch(getActions("universities").readAll()),
mainCampuses: () => dispatch(getActions("mainCampuses").readAll()),
cities: () => dispatch(getActions("cities").readAll()),
countries: () => dispatch(getActions("countries").readAll())
}
};
};
......@@ -221,16 +218,15 @@ const mapDispatchToProps = (dispatch) => {
// ......
```
*NB: more on why we put it inside `api` in the next section.*
*NB: more on why we put it a sub-object `api` in the next section.*
The `getActions` function will give you access to all of the following functions:
The `getActions` function will give you access to all of the following functions (which are defined in the `CrudActions` class -- `frontend/src/redux/api/CrudActions.js`):
- `readAll(params={})`: reads all the data from the endpoint (data will be given as an array of object)
- `readSpecific(id, params={})`: reads the data for an instance (the `id` is the instance's `id`/primary key) (data will be returned as an object)
- `create(data)`: creates an object on the endpoint with attributes values given by `data`.
- `update(data)` (`data` must have an `id` field): updates the instance identified by `id`.
- `setInvalidatedAll()`: invalidates "all" the data for the endpoint,
- `setInvalidatedSpecific()`: invalidates "specific" data from the endpoint.
- `readAll(params={})`
- `readOne(id, params={})`
- `create(data, onSuccessCallback = (newData) => { }, params = {})`
- `update(id, data, onSuccessCallback = (newData) => { }, params = {})`
- `delete(id, onSuccessCallback = () => { }, params = {})`
?> `params` is an object that accepts two keys: `queryParams` and `endPointAttr`. In most cases it should be created through the `getQueryParams` and `getEndPointAttr` (and `mapDispatchToProps`; see just before [this section](Application/Frontend/redux?id=words-on-updating-data-on-the-api)) functions of your components.
......@@ -238,20 +234,41 @@ The `getActions` function will give you access to all of the following functions
> * `queryParams` should be an object that maps the fields to the values you want to filter on (`queryParams = {university: 1, country: 2}` will render as `/api/endpoint?university=1&country=2`).
> * `endPointAttr` should be an array of the endpoint attributes to add to the endpoint (`endPointAttr = [10, 11]` will render as `/api/endpoint/10/11/`)
?> `id` should be the id of the object your want to read, update or delete.
?> `data` would be the expected content of the model instance
?> `onSuccessCallback` is a callback called if the action is successful. The returned data from the API is given to it as a parameter. **It is useful to ease some interactions inside your components: you can boycott redux in some way.**
You also have actions to clear the failures if you need:
- `clearReadAllFailed()`
- `clearReadOneFailed()`
- `clearCreateFailed()`
- `clearUpdateFailed()`
- `clearDeleteFailed()`
And actions related to invalidating the data:
- `invalidateAll()`
- `clearInvalidationAll()`
- `invalidateOne()`
- `clearInvalidationOne()`
!> Not all actions might be performed on any given endpoint: your request my get rejected by the backend depending on the viewset's `permission_classes`, the object, the user who sent the request, etc.
?> :information_desk_person: invalidating data will usually trigger a refresh of that data with a new API call.
?> :information_desk_person: invalidating data will usually trigger a refresh of that data with a new API call. This behavior is implementer in `CustomComponentForApi`
?> Do you recall that an update to the redux store will be propagated to the components that imported that portion of the store (concerned by the update). So they will be updated on their own every time new data comes in, like magic! :confetti_ball:
!> If a viewset is *configured* with endpoint `end_point_route = "universities"`, then in `getActions` you will have access to it by specifying that exact endpoint, ie `getActions("universities")`.
!> `...Specific` and `...All` are stored in distinct location in the the redux store.
!> `...One` and `...All` are stored in distinct location in the the redux store.
### Conventions for reading data
If you are familiar with network request, you will know that those are "async", and that they can go through different states: from `reading` to `readSucceeded` or `readFailed` for instance. We need to keep track of those state so that the UI is coherent and let the user know what is the current state. Therefore, all those different states are stored for all possible actions (and for all possible endpoints) in... the redux store :confetti_ball:.
If you are familiar with network request, you will know that those are "async", and that they can go through different states: from `isReading` to `readSucceeded` or `readFailed` for instance. We need to keep track of those state so that the UI is coherent and let the user know what is the current state. Therefore, all those different states are stored for all possible actions (and for all possible endpoints) in... the redux store :confetti_ball:.
As a result, the real data returned by the endpoint will usually be stored under `...Succeeded.data` state portion.
......@@ -270,7 +287,7 @@ For the `CustomComponentForAPI` to work properly, i.e. for it to fetch the neede
```js
const mapStateToProps = (state) => {
return {
propName: state.api.whateverAll // or whateverSpecific
propName: state.api.whateverAll // or whateverOne
};
};
......@@ -304,7 +321,7 @@ TODO
- Open your console and look at the actions and resulting states that are being logged: explore.
- As explained earlier, the values from the api get be read either for "all" or a "specific" instance. So you need to specify the one you are interested in when getting the action (eg: `.readAll` or `.readSpecific`) and you need to specify the matching one you are retrieving from the redux state: `state.api.whateverAll` or `state.api.whateverSpecific`.
- As explained earlier, the values from the api get be read either for "all" or a "one" instance. So you need to specify the one you are interested in when getting the action (eg: `.readAll` or `.readOne`) and you need to specify the matching one you are retrieving from the redux state: `state.api.whateverAll` or `state.api.whateverOne`.
### Under the hood
......
......@@ -1828,6 +1828,15 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"axios": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
"requires": {
"follow-redirects": "^1.3.0",
"is-buffer": "^1.1.5"
}
},
"babel-eslint": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz",
......@@ -4615,7 +4624,6 @@
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
"integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
"dev": true,
"requires": {
"debug": "^3.2.6"
},
......@@ -4624,7 +4632,6 @@
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
......@@ -4632,8 +4639,7 @@
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
......@@ -7643,11 +7649,6 @@
"integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
"dev": true
},
"js-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz",
"integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s="
},
"js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
......
......@@ -25,10 +25,10 @@
"@material-ui/core": "^3.9.2",
"@material-ui/icons": "^3.0.2",
"@material-ui/lab": "^3.0.0-alpha.30",
"axios": "^0.18.0",
"date-fns": "^2.0.0-alpha.25",
"downshift": "^3.2.3",
"fuzzysort": "^1.1.4",
"js-cookie": "^2.2.0",
"keycode": "^2.2.0",
"leaflet": "^1.4.0",
"lodash": "^4.17.11",
......
......@@ -198,7 +198,7 @@ class CustomComponentForAPI extends Component {
*/
performReadFromApi(propName) {
let endPointAttr = "",
let endPointAttr = [],
queryParams = {};
const tmpQueryParams = this.getQueryParams(propName);
......
......@@ -93,18 +93,19 @@ ThemeProvider.propTypes = {
const mapStateToProps = (state) => {
return {
themeSavedInTheApp: state.app.appTheme,
userData: state.api.userDataSpecific
userData: state.api.userDataOne
};
};
const mapDispatchToProps = (dispatch) => {
return {
saveTheme: (theme) => dispatch(saveAppTheme(theme)),
saveUserData: (data) => dispatch(getActions("userData").update(data)),
// eslint-disable-next-line no-undef
saveUserData: (data) => dispatch(getActions("userData").update(__AppUserId, data)),
api: {
// __AppUserId is defined in the html rendered by django
// eslint-disable-next-line no-undef
userData: () => dispatch(getActions("userData").readSpecific(__AppUserId)),
userData: () => dispatch(getActions("userData").readOne(__AppUserId)),
},
};
};
......
......@@ -44,7 +44,7 @@ class DateField extends Field {
}
/**
* Specific error detection for this field
* One error detection for this field
* @override
* @returns {CustomError}
* @memberof MarkdownField
......
......@@ -25,7 +25,7 @@ class MarkdownField extends Field {
defaultNullValue = "";
/**
* Specific error detection for this field
* One error detection for this field
*
* @override
* @returns {CustomError}
......
......@@ -32,7 +32,7 @@ export default {
renderObjModerationLevelField() {
// hack to access directly the store and get the value we need.
const userData = getLatestReadDataFromStore("userDataSpecific"),
const userData = getLatestReadDataFromStore("userDataOne"),
possibleObjModeration = getObjModerationLevel(userData.owner_level, true);
if (possibleObjModeration.length > 1) {
return (
......
......@@ -345,7 +345,7 @@ const mapStateToProps = (state) => {
return {
currentUiTheme: state.app.appTheme.theme,
state: state.app.colorPickerState,
userData: state.api.userDataSpecific
userData: state.api.userDataOne
};
};
......@@ -353,11 +353,12 @@ const mapDispatchToProps = (dispatch) => {
return {
saveTheme: (theme) => dispatch(saveAppTheme(theme)),
saveColorPicker: (partialState) => dispatch(saveAppColorPicker(partialState)),
saveToServer: (data) => dispatch(getActions("userData").update(data)),
// eslint-disable-next-line no-undef
saveToServer: (data) => dispatch(getActions("userData").update(__AppUserId, data)),
api: {
// __AppUserId is defined in the html rendered by django
// eslint-disable-next-line no-undef
userData: () => dispatch(getActions("userData").readSpecific(__AppUserId)), // id not needed userData
userData: () => dispatch(getActions("userData").readOne(__AppUserId)), // id not needed userData
},
};
};
......
......@@ -16,7 +16,7 @@ export default function getMapDispatchToPropsForEditor(name) {
saveData: (data, onSuccessCallback = (newData) => { }) => {
if ("id" in data) { // it's an update
lastSave = "update";
dispatch(getActions(name).update(data, onSuccessCallback));
dispatch(getActions(name).update(data.id, data, onSuccessCallback));
} else { // it's a create
lastSave = "create";
dispatch(getActions(name).create(data, onSuccessCallback));
......
......@@ -9,7 +9,7 @@ import { getLatestRead } from "../../../../redux/api/utils";
* @returns
*/
export default function getMapStateToPropsForEditor(elKey) {
const propName = `${elKey}Specific`;
const propName = `${elKey}One`;
return (state) => {
let lastUpdateTimeInModel = null;
......
......@@ -87,7 +87,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
countryDri: ({ params }) => dispatch(getActions("countryDri").readAll(params)),
},
invalidateData: () => dispatch(getActions("countryDri").setInvalidatedAll(true))
invalidateData: () => dispatch(getActions("countryDri").invalidateAll())
};
};
......
......@@ -108,7 +108,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
countryScholarships: ({ params }) => dispatch(getActions("countryScholarships").readAll(params)),
},
invalidateData: () => dispatch(getActions("countryScholarships").setInvalidatedAll(true))
invalidateData: () => dispatch(getActions("countryScholarships").invalidateAll())
};
};
......
......@@ -91,7 +91,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
universityDri: ({ params }) => dispatch(getActions("universityDri").readAll(params)),
},
invalidateData: () => dispatch(getActions("universityDri").setInvalidatedAll(true))
invalidateData: () => dispatch(getActions("universityDri").invalidateAll())
};
};
......
......@@ -88,16 +88,16 @@ UniversityGeneral.propTypes = {
const mapStateToProps = (state) => {
return {
university: state.api.universitiesSpecific,
university: state.api.universitiesOne,
};
};
const mapDispatchToProps = (dispatch) => {
return {
api: {
university: ({ props }) => dispatch(getActions("universities").readSpecific(props.univId)),
university: ({ props }) => dispatch(getActions("universities").readOne(props.univId)),
},
invalidateData: () => dispatch(getActions("universities").setInvalidatedSpecific(true))
invalidateData: () => dispatch(getActions("universities").invalidateOne())
};
};
......
......@@ -102,7 +102,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
universityScholarships: ({ params }) => dispatch(getActions("universityScholarships").readAll(params)),
},
invalidateData: () => dispatch(getActions("universityScholarships").setInvalidatedAll(true))
invalidateData: () => dispatch(getActions("universityScholarships").invalidateAll())
};
};
......
......@@ -129,16 +129,16 @@ UniversitySemestersDates.propTypes = {
const mapStateToProps = (state) => {
return {
universitySemestersDates: state.api.universitiesSemestersDatesSpecific,
universitySemestersDates: state.api.universitiesSemestersDatesOne,
};
};
const mapDispatchToProps = (dispatch) => {
return {
api: {
universitySemestersDates: ({ props }) => dispatch(getActions("universitiesSemestersDates").readSpecific(props.univId)),
universitySemestersDates: ({ props }) => dispatch(getActions("universitiesSemestersDates").readOne(props.univId)),
},
invalidateData: () => dispatch(getActions("universitiesSemestersDates").setInvalidatedSpecific(true))
invalidateData: () => dispatch(getActions("universitiesSemestersDates").invalidateOne())
};
};
......
......@@ -46,7 +46,7 @@ class History extends CustomComponentForAPI {
getEndPointAttr = (propName) => {
if (propName === "versions") {
const { contentTypeId, id } = this.props.modelInfo;
return `${contentTypeId}/${id}`;
return [contentTypeId, id];
} else {
return undefined;
}
......@@ -253,7 +253,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
versions: ({ params }) => dispatch(getActions("versions").readAll(params)),
},
resetVersions: () => dispatch(getActions("versions").setInvalidatedAll(true)),
resetVersions: () => dispatch(getActions("versions").invalidateAll()),
openFullScreenDialog: (innerNodes) => dispatch(openFullScreenDialog(innerNodes)),
closeFullScreenDialog: () => dispatch(closeFullScreenDialog()),
};
......
......@@ -55,7 +55,7 @@ class ModuleGroupWrapper extends Component {
render() {
const { classes, groupTitle, endPoint, defaultModelData } = this.props,
userCanPostTo = getLatestReadDataFromStore("userDataSpecific").owner_can_post_to,
userCanPostTo = getLatestReadDataFromStore("userDataOne").owner_can_post_to,
disabled = userCanPostTo.indexOf(endPoint) < 0;
return (
......
......@@ -35,7 +35,7 @@ class PendingModeration extends CustomComponentForAPI {
getEndPointAttr = (propName) => {
if (propName === "pendingModeration") {
const { contentTypeId, id } = this.props.modelInfo;
return `${contentTypeId}/${id}`;
return [contentTypeId, id];
} else {
return undefined;
}
......@@ -201,7 +201,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
pendingModeration: ({ params }) => dispatch(getActions("pendingModerationObj").readAll(params)),
},
resetPendingModeration: () => dispatch(getActions("pendingModerationObj").setInvalidatedAll(true)),
resetPendingModeration: () => dispatch(getActions("pendingModerationObj").invalidateAll()),
openFullScreenDialog: (innerNodes) => dispatch(openFullScreenDialog(innerNodes)),
closeFullScreenDialog: () => dispatch(closeFullScreenDialog()),
};
......
......@@ -2,33 +2,18 @@
* This file contains the functions and class to create the CRUD actions
*/
import SmartActions from "./SmartActions";
import getCrudActionTypes from "./getCrudActionTypes";
import axiosLib from "axios";
/** base configuration of axios. */
const axios = axiosLib.create({
baseURL: "/api",
xsrfCookieName: "csrftoken",
xsrfHeaderName: "X-CSRFToken",
});
// Read generic functions
function _isReading(status, type) {
return {
type,
status
};
}
function _readFailed(failed, error, type) {
return {
type,
failed,
error
};
}
function _readSucceeded(data, type) {
return {
type,
data,
};
}
/** Dumb action */
const emptyAction = () => { };
/**
......@@ -38,243 +23,395 @@ function _readSucceeded(data, type) {
* @class CrudActions
*/
export default class CrudActions {
interactionStatus = new Set();
/**
* Creates an instance of CrudActions.
* @param {object} apiInfo
* @param {string} route Api route associated with the instance
* @memberof CrudActions
*/
constructor(apiInfo) {
this.name = apiInfo.name;
this.apiEndPoint = apiInfo.apiEndPoint;
this.smartActions = new SmartActions();
this.types = getCrudActionTypes(this.name);
constructor(route) {
this.apiEndPoint = route;
this.types = getCrudActionTypes(route);
}
/**
*
*
*
*
*
*
*
* Helper functions
*
*
*
*
*
*
*/
// read all
/**
* Tells redux to read the root of the api endpoint
* PRIVATE
*
* @returns {function}
* Wrap the action to add the `rootType` for optimization purposes.
*
* @param {object} action
* @returns {object}
* @memberof CrudActions
*/
readAll(params = {}) {
const self = this;
wrap(action) {
return Object.assign(action, { rootType: this.types.rootType });
}
function isReadingAll(isReadingAll) {
return _isReading(isReadingAll, self.types.isReadingAll);
}
/**
* PRIVATE
*
* Builds the correct URL for the request
*
* @param {object} params Parameters for the request
* @param {Array.<string>} params.endPointAttr attributes to add to the endpoint route
* @param {object.<string,string>} params.queryParams Object with the query parameters
* @param {string} [id=""]
* @returns
* @memberof CrudActions
*/
getUrl(params, id = "") {
let endPointAttr = [],
queryParams = "";
function readAllSucceeded(data) {
return _readSucceeded(data, self.types.readAllSucceeded);
// We start by "preparing" the request parameters and endpoint attributes
if (params.endPointAttr) {
endPointAttr = params.endPointAttr;
}
if (params.queryParams) {
let queryParamsTmp = Object.entries(params.queryParams).map(([key, val]) => `${key}=${val}`).join("&");
if (queryParamsTmp !== "") {
queryParams = `?${queryParamsTmp}`;
}
}
function readAllFailed(bool, error = null) {
return _readFailed(bool, error, self.types.readAllFailed);
// Then we build the final URL
let url;
if (id !== "") {
url = [this.apiEndPoint, ...endPointAttr, id, ""].join("/");
} else {
url = [this.apiEndPoint, ...endPointAttr, queryParams].join("/");
}
return this.smartActions._FetchData(
"",
params,
this.apiEndPoint,
isReadingAll,
readAllSucceeded,
this.setInvalidatedAll.bind(this),
readAllFailed,
false
);
// clean the end point url from doubles //
return url.replace(/\/+/g, "/");
}
// Read one
/**
* Tells redux to read a specific element at the api endpoint
* PRIVATE
*
* Helper function around axios to make the request to the backend API
*
* @param {object} config axios config (see axios doc)
* @param {string} temporaryType Type to dispatch when things start
* @param {string} successType Type to dispatch when the request is successful
* @param {string} errorType Type to dispatch when an error occurred during request
* @param {function} [onSuccessCallback=emptyAction] Callback that will be called if request is successful. The returned data is passed to the callback.
* @returns {function}
* @memberof CrudActions
*/
readSpecific(id = "", params = {}) {
const self = this;
performAxios(config, temporaryType, successType, errorType, onSuccessCallback = emptyAction) {
const self = this,
requestId = `${config.method}_${config.url}`;
function isReadingSpecific(isReadingSpecific) {
return _isReading(isReadingSpecific, self.types.isReadingSpecific);
// a bit of optimization to make sure not to make overlapping requests
if (this.interactionStatus.has(requestId)) {
return emptyAction;
}
function readSpecificSucceeded(data) {
return _readSucceeded(data, self.types.readSpecificSucceeded);
}
this.interactionStatus.add(requestId);
return (dispatch) => {
const wrapDispatch = (action) => dispatch(this.wrap(action));
wrapDispatch({ type: temporaryType });
axios.request(config)
.then(({ data }) => {
wrapDispatch({ type: successType, data });
onSuccessCallback(data);
})
.catch((error) => {
wrapDispatch({ type: errorType, error });
})
.then(() => self.interactionStatus.delete(requestId));
};
}
function readSpecificFailed(bool, error = null) {
return _readFailed(bool, error, self.types.readSpecificFailed);
}
return this.smartActions._FetchData(
id,
params,
this.apiEndPoint,
isReadingSpecific,
readSpecificSucceeded,
this.setInvalidatedSpecific.bind(this),
readSpecificFailed,
true
);
}
/**
*
*
*
*
*
*
*
* Actions
*
*
*
*
*
*
*
*/
/**
*
*
* Read all
*
*
*/
/**
* Read all objects on the API route.
* You can use request parameters to filter on the endpoint. Set them in the `params` attributes.
* You can specify endpoint attrs with `params`.
*
* @param {Object} [params={}] Parameters for the request
* @param {Array.<string>} params.endPointAttr attributes to add to the endpoint route
* @param {Object.<string,string>} params.queryParams Object with the query parameters
* @returns {function}
* @memberof CrudActions
*/
readAll(params = {}) {
const url = this.getUrl(params),
config = { url, method: "get" },
{ readAllStarted, readAllSucceeded, readAllFailed } = this.types;
/////
/////
// Not readonly functions
/////
return this.performAxios(config, readAllStarted, readAllSucceeded, readAllFailed);
}
/**
* Action to reset a read all failure.
*
* @returns
* @memberof CrudActions
*/
clearReadAllFailed = () => this.wrap({
type: this.types.clearReadAllFailed