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 { ...@@ -35,7 +35,7 @@ class PendingModeration extends CustomComponentForAPI {
getEndPointAttr = (propName) => { getEndPointAttr = (propName) => {
if (propName === "pendingModeration") { if (propName === "pendingModeration") {
const { contentTypeId, id } = this.props.modelInfo; const { contentTypeId, id } = this.props.modelInfo;
return `${contentTypeId}/${id}`; return [contentTypeId, id];
} else { } else {
return undefined; return undefined;
} }
...@@ -201,7 +201,7 @@ const mapDispatchToProps = (dispatch) => { ...@@ -201,7 +201,7 @@ const mapDispatchToProps = (dispatch) => {
api: { api: {
pendingModeration: ({ params }) => dispatch(getActions("pendingModerationObj").readAll(params)), pendingModeration: ({ params }) => dispatch(getActions("pendingModerationObj").readAll(params)),
}, },
resetPendingModeration: () => dispatch(getActions("pendingModerationObj").setInvalidatedAll(true)), resetPendingModeration: () => dispatch(getActions("pendingModerationObj").invalidateAll()),
openFullScreenDialog: (innerNodes) => dispatch(openFullScreenDialog(innerNodes)), openFullScreenDialog: (innerNodes) => dispatch(openFullScreenDialog(innerNodes)),
closeFullScreenDialog: () => dispatch(closeFullScreenDialog()), closeFullScreenDialog: () => dispatch(closeFullScreenDialog()),
}; };
......
...@@ -2,33 +2,18 @@ ...@@ -2,33 +2,18 @@
* This file contains the functions and class to create the CRUD actions * This file contains the functions and class to create the CRUD actions
*/ */
import SmartActions from "./SmartActions";
import getCrudActionTypes from "./getCrudActionTypes"; 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 /** Dumb action */
function _isReading(status, type) { const emptyAction = () => { };
return {
type,
status
};
}
function _readFailed(failed, error, type) {
return {
type,
failed,
error
};
}
function _readSucceeded(data, type) {
return {
type,
data,
};
}
/** /**
...@@ -38,243 +23,395 @@ function _readSucceeded(data, type) { ...@@ -38,243 +23,395 @@ function _readSucceeded(data, type) {
* @class CrudActions * @class CrudActions
*/ */
export default class CrudActions { export default class CrudActions {
interactionStatus = new Set();
/** /**
* Creates an instance of CrudActions. * Creates an instance of CrudActions.
* @param {object} apiInfo * @param {string} route Api route associated with the instance
* @memberof CrudActions * @memberof CrudActions
*/ */
constructor(apiInfo) { constructor(route) {
this.name = apiInfo.name; this.apiEndPoint = route;
this.apiEndPoint = apiInfo.apiEndPoint; this.types = getCrudActionTypes(route);
this.smartActions = new SmartActions();
this.types = getCrudActionTypes(this.name);
} }
/**
*
*
*
*
*
*
*
* 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 * @memberof CrudActions
*/ */
readAll(params = {}) { wrap(action) {
const self = this; 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) { // We start by "preparing" the request parameters and endpoint attributes
return _readSucceeded(data, self.types.readAllSucceeded); 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) { // Then we build the final URL
return _readFailed(bool, error, self.types.readAllFailed); let url;
if (id !== "") {
url = [this.apiEndPoint, ...endPointAttr, id, ""].join("/");
} else {
url = [this.apiEndPoint, ...endPointAttr, queryParams].join("/");
} }
return this.smartActions._FetchData( // clean the end point url from doubles //
"", return url.replace(/\/+/g, "/");
params,
this.apiEndPoint,
isReadingAll,
readAllSucceeded,
this.setInvalidatedAll.bind(this),
readAllFailed,
false
);
} }
// 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} * @returns {function}
* @memberof CrudActions * @memberof CrudActions
*/ */
readSpecific(id = "", params = {}) { performAxios(config, temporaryType, successType, errorType, onSuccessCallback = emptyAction) {
const self = this; const self = this,
requestId = `${config.method}_${config.url}`;
function isReadingSpecific(isReadingSpecific) { // a bit of optimization to make sure not to make overlapping requests
return _isReading(isReadingSpecific, self.types.isReadingSpecific); if (this.interactionStatus.has(requestId)) {
return emptyAction;
} }
function readSpecificSucceeded(data) { this.interactionStatus.add(requestId);
return _readSucceeded(data, self.types.readSpecificSucceeded);
} 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;
///// return this.performAxios(config, readAllStarted, readAllSucceeded, readAllFailed);
///// }
// Not readonly functions
///// /**
* 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} * @returns {function}
* @memberof CrudActions * @memberof CrudActions
*/ */
create(data, onSuccessCallback = () => { }) { readOne(id, params = {}) {
const self = this; const url = this.getUrl(params, id),
config = { url, method: "get" },
function isCreating(status) { { readOneStarted, readOneSucceeded, readOneFailed } = this.types;
return {
type: self.types.isCreating,
status
};
}
return this.performAxios(config, readOneStarted, readOneSucceeded, readOneFailed);
}
function createSucceeded(data) {
return {
type: self.types.createSucceeded,
data,
};
}
function createFailed(failed, error = null) { /**
return { * Action to reset a read one failure.
type: self.types.createFailed, *
failed, * @returns
error * @memberof CrudActions
}; */
} clearReadOneFailed = () => this.wrap({
type: this.types.clearReadOneFailed
});
/**
*
*
* create
*
*
*/
return this.smartActions._ElSaveData( /**
data, * Create an object with fields and values `data`.
this.apiEndPoint, *
isCreating, * @param {object} data
createSucceeded, * @param {function} [onSuccessCallback=emptyAction] Callback that will be called if the request is successful. The returned data is passed to the callback.
createFailed, * @param {object} [params={}] Request parameters
onSuccessCallback, * @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 this.performAxios(config, createStarted, createSucceeded, createFailed, onSuccessCallback);
return {
type: this.types.createFailed,
failed: false,
error: null
};
} }
/**
*
* @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 {string|number} id id of the object to update
* @param {function} [onSuccessCallback=(newData) => { }] CallBack called if the update was successful besides calling updateSucceeded * @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} * @returns {function}
* @memberof CrudActions * @memberof CrudActions
*/ */
update(data, onSuccessCallback = () => { }) { update(id, data, onSuccessCallback = () => { }, params = {}) {
const self = this; const url = this.getUrl(params, id),
config = { url, data, method: "put" },
function isUpdating(status) { { updateStarted, updateSucceeded, updateFailed } = this.types;
return {
type: self.types.isUpdating,
status
};
}
function updateFailed(failed, error = null) { return this.performAxios(config, updateStarted, updateSucceeded, updateFailed, onSuccessCallback);
return { }
type: self.types.updateFailed,
failed,
error
};
}
function updateSucceeded(data) { /**
return { * Action to reset an update failure.
type: self.types.updateSucceeded, * @returns
data, * @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, * delete
updateFailed, *
onSuccessCallback, *
); */
}
clearUpdateFailed() {
return { /**
type: this.types.updateFailed, * Deletes an object identified by its id.
failed: false, *
error: null * @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