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()),
};
......
This diff is collapsed.
This diff is collapsed.
import Cookies from "js-cookie";
/**
* Class to perform network request and store the "status" in the redux store.
* The request are performed in a smart way to prevent multiple request to same endpoint at the same time.
*
* @export
* @class SmartActions
*/
export default class SmartActions {
fetching = new Map();
/**
* Function to check if a request should be sent to the server.
* In fact, redux might be too slow, so this setup is too prevent useless call to the server
* and therefore useless update of the store.
*
* @param {string} endPoint
* @returns {boolean}
* @memberof SmartActions
*/
shouldFetchEndPoint(endPoint) {
return !this.fetching.has(endPoint);
}
/**
* Generic function for handling a GET request to the API
*
* @param {string} pk
* @param {object} params EndPoints attributes and query params
* @param {string} apiEndPoint
* @param {function} isReading
* @param {function} readDataSucceeded
* @param {function} invalidate
* @param {function} failed
* @param {boolean} [pkRequired=false]
* @returns {function} Function that takes the `dispatch` function of redux as argument.
* @memberof SmartActions
*/
_FetchData(pk, params, apiEndPoint, isReading, readDataSucceeded, invalidate, failed, pkRequired = false) {
if (pkRequired && (typeof pk == "undefined")) {
throw "pk shouldn't be empty when requesting a specific element";
}
if (!pkRequired && pk != "") {
throw "pk shouldn't be set if pkRequired is true";
}
let endPointAttr = "",
queryParams = "";
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}`;
}
}
let endPoint;
if (pk != "") {
endPoint = [apiEndPoint, endPointAttr, pk, ""].join("/");
} else {
endPoint = [apiEndPoint, endPointAttr, queryParams].join("/");
}
// clean the end point url from doubles //
endPoint = endPoint.replace(/\/+/g, "/");
if (!this.shouldFetchEndPoint(endPoint)) {
return () => { };
} else {
this.fetching.set(endPoint, true);
}
return (dispatch) => {
dispatch(isReading(true));
let token = Cookies.get("csrftoken");
fetch(endPoint, { credentials: "same-origin", headers: { "X-CSRFToken": token } })
.then((response) => {
if (!response.ok) {
this.fetching.delete(endPoint);
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((obj) => {
dispatch(invalidate(false));
dispatch(readDataSucceeded(obj));
dispatch(isReading(false));
this.fetching.delete(endPoint);
})
.catch((e) => {
dispatch(failed(true, e));
dispatch(isReading(false));
this.fetching.delete(endPoint);
});
};
}
/**
* Generic function for handling a PUT and POST requests to the API
*
* @param {object} data, to update the data, `data` should contains the `id` field.
* @param {string} apiEndPoint
* @param {function} isSavingSpecific
* @param {function} saveSpecificSucceeded
* @param {function} saveSpecificFailed
* @param {function} [onSuccessCallback = (newData) => {}] CallBack called if the update was successful besides calling the saveSpecificSucceeded action
* @returns {function} Function that takes the `dispatch` function of redux as argument.
* @memberof SmartActions
*/
_ElSaveData(data, apiEndPoint, isSavingSpecific, saveSpecificSucceeded, saveSpecificFailed, onSuccessCallback = () => { }) {
return (dispatch) => {
let method = "POST";
let pk = "";
if ("id" in data) {
method = "PUT";
pk = data.id + "/";
}
let errorStatusText = "";
dispatch(isSavingSpecific(true));
let token = Cookies.get("csrftoken");
fetch(apiEndPoint + pk, {
method: method,
credentials: "same-origin",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"X-CSRFToken": token
},
body: JSON.stringify(data)
})
.then((response) => {
if (!response.ok) {
errorStatusText = response.statusText;
throw response;
}
return response.json();
})
.then((_El) => {
onSuccessCallback(_El); // don't put it after the dispatch, it might cause errors with components unMounting
dispatch(saveSpecificSucceeded(_El));
dispatch(isSavingSpecific(false));
})
.catch((e) => {
if (typeof e.json === "function") {
return e.json()
.catch((error) => {
// eslint-disable-next-line no-unused-vars
return new Promise((resolve, reject) => resolve(error));
});
} else {
// eslint-disable-next-line no-unused-vars
return new Promise((resolve, reject) => resolve(e));
}
})
.then((errorContent) => {
if (typeof errorContent !== "undefined") {
dispatch(saveSpecificFailed(true, { message: errorStatusText, content: errorContent }));
dispatch(isSavingSpecific(false));
}
});
};
}
}
......@@ -18,19 +18,12 @@ let apiReducersTmp = {};
function addAPIs(arr) {
arr
.forEach(route => {
const actions = new CrudActions(route);
apiActionsTmp[route] = actions;
let apiEndPoint = `/api/${route}/`,
apiInfo = {
name: route,
apiEndPoint
};
const actions = new CrudActions(apiInfo);
apiActionsTmp[apiInfo.name] = actions;
const reducers = new CrudReducers(apiInfo);
apiReducersTmp[`${apiInfo.name}All`] = reducers.getCombinedAll();
apiReducersTmp[`${apiInfo.name}Specific`] = reducers.getCombinedSpecific();
const reducers = new CrudReducers(route);
apiReducersTmp[`${route}All`] = reducers.getForAll();
apiReducersTmp[`${route}One`] = reducers.getForOne();
});
}
......
import { apiActions } from "./buildApiActionsAndReducers";
// To ease auto completion, not really used here!
// eslint-disable-next-line no-unused-vars
import CrudActions from "./CrudActions";
/**
* Function to get the the actions corresponding to an API end point.
*
......
......@@ -7,28 +7,37 @@
*/
export default function getCrudActionTypes(name) {
return {
isReadingAll: `API_${name}_READ_ALL_IN_PROGRESS`,
readAllStarted: `API_${name}_READ_ALL_STARTED`,
readAllSucceeded: `API_${name}_READ_ALL_SUCCEEDED`,
readAllFailed: `API_${name}_READ_ALL_FAILED`,
clearReadAllFailed: `API_${name}_CLEAR_READ_ALL_FAILED`,
//
isReadingSpecific: `API_${name}_READ_SPECIFIC_IN_PROGRESS`,
readSpecificSucceeded: `API_${name}_READ_SPECIFIC_SUCCEEDED`,
readSpecificFailed: `API_${name}_READ_SPECIFIC_FAILED`,
readOneStarted: `API_${name}_READ_ONE_STARTED`,
readOneSucceeded: `API_${name}_READ_ONE_SUCCEEDED`,
readOneFailed: `API_${name}_READ_ONE_FAILED`,
clearReadOneFailed: `API_${name}_CLEAR_READ_ONE_FAILED`,
//
isCreating: `API_${name}_CREATION_IN_PROGRESS`,
createStarted: `API_${name}_CREATE_STARTED`,
createSucceeded: `API_${name}_CREATE_SUCCEEDED`,
createFailed: `API_${name}_CREATE_FAILED`,
clearCreateFailed: `API_${name}_CLEAR_CREATE_SPECIFIC_FAILED`,
//
isUpdating: `API_${name}_UPDATE_IN_PROGRESS`,
updateStarted: `API_${name}_UPDATE_STARTED`,
updateSucceeded: `API_${name}_UPDATE_SUCCEEDED`,
updateFailed: `API_${name}_UPDATE_FAILED`,
clearUpdateFailed: `API_${name}_CLEAR_UPDATE_SPECIFIC_FAILED`,
//
isDeleting: `API_${name}_DELETION_IN_PROGRESS`,
deleteStarted: `API_${name}_DELETE_STARTED`,
deleteSucceeded: `API_${name}_DELETE_SUCCEEDED`,
deleteFailed: `API_${name}_DELETE_FAILED`,
clearDeleteFailed: `API_${name}_CLEAR_DELETE_SPECIFIC_FAILED`,
//
// Not directly a CRUD action but needed for in app behavior
isInvalidatedAll: `API_${name}_ALL_INVALIDATED`,
isInvalidatedSpecific: `API_${name}_SPECIFIC_INVALIDATED`,
invalidateAll: `API_${name}_ALL_INVALIDATED`,
clearInvalidationAll: `API_${name}_ALL_INVALIDATION_CLEARED`,
invalidateOne: `API_${name}_ONE_INVALIDATED`,
clearInvalidationOne: `API_${name}_ONE_INVALIDATION_CLEARED`,
// One more to optimize performances a bit
rootType: `API_ROOT_${name}`,
};
}
\ No newline at end of file
}
......@@ -29,7 +29,7 @@ export function getLatestRead(stateExtract) {
* It assumes that the data has already been fetched at some point.
*
* @export
* @param {string} entry eg: userDataSpecific
* @param {string} entry eg: userDataOne
* @returns
*/
export function getLatestReadDataFromStore(entry){
......
Markdown is supported
0% or
You are about to add 0