Commit 1451f9ca authored by Florent Chehab's avatar Florent Chehab Committed by Florent Chehab
Browse files

Removed(custom component for api)

parent c9f69977
/* eslint-disable react/sort-comp */
import React, { Component } from "react";
import PropTypes from "prop-types";
import Loading from "./Loading";
import { apiDataIsUsable, getLatestRead } from "../../redux/api/utils";
import RequestParams from "../../redux/api/RequestParams";
import NotificationService from "../../services/NotificationService";
/**
* 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)**
*
* 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 ;)
*
* @class CustomComponentForAPI
* @extends {Component}
* @abstract
*/
class CustomComponentForAPI extends Component {
/**
* 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>}
*/
apiParams = {};
/**
* Should 'smartRefresh' (based on the comparaison of the previous RequestParam obj and what it would look like
* now) be enabled?
* @type {boolean}
*/
enableSmartDataRefreshOnComponentDidUpdate = false;
/**
* a little bit of optimization, Stores the list of props that are "automatically" connected to the API
* @type {string[]}
* @private
*/
_apiAutoPropNames = [];
/**
* Stores the last requestParams intances used for each props
* @type {object.<string,RequestParams>}
* @private
*/
_lastRequestParamsObjs = {};
constructor(props) {
super(props);
if (typeof props === "object" && "api" in props) {
const { api } = props;
this._apiAutoPropNames = Object.keys(api);
}
/**
* 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;
});
}
/**
* @private
*/
smartRefresh() {
Object.entries(this.apiParams).forEach(([propName, mapper]) => {
if (
!mapper({
props: this.props,
state: this.state
}).equals(this._lastRequestParamsObjs[propName])
) {
this.performReadFromApi(propName);
}
});
}
// ///
// React default functions override for uniform custom handling
// ///
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() {
// Override of the default react render behavior to wait for data to arrive.
// You should use `customRender` instead of `render` in your components.
if (this.checkPropsFailed()) {
NotificationService.error(
"Une erreur est survenue sur le serveur. Merci de recharger le page."
);
return (
<>
<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 le
SIMDE.
</p>
</>
);
}
if (!this.allApiDataIsReady()) {
return <Loading />;
}
return this.customRender();
}
/**
* Function to use instead of `render`.
*
* @virtual
* @memberof CustomComponentForAPI
*/
customRender() {
// 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
/**
* Check that none of the API props have failed during reading
*
* @returns {boolean}
* @memberof CustomComponentForAPI
*/
checkPropsFailed() {
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 (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
});
}
/**
* Check that the prop `propName` has been read from the server
*
* @param {string} propName
* @returns {boolean}
* @memberof CustomComponentForAPI
*/
propIsUsable(propName) {
const prop = this.props[propName];
return apiDataIsUsable(prop);
}
/**
* Get the list of props for which a read action needs to be triggered
*
* @returns {array.<string>}
* @memberof CustomComponentForAPI
*/
getListPropsNeedRead() {
return this._apiAutoPropNames.filter(propName => {
if (this.propIsUsable(propName)) {
return false;
}
const prop = this.props[propName];
return !prop.readFailed.failed && !prop.isReading; // general handling of all types of API reducers
});
}
/**
* Read all the props from api that need to be read.
*
* @memberof CustomComponentForAPI
*/
readPropsIfNeeded() {
this.getListPropsNeedRead().forEach(propName => {
this.performReadFromApi(propName);
});
}
/**
* Read the prop corresponding to `propName` from the API.
*
* @param {string} propName
* @memberof CustomComponentForAPI
*/
performReadFromApi(propName) {
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]();
}
}
/**
* Checks that all the data from the API has been read
*
* @returns
* @memberof CustomComponentForAPI
*/
allApiDataIsReady() {
return this._apiAutoPropNames.every(propName =>
this.propIsUsable(propName)
);
}
/**
* Get the latest data that was read from the api given the `propName` and the time at which is was read
* read, create and update are taken into account
*
* @param {string} propName
* @returns {object}
* @memberof CustomComponentForAPI
*/
getLatestReadDataAndTime(propName) {
const prop = this.props[propName];
const out = getLatestRead(prop);
if (!("data" in out)) {
throw Error(
`No read data from the api could be retrieved for: ${propName}`
);
} else {
return out;
}
}
/**
* Get the latest data that was read from the api given the `propName`
* read, create and update are taken into account
*
* @param {string} propName
* @returns {object}
* @memberof CustomComponentForAPI
*/
getLatestReadData(propName) {
return this.getLatestReadDataAndTime(propName).data;
}
/**
* In some very rare case, we need to consider only the data that was read and not create ou updated.
* That what this function does.
*
* @param {string} propName
* @returns
* @memberof CustomComponentForAPI
*/
getOnlyReadData(propName) {
return this.props[propName].readSucceeded.data;
}
/**
* Get the latest time at which the latest data from the api corresponding to `propName` was read
*
* read, create and update are taken into account
*
* @param {string} propName
* @returns {number}
* @memberof CustomComponentForAPI
*/
getLatestReadTime(propName) {
return this.getLatestReadDataAndTime(propName).readAt;
}
/**
* Access to all the latest data loaded from the api given the props
*
* read, create and update are taken into account
*
* @returns {object}
* @memberof CustomComponentForAPI
*/
getAllLatestReadData() {
const out = {};
this._apiAutoPropNames.forEach(propName => {
out[propName] = this.getLatestReadData(propName);
});
return out;
}
/**
* Access to the latest read data from the propNames array
* This should be used instead of getAllLatestReadData to optimize things
*
* read, create and update are taken into account
*
* @param {array} propNames Array of the prop names you want to read data from
* @returns {object}
* @memberof CustomComponentForAPI
*/
getLatestReadDataFor(propNames) {
const out = {};
propNames.forEach(propName => {
out[propName] = this.getLatestReadData(propName);
});
return out;
}
}
CustomComponentForAPI.propTypes = {
api: PropTypes.objectOf(PropTypes.func).isRequired // should be given through redux connect of `mapDispatchToProps`
};
export default CustomComponentForAPI;
import React from "react";
import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles";
import Paper from "@material-ui/core/Paper";
import { compose } from "recompose";
import { connect } from "react-redux";
import { Typography } from "@material-ui/core";
import { withErrorBoundary } from "../common/ErrorBoundary";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import { makeStyles } from "@material-ui/styles";
import RequestParams from "../../redux/api/RequestParams";
import getActions from "../../redux/api/getActions";
import Pictures from "../user/Pictures";
import useInvalidateAll from "../../hooks/useInvalidateAll";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
const useStyles = makeStyles(theme => ({
paper: theme.myPaper
}));
function getUserIdFromUrl(match) {
return match.params.userId;
}
const buildPictureParams = match =>
RequestParams.Builder.withQueryParam(
"owner",
getUserIdFromUrl(match)
).build();
const buildFilesParams = match =>
RequestParams.Builder.withQueryParam(
"owner",
getUserIdFromUrl(match)
).build();
/**
* WARNING BETA files & padding
* Page that lists the files available
* @class PageFiles
* @extends {React.Component}
*/
class PageFiles extends CustomComponentForAPI {
apiParams = {
files: ({ props }) =>
RequestParams.Builder.withQueryParam(
"owner",
this.getUserIdFromUrl(props)
).build(),
pictures: ({ props }) =>
RequestParams.Builder.withQueryParam(
"owner",
this.getUserIdFromUrl(props)
).build()
};
getUserIdFromUrl(props = this.props) {
return props.match.params.userId;
}
// eslint-disable-next-line no-unused-vars
function PageFiles({ pictures, files }) {
const classes = useStyles();
const invalidate = useInvalidateAll("pictures", "files");
customRender() {
// WARNING BETA files & padding
const { theme } = this.props;
const { pictures } = this.getLatestReadDataFor(["pictures", "files"]);
return (
<>
<Paper style={theme.myPaper}>
<Typography variant="h3">Photos</Typography>
<Pictures
pictures={pictures}
onSomethingWasSaved={() => this.props.invalidatePictures()}
/>
</Paper>
{/* <Paper style={theme.myPaper}> */}
{/* <Typography variant={"h3"}>Fichiers</Typography> */}
{/* </Paper> */}
</>
);
}
return (
<>
<Paper className={classes.paper}>
<Typography variant="h3">Photos</Typography>
<Pictures pictures={pictures} onSomethingWasSaved={invalidate} />
</Paper>
{/* <Paper style={theme.myPaper}> */}
{/* <Typography variant={"h3"}>Fichiers</Typography> */}
{/* </Paper> */}
</>
);
}
PageFiles.propTypes = {
theme: PropTypes.object.isRequired,
match: PropTypes.shape({
params: PropTypes.shape({
userId: PropTypes.string.isRequired
})
}).isRequired,
classes: PropTypes.object.isRequired
pictures: PropTypes.array.isRequired,
files: PropTypes.array.isRequired
};
const mapStateToProps = state => ({
pictures: state.api.picturesAll,
files: state.api.filesAll
});
const mapDispatchToProps = dispatch => ({
api: {
pictures: params => dispatch(getActions("pictures").readAll(params)),
files: params => dispatch(getActions("files").readAll(params))
},
invalidatePictures: () => dispatch(getActions("pictures").invalidateAll())
});
// eslint-disable-next-line no-unused-vars
const styles = theme => ({});
export default compose(
withStyles(styles, { withTheme: true }),
connect(
mapStateToProps,
mapDispatchToProps
),
withErrorBoundary()
withNetworkWrapper([
new NetWrapParam("pictures", "all", "pictures", props =>
buildPictureParams(props.match)
),
new NetWrapParam("files", "all", "files", props =>
buildFilesParams(props.match)
)
])
)(PageFiles);
......@@ -12,7 +12,6 @@ import { rgbToHex } from "@material-ui/core/styles/colorManipulator";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider";
import { compose } from "recompose";
import Input from "@material-ui/core/Input";
import ExpansionPanel from "@material-ui/core/ExpansionPanel";
......@@ -23,9 +22,7 @@ import isEqual from "lodash/isEqual";
import Divider from "@material-ui/core/Divider";
import defaultSiteTheme from "../../../config/defaultTheme.json";
import SaveButton from "../../common/SaveButton";
import { getLatestRead } from "../../../redux/api/utils";
import getActions from "../../../redux/api/getActions";
import CustomComponentForAPI from "../../common/CustomComponentForAPI";
import ColorDemo from "./ColorDemo";
import TextLink from "../../common/TextLink";
import deepCopy from "../../../utils/deepCopy";
......@@ -33,6 +30,9 @@ import RequestParams from "../../../redux/api/RequestParams";
import { getTheme } from "../../common/theme/utils";
import { CURRENT_USER_ID } from "../../../config/user";
import LicenseNotice from "../../common/LicenseNotice";
import withNetworkWrapper, {
NetWrapParam
} from "../../../hoc/withNetworkWrapper";
const isRgb = string => /#?([0-9a-f]{6})/i.test(string);
......@@ -40,10 +40,9 @@ const isRgb = string => /#?([0-9a-f]{6})/i.test(string);
* Component to handle website color customization
*
* @class ColorTool
* @extends {CustomComponentForAPI}
* @extends React.Component
*/
class ColorTool extends CustomComponentForAPI {
class ColorTool extends React.Component {
constructor(props) {
super(props);
......@@ -56,7 +55,7 @@ class ColorTool extends CustomComponentForAPI {
* @returns {object}
*/
getInitialTheme() {
return deepCopy(getLatestRead(this.props.userData).data.theme);
return deepCopy(this.props.userData.theme);
}
/**
......@@ -131,7 +130,7 @@ class ColorTool extends CustomComponentForAPI {
* Save the new theme on the server
*/
handleSendToServer() {
const userData = deepCopy(this.getLatestReadData("userData"));
const userData = deepCopy(this.props.userData);
const newUserData = Object.assign(userData, { theme: this.state.theme });
this.props.saveUserDataToServer(newUserData);
}
......@@ -258,7 +257,7 @@ class ColorTool extends CustomComponentForAPI {
);
}
customRender() {
render() {
const { classes } = this.props;
const themeForDemo = getTheme(this.state.theme);
const hasChanges = !isEqual(this.state.theme, this.getInitialTheme());
......@@ -314,9 +313,8 @@ ColorTool.propTypes = {
userData: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
userData: state.api.userDataOne
});
// eslint-disable-next-line no-unused-vars
const mapStateToProps = state => ({});
const mapDispatchToProps = dispatch => ({
saveUserDataToServer: data =>
......@@ -326,15 +324,7 @@ const mapDispatchToProps = dispatch => ({
.withData(data)
.build()
)
),
api: {
userData: () =>
dispatch(
getActions("userData").readOne(
RequestParams.Builder.withId(CURRENT_USER_ID).build()