From a4f9eddd563ab65244068dd08c59a68bdb1bf3ab Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:42:38 +1100 Subject: [PATCH 01/13] feat: main three user queries abstracted --- .../CourseInfoDrawers.tsx | 8 ++----- .../components/PlannerCart/PlannerCart.tsx | 12 +++------- frontend/src/hooks/useIdentity.ts | 18 +++++++++++++++ .../CourseBanner/CourseBanner.tsx | 9 ++------ .../pages/CourseSelector/CourseSelector.tsx | 17 ++++---------- .../CourseGraph/CourseGraph.tsx | 20 ++++++----------- .../GraphicalSelector/GraphicalSelector.tsx | 10 ++------- .../Dashboard/Dashboard.tsx | 15 ++++--------- .../ProgressionChecker/ProgressionChecker.tsx | 22 +++++-------------- .../HideYearTooltip/HideYearTooltip.tsx | 11 ++-------- .../OptionsHeader/OptionsHeader.tsx | 9 +++----- .../src/pages/TermPlanner/TermBox/TermBox.tsx | 15 +++++-------- .../src/pages/TermPlanner/TermPlanner.tsx | 13 ++++------- .../UnplannedColumn/UnplannedColumn.tsx | 16 ++++---------- frontend/src/reducers/identitySlice.ts | 2 +- frontend/src/utils/apiHooks/keys.md | 3 +++ .../src/utils/apiHooks/useUserCourses.tsx | 17 ++++++++++++++ frontend/src/utils/apiHooks/useUserDegree.tsx | 17 ++++++++++++++ .../src/utils/apiHooks/useUserPlanner.tsx | 17 ++++++++++++++ 19 files changed, 121 insertions(+), 130 deletions(-) create mode 100644 frontend/src/hooks/useIdentity.ts create mode 100644 frontend/src/utils/apiHooks/keys.md create mode 100644 frontend/src/utils/apiHooks/useUserCourses.tsx create mode 100644 frontend/src/utils/apiHooks/useUserDegree.tsx create mode 100644 frontend/src/utils/apiHooks/useUserPlanner.tsx diff --git a/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx b/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx index e70afa017..22431491c 100644 --- a/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx +++ b/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx @@ -5,7 +5,7 @@ import { Course, CoursesUnlockedWhenTaken } from 'types/api'; import { CourseList } from 'types/courses'; import { badCourses, badValidations } from 'types/userResponse'; import { validateTermPlanner } from 'utils/api/plannerApi'; -import { getUserCourses } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; import Collapsible from 'components/Collapsible'; import CourseTag from 'components/CourseTag'; import PrerequisiteTree from 'components/PrerequisiteTree'; @@ -30,11 +30,7 @@ const CourseInfoDrawers = ({ }: CourseInfoDrawersProps) => { const token = useToken(); - const courses = - useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }).data || badCourses; + const courses = useUserCourses().data || badCourses; const pathFromInPlanner = pathFrom.filter((courseCode) => Object.keys(courses).includes(courseCode) diff --git a/frontend/src/components/PlannerCart/PlannerCart.tsx b/frontend/src/components/PlannerCart/PlannerCart.tsx index 1e461d585..1a3a7ab36 100644 --- a/frontend/src/components/PlannerCart/PlannerCart.tsx +++ b/frontend/src/components/PlannerCart/PlannerCart.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { CalendarOutlined, DeleteOutlined } from '@ant-design/icons'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { Button, Tooltip, Typography } from 'antd'; import { badCourses } from 'types/userResponse'; import { removeAll } from 'utils/api/plannerApi'; -import { getUserCourses } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; import CourseCartCard from 'components/CourseCartCard'; import useToken from 'hooks/useToken'; import S from './styles'; @@ -17,13 +17,7 @@ const PlannerCart = () => { const [showMenu, setShowMenu] = useState(false); const token = useToken(); - const courses = - useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token), - enabled: showMenu, - staleTime: 100000 - }).data ?? badCourses; + const courses = useUserCourses().data ?? badCourses; const removeAllCourses = useMutation({ mutationKey: ['removeCourses'], diff --git a/frontend/src/hooks/useIdentity.ts b/frontend/src/hooks/useIdentity.ts new file mode 100644 index 000000000..9e688e94d --- /dev/null +++ b/frontend/src/hooks/useIdentity.ts @@ -0,0 +1,18 @@ +import { useAppSelector } from 'hooks'; +import { CompleteIdentity, selectIdentity } from 'reducers/identitySlice'; + +function useIdentity(allowUnset?: false): CompleteIdentity; +function useIdentity(allowUnset: true): CompleteIdentity | null; +function useIdentity(allowUnset: boolean): CompleteIdentity | null; + +function useIdentity(allowUnset?: boolean): CompleteIdentity | null { + const identity = useAppSelector(selectIdentity); + + if (identity === null && allowUnset !== true) { + throw TypeError(`useIdentity: allowUnset was false and identity was null.`); + } + + return identity; +} + +export default useIdentity; diff --git a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx index 1ef5c09bc..c796a3c13 100644 --- a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx +++ b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx @@ -3,10 +3,9 @@ import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; import { CoursesResponse } from 'types/userResponse'; import { fetchAllDegrees } from 'utils/api/programsApi'; -import { getUserDegree } from 'utils/api/userApi'; +import useUserDegree from 'utils/apiHooks/useUserDegree'; import CourseSearchBar from 'components/CourseSearchBar'; import { useAppDispatch } from 'hooks'; -import useToken from 'hooks/useToken'; import { addTab } from 'reducers/courseTabsSlice'; import S from './styles'; @@ -17,13 +16,9 @@ type CourseBannerProps = { }; const CourseBanner = ({ courses }: CourseBannerProps) => { - const token = useToken(); const dispatch = useAppDispatch(); - const degreeQuery = useQuery({ - queryKey: ['degree'], - queryFn: () => getUserDegree(token) - }); + const degreeQuery = useUserDegree(); const allPrograms = useQuery({ queryKey: ['programs'], queryFn: fetchAllDegrees diff --git a/frontend/src/pages/CourseSelector/CourseSelector.tsx b/frontend/src/pages/CourseSelector/CourseSelector.tsx index 0c7674e58..d2af9b87d 100644 --- a/frontend/src/pages/CourseSelector/CourseSelector.tsx +++ b/frontend/src/pages/CourseSelector/CourseSelector.tsx @@ -1,13 +1,12 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useQuery } from '@tanstack/react-query'; -import { getUserCourses, getUserDegree } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; +import useUserDegree from 'utils/apiHooks/useUserDegree'; import openNotification from 'utils/openNotification'; import infographic from 'assets/infographicFontIndependent.svg'; import CourseDescriptionPanel from 'components/CourseDescriptionPanel'; import PageTemplate from 'components/PageTemplate'; import type { RootState } from 'config/store'; -import useToken from 'hooks/useToken'; import { addTab } from 'reducers/courseTabsSlice'; import CourseBanner from './CourseBanner'; import CourseMenu from './CourseMenu'; @@ -15,17 +14,9 @@ import CourseTabs from './CourseTabs'; import S from './styles'; const CourseSelector = () => { - const token = useToken(); + const coursesQuery = useUserCourses(); - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); - - const degreeQuery = useQuery({ - queryKey: ['degree'], - queryFn: () => getUserDegree(token) - }); + const degreeQuery = useUserDegree(); const [showedNotif, setShowedNotif] = useState(false); useEffect(() => { diff --git a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx index 246259136..7ca51a574 100644 --- a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx +++ b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx @@ -12,7 +12,9 @@ import { CourseEdge } from 'types/api'; import { useDebouncedCallback } from 'use-debounce'; import { getAllUnlockedCourses } from 'utils/api/coursesApi'; import { getProgramGraph } from 'utils/api/programsApi'; -import { getUserCourses, getUserDegree, getUserPlanner } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; +import useUserDegree from 'utils/apiHooks/useUserDegree'; +import useUserPlanner from 'utils/apiHooks/useUserPlanner'; import { unwrapQuery } from 'utils/queryUtils'; import Spinner from 'components/Spinner'; import { useAppWindowSize } from 'hooks'; @@ -58,18 +60,10 @@ const CourseGraph = ({ }: Props) => { const token = useToken(); - const degreeQuery = useQuery({ - queryKey: ['degree'], - queryFn: () => getUserDegree(token) - }); - const plannerQuery = useQuery({ - queryKey: ['planner'], - queryFn: () => getUserPlanner(token) - }); - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); + const degreeQuery = useUserDegree(); + const plannerQuery = useUserPlanner(); + const coursesQuery = useUserCourses(); + const windowSize = useAppWindowSize(); const { theme } = useSettings(); const previousTheme = useRef(theme); diff --git a/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx b/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx index 67ae7a844..079d1caa0 100644 --- a/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx +++ b/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx @@ -1,12 +1,10 @@ import React, { useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; import { Tabs } from 'antd'; import { badCourses } from 'types/userResponse'; -import { getUserCourses } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; import CourseSearchBar from 'components/CourseSearchBar'; import PageTemplate from 'components/PageTemplate'; import SidebarDrawer from 'components/SidebarDrawer'; -import useToken from 'hooks/useToken'; import CS from './common/styles'; import { COURSE_INFO_TAB, HELP_TAB, PROGRAM_STRUCTURE_TAB } from './constants'; import CourseGraph from './CourseGraph'; @@ -14,14 +12,10 @@ import HowToUse from './HowToUse'; import S from './styles'; const GraphicalSelector = () => { - const token = useToken(); const [fullscreen, setFullscreen] = useState(false); const [courseCode, setCourseCode] = useState(null); const [activeTab, setActiveTab] = useState(HELP_TAB); - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); + const coursesQuery = useUserCourses(); const [loading, setLoading] = useState(true); const courses = coursesQuery.data || badCourses; diff --git a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx index ee0944f71..2112fb8ac 100644 --- a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx @@ -7,12 +7,12 @@ import { Button, Typography } from 'antd'; import { ProgramStructure } from 'types/structure'; import { badCourses, badDegree } from 'types/userResponse'; import { fetchAllDegrees } from 'utils/api/programsApi'; -import { getUserCourses, getUserDegree } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; +import useUserDegree from 'utils/apiHooks/useUserDegree'; import getNumTerms from 'utils/getNumTerms'; import LiquidProgressChart from 'components/LiquidProgressChart'; import { LoadingDashboard } from 'components/LoadingSkeleton'; import SpecialisationCard from 'components/SpecialisationCard'; -import useToken from 'hooks/useToken'; import FreeElectivesCard from './FreeElectivesCard'; import S from './styles'; @@ -33,7 +33,6 @@ type Props = { const Dashboard = ({ isLoading, structure, totalUOC, freeElectivesUOC }: Props) => { const { Title } = Typography; const currYear = new Date().getFullYear(); - const token = useToken(); const props = useSpring({ from: { opacity: 0 }, @@ -41,15 +40,9 @@ const Dashboard = ({ isLoading, structure, totalUOC, freeElectivesUOC }: Props) reset: true, config: { tension: 80, friction: 60 } }); - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); + const coursesQuery = useUserCourses(); const courses = coursesQuery.data || badCourses; - const degreeQuery = useQuery({ - queryKey: ['degree'], - queryFn: () => getUserDegree(token) - }); + const degreeQuery = useUserDegree(); const degree = degreeQuery.data || badDegree; const { programCode } = degree; diff --git a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx index 3ded9f2a3..c4c25ac46 100644 --- a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx +++ b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx @@ -17,13 +17,14 @@ import { import { ProgramStructure } from 'types/structure'; import { badCourses, badPlanner } from 'types/userResponse'; import { getProgramStructure } from 'utils/api/programsApi'; -import { getUserCourses, getUserDegree, getUserPlanner } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; +import useUserDegree from 'utils/apiHooks/useUserDegree'; +import useUserPlanner from 'utils/apiHooks/useUserPlanner'; import getNumTerms from 'utils/getNumTerms'; import openNotification from 'utils/openNotification'; import Collapsible from 'components/Collapsible'; import PageTemplate from 'components/PageTemplate'; import { MAX_COURSES_OVERFLOW } from 'config/constants'; -import useToken from 'hooks/useToken'; import Dashboard from './Dashboard'; import GenericCoursesSection from './GenericCoursesSection'; import GridView from './GridView'; @@ -33,19 +34,11 @@ import TableView from './TableView'; const { Title } = Typography; const ProgressionChecker = () => { - const token = useToken(); - - const plannerQuery = useQuery({ - queryKey: ['planner'], - queryFn: () => getUserPlanner(token) - }); + const plannerQuery = useUserPlanner(); const planner = plannerQuery.data ?? badPlanner; const { unplanned } = planner; - const degreeQuery = useQuery({ - queryKey: ['degree'], - queryFn: () => getUserDegree(token) - }); + const degreeQuery = useUserDegree(); const degree = degreeQuery.data; const structureQuery = useQuery({ @@ -66,10 +59,7 @@ const ProgressionChecker = () => { }, []); const [view, setView] = useState(Views.GRID_CONCISE); - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); + const coursesQuery = useUserCourses(); const courses = coursesQuery.data ?? badCourses; const countedCourses: string[] = []; diff --git a/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx b/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx index 1a945b97a..eb32eb209 100644 --- a/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx +++ b/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx @@ -1,26 +1,19 @@ import React from 'react'; import { EyeInvisibleFilled } from '@ant-design/icons'; -import { useQuery } from '@tanstack/react-query'; import { Tooltip } from 'antd'; import { badPlanner, PlannerResponse } from 'types/userResponse'; -import { getUserPlanner } from 'utils/api/userApi'; +import useUserPlanner from 'utils/apiHooks/useUserPlanner'; import openNotification from 'utils/openNotification'; import useSettings from 'hooks/useSettings'; -import useToken from 'hooks/useToken'; type Props = { year: number; }; const HideYearTooltip = ({ year }: Props) => { - const token = useToken(); - const { hiddenYears, hideYear } = useSettings(); - const plannerQuery = useQuery({ - queryKey: ['planner'], - queryFn: () => getUserPlanner(token) - }); + const plannerQuery = useUserPlanner(); const planner: PlannerResponse = plannerQuery.data ?? badPlanner; const numYears = planner.years.length; diff --git a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx index 14eccb873..ded568421 100644 --- a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx +++ b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx @@ -9,11 +9,11 @@ import { UploadOutlined, WarningFilled } from '@ant-design/icons'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import Tippy from '@tippyjs/react'; import { Popconfirm, Switch, Tooltip } from 'antd'; import { unscheduleAll } from 'utils/api/plannerApi'; -import { getUserPlanner } from 'utils/api/userApi'; +import useUserPlanner from 'utils/apiHooks/useUserPlanner'; import useSettings from 'hooks/useSettings'; import useToken from 'hooks/useToken'; import ExportPlannerMenu from '../ExportPlannerMenu'; @@ -30,10 +30,7 @@ const OptionsHeader = () => { const token = useToken(); const queryClient = useQueryClient(); - const plannerQuery = useQuery({ - queryKey: ['planner'], - queryFn: () => getUserPlanner(token) - }); + const plannerQuery = useUserPlanner(); const planner = plannerQuery.data; const { diff --git a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx index 6b9bd45be..c057b68ec 100644 --- a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx +++ b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx @@ -1,6 +1,6 @@ import React, { Suspense } from 'react'; import { LockFilled, UnlockFilled } from '@ant-design/icons'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Badge } from 'antd'; import { useTheme } from 'styled-components'; import { Course } from 'types/api'; @@ -8,7 +8,8 @@ import { CourseTime } from 'types/courses'; import { Term } from 'types/planner'; import { ValidateResponse } from 'types/userResponse'; import { toggleLockTerm } from 'utils/api/plannerApi'; -import { getUserCourses, getUserPlanner } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; +import useUserPlanner from 'utils/apiHooks/useUserPlanner'; import { courseHasOffering } from 'utils/getAllCourseOfferings'; import Spinner from 'components/Spinner'; import useMediaQuery from 'hooks/useMediaQuery'; @@ -43,10 +44,7 @@ const TermBox = ({ const theme = useTheme(); const queryClient = useQueryClient(); - const plannerQuery = useQuery({ - queryKey: ['planner'], - queryFn: () => getUserPlanner(token) - }); + const plannerQuery = useUserPlanner(); const toggleLockTermMutation = useMutation({ mutationFn: () => toggleLockTerm(token, year, term), @@ -61,10 +59,7 @@ const TermBox = ({ } }); - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); + const coursesQuery = useUserCourses(); const isSmall = useMediaQuery('(max-width: 1400px)'); if (!coursesQuery.data || !plannerQuery.data) { diff --git a/frontend/src/pages/TermPlanner/TermPlanner.tsx b/frontend/src/pages/TermPlanner/TermPlanner.tsx index 35c5c3e5e..6390a2744 100644 --- a/frontend/src/pages/TermPlanner/TermPlanner.tsx +++ b/frontend/src/pages/TermPlanner/TermPlanner.tsx @@ -20,7 +20,8 @@ import { unscheduleCourse, validateTermPlanner } from 'utils/api/plannerApi'; -import { getUserCourses, getUserPlanner } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; +import useUserPlanner from 'utils/apiHooks/useUserPlanner'; import openNotification from 'utils/openNotification'; import PageTemplate from 'components/PageTemplate'; import Spinner from 'components/Spinner'; @@ -74,17 +75,11 @@ const TermPlanner = () => { const plannerPicRef = useRef(null); // Planer obj - const plannerQuery = useQuery({ - queryKey: ['planner'], - queryFn: () => getUserPlanner(token) - }); + const plannerQuery = useUserPlanner(); const planner: PlannerResponse = plannerQuery.data ?? badPlanner; // The user's actual courses obj??????? - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); + const coursesQuery = useUserCourses(); const courses: CoursesResponse = coursesQuery.data ?? badCourses; const validateQuery = useQuery({ diff --git a/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx b/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx index 1ab8b9572..e08bc110d 100644 --- a/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx +++ b/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx @@ -1,5 +1,4 @@ import React, { Suspense } from 'react'; -import { useQuery } from '@tanstack/react-query'; import { Course } from 'types/api'; import { badCourses, @@ -8,10 +7,10 @@ import { PlannerResponse, ValidateResponse } from 'types/userResponse'; -import { getUserCourses, getUserPlanner } from 'utils/api/userApi'; +import useUserCourses from 'utils/apiHooks/useUserCourses'; +import useUserPlanner from 'utils/apiHooks/useUserPlanner'; import Spinner from 'components/Spinner'; import useMediaQuery from 'hooks/useMediaQuery'; -import useToken from 'hooks/useToken'; import DraggableCourse from '../DraggableCourse'; import S from './styles'; @@ -26,18 +25,11 @@ const Droppable = React.lazy(() => ); const UnplannedColumn = ({ dragging, courseInfos, validateInfos }: Props) => { - const token = useToken(); - const plannerQuery = useQuery({ - queryKey: ['planner'], - queryFn: () => getUserPlanner(token) - }); + const plannerQuery = useUserPlanner(); const planner: PlannerResponse = plannerQuery.data ?? badPlanner; const { unplanned, isSummerEnabled } = planner; - const coursesQuery = useQuery({ - queryKey: ['courses'], - queryFn: () => getUserCourses(token) - }); + const coursesQuery = useUserCourses(); const courses: CoursesResponse = coursesQuery.data ?? badCourses; const isSmall = useMediaQuery('(max-width: 1400px)'); diff --git a/frontend/src/reducers/identitySlice.ts b/frontend/src/reducers/identitySlice.ts index 25962144b..7066e4433 100644 --- a/frontend/src/reducers/identitySlice.ts +++ b/frontend/src/reducers/identitySlice.ts @@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { IdentityResponse } from 'utils/api/authApi'; -type CompleteIdentity = { +export type CompleteIdentity = { token: string; userId: string; expiresAt: number; diff --git a/frontend/src/utils/apiHooks/keys.md b/frontend/src/utils/apiHooks/keys.md new file mode 100644 index 000000000..4f4d30953 --- /dev/null +++ b/frontend/src/utils/apiHooks/keys.md @@ -0,0 +1,3 @@ +- `getUserPlanner`: user, , planner +- `getUserCourses`: user, , planner, courses +- `getUserDegree`: user, , degree \ No newline at end of file diff --git a/frontend/src/utils/apiHooks/useUserCourses.tsx b/frontend/src/utils/apiHooks/useUserCourses.tsx new file mode 100644 index 000000000..d3fc42630 --- /dev/null +++ b/frontend/src/utils/apiHooks/useUserCourses.tsx @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { getUserCourses } from 'utils/api/userApi'; +import useIdentity from 'hooks/useIdentity'; + +const useUserCourses = (allowTokenUnset?: boolean) => { + const { userId, token } = useIdentity(allowTokenUnset === true) ?? {}; + + const coursesQuery = useQuery({ + queryKey: ['user', userId!, 'planner', 'courses'], + queryFn: () => getUserCourses(token!), + enabled: token !== undefined + }); + + return coursesQuery; +}; + +export default useUserCourses; diff --git a/frontend/src/utils/apiHooks/useUserDegree.tsx b/frontend/src/utils/apiHooks/useUserDegree.tsx new file mode 100644 index 000000000..e56c1ea7c --- /dev/null +++ b/frontend/src/utils/apiHooks/useUserDegree.tsx @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { getUserDegree } from 'utils/api/userApi'; +import useIdentity from 'hooks/useIdentity'; + +const useUserDegree = (allowTokenUnset?: boolean) => { + const { userId, token } = useIdentity(allowTokenUnset === true) ?? {}; + + const degreeQuery = useQuery({ + queryKey: ['user', userId!, 'degree'], + queryFn: () => getUserDegree(token!), + enabled: token !== undefined + }); + + return degreeQuery; +}; + +export default useUserDegree; diff --git a/frontend/src/utils/apiHooks/useUserPlanner.tsx b/frontend/src/utils/apiHooks/useUserPlanner.tsx new file mode 100644 index 000000000..6b50c4fb1 --- /dev/null +++ b/frontend/src/utils/apiHooks/useUserPlanner.tsx @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { getUserPlanner } from 'utils/api/userApi'; +import useIdentity from 'hooks/useIdentity'; + +const useUserPlanner = (allowTokenUnset?: boolean) => { + const { userId, token } = useIdentity(allowTokenUnset === true) ?? {}; + + const degreeQuery = useQuery({ + queryKey: ['user', userId!, 'planner'], + queryFn: () => getUserPlanner(token!), + enabled: token !== undefined + }); + + return degreeQuery; +}; + +export default useUserPlanner; From 84005c355ff3db4672a24b7d16c49cec49bc76b2 Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:49:11 +1100 Subject: [PATCH 02/13] fix: bad imports for new use queries --- .../components/CourseDescriptionPanel/CourseInfoDrawers.tsx | 2 +- frontend/src/components/PlannerCart/PlannerCart.tsx | 2 +- .../src/pages/CourseSelector/CourseBanner/CourseBanner.tsx | 2 +- frontend/src/pages/CourseSelector/CourseSelector.tsx | 4 +--- .../src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx | 4 +--- frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx | 2 +- .../src/pages/ProgressionChecker/Dashboard/Dashboard.tsx | 3 +-- frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx | 4 +--- .../pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx | 2 +- .../src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx | 2 +- frontend/src/pages/TermPlanner/TermBox/TermBox.tsx | 3 +-- frontend/src/pages/TermPlanner/TermPlanner.tsx | 3 +-- .../pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx | 3 +-- frontend/src/utils/apiHooks/user/index.ts | 5 +++++ frontend/src/utils/apiHooks/{ => user}/useUserCourses.tsx | 0 frontend/src/utils/apiHooks/{ => user}/useUserDegree.tsx | 0 frontend/src/utils/apiHooks/{ => user}/useUserPlanner.tsx | 0 17 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 frontend/src/utils/apiHooks/user/index.ts rename frontend/src/utils/apiHooks/{ => user}/useUserCourses.tsx (100%) rename frontend/src/utils/apiHooks/{ => user}/useUserDegree.tsx (100%) rename frontend/src/utils/apiHooks/{ => user}/useUserPlanner.tsx (100%) diff --git a/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx b/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx index 22431491c..0be545ccb 100644 --- a/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx +++ b/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx @@ -5,7 +5,7 @@ import { Course, CoursesUnlockedWhenTaken } from 'types/api'; import { CourseList } from 'types/courses'; import { badCourses, badValidations } from 'types/userResponse'; import { validateTermPlanner } from 'utils/api/plannerApi'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; +import { useUserCourses } from 'utils/apiHooks/user'; import Collapsible from 'components/Collapsible'; import CourseTag from 'components/CourseTag'; import PrerequisiteTree from 'components/PrerequisiteTree'; diff --git a/frontend/src/components/PlannerCart/PlannerCart.tsx b/frontend/src/components/PlannerCart/PlannerCart.tsx index 1a3a7ab36..b58e3a19f 100644 --- a/frontend/src/components/PlannerCart/PlannerCart.tsx +++ b/frontend/src/components/PlannerCart/PlannerCart.tsx @@ -5,7 +5,7 @@ import { useMutation } from '@tanstack/react-query'; import { Button, Tooltip, Typography } from 'antd'; import { badCourses } from 'types/userResponse'; import { removeAll } from 'utils/api/plannerApi'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; +import { useUserCourses } from 'utils/apiHooks/user'; import CourseCartCard from 'components/CourseCartCard'; import useToken from 'hooks/useToken'; import S from './styles'; diff --git a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx index c796a3c13..9ede6ee76 100644 --- a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx +++ b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; import { CoursesResponse } from 'types/userResponse'; import { fetchAllDegrees } from 'utils/api/programsApi'; -import useUserDegree from 'utils/apiHooks/useUserDegree'; +import { useUserDegree } from 'utils/apiHooks/user'; import CourseSearchBar from 'components/CourseSearchBar'; import { useAppDispatch } from 'hooks'; import { addTab } from 'reducers/courseTabsSlice'; diff --git a/frontend/src/pages/CourseSelector/CourseSelector.tsx b/frontend/src/pages/CourseSelector/CourseSelector.tsx index d2af9b87d..8a3086835 100644 --- a/frontend/src/pages/CourseSelector/CourseSelector.tsx +++ b/frontend/src/pages/CourseSelector/CourseSelector.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; -import useUserDegree from 'utils/apiHooks/useUserDegree'; +import { useUserCourses, useUserDegree } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import infographic from 'assets/infographicFontIndependent.svg'; import CourseDescriptionPanel from 'components/CourseDescriptionPanel'; @@ -15,7 +14,6 @@ import S from './styles'; const CourseSelector = () => { const coursesQuery = useUserCourses(); - const degreeQuery = useUserDegree(); const [showedNotif, setShowedNotif] = useState(false); diff --git a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx index 7ca51a574..84cb72da0 100644 --- a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx +++ b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx @@ -12,9 +12,7 @@ import { CourseEdge } from 'types/api'; import { useDebouncedCallback } from 'use-debounce'; import { getAllUnlockedCourses } from 'utils/api/coursesApi'; import { getProgramGraph } from 'utils/api/programsApi'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; -import useUserDegree from 'utils/apiHooks/useUserDegree'; -import useUserPlanner from 'utils/apiHooks/useUserPlanner'; +import { useUserCourses, useUserDegree, useUserPlanner } from 'utils/apiHooks/user'; import { unwrapQuery } from 'utils/queryUtils'; import Spinner from 'components/Spinner'; import { useAppWindowSize } from 'hooks'; diff --git a/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx b/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx index 079d1caa0..c62e52d52 100644 --- a/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx +++ b/frontend/src/pages/GraphicalSelector/GraphicalSelector.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Tabs } from 'antd'; import { badCourses } from 'types/userResponse'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; +import { useUserCourses } from 'utils/apiHooks/user'; import CourseSearchBar from 'components/CourseSearchBar'; import PageTemplate from 'components/PageTemplate'; import SidebarDrawer from 'components/SidebarDrawer'; diff --git a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx index 2112fb8ac..9838728e0 100644 --- a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx @@ -7,8 +7,7 @@ import { Button, Typography } from 'antd'; import { ProgramStructure } from 'types/structure'; import { badCourses, badDegree } from 'types/userResponse'; import { fetchAllDegrees } from 'utils/api/programsApi'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; -import useUserDegree from 'utils/apiHooks/useUserDegree'; +import { useUserCourses, useUserDegree } from 'utils/apiHooks/user'; import getNumTerms from 'utils/getNumTerms'; import LiquidProgressChart from 'components/LiquidProgressChart'; import { LoadingDashboard } from 'components/LoadingSkeleton'; diff --git a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx index c4c25ac46..e9f4c33c2 100644 --- a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx +++ b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx @@ -17,9 +17,7 @@ import { import { ProgramStructure } from 'types/structure'; import { badCourses, badPlanner } from 'types/userResponse'; import { getProgramStructure } from 'utils/api/programsApi'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; -import useUserDegree from 'utils/apiHooks/useUserDegree'; -import useUserPlanner from 'utils/apiHooks/useUserPlanner'; +import { useUserCourses, useUserDegree, useUserPlanner } from 'utils/apiHooks/user'; import getNumTerms from 'utils/getNumTerms'; import openNotification from 'utils/openNotification'; import Collapsible from 'components/Collapsible'; diff --git a/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx b/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx index eb32eb209..1833851ff 100644 --- a/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx +++ b/frontend/src/pages/TermPlanner/HideYearTooltip/HideYearTooltip.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { EyeInvisibleFilled } from '@ant-design/icons'; import { Tooltip } from 'antd'; import { badPlanner, PlannerResponse } from 'types/userResponse'; -import useUserPlanner from 'utils/apiHooks/useUserPlanner'; +import { useUserPlanner } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import useSettings from 'hooks/useSettings'; diff --git a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx index ded568421..5c3f2a587 100644 --- a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx +++ b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx @@ -13,7 +13,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import Tippy from '@tippyjs/react'; import { Popconfirm, Switch, Tooltip } from 'antd'; import { unscheduleAll } from 'utils/api/plannerApi'; -import useUserPlanner from 'utils/apiHooks/useUserPlanner'; +import { useUserPlanner } from 'utils/apiHooks/user'; import useSettings from 'hooks/useSettings'; import useToken from 'hooks/useToken'; import ExportPlannerMenu from '../ExportPlannerMenu'; diff --git a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx index c057b68ec..344a17fec 100644 --- a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx +++ b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx @@ -8,8 +8,7 @@ import { CourseTime } from 'types/courses'; import { Term } from 'types/planner'; import { ValidateResponse } from 'types/userResponse'; import { toggleLockTerm } from 'utils/api/plannerApi'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; -import useUserPlanner from 'utils/apiHooks/useUserPlanner'; +import { useUserCourses, useUserPlanner } from 'utils/apiHooks/user'; import { courseHasOffering } from 'utils/getAllCourseOfferings'; import Spinner from 'components/Spinner'; import useMediaQuery from 'hooks/useMediaQuery'; diff --git a/frontend/src/pages/TermPlanner/TermPlanner.tsx b/frontend/src/pages/TermPlanner/TermPlanner.tsx index 6390a2744..3ca9b3e13 100644 --- a/frontend/src/pages/TermPlanner/TermPlanner.tsx +++ b/frontend/src/pages/TermPlanner/TermPlanner.tsx @@ -20,8 +20,7 @@ import { unscheduleCourse, validateTermPlanner } from 'utils/api/plannerApi'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; -import useUserPlanner from 'utils/apiHooks/useUserPlanner'; +import { useUserCourses, useUserPlanner } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import PageTemplate from 'components/PageTemplate'; import Spinner from 'components/Spinner'; diff --git a/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx b/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx index e08bc110d..f4a06a61b 100644 --- a/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx +++ b/frontend/src/pages/TermPlanner/UnplannedColumn/UnplannedColumn.tsx @@ -7,8 +7,7 @@ import { PlannerResponse, ValidateResponse } from 'types/userResponse'; -import useUserCourses from 'utils/apiHooks/useUserCourses'; -import useUserPlanner from 'utils/apiHooks/useUserPlanner'; +import { useUserCourses, useUserPlanner } from 'utils/apiHooks/user'; import Spinner from 'components/Spinner'; import useMediaQuery from 'hooks/useMediaQuery'; import DraggableCourse from '../DraggableCourse'; diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts new file mode 100644 index 000000000..2b1d131ca --- /dev/null +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -0,0 +1,5 @@ +import useUserCourses from './useUserCourses'; +import useUserDegree from './useUserDegree'; +import useUserPlanner from './useUserPlanner'; + +export { useUserCourses, useUserDegree, useUserPlanner }; diff --git a/frontend/src/utils/apiHooks/useUserCourses.tsx b/frontend/src/utils/apiHooks/user/useUserCourses.tsx similarity index 100% rename from frontend/src/utils/apiHooks/useUserCourses.tsx rename to frontend/src/utils/apiHooks/user/useUserCourses.tsx diff --git a/frontend/src/utils/apiHooks/useUserDegree.tsx b/frontend/src/utils/apiHooks/user/useUserDegree.tsx similarity index 100% rename from frontend/src/utils/apiHooks/useUserDegree.tsx rename to frontend/src/utils/apiHooks/user/useUserDegree.tsx diff --git a/frontend/src/utils/apiHooks/useUserPlanner.tsx b/frontend/src/utils/apiHooks/user/useUserPlanner.tsx similarity index 100% rename from frontend/src/utils/apiHooks/useUserPlanner.tsx rename to frontend/src/utils/apiHooks/user/useUserPlanner.tsx From 50338a9023ba1e782fa4c65c4e131ebfef68d010 Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:09:53 +1100 Subject: [PATCH 03/13] wip: doing removeCourseMutation --- .../CourseCartCard/CourseCartCard.tsx | 21 +++---------------- frontend/src/utils/apiHooks/user/index.ts | 3 ++- .../apiHooks/user/useRemoveCourseMutation.tsx | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx diff --git a/frontend/src/components/CourseCartCard/CourseCartCard.tsx b/frontend/src/components/CourseCartCard/CourseCartCard.tsx index 2c06152b2..bd9fdb966 100644 --- a/frontend/src/components/CourseCartCard/CourseCartCard.tsx +++ b/frontend/src/components/CourseCartCard/CourseCartCard.tsx @@ -2,10 +2,8 @@ import React from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { DeleteOutlined } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button, Popconfirm, Tooltip, Typography } from 'antd'; -import { removeCourse } from 'utils/api/plannerApi'; -import useToken from 'hooks/useToken'; +import { useRemoveCourseMutation } from 'utils/apiHooks/user'; import { addTab } from 'reducers/courseTabsSlice'; import S from './styles'; @@ -17,29 +15,16 @@ type Props = { }; const CourseCartCard = ({ code, title }: Props) => { - const token = useToken(); - const dispatch = useDispatch(); const navigate = useNavigate(); - const queryClient = useQueryClient(); + + const remove = useRemoveCourseMutation(); const handleClick = () => { navigate('/course-selector'); dispatch(addTab(code)); }; - const remove = useMutation({ - mutationFn: (courseCode: string) => removeCourse(token, courseCode), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - } - }); - return (
diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index 2b1d131ca..f2223fa1b 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -1,5 +1,6 @@ +import useRemoveCourseMutation from './useRemoveCourseMutation'; import useUserCourses from './useUserCourses'; import useUserDegree from './useUserDegree'; import useUserPlanner from './useUserPlanner'; -export { useUserCourses, useUserDegree, useUserPlanner }; +export { useRemoveCourseMutation, useUserCourses, useUserDegree, useUserPlanner }; diff --git a/frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx b/frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx new file mode 100644 index 000000000..b5be41e3b --- /dev/null +++ b/frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { removeCourse } from 'utils/api/plannerApi'; +import useToken from 'hooks/useToken'; + +const useRemoveCourseMutation = () => { + const token = useToken(); + + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (courseCode: string) => removeCourse(token, courseCode), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['user', 'planner'] + }); + } + }); + + return mutation; +}; + +export default useRemoveCourseMutation; From ebd7df1824fb44d9101c6644bac9b8a0ad40dbad Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:07:30 +1100 Subject: [PATCH 04/13] wip: refactored hook creation and added addToUnplanned and removeCourse --- .../CourseSearchBar/CourseSearchBar.tsx | 23 ++--- .../PlannerButton/PlannerButton.tsx | 26 +----- .../QuickAddCartButton/QuickAddCartButton.tsx | 21 +---- .../CourseSelector/CourseMenu/CourseMenu.tsx | 29 +++--- .../TermPlanner/ContextMenu/ContextMenu.tsx | 20 ++--- frontend/src/utils/apiHooks/keys.md | 5 +- .../src/utils/apiHooks/user/hookHelpers.ts | 88 +++++++++++++++++++ frontend/src/utils/apiHooks/user/index.ts | 14 +-- frontend/src/utils/apiHooks/user/mutations.ts | 6 ++ frontend/src/utils/apiHooks/user/queries.ts | 8 ++ .../apiHooks/user/useRemoveCourseMutation.tsx | 21 ----- .../utils/apiHooks/user/useUserCourses.tsx | 17 ---- .../src/utils/apiHooks/user/useUserDegree.tsx | 17 ---- .../utils/apiHooks/user/useUserPlanner.tsx | 17 ---- 14 files changed, 151 insertions(+), 161 deletions(-) create mode 100644 frontend/src/utils/apiHooks/user/hookHelpers.ts create mode 100644 frontend/src/utils/apiHooks/user/mutations.ts create mode 100644 frontend/src/utils/apiHooks/user/queries.ts delete mode 100644 frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx delete mode 100644 frontend/src/utils/apiHooks/user/useUserCourses.tsx delete mode 100644 frontend/src/utils/apiHooks/user/useUserDegree.tsx delete mode 100644 frontend/src/utils/apiHooks/user/useUserPlanner.tsx diff --git a/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx b/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx index 60b955b45..b40b20028 100644 --- a/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx +++ b/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useState } from 'react'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Flex, Select, Spin, Typography } from 'antd'; import { SearchCourse } from 'types/api'; import { CoursesResponse } from 'types/userResponse'; import { useDebounce } from 'use-debounce'; import { searchCourse } from 'utils/api/coursesApi'; -import { addToUnplanned, removeCourse } from 'utils/api/plannerApi'; +import { useAddToUnplannedMutation, useRemoveCourseMutation } from 'utils/apiHooks/user'; import QuickAddCartButton from 'components/QuickAddCartButton'; import useToken from 'hooks/useToken'; @@ -49,17 +48,13 @@ const CourseSearchBar = ({ onSelectCallback, style, userCourses }: CourseSearchB const isInPlanner = (courseCode: string) => userCourses?.[courseCode] !== undefined; - const queryClient = useQueryClient(); - const courseMutation = useMutation({ - mutationFn: async (courseId: string) => { - const handleMutation = isInPlanner(courseId) ? removeCourse : addToUnplanned; - await handleMutation(token, courseId); - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['courses'] }); - await queryClient.invalidateQueries({ queryKey: ['planner'] }); - } - }); + const removeCourseMutation = useRemoveCourseMutation(); + const addToUnplannedMutation = useAddToUnplannedMutation(); + + const courseMutation = (courseId: string) => + isInPlanner(courseId) + ? removeCourseMutation.mutate(courseId) + : addToUnplannedMutation.mutate(courseId); const courses = Object.entries(searchResults).map(([courseCode, courseTitle]) => ({ label: ( @@ -67,7 +62,7 @@ const CourseSearchBar = ({ onSelectCallback, style, userCourses }: CourseSearchB courseCode={courseCode} courseTitle={courseTitle} isPlanned={isInPlanner(courseCode)} - runMutate={courseMutation.mutate} + runMutate={courseMutation} /> ), value: courseCode diff --git a/frontend/src/components/PlannerButton/PlannerButton.tsx b/frontend/src/components/PlannerButton/PlannerButton.tsx index 337e64e29..3d856cc80 100644 --- a/frontend/src/components/PlannerButton/PlannerButton.tsx +++ b/frontend/src/components/PlannerButton/PlannerButton.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { PlusOutlined, StopOutlined } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from 'antd'; import { Course } from 'types/api'; -import { addToUnplanned, removeCourse } from 'utils/api/plannerApi'; -import useToken from 'hooks/useToken'; +import { useAddToUnplannedMutation, useRemoveCourseMutation } from 'utils/apiHooks/user'; import S from './styles'; interface PlannerButtonProps { @@ -13,26 +11,10 @@ interface PlannerButtonProps { } const PlannerButton = ({ course, isAddedInPlanner }: PlannerButtonProps) => { - const token = useToken(); + const removeCourseMutation = useRemoveCourseMutation(); + const addToUnplannedMutation = useAddToUnplannedMutation(); - const handleMutation = isAddedInPlanner - ? (code: string) => removeCourse(token, code) - : (code: string) => addToUnplanned(token, code); - const queryClient = useQueryClient(); - const mutation = useMutation({ - mutationFn: handleMutation, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - } - }); + const mutation = isAddedInPlanner ? removeCourseMutation : addToUnplannedMutation; const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); diff --git a/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx b/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx index d74a5f1c1..3bd6e9cdb 100644 --- a/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx +++ b/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { MinusOutlined, PlusOutlined } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Tooltip } from 'antd'; -import { addToUnplanned, removeCourse } from 'utils/api/plannerApi'; +import { useAddToUnplannedMutation, useRemoveCourseMutation } from 'utils/apiHooks/user'; import useSettings from 'hooks/useSettings'; -import useToken from 'hooks/useToken'; import S from './styles'; type Props = { @@ -14,26 +12,15 @@ type Props = { }; const QuickAddCartButton = ({ courseCode, runMutate, planned }: Props) => { - const token = useToken(); + const removeCourseMutation = useRemoveCourseMutation(); + const addToUnplannedMutation = useAddToUnplannedMutation(); - const handleMutation = planned - ? (code: string) => removeCourse(token, code) - : (code: string) => addToUnplanned(token, code); + const mutation = planned ? removeCourseMutation : addToUnplannedMutation; const { theme } = useSettings(); const iconStyles = { color: theme === 'light' ? '#000' : '#fff' }; - const queryClient = useQueryClient(); - const mutation = useMutation({ - mutationFn: handleMutation, - onMutate: () => planned, - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - } - }); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); diff --git a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx index 46311f187..09cb67b10 100644 --- a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx +++ b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx @@ -1,14 +1,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import type { MenuProps } from 'antd'; import { CourseUnitsStructure, MenuDataStructure, MenuDataSubgroup } from 'types/courseMenu'; import { CourseValidation } from 'types/courses'; import { ProgramStructure } from 'types/structure'; import { CoursesResponse, DegreeResponse } from 'types/userResponse'; import { getAllUnlockedCourses } from 'utils/api/coursesApi'; -import { addToUnplanned, removeCourse } from 'utils/api/plannerApi'; import { getProgramStructure } from 'utils/api/programsApi'; +import { useAddToUnplannedMutation, useRemoveCourseMutation } from 'utils/apiHooks/user'; import { LoadingCourseMenu } from 'components/LoadingSkeleton'; import { MAX_COURSES_OVERFLOW } from 'config/constants'; import useSettings from 'hooks/useSettings'; @@ -40,8 +40,6 @@ const SubgroupTitle = ({ title, currUOC, totalUOC }: SubgroupTitleProps) => ( const CourseMenu = ({ courses, degree }: CourseMenuProps) => { const token = useToken(); - const inPlanner = (courseId: string) => courses && !!courses[courseId]; - const structureQuery = useQuery({ queryKey: ['structure', degree], queryFn: () => getProgramStructure(degree!.programCode, degree!.specs), @@ -53,15 +51,16 @@ const CourseMenu = ({ courses, degree }: CourseMenuProps) => { queryFn: () => getAllUnlockedCourses(token) }); - const queryClient = useQueryClient(); - const courseMutation = useMutation({ - mutationFn: async (courseId: string) => - inPlanner(courseId) ? removeCourse(token, courseId) : addToUnplanned(token, courseId), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['courses'] }); - await queryClient.invalidateQueries({ queryKey: ['planner'] }); - } - }); + const removeCourseMutation = useRemoveCourseMutation(); + const addToUnplannedMutation = useAddToUnplannedMutation(); + const removeCourse = removeCourseMutation.mutate; + const addToUnplanned = addToUnplannedMutation.mutate; + + const courseMutation = useCallback( + (courseId: string) => + courses && !!courses[courseId] ? removeCourse(courseId) : addToUnplanned(courseId), + [courses, removeCourse, addToUnplanned] + ); const dispatch = useDispatch(); const [menuData, setMenuData] = useState({}); @@ -179,7 +178,7 @@ const CourseMenu = ({ courses, degree }: CourseMenuProps) => { courseCode={course.courseCode} title={course.title} selected={courses[course.courseCode] !== undefined} - runMutate={courseMutation.mutate} + runMutate={courseMutation} accurate={course.accuracy} unlocked={course.unlocked} /> @@ -200,7 +199,7 @@ const CourseMenu = ({ courses, degree }: CourseMenuProps) => { defaultOpenKeys, menuData, pageLoaded, - courseMutation.mutate, + courseMutation, showLockedCourses, structureQuery.data, structureQuery.isSuccess diff --git a/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx b/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx index bfeb9ad7c..7ededd3dd 100644 --- a/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx +++ b/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx @@ -13,7 +13,8 @@ import { } from '@ant-design/icons'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { UnscheduleCourse } from 'types/planner'; -import { removeCourse, toggleIgnoreFromProgression, unscheduleCourse } from 'utils/api/plannerApi'; +import { toggleIgnoreFromProgression, unscheduleCourse } from 'utils/api/plannerApi'; +import { useRemoveCourseMutation } from 'utils/apiHooks/user'; import EditMarkModal from 'components/EditMarkModal'; import useToken from 'hooks/useToken'; import { addTab } from 'reducers/courseTabsSlice'; @@ -48,20 +49,9 @@ const ContextMenu = ({ code, plannedFor, ignoreFromProgression }: Props) => { }); } }); - const handleDelete = useMutation({ - mutationFn: (courseCode: string) => removeCourse(token, courseCode), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - } - }); + + const handleDelete = useRemoveCourseMutation(); + const handleInfo = () => { navigate('/course-selector'); dispatch(addTab(code)); diff --git a/frontend/src/utils/apiHooks/keys.md b/frontend/src/utils/apiHooks/keys.md index 4f4d30953..9b93f33ee 100644 --- a/frontend/src/utils/apiHooks/keys.md +++ b/frontend/src/utils/apiHooks/keys.md @@ -1,3 +1,6 @@ - `getUserPlanner`: user, , planner - `getUserCourses`: user, , planner, courses -- `getUserDegree`: user, , degree \ No newline at end of file +- `getUserDegree`: user, , degree + + +- put validations on planner so removeCourse does this \ No newline at end of file diff --git a/frontend/src/utils/apiHooks/user/hookHelpers.ts b/frontend/src/utils/apiHooks/user/hookHelpers.ts new file mode 100644 index 000000000..dfe23730b --- /dev/null +++ b/frontend/src/utils/apiHooks/user/hookHelpers.ts @@ -0,0 +1,88 @@ +import { + useMutation, + UseMutationOptions, + useQuery, + useQueryClient, + UseQueryOptions +} from '@tanstack/react-query'; +import useIdentity from 'hooks/useIdentity'; + +export type CreateUserQueryOptions = Omit; +export type UserQueryHookOptions = { + allowUnsetToken?: boolean; + queryOptions?: Omit; +}; + +export function createUserQueryHook< + const Key extends string[], + const FArgs extends unknown[], + const FRet +>( + keySuffix: Key, + fn: (token: string, ...args: FArgs) => Promise, + baseOptions?: CreateUserQueryOptions +) { + return (options?: UserQueryHookOptions, ...args: FArgs) => { + const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; + + const query = useQuery({ + queryKey: ['user', userId].concat(keySuffix), + queryFn: () => fn(token!, ...args), + + // ...baseOptions, // TODO-olli + // ...options?.queryOptions, + + enabled: options?.queryOptions?.enabled && token !== undefined + }); + + return query; + }; +} + +export type CreateUserMutationOptions = Omit< + UseMutationOptions, + 'mutationFn' +>; +export type UserMutationHookOptions = { + allowUnsetToken?: boolean; + mutationOptions?: Omit, 'mutationFn'>; +}; + +export function createUserMutationHook( + invalidationKeySuffixes: Keys, + fn: (token: string, data: FArg) => Promise, + baseOptions?: CreateUserMutationOptions +) { + return (options?: UserMutationHookOptions) => { + const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; + + // const lol: MutationFunction = async (data: FData) => + // token !== undefined ? fn(token, data) : Promise.reject(new Error('No token')); + // const lol = (data: FData) => fn(token!, data); + + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: async (data: FArg) => + token !== undefined ? fn(token, data) : Promise.reject(new Error('No token')), + + ...baseOptions, + ...options?.mutationOptions, + + onSuccess: (data, variables, context) => { + invalidationKeySuffixes.forEach((key) => { + queryClient.invalidateQueries({ + queryKey: ['user', userId].concat(key) + }); + }); + + if (options?.mutationOptions?.onSuccess !== undefined) { + options.mutationOptions.onSuccess(data, variables, context); + } else { + baseOptions?.onSuccess?.(data, variables, context); + } + } + }); + + return mutation; + }; +} diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index f2223fa1b..d8f23e877 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -1,6 +1,10 @@ -import useRemoveCourseMutation from './useRemoveCourseMutation'; -import useUserCourses from './useUserCourses'; -import useUserDegree from './useUserDegree'; -import useUserPlanner from './useUserPlanner'; +import { useAddToUnplannedMutation, useRemoveCourseMutation } from './mutations'; +import { useUserCourses, useUserDegree, useUserPlanner } from './queries'; -export { useRemoveCourseMutation, useUserCourses, useUserDegree, useUserPlanner }; +export { + useAddToUnplannedMutation, + useRemoveCourseMutation, + useUserCourses, + useUserDegree, + useUserPlanner +}; diff --git a/frontend/src/utils/apiHooks/user/mutations.ts b/frontend/src/utils/apiHooks/user/mutations.ts new file mode 100644 index 000000000..7ad88afc2 --- /dev/null +++ b/frontend/src/utils/apiHooks/user/mutations.ts @@ -0,0 +1,6 @@ +import { addToUnplanned, removeCourse } from 'utils/api/plannerApi'; +import { createUserMutationHook } from './hookHelpers'; + +export const useRemoveCourseMutation = createUserMutationHook([['planner']], removeCourse); + +export const useAddToUnplannedMutation = createUserMutationHook([['planner']], addToUnplanned); diff --git a/frontend/src/utils/apiHooks/user/queries.ts b/frontend/src/utils/apiHooks/user/queries.ts new file mode 100644 index 000000000..099dad8c3 --- /dev/null +++ b/frontend/src/utils/apiHooks/user/queries.ts @@ -0,0 +1,8 @@ +import { getUserCourses, getUserDegree, getUserPlanner } from 'utils/api/userApi'; +import { createUserQueryHook } from './hookHelpers'; + +export const useUserCourses = createUserQueryHook(['planner', 'courses'], getUserCourses); + +export const useUserPlanner = createUserQueryHook(['planner'], getUserPlanner); + +export const useUserDegree = createUserQueryHook(['degree'], getUserDegree); diff --git a/frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx b/frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx deleted file mode 100644 index b5be41e3b..000000000 --- a/frontend/src/utils/apiHooks/user/useRemoveCourseMutation.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { removeCourse } from 'utils/api/plannerApi'; -import useToken from 'hooks/useToken'; - -const useRemoveCourseMutation = () => { - const token = useToken(); - - const queryClient = useQueryClient(); - const mutation = useMutation({ - mutationFn: (courseCode: string) => removeCourse(token, courseCode), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['user', 'planner'] - }); - } - }); - - return mutation; -}; - -export default useRemoveCourseMutation; diff --git a/frontend/src/utils/apiHooks/user/useUserCourses.tsx b/frontend/src/utils/apiHooks/user/useUserCourses.tsx deleted file mode 100644 index d3fc42630..000000000 --- a/frontend/src/utils/apiHooks/user/useUserCourses.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getUserCourses } from 'utils/api/userApi'; -import useIdentity from 'hooks/useIdentity'; - -const useUserCourses = (allowTokenUnset?: boolean) => { - const { userId, token } = useIdentity(allowTokenUnset === true) ?? {}; - - const coursesQuery = useQuery({ - queryKey: ['user', userId!, 'planner', 'courses'], - queryFn: () => getUserCourses(token!), - enabled: token !== undefined - }); - - return coursesQuery; -}; - -export default useUserCourses; diff --git a/frontend/src/utils/apiHooks/user/useUserDegree.tsx b/frontend/src/utils/apiHooks/user/useUserDegree.tsx deleted file mode 100644 index e56c1ea7c..000000000 --- a/frontend/src/utils/apiHooks/user/useUserDegree.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getUserDegree } from 'utils/api/userApi'; -import useIdentity from 'hooks/useIdentity'; - -const useUserDegree = (allowTokenUnset?: boolean) => { - const { userId, token } = useIdentity(allowTokenUnset === true) ?? {}; - - const degreeQuery = useQuery({ - queryKey: ['user', userId!, 'degree'], - queryFn: () => getUserDegree(token!), - enabled: token !== undefined - }); - - return degreeQuery; -}; - -export default useUserDegree; diff --git a/frontend/src/utils/apiHooks/user/useUserPlanner.tsx b/frontend/src/utils/apiHooks/user/useUserPlanner.tsx deleted file mode 100644 index 6b50c4fb1..000000000 --- a/frontend/src/utils/apiHooks/user/useUserPlanner.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getUserPlanner } from 'utils/api/userApi'; -import useIdentity from 'hooks/useIdentity'; - -const useUserPlanner = (allowTokenUnset?: boolean) => { - const { userId, token } = useIdentity(allowTokenUnset === true) ?? {}; - - const degreeQuery = useQuery({ - queryKey: ['user', userId!, 'planner'], - queryFn: () => getUserPlanner(token!), - enabled: token !== undefined - }); - - return degreeQuery; -}; - -export default useUserPlanner; From 59dfca49a8e7523ef1df726ad732cde62a0e539b Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:22:41 +1100 Subject: [PATCH 05/13] feat: updateMark, removeAllCourses, and resetUser hooks... also fixes type no-arg mutations --- .../EditMarkModal/EditMarkModal.tsx | 25 ++----------------- .../components/PlannerCart/PlannerCart.tsx | 11 ++------ .../src/components/ResetModal/ResetModal.tsx | 20 +++------------ .../src/pages/DegreeWizard/DegreeWizard.tsx | 23 +++-------------- .../src/utils/apiHooks/user/hookHelpers.ts | 25 +++++++++++++------ frontend/src/utils/apiHooks/user/index.ts | 9 ++++++- frontend/src/utils/apiHooks/user/mutations.ts | 15 ++++++++++- 7 files changed, 50 insertions(+), 78 deletions(-) diff --git a/frontend/src/components/EditMarkModal/EditMarkModal.tsx b/frontend/src/components/EditMarkModal/EditMarkModal.tsx index b7ba16d20..ef2167bb3 100644 --- a/frontend/src/components/EditMarkModal/EditMarkModal.tsx +++ b/frontend/src/components/EditMarkModal/EditMarkModal.tsx @@ -1,10 +1,7 @@ import React, { useState } from 'react'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button, message } from 'antd'; -import { CourseMark } from 'types/api'; import { Grade } from 'types/planner'; -import { updateCourseMark } from 'utils/api/plannerApi'; -import useToken from 'hooks/useToken'; +import { useUpdateMarkMutation } from 'utils/apiHooks/user'; import S from './styles'; type Props = { @@ -14,9 +11,7 @@ type Props = { }; const EditMarkModal = ({ code, open, onCancel }: Props) => { - const queryClient = useQueryClient(); const [markValue, setMarkValue] = useState(); - const token = useToken(); const letterGrades: Grade[] = ['SY', 'FL', 'PS', 'CR', 'DN', 'HD']; @@ -26,23 +21,7 @@ const EditMarkModal = ({ code, open, onCancel }: Props) => { setMarkValue(value); }; - const updateMarkMutation = useMutation({ - mutationFn: (courseMark: CourseMark) => updateCourseMark(token, courseMark), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - onCancel(); - message.success('Mark Updated'); - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at updateMarkMutation:', err); - } - }); + const updateMarkMutation = useUpdateMarkMutation(); const handleUpdateMark = () => { if (!Number.isNaN(parseInt(markValue as string, 10))) { diff --git a/frontend/src/components/PlannerCart/PlannerCart.tsx b/frontend/src/components/PlannerCart/PlannerCart.tsx index b58e3a19f..2399dadf1 100644 --- a/frontend/src/components/PlannerCart/PlannerCart.tsx +++ b/frontend/src/components/PlannerCart/PlannerCart.tsx @@ -1,13 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { CalendarOutlined, DeleteOutlined } from '@ant-design/icons'; -import { useMutation } from '@tanstack/react-query'; import { Button, Tooltip, Typography } from 'antd'; import { badCourses } from 'types/userResponse'; -import { removeAll } from 'utils/api/plannerApi'; -import { useUserCourses } from 'utils/apiHooks/user'; +import { useRemoveAllCoursesMutation, useUserCourses } from 'utils/apiHooks/user'; import CourseCartCard from 'components/CourseCartCard'; -import useToken from 'hooks/useToken'; import S from './styles'; const { Text, Title } = Typography; @@ -15,14 +12,10 @@ const { Text, Title } = Typography; const PlannerCart = () => { const navigate = useNavigate(); const [showMenu, setShowMenu] = useState(false); - const token = useToken(); const courses = useUserCourses().data ?? badCourses; - const removeAllCourses = useMutation({ - mutationKey: ['removeCourses'], - mutationFn: () => removeAll(token) - }); + const removeAllCourses = useRemoveAllCoursesMutation(); const pathname = useLocation(); diff --git a/frontend/src/components/ResetModal/ResetModal.tsx b/frontend/src/components/ResetModal/ResetModal.tsx index 088c930b6..7aad89088 100644 --- a/frontend/src/components/ResetModal/ResetModal.tsx +++ b/frontend/src/components/ResetModal/ResetModal.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Modal } from 'antd'; -import { resetUserDegree } from 'utils/api/userApi'; +import { useResetDegreeMutation } from 'utils/apiHooks/user/mutations'; import { useAppDispatch } from 'hooks'; import useToken from 'hooks/useToken'; import { resetTabs } from 'reducers/courseTabsSlice'; @@ -14,26 +13,13 @@ type Props = { // has no internal "open" state since it becomes difficult to juggle with external buttons const ResetModal = ({ open, onOk, onCancel }: Props) => { - const queryClient = useQueryClient(); const dispatch = useAppDispatch(); const token = useToken({ allowUnset: true }); // NOTE: must allow unset since this is used in ErrorBoundary itself - const resetDegreeMutation = useMutation({ - mutationFn: (definedToken: string) => resetUserDegree(definedToken), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['degree'] - }); - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at resetDegreeMutation: ', err); - } - }); - + const resetDegreeMutation = useResetDegreeMutation({ allowUnsetToken: true }); const handleResetDegree = () => { if (token !== undefined) { - resetDegreeMutation.mutate(token); + resetDegreeMutation.mutate(); } }; diff --git a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx index 530ddc94c..ffae3fc61 100644 --- a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx +++ b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { scroller } from 'react-scroll'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { Button, Typography } from 'antd'; import { DegreeWizardPayload } from 'types/degreeWizard'; import { getSpecialisationTypes } from 'utils/api/specsApi'; -import { getUserIsSetup, resetUserDegree } from 'utils/api/userApi'; +import { getUserIsSetup } from 'utils/api/userApi'; +import { useResetDegreeMutation } from 'utils/apiHooks/user/mutations'; import openNotification from 'utils/openNotification'; import MigrationModal from 'components/MigrationModal'; import PageTemplate from 'components/PageTemplate'; @@ -49,23 +50,7 @@ const DegreeWizard = () => { const specs = specTypesQuery.data ?? DEFAULT_SPEC_TYPES; const stepList = ['year', 'degree'].concat(specs).concat(['start browsing']); - const queryClient = useQueryClient(); - - const resetDegree = useMutation({ - mutationFn: () => resetUserDegree(token), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['degree'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.clear(); - } - }); + const resetDegree = useResetDegreeMutation(); useEffect(() => { openNotification({ diff --git a/frontend/src/utils/apiHooks/user/hookHelpers.ts b/frontend/src/utils/apiHooks/user/hookHelpers.ts index dfe23730b..5c6b10b7e 100644 --- a/frontend/src/utils/apiHooks/user/hookHelpers.ts +++ b/frontend/src/utils/apiHooks/user/hookHelpers.ts @@ -1,6 +1,7 @@ import { useMutation, UseMutationOptions, + UseMutationResult, useQuery, useQueryClient, UseQueryOptions @@ -48,7 +49,11 @@ export type UserMutationHookOptions = { mutationOptions?: Omit, 'mutationFn'>; }; -export function createUserMutationHook( +export function createUserMutationHook< + const Keys extends true | string[][], // true signifies you want a full invalidate and clear + const FRet, + const FArg = void +>( invalidationKeySuffixes: Keys, fn: (token: string, data: FArg) => Promise, baseOptions?: CreateUserMutationOptions @@ -56,10 +61,6 @@ export function createUserMutationHook) => { const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; - // const lol: MutationFunction = async (data: FData) => - // token !== undefined ? fn(token, data) : Promise.reject(new Error('No token')); - // const lol = (data: FData) => fn(token!, data); - const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: async (data: FArg) => @@ -69,11 +70,19 @@ export function createUserMutationHook { - invalidationKeySuffixes.forEach((key) => { + if (invalidationKeySuffixes === true) { queryClient.invalidateQueries({ - queryKey: ['user', userId].concat(key) + queryKey: ['user', userId] }); - }); + + queryClient.clear(); + } else { + invalidationKeySuffixes.forEach((key) => + queryClient.invalidateQueries({ + queryKey: ['user', userId].concat(key) + }) + ); + } if (options?.mutationOptions?.onSuccess !== undefined) { options.mutationOptions.onSuccess(data, variables, context); diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index d8f23e877..3e4d78957 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -1,9 +1,16 @@ -import { useAddToUnplannedMutation, useRemoveCourseMutation } from './mutations'; +import { + useAddToUnplannedMutation, + useRemoveAllCoursesMutation, + useRemoveCourseMutation, + useUpdateMarkMutation +} from './mutations'; import { useUserCourses, useUserDegree, useUserPlanner } from './queries'; export { useAddToUnplannedMutation, + useRemoveAllCoursesMutation, useRemoveCourseMutation, + useUpdateMarkMutation, useUserCourses, useUserDegree, useUserPlanner diff --git a/frontend/src/utils/apiHooks/user/mutations.ts b/frontend/src/utils/apiHooks/user/mutations.ts index 7ad88afc2..fefae0bad 100644 --- a/frontend/src/utils/apiHooks/user/mutations.ts +++ b/frontend/src/utils/apiHooks/user/mutations.ts @@ -1,6 +1,19 @@ -import { addToUnplanned, removeCourse } from 'utils/api/plannerApi'; +import { addToUnplanned, removeAll, removeCourse, updateCourseMark } from 'utils/api/plannerApi'; +import { resetUserDegree } from 'utils/api/userApi'; import { createUserMutationHook } from './hookHelpers'; export const useRemoveCourseMutation = createUserMutationHook([['planner']], removeCourse); export const useAddToUnplannedMutation = createUserMutationHook([['planner']], addToUnplanned); + +export const useUpdateMarkMutation = createUserMutationHook( + [ + ['planner', 'courses'], + ['planner', 'validation'] + ], + updateCourseMark +); + +export const useRemoveAllCoursesMutation = createUserMutationHook([['planner']], removeAll); + +export const useResetDegreeMutation = createUserMutationHook(true, resetUserDegree); From daa811831047545df660d92669fa9408e609631a Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:02:12 +1100 Subject: [PATCH 06/13] feat: toggleLockedTerm, toggleSummer, updateStart and updateLength --- .../src/components/ResetModal/ResetModal.tsx | 2 +- .../TermPlanner/SettingsMenu/SettingsMenu.tsx | 74 ++----------------- .../src/pages/TermPlanner/TermBox/TermBox.tsx | 22 +----- frontend/src/utils/api/plannerApi.ts | 6 +- frontend/src/utils/apiHooks/user/index.ts | 12 ++- frontend/src/utils/apiHooks/user/mutations.ts | 26 ++++++- 6 files changed, 51 insertions(+), 91 deletions(-) diff --git a/frontend/src/components/ResetModal/ResetModal.tsx b/frontend/src/components/ResetModal/ResetModal.tsx index 7aad89088..a1f51937d 100644 --- a/frontend/src/components/ResetModal/ResetModal.tsx +++ b/frontend/src/components/ResetModal/ResetModal.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Modal } from 'antd'; -import { useResetDegreeMutation } from 'utils/apiHooks/user/mutations'; +import { useResetDegreeMutation } from 'utils/apiHooks/user'; import { useAppDispatch } from 'hooks'; import useToken from 'hooks/useToken'; import { resetTabs } from 'reducers/courseTabsSlice'; diff --git a/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx b/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx index de6f9bcfd..19008af2e 100644 --- a/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx +++ b/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx @@ -1,14 +1,15 @@ import React, { Suspense } from 'react'; import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DatePicker, Modal, Select, Switch } from 'antd'; import dayjs from 'dayjs'; import { PlannerResponse } from 'types/userResponse'; -import { toggleSummerTerm, updateDegreeLength, updateStartYear } from 'utils/api/userApi'; -import openNotification from 'utils/openNotification'; +import { + useToggleSummerTermMutation, + useUpdateDegreeLengthMutation, + useUpdateStartYearMutation +} from 'utils/apiHooks/user'; import Spinner from 'components/Spinner'; import useSettings from 'hooks/useSettings'; -import useToken from 'hooks/useToken'; import CS from '../common/styles'; type Props = { @@ -16,11 +17,8 @@ type Props = { }; const SettingsMenu = ({ planner }: Props) => { - const queryClient = useQueryClient(); - const { Option } = Select; const { theme } = useSettings(); - const token = useToken(); function willUnplanCourses(numYears: number) { if (!planner) return false; @@ -34,21 +32,7 @@ const SettingsMenu = ({ planner }: Props) => { return false; } - const updateStartYearMutation = useMutation({ - mutationFn: (year: string) => updateStartYear(token, year), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - }, - onError: () => { - openNotification({ - type: 'error', - message: 'Error setting degree start year', - description: 'There was an error updating the degree start year.' - }); - } - }); + const updateStartYearMutation = useUpdateStartYearMutation(); const handleUpdateStartYear = async (_: unknown, dateString: string | string[]) => { if (dateString && typeof dateString === 'string') { @@ -59,24 +43,7 @@ const SettingsMenu = ({ planner }: Props) => { } }; - const updateDegreeLengthMutation = useMutation({ - mutationFn: (numYears: number) => updateDegreeLength(token, numYears), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['settings'] - }); - }, - onError: () => { - openNotification({ - type: 'error', - message: 'Error setting degree length', - description: 'There was an error updating the degree length.' - }); - } - }); + const updateDegreeLengthMutation = useUpdateDegreeLengthMutation(); const handleUpdateDegreeLength = async (value: number) => { if (willUnplanCourses(value)) { @@ -91,32 +58,7 @@ const SettingsMenu = ({ planner }: Props) => { } }; - const summerToggleMutation = useMutation({ - mutationFn: () => toggleSummerTerm(token), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - - if (planner && planner.isSummerEnabled) { - openNotification({ - type: 'info', - message: 'Your summer term courses have been unplanned', - description: - 'Courses that were planned during summer terms have been unplanned including courses that have been planned across different terms.' - }); - } - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at summerToggleMutationMutation: ', err); - openNotification({ - type: 'error', - message: 'Error setting summer term', - description: 'An error occurred when toggling the summer term.' - }); - } - }); + const summerToggleMutation = useToggleSummerTermMutation(); const handleSummerToggle = async () => { summerToggleMutation.mutate(); diff --git a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx index 344a17fec..7daca6af3 100644 --- a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx +++ b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx @@ -1,18 +1,15 @@ import React, { Suspense } from 'react'; import { LockFilled, UnlockFilled } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Badge } from 'antd'; import { useTheme } from 'styled-components'; import { Course } from 'types/api'; import { CourseTime } from 'types/courses'; import { Term } from 'types/planner'; import { ValidateResponse } from 'types/userResponse'; -import { toggleLockTerm } from 'utils/api/plannerApi'; -import { useUserCourses, useUserPlanner } from 'utils/apiHooks/user'; +import { useToggleLockTermMutation, useUserCourses, useUserPlanner } from 'utils/apiHooks/user'; import { courseHasOffering } from 'utils/getAllCourseOfferings'; import Spinner from 'components/Spinner'; import useMediaQuery from 'hooks/useMediaQuery'; -import useToken from 'hooks/useToken'; import DraggableCourse from '../DraggableCourse'; import S from './styles'; @@ -37,26 +34,13 @@ const TermBox = ({ termCourseCodes, draggingCourseCode }: Props) => { - const token = useToken(); const year = name.slice(0, 4); const term = name.match(/T[0-3]/)?.[0] as Term; const theme = useTheme(); - const queryClient = useQueryClient(); const plannerQuery = useUserPlanner(); - const toggleLockTermMutation = useMutation({ - mutationFn: () => toggleLockTerm(token, year, term), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at toggleLockTermMutation: ', err); - } - }); + const toggleLockTermMutation = useToggleLockTermMutation(); const coursesQuery = useUserCourses(); const isSmall = useMediaQuery('(max-width: 1400px)'); @@ -69,7 +53,7 @@ const TermBox = ({ const courses = coursesQuery.data; const handleToggleLockTerm = async () => { - toggleLockTermMutation.mutate(); + toggleLockTermMutation.mutate({ term, year }); }; const termUOC = termCourseCodes.reduce((acc, code) => acc + termCourseInfos[code].UOC, 0); diff --git a/frontend/src/utils/api/plannerApi.ts b/frontend/src/utils/api/plannerApi.ts index 94d3eba03..e1f37e56f 100644 --- a/frontend/src/utils/api/plannerApi.ts +++ b/frontend/src/utils/api/plannerApi.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { CourseMark } from 'types/api'; +import { CourseTime } from 'types/courses'; import { PlannedToTerm, UnPlannedToTerm, UnscheduleCourse } from 'types/planner'; import { ValidatesResponse } from 'types/userResponse'; import { withAuthorization } from './authApi'; @@ -101,10 +102,11 @@ export const updateCourseMark = async (token: string, courseMark: CourseMark) => } }; -export const toggleLockTerm = async (token: string, year: string, term: string) => { +// TODO: dont steal the CourseTime type here... unify all yearterm representations +export const toggleLockTerm = async (token: string, termyear: CourseTime) => { await axios.post( '/planner/toggleTermLocked', {}, - { params: { termyear: `${year}${term}` }, headers: withAuthorization(token) } + { params: { termyear: `${termyear.year}${termyear.term}` }, headers: withAuthorization(token) } ); }; diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index 3e4d78957..719325a9a 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -2,7 +2,12 @@ import { useAddToUnplannedMutation, useRemoveAllCoursesMutation, useRemoveCourseMutation, - useUpdateMarkMutation + useResetDegreeMutation, + useToggleLockTermMutation, + useToggleSummerTermMutation, + useUpdateDegreeLengthMutation, + useUpdateMarkMutation, + useUpdateStartYearMutation } from './mutations'; import { useUserCourses, useUserDegree, useUserPlanner } from './queries'; @@ -10,7 +15,12 @@ export { useAddToUnplannedMutation, useRemoveAllCoursesMutation, useRemoveCourseMutation, + useResetDegreeMutation, + useToggleLockTermMutation, + useToggleSummerTermMutation, + useUpdateDegreeLengthMutation, useUpdateMarkMutation, + useUpdateStartYearMutation, useUserCourses, useUserDegree, useUserPlanner diff --git a/frontend/src/utils/apiHooks/user/mutations.ts b/frontend/src/utils/apiHooks/user/mutations.ts index fefae0bad..bf6c0e507 100644 --- a/frontend/src/utils/apiHooks/user/mutations.ts +++ b/frontend/src/utils/apiHooks/user/mutations.ts @@ -1,5 +1,16 @@ -import { addToUnplanned, removeAll, removeCourse, updateCourseMark } from 'utils/api/plannerApi'; -import { resetUserDegree } from 'utils/api/userApi'; +import { + addToUnplanned, + removeAll, + removeCourse, + toggleLockTerm, + updateCourseMark +} from 'utils/api/plannerApi'; +import { + resetUserDegree, + toggleSummerTerm, + updateDegreeLength, + updateStartYear +} from 'utils/api/userApi'; import { createUserMutationHook } from './hookHelpers'; export const useRemoveCourseMutation = createUserMutationHook([['planner']], removeCourse); @@ -17,3 +28,14 @@ export const useUpdateMarkMutation = createUserMutationHook( export const useRemoveAllCoursesMutation = createUserMutationHook([['planner']], removeAll); export const useResetDegreeMutation = createUserMutationHook(true, resetUserDegree); + +export const useToggleLockTermMutation = createUserMutationHook([['planner']], toggleLockTerm); // TODO-olli: technically no need to courses + +export const useUpdateDegreeLengthMutation = createUserMutationHook( + [['planner'], ['settings']], + updateDegreeLength +); + +export const useUpdateStartYearMutation = createUserMutationHook([['planner']], updateStartYear); + +export const useToggleSummerTermMutation = createUserMutationHook([['planner']], toggleSummerTerm); From 848bd568f26927d7c35098959928c6e243c71a3d Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:16:00 +1100 Subject: [PATCH 07/13] feat: added support for queryClient and toggleShowMarks, hideYears, showYears hooks --- frontend/src/hooks/useSettings.ts | 64 +++++------------ .../src/utils/apiHooks/user/hookHelpers.ts | 71 +++++++++++-------- frontend/src/utils/apiHooks/user/index.ts | 6 ++ frontend/src/utils/apiHooks/user/mutations.ts | 9 +++ 4 files changed, 73 insertions(+), 77 deletions(-) diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts index ab2e3210d..8c5b965de 100644 --- a/frontend/src/hooks/useSettings.ts +++ b/frontend/src/hooks/useSettings.ts @@ -1,12 +1,12 @@ import { useCallback } from 'react'; -import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'; import { SettingsResponse } from 'types/userResponse'; +import { getUserSettings } from 'utils/api/userApi'; import { - getUserSettings, - hideYear as hideYearApi, - showYears as showYearsApi, - toggleShowMarks as toggleShowMarksApi -} from 'utils/api/userApi'; + useHideYearMutation, + useShowYearsMutation, + useToggleMarksMutation +} from 'utils/apiHooks/user'; import { useAppDispatch, useAppSelector } from 'hooks'; import { toggleLockedCourses as toggleLocked, @@ -54,48 +54,20 @@ function useSettings(queryClient?: QueryClient): Settings { ); const userSettings = settingsQuery.data ?? defaultUserSettings; - const showMarksMutation = useMutation( - { - mutationFn: () => (token ? toggleShowMarksApi(token) : Promise.reject(new Error('No token'))), - onSuccess: () => { - realQueryClient.invalidateQueries({ queryKey: ['settings'] }); - }, - onError: (error) => { - // eslint-disable-next-line no-console - console.error('Error toggling show marks: ', error); - } - }, - queryClient - ); + const showMarksMutation = useToggleMarksMutation({ + allowUnsetToken: true, + queryClient: realQueryClient + }); - const hideYearMutation = useMutation( - { - mutationFn: (yearIndex: number) => - token ? hideYearApi(token, yearIndex) : Promise.reject(new Error('No token')), - onSuccess: () => { - realQueryClient.invalidateQueries({ queryKey: ['settings'] }); - }, - onError: (error) => { - // eslint-disable-next-line no-console - console.error('Error hiding year: ', error); - } - }, - queryClient - ); + const hideYearMutation = useHideYearMutation({ + allowUnsetToken: true, + queryClient: realQueryClient + }); - const showYearsMutation = useMutation( - { - mutationFn: () => (token ? showYearsApi(token) : Promise.reject(new Error('No token'))), - onSuccess: () => { - realQueryClient.invalidateQueries({ queryKey: ['settings'] }); - }, - onError: (error) => { - // eslint-disable-next-line no-console - console.error('Error showing years: ', error); - } - }, - queryClient - ); + const showYearsMutation = useShowYearsMutation({ + allowUnsetToken: true, + queryClient: realQueryClient + }); const mutateTheme = useCallback((theme: Theme) => dispatch(toggleTheme(theme)), [dispatch]); const toggleLockedCourses = useCallback(() => dispatch(toggleLocked()), [dispatch]); diff --git a/frontend/src/utils/apiHooks/user/hookHelpers.ts b/frontend/src/utils/apiHooks/user/hookHelpers.ts index 5c6b10b7e..037e928ff 100644 --- a/frontend/src/utils/apiHooks/user/hookHelpers.ts +++ b/frontend/src/utils/apiHooks/user/hookHelpers.ts @@ -1,4 +1,5 @@ import { + QueryClient, useMutation, UseMutationOptions, UseMutationResult, @@ -11,6 +12,7 @@ import useIdentity from 'hooks/useIdentity'; export type CreateUserQueryOptions = Omit; export type UserQueryHookOptions = { allowUnsetToken?: boolean; + queryClient?: QueryClient; queryOptions?: Omit; }; @@ -26,15 +28,18 @@ export function createUserQueryHook< return (options?: UserQueryHookOptions, ...args: FArgs) => { const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; - const query = useQuery({ - queryKey: ['user', userId].concat(keySuffix), - queryFn: () => fn(token!, ...args), + const query = useQuery( + { + queryKey: ['user', userId].concat(keySuffix), + queryFn: () => fn(token!, ...args), - // ...baseOptions, // TODO-olli - // ...options?.queryOptions, + // ...baseOptions, // TODO-olli + // ...options?.queryOptions, - enabled: options?.queryOptions?.enabled && token !== undefined - }); + enabled: options?.queryOptions?.enabled && token !== undefined + }, + options?.queryClient + ); return query; }; @@ -46,6 +51,7 @@ export type CreateUserMutationOptions = Omit< >; export type UserMutationHookOptions = { allowUnsetToken?: boolean; + queryClient?: QueryClient; mutationOptions?: Omit, 'mutationFn'>; }; @@ -62,35 +68,38 @@ export function createUserMutationHook< const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; const queryClient = useQueryClient(); - const mutation = useMutation({ - mutationFn: async (data: FArg) => - token !== undefined ? fn(token, data) : Promise.reject(new Error('No token')), + const mutation = useMutation( + { + mutationFn: async (data: FArg) => + token !== undefined ? fn(token, data) : Promise.reject(new Error('No token')), - ...baseOptions, - ...options?.mutationOptions, + ...baseOptions, + ...options?.mutationOptions, - onSuccess: (data, variables, context) => { - if (invalidationKeySuffixes === true) { - queryClient.invalidateQueries({ - queryKey: ['user', userId] - }); - - queryClient.clear(); - } else { - invalidationKeySuffixes.forEach((key) => + onSuccess: (data, variables, context) => { + if (invalidationKeySuffixes === true) { queryClient.invalidateQueries({ - queryKey: ['user', userId].concat(key) - }) - ); - } + queryKey: ['user', userId] + }); + + queryClient.clear(); + } else { + invalidationKeySuffixes.forEach((key) => + queryClient.invalidateQueries({ + queryKey: ['user', userId].concat(key) + }) + ); + } - if (options?.mutationOptions?.onSuccess !== undefined) { - options.mutationOptions.onSuccess(data, variables, context); - } else { - baseOptions?.onSuccess?.(data, variables, context); + if (options?.mutationOptions?.onSuccess !== undefined) { + options.mutationOptions.onSuccess(data, variables, context); + } else { + baseOptions?.onSuccess?.(data, variables, context); + } } - } - }); + }, + options?.queryClient + ); return mutation; }; diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index 719325a9a..ebf7d0f4d 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -1,9 +1,12 @@ import { useAddToUnplannedMutation, + useHideYearMutation, useRemoveAllCoursesMutation, useRemoveCourseMutation, useResetDegreeMutation, + useShowYearsMutation, useToggleLockTermMutation, + useToggleMarksMutation, useToggleSummerTermMutation, useUpdateDegreeLengthMutation, useUpdateMarkMutation, @@ -13,10 +16,13 @@ import { useUserCourses, useUserDegree, useUserPlanner } from './queries'; export { useAddToUnplannedMutation, + useHideYearMutation, useRemoveAllCoursesMutation, useRemoveCourseMutation, useResetDegreeMutation, + useShowYearsMutation, useToggleLockTermMutation, + useToggleMarksMutation, useToggleSummerTermMutation, useUpdateDegreeLengthMutation, useUpdateMarkMutation, diff --git a/frontend/src/utils/apiHooks/user/mutations.ts b/frontend/src/utils/apiHooks/user/mutations.ts index bf6c0e507..ad364d329 100644 --- a/frontend/src/utils/apiHooks/user/mutations.ts +++ b/frontend/src/utils/apiHooks/user/mutations.ts @@ -6,7 +6,10 @@ import { updateCourseMark } from 'utils/api/plannerApi'; import { + hideYear, resetUserDegree, + showYears, + toggleShowMarks, toggleSummerTerm, updateDegreeLength, updateStartYear @@ -39,3 +42,9 @@ export const useUpdateDegreeLengthMutation = createUserMutationHook( export const useUpdateStartYearMutation = createUserMutationHook([['planner']], updateStartYear); export const useToggleSummerTermMutation = createUserMutationHook([['planner']], toggleSummerTerm); + +export const useToggleMarksMutation = createUserMutationHook([['settings']], toggleShowMarks); + +export const useHideYearMutation = createUserMutationHook([['settings']], hideYear); + +export const useShowYearsMutation = createUserMutationHook([['settings']], showYears); From de1656fedb8a8083f2e5e54e15b35c8b6801dd1d Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:57:44 +1100 Subject: [PATCH 08/13] feat: final few mutations --- .../StartBrowsingStep/StartBrowsingStep.tsx | 31 +--- .../TermPlanner/ContextMenu/ContextMenu.tsx | 37 +---- .../ImportPlannerMenu/ImportPlannerMenu.tsx | 21 +-- .../OptionsHeader/OptionsHeader.tsx | 26 +--- .../src/pages/TermPlanner/TermPlanner.tsx | 138 +++++++----------- .../src/utils/apiHooks/user/hookHelpers.ts | 3 +- frontend/src/utils/apiHooks/user/index.ts | 12 ++ frontend/src/utils/apiHooks/user/mutations.ts | 31 ++++ 8 files changed, 110 insertions(+), 189 deletions(-) diff --git a/frontend/src/pages/DegreeWizard/StartBrowsingStep/StartBrowsingStep.tsx b/frontend/src/pages/DegreeWizard/StartBrowsingStep/StartBrowsingStep.tsx index d8a890ca2..91bd3ef3f 100644 --- a/frontend/src/pages/DegreeWizard/StartBrowsingStep/StartBrowsingStep.tsx +++ b/frontend/src/pages/DegreeWizard/StartBrowsingStep/StartBrowsingStep.tsx @@ -1,11 +1,9 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from 'antd'; import { DegreeWizardPayload } from 'types/degreeWizard'; -import { setupDegreeWizard } from 'utils/api/degreeApi'; +import { useSetupDegreeWizardMutation } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; -import useToken from 'hooks/useToken'; import CS from '../common/styles'; import S from './styles'; @@ -15,32 +13,9 @@ type Props = { const StartBrowsingStep = ({ degreeInfo }: Props) => { const navigate = useNavigate(); - const queryClient = useQueryClient(); - const token = useToken(); - const setupDegreeMutation = useMutation({ - mutationFn: (wizard: DegreeWizardPayload) => setupDegreeWizard(token, wizard), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['degree'] - }); - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.clear(); - navigate('/course-selector'); - }, - onError: (err) => { - openNotification({ - type: 'error', - message: 'Error setting up degree, ensure your specialisations are valid.' - }); - // eslint-disable-next-line no-console - console.error('Error at resetDegreeMutation: ', err); - } + const setupDegreeMutation = useSetupDegreeWizardMutation({ + mutationOptions: { onSuccess: () => navigate('/course-selector') } }); const handleSetupDegree = () => { diff --git a/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx b/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx index 7ededd3dd..37ae3f082 100644 --- a/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx +++ b/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx @@ -11,12 +11,12 @@ import { PieChartOutlined, StarOutlined } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { UnscheduleCourse } from 'types/planner'; -import { toggleIgnoreFromProgression, unscheduleCourse } from 'utils/api/plannerApi'; -import { useRemoveCourseMutation } from 'utils/apiHooks/user'; +import { + useRemoveCourseMutation, + useToggleIgnoreFromProgressionMutation, + useUnscheduleCourseMutation +} from 'utils/apiHooks/user'; import EditMarkModal from 'components/EditMarkModal'; -import useToken from 'hooks/useToken'; import { addTab } from 'reducers/courseTabsSlice'; import 'react-contexify/ReactContexify.css'; @@ -27,28 +27,12 @@ type Props = { }; const ContextMenu = ({ code, plannedFor, ignoreFromProgression }: Props) => { - const token = useToken(); - - const queryClient = useQueryClient(); const [openModal, setOpenModal] = useState(false); const dispatch = useDispatch(); const navigate = useNavigate(); const showEditMark = () => setOpenModal(true); - const handleUnschedule = useMutation({ - mutationFn: (data: UnscheduleCourse) => unscheduleCourse(token, data), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - } - }); + const handleUnschedule = useUnscheduleCourseMutation(); const handleDelete = useRemoveCourseMutation(); @@ -56,13 +40,8 @@ const ContextMenu = ({ code, plannedFor, ignoreFromProgression }: Props) => { navigate('/course-selector'); dispatch(addTab(code)); }; - const ignoreFromProgressionMutation = useMutation({ - mutationFn: (courseId: string) => toggleIgnoreFromProgression(token, courseId), - onSuccess: () => - queryClient.invalidateQueries({ - queryKey: ['courses'] - }) - }); + + const ignoreFromProgressionMutation = useToggleIgnoreFromProgressionMutation(); const handleToggleProgression = () => { ignoreFromProgressionMutation.mutate(code); }; diff --git a/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx b/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx index ad6b1f035..77bf73d92 100644 --- a/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx +++ b/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx @@ -1,31 +1,14 @@ import React, { useRef, useState } from 'react'; import { LoadingOutlined } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Spin } from 'antd'; -import { importUser as importUserApi } from 'utils/api/userApi'; +import { useImportUserMutation } from 'utils/apiHooks/user'; import { importUser, UserJson } from 'utils/export'; import openNotification from 'utils/openNotification'; -import useToken from 'hooks/useToken'; import CS from '../common/styles'; import S from './styles'; const ImportPlannerMenu = () => { - const token = useToken(); - const queryClient = useQueryClient(); - - const importUserMutation = useMutation({ - mutationFn: (user: UserJson) => importUserApi(token, user), - onSuccess: () => { - queryClient.resetQueries(); - }, - onError: () => { - openNotification({ - type: 'error', - message: 'Import failed', - description: 'An error occurred when importing the planner' - }); - } - }); + const importUserMutation = useImportUserMutation(); const handleImport = (user: UserJson) => { importUserMutation.mutate(user); diff --git a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx index 5c3f2a587..0efbb42b0 100644 --- a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx +++ b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx @@ -9,13 +9,11 @@ import { UploadOutlined, WarningFilled } from '@ant-design/icons'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import Tippy from '@tippyjs/react'; import { Popconfirm, Switch, Tooltip } from 'antd'; -import { unscheduleAll } from 'utils/api/plannerApi'; import { useUserPlanner } from 'utils/apiHooks/user'; +import { useUnscheduleAllMutation } from 'utils/apiHooks/user/mutations'; import useSettings from 'hooks/useSettings'; -import useToken from 'hooks/useToken'; import ExportPlannerMenu from '../ExportPlannerMenu'; import HelpMenu from '../HelpMenu/HelpMenu'; import ImportPlannerMenu from '../ImportPlannerMenu'; @@ -27,9 +25,6 @@ import 'tippy.js/dist/tippy.css'; import 'tippy.js/themes/light.css'; const OptionsHeader = () => { - const token = useToken(); - const queryClient = useQueryClient(); - const plannerQuery = useUserPlanner(); const planner = plannerQuery.data; @@ -47,24 +42,7 @@ const OptionsHeader = () => { color: theme === 'light' ? '#323739' : '#f1f1f1' }; - const unscheduleAllMutation = useMutation({ - mutationFn: () => unscheduleAll(token), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at unscheduleAllMutation: ', err); - } - }); + const unscheduleAllMutation = useUnscheduleAllMutation(); const handleUnscheduleAll = async () => { unscheduleAllMutation.mutate(); diff --git a/frontend/src/pages/TermPlanner/TermPlanner.tsx b/frontend/src/pages/TermPlanner/TermPlanner.tsx index 3ca9b3e13..8ad4d3b2e 100644 --- a/frontend/src/pages/TermPlanner/TermPlanner.tsx +++ b/frontend/src/pages/TermPlanner/TermPlanner.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import type { OnDragEndResponder, OnDragStartResponder } from 'react-beautiful-dnd'; -import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; import { Badge } from 'antd'; import { Course } from 'types/api'; import { PlannedToTerm, Term, UnPlannedToTerm, UnscheduleCourse } from 'types/planner'; @@ -14,13 +14,14 @@ import { ValidatesResponse } from 'types/userResponse'; import { getCourseForYearsInfo } from 'utils/api/coursesApi'; +import { validateTermPlanner } from 'utils/api/plannerApi'; import { - setPlannedCourseToTerm, - setUnplannedCourseToTerm, - unscheduleCourse, - validateTermPlanner -} from 'utils/api/plannerApi'; -import { useUserCourses, useUserPlanner } from 'utils/apiHooks/user'; + useSetPlannedCourseToTermMutation, + useSetUnplannedCourseToTermMutation, + useUnscheduleCourseMutation, + useUserCourses, + useUserPlanner +} from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import PageTemplate from 'components/PageTemplate'; import Spinner from 'components/Spinner'; @@ -118,34 +119,21 @@ const TermPlanner = () => { ); // Mutations - const setPlannedCourseToTermMutation = useMutation({ - mutationFn: (data: PlannedToTerm) => setPlannedCourseToTerm(token, data), - onMutate: (data) => { - queryClient.setQueryData(['planner'], (prev: PlannerResponse | undefined) => { - if (!prev) return badPlanner; - const curr: PlannerResponse = structuredClone(prev); - curr.years[data.srcRow][data.srcTerm].splice( - curr.years[data.srcRow][data.srcTerm].indexOf(data.courseCode), - 1 - ); - curr.years[data.destRow][data.destTerm].splice(data.destIndex, 0, data.courseCode); - return curr; - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at setPlannedCourseToTermMutation: ', err); + const setPlannedCourseToTermMutation = useSetPlannedCourseToTermMutation({ + mutationOptions: { + onMutate: (data) => { + // TODO-olli + queryClient.setQueryData(['planner'], (prev: PlannerResponse | undefined) => { + if (!prev) return badPlanner; + const curr: PlannerResponse = structuredClone(prev); + curr.years[data.srcRow][data.srcTerm].splice( + curr.years[data.srcRow][data.srcTerm].indexOf(data.courseCode), + 1 + ); + curr.years[data.destRow][data.destTerm].splice(data.destIndex, 0, data.courseCode); + return curr; + }); + } } }); @@ -153,31 +141,18 @@ const TermPlanner = () => { setPlannedCourseToTermMutation.mutate(data); }; - const setUnplannedCourseToTermMutation = useMutation({ - mutationFn: (data: UnPlannedToTerm) => setUnplannedCourseToTerm(token, data), - onMutate: (data) => { - queryClient.setQueryData(['planner'], (prev: PlannerResponse | undefined) => { - if (!prev) return badPlanner; - const curr: PlannerResponse = structuredClone(prev); - curr.unplanned.splice(curr.unplanned.indexOf(data.courseCode), 1); - curr.years[data.destRow][data.destTerm].splice(data.destIndex, 0, data.courseCode); - return curr; - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at setUnplannedCourseToTermMutation: ', err); + const setUnplannedCourseToTermMutation = useSetUnplannedCourseToTermMutation({ + mutationOptions: { + onMutate: (data) => { + // TODO-olli + queryClient.setQueryData(['planner'], (prev: PlannerResponse | undefined) => { + if (!prev) return badPlanner; + const curr: PlannerResponse = structuredClone(prev); + curr.unplanned.splice(curr.unplanned.indexOf(data.courseCode), 1); + curr.years[data.destRow][data.destTerm].splice(data.destIndex, 0, data.courseCode); + return curr; + }); + } } }); @@ -185,34 +160,21 @@ const TermPlanner = () => { setUnplannedCourseToTermMutation.mutate(data); }; - const unscheduleCourseMutation = useMutation({ - mutationFn: (data: UnscheduleCourse) => unscheduleCourse(token, data), - onMutate: (data) => { - queryClient.setQueryData(['planner'], (prev: PlannerResponse | undefined) => { - if (!prev) return badPlanner; - const curr: PlannerResponse = structuredClone(prev); - curr.years[data.srcRow as number][data.srcTerm as string].splice( - curr.years[data.srcRow as number][data.srcTerm as string].indexOf(data.courseCode), - 1 - ); - curr.unplanned.push(data.courseCode); - return curr; - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['planner'] - }); - queryClient.invalidateQueries({ - queryKey: ['courses'] - }); - queryClient.invalidateQueries({ - queryKey: ['validate'] - }); - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.error('Error at unscheduleCourseMutation: ', err); + const unscheduleCourseMutation = useUnscheduleCourseMutation({ + mutationOptions: { + onMutate: (data) => { + // TODO-olli: remove from here... + queryClient.setQueryData(['planner'], (prev: PlannerResponse | undefined) => { + if (!prev) return badPlanner; + const curr: PlannerResponse = structuredClone(prev); + curr.years[data.srcRow as number][data.srcTerm as string].splice( + curr.years[data.srcRow as number][data.srcTerm as string].indexOf(data.courseCode), + 1 + ); + curr.unplanned.push(data.courseCode); + return curr; + }); + } } }); diff --git a/frontend/src/utils/apiHooks/user/hookHelpers.ts b/frontend/src/utils/apiHooks/user/hookHelpers.ts index 037e928ff..b9b595788 100644 --- a/frontend/src/utils/apiHooks/user/hookHelpers.ts +++ b/frontend/src/utils/apiHooks/user/hookHelpers.ts @@ -2,7 +2,6 @@ import { QueryClient, useMutation, UseMutationOptions, - UseMutationResult, useQuery, useQueryClient, UseQueryOptions @@ -82,7 +81,9 @@ export function createUserMutationHook< queryKey: ['user', userId] }); + // TODO-olli: figure out which one we want to do... queryClient.clear(); + queryClient.resetQueries(); } else { invalidationKeySuffixes.forEach((key) => queryClient.invalidateQueries({ diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index ebf7d0f4d..ce2e2a9fa 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -1,13 +1,19 @@ import { useAddToUnplannedMutation, useHideYearMutation, + useImportUserMutation, useRemoveAllCoursesMutation, useRemoveCourseMutation, useResetDegreeMutation, + useSetPlannedCourseToTermMutation, + useSetUnplannedCourseToTermMutation, + useSetupDegreeWizardMutation, useShowYearsMutation, + useToggleIgnoreFromProgressionMutation, useToggleLockTermMutation, useToggleMarksMutation, useToggleSummerTermMutation, + useUnscheduleCourseMutation, useUpdateDegreeLengthMutation, useUpdateMarkMutation, useUpdateStartYearMutation @@ -17,13 +23,19 @@ import { useUserCourses, useUserDegree, useUserPlanner } from './queries'; export { useAddToUnplannedMutation, useHideYearMutation, + useImportUserMutation, useRemoveAllCoursesMutation, useRemoveCourseMutation, useResetDegreeMutation, + useSetPlannedCourseToTermMutation, + useSetUnplannedCourseToTermMutation, + useSetupDegreeWizardMutation, useShowYearsMutation, + useToggleIgnoreFromProgressionMutation, useToggleLockTermMutation, useToggleMarksMutation, useToggleSummerTermMutation, + useUnscheduleCourseMutation, useUpdateDegreeLengthMutation, useUpdateMarkMutation, useUpdateStartYearMutation, diff --git a/frontend/src/utils/apiHooks/user/mutations.ts b/frontend/src/utils/apiHooks/user/mutations.ts index ad364d329..530dfca21 100644 --- a/frontend/src/utils/apiHooks/user/mutations.ts +++ b/frontend/src/utils/apiHooks/user/mutations.ts @@ -1,12 +1,19 @@ +import { setupDegreeWizard } from 'utils/api/degreeApi'; import { addToUnplanned, removeAll, removeCourse, + setPlannedCourseToTerm, + setUnplannedCourseToTerm, + toggleIgnoreFromProgression, toggleLockTerm, + unscheduleAll, + unscheduleCourse, updateCourseMark } from 'utils/api/plannerApi'; import { hideYear, + importUser, resetUserDegree, showYears, toggleShowMarks, @@ -48,3 +55,27 @@ export const useToggleMarksMutation = createUserMutationHook([['settings']], tog export const useHideYearMutation = createUserMutationHook([['settings']], hideYear); export const useShowYearsMutation = createUserMutationHook([['settings']], showYears); + +export const useSetupDegreeWizardMutation = createUserMutationHook(true, setupDegreeWizard); + +export const useToggleIgnoreFromProgressionMutation = createUserMutationHook( + [['planner']], + toggleIgnoreFromProgression +); + +export const useUnscheduleAllMutation = createUserMutationHook([['planner']], unscheduleAll); + +export const useUnscheduleCourseMutation = createUserMutationHook([['planner']], unscheduleCourse); // TODO-olli: figure out how to get query client into onMutate for baseOptions + +// could change baseOptions to be (queryClient) => { options } +export const useSetPlannedCourseToTermMutation = createUserMutationHook( + [['planner']], + setPlannedCourseToTerm +); + +export const useSetUnplannedCourseToTermMutation = createUserMutationHook( + [['planner']], + setUnplannedCourseToTerm +); + +export const useImportUserMutation = createUserMutationHook(true, importUser); From f9b478720258a05727dd831bd143a70f3682abe2 Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:58:39 +1100 Subject: [PATCH 09/13] fix: removed user/mutation imports --- frontend/src/pages/DegreeWizard/DegreeWizard.tsx | 2 +- frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx | 3 +-- frontend/src/utils/apiHooks/user/index.ts | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx index ffae3fc61..c58dd1d1e 100644 --- a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx +++ b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx @@ -6,7 +6,7 @@ import { Button, Typography } from 'antd'; import { DegreeWizardPayload } from 'types/degreeWizard'; import { getSpecialisationTypes } from 'utils/api/specsApi'; import { getUserIsSetup } from 'utils/api/userApi'; -import { useResetDegreeMutation } from 'utils/apiHooks/user/mutations'; +import { useResetDegreeMutation } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import MigrationModal from 'components/MigrationModal'; import PageTemplate from 'components/PageTemplate'; diff --git a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx index 0efbb42b0..8f9883895 100644 --- a/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx +++ b/frontend/src/pages/TermPlanner/OptionsHeader/OptionsHeader.tsx @@ -11,8 +11,7 @@ import { } from '@ant-design/icons'; import Tippy from '@tippyjs/react'; import { Popconfirm, Switch, Tooltip } from 'antd'; -import { useUserPlanner } from 'utils/apiHooks/user'; -import { useUnscheduleAllMutation } from 'utils/apiHooks/user/mutations'; +import { useUnscheduleAllMutation, useUserPlanner } from 'utils/apiHooks/user'; import useSettings from 'hooks/useSettings'; import ExportPlannerMenu from '../ExportPlannerMenu'; import HelpMenu from '../HelpMenu/HelpMenu'; diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index ce2e2a9fa..702b2f914 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -13,6 +13,7 @@ import { useToggleLockTermMutation, useToggleMarksMutation, useToggleSummerTermMutation, + useUnscheduleAllMutation, useUnscheduleCourseMutation, useUpdateDegreeLengthMutation, useUpdateMarkMutation, @@ -35,6 +36,7 @@ export { useToggleLockTermMutation, useToggleMarksMutation, useToggleSummerTermMutation, + useUnscheduleAllMutation, useUnscheduleCourseMutation, useUpdateDegreeLengthMutation, useUpdateMarkMutation, From bce4909f1056cfd5f3a66ce11cd7c988ede19ddb Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:42:48 +1100 Subject: [PATCH 10/13] wip: created most hooks, working on static hooks --- frontend/src/components/Auth/PreventToken.tsx | 11 +-- frontend/src/components/Auth/RequireToken.tsx | 11 +-- .../CourseAttributes/CourseAttributes.tsx | 8 +- .../CourseDescriptionPanel.tsx | 13 +--- .../CourseInfoDrawers.tsx | 14 +--- frontend/src/hooks/useSettings.ts | 23 +++--- .../CourseSelector/CourseMenu/CourseMenu.tsx | 15 ++-- .../src/pages/DegreeWizard/DegreeWizard.tsx | 10 +-- .../CourseGraph/CourseGraph.tsx | 16 ++-- .../src/pages/LandingPage/LandingPage.tsx | 11 +-- .../src/pages/LoginSuccess/LoginSuccess.tsx | 2 +- .../src/pages/TermPlanner/TermPlanner.tsx | 14 ++-- .../utils/apiHooks/{user => }/hookHelpers.ts | 78 +++++++++++++++---- frontend/src/utils/apiHooks/static.ts | 10 +++ frontend/src/utils/apiHooks/user/index.ts | 18 ++++- frontend/src/utils/apiHooks/user/mutations.ts | 2 +- frontend/src/utils/apiHooks/user/queries.ts | 38 +++++++-- 17 files changed, 175 insertions(+), 119 deletions(-) rename frontend/src/utils/apiHooks/{user => }/hookHelpers.ts (54%) create mode 100644 frontend/src/utils/apiHooks/static.ts diff --git a/frontend/src/components/Auth/PreventToken.tsx b/frontend/src/components/Auth/PreventToken.tsx index 28cc57b75..7e585f73f 100644 --- a/frontend/src/components/Auth/PreventToken.tsx +++ b/frontend/src/components/Auth/PreventToken.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Navigate, Outlet } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; -import { getUserIsSetup } from 'utils/api/userApi'; +import { useUserSetupState } from 'utils/apiHooks/user'; import PageLoading from 'components/PageLoading'; import { useAppSelector } from 'hooks'; import { selectToken } from 'reducers/identitySlice'; @@ -14,11 +13,9 @@ const PreventToken = () => { isPending, data: isSetup, error - } = useQuery({ - queryKey: ['degree', 'isSetup'], // TODO-OLLI(pm): fix this key, including userId - queryFn: () => getUserIsSetup(token!), - enabled: token !== undefined, - refetchOnWindowFocus: 'always' + } = useUserSetupState({ + allowUnsetToken: true, + queryOptions: { refetchOnWindowFocus: 'always' } }); if (token === undefined) { diff --git a/frontend/src/components/Auth/RequireToken.tsx b/frontend/src/components/Auth/RequireToken.tsx index a2a4842e6..ad3a0df34 100644 --- a/frontend/src/components/Auth/RequireToken.tsx +++ b/frontend/src/components/Auth/RequireToken.tsx @@ -1,8 +1,7 @@ import React, { useEffect } from 'react'; import { Navigate, Outlet } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; -import { getUserIsSetup } from 'utils/api/userApi'; +import { useUserSetupState } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import PageLoading from 'components/PageLoading'; import { useAppSelector } from 'hooks'; @@ -19,11 +18,9 @@ const RequireToken = ({ needSetup }: Props) => { isPending, data: isSetup, error - } = useQuery({ - queryKey: ['degree', 'isSetup'], // TODO-OLLI(pm): fix this key, including userId - queryFn: () => getUserIsSetup(token!), - enabled: token !== undefined, - refetchOnWindowFocus: 'always' + } = useUserSetupState({ + allowUnsetToken: true, + queryOptions: { refetchOnWindowFocus: 'always' } }); // TODO-OLLI(pm): multitab support is hard diff --git a/frontend/src/components/CourseDescriptionPanel/CourseAttributes/CourseAttributes.tsx b/frontend/src/components/CourseDescriptionPanel/CourseAttributes/CourseAttributes.tsx index fd24aee6d..b667d6d8c 100644 --- a/frontend/src/components/CourseDescriptionPanel/CourseAttributes/CourseAttributes.tsx +++ b/frontend/src/components/CourseDescriptionPanel/CourseAttributes/CourseAttributes.tsx @@ -1,11 +1,10 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; import { Progress, Rate, Typography } from 'antd'; import { useTheme } from 'styled-components'; import { Course } from 'types/api'; import { EnrolmentCapacityData } from 'types/courseCapacity'; -import { getCourseRating } from 'utils/api/unilectivesApi'; +import { useCourseRatingQuery } from 'utils/apiHooks/static'; import getMostRecentPastTerm from 'utils/getMostRecentPastTerm'; import ProgressBar from 'components/ProgressBar'; import TermTag from 'components/TermTag'; @@ -31,10 +30,7 @@ const CourseAttributes = ({ course, courseCapacity }: CourseAttributesProps) => const sidebar = pathname === '/course-selector'; const theme = useTheme(); - const ratingQuery = useQuery({ - queryKey: ['courseRating', course.code], - queryFn: () => getCourseRating(course.code) - }); + const ratingQuery = useCourseRatingQuery({}, course.code); const rating = ratingQuery.data; const { study_level: studyLevel, terms, campus, code, school, UOC } = course; diff --git a/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx b/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx index 2505a70d9..d8663b31e 100644 --- a/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx +++ b/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx @@ -3,8 +3,9 @@ import { useLocation } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; import { CoursesResponse } from 'types/userResponse'; -import { getCourseInfo, getCoursePrereqs, getCoursesUnlockedWhenTaken } from 'utils/api/coursesApi'; +import { getCourseInfo, getCoursePrereqs } from 'utils/api/coursesApi'; import { getCourseTimetable } from 'utils/api/timetableApi'; +import { useUserCoursesUnlockedWhenTaken } from 'utils/apiHooks/user'; import getEnrolmentCapacity from 'utils/getEnrolmentCapacity'; import { unwrapSettledPromise } from 'utils/queryUtils'; import { @@ -12,7 +13,6 @@ import { LoadingCourseDescriptionPanelSidebar } from 'components/LoadingSkeleton'; import PlannerButton from 'components/PlannerButton'; -import useToken from 'hooks/useToken'; import CourseAttributes from './CourseAttributes'; import CourseInfoDrawers from './CourseInfoDrawers'; import S from './styles'; @@ -40,16 +40,11 @@ const CourseDescriptionPanel = ({ onCourseClick, courses }: CourseDescriptionPanelProps) => { - const token = useToken(); - - const coursesUnlockedQuery = useQuery({ - queryKey: ['courses', 'coursesUnlockedWhenTaken', courseCode], - queryFn: () => getCoursesUnlockedWhenTaken(token, courseCode) - }); - const { pathname } = useLocation(); const sidebar = pathname === '/course-selector'; + const coursesUnlockedQuery = useUserCoursesUnlockedWhenTaken({}, courseCode); + const courseInfoQuery = useQuery({ queryKey: ['courseInfo', courseCode], queryFn: () => getCourseExtendedInfo(courseCode) diff --git a/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx b/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx index 0be545ccb..f71467023 100644 --- a/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx +++ b/frontend/src/components/CourseDescriptionPanel/CourseInfoDrawers.tsx @@ -1,16 +1,13 @@ import React from 'react'; -import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; import { Course, CoursesUnlockedWhenTaken } from 'types/api'; import { CourseList } from 'types/courses'; import { badCourses, badValidations } from 'types/userResponse'; -import { validateTermPlanner } from 'utils/api/plannerApi'; -import { useUserCourses } from 'utils/apiHooks/user'; +import { useUserCourses, useUserTermValidations } from 'utils/apiHooks/user'; import Collapsible from 'components/Collapsible'; import CourseTag from 'components/CourseTag'; import PrerequisiteTree from 'components/PrerequisiteTree'; import { inDev } from 'config/constants'; -import useToken from 'hooks/useToken'; import S from './styles'; const { Text } = Typography; @@ -28,8 +25,6 @@ const CourseInfoDrawers = ({ pathFrom = [], unlocked }: CourseInfoDrawersProps) => { - const token = useToken(); - const courses = useUserCourses().data || badCourses; const pathFromInPlanner = pathFrom.filter((courseCode) => @@ -38,11 +33,8 @@ const CourseInfoDrawers = ({ const pathFromNotInPlanner = pathFrom.filter( (courseCode) => !Object.keys(courses).includes(courseCode) ); - const inPlanner = courses[course.code]; - const validateQuery = useQuery({ - queryKey: ['validate'], - queryFn: () => validateTermPlanner(token) - }); + const inPlanner = !!courses[course.code]; + const validateQuery = useUserTermValidations(); const validations = validateQuery.data ?? badValidations; const isUnlocked = validations.courses_state[course.code]; return ( diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts index 8c5b965de..d3787543c 100644 --- a/frontend/src/hooks/useSettings.ts +++ b/frontend/src/hooks/useSettings.ts @@ -1,11 +1,11 @@ import { useCallback } from 'react'; -import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'; +import { QueryClient, useQueryClient } from '@tanstack/react-query'; import { SettingsResponse } from 'types/userResponse'; -import { getUserSettings } from 'utils/api/userApi'; import { useHideYearMutation, useShowYearsMutation, - useToggleMarksMutation + useToggleMarksMutation, + useUserSettings } from 'utils/apiHooks/user'; import { useAppDispatch, useAppSelector } from 'hooks'; import { @@ -13,7 +13,6 @@ import { toggleShowPastWarnings as togglePastWarnings, toggleTheme } from 'reducers/settingsSlice'; -import useToken from './useToken'; type Theme = 'light' | 'dark'; @@ -41,17 +40,13 @@ function useSettings(queryClient?: QueryClient): Settings { const localSettings = useAppSelector((state) => state.settings); const dispatch = useAppDispatch(); - const token = useToken({ allowUnset: true }); const realQueryClient = useQueryClient(queryClient); - const settingsQuery = useQuery( - { - queryKey: ['settings'], - queryFn: () => getUserSettings(token!), - placeholderData: defaultUserSettings, - enabled: !!token - }, - queryClient - ); + + const settingsQuery = useUserSettings({ + allowUnsetToken: true, + queryClient: realQueryClient, + queryOptions: { placeholderData: defaultUserSettings } + }); const userSettings = settingsQuery.data ?? defaultUserSettings; const showMarksMutation = useToggleMarksMutation({ diff --git a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx index 09cb67b10..01a2dcbfb 100644 --- a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx +++ b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx @@ -6,13 +6,15 @@ import { CourseUnitsStructure, MenuDataStructure, MenuDataSubgroup } from 'types import { CourseValidation } from 'types/courses'; import { ProgramStructure } from 'types/structure'; import { CoursesResponse, DegreeResponse } from 'types/userResponse'; -import { getAllUnlockedCourses } from 'utils/api/coursesApi'; import { getProgramStructure } from 'utils/api/programsApi'; -import { useAddToUnplannedMutation, useRemoveCourseMutation } from 'utils/apiHooks/user'; +import { + useAddToUnplannedMutation, + useRemoveCourseMutation, + useUserAllUnlocked +} from 'utils/apiHooks/user'; import { LoadingCourseMenu } from 'components/LoadingSkeleton'; import { MAX_COURSES_OVERFLOW } from 'config/constants'; import useSettings from 'hooks/useSettings'; -import useToken from 'hooks/useToken'; import { addTab } from 'reducers/courseTabsSlice'; import CourseMenuTitle from '../CourseMenuTitle'; import S from './styles'; @@ -38,18 +40,13 @@ const SubgroupTitle = ({ title, currUOC, totalUOC }: SubgroupTitleProps) => ( ); const CourseMenu = ({ courses, degree }: CourseMenuProps) => { - const token = useToken(); - const structureQuery = useQuery({ queryKey: ['structure', degree], queryFn: () => getProgramStructure(degree!.programCode, degree!.specs), enabled: !!degree }); - const coursesStateQuery = useQuery({ - queryKey: ['courses', 'coursesState'], - queryFn: () => getAllUnlockedCourses(token) - }); + const coursesStateQuery = useUserAllUnlocked(); const removeCourseMutation = useRemoveCourseMutation(); const addToUnplannedMutation = useAddToUnplannedMutation(); diff --git a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx index c58dd1d1e..9181a366e 100644 --- a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx +++ b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx @@ -5,13 +5,11 @@ import { useQuery } from '@tanstack/react-query'; import { Button, Typography } from 'antd'; import { DegreeWizardPayload } from 'types/degreeWizard'; import { getSpecialisationTypes } from 'utils/api/specsApi'; -import { getUserIsSetup } from 'utils/api/userApi'; -import { useResetDegreeMutation } from 'utils/apiHooks/user'; +import { useResetDegreeMutation, useUserSetupState } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import MigrationModal from 'components/MigrationModal'; import PageTemplate from 'components/PageTemplate'; import ResetModal from 'components/ResetModal'; -import useToken from 'hooks/useToken'; import Steps from './common/steps'; import DegreeStep from './DegreeStep'; import SpecialisationStep from './SpecialisationStep'; @@ -24,7 +22,6 @@ const { Title } = Typography; const DEFAULT_SPEC_TYPES = ['majors', 'honours', 'minors']; const DegreeWizard = () => { - const token = useToken(); const [currStep, setCurrStep] = useState(Steps.YEAR); const [degreeInfo, setDegreeInfo] = useState({ @@ -35,10 +32,7 @@ const DegreeWizard = () => { }); const { programCode } = degreeInfo; - const isSetup = useQuery({ - queryKey: ['degree', 'isSetup'], // TODO-OLLI(pm): fix this key - queryFn: () => getUserIsSetup(token) - }).data; + const isSetup = useUserSetupState().data; const navigate = useNavigate(); const specTypesQuery = useQuery({ diff --git a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx index 84cb72da0..66cd50940 100644 --- a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx +++ b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx @@ -10,14 +10,17 @@ import { useQuery } from '@tanstack/react-query'; import { Switch } from 'antd'; import { CourseEdge } from 'types/api'; import { useDebouncedCallback } from 'use-debounce'; -import { getAllUnlockedCourses } from 'utils/api/coursesApi'; import { getProgramGraph } from 'utils/api/programsApi'; -import { useUserCourses, useUserDegree, useUserPlanner } from 'utils/apiHooks/user'; +import { + useUserAllUnlocked, + useUserCourses, + useUserDegree, + useUserPlanner +} from 'utils/apiHooks/user'; import { unwrapQuery } from 'utils/queryUtils'; import Spinner from 'components/Spinner'; import { useAppWindowSize } from 'hooks'; import useSettings from 'hooks/useSettings'; -import useToken from 'hooks/useToken'; import { ZOOM_IN_RATIO, ZOOM_OUT_RATIO } from '../constants'; import { defaultEdge, @@ -56,8 +59,6 @@ const CourseGraph = ({ loading, setLoading }: Props) => { - const token = useToken(); - const degreeQuery = useUserDegree(); const plannerQuery = useUserPlanner(); const coursesQuery = useUserCourses(); @@ -80,10 +81,7 @@ const CourseGraph = ({ enabled: !degreeQuery.isPending && degreeQuery.data && degreeQuery.isSuccess }); - const coursesStateQuery = useQuery({ - queryKey: ['courses', 'coursesState'], - queryFn: () => getAllUnlockedCourses(token) - }); + const coursesStateQuery = useUserAllUnlocked(); const queriesSuccess = degreeQuery.isSuccess && coursesQuery.isSuccess && programGraphQuery.isSuccess; diff --git a/frontend/src/pages/LandingPage/LandingPage.tsx b/frontend/src/pages/LandingPage/LandingPage.tsx index bcfc55041..c127892e7 100644 --- a/frontend/src/pages/LandingPage/LandingPage.tsx +++ b/frontend/src/pages/LandingPage/LandingPage.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import { getUserIsSetup } from 'utils/api/userApi'; +import { useUserSetupState } from 'utils/apiHooks/user'; import { importUser } from 'utils/export'; import PageLoading from 'components/PageLoading'; import { inDev } from 'config/constants'; @@ -45,11 +44,9 @@ const LandingPage = () => { data: userIsSetup, isPending, error - } = useQuery({ - queryKey: ['degree', 'isSetup'], // TODO-OLLI(pm): fix this key, including userId - queryFn: () => getUserIsSetup(token!), - enabled: token !== undefined, - refetchOnWindowFocus: 'always' + } = useUserSetupState({ + allowUnsetToken: true, + queryOptions: { refetchOnWindowFocus: 'always' } }); const nextPage = useMemo(() => { diff --git a/frontend/src/pages/LoginSuccess/LoginSuccess.tsx b/frontend/src/pages/LoginSuccess/LoginSuccess.tsx index f3a5bd110..27cd8b0c8 100644 --- a/frontend/src/pages/LoginSuccess/LoginSuccess.tsx +++ b/frontend/src/pages/LoginSuccess/LoginSuccess.tsx @@ -20,7 +20,7 @@ const LoginSuccess = () => { dispatch(updateIdentityWithAPIRes(identity)); const userIsSetup = await queryClient.fetchQuery({ - queryKey: ['degree', 'isSetup'], // TODO-OLLI(pm): fix this key + queryKey: ['isSetup'], // TODO-OLLI(pm): fix this key queryFn: () => getUserIsSetup(identity.session_token) }); diff --git a/frontend/src/pages/TermPlanner/TermPlanner.tsx b/frontend/src/pages/TermPlanner/TermPlanner.tsx index 8ad4d3b2e..5e29e11f5 100644 --- a/frontend/src/pages/TermPlanner/TermPlanner.tsx +++ b/frontend/src/pages/TermPlanner/TermPlanner.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import type { OnDragEndResponder, OnDragStartResponder } from 'react-beautiful-dnd'; -import { useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQueries, useQueryClient } from '@tanstack/react-query'; import { Badge } from 'antd'; import { Course } from 'types/api'; import { PlannedToTerm, Term, UnPlannedToTerm, UnscheduleCourse } from 'types/planner'; @@ -14,20 +14,19 @@ import { ValidatesResponse } from 'types/userResponse'; import { getCourseForYearsInfo } from 'utils/api/coursesApi'; -import { validateTermPlanner } from 'utils/api/plannerApi'; import { useSetPlannedCourseToTermMutation, useSetUnplannedCourseToTermMutation, useUnscheduleCourseMutation, useUserCourses, - useUserPlanner + useUserPlanner, + useUserTermValidations } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import PageTemplate from 'components/PageTemplate'; import Spinner from 'components/Spinner'; import { LIVE_YEAR } from 'config/constants'; import useSettings from 'hooks/useSettings'; -import useToken from 'hooks/useToken'; import { GridItem } from './common/styles'; import HideYearTooltip from './HideYearTooltip'; import OptionsHeader from './OptionsHeader'; @@ -67,7 +66,6 @@ type CodeToCourseYearsMap = { [code: string]: { [year: number]: Course } }; type YearToCoursesMap = { [year: number]: { [code: string]: Course } }; const TermPlanner = () => { - const token = useToken(); const [draggingCourse, setDraggingCourse] = useState(''); const { hiddenYears } = useSettings(); @@ -82,10 +80,8 @@ const TermPlanner = () => { const coursesQuery = useUserCourses(); const courses: CoursesResponse = coursesQuery.data ?? badCourses; - const validateQuery = useQuery({ - queryKey: ['validate'], - queryFn: () => validateTermPlanner(token) - }); + const validateQuery = useUserTermValidations(); + const validations: ValidatesResponse = validateQuery.data ?? badValidations; const validYears = [...Array(planner.years.length).keys()].map((y) => y + planner.startYear); diff --git a/frontend/src/utils/apiHooks/user/hookHelpers.ts b/frontend/src/utils/apiHooks/hookHelpers.ts similarity index 54% rename from frontend/src/utils/apiHooks/user/hookHelpers.ts rename to frontend/src/utils/apiHooks/hookHelpers.ts index b9b595788..9b15cbc8f 100644 --- a/frontend/src/utils/apiHooks/user/hookHelpers.ts +++ b/frontend/src/utils/apiHooks/hookHelpers.ts @@ -1,5 +1,6 @@ import { QueryClient, + QueryKey, useMutation, UseMutationOptions, useQuery, @@ -8,32 +9,41 @@ import { } from '@tanstack/react-query'; import useIdentity from 'hooks/useIdentity'; -export type CreateUserQueryOptions = Omit; -export type UserQueryHookOptions = { +// type KeyFnBase = (...args: FArgs) => QueryKey; +type UserQueryKey = ['user', uid: string, ...Key]; + +export type CreateUserQueryOptions = Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' | 'enabled' +>; +export type UserQueryHookOptions = { allowUnsetToken?: boolean; queryClient?: QueryClient; - queryOptions?: Omit; + queryOptions?: Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' + >; }; export function createUserQueryHook< - const Key extends string[], + const Key extends QueryKey, const FArgs extends unknown[], const FRet >( - keySuffix: Key, + keySuffixFn: (...args: FArgs) => Key, fn: (token: string, ...args: FArgs) => Promise, - baseOptions?: CreateUserQueryOptions + baseOptions?: CreateUserQueryOptions ) { - return (options?: UserQueryHookOptions, ...args: FArgs) => { + return (options?: UserQueryHookOptions, ...args: FArgs) => { const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; const query = useQuery( { - queryKey: ['user', userId].concat(keySuffix), + queryKey: ['user', userId!, ...keySuffixFn(...args)], queryFn: () => fn(token!, ...args), - // ...baseOptions, // TODO-olli - // ...options?.queryOptions, + ...baseOptions, + ...options?.queryOptions, enabled: options?.queryOptions?.enabled && token !== undefined }, @@ -48,6 +58,7 @@ export type CreateUserMutationOptions = Omit< UseMutationOptions, 'mutationFn' >; + export type UserMutationHookOptions = { allowUnsetToken?: boolean; queryClient?: QueryClient; @@ -55,7 +66,7 @@ export type UserMutationHookOptions = { }; export function createUserMutationHook< - const Keys extends true | string[][], // true signifies you want a full invalidate and clear + const Keys extends true | QueryKey[], // true signifies you want a full invalidate and clear const FRet, const FArg = void >( @@ -82,12 +93,11 @@ export function createUserMutationHook< }); // TODO-olli: figure out which one we want to do... - queryClient.clear(); - queryClient.resetQueries(); + // queryClient.resetQueries(); } else { invalidationKeySuffixes.forEach((key) => queryClient.invalidateQueries({ - queryKey: ['user', userId].concat(key) + queryKey: ['user', userId, ...key] }) ); } @@ -105,3 +115,43 @@ export function createUserMutationHook< return mutation; }; } + +type StaticQueryKey = ['static', ...Key]; + +export type CreateStaticQueryOptions = Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' +>; +export type StaticQueryHookOptions = { + allowUnsetToken?: boolean; + queryClient?: QueryClient; + queryOptions?: Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' + >; +}; + +export function createStaticQueryHook< + const Key extends QueryKey, + const FArgs extends unknown[], + const FRet +>( + keySuffixFn: (...args: FArgs) => Key, + fn: (...args: FArgs) => Promise, + baseOptions?: CreateStaticQueryOptions +) { + return (options?: StaticQueryHookOptions, ...args: FArgs) => { + const query = useQuery( + { + queryKey: ['static', ...keySuffixFn(...args)], + queryFn: () => fn(...args), + + ...baseOptions, + ...options?.queryOptions + }, + options?.queryClient + ); + + return query; + }; +} diff --git a/frontend/src/utils/apiHooks/static.ts b/frontend/src/utils/apiHooks/static.ts new file mode 100644 index 000000000..df015f689 --- /dev/null +++ b/frontend/src/utils/apiHooks/static.ts @@ -0,0 +1,10 @@ +import { getCourseRating } from 'utils/api/unilectivesApi'; +import { createStaticQueryHook } from './hookHelpers'; + +export const useCourseRatingQuery = createStaticQueryHook< + ['courseRating', string], + [string], + Awaited> +>((code) => ['courseRating', code], getCourseRating); + +export const x = 100; diff --git a/frontend/src/utils/apiHooks/user/index.ts b/frontend/src/utils/apiHooks/user/index.ts index 702b2f914..f2937e8d1 100644 --- a/frontend/src/utils/apiHooks/user/index.ts +++ b/frontend/src/utils/apiHooks/user/index.ts @@ -19,7 +19,16 @@ import { useUpdateMarkMutation, useUpdateStartYearMutation } from './mutations'; -import { useUserCourses, useUserDegree, useUserPlanner } from './queries'; +import { + useUserAllUnlocked, + useUserCourses, + useUserCoursesUnlockedWhenTaken, + useUserDegree, + useUserPlanner, + useUserSettings, + useUserSetupState, + useUserTermValidations +} from './queries'; export { useAddToUnplannedMutation, @@ -41,7 +50,12 @@ export { useUpdateDegreeLengthMutation, useUpdateMarkMutation, useUpdateStartYearMutation, + useUserAllUnlocked, useUserCourses, + useUserCoursesUnlockedWhenTaken, useUserDegree, - useUserPlanner + useUserPlanner, + useUserSettings, + useUserSetupState, + useUserTermValidations }; diff --git a/frontend/src/utils/apiHooks/user/mutations.ts b/frontend/src/utils/apiHooks/user/mutations.ts index 530dfca21..b48560b10 100644 --- a/frontend/src/utils/apiHooks/user/mutations.ts +++ b/frontend/src/utils/apiHooks/user/mutations.ts @@ -21,7 +21,7 @@ import { updateDegreeLength, updateStartYear } from 'utils/api/userApi'; -import { createUserMutationHook } from './hookHelpers'; +import { createUserMutationHook } from '../hookHelpers'; export const useRemoveCourseMutation = createUserMutationHook([['planner']], removeCourse); diff --git a/frontend/src/utils/apiHooks/user/queries.ts b/frontend/src/utils/apiHooks/user/queries.ts index 099dad8c3..d8947aee8 100644 --- a/frontend/src/utils/apiHooks/user/queries.ts +++ b/frontend/src/utils/apiHooks/user/queries.ts @@ -1,8 +1,36 @@ -import { getUserCourses, getUserDegree, getUserPlanner } from 'utils/api/userApi'; -import { createUserQueryHook } from './hookHelpers'; +import { getAllUnlockedCourses, getCoursesUnlockedWhenTaken } from 'utils/api/coursesApi'; +import { validateTermPlanner } from 'utils/api/plannerApi'; +import { + getUserCourses, + getUserDegree, + getUserIsSetup, + getUserPlanner, + getUserSettings +} from 'utils/api/userApi'; +import { createUserQueryHook } from '../hookHelpers'; -export const useUserCourses = createUserQueryHook(['planner', 'courses'], getUserCourses); +export const useUserCourses = createUserQueryHook(() => ['planner', 'courses'], getUserCourses); -export const useUserPlanner = createUserQueryHook(['planner'], getUserPlanner); +export const useUserPlanner = createUserQueryHook(() => ['planner'], getUserPlanner); -export const useUserDegree = createUserQueryHook(['degree'], getUserDegree); +export const useUserDegree = createUserQueryHook(() => ['degree'], getUserDegree); + +export const useUserCoursesUnlockedWhenTaken = createUserQueryHook( + (courseCode) => ['planner', 'courses', 'unlockedWhenTaken', courseCode], + getCoursesUnlockedWhenTaken +); + +export const useUserTermValidations = createUserQueryHook( + () => ['planner', 'validation'], + validateTermPlanner +); + +export const useUserSettings = createUserQueryHook(() => ['settings'], getUserSettings); + +export const useUserAllUnlocked = createUserQueryHook( + () => ['planner', 'courses', 'allUnlocked'], + getAllUnlockedCourses +); + +// TODO-olli: refect on window +export const useUserSetupState = createUserQueryHook(() => ['isSetup'], getUserIsSetup); From d8e1f619c2667e7f8f731d60933e9b69686f2e67 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 2 Nov 2024 17:19:53 +1100 Subject: [PATCH 11/13] Add more static queries --- .../CourseDescriptionPanel.tsx | 17 ++--------- .../CourseBanner/CourseBanner.tsx | 8 ++--- .../CourseSelector/CourseMenu/CourseMenu.tsx | 15 +++++----- .../Dashboard/Dashboard.tsx | 10 ++----- .../ProgressionChecker/ProgressionChecker.tsx | 17 ++++++----- frontend/src/utils/apiHooks/static.ts | 29 ++++++++++++++++++- 6 files changed, 52 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx b/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx index d8663b31e..765177f22 100644 --- a/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx +++ b/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; import { CoursesResponse } from 'types/userResponse'; -import { getCourseInfo, getCoursePrereqs } from 'utils/api/coursesApi'; -import { getCourseTimetable } from 'utils/api/timetableApi'; +import { useCourseInfoQuery } from 'utils/apiHooks/static'; import { useUserCoursesUnlockedWhenTaken } from 'utils/apiHooks/user'; import getEnrolmentCapacity from 'utils/getEnrolmentCapacity'; import { unwrapSettledPromise } from 'utils/queryUtils'; @@ -19,14 +17,6 @@ import S from './styles'; const { Title, Text } = Typography; -const getCourseExtendedInfo = async (courseCode: string) => { - return Promise.allSettled([ - getCourseInfo(courseCode), - getCoursePrereqs(courseCode), - getCourseTimetable(courseCode) - ]); -}; - type CourseDescriptionPanelProps = { className?: string; courseCode: string; @@ -45,10 +35,7 @@ const CourseDescriptionPanel = ({ const coursesUnlockedQuery = useUserCoursesUnlockedWhenTaken({}, courseCode); - const courseInfoQuery = useQuery({ - queryKey: ['courseInfo', courseCode], - queryFn: () => getCourseExtendedInfo(courseCode) - }); + const courseInfoQuery = useCourseInfoQuery({}, courseCode); const loadingWrapper = ( diff --git a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx index 9ede6ee76..898083d32 100644 --- a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx +++ b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; import { CoursesResponse } from 'types/userResponse'; -import { fetchAllDegrees } from 'utils/api/programsApi'; +import { useProgramsQuery } from 'utils/apiHooks/static'; import { useUserDegree } from 'utils/apiHooks/user'; import CourseSearchBar from 'components/CourseSearchBar'; import { useAppDispatch } from 'hooks'; @@ -19,10 +18,7 @@ const CourseBanner = ({ courses }: CourseBannerProps) => { const dispatch = useAppDispatch(); const degreeQuery = useUserDegree(); - const allPrograms = useQuery({ - queryKey: ['programs'], - queryFn: fetchAllDegrees - }); + const allPrograms = useProgramsQuery(); const getUserProgramTitle = (): string => { if (degreeQuery.data?.programCode) { diff --git a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx index 01a2dcbfb..2fe0c35ad 100644 --- a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx +++ b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx @@ -1,12 +1,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useQuery } from '@tanstack/react-query'; import type { MenuProps } from 'antd'; import { CourseUnitsStructure, MenuDataStructure, MenuDataSubgroup } from 'types/courseMenu'; import { CourseValidation } from 'types/courses'; import { ProgramStructure } from 'types/structure'; import { CoursesResponse, DegreeResponse } from 'types/userResponse'; -import { getProgramStructure } from 'utils/api/programsApi'; +import { useStructureQuery } from 'utils/apiHooks/static'; import { useAddToUnplannedMutation, useRemoveCourseMutation, @@ -40,11 +39,13 @@ const SubgroupTitle = ({ title, currUOC, totalUOC }: SubgroupTitleProps) => ( ); const CourseMenu = ({ courses, degree }: CourseMenuProps) => { - const structureQuery = useQuery({ - queryKey: ['structure', degree], - queryFn: () => getProgramStructure(degree!.programCode, degree!.specs), - enabled: !!degree - }); + const structureQuery = useStructureQuery( + { + queryOptions: { enabled: degree !== undefined } + }, + degree!.programCode, + degree!.specs + ); const coursesStateQuery = useUserAllUnlocked(); diff --git a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx index 9838728e0..925752a6b 100644 --- a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx @@ -2,11 +2,10 @@ import React, { useMemo } from 'react'; import { scroller } from 'react-scroll'; import { ArrowDownOutlined } from '@ant-design/icons'; import { useSpring } from '@react-spring/web'; -import { useQuery } from '@tanstack/react-query'; import { Button, Typography } from 'antd'; import { ProgramStructure } from 'types/structure'; import { badCourses, badDegree } from 'types/userResponse'; -import { fetchAllDegrees } from 'utils/api/programsApi'; +import { useProgramsQuery } from 'utils/apiHooks/static'; import { useUserCourses, useUserDegree } from 'utils/apiHooks/user'; import getNumTerms from 'utils/getNumTerms'; import LiquidProgressChart from 'components/LiquidProgressChart'; @@ -45,12 +44,7 @@ const Dashboard = ({ isLoading, structure, totalUOC, freeElectivesUOC }: Props) const degree = degreeQuery.data || badDegree; const { programCode } = degree; - const programName = (useQuery({ - queryKey: ['program'], - queryFn: fetchAllDegrees - }).data?.programs || { - [programCode]: '' - })[programCode]; + const programName = (useProgramsQuery().data?.programs || { [programCode]: '' })[programCode]; let completedUOC = 0; Object.keys(courses).forEach((courseCode) => { diff --git a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx index e9f4c33c2..0c5b805cd 100644 --- a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx +++ b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx @@ -5,7 +5,6 @@ import { EyeInvisibleOutlined, TableOutlined } from '@ant-design/icons'; -import { useQuery } from '@tanstack/react-query'; import { Button, Divider, Typography } from 'antd'; import { ProgressionAdditionalCourses, @@ -16,7 +15,7 @@ import { } from 'types/progressionViews'; import { ProgramStructure } from 'types/structure'; import { badCourses, badPlanner } from 'types/userResponse'; -import { getProgramStructure } from 'utils/api/programsApi'; +import { useStructureQuery } from 'utils/apiHooks/static'; import { useUserCourses, useUserDegree, useUserPlanner } from 'utils/apiHooks/user'; import getNumTerms from 'utils/getNumTerms'; import openNotification from 'utils/openNotification'; @@ -39,11 +38,15 @@ const ProgressionChecker = () => { const degreeQuery = useUserDegree(); const degree = degreeQuery.data; - const structureQuery = useQuery({ - queryKey: ['structure', degree?.programCode, degree?.specs], - queryFn: () => getProgramStructure(degree!.programCode, degree!.specs), - enabled: degree !== undefined - }); + const structureQuery = useStructureQuery( + { + queryOptions: { + enabled: degree !== undefined + } + }, + degree!.programCode, + degree!.specs + ); const structure: ProgramStructure = structureQuery.data?.structure ?? {}; const uoc = structureQuery.data?.uoc ?? 0; diff --git a/frontend/src/utils/apiHooks/static.ts b/frontend/src/utils/apiHooks/static.ts index df015f689..8f723c0e5 100644 --- a/frontend/src/utils/apiHooks/static.ts +++ b/frontend/src/utils/apiHooks/static.ts @@ -1,3 +1,6 @@ +import { getCourseInfo, getCoursePrereqs } from 'utils/api/coursesApi'; +import { fetchAllDegrees, getProgramStructure } from 'utils/api/programsApi'; +import { getCourseTimetable } from 'utils/api/timetableApi'; import { getCourseRating } from 'utils/api/unilectivesApi'; import { createStaticQueryHook } from './hookHelpers'; @@ -7,4 +10,28 @@ export const useCourseRatingQuery = createStaticQueryHook< Awaited> >((code) => ['courseRating', code], getCourseRating); -export const x = 100; +const getCourseExtendedInfo = async (courseCode: string) => { + return Promise.allSettled([ + getCourseInfo(courseCode), + getCoursePrereqs(courseCode), + getCourseTimetable(courseCode) + ]); +}; + +export const useCourseInfoQuery = createStaticQueryHook< + ['courseInfo', string], + [string], + Awaited> +>((code) => ['courseInfo', code], getCourseExtendedInfo); + +export const useProgramsQuery = createStaticQueryHook< + ['programs'], + [], + Awaited> +>(() => ['programs'], fetchAllDegrees); + +export const useStructureQuery = createStaticQueryHook< + ['structure', string, string[]], + [string, string[]], + Awaited> +>((programCode, specs) => ['structure', programCode, specs], getProgramStructure); From 9d8ce6f8a6f66c6c439de551906a11ba48008389 Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Mon, 4 Nov 2024 00:15:39 +1100 Subject: [PATCH 12/13] wip: finished all the static hooks --- .../PrerequisiteTree/PrerequisiteTree.tsx | 23 +++---- .../DegreeWizard/DegreeStep/DegreeStep.tsx | 19 +++-- .../src/pages/DegreeWizard/DegreeWizard.tsx | 15 ++-- .../SpecialisationStep/SpecialisationStep.tsx | 16 ++--- .../CourseGraph/CourseGraph.tsx | 15 ++-- frontend/src/utils/apiHooks/hookHelpers.ts | 37 +++++----- frontend/src/utils/apiHooks/static.ts | 69 ++++++++++++------- 7 files changed, 106 insertions(+), 88 deletions(-) diff --git a/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx b/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx index 52086e400..933f38c6d 100644 --- a/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx +++ b/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import type { Item, TreeGraph, TreeGraphData } from '@antv/g6'; -import { useQuery } from '@tanstack/react-query'; -import { getCourseChildren, getCoursePrereqs } from 'utils/api/coursesApi'; +import { useCourseChildrenQuery, useCoursePrereqsQuery } from 'utils/apiHooks/static'; import Spinner from 'components/Spinner'; import GRAPH_STYLE from './config'; import TREE_CONSTANTS from './constants'; @@ -18,17 +17,15 @@ const PrerequisiteTree = ({ courseCode, onCourseClick }: Props) => { const graphRef = useRef(null); const ref = useRef(null); - const childrenQuery = useQuery({ - queryKey: ['course', 'children', courseCode], // TODO: make this key reasonable when we rework all keys - queryFn: () => getCourseChildren(courseCode), - select: (data) => data.courses - }); - - const prereqsQuery = useQuery({ - queryKey: ['course', 'prereqs', courseCode], - queryFn: () => getCoursePrereqs(courseCode), - select: (data) => data.courses - }); + const childrenQuery = useCourseChildrenQuery( + { queryOptions: { select: (data) => data.courses } }, + courseCode + ); + + const prereqsQuery = useCoursePrereqsQuery( + { queryOptions: { select: (data) => data.courses } }, + courseCode + ); useEffect(() => { // if the course code changes, force a reload (REQUIRED) diff --git a/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx b/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx index a0b283deb..67a98d1a4 100644 --- a/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx +++ b/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx @@ -1,10 +1,9 @@ import React, { useState } from 'react'; import { animated, useSpring } from '@react-spring/web'; -import { useQuery } from '@tanstack/react-query'; import { Select, Typography } from 'antd'; import { fuzzy } from 'fast-fuzzy'; import { DegreeWizardPayload } from 'types/degreeWizard'; -import { fetchAllDegrees } from 'utils/api/programsApi'; +import { useAllDegreesQuery } from 'utils/apiHooks/static'; import springProps from '../common/spring'; import Steps from '../common/steps'; import CS from '../common/styles'; @@ -19,14 +18,14 @@ type Props = { }; const DegreeStep = ({ incrementStep, setDegreeInfo }: Props) => { - const allDegreesQuery = useQuery({ - queryKey: ['programs'], - queryFn: fetchAllDegrees, - select: (data) => - Object.keys(data.programs).map((code) => ({ - label: `${code} ${data.programs[code]}`, - value: `${code} ${data.programs[code]}` - })) + const allDegreesQuery = useAllDegreesQuery({ + queryOptions: { + select: (data) => + Object.keys(data.programs).map((code) => ({ + label: `${code} ${data.programs[code]}`, + value: `${code} ${data.programs[code]}` + })) + } }); const allDegrees = allDegreesQuery.data ?? []; diff --git a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx index 9181a366e..aa4b6982d 100644 --- a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx +++ b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { scroller } from 'react-scroll'; -import { useQuery } from '@tanstack/react-query'; import { Button, Typography } from 'antd'; import { DegreeWizardPayload } from 'types/degreeWizard'; -import { getSpecialisationTypes } from 'utils/api/specsApi'; +import { useSpecTypesQuery } from 'utils/apiHooks/static'; import { useResetDegreeMutation, useUserSetupState } from 'utils/apiHooks/user'; import openNotification from 'utils/openNotification'; import MigrationModal from 'components/MigrationModal'; @@ -35,12 +34,12 @@ const DegreeWizard = () => { const isSetup = useUserSetupState().data; const navigate = useNavigate(); - const specTypesQuery = useQuery({ - queryKey: ['specialisations', 'types', programCode], - queryFn: () => getSpecialisationTypes(programCode), - select: (data) => data.types, - enabled: programCode !== '' - }); + const specTypesQuery = useSpecTypesQuery( + { + queryOptions: { select: (data) => data.types, enabled: programCode !== '' } + }, + programCode + ); const specs = specTypesQuery.data ?? DEFAULT_SPEC_TYPES; const stepList = ['year', 'degree'].concat(specs).concat(['start browsing']); diff --git a/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx b/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx index d4d8117de..fd807127f 100644 --- a/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx +++ b/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { animated, useSpring } from '@react-spring/web'; -import { useQuery } from '@tanstack/react-query'; import { Button, Select, Typography } from 'antd'; import { DegreeWizardPayload } from 'types/degreeWizard'; -import { getSpecialisationsForProgram } from 'utils/api/specsApi'; +import { useSpecsForProgramQuery } from 'utils/apiHooks/static'; import openNotification from 'utils/openNotification'; import Spinner from 'components/Spinner'; import springProps from '../common/spring'; @@ -45,12 +44,13 @@ const SpecialisationStep = ({ })); }; - const specsQuery = useQuery({ - queryKey: ['specialisations', degreeInfo.programCode, type], - queryFn: () => getSpecialisationsForProgram(degreeInfo.programCode, type), - select: (data) => data.spec, - enabled: degreeInfo.programCode !== '' - }); + const specsQuery = useSpecsForProgramQuery( + { + queryOptions: { select: (data) => data.spec, enabled: degreeInfo.programCode !== '' } + }, + degreeInfo.programCode, + type + ); const options = specsQuery.data; type SelectGroup = { diff --git a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx index 66cd50940..0bf86972c 100644 --- a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx +++ b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx @@ -6,11 +6,10 @@ import { ZoomOutOutlined } from '@ant-design/icons'; import type { Graph, GraphOptions, IG6GraphEvent, INode, Item } from '@antv/g6'; -import { useQuery } from '@tanstack/react-query'; import { Switch } from 'antd'; import { CourseEdge } from 'types/api'; import { useDebouncedCallback } from 'use-debounce'; -import { getProgramGraph } from 'utils/api/programsApi'; +import { useProgramGraphQuery } from 'utils/apiHooks/static'; import { useUserAllUnlocked, useUserCourses, @@ -75,11 +74,13 @@ const CourseGraph = ({ const containerRef = useRef(null); const [showingUnlockedCourses, setShowingUnlockedCourses] = useState(false); - const programGraphQuery = useQuery({ - queryKey: ['graph', { code: degreeQuery.data?.programCode, specs: degreeQuery.data?.specs }], - queryFn: () => getProgramGraph(degreeQuery.data!.programCode, degreeQuery.data!.specs), - enabled: !degreeQuery.isPending && degreeQuery.data && degreeQuery.isSuccess - }); + const programGraphQuery = useProgramGraphQuery( + { + queryOptions: { enabled: !degreeQuery.isPending && degreeQuery.data && degreeQuery.isSuccess } + }, + degreeQuery.data!.programCode, + degreeQuery.data!.specs + ); const coursesStateQuery = useUserAllUnlocked(); diff --git a/frontend/src/utils/apiHooks/hookHelpers.ts b/frontend/src/utils/apiHooks/hookHelpers.ts index 9b15cbc8f..2960d621e 100644 --- a/frontend/src/utils/apiHooks/hookHelpers.ts +++ b/frontend/src/utils/apiHooks/hookHelpers.ts @@ -13,14 +13,14 @@ import useIdentity from 'hooks/useIdentity'; type UserQueryKey = ['user', uid: string, ...Key]; export type CreateUserQueryOptions = Omit< - UseQueryOptions>, - 'queryKey' | 'queryFn' | 'enabled' + UseQueryOptions>, + 'queryKey' | 'queryFn' | 'enabled' | 'select' >; -export type UserQueryHookOptions = { +export type UserQueryHookOptions = { allowUnsetToken?: boolean; queryClient?: QueryClient; queryOptions?: Omit< - UseQueryOptions>, + UseQueryOptions>, 'queryKey' | 'queryFn' >; }; @@ -34,10 +34,13 @@ export function createUserQueryHook< fn: (token: string, ...args: FArgs) => Promise, baseOptions?: CreateUserQueryOptions ) { - return (options?: UserQueryHookOptions, ...args: FArgs) => { + return ( + options?: UserQueryHookOptions, + ...args: FArgs + ) => { const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; - const query = useQuery( + return useQuery>( { queryKey: ['user', userId!, ...keySuffixFn(...args)], queryFn: () => fn(token!, ...args), @@ -49,8 +52,6 @@ export function createUserQueryHook< }, options?.queryClient ); - - return query; }; } @@ -119,14 +120,13 @@ export function createUserMutationHook< type StaticQueryKey = ['static', ...Key]; export type CreateStaticQueryOptions = Omit< - UseQueryOptions>, - 'queryKey' | 'queryFn' + UseQueryOptions>, + 'queryKey' | 'queryFn' | 'enabled' | 'select' >; -export type StaticQueryHookOptions = { - allowUnsetToken?: boolean; +export type StaticQueryHookOptions = { queryClient?: QueryClient; queryOptions?: Omit< - UseQueryOptions>, + UseQueryOptions>, 'queryKey' | 'queryFn' >; }; @@ -136,12 +136,15 @@ export function createStaticQueryHook< const FArgs extends unknown[], const FRet >( - keySuffixFn: (...args: FArgs) => Key, + keySuffixFn: (...args: Parameters) => Key, fn: (...args: FArgs) => Promise, baseOptions?: CreateStaticQueryOptions ) { - return (options?: StaticQueryHookOptions, ...args: FArgs) => { - const query = useQuery( + return ( + options?: StaticQueryHookOptions, + ...args: FArgs + ) => { + return useQuery>( { queryKey: ['static', ...keySuffixFn(...args)], queryFn: () => fn(...args), @@ -151,7 +154,5 @@ export function createStaticQueryHook< }, options?.queryClient ); - - return query; }; } diff --git a/frontend/src/utils/apiHooks/static.ts b/frontend/src/utils/apiHooks/static.ts index 8f723c0e5..c04f1b6a7 100644 --- a/frontend/src/utils/apiHooks/static.ts +++ b/frontend/src/utils/apiHooks/static.ts @@ -1,14 +1,14 @@ -import { getCourseInfo, getCoursePrereqs } from 'utils/api/coursesApi'; -import { fetchAllDegrees, getProgramStructure } from 'utils/api/programsApi'; +import { getCourseChildren, getCourseInfo, getCoursePrereqs } from 'utils/api/coursesApi'; +import { fetchAllDegrees, getProgramGraph, getProgramStructure } from 'utils/api/programsApi'; +import { getSpecialisationsForProgram, getSpecialisationTypes } from 'utils/api/specsApi'; import { getCourseTimetable } from 'utils/api/timetableApi'; import { getCourseRating } from 'utils/api/unilectivesApi'; import { createStaticQueryHook } from './hookHelpers'; -export const useCourseRatingQuery = createStaticQueryHook< - ['courseRating', string], - [string], - Awaited> ->((code) => ['courseRating', code], getCourseRating); +export const useCourseRatingQuery = createStaticQueryHook( + (code) => ['courseRating', code], + getCourseRating +); const getCourseExtendedInfo = async (courseCode: string) => { return Promise.allSettled([ @@ -18,20 +18,41 @@ const getCourseExtendedInfo = async (courseCode: string) => { ]); }; -export const useCourseInfoQuery = createStaticQueryHook< - ['courseInfo', string], - [string], - Awaited> ->((code) => ['courseInfo', code], getCourseExtendedInfo); - -export const useProgramsQuery = createStaticQueryHook< - ['programs'], - [], - Awaited> ->(() => ['programs'], fetchAllDegrees); - -export const useStructureQuery = createStaticQueryHook< - ['structure', string, string[]], - [string, string[]], - Awaited> ->((programCode, specs) => ['structure', programCode, specs], getProgramStructure); +export const useCourseInfoQuery = createStaticQueryHook( + (code) => ['courseInfo', code], + getCourseExtendedInfo +); + +export const useProgramsQuery = createStaticQueryHook(() => ['programs'], fetchAllDegrees); + +export const useStructureQuery = createStaticQueryHook( + (programCode, specs) => ['structure', programCode, specs], + getProgramStructure +); + +export const useCourseChildrenQuery = createStaticQueryHook( + (courseCode) => ['course', 'children', courseCode], + getCourseChildren +); + +export const useCoursePrereqsQuery = createStaticQueryHook( + (courseCode) => ['course', 'prereqs', courseCode], + getCoursePrereqs +); + +export const useAllDegreesQuery = createStaticQueryHook(() => ['programs'], fetchAllDegrees); + +export const useSpecsForProgramQuery = createStaticQueryHook( + (programCode, specType) => ['specialisations', programCode, specType], + getSpecialisationsForProgram +); + +export const useSpecTypesQuery = createStaticQueryHook( + (programCode) => ['specialisations', 'types', programCode], + getSpecialisationTypes +); + +export const useProgramGraphQuery = createStaticQueryHook( + (programCode, specs) => ['graph', { code: programCode, specs }], + getProgramGraph +); From 92cae78ce1f0598b4d40c40f2539eb38126b6927 Mon Sep 17 00:00:00 2001 From: ollibowers <80164276+ollibowers@users.noreply.github.com> Date: Mon, 4 Nov 2024 01:16:29 +1100 Subject: [PATCH 13/13] wip: temporary fixed non-null assertion issues --- frontend/src/components/Auth/RequireToken.tsx | 28 ++++++++++--------- .../CourseSelector/CourseMenu/CourseMenu.tsx | 5 ++-- .../CourseGraph/CourseGraph.tsx | 5 ++-- .../ProgressionChecker/ProgressionChecker.tsx | 5 ++-- frontend/src/utils/apiHooks/hookHelpers.ts | 12 ++++---- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/Auth/RequireToken.tsx b/frontend/src/components/Auth/RequireToken.tsx index ad3a0df34..614e9fb86 100644 --- a/frontend/src/components/Auth/RequireToken.tsx +++ b/frontend/src/components/Auth/RequireToken.tsx @@ -26,20 +26,22 @@ const RequireToken = ({ needSetup }: Props) => { useEffect(() => { // TODO-OLLI(pm): wont need this when we get new notification hook - if (token === undefined || !!error) { - openNotification({ - type: 'error', - message: 'Error', - description: 'You must be logged in before visiting this page 🙂' - }); - } else if (isSetup === false && !!needSetup) { - openNotification({ - type: 'warning', - message: 'Warning', - description: 'You must setup your degree before visiting this page 🙂' - }); + if (!isPending) { + if (token === undefined || !!error) { + openNotification({ + type: 'error', + message: 'Error', + description: 'You must be logged in before visiting this page 🙂' + }); + } else if (isSetup === false && !!needSetup) { + openNotification({ + type: 'warning', + message: 'Warning', + description: 'You must setup your degree before visiting this page 🙂' + }); + } } - }, [token, isSetup, error, needSetup]); + }, [token, isPending, isSetup, error, needSetup]); if (token === undefined) { return ; diff --git a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx index 2fe0c35ad..01347f66a 100644 --- a/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx +++ b/frontend/src/pages/CourseSelector/CourseMenu/CourseMenu.tsx @@ -43,8 +43,9 @@ const CourseMenu = ({ courses, degree }: CourseMenuProps) => { { queryOptions: { enabled: degree !== undefined } }, - degree!.programCode, - degree!.specs + // TODO-olli: this is ugly, but we will need some interesting type rules to allow this otherwise + degree?.programCode ?? '', + degree?.specs ?? [] ); const coursesStateQuery = useUserAllUnlocked(); diff --git a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx index 0bf86972c..3d1c8e248 100644 --- a/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx +++ b/frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx @@ -78,8 +78,9 @@ const CourseGraph = ({ { queryOptions: { enabled: !degreeQuery.isPending && degreeQuery.data && degreeQuery.isSuccess } }, - degreeQuery.data!.programCode, - degreeQuery.data!.specs + // TODO-olli: this is ugly, but we will need some interesting type rules to allow this otherwise + degreeQuery.data?.programCode ?? '', + degreeQuery.data?.specs ?? [] ); const coursesStateQuery = useUserAllUnlocked(); diff --git a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx index 0c5b805cd..47380f181 100644 --- a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx +++ b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx @@ -44,8 +44,9 @@ const ProgressionChecker = () => { enabled: degree !== undefined } }, - degree!.programCode, - degree!.specs + // TODO-olli: this is ugly, but we will need some interesting type rules to allow this otherwise + degree?.programCode ?? '', + degree?.specs ?? [] ); const structure: ProgramStructure = structureQuery.data?.structure ?? {}; const uoc = structureQuery.data?.uoc ?? 0; diff --git a/frontend/src/utils/apiHooks/hookHelpers.ts b/frontend/src/utils/apiHooks/hookHelpers.ts index 2960d621e..bd4b95368 100644 --- a/frontend/src/utils/apiHooks/hookHelpers.ts +++ b/frontend/src/utils/apiHooks/hookHelpers.ts @@ -78,7 +78,7 @@ export function createUserMutationHook< return (options?: UserMutationHookOptions) => { const { userId, token } = useIdentity(options?.allowUnsetToken === true) ?? {}; - const queryClient = useQueryClient(); + const queryClient = useQueryClient(options?.queryClient); const mutation = useMutation( { mutationFn: async (data: FArg) => @@ -89,12 +89,11 @@ export function createUserMutationHook< onSuccess: (data, variables, context) => { if (invalidationKeySuffixes === true) { - queryClient.invalidateQueries({ - queryKey: ['user', userId] - }); - // TODO-olli: figure out which one we want to do... - // queryClient.resetQueries(); + queryClient.resetQueries({ queryKey: ['user', userId] }); + // queryClient.invalidateQueries({ + // queryKey: ['user', userId] + // }); } else { invalidationKeySuffixes.forEach((key) => queryClient.invalidateQueries({ @@ -131,6 +130,7 @@ export type StaticQueryHookOptions = { >; }; +// TODO-olli: allow nullish params with a new option that auto includes them in enabled export function createStaticQueryHook< const Key extends QueryKey, const FArgs extends unknown[],