Commit 699dee3e authored by Florent Chehab's avatar Florent Chehab

refactoring(front): new way to use data from backend (not yet used everywhere) & tweaks

* Created new hook useSingleApiData
* Created new hoc withNetworkWrapper
* Converted some components to use the new setup
* Added new services for common data (helpers should later be merged in the services)
* Few bugs in search and tips and tricks (need other refactoring to be fixed)
etc.

WIP #126 #131
parent 0078dbd5
......@@ -20,7 +20,7 @@ module.exports = {
plugins: ["react", "jest", "import"],
rules: {
"no-warning-comments": [
"error",
"warn",
{
terms: ["todo", "fixme"],
location: "anywhere"
......
......@@ -2,16 +2,11 @@
*/
import React from "react";
import { connect } from "react-redux";
import { compose } from "recompose";
import { Route, Switch } from "react-router-dom";
import FullScreenDialog from "./FullScreenDialog";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import { withErrorBoundary } from "../common/ErrorBoundary";
import getActions from "../../redux/api/getActions";
import PageMap from "../pages/PageMap";
import PageHome from "../pages/PageHome";
import PageUniversity from "../pages/PageUniversity";
......@@ -30,97 +25,75 @@ import NotifierImportantInformation from "./NotifierImportantInformation";
import FooterImportantInformation from "./FooterImportantInformation";
import PageAboutUnlinkedPartners from "../pages/PageAboutUnlinkedPartners";
import PageLogout from "../pages/PageLogout";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
// import PageFiles from "../pages/PageFiles";
/**
* @class App
* @extends {CustomComponentForAPI}
* @extends React.Component
* Main entry
*/
class App extends CustomComponentForAPI {
customRender() {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyItems: "flex-end",
minHeight: "100vh"
}}
>
<MainAppFrame>
<FullScreenDialog />
<NotifierImportantInformation />
<main>
<Switch>
<Route exact path={APP_ROUTES.base} component={PageHome} />
<Route path={APP_ROUTES.search} component={PageSearch} />
<Route path={APP_ROUTES.map} component={PageMap} />
<Route path={APP_ROUTES.themeSettings} component={PageSettings} />
<Route path={APP_ROUTES.listsWithParams} component={PageLists} />
<Route
path={APP_ROUTES.universityWithParams}
component={PageUniversity}
/>
<Route
path={APP_ROUTES.myExchanges}
component={PageMyExchanges}
/>
<Route
path={APP_ROUTES.editPreviousExchangeWithParams}
component={PageEditPreviousExchanges}
/>
<Route path={APP_ROUTES.userWithParams} component={PageUser} />
{/* <Route path={APP_ROUTES.userFilesWithParams} component={PageFiles}/> WARNING BETA */}
<Route
path={APP_ROUTES.aboutProject}
component={PageAboutProject}
/>
<Route path={APP_ROUTES.aboutRgpd} component={PageRgpd} />
<Route path={APP_ROUTES.aboutCgu} component={PageCgu} />
<Route
path={APP_ROUTES.aboutUnlinkedPartners}
component={PageAboutUnlinkedPartners}
/>
<Route path={APP_ROUTES.logout} component={PageLogout} />
<Route component={PageNotFound} />
</Switch>
</main>
</MainAppFrame>
<FooterImportantInformation />
</div>
);
}
function App() {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyItems: "flex-end",
minHeight: "100vh"
}}
>
<MainAppFrame>
<FullScreenDialog />
<NotifierImportantInformation />
<main>
<Switch>
<Route exact path={APP_ROUTES.base} component={PageHome} />
<Route path={APP_ROUTES.search} component={PageSearch} />
<Route path={APP_ROUTES.map} component={PageMap} />
<Route path={APP_ROUTES.themeSettings} component={PageSettings} />
<Route path={APP_ROUTES.listsWithParams} component={PageLists} />
<Route
path={APP_ROUTES.universityWithParams}
component={PageUniversity}
/>
<Route path={APP_ROUTES.myExchanges} component={PageMyExchanges} />
<Route
path={APP_ROUTES.editPreviousExchangeWithParams}
component={PageEditPreviousExchanges}
/>
<Route path={APP_ROUTES.userWithParams} component={PageUser} />
{/* <Route path={APP_ROUTES.userFilesWithParams} component={PageFiles}/> WARNING BETA */}
<Route
path={APP_ROUTES.aboutProject}
component={PageAboutProject}
/>
<Route path={APP_ROUTES.aboutRgpd} component={PageRgpd} />
<Route path={APP_ROUTES.aboutCgu} component={PageCgu} />
<Route
path={APP_ROUTES.aboutUnlinkedPartners}
component={PageAboutUnlinkedPartners}
/>
<Route path={APP_ROUTES.logout} component={PageLogout} />
<Route component={PageNotFound} />
</Switch>
</main>
</MainAppFrame>
<FooterImportantInformation />
</div>
);
}
App.propTypes = {};
// Already load some of the data even if it's not use here.
// /!\ Don't performDelete it
const mapStateToProps = state => ({
countries: state.api.countriesAll,
currencies: state.api.currenciesAll,
universities: state.api.universitiesAll,
languages: state.api.languagesAll,
serverModerationStatus: state.api.serverModerationStatusAll
});
const mapDispatchToProps = dispatch => ({
api: {
countries: () => dispatch(getActions("countries").readAll()),
currencies: () => dispatch(getActions("currencies").readAll()),
universities: () => dispatch(getActions("universities").readAll()),
languages: () => dispatch(getActions("languages").readAll()),
serverModerationStatus: () =>
dispatch(getActions("serverModerationStatus").readAll()) // not needed for server moderation status
}
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
withNetworkWrapper([
new NetWrapParam("countries", "all"),
new NetWrapParam("cities", "all"),
new NetWrapParam("universities", "all"),
new NetWrapParam("mainCampuses", "all"),
new NetWrapParam("currencies", "all"),
new NetWrapParam("languages", "all"),
new NetWrapParam("serverModerationStatus", "all") // not needed for server moderation status
]),
withErrorBoundary()
)(App);
import React from "react";
import { connect } from "react-redux";
import { compose } from "recompose";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import Typography from "@material-ui/core/Typography";
import PropTypes from "prop-types";
import toDateFr from "../../utils/dateToFr";
import getActions from "../../redux/api/getActions";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
function getLabel(source) {
if (source === "fixer") return "Données concernant les taux de change";
......@@ -18,56 +15,39 @@ function getLabel(source) {
}
/**
* Class to render notifications for important stuff
*
* @class ExternalDataUpdateInfo
* @extends {CustomComponentForAPI}
* @extends React.Component
* Component to render notifications for important stuff
*/
class ExternalDataUpdateInfo extends CustomComponentForAPI {
customRender() {
const updateList = this.getLatestReadData("updates");
updateList.sort((a, b) => a.source.localeCompare(b.source));
function ExternalDataUpdateInfo({ updates }) {
updates.sort((a, b) => a.source.localeCompare(b.source));
if (updateList.length > 0) {
return (
<List aria-label="List of updates">
{updateList.map(el => (
<ListItem key={el.source}>
<ListItemText
primary={getLabel(el.source)}
secondary={toDateFr(el.timestamp)}
/>
</ListItem>
))}
</List>
);
}
if (updates.length > 0) {
return (
<Typography variant="caption" display="block">
Aucune mise-à-jour ne semble avoir été réalisée.
</Typography>
<List aria-label="List of updates">
{updates.map(el => (
<ListItem key={el.source}>
<ListItemText
primary={getLabel(el.source)}
secondary={toDateFr(el.timestamp)}
/>
</ListItem>
))}
</List>
);
}
return (
<Typography variant="caption" display="block">
Aucune mise-à-jour ne semble avoir été réalisée.
</Typography>
);
}
ExternalDataUpdateInfo.propTypes = {};
ExternalDataUpdateInfo.propTypes = {
updates: PropTypes.array.isRequired
};
ExternalDataUpdateInfo.defaultProps = {};
const mapStateToProps = state => ({
updates: state.api.externalDataUpdateInfoAll
});
const mapDispatchToProps = dispatch => ({
api: {
updates: () => dispatch(getActions("externalDataUpdateInfo").readAll())
}
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
)
)(ExternalDataUpdateInfo);
export default withNetworkWrapper([
new NetWrapParam("externalDataUpdateInfo", "all", "updates")
])(ExternalDataUpdateInfo);
import React from "react";
import { connect } from "react-redux";
import { compose } from "recompose";
import ErrorIcon from "@material-ui/icons/Error";
import StartIcon from "@material-ui/icons/StarTwoTone";
import InfoIcon from "@material-ui/icons/Info";
import WarningIcon from "@material-ui/icons/Warning";
import { withSnackbar } from "notistack";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import PropTypes from "prop-types";
import RequestParams from "../../redux/api/RequestParams";
import getActions from "../../redux/api/getActions";
import { withErrorBoundary } from "../common/ErrorBoundary";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import toDateFr from "../../utils/dateToFr";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
const INFORMATION_ICONS = {
success: <StartIcon />,
......@@ -27,63 +22,44 @@ const INFORMATION_ICONS = {
};
/**
* Class to render notifications for important stuff
*
* @class InformationList
* @extends {CustomComponentForAPI}
* @extends React.Component
* Component to render notifications for important stuff
*/
class InformationList extends CustomComponentForAPI {
customRender() {
const { includeVariants } = this.props;
const informationList = this.getLatestReadData("information").filter(
({ variant }) => includeVariants.includes(variant)
);
function InformationList({ includeVariants, informationList }) {
const informationListFiltered = informationList.filter(({ variant }) =>
includeVariants.includes(variant)
);
informationList.sort((a, b) => -a.start.localeCompare(b.start));
informationListFiltered.sort((a, b) => -a.start.localeCompare(b.start));
return (
<List aria-label="List of information">
{informationList.map(el => (
<ListItem key={el.id}>
<ListItemIcon>{INFORMATION_ICONS[el.variant]}</ListItemIcon>
<ListItemText primary={el.message} secondary={toDateFr(el.start)} />
</ListItem>
))}
</List>
);
}
return (
<List aria-label="List of information">
{informationListFiltered.map(el => (
<ListItem key={el.id}>
<ListItemIcon>{INFORMATION_ICONS[el.variant]}</ListItemIcon>
<ListItemText primary={el.message} secondary={toDateFr(el.start)} />
</ListItem>
))}
</List>
);
}
InformationList.propTypes = {
includeVariants: PropTypes.arrayOf(PropTypes.string.isRequired)
includeVariants: PropTypes.arrayOf(PropTypes.string.isRequired),
informationList: PropTypes.array.isRequired
};
InformationList.defaultProps = {
includeVariants: ["warning", "error", "info", "success"]
};
const mapStateToProps = state => ({
information: state.api.informationAll
});
const mapDispatchToProps = dispatch => ({
api: {
information: () =>
dispatch(
getActions("information").readAll(
RequestParams.Builder.withQueryParam("now", "true").build()
)
)
}
});
export default compose(
withSnackbar,
connect(
mapStateToProps,
mapDispatchToProps
),
withNetworkWrapper([
new NetWrapParam(
"information",
"all",
"informationList",
RequestParams.Builder.withQueryParam("now", "true").build()
)
]),
withErrorBoundary()
)(InformationList);
import React from "react";
import { connect } from "react-redux";
import { compose } from "recompose";
import { withSnackbar } from "notistack";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import PropTypes from "prop-types";
import { withErrorBoundary } from "../common/ErrorBoundary";
import getActions from "../../redux/api/getActions";
import Notifier from "../common/Notifier";
import RequestParams from "../../redux/api/RequestParams";
/**
* Class to create notifications for important stuff on start up
*
* @class NotifierImportantInformation
* @extends {CustomComponentForAPI}
* @extends React.Component
*/
class NotifierImportantInformation extends CustomComponentForAPI {
/**
* Use standard render method to make no loading indicator is displayed.
*/
render() {
if (this.checkPropsFailed() || !this.allApiDataIsReady()) {
return <></>;
}
const informationList = this.getLatestReadData("information");
return (
<>
{informationList
.filter(el => el.should_notify)
.map(el => (
<Notifier
key={el.id}
message={el.message}
options={{
variant: el.variant,
autoHideDuration: null
}}
/>
))}
</>
);
}
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
function NotifierImportantInformation({ informationList }) {
return (
<>
{informationList
.filter(el => el.should_notify)
.map(el => (
<Notifier
key={el.id}
message={el.message}
options={{
variant: el.variant,
autoHideDuration: null
}}
/>
))}
</>
);
}
NotifierImportantInformation.propTypes = {};
const mapStateToProps = state => ({
information: state.api.informationAll
});
const mapDispatchToProps = dispatch => ({
api: {
information: () =>
dispatch(
getActions("information").readAll(
RequestParams.Builder.withQueryParam("now", "true").build()
)
)
}
});
NotifierImportantInformation.propTypes = {
informationList: PropTypes.array.isRequired
};
export default compose(
withSnackbar,
connect(
mapStateToProps,
mapDispatchToProps
),
withNetworkWrapper([
new NetWrapParam(
"information",
"all",
"informationList",
RequestParams.Builder.withQueryParam("now", "true").build()
)
]),
withErrorBoundary()
)(NotifierImportantInformation);
import React from "react";
import { connect } from "react-redux";
import { compose } from "recompose";
import PropTypes from "prop-types";
import Typography from "@material-ui/core/Typography";
import List from "@material-ui/core/List";
......@@ -10,99 +7,73 @@ import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import APP_ROUTES from "../../config/appRoutes";
import CustomLink from "../common/CustomLink";
import getActions from "../../redux/api/getActions";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
/**
* Class to render notifications for important stuff
*
* @class UnlinkedPartners
* @extends {CustomComponentForAPI}
* @extends React.Component
*/
class UnlinkedPartners extends CustomComponentForAPI {
customRender() {
const unlinkedPartners = this.getLatestReadData("unlinkedUtcPartners");
const nUnlinked = unlinkedPartners.length;
const { variant } = this.props;
function UnlinkedPartners({ unlinkedPartners, variant }) {
const nUnlinked = unlinkedPartners.length;
if (variant === "detailed") {
return (
<>
{nUnlinked > 0 ? (
<>
<Typography>
{nUnlinked}
&nbsp; partenaires sont dans ce cas. En voici la liste :
</Typography>
<List aria-label="List des partenaires qui ne sont pas encore disponibles sur la plateforme REX-DRI">
{unlinkedPartners.map((nameOnEnt, idx) => (
// eslint-disable-next-line react/no-array-index-key
<ListItem key={idx}>
<ListItemText primary={nameOnEnt} />
</ListItem>
))}
</List>
</>
) : (
if (variant === "detailed") {
return (
<>
{nUnlinked > 0 ? (
<>
<Typography>
Actuellement, tous les partenaires de l'UTC sont disponibles sur
la plateforme ! 🎉
{nUnlinked}
&nbsp; partenaires sont dans ce cas. En voici la liste :
</Typography>
)}
</>
);
}
if (variant === "summary") {
return (
<>
{nUnlinked > 0 ? (
<>
<Typography variant="caption" color="primary">
&nbsp;
{nUnlinked === 1
? "1 université partenaire de l'UTC n'est pas encore pleinement disponible"
: `${nUnlinked} universités partenaires de l'UTC ne sont pas encore pleinement disponibles`}
sur la plateforme. Plus d'informations&nbsp;
<CustomLink to={APP_ROUTES.aboutUnlinkedPartners}>
<Typography variant="caption" color="primary">
ici
</Typography>
</CustomLink>
.
</Typography>
</>
) : (
<></>
)}
</>
);
}
return <></>;
<List aria-label="List des partenaires qui ne sont pas encore disponibles sur la plateforme REX-DRI">
{unlinkedPartners.map((nameOnEnt, idx) => (
// eslint-disable-next-line react/no-array-index-key
<ListItem key={idx}>
<ListItemText primary={nameOnEnt} />
</ListItem>
))}
</List>
</>
) : (
<Typography>
Actuellement, tous les partenaires de l'UTC sont disponibles sur la
plateforme ! 🎉
</Typography>
)}
</>
);
}
if (variant === "summary") {
return (
<>
{nUnlinked > 0 && (
<>
<Typography variant="caption" color="primary">
&nbsp;
{nUnlinked === 1
? "1 université partenaire de l'UTC n'est pas encore pleinement disponible"
: `${nUnlinked} universités partenaires de l'UTC ne sont pas encore pleinement disponibles`}
sur la plateforme. Plus d'informations&nbsp;
<CustomLink to={APP_ROUTES.aboutUnlinkedPartners}>
<Typography variant="caption" color="primary">
ici
</Typography>
</CustomLink>
.
</Typography>
</>
)}
</>
);
}
return <></>;
}
UnlinkedPartners.propTypes = {
variant: PropTypes.oneOf(["summary", "detailed"]).isRequired
variant: PropTypes.oneOf(["summary", "detailed"]).isRequired,
unlinkedPartners: PropTypes.array.isRequired
};
UnlinkedPartners.defaultProps = {};
const mapStateToProps = state => ({
unlinkedUtcPartners: state.api.unlinkedUtcPartnersAll
});
const mapDispatchToProps = dispatch => ({
api: {
unlinkedUtcPartners: () =>
dispatch(getActions("unlinkedUtcPartners").readAll())
}
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
)
)(UnlinkedPartners);
export default withNetworkWrapper([
new NetWrapParam("unlinkedUtcPartners", "all", "unlinkedPartners")
])(UnlinkedPartners);
......@@ -2,7 +2,7 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import Loading from "./Loading";
import { getLatestRead, successActionsWithReads } from "../../redux/api/utils";
import { apiDataIsUsable, getLatestRead } from "../../redux/api/utils";
import Notifier from "./Notifier";
import RequestParams from "../../redux/api/RequestParams";
......@@ -212,15 +212,7 @@ class CustomComponentForAPI extends Component {
*/
propIsUsable(propName) {
const prop = this.props[propName];
return (
!prop.isInvalidated &&
successActionsWithReads
.filter(action => action in prop) // general handling of all types of API reducers
.some(action => prop[action].readAt !== 0) && // makes sure will consider all success actions
["isReading"] // , "isUpdating", "isCreating"] Don't put those in here it may cause unwanted rerendering whole tree when saving
.filter(action => action in prop)
.every(action => prop[action] === false)
);
return apiDataIsUsable(prop);
}
/**
...