[Hooks] Add curriculum context

use to avoid excess prop drilling and desynced state between the view
and editor modal
This commit is contained in:
joonas-a
2026-09-29 13:21:10 +03:00
parent a8fe237e3d
commit 1a48b7a028
6 changed files with 84 additions and 28 deletions
@@ -31,7 +31,7 @@ import { AdvancedSettings } from '@/components/PopulationDetails/AdvancedSetting
import { PopulationQueryCard } from '@/components/PopulationDetails/PopulationQueryCard'
import { useColumns as columnsGeneralTab } from '@/components/PopulationDetails/studentColumns'
import { useDebouncedState } from '@/hooks/debouncedState'
import { useCurriculumState } from '@/hooks/useCurriculums'
import { CurriculumProvider, useCurriculum } from '@/hooks/useCurriculum'
import { useGetAuthorizedUserQuery } from '@/redux/auth'
import { KeyboardBackspaceIcon } from '@/theme'
import { PopulationQuery } from '@/types/populationSearch'
@@ -48,7 +48,13 @@ type PopulationDetailsProps = {
idToGroupIdMap: Record<string, string>
}
export const PopulationDetails = ({
export const PopulationDetails = (props: PopulationDetailsProps) => (
<CurriculumProvider programmeCode={props.query.programme} year={props.query?.years?.[0]}>
<PopulationDetailsContent {...props} />
</CurriculumProvider>
)
const PopulationDetailsContent = ({
isLoading,
query,
populationTags,
@@ -64,7 +70,7 @@ export const PopulationDetails = ({
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false)
const [courseTableMode, setCourseTableMode] = useState<'curriculum' | 'all'>('curriculum')
const [studentAmountLimit, setStudentAmountLimit] = useDebouncedState(0, 1000)
const [curriculum, curriculumList, setCurriculum] = useCurriculumState(programme, query?.years?.[0])
const { curriculum, curriculumList, setSelectedCurriculum: setCurriculum } = useCurriculum()
const [showModules, setShowModules] = useState(false) // Shows courses if modules not selected
const primaryHopsCreditFilter = useFilterSelector(hopsFilter.selectors.isPrimarySelected(undefined))
@@ -21,7 +21,7 @@ import { useFormat as formatGeneralTab } from '@/components/PopulationComponents
import { useColumns as columnsGeneralTab } from '@/components/StudyGuidanceGroups/studentColumns'
import { StudyGuidanceGroupPopulationCourses } from '@/components/StudyGuidanceGroups/StudyGuidanceGroupPopulationCourses'
import { createAcademicYearStartDate } from '@/components/StudyGuidanceGroups/utils'
import { useCurriculumState } from '@/hooks/useCurriculums'
import { CurriculumProvider, useCurriculum } from '@/hooks/useCurriculum'
import { KeyboardBackspaceIcon } from '@/theme'
import { FilteredCourse } from '@/util/coursesOfPopulation'
import { FormattedStudent } from '@oodikone/shared/types'
@@ -29,17 +29,30 @@ import { GroupsWithTags } from '@oodikone/shared/types/studyGuidanceGroup'
dayjsExtend(isBetween)
export const SingleStudyGuidanceGroupPanels = ({
filteredStudents,
filteredCourses,
group,
idToGroupIdMap,
}: {
type SingleStudyGuidanceGroupPanelsProps = {
filteredStudents: FormattedStudent[]
filteredCourses: FilteredCourse[]
group: GroupsWithTags
idToGroupIdMap: Record<string, string>
}) => {
}
export const SingleStudyGuidanceGroupPanels = (props: SingleStudyGuidanceGroupPanelsProps) => {
const [programme] = props.group.tags?.studyProgramme?.split('+') ?? []
const year = props.group.tags?.year ?? undefined
return (
<CurriculumProvider programmeCode={programme} year={year}>
<SingleStudyGuidanceGroupPanelsContent {...props} />
</CurriculumProvider>
)
}
const SingleStudyGuidanceGroupPanelsContent = ({
filteredStudents,
filteredCourses,
group,
idToGroupIdMap,
}: SingleStudyGuidanceGroupPanelsProps) => {
const { useFilterSelector, useFilterDispatch: filterDispatch } = useFilters()
const groupYear = group.tags?.year
@@ -53,7 +66,7 @@ export const SingleStudyGuidanceGroupPanels = ({
years: groupYear ? [Number(groupYear)] : [],
}
const [curriculum, curriculumList, setCurriculum] = useCurriculumState(programme, groupYear!) // TODO: fix year
const { curriculum, curriculumList, setSelectedCurriculum: setCurriculum } = useCurriculum()
const creditDateFilterActive = useFilterSelector(creditDateFilter.selectors.isActive(undefined))
const studyPlanFilterIsActive = useFilterSelector(studyPlanFilter.selectors.isActive(undefined))
@@ -0,0 +1,40 @@
import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useMemo } from 'react'
import { ExtendedCurriculumDetails, useCurriculumState } from '@/hooks/useCurriculums'
import { CurriculumOption } from '@oodikone/shared/types'
type CurriculumContextType = {
curriculum: ExtendedCurriculumDetails | null
curriculumList: CurriculumOption[]
setSelectedCurriculum: Dispatch<SetStateAction<CurriculumOption | null>>
}
const CurriculumContext = createContext<CurriculumContextType | null>(null)
CurriculumContext.displayName = 'Curriculum'
export const CurriculumProvider = ({
programmeCode,
year,
children,
}: {
programmeCode: string
year: string | number | undefined
children: ReactNode
}) => {
const [curriculum, curriculumList, setSelectedCurriculum] = useCurriculumState(programmeCode, year)
const value = useMemo(
() => ({ curriculum, curriculumList, setSelectedCurriculum }),
[curriculum, curriculumList, setSelectedCurriculum]
)
return <CurriculumContext.Provider value={value}>{children}</CurriculumContext.Provider>
}
/** Context variant of the curriculum hook */
export const useCurriculum = () => {
const context = useContext(CurriculumContext)
if (!context) {
throw new Error('useCurriculum must be used within a CurriculumProvider')
}
return context
}
@@ -1,4 +1,4 @@
import { useEffect, useState, Dispatch, SetStateAction } from 'react'
import { useMemo, useState, Dispatch, SetStateAction } from 'react'
import { useGetCurriculumsQuery, useGetCurriculumOptionsQuery } from '@/redux/curriculum'
import { CurriculumOption, CurriculumDetails } from '@oodikone/shared/types'
@@ -7,10 +7,9 @@ export type ExtendedCurriculumDetails = CurriculumDetails & CurriculumOption
export const useCurriculumState = (
programmeCode: string,
year: string | number
year: string | number | undefined
): [ExtendedCurriculumDetails | null, CurriculumOption[], Dispatch<SetStateAction<CurriculumOption | null>>] => {
const [selectedCurriculum, setSelectedCurriculum] = useState<CurriculumOption | null>(null)
const [curriculum, setCurriculum] = useState<ExtendedCurriculumDetails | null>(null)
const { data: curriculumList = [], isFetching: curriculumsLoading } = useGetCurriculumOptionsQuery(
{ code: programmeCode },
@@ -19,7 +18,7 @@ export const useCurriculumState = (
const chosenCurriculum: CurriculumOption | null =
selectedCurriculum ??
curriculumList.find(curriculum => new Date(curriculum.validFrom) <= new Date(`${year}-08-01`)) ??
(year ? curriculumList.find(curriculum => new Date(curriculum.validFrom) <= new Date(`${year}-08-01`)) : null) ??
curriculumList[0] ??
null
@@ -31,11 +30,10 @@ export const useCurriculumState = (
{ skip: curriculumsLoading || !chosenCurriculum?.periodIds }
)
useEffect(() => {
if (curriculumData) {
setCurriculum({ ...curriculumData, ...chosenCurriculum })
}
}, [curriculumData, chosenCurriculum])
const curriculum = useMemo<ExtendedCurriculumDetails | null>(
() => (curriculumData ? { ...curriculumData, ...chosenCurriculum } : null),
[curriculumData, chosenCurriculum]
)
return [curriculum, curriculumList, setSelectedCurriculum]
}
@@ -3,7 +3,7 @@ import Stack from '@mui/material/Stack'
import { orderBy } from 'lodash-es'
import { useEffect, useState } from 'react'
import { useCurriculumState } from '@/hooks/useCurriculums'
import { useCurriculum } from '@/hooks/useCurriculum'
import { CreditCriteriaSection } from '@/pages/StudyProgramme/DegreeCoursesTab/CreditCriteriaSection'
import { CurriculumSection } from '@/pages/StudyProgramme/DegreeCoursesTab/CurriculumSection'
import { DegreeCourseTable } from '@/pages/StudyProgramme/DegreeCoursesTab/DegreeCourseTable'
@@ -23,9 +23,9 @@ export const DegreeCoursesTab = ({
}) => {
const [defaultProgrammeModules, setDefaultProgrammeModules] = useState<Module[]>([])
const [secondProgrammeModules, setSecondProgrammeModules] = useState<Module[]>([])
const [curriculum, curriculumList, setCurriculum] = useCurriculumState(degreeProgramme, new Date().getFullYear())
const { data: criteria } = useGetProgressCriteriaQuery({ programmeCode: degreeProgramme })
const { getTextIn } = useLanguage()
const { curriculum, curriculumList, setSelectedCurriculum: setCurriculum } = useCurriculum()
const getModules = (courses: ProgrammeCourse[]): Module[] => {
const modules: Record<string, ProgrammeCourse[]> = {}
@@ -8,6 +8,7 @@ import { PageLayout } from '@/components/common/PageLayout'
import { PageTitle } from '@/components/common/PageTitle'
import { GetTextIn, useLanguage } from '@/components/LanguagePicker/useLanguage'
import { useTabs } from '@/hooks/tabs'
import { CurriculumProvider } from '@/hooks/useCurriculum'
import { useTitle } from '@/hooks/title'
import { BasicInformationTab } from '@/pages/StudyProgramme/BasicInformationTab'
import { DegreeCoursesTab } from '@/pages/StudyProgramme/DegreeCoursesTab'
@@ -136,11 +137,9 @@ export const StudyProgramme = () => {
key: 'DegreeCoursesTab',
label: 'Degree courses',
component: (
<DegreeCoursesTab
combinedProgramme={secondProgrammeId}
degreeProgramme={programmeId}
key="DegreeCoursesTabContent"
/>
<CurriculumProvider key="DegreeCoursesTabContent" programmeCode={programmeId} year={new Date().getFullYear()}>
<DegreeCoursesTab combinedProgramme={secondProgrammeId} degreeProgramme={programmeId} />
</CurriculumProvider>
),
},
{