From 1d9c1ec3b1785dd4952710bd6d21bede280ff62b Mon Sep 17 00:00:00 2001 From: Matthew Rowland Date: Fri, 11 Oct 2024 13:27:21 -0700 Subject: [PATCH] chore: Fix create, update, and delete schedules --- .../src/modules/schedule/controller.ts | 3 +- .../backend/src/modules/schedule/formatter.ts | 8 +- .../src/modules/schedule/typedefs/schedule.ts | 11 +- apps/frontend/src/app/Schedule/index.tsx | 37 +------ apps/frontend/src/app/Schedules/Test.tsx | 79 -------------- apps/frontend/src/app/Schedules/index.tsx | 41 ++++++- apps/frontend/src/contexts/ScheduleContext.ts | 11 ++ .../src/hooks/schedules/useCreateSchedule.ts | 63 +++++++++++ .../src/hooks/schedules/useDeleteSchedule.ts | 42 ++++++++ .../src/hooks/schedules/useSchedule.ts | 24 +++++ .../src/hooks/schedules/useSchedules.ts | 16 +++ .../src/hooks/schedules/useUpdateSchedule.ts | 49 +++++++++ apps/frontend/src/hooks/useSchedules.ts | 18 ---- apps/frontend/src/lib/api/schedule.ts | 102 +++++++++++++----- packages/common/src/models/schedule.ts | 26 ++--- 15 files changed, 345 insertions(+), 185 deletions(-) delete mode 100644 apps/frontend/src/app/Schedules/Test.tsx create mode 100644 apps/frontend/src/contexts/ScheduleContext.ts create mode 100644 apps/frontend/src/hooks/schedules/useCreateSchedule.ts create mode 100644 apps/frontend/src/hooks/schedules/useDeleteSchedule.ts create mode 100644 apps/frontend/src/hooks/schedules/useSchedule.ts create mode 100644 apps/frontend/src/hooks/schedules/useSchedules.ts create mode 100644 apps/frontend/src/hooks/schedules/useUpdateSchedule.ts delete mode 100644 apps/frontend/src/hooks/useSchedules.ts diff --git a/apps/backend/src/modules/schedule/controller.ts b/apps/backend/src/modules/schedule/controller.ts index 4136ebf75..d03aa2cff 100644 --- a/apps/backend/src/modules/schedule/controller.ts +++ b/apps/backend/src/modules/schedule/controller.ts @@ -59,7 +59,8 @@ export const updateSchedule = async ( ) => { const schedule = await ScheduleModel.findOneAndUpdate( { _id: id, createdBy: context.user._id }, - input + input, + { new: true } ); if (!schedule) throw new Error("Not found"); diff --git a/apps/backend/src/modules/schedule/formatter.ts b/apps/backend/src/modules/schedule/formatter.ts index a18513554..e4e8e6e8a 100644 --- a/apps/backend/src/modules/schedule/formatter.ts +++ b/apps/backend/src/modules/schedule/formatter.ts @@ -13,10 +13,10 @@ export const formatSchedule = async (schedule: ScheduleType) => { for (const selectedClass of schedule.classes) { const _class = await ClassModel.findOne({ - number: selectedClass.classNumber, + number: selectedClass.number, "course.subjectArea.code": selectedClass.subject, "course.catalogNumber.formatted": selectedClass.courseNumber, - "session.term.name": `${schedule.term.year} ${schedule.term.semester}`, + "session.term.name": `${schedule.year} ${schedule.semester}`, }).lean(); if (!_class) continue; @@ -33,8 +33,8 @@ export const formatSchedule = async (schedule: ScheduleType) => { createdBy: schedule.createdBy, public: schedule.public, classes, - year: schedule.term.year, - semester: schedule.term.semester, + year: schedule.year, + semester: schedule.semester, term: null, events: schedule.events, } as IntermediateSchedule; diff --git a/apps/backend/src/modules/schedule/typedefs/schedule.ts b/apps/backend/src/modules/schedule/typedefs/schedule.ts index 8ab3b4582..14d7c382f 100644 --- a/apps/backend/src/modules/schedule/typedefs/schedule.ts +++ b/apps/backend/src/modules/schedule/typedefs/schedule.ts @@ -23,13 +23,13 @@ const typedef = gql` semester: String! term: Term! public: Boolean! - classes: [SelectedClass!] - events: [Event!] + classes: [SelectedClass!]! + events: [Event!]! } type Query { schedules: [Schedule] @auth - schedule(id: String!): Schedule + schedule(id: ID!): Schedule } input EventInput { @@ -44,7 +44,7 @@ const typedef = gql` input SelectedClassInput { subject: String! courseNumber: String! - classNumber: String! + number: String! sections: [String!]! } @@ -57,7 +57,8 @@ const typedef = gql` input CreateScheduleInput { name: String! - term: TermInput! + year: Int! + semester: String! events: [EventInput!] classes: [SelectedClassInput!] public: Boolean diff --git a/apps/frontend/src/app/Schedule/index.tsx b/apps/frontend/src/app/Schedule/index.tsx index 515365fd1..caff99bc0 100644 --- a/apps/frontend/src/app/Schedule/index.tsx +++ b/apps/frontend/src/app/Schedule/index.tsx @@ -1,45 +1,16 @@ -import { useState } from "react"; - -import { useQuery } from "@apollo/client"; -import { Outlet } from "react-router"; import { useNavigate, useParams } from "react-router-dom"; -import { Boundary, LoadingIndicator } from "@repo/theme"; - -import { GET_SCHEDULE, GetScheduleResponse, IClass, ISection } from "@/lib/api"; - -import { ScheduleContextType } from "./schedule"; +import { useSchedule } from "@/hooks/schedules/useSchedule"; +import { ScheduleIdentifier } from "@/lib/api"; export default function Schedule() { const { scheduleId } = useParams(); const navigate = useNavigate(); - const { data } = useQuery(GET_SCHEDULE, { + const { data: schedule } = useSchedule(scheduleId as ScheduleIdentifier, { onError: () => navigate("/schedules"), - variables: { id: scheduleId }, }); - const [selectedSections, setSelectedSections] = useState([]); - const [classes, setClasses] = useState([]); - const [expanded, setExpanded] = useState([]); - - return data ? ( - - ) : ( - - - - ); + return schedule ? schedule.name : <>; } diff --git a/apps/frontend/src/app/Schedules/Test.tsx b/apps/frontend/src/app/Schedules/Test.tsx deleted file mode 100644 index adf767742..000000000 --- a/apps/frontend/src/app/Schedules/Test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Reference, gql, useMutation } from "@apollo/client"; - -import { - CREATE_SCHEDULE, - CreateScheduleResponse, - DELETE_SCHEDULE, - DeleteScheduleResponse, -} from "@/lib/api"; - -export const [createSchedule] = useMutation( - CREATE_SCHEDULE, - { - variables: { - schedule: { - name: "Untitled schedule", - classes: [ - { - subject: "COMPSCI", - courseNumber: "61B", - classNumber: "001", - sections: [], - }, - ], - term: { - year: 2024, - semester: "Fall", - }, - }, - }, - update(cache, { data }) { - const schedule = data?.createSchedule; - - if (!schedule) return; - - cache.modify({ - fields: { - schedules: (existingSchedules = []) => { - const ref = cache.writeFragment({ - data: schedule, - fragment: gql` - fragment NewSchedule on Schedule { - _id - name - term { - year - semester - } - } - `, - }); - - return [...existingSchedules, ref]; - }, - }, - }); - }, - } -); - -export const [deleteSchedule] = useMutation( - DELETE_SCHEDULE, - { - update(cache, { data }) { - const id = data?.deleteSchedule; - - if (!id) return; - - cache.modify({ - fields: { - schedules(existingSchedules = [], { readField }) { - return existingSchedules.filter( - (scheduleRef: Reference) => id !== readField("_id", scheduleRef) - ); - }, - }, - }); - }, - } -); diff --git a/apps/frontend/src/app/Schedules/index.tsx b/apps/frontend/src/app/Schedules/index.tsx index 17d5cfb12..14291e401 100644 --- a/apps/frontend/src/app/Schedules/index.tsx +++ b/apps/frontend/src/app/Schedules/index.tsx @@ -2,8 +2,12 @@ import { Link } from "react-router-dom"; import { Container } from "@repo/theme"; -import useSchedules from "@/hooks/useSchedules"; +import { useCreateSchedule } from "@/hooks/schedules/useCreateSchedule"; +import { useDeleteSchedule } from "@/hooks/schedules/useDeleteSchedule"; +import useSchedules from "@/hooks/schedules/useSchedules"; +import { useUpdateSchedule } from "@/hooks/schedules/useUpdateSchedule"; import useUser from "@/hooks/useUser"; +import { Semester } from "@/lib/api"; export default function Schedules() { const { data: user, loading: userLoading } = useUser(); @@ -12,6 +16,12 @@ export default function Schedules() { skip: !user, }); + const [deleteSchedule] = useDeleteSchedule(); + + const [updateSchedule] = useUpdateSchedule(); + + const [createSchedule] = useCreateSchedule(); + if (userLoading || schedulesLoading) return <>; if (!user) return <>; @@ -19,10 +29,33 @@ export default function Schedules() { if (schedules) { return ( + {schedules?.map((schedule) => ( - - {schedule.name} - +
+ {schedule.name} + + +
))}
); diff --git a/apps/frontend/src/contexts/ScheduleContext.ts b/apps/frontend/src/contexts/ScheduleContext.ts new file mode 100644 index 000000000..f7937f76c --- /dev/null +++ b/apps/frontend/src/contexts/ScheduleContext.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; + +import { ISchedule } from "@/lib/api"; + +export interface ScheduleContextType { + schedule: ISchedule; +} + +const ScheduleContext = createContext(null); + +export default ScheduleContext; diff --git a/apps/frontend/src/hooks/schedules/useCreateSchedule.ts b/apps/frontend/src/hooks/schedules/useCreateSchedule.ts new file mode 100644 index 000000000..c09c15b37 --- /dev/null +++ b/apps/frontend/src/hooks/schedules/useCreateSchedule.ts @@ -0,0 +1,63 @@ +import { useCallback } from "react"; + +import { gql, useMutation } from "@apollo/client"; + +import { + CREATE_SCHEDULE, + CreateScheduleResponse, + IScheduleInput, +} from "@/lib/api"; + +export const useCreateSchedule = () => { + const mutation = useMutation(CREATE_SCHEDULE, { + update(cache, { data }) { + const schedule = data?.createSchedule; + + if (!schedule) return; + + cache.modify({ + fields: { + schedules: (existingSchedules = []) => { + const reference = cache.writeFragment({ + data: schedule, + fragment: gql` + fragment CreatedSchedule on Schedule { + _id + name + public + createdBy + year + semester + classes { + class { + subject + courseNumber + number + } + selectedSections + } + } + `, + }); + + return [...existingSchedules, reference]; + }, + }, + }); + }, + }); + + const createSchedule = useCallback( + async (schedule: IScheduleInput) => { + const mutate = mutation[0]; + + return await mutate({ variables: { schedule } }); + }, + [mutation] + ); + + return [createSchedule, mutation[1]] as [ + mutate: typeof createSchedule, + result: (typeof mutation)[1], + ]; +}; diff --git a/apps/frontend/src/hooks/schedules/useDeleteSchedule.ts b/apps/frontend/src/hooks/schedules/useDeleteSchedule.ts new file mode 100644 index 000000000..38d6b776a --- /dev/null +++ b/apps/frontend/src/hooks/schedules/useDeleteSchedule.ts @@ -0,0 +1,42 @@ +import { useCallback } from "react"; + +import { useMutation } from "@apollo/client"; + +import { + DELETE_SCHEDULE, + DeleteScheduleResponse, + ScheduleIdentifier, +} from "@/lib/api"; + +export const useDeleteSchedule = () => { + const mutation = useMutation(DELETE_SCHEDULE, { + update(cache, { data }) { + const id = data?.deleteSchedule; + + if (!id) return; + + cache.modify({ + fields: { + schedules: (existingSchedules = [], { readField }) => + existingSchedules.filter( + (reference: any) => readField("_id", reference) !== id + ), + }, + }); + }, + }); + + const deleteSchedule = useCallback( + async (id: ScheduleIdentifier) => { + const mutate = mutation[0]; + + return await mutate({ variables: { id } }); + }, + [mutation] + ); + + return [deleteSchedule, mutation[1]] as [ + mutate: typeof deleteSchedule, + result: (typeof mutation)[1], + ]; +}; diff --git a/apps/frontend/src/hooks/schedules/useSchedule.ts b/apps/frontend/src/hooks/schedules/useSchedule.ts new file mode 100644 index 000000000..7e5cc1415 --- /dev/null +++ b/apps/frontend/src/hooks/schedules/useSchedule.ts @@ -0,0 +1,24 @@ +import { QueryHookOptions, useQuery } from "@apollo/client"; + +import { + GET_SCHEDULE, + GetScheduleResponse, + ScheduleIdentifier, +} from "@/lib/api"; + +export const useSchedule = ( + id: ScheduleIdentifier, + options?: Omit, "variables"> +) => { + const query = useQuery(GET_SCHEDULE, { + ...options, + variables: { + id, + }, + }); + + return { + ...query, + data: query.data?.schedule, + }; +}; diff --git a/apps/frontend/src/hooks/schedules/useSchedules.ts b/apps/frontend/src/hooks/schedules/useSchedules.ts new file mode 100644 index 000000000..089e91577 --- /dev/null +++ b/apps/frontend/src/hooks/schedules/useSchedules.ts @@ -0,0 +1,16 @@ +import { QueryHookOptions, useQuery } from "@apollo/client"; + +import { GET_SCHEDULES, GetSchedulesResponse } from "@/lib/api"; + +const useSchedules = ( + options?: Omit, "variables"> +) => { + const query = useQuery(GET_SCHEDULES, options); + + return { + ...query, + data: query.data?.schedules, + }; +}; + +export default useSchedules; diff --git a/apps/frontend/src/hooks/schedules/useUpdateSchedule.ts b/apps/frontend/src/hooks/schedules/useUpdateSchedule.ts new file mode 100644 index 000000000..fe5db224f --- /dev/null +++ b/apps/frontend/src/hooks/schedules/useUpdateSchedule.ts @@ -0,0 +1,49 @@ +import { useCallback } from "react"; + +import { Reference, useMutation } from "@apollo/client"; + +import { + IScheduleInput, + ScheduleIdentifier, + UPDATE_SCHEDULE, + UpdateScheduleResponse, +} from "@/lib/api"; + +export const useUpdateSchedule = () => { + const mutation = useMutation(UPDATE_SCHEDULE, { + update(cache, { data }) { + const schedule = data?.updateSchedule; + + if (!schedule) return; + + cache.modify({ + fields: { + // TODO: Properly type + schedules: (existingSchedules = [], { readField }) => + existingSchedules.map((reference: Reference) => + readField("_id", reference) === schedule._id + ? { ...reference, ...schedule } + : reference + ), + }, + }); + }, + }); + + const updateSchedule = useCallback( + async ( + id: ScheduleIdentifier, + schedule: Pick + ) => { + const mutate = mutation[0]; + + return await mutate({ variables: { id, schedule } }); + }, + [mutation] + ); + + return [updateSchedule, mutation[1]] as [ + mutate: typeof updateSchedule, + result: (typeof mutation)[1], + ]; +}; diff --git a/apps/frontend/src/hooks/useSchedules.ts b/apps/frontend/src/hooks/useSchedules.ts deleted file mode 100644 index 5bc9ed57c..000000000 --- a/apps/frontend/src/hooks/useSchedules.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { QueryHookOptions, useQuery } from "@apollo/client"; - -import { GET_SCHEDULES, ISchedule } from "@/lib/api"; - -interface Data { - schedules: ISchedule[]; -} - -const useSchedules = (options?: QueryHookOptions) => { - const query = useQuery(GET_SCHEDULES, options); - - return { - ...query, - data: query.data?.schedules, - }; -}; - -export default useSchedules; diff --git a/apps/frontend/src/lib/api/schedule.ts b/apps/frontend/src/lib/api/schedule.ts index 319f24ac3..50f46f944 100644 --- a/apps/frontend/src/lib/api/schedule.ts +++ b/apps/frontend/src/lib/api/schedule.ts @@ -1,14 +1,25 @@ import { gql } from "@apollo/client"; import { IClass } from "../api"; -import { Semester } from "./term"; +import { ITerm, Semester } from "./term"; -export interface ISelectedClass { +export type ScheduleIdentifier = string & { + readonly __brand: unique symbol; +}; + +export interface IScheduleClass { class: IClass; selectedSections: string[]; } -export interface ICustomEvent { +export interface IScheduleClassInput { + subject: string; + courseNumber: string; + number: string; + sections: string[]; +} + +export interface IScheduleEvent { startTime: string; endTime: string; title: string; @@ -17,16 +28,24 @@ export interface ICustomEvent { days: boolean[]; } +export interface IScheduleInput { + name: string; + year: number; + semester: Semester; + classes?: IScheduleClassInput[]; + events?: IScheduleEvent[]; + public?: boolean; +} + export interface ISchedule { - _id: string; + _id: ScheduleIdentifier; name: string; - classes: ISelectedClass[]; - events: ICustomEvent[]; + classes: IScheduleClass[]; + events: IScheduleEvent[]; createdBy: string; - term: { - year: number; - semester: Semester; - }; + term: ITerm; + year: number; + semester: Semester; } export interface GetScheduleResponse { @@ -34,26 +53,53 @@ export interface GetScheduleResponse { } export const GET_SCHEDULE = gql` - query GetSchedule($id: String!) { + query GetSchedule($id: ID!) { schedule(id: $id) { _id name + public + createdBy + year + semester classes { class { - title + subject + courseNumber + number } selectedSections } - term { - year - semester + } + } +`; + +export interface UpdateScheduleResponse { + updateSchedule: ISchedule; +} + +export const UPDATE_SCHEDULE = gql` + mutation UpdateSchedule($id: ID!, $schedule: UpdateScheduleInput!) { + updateSchedule(id: $id, schedule: $schedule) { + _id + name + public + year + createdBy + semester + classes { + class { + subject + courseNumber + number + } + selectedSections } } } `; export interface DeleteScheduleResponse { - deleteSchedule: string; + deleteSchedule: ScheduleIdentifier; } export const DELETE_SCHEDULE = gql` @@ -71,9 +117,17 @@ export const CREATE_SCHEDULE = gql` createSchedule(schedule: $schedule) { _id name - term { - year - semester + public + year + createdBy + semester + classes { + class { + subject + courseNumber + number + } + selectedSections } } } @@ -88,16 +142,16 @@ export const GET_SCHEDULES = gql` schedules { _id name + year + semester classes { class { - title + subject + courseNumber + number } selectedSections } - term { - year - semester - } } } `; diff --git a/packages/common/src/models/schedule.ts b/packages/common/src/models/schedule.ts index 19a0c292e..b233bd2da 100644 --- a/packages/common/src/models/schedule.ts +++ b/packages/common/src/models/schedule.ts @@ -42,7 +42,7 @@ export const selectedClassSchema = new Schema({ trim: true, required: true, }, - classNumber: { + number: { type: String, trim: true, required: true, @@ -75,20 +75,15 @@ export const scheduleSchema = new Schema( required: true, default: [], }, - term: { - type: { - year: { - type: Number, - required: true, - }, - semester: { - type: String, - required: true, - trim: true, - }, - }, + year: { + type: Number, required: true, }, + semester: { + type: String, + required: true, + trim: true, + }, events: { type: [customEventSchema], required: true, @@ -96,10 +91,7 @@ export const scheduleSchema = new Schema( }, }, { - timestamps: { - createdAt: "createdAt", - updatedAt: "updatedAt", - }, + timestamps: true, } );