Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Rex Dri
Rex Dri
Commits
57ce7f17
Commit
57ce7f17
authored
Sep 08, 2019
by
Florent Chehab
Committed by
Florent Chehab
Jan 12, 2020
Browse files
refacto(front): more switch from custom component to hook & tweaks & bug fix
parent
6b97161d
Changes
26
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/app/App.jsx
View file @
57ce7f17
...
...
@@ -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";
...
...
frontend/src/components/filter/Filter.jsx
View file @
57ce7f17
/* 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/getAction
s
"
;
import
{
makeStyles
}
from
"
@material-ui/style
s
"
;
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)
"