Skip to content

Commit

Permalink
[Class statistics] Refactor population search form
Browse files Browse the repository at this point in the history
  • Loading branch information
valtterikantanen committed Oct 28, 2024
1 parent 8337e9a commit d83a362
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 187 deletions.
22 changes: 0 additions & 22 deletions services/frontend/src/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,6 @@ export const getUnifyTextIn = unifyCourses => {
}
}

export const cancelablePromise = promise => {
let hasCanceled = false

// eslint-disable-next-line no-async-promise-executor
const wrappedPromise = new Promise(async (res, rej) => {
try {
await promise
if (hasCanceled) res(false)
res(true)
} catch (error) {
rej(error)
}
})

return {
promise: wrappedPromise,
cancel: () => {
hasCanceled = true
},
}
}

// Gives students course completion date
export const getStudentToTargetCourseDateMap = (students, codes) => {
const codeSet = new Set(codes)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { isEqual, sortBy } from 'lodash'
import moment from 'moment'
import qs from 'query-string'
import { useEffect, useRef, useState } from 'react'
import { useEffect, useState } from 'react'
import Datetime from 'react-datetime'
import { useDispatch, useSelector } from 'react-redux'
import { useHistory, useLocation } from 'react-router-dom'
import { Button, Form, Grid, Icon, Message } from 'semantic-ui-react'
import { Button, Form, Icon, Message } from 'semantic-ui-react'

import { cancelablePromise, createPinnedFirstComparator, isNewStudyProgramme, textAndDescriptionSearch } from '@/common'
import { createPinnedFirstComparator, isNewStudyProgramme, textAndDescriptionSearch } from '@/common'
import { useSearchHistory } from '@/common/hooks'
import { FilterOldProgrammesToggle } from '@/components/common/FilterOldProgrammesToggle'
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
Expand Down Expand Up @@ -35,18 +35,12 @@ export const PopulationSearchForm = ({ onProgress }) => {
const location = useLocation()
const dispatch = useDispatch()
const populations = useSelector(state => state.populations)
const queries = populations.query || {}
const previousQuery = populations.query || {}
const { getTextIn } = useLanguage()
const { fullAccessToStudentData } = useGetAuthorizedUserQuery()
const [totalState, setTotalState] = useState({
query: initialQuery(),
isLoading: false,
momentYear: Datetime.moment('2017-01-01'),
})
const [didMount, setDidMount] = useState(false)
const [query, setQuery] = useState(initialQuery())
const [searchHistory, addItemToSearchHistory, updateItemInSearchHistory] = useSearchHistory('populationSearch', 8)
const [filterProgrammes, setFilterProgrammes] = useState(fullAccessToStudentData)
const fetchPopulationPromises = useRef()
const { data: programmes = {}, isLoading: programmesAreLoading } = useGetProgrammesQuery()
const studyProgrammes =
(programmes.KH90_001 || programmes.MH90_001) && !Object.keys(programmes).includes('KH90_001+MH90_001')
Expand All @@ -63,13 +57,9 @@ export const PopulationSearchForm = ({ onProgress }) => {
},
}
: programmes
const studyProgrammePins = useGetStudyProgrammePinsQuery().data
const { data: studyProgrammePins } = useGetStudyProgrammePinsQuery()
const pinnedProgrammes = studyProgrammePins?.studyProgrammes || []

const setState = newState => setTotalState({ ...totalState, ...newState })

const { query, isLoading } = totalState

const parseQueryFromUrl = () => {
const initial = initialQuery()
const { studyRights, months, ...rest } = qs.parse(location.search)
Expand Down Expand Up @@ -97,87 +87,50 @@ export const PopulationSearchForm = ({ onProgress }) => {
const fetchPopulation = async query => {
const formattedQueryParams = formatQueryParamsToArrays(query, ['semesters', 'studentStatuses', 'years'])
const uuid = crypto.randomUUID()
setState({ isLoading: true })
dispatch(clearSelected())
fetchPopulationPromises.current = cancelablePromise(
Promise.all([dispatch(getPopulationStatistics({ ...formattedQueryParams, uuid, onProgress }), [])])
)
const success = await fetchPopulationPromises.current.promise
if (success) {
setState({
isLoading: false,
})
}
dispatch(getPopulationStatistics({ ...formattedQueryParams, uuid, onProgress }))
}

const fetchPopulationFromUrlParams = () => {
const previousQuery = queries
const query = parseQueryFromUrl()
const formattedQuery = formatQueryParamsToArrays(query, ['semesters', 'studentStatuses', 'years'])
if (!checkPreviousQuery(formattedQuery, previousQuery)) {
setState({ query })
setQuery(query)
fetchPopulation(query)
}
}

useEffect(() => {
if (!studyProgrammes || Object.values(studyProgrammes).length === 0) {
setState({ query: initialQuery() })
setQuery(initialQuery())
}
if (location.search) {
fetchPopulationFromUrlParams()
}
if (!location.search) {
setState({
query: {
...query,
studentStatuses: [],
semesters: ['FALL', 'SPRING'],
},
setQuery({
...query,
studentStatuses: [],
semesters: ['FALL', 'SPRING'],
})
}
setDidMount(true)
return () => {
if (fetchPopulationPromises.current) fetchPopulationPromises.current.cancel()
}
}, [location.search])

const handleClear = type => {
setState({
query: {
...query,
studyRights: {
...query.studyRights,
[type]: undefined,
},
},
})
}

const handleProgrammeChange = (_event, { value }) => {
const programme = value
if (programme === '') {
handleClear('programme')
return
}
setState({
query: {
...query,
studyRights: {
programme,
},
},
const handleProgrammeChange = (_event, { value: programme }) => {
setQuery({
...query,
studyRights: programme === '' ? {} : { programme },
})
}

useEffect(() => {
if (studyProgrammes && Object.values(studyProgrammes).length === 1 && !query.studyRights.programme && didMount) {
if (studyProgrammes && Object.values(studyProgrammes).length === 1 && !query.studyRights.programme) {
handleProgrammeChange(null, { value: Object.values(studyProgrammes)[0].code })
}
})
}, [])

const pushQueryToUrl = query => {
if (!checkPreviousQuery(query, queries)) {
if (!checkPreviousQuery(query, previousQuery)) {
dispatch(clearPopulations())
}
// Just to be sure that the previous population's data has been cleared
Expand Down Expand Up @@ -219,48 +172,16 @@ export const PopulationSearchForm = ({ onProgress }) => {

const handleYearSelection = momentYear => {
if (!moment.isMoment(momentYear)) {
setState({
momentYear: null,
query: {
...query,
studyRights: {
...query.studyRights,
studyTrack: null,
},
},
})
return
}

// When changing year, remove track selection, if it is no longer possible to select
let { studyTrack } = query.studyRights
if (studyTrack) {
if (!query.studyRights.programme) {
studyTrack = null
} else {
const associations = studyProgrammes[query.studyRights.programme].enrollmentStartYears[momentYear.year()]
if (!associations) {
studyTrack = null
} else if (!associations.studyTracks[query.studyRights.studyTrack]) {
studyTrack = null
}
}
}

setState({
momentYear,
query: {
...query,
year: reformatDate(momentYear, YEAR_DATE_FORMAT),
months: getMonths(
reformatDate(momentYear, YEAR_DATE_FORMAT),
query.semesters.includes('FALL') ? 'FALL' : 'SPRING'
),
studyRights: {
...query.studyRights,
studyTrack,
},
},
setQuery({
...query,
year: reformatDate(momentYear, YEAR_DATE_FORMAT),
months: getMonths(
reformatDate(momentYear, YEAR_DATE_FORMAT),
query.semesters.includes('FALL') ? 'FALL' : 'SPRING'
),
})
}

Expand All @@ -276,16 +197,6 @@ export const PopulationSearchForm = ({ onProgress }) => {
handleYearSelection(previousYear)
}

const renderableList = list =>
list.map(({ code, name }) => ({
code,
description: code,
icon: pinnedProgrammes.includes(code) ? 'pin' : '',
name,
text: getTextIn(name),
value: code,
}))

const renderEnrollmentDateSelector = () => {
const { year } = query
const currentYear = moment().year()
Expand Down Expand Up @@ -319,8 +230,22 @@ export const PopulationSearchForm = ({ onProgress }) => {
</Form.Field>
<Form.Field className="yearControl">
<Button.Group basic className="yearControlButtonGroup" vertical>
<Button className="yearControlButton" icon="plus" onClick={addYear} tabIndex="-1" type="button" />
<Button className="yearControlButton" icon="minus" onClick={subtractYear} tabIndex="-1" type="button" />
<Button
className="yearControlButton"
disabled={currentYear <= parseInt(year, 10)}
icon="plus"
onClick={addYear}
tabIndex="-1"
type="button"
/>
<Button
className="yearControlButton"
disabled={parseInt(year, 10) <= 1900}
icon="minus"
onClick={subtractYear}
tabIndex="-1"
type="button"
/>
</Button.Group>
</Form.Field>
<Form.Field>
Expand All @@ -335,30 +260,9 @@ export const PopulationSearchForm = ({ onProgress }) => {
)
}

const renderStudyProgrammeDropdown = (studyRights, programmesToRender) => (
<Form.Field>
<label>Study programme</label>
<Form.Dropdown
clearable
closeOnChange
data-cy="select-study-programme"
fluid
noResultsMessage="No selectable study programmes"
onChange={handleProgrammeChange}
options={programmesToRender}
placeholder="Select study programme"
search={textAndDescriptionSearch}
selectOnBlur={false}
selectOnNavigation={false}
selection
value={studyRights.programme}
/>
</Form.Field>
)

const renderStudyProgrammeSelector = () => {
const { studyRights } = query
if (programmesAreLoading || !didMount) {
if (programmesAreLoading) {
return <Icon color="black" loading name="spinner" size="big" style={{ marginLeft: '45%' }} />
}
if (Object.values(studyProgrammes).length === 0 && !programmesAreLoading) {
Expand All @@ -377,47 +281,57 @@ export const PopulationSearchForm = ({ onProgress }) => {
if (filterProgrammes) {
sortedStudyProgrammes = sortedStudyProgrammes.filter(programme => isNewStudyProgramme(programme.code))
}
programmesToRender = renderableList(sortedStudyProgrammes)
programmesToRender = sortedStudyProgrammes.map(({ code, name }) => ({
code,
description: code,
icon: pinnedProgrammes.includes(code) ? 'pin' : '',
name,
text: getTextIn(name),
value: code,
}))
}
const pinnedFirstComparator = createPinnedFirstComparator(pinnedProgrammes)

return <div>{renderStudyProgrammeDropdown(studyRights, programmesToRender.sort(pinnedFirstComparator))}</div>
}

const shouldRenderSearchForm = () => {
const queryIsEmpty = Object.getOwnPropertyNames(queries).length > 0
return !queryIsEmpty
return (
<Form.Field>
<label>Study programme</label>
<Form.Dropdown
clearable
closeOnChange
data-cy="select-study-programme"
fluid
noResultsMessage="No selectable study programmes"
onChange={handleProgrammeChange}
options={programmesToRender.sort(pinnedFirstComparator)}
placeholder="Select study programme"
search={textAndDescriptionSearch}
selectOnBlur={false}
selectOnNavigation={false}
selection
value={studyRights.programme}
/>
</Form.Field>
)
}

if (!shouldRenderSearchForm() && location.search !== '') {
if (location.search !== '') {
return null
}

let invalidQuery = false
let errorMessage = 'Selected population already in analysis'

if (query.semesters.length === 0) {
invalidQuery = true
errorMessage = 'Select at least one semester'
}
let errorMessage

if (!query.studyRights.programme) {
invalidQuery = true
errorMessage = 'Select study programme'
}

return (
<Form error={invalidQuery} loading={isLoading}>
<Grid divided padded="vertically">
<Grid.Row>
<Grid.Column width={10}>
{renderEnrollmentDateSelector()}
{renderStudyProgrammeSelector()}
<Message color="blue" error header={errorMessage} />
</Grid.Column>
</Grid.Row>
</Grid>
<Form.Button color="blue" disabled={invalidQuery || query.months < 0} onClick={handleSubmit}>
<Form error={invalidQuery}>
{renderEnrollmentDateSelector()}
{renderStudyProgrammeSelector()}
<Message color="blue" error header={errorMessage} />
<Form.Button color="blue" disabled={invalidQuery} onClick={handleSubmit}>
See class
</Form.Button>
<SearchHistory
Expand Down

0 comments on commit d83a362

Please sign in to comment.