From a1684deeaeeeaadc2e25d461181f45e09d2c196b Mon Sep 17 00:00:00 2001 From: Jay Wu Date: Tue, 26 Nov 2024 13:47:11 -0800 Subject: [PATCH] Duplicate calendar (#1048) --- .../src/actions/ActionTypesStore.ts | 4 +- .../antalmanac/src/actions/AppStoreActions.ts | 12 ++--- .../src/components/dialogs/AddSchedule.tsx | 20 +++++++-- .../src/components/dialogs/CopySchedule.tsx | 45 +++++-------------- .../src/components/dialogs/DeleteSchedule.tsx | 21 ++++++--- .../src/components/dialogs/RenameSchedule.tsx | 20 +++------ apps/antalmanac/src/stores/AppStore.ts | 13 ++++-- apps/antalmanac/src/stores/Schedules.ts | 44 ++++++++++-------- 8 files changed, 92 insertions(+), 87 deletions(-) diff --git a/apps/antalmanac/src/actions/ActionTypesStore.ts b/apps/antalmanac/src/actions/ActionTypesStore.ts index c57d018a3..0284234ef 100644 --- a/apps/antalmanac/src/actions/ActionTypesStore.ts +++ b/apps/antalmanac/src/actions/ActionTypesStore.ts @@ -59,7 +59,7 @@ export interface ClearScheduleAction { export interface CopyScheduleAction { type: 'copySchedule'; - to: number; + newScheduleName: string; } export interface ChangeCourseColorAction { @@ -159,7 +159,7 @@ class ActionTypesStore extends EventEmitter { AppStore.schedule.clearCurrentSchedule(); break; case 'copySchedule': - AppStore.schedule.copySchedule(action.to); + AppStore.schedule.copySchedule(action.newScheduleName); break; default: break; diff --git a/apps/antalmanac/src/actions/AppStoreActions.ts b/apps/antalmanac/src/actions/AppStoreActions.ts index 78bbf2c55..e54a5be49 100644 --- a/apps/antalmanac/src/actions/AppStoreActions.ts +++ b/apps/antalmanac/src/actions/AppStoreActions.ts @@ -11,8 +11,8 @@ import { removeLocalStorageUserId, setLocalStorageUserId } from '$lib/localStora import AppStore from '$stores/AppStore'; export interface CopyScheduleOptions { - onSuccess: (index: number) => unknown; - onError: (index: number) => unknown; + onSuccess: (scheduleName: string) => unknown; + onError: (scheduleName: string) => unknown; } export const addCourse = ( @@ -250,17 +250,17 @@ export const changeCourseColor = (sectionCode: string, term: string, newColor: s AppStore.changeCourseColor(sectionCode, term, newColor); }; -export const copySchedule = (to: number, options?: CopyScheduleOptions) => { +export const copySchedule = (newScheduleName: string, options?: CopyScheduleOptions) => { logAnalytics({ category: analyticsEnum.addedClasses.title, action: analyticsEnum.addedClasses.actions.COPY_SCHEDULE, }); try { - AppStore.copySchedule(to); - options?.onSuccess(to); + AppStore.copySchedule(newScheduleName); + options?.onSuccess(newScheduleName); } catch (error) { - options?.onError(to); + options?.onError(newScheduleName); } }; diff --git a/apps/antalmanac/src/components/dialogs/AddSchedule.tsx b/apps/antalmanac/src/components/dialogs/AddSchedule.tsx index 3bf91f87a..ae4bc6305 100644 --- a/apps/antalmanac/src/components/dialogs/AddSchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/AddSchedule.tsx @@ -1,6 +1,6 @@ import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material'; import type { DialogProps } from '@mui/material'; -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { addSchedule } from '$actions/AppStoreActions'; import AppStore from '$stores/AppStore'; @@ -12,7 +12,9 @@ import { useThemeStore } from '$stores/SettingsStore'; function AddScheduleDialog({ onClose, onKeyDown, ...props }: DialogProps) { const isDark = useThemeStore((store) => store.isDark); - const [name, setName] = useState(AppStore.getDefaultScheduleName()); + const [name, setName] = useState( + AppStore.getNextScheduleName(AppStore.getDefaultScheduleName(), AppStore.getScheduleNames().length) + ); const handleCancel = () => { onClose?.({}, 'escapeKeyDown'); @@ -24,7 +26,6 @@ function AddScheduleDialog({ onClose, onKeyDown, ...props }: DialogProps) { const submitName = () => { addSchedule(name); - setName(AppStore.schedule.getDefaultScheduleName()); onClose?.({}, 'escapeKeyDown'); }; @@ -46,6 +47,17 @@ function AddScheduleDialog({ onClose, onKeyDown, ...props }: DialogProps) { } }; + const handleScheduleNamesChange = useCallback(() => { + setName(AppStore.getNextScheduleName(AppStore.getDefaultScheduleName(), AppStore.getScheduleNames().length)); + }, []); + + useEffect(() => { + AppStore.on('scheduleNamesChange', handleScheduleNamesChange); + return () => { + AppStore.off('scheduleNamesChange', handleScheduleNamesChange); + }; + }, [handleScheduleNamesChange]); + return ( Add Schedule @@ -60,7 +72,7 @@ function AddScheduleDialog({ onClose, onKeyDown, ...props }: DialogProps) { - diff --git a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx index c0722d461..5a9da35d5 100644 --- a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx @@ -5,11 +5,9 @@ import { DialogActions, DialogContent, DialogTitle, - MenuItem, - Select, + TextField, type DialogProps, } from '@mui/material'; -import { SelectChangeEvent } from '@mui/material'; import { useState, useEffect, useCallback } from 'react'; import { copySchedule } from '$actions/AppStoreActions'; @@ -22,11 +20,10 @@ interface CopyScheduleDialogProps extends DialogProps { function CopyScheduleDialog(props: CopyScheduleDialogProps) { const { index } = props; const { onClose } = props; // destructured separately for memoization. - const [scheduleNames, setScheduleNames] = useState(AppStore.getScheduleNames()); - const [selectedSchedule, setSelectedSchedule] = useState(0); + const [name, setName] = useState(`Copy of ${AppStore.getScheduleNames()[index]}`); - const handleScheduleChange = useCallback((event: SelectChangeEvent) => { - setSelectedSchedule(event.target.value as number); + const handleNameChange = useCallback((event: React.ChangeEvent) => { + setName(event.target.value); }, []); const handleCancel = useCallback(() => { @@ -34,25 +31,16 @@ function CopyScheduleDialog(props: CopyScheduleDialogProps) { }, [onClose]); const handleCopy = useCallback(() => { - if (selectedSchedule !== scheduleNames.length) { - copySchedule(selectedSchedule); - } else { - scheduleNames.forEach((_, scheduleIndex) => { - if (scheduleIndex !== index) { - copySchedule(scheduleIndex); - } - }); - } + copySchedule(name); onClose?.({}, 'escapeKeyDown'); - }, [index, onClose, selectedSchedule, scheduleNames]); + }, [onClose, name]); const handleScheduleNamesChange = useCallback(() => { - setScheduleNames([...AppStore.getScheduleNames()]); - }, []); + setName(`Copy of ${AppStore.getScheduleNames()[index]}`); + }, [index]); useEffect(() => { AppStore.on('scheduleNamesChange', handleScheduleNamesChange); - return () => { AppStore.off('scheduleNamesChange', handleScheduleNamesChange); }; @@ -60,27 +48,18 @@ function CopyScheduleDialog(props: CopyScheduleDialogProps) { return ( - Copy To Schedule - + Copy Schedule - + - - diff --git a/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx b/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx index 2bcf7ed96..402bf1d40 100644 --- a/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx @@ -7,7 +7,7 @@ import { DialogTitle, type DialogProps, } from '@mui/material'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { deleteSchedule } from '$actions/AppStoreActions'; import AppStore from '$stores/AppStore'; @@ -35,10 +35,7 @@ function DeleteScheduleDialog(props: ScheduleNameDialogProps) { * This is destructured separately for memoization. */ const { onClose } = props; - - const scheduleName = useMemo(() => { - return AppStore.schedule.getScheduleName(index); - }, [index]); + const [name, setName] = useState(AppStore.getScheduleNames()[index]); const handleCancel = useCallback(() => { onClose?.({}, 'escapeKeyDown'); @@ -49,12 +46,24 @@ function DeleteScheduleDialog(props: ScheduleNameDialogProps) { onClose?.({}, 'escapeKeyDown'); }, [index, onClose]); + const handleScheduleNamesChange = useCallback(() => { + setName(AppStore.getScheduleNames()[index]); + }, [index]); + + useEffect(() => { + AppStore.on('scheduleNamesChange', handleScheduleNamesChange); + + return () => { + AppStore.off('scheduleNamesChange', handleScheduleNamesChange); + }; + }, [handleScheduleNamesChange]); + return ( Delete Schedule - Are you sure you want to delete "{scheduleName}"? + Are you sure you want to delete "{name}"? diff --git a/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx b/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx index 0d3611a2a..a365413d3 100644 --- a/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx @@ -8,7 +8,7 @@ import { TextField, type DialogProps, } from '@mui/material'; -import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import { renameSchedule } from '$actions/AppStoreActions'; import AppStore from '$stores/AppStore'; @@ -34,19 +34,11 @@ function RenameScheduleDialog(props: ScheduleNameDialogProps) { * This is destructured separately for memoization. */ const { onClose } = props; - - const [scheduleNames, setScheduleNames] = useState(AppStore.getScheduleNames()); - - const [name, setName] = useState(scheduleNames[index]); - - const disabled = useMemo(() => { - return name?.trim() === ''; - }, [name]); + const [name, setName] = useState(AppStore.getScheduleNames()[index]); const handleCancel = useCallback(() => { onClose?.({}, 'escapeKeyDown'); - setName(scheduleNames[index]); - }, [onClose, scheduleNames, index]); + }, [onClose, index]); const handleNameChange = useCallback((event: React.ChangeEvent) => { setName(event.target.value); @@ -75,8 +67,8 @@ function RenameScheduleDialog(props: ScheduleNameDialogProps) { ); const handleScheduleNamesChange = useCallback(() => { - setScheduleNames(AppStore.getScheduleNames()); - }, []); + setName(AppStore.getScheduleNames()[index]); + }, [index]); useEffect(() => { AppStore.on('scheduleNamesChange', handleScheduleNamesChange); @@ -100,7 +92,7 @@ function RenameScheduleDialog(props: ScheduleNameDialogProps) { - diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts index bfcd43456..b9ac5c496 100644 --- a/apps/antalmanac/src/stores/AppStore.ts +++ b/apps/antalmanac/src/stores/AppStore.ts @@ -71,6 +71,10 @@ class AppStore extends EventEmitter { } } + getNextScheduleName(newScheduleName: string, scheduleIndex: number) { + return this.schedule.getNextScheduleName(newScheduleName, scheduleIndex); + } + getDefaultScheduleName() { return this.schedule.getDefaultScheduleName(); } @@ -291,14 +295,17 @@ class AppStore extends EventEmitter { window.localStorage.removeItem('unsavedActions'); } - copySchedule(to: number) { - this.schedule.copySchedule(to); + copySchedule(newScheduleName: string) { + this.schedule.copySchedule(newScheduleName); this.unsavedChanges = true; const action: CopyScheduleAction = { type: 'copySchedule', - to: to, + newScheduleName: newScheduleName, }; actionTypesStore.autoSaveSchedule(action); + this.emit('scheduleNamesChange'); + this.emit('currentScheduleIndexChange'); + this.emit('scheduleNotesChange'); this.emit('addedCoursesChange'); this.emit('customEventsChange'); } diff --git a/apps/antalmanac/src/stores/Schedules.ts b/apps/antalmanac/src/stores/Schedules.ts index 0129082ff..9d14a983e 100644 --- a/apps/antalmanac/src/stores/Schedules.ts +++ b/apps/antalmanac/src/stores/Schedules.ts @@ -50,10 +50,20 @@ export class Schedules { this.skeletonSchedules = []; } + getNextScheduleName(newScheduleName: string, scheduleIndex: number) { + const scheduleNames = this.getScheduleNames(); + scheduleNames.splice(scheduleIndex, 1); + let nextScheduleName = newScheduleName; + let counter = 1; + + while (scheduleNames.includes(nextScheduleName)) { + nextScheduleName = `${newScheduleName}(${counter++})`; + } + return nextScheduleName; + } + getDefaultScheduleName() { - const termName = termData[0].shortName.replaceAll(' ', '-'); - const countSameScheduleNames = this.getScheduleNames().filter((name) => name.includes(termName)).length; - return `${termName + (countSameScheduleNames == 0 ? '' : '(' + countSameScheduleNames + ')')}`; + return termData[0].shortName.replaceAll(' ', '-'); } getCurrentScheduleIndex() { @@ -92,12 +102,13 @@ export class Schedules { /** * Create an empty schedule. + * @param newScheduleName The name of the new schedule. If a schedule with the same name already exists, a number will be appended to the name. */ addNewSchedule(newScheduleName: string) { this.addUndoState(); const scheduleNoteId = Math.random(); this.schedules.push({ - scheduleName: newScheduleName, + scheduleName: this.getNextScheduleName(newScheduleName, this.getNumberOfSchedules()), courses: [], customEvents: [], scheduleNoteId: scheduleNoteId, @@ -109,10 +120,11 @@ export class Schedules { /** * Rename schedule with the specified index. + * @param newScheduleName The name of the new schedule. If a schedule with the same name already exists, a number will be appended to the name. */ renameSchedule(newScheduleName: string, scheduleIndex: number) { this.addUndoState(); - this.schedules[scheduleIndex].scheduleName = newScheduleName; + this.schedules[scheduleIndex].scheduleName = this.getNextScheduleName(newScheduleName, scheduleIndex); } /** @@ -134,25 +146,19 @@ export class Schedules { } /** - * Append all courses from current schedule to the schedule with the target index. - * @param to Index of the schedule to append courses to. If equal to number of schedules, will append courses to all schedules. + * Copy the current schedule to a newly created schedule with the specified name. */ - copySchedule(to: number) { - this.addUndoState(); + copySchedule(newScheduleName: string) { + this.addNewSchedule(newScheduleName); + this.currentScheduleIndex = this.previousStates[this.previousStates.length - 1].scheduleIndex; // return to previous schedule index for copying + const to = this.getNumberOfSchedules() - 1; + for (const course of this.getCurrentCourses()) { - if (to === this.getNumberOfSchedules()) { - this.addCourseToAllSchedules(course); - } else { - this.addCourse(course, to, false); - } + this.addCourse(course, to, false); } for (const customEvent of this.getCurrentCustomEvents()) { - if (to === this.getNumberOfSchedules()) { - this.addCustomEvent(customEvent, [...Array(to).keys()]); - } else { - this.addCustomEvent(customEvent, [to]); - } + this.addCustomEvent(customEvent, [to]); } }