Commit 57ce7f17 authored by Florent Chehab's avatar Florent Chehab Committed by Florent Chehab
Browse files

refacto(front): more switch from custom component to hook & tweaks & bug fix

parent 6b97161d
......@@ -34,13 +34,15 @@ import CityService from "../../services/data/CityService";
import CountryService from "../../services/data/CountryService";
import CurrencyService from "../../services/data/CurrencyService";
import LanguageService from "../../services/data/LanguageService";
import FilterService from "../../services/FilterService";
const SERVICES_TO_INITIALIZE = [
UniversityService,
CityService,
CountryService,
CurrencyService,
LanguageService
LanguageService,
FilterService
];
// import PageFiles from "../pages/PageFiles";
......
/* eslint-disable indent */
import React from "react";
import PropTypes from "prop-types";
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import ExpansionPanel from "@material-ui/core/ExpansionPanel";
import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import Typography from "@material-ui/core/Typography";
import withStyles from "@material-ui/core/styles/withStyles";
import InfoIcon from "@material-ui/icons/InfoOutlined";
import { compose } from "recompose";
import uuid from "uuid/v4";
import getActions from "../../redux/api/getActions";
import { makeStyles } from "@material-ui/styles";
import { saveSelectedUniversities } from "../../redux/actions/filter";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import DownshiftMultiple from "../common/DownshiftMultiple";
import { getMostNRecentSemesters } from "../../utils/compareSemesters";
import UniversityService from "../../services/data/UniversityService";
// TODO bug when directly loading map
/**
* Class that handle all the filter manipulation with caching
*/
class FilterHandler {
static _universitiesCountriesCache = undefined;
static _countriesWereThereAreUniversities = undefined;
constructor(data) {
this.countries = data.countries;
this.cities = data.cities;
this.mainCampuses = data.mainCampuses;
this.universities = data.universities;
}
get universityIdsCountries() {
if (typeof FilterHandler._universitiesCountriesCache === "undefined") {
const { mainCampuses, cities, countries } = this;
const citiesMap = new Map();
const countriesMap = new Map();
cities.forEach(el => citiesMap.set(el.id, el));
countries.forEach(el => countriesMap.set(el.id, el));
const res = new Map();
mainCampuses.forEach(el => {
const cityId = el.city;
const city = citiesMap.get(cityId);
const countryId = city.country;
const country = countriesMap.get(countryId);
res.set(el.university, country);
});
FilterHandler._universitiesCountriesCache = res;
}
return FilterHandler._universitiesCountriesCache;
}
/**
* Function to get the list of countries where there are universities.
*
* @returns {Array} of the countries instances
*/
get countriesWhereThereAreUniversities() {
if (
typeof FilterHandler._countriesWereThereAreUniversities === "undefined"
) {
const out = new Map();
this.universityIdsCountries.forEach(country =>
out.set(country.id, country)
);
FilterHandler._countriesWereThereAreUniversities = [...out.values()];
}
return FilterHandler._countriesWereThereAreUniversities;
}
static _countriesOptions = undefined;
get countriesOptions() {
if (typeof FilterHandler._countriesOptions === "undefined") {
FilterHandler._countriesOptions = this.countriesWhereThereAreUniversities.map(
c => ({
value: c.iso_alpha2_code,
label: c.name
})
);
}
return FilterHandler._countriesOptions;
}
static _majorMinorOptions = undefined;
/**
* @return {array.<object>}
*/
get majorMinorOptions() {
if (typeof FilterHandler._majorMinorOptions === "undefined") {
const out = new Set(
this.universities.flatMap(u => this.getMajorMinorsInUniv(u))
);
FilterHandler._majorMinorOptions = [...out].map(el => ({
value: el,
label: el
}));
}
return FilterHandler._majorMinorOptions;
}
static _semesterOptions = undefined;
/**
* @return {array.<object>}
*/
get semesterOptions() {
if (typeof FilterHandler._semesterOptions === "undefined") {
const out = new Set(
this.universities.flatMap(u => this.getSemestersInUniv(u))
);
FilterHandler._semesterOptions = [...out].map(semester => ({
value: semester,
label: semester
}));
}
return FilterHandler._semesterOptions;
}
static _defaultSemesters = undefined;
get defaultSemesters() {
if (typeof FilterHandler._defaultSemesters === "undefined") {
FilterHandler._defaultSemesters = getMostNRecentSemesters(
this.semesterOptions.map(el => el.value),
4
);
}
return FilterHandler._defaultSemesters;
}
getMajorInUniv(univObj) {
return [
...new Set(
Object.values(univObj.denormalized_infos).flatMap(forSemester =>
Object.keys(forSemester)
)
)
];
}
/**
* @param univObj
* @param allowedSemesters
* @return {array.<string>}
*/
getMajorMinorsInUniv(univObj, allowedSemesters = null) {
const realMinors = Object.entries(univObj.denormalized_infos)
.filter(([sem]) =>
allowedSemesters === null ? true : allowedSemesters.includes(sem)
)
.map(([, forSemester]) => forSemester)
.flatMap(forSemester => Object.entries(forSemester))
.flatMap(([major, minors]) => minors.map(minor => `${major}${minor}`));
const extraMinors = this.getMajorInUniv(univObj).map(
major => `${major} — Toutes filières confondues`
);
return [...new Set(realMinors), ...extraMinors];
}
getSemestersInUniv(univObj) {
return Object.keys(univObj.denormalized_infos);
}
/**
*
* @param {array.<string>} countryCodes
* @return {array.<object>}
*/
getUniversitiesInCountries(countryCodes) {
const possiblesCountries = new Set(countryCodes);
const out = [];
this.universityIdsCountries.forEach((country, univId) => {
if (possiblesCountries.has(country.id)) out.push(univId);
});
return out;
}
/**
*
* @param {array.<string>} selectedCountriesCode
* @param {array.<string>} selectedSemesters
* @param {array.<string>} selectedMajorMinors
* @return {array.<number>}
*/
getSelection(
selectedCountriesCode = [],
selectedSemesters = [],
selectedMajorMinors = []
) {
let possible =
selectedCountriesCode.length === 0
? this.universities
: this.getUniversitiesInCountries(selectedCountriesCode).map(id =>
UniversityService.getUniversityById(id)
);
if (selectedSemesters.length > 0) {
const possibleSemesters = new Set(selectedSemesters);
possible = possible.filter(univ => {
const semestersInUniv = this.getSemestersInUniv(univ);
return semestersInUniv.some(semester =>
possibleSemesters.has(semester)
);
});
}
if (selectedMajorMinors.length > 0) {
const possibleMajorMinors = new Set(selectedMajorMinors);
possible = possible.filter(univ => {
const majorMinorsInUniv = new Set(this.getMajorMinorsInUniv(univ));
return [...majorMinorsInUniv].some(el => possibleMajorMinors.has(el));
});
}
if (selectedSemesters.length > 0 && selectedMajorMinors.length > 0) {
const possibleMajorMinors = new Set(selectedMajorMinors);
const possibleSemesters = new Set(selectedSemesters);
possible = possible.filter(univ => {
const semestersInUniv = this.getSemestersInUniv(univ);
const possibleSemesterInUniv = semestersInUniv.filter(sem =>
possibleSemesters.has(sem)
);
return this.getMajorMinorsInUniv(univ, possibleSemesterInUniv).some(
el => possibleMajorMinors.has(el)
);
});
}
return possible.map(univ => univ.id);
}
}
/**
* Implementation of a filter component
*
* @class Filter
* @extends {CustomComponentForAPI}
* @extends React.Component
*/
class Filter extends CustomComponentForAPI {
/**
* Static variables to share behaviors between instances
*/
static DOWNSHIFT_COUNTRIES_ID = uuid();
static DOWNSHIFT_SEMESTERS_ID = uuid();
static DOWNSHIFT_MAJORS_ID = uuid();
static isOpened = false;
static hasSelection = false;
static nbSelection = 0;
static hasBeenChanged = false;
static univHandler = undefined;
static values = {
countries: [],
semesters: [],
majorMinors: []
};
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
if (Filter.univHandler) {
// make sure it has been initialized elsewhere first
const mostRecentSemesters = this.univHandler.defaultSemesters;
// Set the default value for the filter
if (!Filter.hasBeenChanged)
this.updateSelectedUniversities("semesters", mostRecentSemesters);
}
}
componentWillUnmount() {
Filter.univHandler = undefined;
}
/**
* @return {FilterHandler}
*/
get univHandler() {
if (typeof Filter.univHandler === "undefined") {
Filter.univHandler = new FilterHandler(
this.getLatestReadDataFor([
"universities",
"countries",
"cities",
"mainCampuses"
])
);
}
return Filter.univHandler;
}
getEndMessage() {
if (!Filter.hasSelection) return "(Aucun filtre est actif)";
const base = "(Un filtre est actif — ";
if (Filter.nbSelection === 0)
return `${base}aucune université ne correspond)`;
if (Filter.nbSelection === 1) return `${base}1 université correspond)`;
return `${base}${Filter.nbSelection} universités correspondent)`;
}
updateSelectedUniversities(key, selection) {
Filter.values[key] = selection;
const { values } = Filter;
const selectedUniversities = this.univHandler.getSelection(
values.countries,
values.semesters,
values.majorMinors
);
import usePersistentState from "../../hooks/usePersistentState";
import FilterService from "../../services/FilterService";
import FilterStatus from "./FilterStatus";
Filter.hasSelection = Object.values(Filter.values).some(
arr => arr.length !== 0
);
Filter.nbSelection = selectedUniversities.length;
this.props.saveSelection(Filter.hasSelection ? selectedUniversities : null);
Filter.hasBeenChanged = true;
this.forceUpdate();
}
customRender() {
const { countriesOptions } = this.univHandler;
const { majorMinorOptions } = this.univHandler;
const semestersOptions = this.univHandler.semesterOptions;
const mostRecentSemesters = this.univHandler.defaultSemesters;
const { classes } = this.props;
return (
<ExpansionPanel
expanded={Filter.isOpened}
onChange={() => {
Filter.isOpened = !Filter.isOpened;
this.forceUpdate();
}}
>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography className={classes.heading}>
Appliquer des filtres
</Typography>
<div className={classes.infoFilter}>
<InfoIcon color={Filter.hasSelection ? "primary" : "disabled"} />
<Typography
className={classes.caption}
color={Filter.hasSelection ? "primary" : "textSecondary"}
>
<em>{this.getEndMessage()}</em>
</Typography>
</div>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={{ display: "block" }}>
<Typography variant="caption">
Le options internes des filtres sont composées avec un « ou »
logique. Les filtres sont composés entre eux avec un « et » logique.
</Typography>
<div className={classes.spacer1} />
<div className={classes.input}>
<DownshiftMultiple
fieldPlaceholder="Filter par pays"
options={countriesOptions}
onChange={selection =>
this.updateSelectedUniversities("countries", selection)
}
cacheId={Filter.DOWNSHIFT_COUNTRIES_ID}
/>
</div>
<div className={classes.spacer2} />
<Typography>Filtres avancés</Typography>
<Typography variant="caption">
REX-DRI s'efforce d'être à jour avec l'ENT. Toutefois, seul l'ENT
fait foi à 100% concernant les possibilités passées et actuelles
d'échanges.
</Typography>
<div className={classes.input}>
<DownshiftMultiple
fieldPlaceholder={
"Filter par semestre (lorsqu'un échange est/était possible)"
}
options={semestersOptions}
onChange={selection =>
this.updateSelectedUniversities("semesters", selection)
}
cacheId={Filter.DOWNSHIFT_SEMESTERS_ID}
value={[...mostRecentSemesters].reverse()}
/>
</div>
<div className={classes.input}>
<DownshiftMultiple
fieldPlaceholder={
"Filter par branches et filières (lorsqu'un échange est/était possible)"
}
options={majorMinorOptions}
onChange={selection =>
this.updateSelectedUniversities("majorMinors", selection)
}
cacheId={Filter.DOWNSHIFT_MAJORS_ID}
/>
<Typography variant="caption">
Attention, en filtrant par filière, seuls les échanges déjà
effectués sont pris en compte.
</Typography>
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
);
}
}
Filter.propTypes = {
classes: PropTypes.object.isRequired,
saveSelection: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
universities: state.api.universitiesAll,
mainCampuses: state.api.mainCampusesAll,
cities: state.api.citiesAll,
countries: state.api.countriesAll
});
const mapDispatchToProps = dispatch => ({
api: {
universities: () => dispatch(getActions("universities").readAll()),
mainCampuses: () => dispatch(getActions("mainCampuses").readAll()),
cities: () => dispatch(getActions("cities").readAll()),
countries: () => dispatch(getActions("countries").readAll())
},
saveSelection: selectedUniversities =>
dispatch(saveSelectedUniversities(selectedUniversities))
});
const styles = theme => ({
const useStyles = makeStyles(theme => ({
root: {
width: "100%"
},
......@@ -464,10 +22,6 @@ const styles = theme => ({
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightMedium
},
infoFilter: {
marginLeft: theme.spacing(2),
display: "inherit"
},
input: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(0.5)
......@@ -478,12 +32,114 @@ const styles = theme => ({
spacer2: {
marginTop: theme.spacing(2)
}
});
}));
const DOWNSHIFT_COUNTRIES_ID = uuid();
const DOWNSHIFT_SEMESTERS_ID = uuid();
const DOWNSHIFT_MAJORS_ID = uuid();
/**
* Implementation of a filter component
*/
function Filter() {
const classes = useStyles();
const dispatch = useDispatch();
const [isOpened, setIsOpened] = usePersistentState("filter-open", false);
const [countries, setCountries] = usePersistentState("filter-countries", []);
const [semesters, setSemesters] = usePersistentState(
"filter-semesters",
[...FilterService.defaultSemesters].reverse()
);
const [majorMinors, setMajorMinors] = usePersistentState(
"filter-major-minors",
[]
);
useEffect(() => {
const selectedUniversities = FilterService.getSelection(
countries,
semesters,
majorMinors
);
const hasSelection = [countries, semesters, majorMinors].some(
arr => arr.length !== 0
);
dispatch(
saveSelectedUniversities(hasSelection ? selectedUniversities : null)
);
}, [countries, semesters, majorMinors]);
const {
countriesOptions,
majorMinorOptions,
semesterOptions
} = FilterService;
return (
<ExpansionPanel
expanded={isOpened}
onChange={() => {
setIsOpened(!isOpened);
}}
>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography className={classes.heading}>
Appliquer des filtres
</Typography>
<FilterStatus />
</ExpansionPanelSummary>
<ExpansionPanelDetails style={{ display: "block" }}>
<Typography variant="caption">
Le options internes des filtres sont composées avec un « ou » logique.
Les filtres sont composés entre eux avec un « et » logique.
</Typography>
<div className={classes.spacer1} />
<div className={classes.input}>
<DownshiftMultiple
fieldPlaceholder="Filter par pays"
options={countriesOptions}
onChange={setCountries}
cacheId={DOWNSHIFT_COUNTRIES_ID}
/>
</div>
<div className={classes.spacer2} />
<Typography>Filtres avancés</Typography>
<Typography variant="caption">
REX-DRI s'efforce d'être à jour avec l'ENT. Toutefois, seul l'ENT fait
foi à 100% concernant les possibilités passées et actuelles
d'échanges.
</Typography>
<div className={classes.input}>
<DownshiftMultiple
fieldPlaceholder={
"Filter par semestre (lorsqu'un échange est/était possible)"
}
options={semesterOptions}
onChange={setSemesters}
cacheId={DOWNSHIFT_SEMESTERS_ID}
value={semesters}
/>
</div>
<div className={classes.input}>
<DownshiftMultiple
fieldPlaceholder={
"Filter par branches et filières (lorsqu'un échange est/était possible)"