Commit d1e7419f authored by Alexandre Lanceart's avatar Alexandre Lanceart Committed by Florent Chehab

feature(filter)

* Filtering universities is now possible based on the country of the university
* Filters are synchronized between the map and the search page

Fixes #13
In progress: #31
parent 1dd31731
Pipeline #39545 passed with stages
in 3 minutes and 32 seconds
/**
*
* WARNING THIS FILE HAS NOT BEEN REVIEWED AS OF 22.02.2019
* THINGS MIGHT NOT BE SUPER CLEAR OR BROKEN
*
*
*
*
*
*
*
*
*
*
*
*
*/
// Inspired by : https://material-ui.com/demos/autocomplete/ // Inspired by : https://material-ui.com/demos/autocomplete/
import React from "react"; import React from "react";
...@@ -30,19 +9,19 @@ import TextField from "@material-ui/core/TextField"; ...@@ -30,19 +9,19 @@ import TextField from "@material-ui/core/TextField";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import MenuItem from "@material-ui/core/MenuItem"; import MenuItem from "@material-ui/core/MenuItem";
import Chip from "@material-ui/core/Chip"; import Chip from "@material-ui/core/Chip";
import __map from "lodash/map";
import __difference from "lodash/difference"; import __difference from "lodash/difference";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import isEqual from "lodash/isEqual";
function renderInput(inputProps) { function renderInput(inputProps) {
const { InputProps, classes, ref, ...other } = inputProps; const {InputProps, classes, ref, ...other} = inputProps;
return ( return (
<TextField <TextField
InputProps={{ InputProps={{
inputRef: ref, inputRef: ref,
classes: { root: classes.inputRoot }, classes: {root: classes.inputRoot},
...InputProps, ...InputProps,
}} }}
{...other} {...other}
...@@ -50,7 +29,7 @@ function renderInput(inputProps) { ...@@ -50,7 +29,7 @@ function renderInput(inputProps) {
); );
} }
function renderSuggestion({ suggestion, index, itemProps, highlightedIndex, selectedItem }) { function renderSuggestion({suggestion, index, itemProps, highlightedIndex, selectedItem}) {
const isHighlighted = highlightedIndex === index; const isHighlighted = highlightedIndex === index;
const isSelected = (selectedItem || "").indexOf(suggestion) > -1; const isSelected = (selectedItem || "").indexOf(suggestion) > -1;
...@@ -68,17 +47,22 @@ function renderSuggestion({ suggestion, index, itemProps, highlightedIndex, sele ...@@ -68,17 +47,22 @@ function renderSuggestion({ suggestion, index, itemProps, highlightedIndex, sele
</MenuItem> </MenuItem>
); );
} }
renderSuggestion.propTypes = { renderSuggestion.propTypes = {
highlightedIndex: PropTypes.number, highlightedIndex: PropTypes.number,
index: PropTypes.number, index: PropTypes.number,
itemProps: PropTypes.object, itemProps: PropTypes.object,
selectedItem: PropTypes.string, selectedItem: PropTypes.string,
suggestion: PropTypes.shape({ label: PropTypes.string, id: PropTypes.number }).isRequired, suggestion: PropTypes.shape({label: PropTypes.string, id: PropTypes.number}).isRequired,
}; };
class DownshiftMultiple extends React.Component { class DownshiftMultiple extends React.Component {
state = this.props.config;
constructor(props) {
super(props);
this.state = Object.assign({}, this.props.config);
}
componentDidMount() { componentDidMount() {
this.setState(this.props.config); this.setState(this.props.config);
...@@ -86,7 +70,11 @@ class DownshiftMultiple extends React.Component { ...@@ -86,7 +70,11 @@ class DownshiftMultiple extends React.Component {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.selectedItems != prevState.selectedItems) { if (!isEqual(prevProps.config.selectedItems, this.props.config.selectedItems)) {
this.setState(this.props.config);
}
if (this.state.selectedItems !== prevState.selectedItems) {
this.props.onChange(this.state.selectedItems); this.props.onChange(this.state.selectedItems);
} }
} }
...@@ -99,19 +87,19 @@ class DownshiftMultiple extends React.Component { ...@@ -99,19 +87,19 @@ class DownshiftMultiple extends React.Component {
getSuggestions(value) { getSuggestions(value) {
const { options } = this.props; const {options} = this.props;
const { selectedItems } = this.state; const {selectedItems} = this.state;
let possible = __difference(options, selectedItems); let possible = __difference(options, selectedItems);
const filter = fuzzysort.go(value, possible, { limit: 5, key: "label" }); const filter = fuzzysort.go(value, possible, {limit: 5, key: "label"});
if (filter.length > 0) { if (filter.length > 0) {
return __map(filter, (item) => item.obj); return filter.map(item => item.obj);
} else { } else {
return possible.slice(0, 4); return possible.slice(0, 4);
} }
} }
handleKeyDown = event => { handleKeyDown = event => {
const { inputValue, selectedItems } = this.state; const {inputValue, selectedItems} = this.state;
if (selectedItems.length && !inputValue.length && keycode(event) === "backspace") { if (selectedItems.length && !inputValue.length && keycode(event) === "backspace") {
this.setState({ this.setState({
selectedItems: selectedItems.slice(0, selectedItems.length - 1), selectedItems: selectedItems.slice(0, selectedItems.length - 1),
...@@ -120,12 +108,12 @@ class DownshiftMultiple extends React.Component { ...@@ -120,12 +108,12 @@ class DownshiftMultiple extends React.Component {
}; };
handleInputChange = event => { handleInputChange = event => {
this.setState({ inputValue: event.target.value }); this.setState({inputValue: event.target.value});
}; };
handleChange = item => { handleChange = item => {
const { options } = this.props; const {options} = this.props;
let { selectedItems } = this.state; let {selectedItems} = this.state;
if (selectedItems.indexOf(item) === -1) { if (selectedItems.indexOf(item) === -1) {
for (let ind in options) { for (let ind in options) {
let el = options[ind]; let el = options[ind];
...@@ -146,30 +134,23 @@ class DownshiftMultiple extends React.Component { ...@@ -146,30 +134,23 @@ class DownshiftMultiple extends React.Component {
this.setState(state => { this.setState(state => {
const selectedItems = [...state.selectedItems]; const selectedItems = [...state.selectedItems];
selectedItems.splice(selectedItems.indexOf(item), 1); selectedItems.splice(selectedItems.indexOf(item), 1);
return { selectedItems }; return {selectedItems};
}); });
}; };
render() { render() {
const { classes, field_label, field_placeholder } = this.props; const {classes, fieldLabel, fieldPlaceholder} = this.props;
const { inputValue, selectedItems } = this.state; const {inputValue, selectedItems} = this.state;
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Downshift <Downshift
id="downshift-multiple" id="downshift-multiple"
inputValue={inputValue} inputValue={inputValue}
onChange={this.handleChange} onChange={this.handleChange}
selectedItem={selectedItems}
> >
{ {
({ ({getInputProps, getItemProps, isOpen, inputValue: inputValue2, selectedItem: selectedItem2, highlightedIndex,}) =>
getInputProps,
getItemProps,
isOpen,
inputValue: inputValue2,
selectedItem: selectedItem2,
highlightedIndex,
}) =>
( (
<div className={classes.container}> <div className={classes.container}>
{renderInput({ {renderInput({
...@@ -189,9 +170,9 @@ class DownshiftMultiple extends React.Component { ...@@ -189,9 +170,9 @@ class DownshiftMultiple extends React.Component {
)), )),
onChange: this.handleInputChange, onChange: this.handleInputChange,
onKeyDown: this.handleKeyDown, onKeyDown: this.handleKeyDown,
placeholder: field_placeholder, placeholder: fieldPlaceholder,
}), }),
label: field_label, label: fieldLabel,
})} })}
{isOpen ? ( {isOpen ? (
<Paper className={classes.paper} square> <Paper className={classes.paper} square>
...@@ -199,7 +180,7 @@ class DownshiftMultiple extends React.Component { ...@@ -199,7 +180,7 @@ class DownshiftMultiple extends React.Component {
renderSuggestion({ renderSuggestion({
suggestion, suggestion,
index, index,
itemProps: getItemProps({ item: suggestion.id }), itemProps: getItemProps({item: suggestion.id}),
highlightedIndex, highlightedIndex,
selectedItem: selectedItem2, selectedItem: selectedItem2,
}), }),
...@@ -221,27 +202,20 @@ DownshiftMultiple.propTypes = { ...@@ -221,27 +202,20 @@ DownshiftMultiple.propTypes = {
onComponentUnmount: PropTypes.func.isRequired, onComponentUnmount: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
config: PropTypes.object.isRequired, config: PropTypes.object.isRequired,
field_label: PropTypes.string.isRequired, fieldLabel: PropTypes.string.isRequired,
field_placeholder: PropTypes.string.isRequired, fieldPlaceholder: PropTypes.string.isRequired,
options: PropTypes.array.isRequired, options: PropTypes.array.isRequired,
}; };
DownshiftMultiple.defaultProps = { DownshiftMultiple.defaultProps = {
options: [ fieldLabel: "",
{ label: "Item 1", id: 1 }, fieldPlaceholder: "Filtrer par Pays",
{ label: "Item 2", id: 2 },
{ label: "Item 3", id: 3 },
{ label: "Item 4", id: 4 },
],
field_label: "label",
field_placeholder: "placeholder",
config: { selectedItems: [], inputValue: "" }
}; };
const styles = theme => ({ const styles = theme => ({
root: { root: {
flexGrow: 1, flexGrow: 1,
height: 250, zIndex: 100000,
}, },
container: { container: {
flexGrow: 1, flexGrow: 1,
......
/**
*
* WARNING THIS FILE HAS NOT BEEN REVIEWED AS OF 22.02.2019
* THINGS MIGHT NOT BE SUPER CLEAR OR BROKEN
*
*
*
*
*
*
*
*
*
*
*
*
*/
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import DownshiftMultiple from "./DownshiftMultiple"; import DownshiftMultiple from "./DownshiftMultiple";
import CustomComponentForAPI from "../common/CustomComponentForAPI"; import CustomComponentForAPI from "../common/CustomComponentForAPI";
import { connect } from "react-redux"; import {connect} from "react-redux";
import __map from "lodash/map"; import {saveFilterConfig, saveSelectedUniversities} from "../../redux/actions/filter";
import __indexOf from "lodash/indexOf";
import { saveSelectedUniversities, saveFilterConfig } from "../../redux/actions/filter";
import ExpansionPanel from "@material-ui/core/ExpansionPanel"; import ExpansionPanel from "@material-ui/core/ExpansionPanel";
import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import InfoIcon from "@material-ui/icons/InfoOutlined";
import getActions from "../../redux/api/getActions"; import getActions from "../../redux/api/getActions";
import {compose} from "recompose";
let isOpen = false;
let hasSelection = false;
/** /**
* Implementation of a filter component * Implementation of a filter component
...@@ -48,8 +28,9 @@ import getActions from "../../redux/api/getActions"; ...@@ -48,8 +28,9 @@ import getActions from "../../redux/api/getActions";
*/ */
class Filter extends CustomComponentForAPI { class Filter extends CustomComponentForAPI {
saveCountriesFilterConfig(state) { saveCountriesFilterConfig(state) {
this.props.saveConfig({ countriesFilter: state }); this.props.saveConfig({countriesFilter: state});
} }
/** /**
...@@ -58,44 +39,66 @@ class Filter extends CustomComponentForAPI { ...@@ -58,44 +39,66 @@ class Filter extends CustomComponentForAPI {
* @returns {Array} of the countries instances * @returns {Array} of the countries instances
* @memberof Filter * @memberof Filter
*/ */
getCountriesWhereThereAreUniversities() { getCountriesWhereThereAreUniversities() {
const mainCampuses = this.getLatestReadData("mainCampuses"); const {mainCampuses, cities, countries} = this.getLatestReadDataFor(["mainCampuses", "cities", "countries"]);
let citiesMap = new Map(),
countriesMap = new Map();
cities.forEach(el => citiesMap.set(el.id, el));
countries.forEach(el => countriesMap.set(el.id, el));
// use of map to get only each country once
let res = new Map(); let res = new Map();
mainCampuses.forEach(campus => { mainCampuses.forEach(el => {
const country = this.joinCampus(campus).country, const cityId = el.city,
code = country.iso_alpha2_code; city = citiesMap.get(cityId),
res.set(code, country); countryId = city.country,
}); country = countriesMap.get(countryId);
res.set(countryId, country);
});
return [...res.values()]; return [...res.values()];
} }
updateSelectedUniversities(selection) { updateSelectedUniversities(selection) {
const mainCampuses = this.getLatestReadData("mainCampuses"), const mainCampuses = this.getLatestReadData("mainCampuses"),
listOfCountries = __map(selection, (s) => s.id); listOfCountries = selection.map(s => s.id);
let selectedUniversities = []; let selectedUniversities = [];
mainCampuses.forEach(campus => { mainCampuses.forEach(campus => {
const campusFull = this.joinCampus(campus); const campusFull = this.joinCampus(campus);
if (__indexOf(listOfCountries, campusFull.country.iso_alpha2_code) > -1) { if (listOfCountries.includes(campusFull.country.iso_alpha2_code)) {
selectedUniversities.push(campusFull.university.id); selectedUniversities.push(campusFull.university.id);
} }
}); });
this.props.saveSelection(selectedUniversities); this.props.saveSelection(selectedUniversities);
hasSelection = selectedUniversities.length !== 0;
this.forceUpdate();
} }
customRender() { customRender() {
const options = __map(this.getCountriesWhereThereAreUniversities(), const options = this.getCountriesWhereThereAreUniversities()
(c) => { return { id: c.iso_alpha2_code, label: c.name }; }); .map(c => ({id: c.iso_alpha2_code, label: c.name}));
const { classes } = this.props; const {classes} = this.props;
return ( return (
<ExpansionPanel> <ExpansionPanel expanded={isOpen} onChange={() => {
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}> isOpen = !isOpen;
this.forceUpdate();
}}>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>}>
<Typography className={classes.heading}>Appliquer des filtres</Typography> <Typography className={classes.heading}>Appliquer des filtres</Typography>
<div className={classes.infoFilter}>
<InfoIcon color={hasSelection ? "primary" : "disabled"}/>
<Typography className={classes.caption} color={hasSelection ? "primary" : "textSecondary"}>
<em>{hasSelection ? "(Un filtre est actif)" : "(Aucun filtre est actif)"}</em>
</Typography>
</div>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails> <ExpansionPanelDetails>
<DownshiftMultiple <DownshiftMultiple
...@@ -114,7 +117,7 @@ Filter.propTypes = { ...@@ -114,7 +117,7 @@ Filter.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
saveConfig: PropTypes.func.isRequired, saveConfig: PropTypes.func.isRequired,
saveSelection: PropTypes.func.isRequired, saveSelection: PropTypes.func.isRequired,
countriesFilterConfig: PropTypes.array countriesFilterConfig: PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
...@@ -123,7 +126,7 @@ const mapStateToProps = (state) => { ...@@ -123,7 +126,7 @@ const mapStateToProps = (state) => {
mainCampuses: state.api.mainCampusesAll, mainCampuses: state.api.mainCampusesAll,
cities: state.api.citiesAll, cities: state.api.citiesAll,
countries: state.api.countriesAll, countries: state.api.countriesAll,
countriesFilterConfig: state.app.filter.countriesFilter countriesFilterConfig: state.app.filter.countriesFilter,
}; };
}; };
...@@ -147,8 +150,15 @@ const styles = theme => ({ ...@@ -147,8 +150,15 @@ const styles = theme => ({
}, },
heading: { heading: {
fontSize: theme.typography.pxToRem(15), fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular, fontWeight: theme.typography.fontWeightMedium,
}, },
infoFilter: {
marginLeft: 2 * theme.spacing.unit,
display: "inherit"
}
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Filter)); export default compose(
connect(mapStateToProps, mapDispatchToProps),
withStyles(styles)
)(Filter);
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { Marker, Popup } from "react-leaflet"; import {Marker, Popup} from "react-leaflet";
import UnivPopupContent from "./UnivPopupContent"; import UnivPopupContent from "./UnivPopupContent";
import getActions from "../../redux/api/getActions"; import getActions from "../../redux/api/getActions";
...@@ -10,7 +10,6 @@ import CustomComponentForAPI from "../common/CustomComponentForAPI"; ...@@ -10,7 +10,6 @@ import CustomComponentForAPI from "../common/CustomComponentForAPI";
import arrayOfInstancesToMap from "../../utils/arrayOfInstancesToMap"; import arrayOfInstancesToMap from "../../utils/arrayOfInstancesToMap";
/** /**
* Class that renders the markers for the map of the universities. * Class that renders the markers for the map of the universities.
* *
...@@ -23,10 +22,12 @@ import arrayOfInstancesToMap from "../../utils/arrayOfInstancesToMap"; ...@@ -23,10 +22,12 @@ import arrayOfInstancesToMap from "../../utils/arrayOfInstancesToMap";
class UnivMarkers extends CustomComponentForAPI { class UnivMarkers extends CustomComponentForAPI {
customRender() { customRender() {
const { universities, const {
universities,
mainCampuses, mainCampuses,
countries, countries,
cities } = this.getAllLatestReadData(); cities
} = this.getAllLatestReadData();
// some conversions for optimization (faster search of element in a map) // some conversions for optimization (faster search of element in a map)
const universitiesMap = arrayOfInstancesToMap(universities), const universitiesMap = arrayOfInstancesToMap(universities),
...@@ -34,23 +35,30 @@ class UnivMarkers extends CustomComponentForAPI { ...@@ -34,23 +35,30 @@ class UnivMarkers extends CustomComponentForAPI {
citiesMap = arrayOfInstancesToMap(cities); citiesMap = arrayOfInstancesToMap(cities);
let mainCampusesSelection = []; let mainCampusesSelection = [];
const listUnivsel = this.props.selectedUniversities;
//console.log(this.props.selectedUniversities);
// Merge the data and add it to the selection // Merge the data and add it to the selection
mainCampuses.forEach(campus => { mainCampuses.forEach(campus => {
const univ = universitiesMap.get(campus.university); const univ = universitiesMap.get(campus.university);
if (campus && univ) { if ((listUnivsel.length > 0 && listUnivsel.indexOf(univ.id) > -1) || listUnivsel.length === 0) {
const city = citiesMap.get(campus.city),
country = countriesMap.get(city.country); if (campus && univ) {
const city = citiesMap.get(campus.city),
mainCampusesSelection.push({ country = countriesMap.get(city.country);
univName: univ.name,
univLogo: univ.logo, mainCampusesSelection.push({
univCity: city.name, univName: univ.name,
univCountry: country.name, univLogo: univ.logo,
lat: campus.lat, univCity: city.name,
lon: campus.lon, univCountry: country.name,
id: univ.id lat: campus.lat,
}); lon: campus.lon,
id: univ.id
});
}
} }
}); });
...@@ -58,7 +66,7 @@ class UnivMarkers extends CustomComponentForAPI { ...@@ -58,7 +66,7 @@ class UnivMarkers extends CustomComponentForAPI {
return ( return (
mainCampusesSelection.map((el, idx) => ( mainCampusesSelection.map((el, idx) => (
<Marker key={idx} position={[el.lat, el.lon]}> <Marker key={idx} position={[el.lat, el.lon]}>
<Popup closeButton={false} > <Popup closeButton={false}>
<UnivPopupContent <UnivPopupContent
name={el.univName} name={el.univName}
logo={el.univLogo} logo={el.univLogo}
...@@ -79,6 +87,7 @@ UnivMarkers.propTypes = { ...@@ -79,6 +87,7 @@ UnivMarkers.propTypes = {
mainCampuses: PropTypes.object.isRequired, mainCampuses: PropTypes.object.isRequired,
cities: PropTypes.object.isRequired, cities: PropTypes.object.isRequired,
countries: PropTypes.object.isRequired, countries: PropTypes.object.isRequired,
selectedUniversities: PropTypes.array.isRequired
}; };
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
...@@ -86,7 +95,8 @@ const mapStateToProps = (state) => { ...@@ -86,7 +95,8 @@ const mapStateToProps = (state) => {
universities: state.api.universitiesAll, universities: state.api.universitiesAll,
mainCampuses: state.api.mainCampusesAll, mainCampuses: state.api.mainCampusesAll,
cities: state.api.citiesAll, cities: state.api.citiesAll,
countries: state.api.countriesAll countries: state.api.countriesAll,
selectedUniversities: state.app.selectedUniversities
}; };
}; };
......
import React, { Component } from "react"; import React, {Component} from "react";
import { connect } from "react-redux"; import {connect} from "react-redux";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Map, TileLayer, LayersControl, LayerGroup } from "react-leaflet"; import {LayerGroup, LayersControl, Map, TileLayer} from "react-leaflet";
import UnivMarkers from "./UnivMakers"; import UnivMarkers from "./UnivMakers";
import { saveMainMapStatus } from "../../redux/actions/map"; import {saveMainMapStatus} from "../../redux/actions/map";
/** /**
...@@ -20,7 +20,7 @@ class UnivMap extends Component { ...@@ -20,7 +20,7 @@ class UnivMap extends Component {
state = { state = {
leafl