From 85769e9d2c31b5817593d2876d51ee0514d21f56 Mon Sep 17 00:00:00 2001 From: Razboy20 Date: Thu, 14 Mar 2024 23:09:04 -0500 Subject: [PATCH] refactor(UserSchedule): index by a unique id rather than name (#166) * refactor(UserSchedule): index by a unique id rather than name * refactor: Update parameter names in schedule function jsdocs * refactor: change more instances of .name to .id * refactor: Fix typo in variable name and update references * refactor: Remove console.log statement * fix(chromatic): Update ScheduleListItem story * refactor: remove unused eslint rule --- package.json | 1 + pnpm-lock.yaml | 9 ++ .../background/handler/userScheduleHandler.ts | 12 +- src/pages/background/lib/addCourse.ts | 6 +- src/pages/background/lib/clearCourses.ts | 8 +- src/pages/background/lib/createSchedule.ts | 8 +- src/pages/background/lib/deleteSchedule.ts | 8 +- src/pages/background/lib/removeCourse.ts | 4 +- src/pages/background/lib/renameSchedule.ts | 14 +- src/pages/background/lib/switchSchedule.ts | 8 +- src/shared/messages/UserScheduleMessages.ts | 24 ++-- src/shared/storage/UserScheduleStore.ts | 3 + src/shared/types/UserSchedule.ts | 10 +- src/shared/util/random.ts | 28 +--- src/stories/components/Dropdown.stories.tsx | 10 +- src/stories/components/List.stories.tsx | 1 + .../components/ScheduleListItem.stories.tsx | 64 +++++---- .../calendar/CalendarSchedules.stories.tsx | 131 +----------------- .../CourseCatalogInjectedPopup.stories.tsx | 28 +--- src/stories/injected/mocked.ts | 11 +- src/views/components/CourseCatalogMain.tsx | 1 - src/views/components/PopupMain.tsx | 10 +- .../components/calendar/Calendar/Calendar.tsx | 1 - .../CalendarHeader/CalenderHeader.tsx | 6 +- .../CalendarSchedules/CalendarSchedules.tsx | 10 +- src/views/components/common/List/List.tsx | 23 +-- .../ScheduleListItem/ScheduleListItem.tsx | 9 +- .../CourseCatalogInjectedPopup.tsx | 6 +- .../HeadingAndActions.tsx | 4 +- src/views/hooks/useFlattenedCourseSchedule.ts | 1 + src/views/hooks/useSchedules.ts | 27 ++-- 31 files changed, 182 insertions(+), 304 deletions(-) diff --git a/package.json b/package.json index 262de02e..e612cadc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "highcharts-react-official": "^3.2.1", "html-to-image": "^1.11.11", "husky": "^9.0.11", + "nanoid": "^5.0.6", "react": "^18.2.0", "react-devtools-core": "^5.0.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ff41d6f..f4588410 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ dependencies: husky: specifier: ^9.0.11 version: 9.0.11 + nanoid: + specifier: ^5.0.6 + version: 5.0.6 react: specifier: ^18.2.0 version: 18.2.0 @@ -10054,6 +10057,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@5.0.6: + resolution: {integrity: sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true diff --git a/src/pages/background/handler/userScheduleHandler.ts b/src/pages/background/handler/userScheduleHandler.ts index f7f19039..5c53c428 100644 --- a/src/pages/background/handler/userScheduleHandler.ts +++ b/src/pages/background/handler/userScheduleHandler.ts @@ -11,25 +11,25 @@ import type { MessageHandler } from 'chrome-extension-toolkit'; const userScheduleHandler: MessageHandler = { addCourse({ data, sendResponse }) { - addCourse(data.scheduleName, new Course(data.course)).then(sendResponse); + addCourse(data.scheduleId, new Course(data.course)).then(sendResponse); }, removeCourse({ data, sendResponse }) { - removeCourse(data.scheduleName, new Course(data.course)).then(sendResponse); + removeCourse(data.scheduleId, new Course(data.course)).then(sendResponse); }, clearCourses({ data, sendResponse }) { - clearCourses(data.scheduleName).then(sendResponse); + clearCourses(data.scheduleId).then(sendResponse); }, switchSchedule({ data, sendResponse }) { - switchSchedule(data.scheduleName).then(sendResponse); + switchSchedule(data.scheduleId).then(sendResponse); }, createSchedule({ data, sendResponse }) { createSchedule(data.scheduleName).then(sendResponse); }, deleteSchedule({ data, sendResponse }) { - deleteSchedule(data.scheduleName).then(sendResponse); + deleteSchedule(data.scheduleId).then(sendResponse); }, renameSchedule({ data, sendResponse }) { - renameSchedule(data.scheduleName, data.newName).then(sendResponse); + renameSchedule(data.scheduleId, data.newName).then(sendResponse); }, }; diff --git a/src/pages/background/lib/addCourse.ts b/src/pages/background/lib/addCourse.ts index 511720b5..c003b277 100644 --- a/src/pages/background/lib/addCourse.ts +++ b/src/pages/background/lib/addCourse.ts @@ -3,14 +3,14 @@ import type { Course } from '@shared/types/Course'; /** * Adds a course to a user's schedule. - * @param scheduleName - The name of the schedule to add the course to. + * @param scheduleId - The id of the schedule to add the course to. * @param course - The course to add. * @returns A promise that resolves to void. * @throws An error if the schedule is not found. */ -export default async function addCourse(scheduleName: string, course: Course): Promise { +export default async function addCourse(scheduleId: string, course: Course): Promise { const schedules = await UserScheduleStore.get('schedules'); - const activeSchedule = schedules.find(s => s.name === scheduleName); + const activeSchedule = schedules.find(s => s.id === scheduleId); if (!activeSchedule) { throw new Error('Schedule not found'); } diff --git a/src/pages/background/lib/clearCourses.ts b/src/pages/background/lib/clearCourses.ts index e226b038..08c9b12f 100644 --- a/src/pages/background/lib/clearCourses.ts +++ b/src/pages/background/lib/clearCourses.ts @@ -2,14 +2,14 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; /** * Clears the courses for a given schedule. - * @param scheduleName - The name of the schedule. + * @param scheduleId - The id of the schedule. * @throws Error if the schedule does not exist. */ -export default async function clearCourses(scheduleName: string): Promise { +export default async function clearCourses(scheduleId: string): Promise { const schedules = await UserScheduleStore.get('schedules'); - const schedule = schedules.find(schedule => schedule.name === scheduleName); + const schedule = schedules.find(schedule => schedule.id === scheduleId); if (!schedule) { - throw new Error(`Schedule ${scheduleName} does not exist`); + throw new Error(`Schedule ${scheduleId} does not exist`); } schedule.courses = []; schedule.updatedAt = Date.now(); diff --git a/src/pages/background/lib/createSchedule.ts b/src/pages/background/lib/createSchedule.ts index a602d081..52ceff1b 100644 --- a/src/pages/background/lib/createSchedule.ts +++ b/src/pages/background/lib/createSchedule.ts @@ -1,4 +1,5 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; +import { generateRandomId } from '@shared/util/random'; /** * Creates a new schedule with the given name @@ -7,11 +8,12 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; */ export default async function createSchedule(scheduleName: string): Promise { const schedules = await UserScheduleStore.get('schedules'); - if (schedules.find(schedule => schedule.name === scheduleName)) { - return `Schedule ${scheduleName} already exists`; - } + // if (schedules.find(schedule => schedule.name === scheduleName)) { + // return `Schedule ${scheduleName} already exists`; + // } schedules.push({ + id: generateRandomId(), name: scheduleName, courses: [], hours: 0, diff --git a/src/pages/background/lib/deleteSchedule.ts b/src/pages/background/lib/deleteSchedule.ts index 485e26b3..c7f2eaf0 100644 --- a/src/pages/background/lib/deleteSchedule.ts +++ b/src/pages/background/lib/deleteSchedule.ts @@ -3,18 +3,18 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; /** * Deletes a schedule with the specified name. * - * @param scheduleName - The name of the schedule to delete. + * @param scheduleId - The id of the schedule to delete. * @returns A promise that resolves to a string if there is an error, or undefined if the schedule is deleted successfully. */ -export default async function deleteSchedule(scheduleName: string): Promise { +export default async function deleteSchedule(scheduleId: string): Promise { const [schedules, activeIndex] = await Promise.all([ UserScheduleStore.get('schedules'), UserScheduleStore.get('activeIndex'), ]); - const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName); + const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId); if (scheduleIndex === -1) { - return `Schedule ${scheduleName} does not exist`; + return `Schedule ${scheduleId} does not exist`; } if (scheduleIndex === activeIndex) { return 'Cannot delete active schedule'; diff --git a/src/pages/background/lib/removeCourse.ts b/src/pages/background/lib/removeCourse.ts index dd010a31..67b020c2 100644 --- a/src/pages/background/lib/removeCourse.ts +++ b/src/pages/background/lib/removeCourse.ts @@ -4,9 +4,9 @@ import type { Course } from '@shared/types/Course'; /** * */ -export default async function removeCourse(scheduleName: string, course: Course): Promise { +export default async function removeCourse(scheduleId: string, course: Course): Promise { const schedules = await UserScheduleStore.get('schedules'); - const activeSchedule = schedules.find(s => s.name === scheduleName); + const activeSchedule = schedules.find(s => s.id === scheduleId); if (!activeSchedule) { throw new Error('Schedule not found'); } diff --git a/src/pages/background/lib/renameSchedule.ts b/src/pages/background/lib/renameSchedule.ts index b13a0817..cfd38db5 100644 --- a/src/pages/background/lib/renameSchedule.ts +++ b/src/pages/background/lib/renameSchedule.ts @@ -2,19 +2,19 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; /** * Renames a schedule with the specified name to a new name. - * @param scheduleName - The name of the schedule to be renamed. + * @param scheduleId - The id of the schedule to be renamed. * @param newName - The new name for the schedule. * @returns A promise that resolves to a string if there is an error, or undefined if the schedule is renamed successfully. */ -export default async function renameSchedule(scheduleName: string, newName: string): Promise { +export default async function renameSchedule(scheduleId: string, newName: string): Promise { const schedules = await UserScheduleStore.get('schedules'); - const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName); + const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId); if (scheduleIndex === -1) { - return `Schedule ${scheduleName} does not exist`; - } - if (schedules.find(schedule => schedule.name === newName)) { - return `Schedule ${newName} already exists`; + return `Schedule ${scheduleId} does not exist`; } + // if (schedules.find(schedule => schedule.name === newName)) { + // return `Schedule ${newName} already exists`; + // } schedules[scheduleIndex].name = newName; schedules[scheduleIndex].updatedAt = Date.now(); diff --git a/src/pages/background/lib/switchSchedule.ts b/src/pages/background/lib/switchSchedule.ts index a72301b4..1f461db5 100644 --- a/src/pages/background/lib/switchSchedule.ts +++ b/src/pages/background/lib/switchSchedule.ts @@ -3,15 +3,15 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; /** * Switches the active schedule to the specified schedule name. * Throws an error if the schedule does not exist. - * @param scheduleName - The name of the schedule to switch to. + * @param scheduleId - The id of the schedule to switch to. * @returns A Promise that resolves when the active schedule is successfully switched. */ -export default async function switchSchedule(scheduleName: string): Promise { +export default async function switchSchedule(scheduleId: string): Promise { const schedules = await UserScheduleStore.get('schedules'); - const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName); + const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId); if (scheduleIndex === -1) { - throw new Error(`Schedule ${scheduleName} does not exist`); + throw new Error(`Schedule ${scheduleId} does not exist`); } schedules[scheduleIndex].updatedAt = Date.now(); diff --git a/src/shared/messages/UserScheduleMessages.ts b/src/shared/messages/UserScheduleMessages.ts index 1cec4782..f6b9ab4f 100644 --- a/src/shared/messages/UserScheduleMessages.ts +++ b/src/shared/messages/UserScheduleMessages.ts @@ -6,25 +6,25 @@ import type { Course } from '@shared/types/Course'; export interface UserScheduleMessages { /** * Add a course to a schedule - * @param data the schedule name and course to add + * @param data the schedule id and course to add */ - addCourse: (data: { scheduleName: string; course: Course }) => void; + addCourse: (data: { scheduleId: string; course: Course }) => void; /** * Remove a course from a schedule - * @param data the schedule name and course to remove + * @param data the schedule id and course to remove */ - removeCourse: (data: { scheduleName: string; course: Course }) => void; + removeCourse: (data: { scheduleId: string; course: Course }) => void; /** * Clears all courses from a schedule - * @param data the name of the schedule to clear + * @param data the id of the schedule to clear */ - clearCourses: (data: { scheduleName: string }) => void; + clearCourses: (data: { scheduleId: string }) => void; /** * Switches the active schedule to the one specified - * @param data the name of the schedule to switch to + * @param data the id of the schedule to switch to */ - switchSchedule: (data: { scheduleName: string }) => void; + switchSchedule: (data: { scheduleId: string }) => void; /** * Creates a new schedule with the specified name @@ -34,14 +34,14 @@ export interface UserScheduleMessages { createSchedule: (data: { scheduleName: string }) => string | undefined; /** * Deletes a schedule with the specified name - * @param data the name of the schedule to delete + * @param data the id of the schedule to delete * @returns undefined if successful, otherwise an error message */ - deleteSchedule: (data: { scheduleName: string }) => string | undefined; + deleteSchedule: (data: { scheduleId: string }) => string | undefined; /** * Renames a schedule with the specified name - * @param data the name of the schedule to rename and the new name + * @param data the id of the schedule to rename and the new name * @returns undefined if successful, otherwise an error message */ - renameSchedule: (data: { scheduleName: string; newName: string }) => string | undefined; + renameSchedule: (data: { scheduleId: string; newName: string }) => string | undefined; } diff --git a/src/shared/storage/UserScheduleStore.ts b/src/shared/storage/UserScheduleStore.ts index f0a87ea0..4197f602 100644 --- a/src/shared/storage/UserScheduleStore.ts +++ b/src/shared/storage/UserScheduleStore.ts @@ -1,6 +1,8 @@ import { UserSchedule } from '@shared/types/UserSchedule'; import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; +import { generateRandomId } from '../util/random'; + interface IUserScheduleStore { schedules: UserSchedule[]; activeIndex: number; @@ -13,6 +15,7 @@ export const UserScheduleStore = createLocalStore({ schedules: [ new UserSchedule({ courses: [], + id: generateRandomId(), name: 'Schedule 1', hours: 0, updatedAt: Date.now(), diff --git a/src/shared/types/UserSchedule.ts b/src/shared/types/UserSchedule.ts index e6f20483..3c84dcfc 100644 --- a/src/shared/types/UserSchedule.ts +++ b/src/shared/types/UserSchedule.ts @@ -1,5 +1,6 @@ import type { Serialized } from 'chrome-extension-toolkit'; +import { generateRandomId } from '../util/random'; import { Course } from './Course'; /** @@ -7,6 +8,7 @@ import { Course } from './Course'; */ export class UserSchedule { courses: Course[]; + id: string; name: string; hours: number; /** Unix timestamp of when the schedule was last updated */ @@ -14,12 +16,10 @@ export class UserSchedule { constructor(schedule: Serialized) { this.courses = schedule.courses.map(c => new Course(c)); + this.id = schedule.id ?? generateRandomId(); this.name = schedule.name; - this.hours = 0; - for (const course of this.courses) { - this.hours += course.creditHours; - } - this.updatedAt = schedule.updatedAt; + this.hours = this.courses.reduce((acc, c) => acc + c.creditHours, 0); + this.updatedAt = schedule.updatedAt ?? 0; } containsCourse(course: Course): boolean { diff --git a/src/shared/util/random.ts b/src/shared/util/random.ts index 7046d43e..c1457f06 100644 --- a/src/shared/util/random.ts +++ b/src/shared/util/random.ts @@ -1,25 +1,9 @@ -/** - * Generate a random ID - * - * @returns string of size 10 made up of random numbers and letters - * @param length the length of the ID to generate - * @example "cdtl9l88pj" - */ -export function generateRandomId(length: number = 10): string { - const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let result = ''; - for (let i = 0; i < length; i += 1) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; -} +import { customAlphabet } from 'nanoid'; /** - * Generate a random number between min and max - * @param min the minimum number - * @param max the maximum number - * @returns a random number between min and max + * Generate secure URL-friendly unique ID. + * + * @param size Size of the ID. The default size is 12. + * @returns A random string. */ -export function rangeRandom(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min; -} +export const generateRandomId = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12); diff --git a/src/stories/components/Dropdown.stories.tsx b/src/stories/components/Dropdown.stories.tsx index 129f0829..e5bf09d7 100644 --- a/src/stories/components/Dropdown.stories.tsx +++ b/src/stories/components/Dropdown.stories.tsx @@ -1,5 +1,6 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserSchedule } from '@shared/types/UserSchedule'; +import { generateRandomId } from '@shared/util/random'; import type { Meta, StoryObj } from '@storybook/react'; import List from '@views/components/common/List/List'; import ScheduleDropdown from '@views/components/common/ScheduleDropdown/ScheduleDropdown'; @@ -14,6 +15,7 @@ const schedules: UserSchedule[] = new Array(10).fill(exampleSchedule).map( (schedule: UserSchedule, index) => new UserSchedule({ ...schedule, + id: generateRandomId(), name: `Schedule ${index + 1}`, }) ); @@ -61,10 +63,10 @@ const meta: Meta = { a.name === b.name} + itemKey={s => s.id} onReordered={reordered => { const activeSchedule = getActiveSchedule(); - const activeIndex = reordered.findIndex(s => s.name === activeSchedule.name); + const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id); // don't care about the promise UserScheduleStore.set('schedules', reordered); @@ -74,9 +76,9 @@ const meta: Meta = { > {(schedule, handleProps) => ( { - switchSchedule(schedule.name); + switchSchedule(schedule.id); }} dragHandleProps={handleProps} /> diff --git a/src/stories/components/List.stories.tsx b/src/stories/components/List.stories.tsx index 1e4593f7..f92c341b 100644 --- a/src/stories/components/List.stories.tsx +++ b/src/stories/components/List.stories.tsx @@ -96,6 +96,7 @@ export const Default: Story = { args: { draggables: exampleCourses.map((course, i) => ({ course, colors: tailwindColorways[i] })), children: generateCourseBlocks, + itemKey: (item: { course: Course }) => item.course.uniqueId, gap: 12, }, render: args => ( diff --git a/src/stories/components/ScheduleListItem.stories.tsx b/src/stories/components/ScheduleListItem.stories.tsx index d5926edc..e6f5a707 100644 --- a/src/stories/components/ScheduleListItem.stories.tsx +++ b/src/stories/components/ScheduleListItem.stories.tsx @@ -1,37 +1,49 @@ -/* eslint-disable jsdoc/require-jsdoc */ +import { UserSchedule } from '@shared/types/UserSchedule'; +import type { Meta, StoryObj } from '@storybook/react'; import ScheduleListItem from '@views/components/common/ScheduleListItem/ScheduleListItem'; +import useSchedules from '@views/hooks/useSchedules'; import React from 'react'; -export default { +import { exampleSchedule } from '../injected/mocked'; + +const meta = { title: 'Components/Common/ScheduleListItem', component: ScheduleListItem, parameters: { layout: 'centered', - tags: ['autodocs'], }, argTypes: { - active: { control: 'boolean' }, - name: { control: 'text' }, + schedule: { + control: { + type: 'UserSchedule', + }, + }, + }, + args: { + schedule: exampleSchedule, + }, + tags: ['autodocs'], +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Active: Story = { + render(args) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [activeSchedule] = useSchedules(); + + return ( + + ); }, }; - -export const Default = args => ; - -Default.args = { - name: 'My Schedule', - active: true, -}; - -export const Active = args => ; - -Active.args = { - name: 'My Schedule', - active: true, -}; - -export const Inactive = args => ; - -Inactive.args = { - name: 'My Schedule', - active: false, -}; +export const Inactive: Story = {}; diff --git a/src/stories/components/calendar/CalendarSchedules.stories.tsx b/src/stories/components/calendar/CalendarSchedules.stories.tsx index ba0a8962..d9668bc0 100644 --- a/src/stories/components/calendar/CalendarSchedules.stories.tsx +++ b/src/stories/components/calendar/CalendarSchedules.stories.tsx @@ -1,8 +1,3 @@ -import { Course, Status } from '@shared/types/Course'; -import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting'; -import { CourseSchedule } from '@shared/types/CourseSchedule'; -import Instructor from '@shared/types/Instructor'; -import { UserSchedule } from '@shared/types/UserSchedule'; import type { Meta, StoryObj } from '@storybook/react'; import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules'; import React from 'react'; @@ -14,11 +9,7 @@ const meta = { layout: 'centered', tags: ['autodocs'], }, - argTypes: { - // dummySchedules: { control: 'object' }, - // dummyActiveIndex: { control: 'number' }, - }, - render: (args: any) => ( + render: args => (
@@ -28,122 +19,4 @@ const meta = { export default meta; type Story = StoryObj; -const schedules = [ - new UserSchedule({ - courses: [ - new Course({ - uniqueId: 123, - number: '311C', - fullName: "311C - Bevo's Default Course", - courseName: "Bevo's Default Course", - department: 'BVO', - creditHours: 3, - status: Status.WAITLISTED, - instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })], - isReserved: false, - url: '', - flags: [], - schedule: new CourseSchedule({ - meetings: [ - new CourseMeeting({ - days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F], - startTime: 480, - endTime: 570, - location: { - building: 'UTC', - room: '123', - }, - }), - ], - }), - instructionMode: 'In Person', - semester: { - year: 2024, - season: 'Fall', - }, - scrapedAt: Date.now(), - }), - ], - name: 'Main Schedule', - hours: 0, - updatedAt: Date.now(), - }), - new UserSchedule({ - courses: [ - new Course({ - uniqueId: 123, - number: '311C', - fullName: "311C - Bevo's Default Course", - courseName: "Bevo's Default Course", - department: 'BVO', - creditHours: 3, - status: Status.WAITLISTED, - instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })], - isReserved: false, - url: '', - flags: [], - schedule: new CourseSchedule({ - meetings: [ - new CourseMeeting({ - days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F], - startTime: 480, - endTime: 570, - location: { - building: 'UTC', - room: '123', - }, - }), - ], - }), - instructionMode: 'In Person', - semester: { - year: 2024, - season: 'Spring', - }, - scrapedAt: Date.now(), - }), - new Course({ - uniqueId: 123, - number: '311C', - fullName: "311C - Bevo's Default Course", - courseName: "Bevo's Default Course", - department: 'BVO', - creditHours: 3, - status: Status.WAITLISTED, - instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })], - isReserved: false, - url: '', - flags: [], - schedule: new CourseSchedule({ - meetings: [ - new CourseMeeting({ - days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F], - startTime: 480, - endTime: 570, - location: { - building: 'UTC', - room: '123', - }, - }), - ], - }), - instructionMode: 'In Person', - semester: { - year: 2024, - season: 'Fall', - }, - scrapedAt: Date.now(), - }), - ], - name: 'Backup #3', - hours: 0, - updatedAt: Date.now(), - }), -]; - -export const Default: Story = { - args: { - // dummySchedules: schedules, - // dummyActiveIndex: 0, - }, -}; +export const Default: Story = {}; diff --git a/src/stories/injected/CourseCatalogInjectedPopup.stories.tsx b/src/stories/injected/CourseCatalogInjectedPopup.stories.tsx index 73c8f1fa..5ae1c25b 100644 --- a/src/stories/injected/CourseCatalogInjectedPopup.stories.tsx +++ b/src/stories/injected/CourseCatalogInjectedPopup.stories.tsx @@ -1,11 +1,10 @@ import type { Course } from '@shared/types/Course'; import { Status } from '@shared/types/Course'; -import { UserSchedule } from '@shared/types/UserSchedule'; import type { Meta, StoryObj } from '@storybook/react'; import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup'; import React, { useState } from 'react'; -import { bevoCourse, bevoScheule, MikeScottCS314Course, MikeScottCS314Schedule } from './mocked'; +import { bevoCourse, mikeScottCS314Course } from './mocked'; const meta = { title: 'Components/Injected/CourseCatalogInjectedPopup', @@ -14,23 +13,7 @@ const meta = { open: true, onClose: () => {}, }, - argTypes: { - course: { - control: { - type: 'object', - }, - }, - activeSchedule: { - control: { - type: 'object', - }, - }, - onClose: { - control: { - type: 'function', - }, - }, - }, + tags: ['autodocs'], render(args) { // eslint-disable-next-line react-hooks/rules-of-hooks const [isOpen, setIsOpen] = useState(args.open); @@ -44,24 +27,21 @@ type Story = StoryObj; export const OpenCourse: Story = { args: { - course: MikeScottCS314Course, - activeSchedule: MikeScottCS314Schedule, + course: mikeScottCS314Course, }, }; export const ClosedCourse: Story = { args: { course: { - ...MikeScottCS314Course, + ...mikeScottCS314Course, status: Status.CLOSED, } as Course, - activeSchedule: new UserSchedule({ courses: [], name: '', hours: 0, updatedAt: Date.now() }), }, }; export const CourseWithNoData: Story = { args: { course: bevoCourse, - activeSchedule: bevoScheule, }, }; diff --git a/src/stories/injected/mocked.ts b/src/stories/injected/mocked.ts index 0be0195d..4b0641c4 100644 --- a/src/stories/injected/mocked.ts +++ b/src/stories/injected/mocked.ts @@ -55,6 +55,7 @@ export const exampleCourse: Course = new Course({ export const exampleSchedule: UserSchedule = new UserSchedule({ courses: [exampleCourse], + id: 'az372389blep', name: 'Example Schedule', hours: 3, updatedAt: Date.now(), @@ -105,14 +106,15 @@ export const bevoCourse: Course = new Course({ scrapedAt: Date.now(), }); -export const bevoScheule: UserSchedule = new UserSchedule({ +export const bevoSchedule: UserSchedule = new UserSchedule({ courses: [bevoCourse], + id: 'bevoshenanigans52', name: 'Bevo Schedule', hours: 3, updatedAt: Date.now(), }); -export const MikeScottCS314Course: Course = new Course({ +export const mikeScottCS314Course: Course = new Course({ uniqueId: 50805, number: '314', fullName: 'C S 314 DATA STRUCTURES', @@ -158,8 +160,9 @@ export const MikeScottCS314Course: Course = new Course({ scrapedAt: Date.now(), }); -export const MikeScottCS314Schedule: UserSchedule = new UserSchedule({ - courses: [MikeScottCS314Course], +export const mikeScottCS314Schedule: UserSchedule = new UserSchedule({ + courses: [mikeScottCS314Course], + id: 'omgitsmikescott314', name: 'Mike Scott CS314 Schedule', hours: 3, updatedAt: Date.now(), diff --git a/src/views/components/CourseCatalogMain.tsx b/src/views/components/CourseCatalogMain.tsx index 0de205d4..160bbf41 100644 --- a/src/views/components/CourseCatalogMain.tsx +++ b/src/views/components/CourseCatalogMain.tsx @@ -79,7 +79,6 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element { )} setShowPopup(false)} afterLeave={() => setSelectedCourse(null)} diff --git a/src/views/components/PopupMain.tsx b/src/views/components/PopupMain.tsx index 3d45fa89..e3d5bd16 100644 --- a/src/views/components/PopupMain.tsx +++ b/src/views/components/PopupMain.tsx @@ -64,10 +64,10 @@ export default function PopupMain(): JSX.Element { a.name === b.name} + itemKey={schedule => schedule.id} onReordered={reordered => { const activeSchedule = getActiveSchedule(); - const activeIndex = reordered.findIndex(s => s.name === activeSchedule.name); + const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id); // don't care about the promise UserScheduleStore.set('schedules', reordered); @@ -77,9 +77,9 @@ export default function PopupMain(): JSX.Element { > {(schedule, handleProps) => ( { - switchSchedule(schedule.name); + switchSchedule(schedule.id); }} dragHandleProps={handleProps} /> @@ -98,7 +98,7 @@ export default function PopupMain(): JSX.Element { activeSchedule.courses = reordered.map(c => c.course); replaceSchedule(getActiveSchedule(), activeSchedule); }} - equalityCheck={(a, b) => a.course.uniqueId === b.course.uniqueId} + itemKey={e => e.course.uniqueId} gap={10} > {({ course, colors }, handleProps) => ( diff --git a/src/views/components/calendar/Calendar/Calendar.tsx b/src/views/components/calendar/Calendar/Calendar.tsx index bac0b924..acce65d3 100644 --- a/src/views/components/calendar/Calendar/Calendar.tsx +++ b/src/views/components/calendar/Calendar/Calendar.tsx @@ -83,7 +83,6 @@ export default function Calendar(): JSX.Element { setShowPopup(false)} open={showPopup} afterLeave={() => setCourse(null)} diff --git a/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx b/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx index 3bfbe2da..93a84b5d 100644 --- a/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx +++ b/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx @@ -40,8 +40,8 @@ export default function CalendarHeader(): JSX.Element {
- UT Registration - Plus + UT Registration + Plus
@@ -52,7 +52,7 @@ export default function CalendarHeader(): JSX.Element { totalHours={activeSchedule.hours} totalCourses={activeSchedule.courses.length} /> - + LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} diff --git a/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx b/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx index 236aa049..da930b6b 100644 --- a/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx +++ b/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx @@ -30,7 +30,7 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P const [activeSchedule, schedules] = useSchedules(); useEffect(() => { - const index = schedules.findIndex(schedule => schedule.name === activeSchedule.name); + const index = schedules.findIndex(schedule => schedule.id === activeSchedule.id); if (index !== -1) { setActiveScheduleIndex(index); } @@ -68,10 +68,10 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P a.name === b.name} + itemKey={s => s.id} onReordered={reordered => { const activeSchedule = getActiveSchedule(); - const activeIndex = reordered.findIndex(s => s.name === activeSchedule.name); + const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id); // don't care about the promise UserScheduleStore.set('schedules', reordered); @@ -80,9 +80,9 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P > {(schedule, handleProps) => ( { - switchSchedule(schedule.name); + switchSchedule(schedule.id); }} dragHandleProps={handleProps} /> diff --git a/src/views/components/common/List/List.tsx b/src/views/components/common/List/List.tsx index 7f5bbb8b..e8c7b410 100644 --- a/src/views/components/common/List/List.tsx +++ b/src/views/components/common/List/List.tsx @@ -3,6 +3,8 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import type { ReactElement } from 'react'; import React, { useCallback, useEffect, useState } from 'react'; +import ExtensionRoot from '../ExtensionRoot/ExtensionRoot'; + /* * Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code) */ @@ -14,13 +16,13 @@ export interface ListProps { draggables: T[]; children: (draggable: T, handleProps: DraggableProvidedDragHandleProps) => ReactElement; onReordered: (elements: T[]) => void; - equalityCheck?: (a: T, b: T) => boolean; + itemKey: (item: T) => React.Key; gap?: number; // Impacts the spacing between items in the list } -function wrap(draggableElements: T[]) { - return draggableElements.map((element, index) => ({ - id: `id:${index}`, +function wrap(draggableElements: T[], keyTransform: ListProps['itemKey']) { + return draggableElements.map(element => ({ + id: keyTransform(element), content: element, })); } @@ -68,20 +70,19 @@ function Item(props: { * */ function List(props: ListProps): JSX.Element { - const [items, setItems] = useState(wrap(props.draggables)); + const [items, setItems] = useState(wrap(props.draggables, props.itemKey)); - const equalityCheck = props.equalityCheck || ((a, b) => a === b); const transformFunction = props.children; useEffect(() => { // check if the draggables content has *actually* changed if ( props.draggables.length === items.length && - props.draggables.every((element, index) => equalityCheck(element, items[index].content)) + props.draggables.every((element, index) => props.itemKey(element) === items[index].id) ) { return; } - setItems(wrap(props.draggables)); + setItems(wrap(props.draggables, props.itemKey)); }, [props.draggables]); const onDragEnd: OnDragEndResponder = useCallback( @@ -123,7 +124,9 @@ function List(props: ListProps): JSX.Element { ...style, }} > - {transformFunction(items[rubric.source.index].content, provided.dragHandleProps)} + + {transformFunction(items[rubric.source.index].content, provided.dragHandleProps)} + ); }} @@ -135,7 +138,7 @@ function List(props: ListProps): JSX.Element { style={{ marginBottom: `-${props.gap}px` }} > {items.map((item, index) => ( - + {draggableProvided => (
, 'className'>; onClick?: React.DOMAttributes['onClick']; }; @@ -18,10 +19,10 @@ export type Props = { /** * This is a reusable dropdown component that can be used to toggle the visiblity of information */ -export default function ScheduleListItem({ style, name, dragHandleProps, onClick }: Props): JSX.Element { +export default function ScheduleListItem({ style, schedule, dragHandleProps, onClick }: Props): JSX.Element { const [activeSchedule] = useSchedules(); - const isActive = useMemo(() => activeSchedule.name === name, [activeSchedule, name]); + const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]); return (
@@ -42,7 +43,7 @@ export default function ScheduleListItem({ style, name, dragHandleProps, onClick } )} /> - {name} + {schedule.name}
diff --git a/src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup.tsx b/src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup.tsx index 7c606015..ed7416af 100644 --- a/src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup.tsx +++ b/src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup.tsx @@ -1,7 +1,7 @@ import type { Course } from '@shared/types/Course'; -import type { UserSchedule } from '@shared/types/UserSchedule'; import type { DialogProps } from '@views/components/common/Dialog/Dialog'; import Dialog from '@views/components/common/Dialog/Dialog'; +import useSchedules from '@views/hooks/useSchedules'; import React from 'react'; import Description from './Description'; @@ -10,7 +10,6 @@ import HeadingAndActions from './HeadingAndActions'; export type CourseCatalogInjectedPopupProps = DialogProps & { course: Course; - activeSchedule: UserSchedule; }; /** @@ -23,8 +22,9 @@ export type CourseCatalogInjectedPopupProps = DialogProps & { * @param {Function} props.onClose - The function to close the popup. * @returns {JSX.Element} The CourseCatalogInjectedPopup component. */ -function CourseCatalogInjectedPopup({ course, activeSchedule, ...rest }: CourseCatalogInjectedPopupProps): JSX.Element { +function CourseCatalogInjectedPopup({ course, ...rest }: CourseCatalogInjectedPopupProps): JSX.Element { const emptyRef = React.useRef(null); + const [activeSchedule] = useSchedules(); return ( diff --git a/src/views/components/injected/CourseCatalogInjectedPopup/HeadingAndActions.tsx b/src/views/components/injected/CourseCatalogInjectedPopup/HeadingAndActions.tsx index bef737c0..3998ca69 100644 --- a/src/views/components/injected/CourseCatalogInjectedPopup/HeadingAndActions.tsx +++ b/src/views/components/injected/CourseCatalogInjectedPopup/HeadingAndActions.tsx @@ -100,9 +100,9 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H const handleAddOrRemoveCourse = async () => { if (!activeSchedule) return; if (!courseAdded) { - addCourse({ course, scheduleName: activeSchedule.name }); + addCourse({ course, scheduleId: activeSchedule.id }); } else { - removeCourse({ course, scheduleName: activeSchedule.name }); + removeCourse({ course, scheduleId: activeSchedule.id }); } }; diff --git a/src/views/hooks/useFlattenedCourseSchedule.ts b/src/views/hooks/useFlattenedCourseSchedule.ts index 9e195e28..66e5bb1e 100644 --- a/src/views/hooks/useFlattenedCourseSchedule.ts +++ b/src/views/hooks/useFlattenedCourseSchedule.ts @@ -59,6 +59,7 @@ export function useFlattenedCourseSchedule(): FlattenedCourseSchedule { courseCells: [] as CalendarGridCourse[], activeSchedule: new UserSchedule({ courses: [], + id: 'error', name: 'Something may have went wrong', hours: 0, updatedAt: Date.now(), diff --git a/src/views/hooks/useSchedules.ts b/src/views/hooks/useSchedules.ts index 4a27516a..0b64f443 100644 --- a/src/views/hooks/useSchedules.ts +++ b/src/views/hooks/useSchedules.ts @@ -3,9 +3,17 @@ import { UserSchedule } from '@shared/types/UserSchedule'; import { useEffect, useState } from 'react'; let schedulesCache = []; -let activeIndexCache = 0; +let activeIndexCache = -1; let initialLoad = true; +const errorSchedule = new UserSchedule({ + courses: [], + id: 'error', + name: 'An error has occurred', + hours: 0, + updatedAt: Date.now(), +}); + /** * Fetches the user schedules from storage and sets the cached state. */ @@ -25,7 +33,7 @@ async function fetchData() { export default function useSchedules(): [active: UserSchedule, schedules: UserSchedule[]] { const [schedules, setSchedules] = useState(schedulesCache); const [activeIndex, setActiveIndex] = useState(activeIndexCache); - const [activeSchedule, setActiveSchedule] = useState(schedules[activeIndex]); + const [activeSchedule, setActiveSchedule] = useState(schedules[activeIndex] ?? errorSchedule); if (initialLoad) { initialLoad = false; @@ -56,22 +64,19 @@ export default function useSchedules(): [active: UserSchedule, schedules: UserSc // recompute active schedule on a schedule/index change useEffect(() => { - setActiveSchedule(schedules[activeIndex]); + setActiveSchedule(schedules[activeIndex] ?? errorSchedule); }, [activeIndex, schedules]); return [activeSchedule, schedules]; } export function getActiveSchedule(): UserSchedule { - return ( - schedulesCache[activeIndexCache] || - new UserSchedule({ courses: [], name: 'An error has occurred', hours: 0, updatedAt: Date.now() }) - ); + return schedulesCache[activeIndexCache] ?? errorSchedule; } export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: UserSchedule) { const schedules = await UserScheduleStore.get('schedules'); - let oldIndex = schedules.findIndex(s => s.name === oldSchedule.name); + let oldIndex = schedules.findIndex(s => s.id === oldSchedule.id); oldIndex = oldIndex !== -1 ? oldIndex : 0; schedules[oldIndex] = newSchedule; await UserScheduleStore.set('schedules', schedules); @@ -80,12 +85,12 @@ export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: Us /** * Switches the active schedule to the one with the specified name. - * @param name - The name of the schedule to switch to. + * @param id - The id of the schedule to switch to. * @returns A promise that resolves when the active schedule has been switched. */ -export async function switchSchedule(name: string): Promise { +export async function switchSchedule(id: string): Promise { console.log('Switching schedule...'); const schedules = await UserScheduleStore.get('schedules'); - const activeIndex = schedules.findIndex(s => s.name === name); + const activeIndex = schedules.findIndex(s => s.id === id); await UserScheduleStore.set('activeIndex', activeIndex); }