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) => {
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={})`
- `readOne(id, params={})`
- `create(data, onSuccessCallback = (newData) => { }, params = {})`
- `update(id, data, onSuccessCallback = (newData) => { }, params = {})`
- `delete(id, onSuccessCallback = () => { }, params = {})`
- `readAll(params=RequestParams.Builder.build())`
- `readOne(params)`
- `create(params)`
- `update(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`).
> * `endPointAttr` should be an array of the endpoint attributes to add to the endpoint (`endPointAttr = [10, 11]` will render as `/api/endpoint/10/11/`)
?> :information_desk_person: All those functions can be chained.
?> `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.**
!> Never forget the `.build()` at the end of your chain.
You also have actions to clear the failures if you need:
......@@ -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.
?> :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:
......@@ -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.**
<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:.
......
import React, { Component } from "react";
import React, {Component} from "react";
import Loading from "./Loading";
import PropTypes from "prop-types";
import { successActionsWithReads, getLatestRead } from "../../redux/api/utils";
import {getLatestRead, successActionsWithReads} from "../../redux/api/utils";
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.
*
* 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))
};
};
```
* **When extending, you shouldn't use the `render` function but the `customRender` (that will be called once all the data is ready)**
*
* IMPORTANT: to use some of the function contained in this class, you need to have a matching `mapStateToProps` function for read
* elements: the key of the object should match the one in `api`.
*
* 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
};
};
```
* 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
* And get inspired by the code base ;)
*
*
* @abstract
* @class CustomComponentForAPI
* @extends {Component}
* @abstract
*/
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
getQueryParams = (propName) => undefined;
apiParams = {};
/**
* 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
getEndPointAttr = (propName) => undefined;
enableSmartDataRefreshOnComponentDidUpdate = false;
/**
* 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) {
super(props);
if (typeof this.getEndPointAttr !== "function") {
throw new Error("getEndPointAttr misconfigured: it must be a function");
if (typeof props === "object" && "api" in props) {
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
this.apiProps = Array();
if (typeof props === "object" && "api" in props) {
const { api } = props;
this.apiProps = Object.keys(api);
}
/**
* @private
*/
smartRefresh() {
Object.entries(this.apiParams).forEach(([propName, mapper]) => {
if (!mapper({props: this.props, state: this.state})
.equals(this._lastRequestParamsObjs[propName])) {
this.performReadFromApi(propName);
}
});
}
/////
......@@ -82,12 +112,18 @@ class CustomComponentForAPI extends Component {
/////
componentDidMount() {
this.readPropsIfNeeded();
// auto refetch data if outdated
this.smartRefresh();
}
// eslint-disable-next-line no-unused-vars
componentDidUpdate(prevProps, prevState, snapshot) {
// extends the default behavior of react to load props (api related) if needed on update.
this.readPropsIfNeeded();
if (this.enableSmartDataRefreshOnComponentDidUpdate) {
// auto refetch data if outdated
this.smartRefresh();
}
}
render() {
......@@ -99,13 +135,14 @@ class CustomComponentForAPI extends Component {
<Notifier
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()) {
return <Loading />;
return <Loading/>;
}
return this.customRender();
......@@ -121,8 +158,8 @@ class CustomComponentForAPI extends Component {
// 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");
}
// End of react functions override
// End of react functions override
/**
......@@ -132,11 +169,11 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
checkPropsFailed() {
return this.apiProps.some((propName) => {
return this._apiAutoPropNames.some((propName) => {
if (typeof this.props[propName] === "undefined") {
// eslint-disable-next-line no-console
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];
return prop.readFailed.failed; // general handling of all types of API reducers
......@@ -165,11 +202,11 @@ class CustomComponentForAPI extends Component {
/**
* Get the list of props for which a read action needs to be triggered
*
* @returns {Array[string]}
* @returns {array.<string>}
* @memberof CustomComponentForAPI
*/
getListPropsNeedRead() {
return this.apiProps.filter((propName) => {
return this._apiAutoPropNames.filter((propName) => {
if (this.propIsUsable(propName)) {
return false;
} else {
......@@ -185,7 +222,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
readPropsIfNeeded() {
this.getListPropsNeedRead().map((propName) => {
this.getListPropsNeedRead().forEach((propName) => {
this.performReadFromApi(propName);
});
}
......@@ -197,21 +234,13 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
performReadFromApi(propName) {
let endPointAttr = [],
queryParams = {};
const tmpQueryParams = this.getQueryParams(propName);
if (tmpQueryParams) {
queryParams = tmpQueryParams;
}
const tmpEndPointAttr = this.getEndPointAttr(propName);
if (tmpEndPointAttr) {
endPointAttr = tmpEndPointAttr;
if (propName in this.apiParams) {
const requestParams = this.apiParams[propName]({props: this.props, state: this.state});
this._lastRequestParamsObjs[propName] = requestParams;
this.props.api[propName](requestParams);
} else {
this.props.api[propName]();
}
this.props.api[propName]({ params: { queryParams, endPointAttr }, props: this.props });
}
/**
......@@ -221,7 +250,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
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 {
* read, create and update are taken into account
*
* @param {string} propName
* @returns {Any}
* @returns {object}
* @memberof CustomComponentForAPI
*/
getLatestReadDataAndTime(propName) {
......@@ -249,7 +278,7 @@ class CustomComponentForAPI extends Component {
* read, create and update are taken into account
*
* @param {string} propName
* @returns {Any}
* @returns {object}
* @memberof CustomComponentForAPI
*/
getLatestReadData(propName) {
......@@ -290,8 +319,8 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
getAllLatestReadData() {
let out = Object();
this.apiProps.forEach((propName) => {
let out = {};
this._apiAutoPropNames.forEach((propName) => {
out[propName] = this.getLatestReadData(propName);
});
return out;
......@@ -308,7 +337,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
getLatestReadDataFor(propNames) {
let out = Object();
let out = {};
propNames.forEach(propName => out[propName] = this.getLatestReadData(propName));
return out;
}
......@@ -324,7 +353,7 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
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
res.university = universities.find(univ => univ.id == campus.university);
res.city = cities.find(city => city.id == campus.city);
......@@ -342,10 +371,10 @@ class CustomComponentForAPI extends Component {
*/
getUnivCityAndCountry(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),
country = countries.find(country => country.id == city.country);
return { city, country };
return {city, country};
}
/**
......
......@@ -9,6 +9,7 @@ import CustomComponentForAPI from "./CustomComponentForAPI";
import "typeface-roboto";
import getActions from "../../redux/api/getActions";
import {RequestParams} from "../../redux/api/RequestParams";
const siteSettings = {
typography: {
......@@ -76,7 +77,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
// __AppUserId is defined in the html rendered by django
// 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 { openFullScreenDialog, closeFullScreenDialog } from "../../redux/actions/fullScreenDialog";
import {RequestParams} from "../../redux/api/RequestParams";
/**
* Function to create the mapDispatchToProps function for editor in a "generic way"
......@@ -13,13 +14,22 @@ export default function getMapDispatchToPropsForEditor(name) {
let lastSave = "update";
return {
// eslint-disable-next-line no-unused-vars
saveData: (data, onSuccessCallback = (newData) => { }) => {
saveData: (data, onSuccessCallback = new Function()) => {
if ("id" in data) { // it's an 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
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)),
......
import {Component} from "react";
import PropTypes from "prop-types";
import areSameObjects from "../../utils/areSameObjects";
import isEqual from "lodash/isEqual";
import renderFieldsMixIn from "./renderFieldsMixIn";
import CustomError from "../common/CustomError";
......@@ -58,7 +58,8 @@ class Form extends Component {
* with resetting it for some reason
* @type {object.<string, Field>}
*/
fields = Object();
fields = {};
/**
* Array containing the possible form level errors
* @abstract
......@@ -142,7 +143,7 @@ class Form extends Component {
// we need to compare objects (ie JSON objects) differently
if (typeof cmp1 === "object") {
return !areSameObjects(cmp1, cmp2);
return !isEqual(cmp1, cmp2);
} else {
return cmp1 !== cmp2;
}
......
......@@ -22,7 +22,7 @@ class MultiSelectField extends Field {
constructor(props) {
super(props);
this.optionsByValue = Object();
this.optionsByValue = {};
props.options.map((opt) => this.optionsByValue[opt.value] = opt.label);
}
......
......@@ -16,7 +16,6 @@ import {Redirect} from "react-router-dom";
import getActions from "../../redux/api/getActions";
import {saveUniversityBeingViewed} from "../../redux/actions/universityPage";
import compose from "recompose/compose";
import {withStyles} from "@material-ui/core";
import {withErrorBoundary} from "../common/ErrorBoundary";
......@@ -24,6 +23,7 @@ import {APP_ROUTES} from "../../config/appRoutes";
import CustomNavLink from "../common/CustomNavLink";
import CustomLink from "../common/CustomLink";
let previousUnivId = -1;
/**
* Component holding the page with the university details
......@@ -34,23 +34,22 @@ import CustomLink from "../common/CustomLink";
*/
class PageUniversity extends CustomComponentForAPI {
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
if (this.props.universities.readSucceeded.readAt) {
// we have the university data
const universities = this.getLatestReadData("universities"),
{match, universityBeingViewed} = this.props,
requestedUniversity = match.params.univId;
componentDidMount() {
super.componentDidMount();
const requestedUnivId = this.getUnivIdFromProps();
if (requestedUniversity != "undefined"
&& universities.find(univ => univ.id == requestedUniversity)
&& requestedUniversity != universityBeingViewed) {
this.props.saveUniversityInView(requestedUniversity);
}
if (requestedUnivId && requestedUnivId !== "previousOne") {
previousUnivId = requestedUnivId;
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
}
getUnivIdFromProps() {
return this.props.match.params.univId;
}
renderUniversityNotFound() {
return (
......@@ -82,7 +81,8 @@ class PageUniversity extends CustomComponentForAPI {
return (
<Paper className={this.props.classes.paper}>
<Typography>
C'est la première fois que vous consulter cet onglet. Nous vous invitons à <CustomLink to={APP_ROUTES.map}>parcourir les universités</CustomLink> dans un
C'est la première fois que vous consulter cet onglet. Nous vous invitons à <CustomLink to={APP_ROUTES.map}>parcourir
les universités</CustomLink> dans un
premier temps. 😁
</Typography>
</Paper>
......@@ -99,14 +99,14 @@ class PageUniversity extends CustomComponentForAPI {
}
customRender() {
const {match, universityBeingViewed} = this.props,
const {match} = this.props,
requestedUnivId = match.params.univId,
tabName = match.params.tabName,
universities = this.getLatestReadData("universities");
if (requestedUnivId === "previousOne" || typeof requestedUnivId === "undefined") {
if (universityBeingViewed != null) {
return this.renderUniversityUndefinedButHavePrevious(universityBeingViewed);
if (previousUnivId !== -1) {
return this.renderUniversityUndefinedButHavePrevious(previousUnivId);
} else {
return this.renderFirstTimeHere();
}
......@@ -121,17 +121,14 @@ class PageUniversity extends CustomComponentForAPI {
}
PageUniversity.propTypes = {
universityBeingViewed: PropTypes.string,
universities: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
saveUniversityInView: PropTypes.func.isRequired,
classes: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => {
return {
universities: state.api.universitiesAll,
universityBeingViewed: state.app.universityBeingViewed
};
};
......@@ -140,7 +137,6 @@ const mapDispatchToProps = (dispatch) => {
api: {
universities: () => dispatch(getActions("universities").readAll()),
},
saveUniversityInView: (univId) => dispatch(saveUniversityBeingViewed(univId))
};
};
......
......@@ -26,11 +26,12 @@ import ExpansionPanel from "@material-ui/core/ExpansionPanel/index";
import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary/index";
import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails/index";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import areSameObjects from "../../../utils/areSameObjects";
import isEqual from "lodash/isEqual";
import defaultSiteTheme from "../../../config/defaultTheme.json";
import Divider from "@material-ui/core/Divider/index";
import TextLink from "../../common/TextLink";
import {deepCopy} from "../../../utils/deepCopy";
import {RequestParams} from "../../../redux/api/RequestParams";
const isRgb = string => /#?([0-9a-f]{6})/i.test(string);
......@@ -254,7 +255,7 @@ class ColorTool extends CustomComponentForAPI {
customRender() {
const {classes} = this.props,
themeForDemo = getTheme(this.state.theme),
hasChanges = !areSameObjects(this.state.theme, this.getInitialTheme()),
hasChanges = !isEqual(this.state.theme, this.getInitialTheme()),
darkModeActivated = this.state.theme.mode === "dark";
return (
......@@ -310,11 +311,11 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
// eslint-disable-next-line no-undef
saveUserDataToServer: (data) => dispatch(getActions("userData").update(__AppUserId, data)),
saveUserDataToServer: (data) => dispatch(getActions("userData").update(RequestParams.Builder.withId(__AppUserId).withData(data).build())),
api: {
// __AppUserId is defined in the html rendered by django
// eslint-disable-next-line no-undef
userData: () => dispatch(getActions("userData").readOne(__AppUserId)), // id not needed userData
userData: () => dispatch(getActions("userData").readOne(RequestParams.Builder.withId(__AppUserId).build())), // id not needed userData
},
};
};
......
......@@ -14,6 +14,7 @@ import ModuleGroupWrapper from "./common/ModuleGroupWrapper";
import CountryDriEditor from "../editors/CountryDriEditor";
import getActions from "../../../redux/api/getActions";
import {withUnivInfo} from "../common/withUnivInfo";
import {RequestParams} from "../../../redux/api/RequestParams";
// eslint-disable-next-line no-unused-vars
......@@ -29,10 +30,9 @@ function renderCore(rawModelData, classes, outsideData) {
}
class CountryDri extends Module {
/**
* @override
*/
getQueryParams = (propName) => propName === "countryDri" ? ({ countries: this.props.countryId }) : undefined;
apiParams = {
countryDri: ({props}) => RequestParams.Builder.withQueryParam("countries", props.countryId).build(),
};
customRender() {
const countryDriItems = this.getOnlyReadData("countryDri"),
......@@ -86,7 +86,7 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
api: {
countryDri: ({ params }) => dispatch(getActions("countryDri").readAll(params)),
countryDri: (params) => dispatch(getActions("countryDri").readAll(params)),
},
invalidateData: () => dispatch(getActions("countryDri").invalidateAll())
};
......
......@@ -16,6 +16,7 @@ import CountryScholarshipEditor from "../editors/CountryScholarshipEditor";
import getActions from "../../../redux/api/getActions";
import {withUnivInfo} from "../common/withUnivInfo";
import {RequestParams} from "../../../redux/api/RequestParams";
// eslint-disable-next-line no-unused-vars
......@@ -43,11 +44,9 @@ function renderCore(rawModelData, classes, outsideData) {
</