Commit d3681935 authored by Florent Chehab's avatar Florent Chehab

refactor(smart actions parameters) : BREAKING & tweaks

* Created `RequestedParams` class with builder to create request parameters object in a standard way,
* All the generic actions only take an instance of this object now,
* All dynamic parametrization of the request params now happens in `apiParams`
* This enables an ultra smart magic piece of logic to auto refresh the data from the server if the props / state of the component has changed since the last request. (the requestParams object are now stored in the redux store and we can compare theme 😄 ). And also not to make duplicate queries.
* Updated doc accordingly,

Tweaks/fixes:
* use of `lodash/isEqual` to deep compare objects
* Removed now useless behiavor
parent 6b4719a2
Pipeline #38664 passed with stages
in 3 minutes and 16 seconds
...@@ -222,23 +222,24 @@ const mapDispatchToProps = (dispatch) => { ...@@ -222,23 +222,24 @@ const mapDispatchToProps = (dispatch) => {
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`): 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={})` - `readAll(params=RequestParams.Builder.build())`
- `readOne(id, params={})` - `readOne(params)`
- `create(data, onSuccessCallback = (newData) => { }, params = {})` - `create(params)`
- `update(id, data, onSuccessCallback = (newData) => { }, params = {})` - `update(params)`
- `delete(id, onSuccessCallback = () => { }, params = {})` - `delete(params)`
?> `params` **must be an instance of `RequestParams`**, which is a helper class defined in the project. This class comes with a handy `Builder` static class (say hello to the Builder design pattern) to help you parameterize your requests. Here is a quick summary of the functions provided by the builder (all are *optional*):
?> `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. * `withId(id)`: specify the id that will be added to the url (eg: `withId(1)` => `/endpoint/1`)
* `withData(data)`: specify the payload that will go with request (useful for creating/updating models instances)
* `withQueryParam(key, value)`: add a *query param* to the request object (eg: `withQueryParam("currency", "CHF")` will result in the request `/endpoint?currency=CHF`); you can chain multiple `withQueryParam`.
* `withEndPointAttrs(endPointAttrs)`: `endPointAttrs` should be an array of the endpoint attributes to add to the endpoint (`withEndPointAttrs([10, 11])` will render as `/endpoint/10/11/`).
* ` withOnSuccessCallback(callback)`: register a callback that will be called when the request is successful. *The data returned by the server will be passed as parameter to this callback.*
* `build()`: to conclude the building process :smile:
> * `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`). ?> :information_desk_person: All those functions can be chained.
> * `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. !> Never forget the `.build()` at the end of your chain.
?> `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: You also have actions to clear the failures if you need:
...@@ -257,7 +258,7 @@ And actions related to invalidating the data: ...@@ -257,7 +258,7 @@ And actions related to invalidating the data:
!> 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. !> 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. This behavior is implementer in `CustomComponentForApi` ?> :information_desk_person: invalidating data will usually trigger a refresh of that data with a new API call. This behavior is implemented 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: ?> 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:
...@@ -305,9 +306,36 @@ const mapDispatchToProps = (dispatch) => { ...@@ -305,9 +306,36 @@ const mapDispatchToProps = (dispatch) => {
!> **The function that will be used to dispatch the action associated with fetching the data for `propName` must be identified by `api.propName` in the `mapDispatchToProps` function.** !> **The function that will be used to dispatch the action associated with fetching the data for `propName` must be identified by `api.propName` in the `mapDispatchToProps` function.**
<div id="customGettersApi"></div> #### Dynamic parametrization of the requests
Often, you will need to access data on the API based on a value stored in the `props` of your components. As a result you need to generate a new `RequestParams` object on demand.
:warning: To do so, in your component you should define he **`apiParams`** property like follow:
```js
apiParams = {
countryDri: ({props, state}) => RequestParams.Builder.withQueryParam("countries", props.countryId).build()
};
```
`apiParams` is an object with keys the `propName` (we have been talking about before) and value a function that accepts an object and that **must return** a `RequestParams` instance.
Then, automatically and when needed, a new `RequestParams` will be built by the internal functions of `CustomComponentForApi`; as you can see this *mapper* function takes as an argument an object with two keys that corresponds to the current `state` and `props` of the object.
When using `apiParams`, your `mapDispatchToProps` should look like this:
```js
const mapDispatchToProps = (dispatch) => {
return {
api: {
countryDri: (params) => dispatch(getActions("countryDri").readAll(params)),
},
};
};
```
Your function must take one argument (`params`) (that will be automatically built) and pass it down to the action like a breeze.
?> :information_desk_person: All the subfunctions you define in `mapDispatchToProps` under the `api` key will be given one object parameter you may want to use. It is composed as follow: `{props, params}`. `props` will be an object corresponding to the `props` of the *connected* component. `params` is built with the `getQueryParams` and `getEndPointAttr` you may wish to override in your connected components. **`params` shall be *passed on* for filtering to work correctly**. ?> :information_desk_person: For all "dynamic" attributes (defined inside `apiParams`) some awesome magical behaviors will be automatically inherited, such as the fact that a new request will be made to the server if the parametrization has changed (e.g. if a `prop` has changed) without needing to detect it yourself (only on ). :tada: This behavior is only present on `ComponentDidMount` react hook. If you want to activate it on `ComponentDidUpdate`, you should set `enableSmartDataRefreshOnComponentDidUpdate = true` as a component property.
That's all :confetti_ball:. That's all :confetti_ball:.
......
import React, { Component } from "react"; import React, {Component} from "react";
import Loading from "./Loading"; import Loading from "./Loading";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { successActionsWithReads, getLatestRead } from "../../redux/api/utils"; import {getLatestRead, successActionsWithReads} from "../../redux/api/utils";
import Notifier from "./Notifier"; import Notifier from "./Notifier";
import {RequestParams} from "../../redux/api/RequestParams";
/** /**
* Custom react component to be used when called to the api are required to display data of the component. * Custom react component to be used when called to the api are required to display data of the component.
* *
* When extending, you shouldn't use the `render` function but the `customRender` (that will be called once all the data is ready) * **When extending, you shouldn't use the `render` function but the `customRender` (that will be called once all the data is ready)**
*
* Also, when connecting your component to redux, you should use a `mapDispatchToProps` such as this one.
* What is important is that "api" related elements are under the `api` key for optimization purposes.
* NB: if you don't do it this way, the data won't be fetched.
*
```JS
const mapDispatchToProps = (dispatch) => {
return {
api: {
universities: () => dispatch(getActions("universities").readAll()),
},
saveUniversityInView: (univId) => dispatch(saveUniversityBeingViewed(univId))
};
};
```
* *
* IMPORTANT: to use some of the function contained in this class, you need to have a matching `mapStateToProps` function for read * See here for more info on how to use it: https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Frontend/redux?id=conventions-for-reading-data
* elements: the key of the object should match the one in `api`. * And get inspired by the code base ;)
*
* Here `universities` is both in the `api` of `mapDispatchToProps` and in `mapStateToProps`.
```JS
const mapStateToProps = (state) => {
return {
universities: state.api.universitiesAll,
universityBeingViewed: state.app.universityBeingViewed
};
};
```
* *
*
* @abstract
* @class CustomComponentForAPI * @class CustomComponentForAPI
* @extends {Component} * @extends {Component}
* @abstract
*/ */
class CustomComponentForAPI extends Component { class CustomComponentForAPI extends Component {
customErrorHandlers = {}
/** /**
* A function with a propName parameter that should return the query parameters to add when reading that propName * A mapping from propName to a function that returns the appropriate RequestParams instance.
* This functions takes as argument the props and the state of the component.
* @type {object.<string, function({props, state}):RequestParams>}
*/ */
// eslint-disable-next-line no-unused-vars apiParams = {};
getQueryParams = (propName) => undefined;
/** /**
* A function with a propName parameter that should return the endPoint attrs to add when reading that propName * Should 'smartRefresh' (based on the comparaison of the previous RequestParam obj and what it would look like
* now) be enabled?
* @type {boolean}
*/ */
// eslint-disable-next-line no-unused-vars enableSmartDataRefreshOnComponentDidUpdate = false;
getEndPointAttr = (propName) => undefined;
/**
* a little bit of optimization, Stores the list of props that are "automatically" connected to the API
* @type {string[]}
* @private
*/
_apiAutoPropNames = Array();
/**
* Stores the last requestParams intances used for each props
* @type {object.<string,RequestParams>}
* @private
*/
_lastRequestParamsObjs = {};
constructor(props) { constructor(props) {
super(props); super(props);
if (typeof this.getEndPointAttr !== "function") { if (typeof props === "object" && "api" in props) {
throw new Error("getEndPointAttr misconfigured: it must be a function"); const {api} = props;
this._apiAutoPropNames = Object.keys(api);
} }
if (typeof this.getQueryParams !== "function") { /**
throw new Error("getQueryParams misconfigured: it must be a function"); * configuration checks
*/
// eslint-disable-next-line no-undef
if (process.env.NODE_ENV !== "production") {
Object.entries(this.apiParams).forEach(([propName, mapper]) => {
if (typeof props[propName] === "undefined") {
throw new Error("Miss-configuration: " +
`The propName ${propName} you defined in apiParams doesn't exist in the props ` +
"See: https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Frontend/redux?id=dynamic-parametrization-of-the-requests");
}
if (typeof props.api[propName] !== "function") {
throw new Error("Miss-configuration: " +
`The propName ${propName}: 'props.api.${propName}' must be a function (with a transfer parameter 'params') ` +
"See: https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Frontend/redux?id=dynamic-parametrization-of-the-requests");
}
if (typeof mapper !== "function") {
throw new Error("Miss-configuration: " +
`apiParams.'${propName}' must be a function. ` +
"See: https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Frontend/redux?id=dynamic-parametrization-of-the-requests");
}
if (!(mapper({props, state: this.state}) instanceof RequestParams)) {
throw new Error("Miss-configuration: " +
`The function defined in apiParams.${propName} must return an instance of RequestParams. ` +
"See: https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Frontend/redux?id=dynamic-parametrization-of-the-requests");
}
});
} }
/**
* End of configuration checks
*/
// Finally we try to restore the previous RequestParams objects
// So that we don't refetch the same data twice.
this._apiAutoPropNames.forEach(propName => {
this._lastRequestParamsObjs[propName] = props[propName].readSucceeded.requestParams;
});
}
// a little bit of optimization /**
// Stores the list of props that use the API * @private
this.apiProps = Array(); */
if (typeof props === "object" && "api" in props) { smartRefresh() {
const { api } = props; Object.entries(this.apiParams).forEach(([propName, mapper]) => {
this.apiProps = Object.keys(api); if (!mapper({props: this.props, state: this.state})
} .equals(this._lastRequestParamsObjs[propName])) {
this.performReadFromApi(propName);
}
});
} }
///// /////
...@@ -82,12 +112,18 @@ class CustomComponentForAPI extends Component { ...@@ -82,12 +112,18 @@ class CustomComponentForAPI extends Component {
///// /////
componentDidMount() { componentDidMount() {
this.readPropsIfNeeded(); this.readPropsIfNeeded();
// auto refetch data if outdated
this.smartRefresh();
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
// extends the default behavior of react to load props (api related) if needed on update. // extends the default behavior of react to load props (api related) if needed on update.
this.readPropsIfNeeded(); this.readPropsIfNeeded();
if (this.enableSmartDataRefreshOnComponentDidUpdate) {
// auto refetch data if outdated
this.smartRefresh();
}
} }
render() { render() {
...@@ -99,13 +135,14 @@ class CustomComponentForAPI extends Component { ...@@ -99,13 +135,14 @@ class CustomComponentForAPI extends Component {
<Notifier <Notifier
message={"Une erreur est survenue sur le serveur. Merci de recharger le page."} message={"Une erreur est survenue sur le serveur. Merci de recharger le page."}
/> />
<p>Une erreur est survenue lors du téléchargement des données. Merci de recharger la page et si l'erreur persiste, merci de contacter les administrateurs du site.</p> <p>Une erreur est survenue lors du téléchargement des données. Merci de recharger la page et si l'erreur
persiste, merci de contacter les administrateurs du site.</p>
</> </>
); );
} }
if (!this.allApiDataIsReady()) { if (!this.allApiDataIsReady()) {
return <Loading />; return <Loading/>;
} }
return this.customRender(); return this.customRender();
...@@ -121,8 +158,8 @@ class CustomComponentForAPI extends Component { ...@@ -121,8 +158,8 @@ class CustomComponentForAPI extends Component {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
throw new Error("Dev: you forget to define the `customRender` function that is used when rendering within a subClass of CustomComponentForAPI"); throw new Error("Dev: you forget to define the `customRender` function that is used when rendering within a subClass of CustomComponentForAPI");
} }
// End of react functions override
// End of react functions override
/** /**
...@@ -132,11 +169,11 @@ class CustomComponentForAPI extends Component { ...@@ -132,11 +169,11 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
checkPropsFailed() { checkPropsFailed() {
return this.apiProps.some((propName) => { return this._apiAutoPropNames.some((propName) => {
if (typeof this.props[propName] === "undefined") { if (typeof this.props[propName] === "undefined") {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(this.props, propName); console.error(this.props, propName);
throw Error(`${propName} is not in the class props ! Dev, check what your are doing`); throw Error(`${propName} is not in the class props (or might be undefined due to a wrong state extract) ! Dev, check what your are doing`);
} }
const prop = this.props[propName]; const prop = this.props[propName];
return prop.readFailed.failed; // general handling of all types of API reducers return prop.readFailed.failed; // general handling of all types of API reducers
...@@ -165,11 +202,11 @@ class CustomComponentForAPI extends Component { ...@@ -165,11 +202,11 @@ class CustomComponentForAPI extends Component {
/** /**
* Get the list of props for which a read action needs to be triggered * Get the list of props for which a read action needs to be triggered
* *
* @returns {Array[string]} * @returns {array.<string>}
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
getListPropsNeedRead() { getListPropsNeedRead() {
return this.apiProps.filter((propName) => { return this._apiAutoPropNames.filter((propName) => {
if (this.propIsUsable(propName)) { if (this.propIsUsable(propName)) {
return false; return false;
} else { } else {
...@@ -185,7 +222,7 @@ class CustomComponentForAPI extends Component { ...@@ -185,7 +222,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
readPropsIfNeeded() { readPropsIfNeeded() {
this.getListPropsNeedRead().map((propName) => { this.getListPropsNeedRead().forEach((propName) => {
this.performReadFromApi(propName); this.performReadFromApi(propName);
}); });
} }
...@@ -197,21 +234,13 @@ class CustomComponentForAPI extends Component { ...@@ -197,21 +234,13 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
performReadFromApi(propName) { performReadFromApi(propName) {
if (propName in this.apiParams) {
let endPointAttr = [], const requestParams = this.apiParams[propName]({props: this.props, state: this.state});
queryParams = {}; this._lastRequestParamsObjs[propName] = requestParams;
this.props.api[propName](requestParams);
const tmpQueryParams = this.getQueryParams(propName); } else {
if (tmpQueryParams) { this.props.api[propName]();
queryParams = tmpQueryParams;
}
const tmpEndPointAttr = this.getEndPointAttr(propName);
if (tmpEndPointAttr) {
endPointAttr = tmpEndPointAttr;
} }
this.props.api[propName]({ params: { queryParams, endPointAttr }, props: this.props });
} }
/** /**
...@@ -221,7 +250,7 @@ class CustomComponentForAPI extends Component { ...@@ -221,7 +250,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
allApiDataIsReady() { allApiDataIsReady() {
return this.apiProps.every((propName) => this.propIsUsable(propName)); return this._apiAutoPropNames.every((propName) => this.propIsUsable(propName));
} }
/** /**
...@@ -229,7 +258,7 @@ class CustomComponentForAPI extends Component { ...@@ -229,7 +258,7 @@ class CustomComponentForAPI extends Component {
* read, create and update are taken into account * read, create and update are taken into account
* *
* @param {string} propName * @param {string} propName
* @returns {Any} * @returns {object}
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
getLatestReadDataAndTime(propName) { getLatestReadDataAndTime(propName) {
...@@ -249,7 +278,7 @@ class CustomComponentForAPI extends Component { ...@@ -249,7 +278,7 @@ class CustomComponentForAPI extends Component {
* read, create and update are taken into account * read, create and update are taken into account
* *
* @param {string} propName * @param {string} propName
* @returns {Any} * @returns {object}
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
getLatestReadData(propName) { getLatestReadData(propName) {
...@@ -290,8 +319,8 @@ class CustomComponentForAPI extends Component { ...@@ -290,8 +319,8 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
getAllLatestReadData() { getAllLatestReadData() {
let out = Object(); let out = {};
this.apiProps.forEach((propName) => { this._apiAutoPropNames.forEach((propName) => {
out[propName] = this.getLatestReadData(propName); out[propName] = this.getLatestReadData(propName);
}); });
return out; return out;
...@@ -308,7 +337,7 @@ class CustomComponentForAPI extends Component { ...@@ -308,7 +337,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
getLatestReadDataFor(propNames) { getLatestReadDataFor(propNames) {
let out = Object(); let out = {};
propNames.forEach(propName => out[propName] = this.getLatestReadData(propName)); propNames.forEach(propName => out[propName] = this.getLatestReadData(propName));
return out; return out;
} }
...@@ -324,7 +353,7 @@ class CustomComponentForAPI extends Component { ...@@ -324,7 +353,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
joinCampus(campus) { joinCampus(campus) {
const { universities, countries, cities } = this.getLatestReadDataFor(["universities", "countries", "cities"]); const {universities, countries, cities} = this.getLatestReadDataFor(["universities", "countries", "cities"]);
let res = Object.assign({}, campus); //copy for safety let res = Object.assign({}, campus); //copy for safety
res.university = universities.find(univ => univ.id == campus.university); res.university = universities.find(univ => univ.id == campus.university);
res.city = cities.find(city => city.id == campus.city); res.city = cities.find(city => city.id == campus.city);
...@@ -342,10 +371,10 @@ class CustomComponentForAPI extends Component { ...@@ -342,10 +371,10 @@ class CustomComponentForAPI extends Component {
*/ */
getUnivCityAndCountry(univId) { getUnivCityAndCountry(univId) {
const univMainCampus = this.findMainCampus(univId), const univMainCampus = this.findMainCampus(univId),
{ countries, cities } = this.getLatestReadDataFor(["countries", "cities"]), {countries, cities} = this.getLatestReadDataFor(["countries", "cities"]),
city = cities.find(city => city.id == univMainCampus.city), city = cities.find(city => city.id == univMainCampus.city),
country = countries.find(country => country.id == city.country); country = countries.find(country => country.id == city.country);
return { city, country }; return {city, country};
} }
/** /**
......
...@@ -9,6 +9,7 @@ import CustomComponentForAPI from "./CustomComponentForAPI"; ...@@ -9,6 +9,7 @@ import CustomComponentForAPI from "./CustomComponentForAPI";
import "typeface-roboto"; import "typeface-roboto";
import getActions from "../../redux/api/getActions"; import getActions from "../../redux/api/getActions";
import {RequestParams} from "../../redux/api/RequestParams";
const siteSettings = { const siteSettings = {
typography: { typography: {
...@@ -76,7 +77,7 @@ const mapDispatchToProps = (dispatch) => { ...@@ -76,7 +77,7 @@ const mapDispatchToProps = (dispatch) => {
api: { api: {
// __AppUserId is defined in the html rendered by django // __AppUserId is defined in the html rendered by django
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
userData: () => dispatch(getActions("userData").readOne(__AppUserId)), userData: () => dispatch(getActions("userData").readOne(RequestParams.Builder.withId(__AppUserId).build())),
}, },
}; };
}; };
......
import getActions from "../../redux/api/getActions"; import getActions from "../../redux/api/getActions";
import { openFullScreenDialog, closeFullScreenDialog } from "../../redux/actions/fullScreenDialog"; import { openFullScreenDialog, closeFullScreenDialog } from "../../redux/actions/fullScreenDialog";
import {RequestParams} from "../../redux/api/RequestParams";
/** /**
* Function to create the mapDispatchToProps function for editor in a "generic way" * Function to create the mapDispatchToProps function for editor in a "generic way"
...@@ -13,13 +14,22 @@ export default function getMapDispatchToPropsForEditor(name) { ...@@ -13,13 +14,22 @@ export default function getMapDispatchToPropsForEditor(name) {
let lastSave = "update"; let lastSave = "update";
return { return {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
saveData: (data, onSuccessCallback = (newData) => { }) => { saveData: (data, onSuccessCallback = new Function()) => {
if ("id" in data) { // it's an update if ("id" in data) { // it's an update
lastSave = "update"; lastSave = "update";
dispatch(getActions(name).update(data.id, data, onSuccessCallback)); const params = RequestParams.Builder
.withId(data.id)
.withData(data)
.withOnSuccessCallback(onSuccessCallback)
.build();
dispatch(getActions(name).update(params));
} else { // it's a create } else { // it's a create
lastSave = "create"; lastSave = "create";
dispatch(getActions(name).create(data, onSuccessCallback)); const params = RequestParams.Builder
.withData(data)
.withOnSuccessCallback(onSuccessCallback)
.build();
dispatch(getActions(name).create(params));
} }
}, },
openFullScreenDialog: (innerNodes) => dispatch(openFullScreenDialog(innerNodes)), openFullScreenDialog: (innerNodes) => dispatch(openFullScreenDialog(innerNodes)),
......
import {Component} from "react"; import {Component} from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import areSameObjects from "../../utils/areSameObjects"; import isEqual from "lodash/isEqual";
import renderFieldsMixIn from "./renderFieldsMixIn"; import renderFieldsMixIn from "./renderFieldsMixIn";
import CustomError from "../common/CustomError"; import CustomError from "../common/CustomError";
...@@ -58,7 +58,8 @@ class Form extends Component { ...@@ -58,7 +58,8 @@ class Form extends Component {
* with resetting it for some reason * with resetting it for some reason
* @type {object.<string, Field>} * @type {object.<string, Field>}
*/ */
fields = Object(); fields = {};
/** /**
* Array containing the possible form level errors * Array containing the possible form level errors
* @abstract * @abstract
...@@ -142,7 +143,7 @@ class Form extends Component { ...@@ -142,7 +143,7 @@ class Form extends Component {
// we need to compare objects (ie JSON objects) differently // we need to compare objects (ie JSON objects) differently
if (typeof cmp1 === "object") { if (typeof cmp1 === "object") {
return !areSameObjects(cmp1, cmp2); return !isEqual(cmp1, cmp2);
} else { } else {
return cmp1 !== cmp2; return cmp1 !== cmp2;
} }
......
...@@ -22,7 +22,7 @@ class MultiSelectField extends Field { ...@@ -22,7 +22,7 @@ class MultiSelectField extends Field {
constructor(props) { constructor(props) {
super(props); super(props);
this.optionsByValue = Object(); this.optionsByValue = {};
props.options.map((opt) => this.optionsByValue[opt.value] = opt.label); props.options.map((opt) => this.optionsByValue[opt.value] = opt.label);
} }
......
...@@ -16,7 +16,6 @@ import {Redirect} from "react-router-dom"; ...@@ -16,7 +16,6 @@ import {Redirect} from "react-router-dom";
import getActions from "../../redux/api/getActions"; import getActions from "../../redux/api/getActions";
import {saveUniversityBeingViewed} from "../../redux/actions/universityPage";
import compose from "recompose/compose"; import compose from "recompose/compose";
import {withStyles} from "@material-ui/core"; import {withStyles} from "@material-ui/core";
import {withErrorBoundary} from "../common/ErrorBoundary"; import {withErrorBoundary} from "../common/ErrorBoundary";
...@@ -24,6 +23,7 @@ import {APP_ROUTES} from "../../config/appRoutes"; ...@@ -24,6 +23,7 @@ import {APP_ROUTES} from "../../config/appRoutes";
import CustomNavLink from "../common/CustomNavLink"; import CustomNavLink from "../common/CustomNavLink";
import CustomLink from "../common/CustomLink"; import CustomLink from "../common/CustomLink";
let previousUnivId = -1;
/** /**
* Component holding the page with the university details * Component holding the page with the university details
...@@ -34,23 +34,22 @@ import CustomLink from "../common/CustomLink"; ...@@ -34,23 +34,22 @@ import CustomLink from "../common/CustomLink";
*/ */
class PageUniversity extends CustomComponentForAPI { class PageUniversity extends CustomComponentForAPI {