Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(queries): refactor to a unified query interface #1204

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions frontend/src/components/Auth/PreventToken.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down
39 changes: 19 additions & 20 deletions frontend/src/components/Auth/RequireToken.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,30 +18,30 @@ 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

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 <Navigate to="/login" />;
Expand Down
21 changes: 3 additions & 18 deletions frontend/src/components/CourseCartCard/CourseCartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 (
<S.CourseCardWrapper>
<div role="menuitem" onClick={handleClick}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
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, getCoursesUnlockedWhenTaken } 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';
import {
LoadingCourseDescriptionPanel,
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';

const { Title, Text } = Typography;

const getCourseExtendedInfo = async (courseCode: string) => {
return Promise.allSettled([
getCourseInfo(courseCode),
getCoursePrereqs(courseCode),
getCourseTimetable(courseCode)
]);
};

type CourseDescriptionPanelProps = {
className?: string;
courseCode: string;
Expand All @@ -40,20 +30,12 @@ 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 courseInfoQuery = useQuery({
queryKey: ['courseInfo', courseCode],
queryFn: () => getCourseExtendedInfo(courseCode)
});
const coursesUnlockedQuery = useUserCoursesUnlockedWhenTaken({}, courseCode);

const courseInfoQuery = useCourseInfoQuery({}, courseCode);

const loadingWrapper = (
<S.Wrapper $sidebar={sidebar}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { getUserCourses } from 'utils/api/userApi';
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;
Expand All @@ -28,25 +25,16 @@ const CourseInfoDrawers = ({
pathFrom = [],
unlocked
}: 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)
);
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 (
Expand Down
23 changes: 9 additions & 14 deletions frontend/src/components/CourseSearchBar/CourseSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -49,25 +48,21 @@ 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: (
<SearchResultLabel
courseCode={courseCode}
courseTitle={courseTitle}
isPlanned={isInPlanner(courseCode)}
runMutate={courseMutation.mutate}
runMutate={courseMutation}
/>
),
value: courseCode
Expand Down
25 changes: 2 additions & 23 deletions frontend/src/components/EditMarkModal/EditMarkModal.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -14,9 +11,7 @@ type Props = {
};

const EditMarkModal = ({ code, open, onCancel }: Props) => {
const queryClient = useQueryClient();
const [markValue, setMarkValue] = useState<string | number | undefined>();
const token = useToken();

const letterGrades: Grade[] = ['SY', 'FL', 'PS', 'CR', 'DN', 'HD'];

Expand All @@ -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))) {
Expand Down
26 changes: 4 additions & 22 deletions frontend/src/components/PlannerButton/PlannerButton.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<HTMLElement>) => {
e.stopPropagation();
Expand Down
Loading
Loading