Commit da3d378b authored by Florent Chehab's avatar Florent Chehab
Browse files

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
......@@ -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
});
/**
* Function to create a new entry
* If `data` contains an `id` then the behavior should be similar to an update.
*
* @param {object} data
* @param {function} [onSuccessCallback=(newData) => { }] CallBack called if the update was successful besides calling updateSucceeded
*
* Read One
*
*
*/
/**
* Read the object identified by the id `id` on the configured route.
* You can specify endpoint attrs with `param`.
*
* @param {string|number} id id of the object to fetch
* @param {object} [params={}] Request parameters
* @param {Array.<string>} params.endPointAttr attributes to add to the endpoint route
* @returns {function}
* @memberof CrudActions
*/
create(data, onSuccessCallback = () => { }) {
const self = this;
function isCreating(status) {
return {
type: self.types.isCreating,
status
};
}
readOne(id, params = {}) {
const url = this.getUrl(params, id),
config = { url, method: "get" },
{ readOneStarted, readOneSucceeded, readOneFailed } = this.types;
return this.performAxios(config, readOneStarted, readOneSucceeded, readOneFailed);
}
function createSucceeded(data) {
return {
type: self.types.createSucceeded,
data,
};
}
function createFailed(failed, error = null) {
return {
type: self.types.createFailed,
failed,
error
};
}
/**
* Action to reset a read one failure.
*
* @returns
* @memberof CrudActions
*/
clearReadOneFailed = () => this.wrap({
type: this.types.clearReadOneFailed
});
/**
*
*
* create
*
*
*/
return this.smartActions._ElSaveData(
data,
this.apiEndPoint,
isCreating,
createSucceeded,
createFailed,
onSuccessCallback,
);
}
/**
* Create an object with fields and values `data`.
*
* @param {object} data
* @param {function} [onSuccessCallback=emptyAction] Callback that will be called if the request is successful. The returned data is passed to the callback.
* @param {object} [params={}] Request parameters
* @param {Array.<string>} params.endPointAttr attributes to add to the endpoint route
* @returns {function}
* @memberof CrudActions
*/
create(data, onSuccessCallback = () => { }, params = {}) {
const url = this.getUrl(params),
config = { url, data, method: "post" },
{ createStarted, createSucceeded, createFailed } = this.types;
clearCreateFailed() {
return {
type: this.types.createFailed,
failed: false,
error: null
};
return this.performAxios(config, createStarted, createSucceeded, createFailed, onSuccessCallback);
}
/**
*
* @returns
* @memberof CrudActions
*/
clearCreateFailed = () => this.wrap({
type: this.types.clearCreateFailed
});
/**
*
*
*
* update
*
*
*/
/**
* Function to update an entry
* Updates an object identified by its id.
*
* @param {object} data, an `id` field should be contained in this object
* @param {function} [onSuccessCallback=(newData) => { }] CallBack called if the update was successful besides calling updateSucceeded
* @param {string|number} id id of the object to update
* @param {object} data the new object fields
* @param {function} [onSuccessCallback=emptyAction] Callback that will be called if the request is successful. The returned data is passed to the callback.
* @param {object} [params={}] Request parameters
* @param {Array.<string>} params.endPointAttr attributes to add to the endpoint route
* @returns {function}
* @memberof CrudActions
*/
update(data, onSuccessCallback = () => { }) {
const self = this;
function isUpdating(status) {
return {
type: self.types.isUpdating,
status
};
}
update(id, data, onSuccessCallback = () => { }, params = {}) {
const url = this.getUrl(params, id),
config = { url, data, method: "put" },
{ updateStarted, updateSucceeded, updateFailed } = this.types;
function updateFailed(failed, error = null) {
return {
type: self.types.updateFailed,
failed,
error
};
}
return this.performAxios(config, updateStarted, updateSucceeded, updateFailed, onSuccessCallback);
}
function updateSucceeded(data) {
return {
type: self.types.updateSucceeded,
data,
};
}
/**
* Action to reset an update failure.
* @returns
* @memberof CrudActions
*/
clearUpdateFailed = () => this.wrap({
type: this.types.clearUpdateFailed
});
if (!("id" in data)) {
throw Error("When updating an object, an id is expected in the data");
}
return this.smartActions._ElSaveData(
data,
this.apiEndPoint,
isUpdating,
updateSucceeded,
updateFailed,
onSuccessCallback,
);
}
/**
*
*
*
* delete
*
*
*/
clearUpdateFailed() {
return {
type: this.types.updateFailed,
failed: false,
error: null
};
/**
* Deletes an object identified by its id.
*
* @param {string|number} id id of the object to delete on the route
* @param {function} [onSuccessCallback=emptyAction] Callback that will be called if the request is successful. The returned data is passed to the callback.
* @param {object} [params={}] Request parameters
* @param {Array.<string>} params.endPointAttr attributes to add to the endpoint route
* @returns {function}
* @memberof CrudActions
*/
delete(id, onSuccessCallback = () => { }, params = {}) {
const url = this.getUrl(params, id),
config = { url, method: "delete" },
{ deleteStarted, deleteSucceeded, deleteFailed } = this.types;
return this.performAxios(config, deleteStarted, deleteSucceeded, deleteFailed, onSuccessCallback);
}
///////////
// NOT CRUD FUNCTIONS
///////////
/**
* Action to reset a delete failure.
* @returns
* @memberof CrudActions
*/
clearDeleteFailed = () => this.wrap({
type: this.types.clearDeleteFailed
});
/**
* Function to tell the store that this element has been invalidated relative to "ALL"
*
* @param {boolean} bool
*
* invalidate all
*
*
*
*/
/**
* Action to mark the "read all" data as invalidated.
* @returns
* @memberof CrudActions
*/
setInvalidatedAll(bool) {
return {
type: this.types.isInvalidatedAll,
isInvalidated: bool
};
}
invalidateAll = () => this.wrap({
type: this.types.invalidateAll,
});
/**
* Action to clear the invalidation of the "read all" data.
* @returns
* @memberof CrudActions
*/
clearInvalidationAll = () => this.wrap({
type: this.types.clearInvalidationAll,
});
/**
* Function to tell the store that this element has been invalidated relative to "specific"
*
* @param {boolean} bool
*
*
*
* invalidate One
*
*
*/
/**
* Action to mark the "read one" data as invalidated.
* @returns
* @memberof CrudActions
*/
setInvalidatedSpecific(bool) {
return {
type: this.types.isInvalidatedSpecific,
isInvalidated: bool
};
}
invalidateOne = () => this.wrap({
type: this.types.invalidateOne,
});
/**