Commit a875ca41 authored by Florent Chehab's avatar Florent Chehab Committed by Florent Chehab
Browse files

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 0223fb20
......@@ -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>
);
......
// Inspired by : https://material-ui.com/demos/autocomplete/
import React from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import PropTypes from "prop-types";
import keycode from "keycode";
import Downshift from "downshift";
import withStyles from "@material-ui/core/styles/withStyles";
import TextField from "@material-ui/core/TextField";
import Paper from "@material-ui/core/Paper";
import MenuItem from "@material-ui/core/MenuItem";
import Chip from "@material-ui/core/Chip";
import fuzzysort from "fuzzysort";
import { makeStyles } from "@material-ui/styles";
import usePersistentState from "../../hooks/usePersistentState";
function renderInput(inputProps, autoFocus) {
const { InputProps, classes, ref, ...other } = inputProps;
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1
},
container: {
flexGrow: 1,
position: "relative"
},
paper: {
position: "absolute",
zIndex: 200000,
marginTop: theme.spacing(1),
left: 0,
right: 0
},
chip: {
margin: theme.spacing(0.5, 0.25)
},
inputRoot: {
flexWrap: "wrap"
},
notSelectedSuggestion: {
fontWeight: 400
},
selectedSuggestion: {
fontWeight: 500
}
}));
function Input({ inputProps, autoFocus }) {
const classes = useStyles();
const { InputProps, ref, ...other } = inputProps;
return (
<TextField
......@@ -27,236 +59,234 @@ function renderInput(inputProps, autoFocus) {
);
}
function renderSuggestion({
Input.propTypes = {
inputProps: PropTypes.object.isRequired,
autoFocus: PropTypes.bool.isRequired
};
function Suggestion({
suggestion,
index,
itemProps,
highlightedIndex,
selectedItem
}) {
const classes = useStyles();
const isHighlighted = highlightedIndex === index;
const isSelected = (selectedItem || "").indexOf(suggestion) > -1;
return (
<MenuItem
{...itemProps}
key={suggestion.value}
selected={isHighlighted}
component="div"
style={{
fontWeight: isSelected ? 500 : 400
}}
className={
isSelected ? classes.selectedSuggestion : classes.notSelectedSuggestion
}
>
{suggestion.label}
</MenuItem>
);
}
renderSuggestion.propTypes = {
highlightedIndex: PropTypes.number.isRequired,
Suggestion.propTypes = {
highlightedIndex: PropTypes.number,
index: PropTypes.number.isRequired,
itemProps: PropTypes.object.isRequired,
selectedItem: PropTypes.string.isRequired,
selectedItem: PropTypes.string,
suggestion: PropTypes.shape({
label: PropTypes.string,
value: PropTypes.number
value: PropTypes.string
}).isRequired
};
class DownshiftMultiple extends React.Component {
static CACHE = {};
// options cannot be changed once the component in mounted
reverseOptions = new Map();
// holds the mapping from value to label of all options
constructor(props) {
super(props);
const { value, inputValue } = props;
this.state = {
value: [...value],
inputValue,
options: this.getFinalOptions()
};
this.reverseOptions = new Map();
}
Suggestion.defaultProps = {
highlightedIndex: null,
selectedItem: null
};
componentDidMount() {
const { options } = this.props;
this.reverseOptions = new Map();
options.forEach(el => this.reverseOptions.set(el.value, el.label));
function DownshiftMultiple({
cacheId,
value: valueFromProps,
inputValue: inputValueFromProps,
options: optionsFromProps,
onChange: onChangeFromProps,
multiple,
fieldLabel,
fieldPlaceholder,
autoFocusInput
}) {
const reverseOptions = useMemo(() => {
const out = new Map();
optionsFromProps.forEach(el => out.set(el.value, el.label));
return out;
}, [optionsFromProps]);
const proposal = {
value: this.props.value,
inputValue: this.props.inputValue
};
const { cacheId } = this.props;
if (cacheId !== null && DownshiftMultiple.CACHE[cacheId]) {
Object.assign(proposal, DownshiftMultiple.CACHE[cacheId]);
}
const [cache, setCache] = usePersistentState(`app-downshift-${cacheId}`, {
value: valueFromProps,
inputValue: inputValueFromProps
});
this.setState({
value: [...proposal.value],
inputValue: proposal.inputValue,
options: this.getFinalOptions()
});
}
const [value, setValue] = useState(
cacheId === null ? [...valueFromProps] : [...cache.value]
);
const [inputValue, setInputValue] = useState(
cacheId === null ? inputValueFromProps : cache.inputValue
);
componentWillUnmount() {
const { cacheId } = this.props;
if (cacheId !== null) {
const { value, inputValue } = this.state;
DownshiftMultiple.CACHE[cacheId] = {
value,
inputValue
};
}
}
// saving also to cache
useEffect(() => {
if (cacheId !== null) setCache({ value, inputValue });
}, [value, inputValue]);
onChange(value) {
if (this.props.multiple) {
this.props.onChange(value);
} else {
// we return only one value or null
this.props.onChange(value.length === 0 ? null : value[0]);
}
}
getFinalOptions() {
return this.props.options.map(({ value, label }) => ({
value,
label,
valueStr: value.toString()
const options = useMemo(() => {
return optionsFromProps.map(el => ({
value: el.value,
label: el.label,
valueStr: el.value.toString()
}));
}
}, [optionsFromProps]);
getSuggestions(inputValue) {
const { value, options } = this.state;
const possible = options.filter(el => !value.includes(el.value));
const onChange = useCallback(
newValue => {
if (multiple) {
onChangeFromProps(newValue);
} else {
// we return only one value or null
onChangeFromProps(newValue.length === 0 ? null : newValue[0]);
}
},
[onChangeFromProps]
);
const filter = fuzzysort.go(inputValue, possible, {
limit: 5,
keys: ["label", "valueStr"]
});
if (filter.length > 0) {
return filter.map(item => item.obj);
}
return possible.slice(0, 4);
}
const getSuggestions = useCallback(
newInputValue => {
// Suggest only unselected options
const possible = options.filter(el => !value.includes(el.value));
handleKeyDown = event => {
const { inputValue, value } = this.state;
if (value.length && !inputValue.length && keycode(event) === "backspace") {
this.setState({
value: value.slice(0, value.length - 1)
const filter = fuzzysort.go(newInputValue, possible, {
limit: 5,
keys: ["label", "valueStr"]
});
}
};
if (filter.length > 0) {
return filter.map(item => item.obj);
}
return possible.slice(0, 4);
},
[value, options]
);
handleInputChange = event => {
this.setState({ inputValue: event.target.value });
};
const handleKeyDown = useCallback(
event => {
if (
value.length &&
!inputValue.length &&
keycode(event) === "backspace"
) {
setValue(v => v.slice(0, v.length - 1));
}
},
[value, inputValue]
);
handleChange = val => {
if (val === null) return; // on reset will be null
const handleInputChange = useCallback(event => {
setInputValue(event.target.value);
}, []);
let { value } = this.state;
if (value.indexOf(val) === -1) {
value = [...value, val];
if (!this.props.multiple) {
value = value.length === 0 ? [] : [value[value.length - 1]];
}
// Tell subscriber
this.onChange(value);
}
const handleChange = useCallback(
val => {
if (val === null) return; // on reset will be null
this.setState({
inputValue: "",
value
});
};
let newValue = value;
if (value.indexOf(val) === -1) {
newValue = [...value, val];
if (!multiple) {
newValue =
newValue.length === 0 ? [] : [newValue[newValue.length - 1]];
}
// Tell subscriber
onChange(newValue);
}
setInputValue("");
setValue(newValue);
},
[value]
);
handleDelete = val => () => {
this.setState(state => {
const value = state.value.filter(v => val !== v);
this.onChange(value);
return { value };
});
};
const handleDelete = useCallback(
val => {
setValue(valueInState => {
const newValue = valueInState.filter(v => val !== v);
onChange(newValue);
return newValue;
});
},
[onChange]
);
render() {
const {
classes,
fieldLabel,
fieldPlaceholder,
autoFocusInput
} = this.props;
const { inputValue, value } = this.state;
const classes = useStyles();
return (
<div className={classes.root}>
<Downshift inputValue={inputValue} onChange={this.handleChange}>
{({
getInputProps,
getItemProps,
isOpen,
inputValue: inputValue2,
selectedItem: selectedItem2,
highlightedIndex,
clearSelection
}) => (
<div className={classes.container}>
{renderInput(
{
fullWidth: true,
classes,
InputProps: getInputProps({
startAdornment: value.map(val => (
<Chip
key={val}
tabIndex={-1}
label={this.reverseOptions.get(val)}
className={classes.chip}
onDelete={() => {
this.handleDelete(val)();
clearSelection();
}}
variant="outlined"
color="primary"
/>
)),
onChange: this.handleInputChange,
onKeyDown: this.handleKeyDown,
placeholder: fieldPlaceholder
}),
label: fieldLabel
},
autoFocusInput
)}
{isOpen ? (
<Paper className={classes.paper} square>
{this.getSuggestions(inputValue2).map((suggestion, index) =>
renderSuggestion({
suggestion,
index,
itemProps: getItemProps({ item: suggestion.value }),
highlightedIndex,
selectedItem: selectedItem2
})
)}
</Paper>
) : null}
</div>
)}
</Downshift>
</div>
);
}
return (
<div className={classes.root}>
<Downshift inputValue={inputValue} onChange={handleChange}>
{({
getInputProps,
getItemProps,
isOpen,
inputValue: inputValue2,
selectedItem: selectedItem2,
highlightedIndex,
clearSelection
}) => (
<div className={classes.container}>
<Input
autoFocus={autoFocusInput}
inputProps={{
fullWidth: true,
InputProps: getInputProps({
startAdornment: value.map(val => (
<Chip
key={val}
tabIndex={-1}
label={reverseOptions.get(val)}
className={classes.chip}
onDelete={() => {
handleDelete(val);
clearSelection();
}}
variant="outlined"
color="primary"
/>
)),
onChange: handleInputChange,
onKeyDown: handleKeyDown,
placeholder: fieldPlaceholder
}),
label: fieldLabel
}}
/>
{isOpen ? (
<Paper className={classes.paper} square>
{getSuggestions(inputValue2).map((suggestion, index) => (
<Suggestion
key={suggestion.value}
highlightedIndex={highlightedIndex}
selectedItem={selectedItem2}
suggestion={suggestion}
index={index}
itemProps={getItemProps({ item: suggestion.value })}
/>
))}
</Paper>
) : null}
</div>
)}
</Downshift>
</div>
);
}
DownshiftMultiple.propTypes = {
classes: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
......@@ -275,7 +305,7 @@ DownshiftMultiple.propTypes = {
})
).isRequired,
multiple: PropTypes.bool, // do we allow multiple values to be selected.
cacheId: PropTypes.string // if not null enable cachig of value and input Value
cacheId: PropTypes.string // if not null enable caching of value and input Value
// If multiple is true than null or one value in returned
};
......@@ -289,27 +319,4 @@ DownshiftMultiple.defaultProps = {
cacheId: null
};
const styles = theme => ({
root: {
flexGrow: 1
},
container: {
flexGrow: 1,
position: "relative"
},
paper: {
position: "absolute",
zIndex: 200000,
marginTop: theme.spacing(1),
left: 0,
right: 0
},
chip: {