Commit 68b28c07 authored by Florent Chehab's avatar Florent Chehab

done(frontend hookification)

* Hookified all fields
* Hookified all service components
* Hookified Downshift multiple
* Hookified maps
(remaining class components are a bit harder to hookify)

* **Big update to custom hooks replacing redux to make sure there is no bugs**

* Reoganized scholarship & fixed creation bugs
* onSave instead of invalidateGroup in moduleGroupWrapper
* Fixed currency bugs
* Fixed editor not invalidating data on save
* Fixed duplicate requests
* Change link style, to something more normal
* Cleaned conditionnal jsx

Fixes #126
parent dc9fd10f
......@@ -26,9 +26,7 @@ import FooterImportantInformation from "./FooterImportantInformation";
import PageAboutUnlinkedPartners from "../pages/PageAboutUnlinkedPartners";
import PageLogout from "../pages/PageLogout";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
import FullScreenDialogService from "../../services/FullScreenDialogService";
import NotificationServiceComponent from "../services/NotificationServiceComponent";
import NotificationService from "../../services/NotificationService";
import UniversityService from "../../services/data/UniversityService";
import CityService from "../../services/data/CityService";
import CountryService from "../../services/data/CountryService";
......@@ -36,7 +34,6 @@ import CurrencyService from "../../services/data/CurrencyService";
import LanguageService from "../../services/data/LanguageService";
import FilterService from "../../services/FilterService";
import AlertServiceComponent from "../services/AlertServiceComponent";
import AlertService from "../../services/AlertService";
const SERVICES_TO_INITIALIZE = [
UniversityService,
......@@ -68,11 +65,9 @@ function App() {
}}
>
<MainAppFrame>
<AlertServiceComponent ref={AlertService.setComponent} />
<FullScreenDialogServiceComponent
ref={FullScreenDialogService.setComponent}
/>
<NotificationServiceComponent ref={NotificationService.setComponent} />
<AlertServiceComponent />
<FullScreenDialogServiceComponent />
<NotificationServiceComponent />
<NotifierImportantInformation />
<main>
<Switch>
......
/**
* Class to handle app errors (such as _form errors) in a nice wrapped way
* It's for non fatal errors.
*
* @export
* @class CustomError
*/
export default class CustomError {
/**
*Creates an instance of CustomError.
*
* @param {Array[string] = []} messages
* @memberof CustomError
*/
constructor(messages = []) {
this.messages = messages;
......@@ -23,7 +19,6 @@ export default class CustomError {
* @static
* @param {Array[CustomError]} arrOfCustomErrors
* @returns
* @memberof CustomError
*/
static superCombine(arrOfCustomErrors) {
return new CustomError(
......@@ -36,7 +31,6 @@ export default class CustomError {
*
* @param {CustomError} other
* @returns {CustomError}
* @memberof CustomError
*/
combine(other) {
return new CustomError(
......
......@@ -9,7 +9,7 @@ import { styles } from "./TextLink";
*/
function CustomLink({ classes, to, children }) {
return (
<Link to={to} className={classes.link} style={{ textDecoration: "none" }}>
<Link to={to} className={classes.link}>
{children}
</Link>
);
......
......@@ -15,8 +15,10 @@ const useStyles = makeStyles(theme => ({
*/
function CustomNavLink(props) {
const { to, children } = props;
const classes = useStyles();
return (
<NavLink to={to} className={useStyles().link}>
<NavLink to={to} className={classes.link}>
{children}
</NavLink>
);
......
import React from "react";
import PropTypes from "prop-types";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
import TextLink from "./TextLink";
import APP_ROUTES from "../../config/appRoutes";
import getWindowBase from "../../utils/getWindowBase";
const useStyles = makeStyles(theme => ({
spacer: {
marginBottom: theme.spacing(2)
}
}));
/**
* Component to render the links in custom manner
*
*/
function LicenseNotice(props) {
const classes = useStyles();
return (
<>
<Typography variant="caption" display="block">
......@@ -17,6 +25,7 @@ function LicenseNotice(props) {
{props.variant}. Plus d'informations à ce propos sont disponibles&nbsp;
<TextLink href={getWindowBase() + APP_ROUTES.aboutCgu}>ici</TextLink>.
</Typography>
<div className={classes.spacer} />
</>
);
}
......
......@@ -137,7 +137,7 @@ function MetricFeedback(props) {
return (
<>
<div className={classes.barContainer}>
{showBarIcons ? <LeftBarIcon color="disabled" /> : <></>}
{showBarIcons && <LeftBarIcon color="disabled" />}
<div className={classes.barContent}>
<div className={colorBarClasses} style={{ width }}>
<div
......@@ -151,7 +151,7 @@ function MetricFeedback(props) {
</div>
</div>
</div>
{showBarIcons ? <RightBarIcon color="disabled" /> : <></>}
{showBarIcons && <RightBarIcon color="disabled" />}
</div>
</>
);
......
......@@ -90,9 +90,9 @@ function PaginatedData(props) {
return (
<div>
{stepperOnTop ? renderStepper() : <></>}
{stepperOnTop && renderStepper()}
<div>{content.map(dataEl => render(dataEl))}</div>
{stepperOnBottom ? renderStepper() : <></>}
{stepperOnBottom && renderStepper()}
</div>
);
}
......
import React from "react";
import withStyles from "@material-ui/core/styles/withStyles";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
export function getLinkColor(theme) {
return theme.palette.type === "dark"
......@@ -11,31 +11,33 @@ export function getLinkColor(theme) {
export const styles = theme => ({
link: {
color: theme.palette.text.primary,
textDecoration: "none",
boxShadow: `0px 0.08em 0.01em -0.01em ${getLinkColor(
theme
)}, inset 0px -0.1em 0.03em -0.03em ${getLinkColor(theme)}`,
borderTopRightRadius: "100%",
borderTopLeftRadius: "100%",
borderBottomLeftRadius: "10%",
borderBottomRightRadius: "30%"
textDecoration: `underline ${getLinkColor(theme)}`
// boxShadow: `0px 0.08em 0.01em -0.01em ${getLinkColor(
// theme
// )}, inset 0px -0.1em 0.03em -0.03em ${getLinkColor(theme)}`,
// borderTopRightRadius: "100%",
// borderTopLeftRadius: "100%",
// borderBottomLeftRadius: "10%",
// borderBottomRightRadius: "30%"
}
});
const useStyles = makeStyles(styles);
/**
* Component to render the links in custom manner
*
*/
function TextLink(params) {
const { classes, ...props } = params;
function TextLink({ href, children }) {
const classes = useStyles();
return (
<a
href={props.href}
href={href}
className={classes.link}
target="_blank"
rel="noopener noreferrer"
>
{props.children}
{children}
</a>
);
}
......@@ -45,4 +47,4 @@ TextLink.propTypes = {
children: PropTypes.node.isRequired
};
export default withStyles(styles, { withTheme: true })(TextLink);
export default TextLink;
......@@ -172,9 +172,6 @@ function getRenderers(headingOffset) {
/**
* Custom Markdown component renderer to make use of material UI
*
* @class BaseMarkdown
* @extends {Component}
*/
function BaseMarkdown({ compileSource, source, headingOffset }) {
const compiledSource = compileSource(source);
......
......@@ -39,8 +39,6 @@ function compileSource(source) {
*
* We don't need to fetch them here.
*
* @class Markdown
* @extends {Component}
* @return {string}
*/
export default React.memo(props => (
......
......@@ -20,7 +20,7 @@ function TruncatedMarkdown(props) {
<Collapse in={truncated} collapsedHeight="4rem">
<Markdown source={sourceAsString} />
</Collapse>
{!truncated ? <div className={classes.gradientBorder} /> : <></>}
{!truncated && <div className={classes.gradientBorder} />}
<Button
variant="contained"
className={classes.moreButton}
......
/* eslint-disable react/sort-comp */
import React from "react";
import PropTypes from "prop-types";
import Divider from "@material-ui/core/Divider";
import Button from "@material-ui/core/Button";
import FormManager from "../../utils/editionRelated/FormManager";
import FullScreenDialogService from "../../services/FullScreenDialogService";
......@@ -53,7 +52,7 @@ class Editor extends React.Component {
*/
parseRawModelData() {
const out = {};
this.formManager.getFields().forEach(({ fieldMapping }) => {
this.formManager.getFieldsMapping().forEach(fieldMapping => {
out[fieldMapping] = this.props.rawModelData[fieldMapping];
});
......@@ -182,10 +181,7 @@ class Editor extends React.Component {
</Button>
}
>
<div>
<LicenseNotice variant={license} />
<Divider />
</div>
<LicenseNotice variant={license} />
<FormContext.Provider value={{ formManager: this.formManager }}>
<Form />
</FormContext.Provider>
......
import React from "react";
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import Switch from "@material-ui/core/Switch";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Field from "./Field";
import CustomError from "../../common/CustomError";
import useField from "../../../hooks/useField";
import FieldWrapper from "./FieldWrapper";
/**
* Form field for a simple boolean switch field
*
*
* @class BooleanField
* @extends {Field}
*/
class BooleanField extends Field {
/**
* @override
* @returns
*/
getError() {
// There are no possible errors, but we need to redefine this function
return new CustomError([]);
}
handleChangeValue(value) {
this.setState({ value });
}
/**
* @override
* @returns
*/
renderField() {
const checked = this.state.value;
const { labelIfTrue, labelIfFalse } = this.props;
return (
function BooleanField({
fieldMapping,
required,
label,
comment,
labelIfTrue,
labelIfFalse
}) {
const [checked, setCheckedInt, error] = useField(fieldMapping);
const setChecked = useCallback(e => {
setCheckedInt(e.target.checked);
}, []);
return (
<FieldWrapper
required={required}
errorObj={error}
label={label}
commentText={comment}
>
<FormControlLabel
control={
<Switch
checked={checked}
onChange={event => this.handleChangeValue(event.target.checked)}
color="primary"
/>
<Switch checked={checked} onChange={setChecked} color="primary" />
}
label={checked ? labelIfTrue : labelIfFalse}
/>
);
}
</FieldWrapper>
);
}
BooleanField.defaultProps = {
...Field.defaultProps,
value: false,
labelIfTrue: "",
labelIfFalse: ""
};
BooleanField.propTypes = {
...Field.propTypes,
value: PropTypes.bool.isRequired,
labelIfTrue: PropTypes.string,
labelIfFalse: PropTypes.string
};
......
import React from "react";
import PropTypes from "prop-types";
import React, { useCallback } from "react";
import { DatePicker, MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
import frLocale from "date-fns/locale/fr";
import format from "date-fns/format";
import KeyboardArrowLeftIcon from "@material-ui/icons/KeyboardArrowLeft";
import KeyboardArrowRightIcon from "@material-ui/icons/KeyboardArrowRight";
import dateToDateStr from "../../../utils/dateToDateStr";
import Field from "./Field";
import CustomError from "../../common/CustomError";
import useField from "../../../hooks/useField";
import FieldWrapper from "./FieldWrapper";
/**
* Class to customize the header of the date selection box
*
* @class LocalizedUtils
* @extends {DateFnsUtils}
*/
class LocalizedUtils extends DateFnsUtils {
getDatePickerHeaderText(date) {
......@@ -28,76 +24,50 @@ class LocalizedUtils extends DateFnsUtils {
/**
* Form field for dates
*
* @class DateField
* @extends {Field}
*/
class DateField extends Field {
/**
* @override
* @returns
* @memberof DateField
*/
serializeFromField() {
return dateToDateStr(this.state.value);
}
/**
* One error detection for this field
* @override
* @returns {CustomError}
* @memberof MarkdownField
*/
getError() {
const date = this.state.value;
function DateField({ fieldMapping, required, label, comment }) {
const getError = useCallback(v => {
const messages = [];
if (this.props.required && !date) {
messages.push("Date requise !");
if (required && !v) {
messages.push("Date requise.");
}
return new CustomError(messages);
}
/**
* @param {Date} date
* @memberof DateField
*/
handleDateChange(date) {
this.setState({
value: date
});
}
});
const [date, setDateInt, error] = useField(fieldMapping, {
serializeFromField: dateToDateStr,
getError
});
/**
* @override
* @memberof DateField
*/
renderField() {
return (
return (
<FieldWrapper
required={required}
errorObj={error}
label={label}
commentText={comment}
>
<MuiPickersUtilsProvider utils={LocalizedUtils} locale={frLocale}>
<DatePicker
clearable
format="d MMM yyyy"
value={this.state.value}
onChange={d => this.handleDateChange(d)}
value={date}
onChange={setDateInt}
clearLabel="vider"
cancelLabel="annuler"
leftArrowIcon={<KeyboardArrowLeftIcon />}
rightArrowIcon={<KeyboardArrowRightIcon />}
/>
</MuiPickersUtilsProvider>
);
}
</FieldWrapper>
);
}
DateField.defaultProps = {
...Field.defaultProps,
value: null
...Field.defaultProps
};
DateField.propTypes = {
...Field.propTypes,
value: PropTypes.instanceOf(Date)
...Field.propTypes
};
export default DateField;
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import FieldWrapper from "./FieldWrapper";
// eslint-disable-next-line no-unused-vars
import CustomError from "../../common/CustomError";
import FormContext from "../../../contexts/FormContext";
/**
* Class that handle fields logic
*
* Hard to hookify as there are many interactions and the need to use a FieldWrapper.
*
* @abstract
* @class Field
* @extends {PureComponent}
*/
class Field extends PureComponent {
// eslint-disable-next-line react/static-property-placement
static contextType = FormContext;
// Attribute that will be used to replace null value
defaultNullValue = undefined;
constructor(props, context) {
super(props, context);
// make sure to subscribe to the _form ! IMPORTANT
this.context.formManager.fieldSubscribe(props.fieldMapping, this);
this.state = {
value: this.context.formManager.getInitialValueForMapping(
props.fieldMapping
)
};
}
/**
* Sets the new state. Before it's done it replaces the value by null if appropriate.
*
* @override
* @param {object} newState
*/
setState(newState) {
const state = { ...newState };
if (typeof this.defaultNullValue !== "undefined") {
Object.assign(state, {
value: newState.value === this.defaultNullValue ? null : newState.value
});
}
const { fieldMapping } = this.props;
super.setState(newState, () => {
this.context.formManager.fieldUpdated(fieldMapping);
});
}
/**
* What is error state of the field.
*
* @virtual
* @returns {CustomError}
*/
getError() {
throw Error("This methods has to be override in sub classes");
}
/**
* YOU SHOULDN'T OVERRIDE THIS
*
* Return the errors from the field and the errors from the _form corresponding to the field.
* @returns {CustomError}
*/
getAllErrors() {
const { fieldMapping } = this.props;
return this.getError().combine(
this.context.formManager.getErrorForField(fieldMapping)
);
}
/**
* Returns the serialized value of the field
*
* @returns
*/
getValue() {
return this.serializeFromField();
}
/**
* function to get serialize the value from the field, to get it ready to send to server
* You might need to override this for weird formats such as dates.
*
* @returns {string|object}
*/
serializeFromField() {
const { value } = this.state;
return value;
}
/**
* Function that should render the field itself
*
* MUST BE OVERRIDEN
* @returns "*"
* @virtual
*/
renderField() {
throw new Error("This method should be override in subclass of Field");
}
/**
* Default react render function
*
* @returns
*/
render() {
return (
<FieldWrapper
required={this.props.required}
errorObj={this.getAllErrors()}
label={this.props.label}
commentText={this.props.comment}
>
{this.renderField()}
</FieldWrapper>
);
}
}
Field.defaultProps = {
required: false,
label: "mon label",
comment: ""
};
const Field = {};
Field.propTypes = {
required: PropTypes.bool, // is the field required ?
......@@ -139,4 +9,10 @@ Field.propTypes = {
fieldMapping: PropTypes.string.isRequired // name of the field in the data
};
Field.defaultProps = {
required: false,
label: "mon label",
comment: ""
};
export default Field;
import React, { PureComponent } from "react";
import React from "react";
import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles";
import compose from "recompose/compose";
import FormControl from "@material-ui/core/FormControl";
import FormLabel from "@material-ui/core/FormLabel";
import FormHelperText from "@material-ui/core/FormHelperText";
import { makeStyles } from "@material-ui/styles";
import CustomError from "../../common/CustomError";
const useStyles = makeStyles(theme => ({
formElement: {
marginBottom: theme.spacing(3)
}
}));
/**
* Wrapper for fields in forms to handle labels and visual feedback
*
* @class FieldWrapper
* @extends {PureComponent}
*/
class FieldWrapper extends PureComponent {
render() {
const {
classes,
required,
errorObj,
label,
children,
commentText
} = this.props;
return (
<FormControl
className={classes.formElement}
required={required}
error={errorObj.status}
fullWidth
>
<FormLabel>{label}</FormLabel>
{commentText ? <FormHelperText>{commentText}</FormHelperText> : <></>}
{errorObj.messages.map((error, idx) => (