From 89d7e4170efa2a3c190eadde49b0d3a481d0184f Mon Sep 17 00:00:00 2001 From: jotalis Date: Fri, 27 Dec 2024 23:42:01 -0800 Subject: [PATCH 01/10] fix: fetch updated schedule names for copy, delete, rename --- apps/antalmanac/src/components/dialogs/CopySchedule.tsx | 4 ++++ apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx | 4 ++++ apps/antalmanac/src/components/dialogs/RenameSchedule.tsx | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx index 5a9da35d5..ba927da85 100644 --- a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx @@ -39,6 +39,10 @@ function CopyScheduleDialog(props: CopyScheduleDialogProps) { setName(`Copy of ${AppStore.getScheduleNames()[index]}`); }, [index]); + useEffect(() => { + setName(`Copy of ${AppStore.getScheduleNames()[index]}`); + }, [index]); + useEffect(() => { AppStore.on('scheduleNamesChange', handleScheduleNamesChange); return () => { diff --git a/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx b/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx index 402bf1d40..7d27f0dd6 100644 --- a/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx @@ -50,6 +50,10 @@ function DeleteScheduleDialog(props: ScheduleNameDialogProps) { setName(AppStore.getScheduleNames()[index]); }, [index]); + useEffect(() => { + setName(AppStore.getScheduleNames()[index]); + }, [index]); + useEffect(() => { AppStore.on('scheduleNamesChange', handleScheduleNamesChange); diff --git a/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx b/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx index aca9c387a..1c2c93e38 100644 --- a/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx @@ -70,6 +70,10 @@ function RenameScheduleDialog(props: ScheduleNameDialogProps) { setName(AppStore.getScheduleNames()[index]); }, [index]); + useEffect(() => { + setName(AppStore.getScheduleNames()[index]); + }, [index]); + useEffect(() => { AppStore.on('scheduleNamesChange', handleScheduleNamesChange); From 021e4f4921dcb3de191ebeb3ae8ab33cfc5e7b68 Mon Sep 17 00:00:00 2001 From: jotalis Date: Fri, 27 Dec 2024 23:49:27 -0800 Subject: [PATCH 02/10] feat: build the sortable dnd components --- apps/antalmanac/package.json | 4 + .../drag-and-drop/SortableItem.tsx | 83 +++++++++++++++++++ .../drag-and-drop/SortableList.tsx | 67 +++++++++++++++ .../drag-and-drop/SortableOverlay.tsx | 11 +++ 4 files changed, 165 insertions(+) create mode 100644 apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableItem.tsx create mode 100644 apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx create mode 100644 apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableOverlay.tsx diff --git a/apps/antalmanac/package.json b/apps/antalmanac/package.json index 4216b6f08..5526e817a 100644 --- a/apps/antalmanac/package.json +++ b/apps/antalmanac/package.json @@ -23,6 +23,10 @@ "dependencies": { "@babel/runtime": "^7.26.0", "@date-io/date-fns": "^2.16.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@material-ui/core": "^4.12.4", diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableItem.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableItem.tsx new file mode 100644 index 000000000..719b3194e --- /dev/null +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableItem.tsx @@ -0,0 +1,83 @@ +import type { DraggableSyntheticListeners, UniqueIdentifier } from '@dnd-kit/core'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import { Box, ListItem } from '@mui/material'; +import type { CSSProperties, PropsWithChildren } from 'react'; +import { createContext, useContext, useMemo } from 'react'; + +interface Props { + id: UniqueIdentifier; +} + +interface Context { + attributes: Record; + listeners: DraggableSyntheticListeners; + ref(node: HTMLElement | null): void; +} + +const SortableItemContext = createContext({ + attributes: {}, + listeners: undefined, + ref: (_node: HTMLElement | null) => { + // Intentionally left blank + }, +}); + +export function SortableItem({ children, id }: PropsWithChildren) { + const { attributes, isDragging, listeners, setNodeRef, setActivatorNodeRef, transform } = useSortable({ + id, + }); + const context = useMemo( + () => ({ + attributes, + listeners, + ref: setActivatorNodeRef, + }), + [attributes, listeners, setActivatorNodeRef] + ); + const style: CSSProperties = { + opacity: isDragging ? 0.4 : undefined, + transform: CSS.Translate.toString(transform), + padding: 0, + }; + + return ( + + + {children} + + + ); +} + +interface DragHandleProps { + disabled?: boolean; +} + +export function DragHandle({ disabled = false }: DragHandleProps) { + const { attributes, listeners, ref } = useContext(SortableItemContext); + + return ( + + + + ); +} diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx new file mode 100644 index 000000000..494fc1e74 --- /dev/null +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx @@ -0,0 +1,67 @@ +import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import type { Active, UniqueIdentifier } from '@dnd-kit/core'; +import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; +import { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'; +import { List } from '@mui/material'; +import type { ReactNode } from 'react'; +import { Fragment, useMemo, useState } from 'react'; + +import { DragHandle, SortableItem } from './SortableItem'; +import { SortableOverlay } from './SortableOverlay'; + +import AppStore from '$stores/AppStore'; + +interface BaseItem { + id: UniqueIdentifier; +} + +interface Props { + items: T[]; + onChange(items: T[]): void; + renderItem(item: T): ReactNode; +} + +export function SortableList({ items, onChange, renderItem }: Props) { + const [active, setActive] = useState(null); + const activeItem = useMemo(() => items.find((item) => item.id === active?.id), [active, items]); + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + return ( + { + setActive(active); + }} + onDragEnd={({ active, over }) => { + if (over && active.id !== over?.id) { + const activeIndex = items.findIndex(({ id }) => id === active.id); + const overIndex = items.findIndex(({ id }) => id === over.id); + onChange(arrayMove(items, activeIndex, overIndex)); + AppStore.reorderSchedules(activeIndex, overIndex); + } + setActive(null); + }} + onDragCancel={() => { + setActive(null); + }} + > + + + {items.map((item) => ( + {renderItem(item)} + ))} + + + {activeItem ? renderItem(activeItem) : null} + + ); +} + +SortableList.Item = SortableItem; +SortableList.DragHandle = DragHandle; diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableOverlay.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableOverlay.tsx new file mode 100644 index 000000000..b21b7e4e8 --- /dev/null +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableOverlay.tsx @@ -0,0 +1,11 @@ +import { DragOverlay } from '@dnd-kit/core'; +import type { DropAnimation } from '@dnd-kit/core'; + +const dropAnimationConfig: DropAnimation = { + duration: 500, + easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)', +}; + +export function SortableOverlay({ children }: { children: React.ReactNode }) { + return {children}; +} From d26325599db11adcf492406233a331c5db62075a Mon Sep 17 00:00:00 2001 From: jotalis Date: Fri, 27 Dec 2024 23:52:38 -0800 Subject: [PATCH 03/10] feat: reordering schedule functionality --- apps/antalmanac/src/stores/AppStore.ts | 7 +++++++ apps/antalmanac/src/stores/Schedules.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts index b9ac5c496..8aa360a00 100644 --- a/apps/antalmanac/src/stores/AppStore.ts +++ b/apps/antalmanac/src/stores/AppStore.ts @@ -310,6 +310,13 @@ class AppStore extends EventEmitter { this.emit('customEventsChange'); } + reorderSchedules(from: number, to: number) { + this.schedule.reorderSchedules(from, to); + this.emit('currentScheduleIndexChange'); + this.emit('scheduleNamesChange', { triggeredBy: 'reorder' }); + this.emit('reorderSchedule'); + } + async loadSchedule(savedSchedule: ScheduleSaveState) { try { await this.schedule.fromScheduleSaveState(savedSchedule); diff --git a/apps/antalmanac/src/stores/Schedules.ts b/apps/antalmanac/src/stores/Schedules.ts index 8aedc7657..58a4c4511 100644 --- a/apps/antalmanac/src/stores/Schedules.ts +++ b/apps/antalmanac/src/stores/Schedules.ts @@ -162,6 +162,23 @@ export class Schedules { } } + /** + * Reorder schedules by moving a schedule from one index to another. + * This modifies the order of schedules and updates the current schedule index to maintain the correct reference. + */ + reorderSchedules(from: number, to: number) { + this.addUndoState(); + const [removed] = this.schedules.splice(from, 1); + this.schedules.splice(to, 0, removed); + if (this.currentScheduleIndex === from) { + this.currentScheduleIndex = to; + } else if (this.currentScheduleIndex > from && this.currentScheduleIndex <= to) { + this.currentScheduleIndex -= 1; + } else if (this.currentScheduleIndex < from && this.currentScheduleIndex >= to) { + this.currentScheduleIndex += 1; + } + } + getCurrentCourses() { return this.schedules[this.currentScheduleIndex]?.courses || []; } From 513a5687d899f6ee960d76e9873a813c1f90136d Mon Sep 17 00:00:00 2001 From: jotalis Date: Sat, 28 Dec 2024 00:33:39 -0800 Subject: [PATCH 04/10] feat: implement schedule reordering into ScheduleSelect --- .../Calendar/Toolbar/CalendarToolbar.tsx | 17 +- .../Toolbar/ScheduleSelect/ScheduleSelect.tsx | 190 +++++++++++++----- .../drag-and-drop/SortableList.tsx | 5 +- 3 files changed, 146 insertions(+), 66 deletions(-) diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/CalendarToolbar.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/CalendarToolbar.tsx index 415914382..0e8d909ff 100644 --- a/apps/antalmanac/src/components/Calendar/Toolbar/CalendarToolbar.tsx +++ b/apps/antalmanac/src/components/Calendar/Toolbar/CalendarToolbar.tsx @@ -31,9 +31,7 @@ export interface CalendarPaneToolbarProps { */ export const CalendarToolbar = memo((props: CalendarPaneToolbarProps) => { const { showFinalsSchedule, toggleDisplayFinalsSchedule } = props; - const [scheduleNames, setScheduleNames] = useState(AppStore.getScheduleNames()); const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); - const [skeletonScheduleNames, setSkeletonScheduleNames] = useState(AppStore.getSkeletonScheduleNames()); const handleToggleFinals = useCallback(() => { logAnalytics({ @@ -43,14 +41,9 @@ export const CalendarToolbar = memo((props: CalendarPaneToolbarProps) => { toggleDisplayFinalsSchedule(); }, [toggleDisplayFinalsSchedule]); - const handleScheduleNamesChange = useCallback(() => { - setScheduleNames(AppStore.getScheduleNames()); - }, []); - useEffect(() => { const handleSkeletonModeChange = () => { setSkeletonMode(AppStore.getSkeletonMode()); - setSkeletonScheduleNames(AppStore.getSkeletonScheduleNames()); }; AppStore.on('skeletonModeChange', handleSkeletonModeChange); @@ -60,14 +53,6 @@ export const CalendarToolbar = memo((props: CalendarPaneToolbarProps) => { }; }, []); - useEffect(() => { - AppStore.on('scheduleNamesChange', handleScheduleNamesChange); - - return () => { - AppStore.off('scheduleNamesChange', handleScheduleNamesChange); - }; - }, [handleScheduleNamesChange]); - return ( { }} > - + @@ -105,46 +169,78 @@ export function SelectSchedulePopover(props: { scheduleNames: string[] }) { anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} > - {props.scheduleNames.map((name, index) => ( - - - - - - - - - - - - - ))} - + + + + + + + + + + + + + + + ); + }} + /> - diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx index 494fc1e74..c4c5b5285 100644 --- a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx @@ -6,9 +6,8 @@ import { List } from '@mui/material'; import type { ReactNode } from 'react'; import { Fragment, useMemo, useState } from 'react'; -import { DragHandle, SortableItem } from './SortableItem'; -import { SortableOverlay } from './SortableOverlay'; - +import { DragHandle, SortableItem } from '$components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableItem'; +import { SortableOverlay } from '$components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableOverlay'; import AppStore from '$stores/AppStore'; interface BaseItem { From f7efdcf476d92789a6463b25269f21f4bc0fe8cc Mon Sep 17 00:00:00 2001 From: jotalis Date: Sat, 28 Dec 2024 00:59:30 -0800 Subject: [PATCH 05/10] fix(copy-button): ensure correct schedule is copied previously, the copy button would copy the selected schedule instead of the schedule it was linked to. --- .../antalmanac/src/actions/AppStoreActions.ts | 4 +- .../src/components/dialogs/CopySchedule.tsx | 2 +- apps/antalmanac/src/stores/AppStore.ts | 4 +- apps/antalmanac/src/stores/Schedules.ts | 15 ++-- pnpm-lock.yaml | 78 ++++++++++++++++++- 5 files changed, 89 insertions(+), 14 deletions(-) diff --git a/apps/antalmanac/src/actions/AppStoreActions.ts b/apps/antalmanac/src/actions/AppStoreActions.ts index e54a5be49..2ecd9898a 100644 --- a/apps/antalmanac/src/actions/AppStoreActions.ts +++ b/apps/antalmanac/src/actions/AppStoreActions.ts @@ -250,14 +250,14 @@ export const changeCourseColor = (sectionCode: string, term: string, newColor: s AppStore.changeCourseColor(sectionCode, term, newColor); }; -export const copySchedule = (newScheduleName: string, options?: CopyScheduleOptions) => { +export const copySchedule = (index: number, newScheduleName: string, options?: CopyScheduleOptions) => { logAnalytics({ category: analyticsEnum.addedClasses.title, action: analyticsEnum.addedClasses.actions.COPY_SCHEDULE, }); try { - AppStore.copySchedule(newScheduleName); + AppStore.copySchedule(index, newScheduleName); options?.onSuccess(newScheduleName); } catch (error) { options?.onError(newScheduleName); diff --git a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx index ba927da85..1d267b24a 100644 --- a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx @@ -31,7 +31,7 @@ function CopyScheduleDialog(props: CopyScheduleDialogProps) { }, [onClose]); const handleCopy = useCallback(() => { - copySchedule(name); + copySchedule(index, name); onClose?.({}, 'escapeKeyDown'); }, [onClose, name]); diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts index 8aa360a00..1e7171eae 100644 --- a/apps/antalmanac/src/stores/AppStore.ts +++ b/apps/antalmanac/src/stores/AppStore.ts @@ -295,8 +295,8 @@ class AppStore extends EventEmitter { window.localStorage.removeItem('unsavedActions'); } - copySchedule(newScheduleName: string) { - this.schedule.copySchedule(newScheduleName); + copySchedule(index: number, newScheduleName: string) { + this.schedule.copySchedule(index, newScheduleName); this.unsavedChanges = true; const action: CopyScheduleAction = { type: 'copySchedule', diff --git a/apps/antalmanac/src/stores/Schedules.ts b/apps/antalmanac/src/stores/Schedules.ts index 58a4c4511..5de5b081e 100644 --- a/apps/antalmanac/src/stores/Schedules.ts +++ b/apps/antalmanac/src/stores/Schedules.ts @@ -146,11 +146,11 @@ export class Schedules { } /** - * Copy the current schedule to a newly created schedule with the specified name. + * Copy the schedule at the provided index to a newly created schedule with the specified name. */ - copySchedule(newScheduleName: string) { + copySchedule(index: number, newScheduleName: string) { this.addNewSchedule(newScheduleName); - this.currentScheduleIndex = this.previousStates[this.previousStates.length - 1].scheduleIndex; // return to previous schedule index for copying + this.currentScheduleIndex = index; // temporarily set current schedule to the one being copied const to = this.getNumberOfSchedules() - 1; for (const course of this.getCurrentCourses()) { @@ -158,8 +158,9 @@ export class Schedules { } for (const customEvent of this.getCurrentCustomEvents()) { - this.addCustomEvent(customEvent, [to]); + this.addCustomEvent(customEvent, [to], false); } + this.currentScheduleIndex = this.previousStates[this.previousStates.length - 1].scheduleIndex; // return to previously selected schedule index } /** @@ -356,8 +357,10 @@ export class Schedules { /** * Adds a new custom event to given indices */ - addCustomEvent(newCustomEvent: RepeatingCustomEvent, scheduleIndices: number[]) { - this.addUndoState(); + addCustomEvent(newCustomEvent: RepeatingCustomEvent, scheduleIndices: number[], addUndoState = true) { + if (addUndoState) { + this.addUndoState(); + } for (const scheduleIndex of scheduleIndices) { if (!this.doesCustomEventExistInSchedule(newCustomEvent.customEventID, scheduleIndex)) { this.schedules[scheduleIndex].customEvents.push(newCustomEvent); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f73333a81..50e0054c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,18 @@ importers: '@date-io/date-fns': specifier: ^2.16.0 version: 2.17.0(date-fns@2.30.0) + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) '@emotion/react': specifier: ^11.10.5 version: 11.13.3(@types/react@18.3.12)(react@18.3.1) @@ -744,6 +756,34 @@ packages: date-fns: optional: true + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -5906,6 +5946,38 @@ snapshots: optionalDependencies: date-fns: 2.30.0 + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + '@drizzle-team/brocli@0.10.2': {} '@emotion/babel-plugin@11.12.0': @@ -8395,7 +8467,7 @@ snapshots: debug: 4.3.7(supports-color@9.4.0) enhanced-resolve: 5.15.1 eslint: 9.15.0 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@9.15.0))(eslint@9.15.0) + eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -8419,7 +8491,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@9.15.0))(eslint@9.15.0): + eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: @@ -8469,7 +8541,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.15.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@9.15.0))(eslint@9.15.0) + eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 From 84104220b80611f2b9cd17f1f8f2aafb4c834fab Mon Sep 17 00:00:00 2001 From: jotalis Date: Sat, 28 Dec 2024 01:31:29 -0800 Subject: [PATCH 06/10] fix: offset tooltip to be consistent with button tooltips --- .../Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx index 1170d4816..051e19684 100644 --- a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx @@ -139,7 +139,7 @@ export function SelectSchedulePopover() { { name: 'offset', options: { - offset: [-2, -14], + offset: [-2, -10], }, }, ], @@ -194,7 +194,7 @@ export function SelectSchedulePopover() { { name: 'offset', options: { - offset: [-2, -14], + offset: [-2, -10], }, }, ], From cbddd62f18100c131d45385a4a8070e2ec6febaf Mon Sep 17 00:00:00 2001 From: jotalis Date: Sat, 28 Dec 2024 01:35:22 -0800 Subject: [PATCH 07/10] feat: make schedule note in skeleton mode more clearly disabled --- .../RightPane/AddedCourses/AddedCoursePane.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx index 7842f4469..599130a40 100644 --- a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx +++ b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx @@ -191,11 +191,19 @@ function ScheduleNoteBox() { label="Click here to start typing!" onChange={handleNoteChange} value={scheduleNote} - inputProps={{ maxLength: NOTE_MAX_LEN }} + inputProps={{ + maxLength: NOTE_MAX_LEN, + style: { cursor: skeletonMode ? 'not-allowed' : 'text' }, + }} InputProps={{ disableUnderline: true }} fullWidth multiline disabled={skeletonMode} + sx={{ + '& .MuiInputBase-root': { + cursor: skeletonMode ? 'not-allowed' : 'text', + }, + }} /> ); From 101b66ddb4e60b460ce8ae2108b2b5467f0d63d2 Mon Sep 17 00:00:00 2001 From: jotalis Date: Sat, 28 Dec 2024 02:26:42 -0800 Subject: [PATCH 08/10] chore: remove unused code and improve readability --- .../Toolbar/ScheduleSelect/ScheduleSelect.tsx | 69 ++++++++----------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx index 051e19684..0c3078da6 100644 --- a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx @@ -11,6 +11,20 @@ import { CopyScheduleButton } from '$components/buttons/Copy'; import analyticsEnum, { logAnalytics } from '$lib/analytics'; import AppStore from '$stores/AppStore'; +type EventContext = { + triggeredBy?: string; +}; + +type ScheduleItem = { + id: number; + name: string; +}; + +function getScheduleItems(items?: string[]): ScheduleItem[] { + const scheduleNames: string[] = items || AppStore.getScheduleNames(); + return scheduleNames.map((name, index) => ({ id: index, name })); +} + function handleScheduleChange(index: number) { logAnalytics({ category: analyticsEnum.calendar.title, @@ -36,8 +50,6 @@ function createScheduleSelector(index: number) { export function SelectSchedulePopover() { const theme = useTheme(); - // const [currentScheduleIndex, setCurrentScheduleIndex] = useState(() => AppStore.getCurrentScheduleIndex()); - // const [skeletonMode, setSkeletonMode] = useState(() => AppStore.getSkeletonMode()); const [currentScheduleIndex, setCurrentScheduleIndex] = useState(AppStore.getCurrentScheduleIndex()); const [scheduleMapping, setScheduleMapping] = useState(getScheduleItems()); const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); @@ -65,63 +77,40 @@ export function SelectSchedulePopover() { setCurrentScheduleIndex(AppStore.getCurrentScheduleIndex()); }, []); - const handleSkeletonModeChange = () => { - setSkeletonMode(AppStore.getSkeletonMode()); - setSkeletonScheduleMapping(getScheduleItems(AppStore.getSkeletonScheduleNames())); - }; - - function getScheduleItems(items: string[] = AppStore.getScheduleNames()) { - return [...new Array(items.length)].map((_, index) => ({ - id: index, - name: items[index], - })); - } - - useEffect(() => { - AppStore.on('skeletonModeChange', handleSkeletonModeChange); - - return () => { - AppStore.off('skeletonModeChange', handleSkeletonModeChange); - }; - }, []); - useEffect(() => { - // AppStore.on('scheduleNamesChange', handleScheduleIndexChange); AppStore.on('addedCoursesChange', handleScheduleIndexChange); AppStore.on('customEventsChange', handleScheduleIndexChange); AppStore.on('colorChange', handleScheduleIndexChange); AppStore.on('currentScheduleIndexChange', handleScheduleIndexChange); - AppStore.on('skeletonModeChange', handleSkeletonModeChange); return () => { - // AppStore.off('scheduleNamesChange', handleScheduleIndexChange); AppStore.off('addedCoursesChange', handleScheduleIndexChange); AppStore.off('customEventsChange', handleScheduleIndexChange); AppStore.off('colorChange', handleScheduleIndexChange); AppStore.off('currentScheduleIndexChange', handleScheduleIndexChange); - AppStore.off('skeletonModeChange', handleSkeletonModeChange); }; }, [handleScheduleIndexChange]); - type EventContext = { - triggeredBy?: string; - [key: string]: any; - }; - - function handleScheduleNamesChange(context?: EventContext) { - if (context?.triggeredBy === 'reorder') { - return; - } - setScheduleMapping(getScheduleItems()); - } - useEffect(() => { - AppStore.on('scheduleNamesChange', (context) => handleScheduleNamesChange(context)); + const handleScheduleNamesChange = (context?: EventContext) => { + if (context?.triggeredBy === 'reorder') { + return; + } + setScheduleMapping(getScheduleItems()); + }; + const handleSkeletonModeChange = () => { + setSkeletonMode(AppStore.getSkeletonMode()); + setSkeletonScheduleMapping(getScheduleItems(AppStore.getSkeletonScheduleNames())); + }; + + AppStore.on('scheduleNamesChange', handleScheduleNamesChange); + AppStore.on('skeletonModeChange', handleSkeletonModeChange); return () => { AppStore.off('scheduleNamesChange', handleScheduleNamesChange); + AppStore.off('skeletonModeChange', handleSkeletonModeChange); }; - }, [handleScheduleNamesChange]); + }, []); const scheduleMappingToUse = useMemo( () => (skeletonMode ? skeletonScheduleMapping : scheduleMapping), From 378acd8b11a1a8d993c32d279d7e13593a712a91 Mon Sep 17 00:00:00 2001 From: jotalis Date: Sat, 28 Dec 2024 02:41:24 -0800 Subject: [PATCH 09/10] chore: cleanup code --- apps/antalmanac/src/components/dialogs/CopySchedule.tsx | 5 +---- apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx | 6 +----- apps/antalmanac/src/components/dialogs/RenameSchedule.tsx | 5 +---- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx index 1d267b24a..eb0d56db3 100644 --- a/apps/antalmanac/src/components/dialogs/CopySchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/CopySchedule.tsx @@ -40,10 +40,7 @@ function CopyScheduleDialog(props: CopyScheduleDialogProps) { }, [index]); useEffect(() => { - setName(`Copy of ${AppStore.getScheduleNames()[index]}`); - }, [index]); - - useEffect(() => { + handleScheduleNamesChange(); AppStore.on('scheduleNamesChange', handleScheduleNamesChange); return () => { AppStore.off('scheduleNamesChange', handleScheduleNamesChange); diff --git a/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx b/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx index 7d27f0dd6..98623d26a 100644 --- a/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/DeleteSchedule.tsx @@ -51,12 +51,8 @@ function DeleteScheduleDialog(props: ScheduleNameDialogProps) { }, [index]); useEffect(() => { - setName(AppStore.getScheduleNames()[index]); - }, [index]); - - useEffect(() => { + handleScheduleNamesChange(); AppStore.on('scheduleNamesChange', handleScheduleNamesChange); - return () => { AppStore.off('scheduleNamesChange', handleScheduleNamesChange); }; diff --git a/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx b/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx index 1c2c93e38..3641e7ebd 100644 --- a/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx +++ b/apps/antalmanac/src/components/dialogs/RenameSchedule.tsx @@ -71,10 +71,7 @@ function RenameScheduleDialog(props: ScheduleNameDialogProps) { }, [index]); useEffect(() => { - setName(AppStore.getScheduleNames()[index]); - }, [index]); - - useEffect(() => { + handleScheduleNamesChange(); AppStore.on('scheduleNamesChange', handleScheduleNamesChange); return () => { From fd4ef8eeda3951530b084ea85f0c0d168c09dc89 Mon Sep 17 00:00:00 2001 From: jotalis Date: Sat, 28 Dec 2024 19:00:25 -0800 Subject: [PATCH 10/10] feat: add reordering as an action stored in local storage --- apps/antalmanac/src/actions/ActionTypesStore.ts | 13 ++++++++++++- .../Toolbar/ScheduleSelect/ScheduleSelect.tsx | 5 +---- .../ScheduleSelect/drag-and-drop/SortableList.tsx | 2 +- apps/antalmanac/src/stores/AppStore.ts | 13 +++++++++++-- apps/antalmanac/src/stores/Schedules.ts | 2 +- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/apps/antalmanac/src/actions/ActionTypesStore.ts b/apps/antalmanac/src/actions/ActionTypesStore.ts index 0284234ef..b7d9de78a 100644 --- a/apps/antalmanac/src/actions/ActionTypesStore.ts +++ b/apps/antalmanac/src/actions/ActionTypesStore.ts @@ -59,9 +59,16 @@ export interface ClearScheduleAction { export interface CopyScheduleAction { type: 'copySchedule'; + index: number; newScheduleName: string; } +export interface ReorderScheduleAction { + type: 'reorderSchedule'; + from: number; + to: number; +} + export interface ChangeCourseColorAction { type: 'changeCourseColor'; sectionCode: string; @@ -78,6 +85,7 @@ export type ActionType = | ChangeCustomEventColorAction | ClearScheduleAction | CopyScheduleAction + | ReorderScheduleAction | ChangeCourseColorAction | UndoAction; @@ -159,7 +167,10 @@ class ActionTypesStore extends EventEmitter { AppStore.schedule.clearCurrentSchedule(); break; case 'copySchedule': - AppStore.schedule.copySchedule(action.newScheduleName); + AppStore.schedule.copySchedule(action.index, action.newScheduleName); + break; + case 'reorderSchedule': + AppStore.schedule.reorderSchedule(action.from, action.to); break; default: break; diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx index 0c3078da6..46e34a370 100644 --- a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/ScheduleSelect.tsx @@ -112,10 +112,7 @@ export function SelectSchedulePopover() { }; }, []); - const scheduleMappingToUse = useMemo( - () => (skeletonMode ? skeletonScheduleMapping : scheduleMapping), - [skeletonMode, skeletonScheduleMapping, scheduleMapping] - ); + const scheduleMappingToUse = skeletonMode ? skeletonScheduleMapping : scheduleMapping; return ( diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx index c4c5b5285..3b6fe9172 100644 --- a/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx +++ b/apps/antalmanac/src/components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList.tsx @@ -42,7 +42,7 @@ export function SortableList({ items, onChange, renderItem } const activeIndex = items.findIndex(({ id }) => id === active.id); const overIndex = items.findIndex(({ id }) => id === over.id); onChange(arrayMove(items, activeIndex, overIndex)); - AppStore.reorderSchedules(activeIndex, overIndex); + AppStore.reorderSchedule(activeIndex, overIndex); } setActive(null); }} diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts index 1e7171eae..1fad70f47 100644 --- a/apps/antalmanac/src/stores/AppStore.ts +++ b/apps/antalmanac/src/stores/AppStore.ts @@ -13,6 +13,7 @@ import type { ChangeCustomEventColorAction, ClearScheduleAction, CopyScheduleAction, + ReorderScheduleAction, ChangeCourseColorAction, UndoAction, } from '$actions/ActionTypesStore'; @@ -300,6 +301,7 @@ class AppStore extends EventEmitter { this.unsavedChanges = true; const action: CopyScheduleAction = { type: 'copySchedule', + index: index, newScheduleName: newScheduleName, }; actionTypesStore.autoSaveSchedule(action); @@ -310,8 +312,15 @@ class AppStore extends EventEmitter { this.emit('customEventsChange'); } - reorderSchedules(from: number, to: number) { - this.schedule.reorderSchedules(from, to); + reorderSchedule(from: number, to: number) { + this.schedule.reorderSchedule(from, to); + this.unsavedChanges = true; + const action: ReorderScheduleAction = { + type: 'reorderSchedule', + from: from, + to: to, + }; + actionTypesStore.autoSaveSchedule(action); this.emit('currentScheduleIndexChange'); this.emit('scheduleNamesChange', { triggeredBy: 'reorder' }); this.emit('reorderSchedule'); diff --git a/apps/antalmanac/src/stores/Schedules.ts b/apps/antalmanac/src/stores/Schedules.ts index 5de5b081e..55c7e331f 100644 --- a/apps/antalmanac/src/stores/Schedules.ts +++ b/apps/antalmanac/src/stores/Schedules.ts @@ -167,7 +167,7 @@ export class Schedules { * Reorder schedules by moving a schedule from one index to another. * This modifies the order of schedules and updates the current schedule index to maintain the correct reference. */ - reorderSchedules(from: number, to: number) { + reorderSchedule(from: number, to: number) { this.addUndoState(); const [removed] = this.schedules.splice(from, 1); this.schedules.splice(to, 0, removed);