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
This commit is contained in:
Razboy20
2024-03-14 23:09:04 -05:00
committed by GitHub
parent 5714ed16d7
commit 85769e9d2c
31 changed files with 182 additions and 304 deletions

View File

@@ -79,7 +79,6 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element {
)}
<CourseCatalogInjectedPopup
course={selectedCourse}
activeSchedule={activeSchedule}
show={showPopup}
onClose={() => setShowPopup(false)}
afterLeave={() => setSelectedCourse(null)}

View File

@@ -64,10 +64,10 @@ export default function PopupMain(): JSX.Element {
<ScheduleDropdown>
<List
draggables={schedules}
equalityCheck={(a, b) => 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) => (
<ScheduleListItem
name={schedule.name}
schedule={schedule}
onClick={() => {
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) => (

View File

@@ -83,7 +83,6 @@ export default function Calendar(): JSX.Element {
<CourseCatalogInjectedPopup
course={course}
activeSchedule={activeSchedule}
onClose={() => setShowPopup(false)}
open={showPopup}
afterLeave={() => setCourse(null)}

View File

@@ -40,8 +40,8 @@ export default function CalendarHeader(): JSX.Element {
<div className='flex items-center gap-2'>
<LogoIcon />
<div className='flex flex-col whitespace-nowrap'>
<Text className='text-lg! text-ut-burntorange font-medium!'>UT Registration</Text>
<Text className='text-lg! text-ut-orange font-medium!'>Plus</Text>
<Text className='text-ut-burntorange text-lg! font-medium!'>UT Registration</Text>
<Text className='text-ut-orange text-lg! font-medium!'>Plus</Text>
</div>
</div>
</div>
@@ -52,7 +52,7 @@ export default function CalendarHeader(): JSX.Element {
totalHours={activeSchedule.hours}
totalCourses={activeSchedule.courses.length}
/>
<Text variant='h4' className='text-xs! text-gray font-medium! leading-normal!'>
<Text variant='h4' className='text-gray text-xs! font-medium! leading-normal!'>
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
</div>

View File

@@ -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
<List
gap={10}
draggables={schedules}
equalityCheck={(a, b) => 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) => (
<ScheduleListItem
name={schedule.name}
schedule={schedule}
onClick={() => {
switchSchedule(schedule.name);
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>

View File

@@ -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<T> {
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<T>(draggableElements: T[]) {
return draggableElements.map((element, index) => ({
id: `id:${index}`,
function wrap<T>(draggableElements: T[], keyTransform: ListProps<T>['itemKey']) {
return draggableElements.map(element => ({
id: keyTransform(element),
content: element,
}));
}
@@ -68,20 +70,19 @@ function Item<T>(props: {
* <List draggableElements={elements} />
*/
function List<T>(props: ListProps<T>): 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<T>(props: ListProps<T>): JSX.Element {
...style,
}}
>
{transformFunction(items[rubric.source.index].content, provided.dragHandleProps)}
<ExtensionRoot>
{transformFunction(items[rubric.source.index].content, provided.dragHandleProps)}
</ExtensionRoot>
</Item>
);
}}
@@ -135,7 +138,7 @@ function List<T>(props: ListProps<T>): JSX.Element {
style={{ marginBottom: `-${props.gap}px` }}
>
{items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
<Draggable key={item.id} draggableId={item.id.toString()} index={index}>
{draggableProvided => (
<div
ref={draggableProvided.innerRef}

View File

@@ -1,3 +1,4 @@
import type { UserSchedule } from '@shared/types/UserSchedule';
import Text from '@views/components/common/Text/Text';
import useSchedules from '@views/hooks/useSchedules';
import clsx from 'clsx';
@@ -10,7 +11,7 @@ import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
*/
export type Props = {
style?: React.CSSProperties;
name: string;
schedule: UserSchedule;
dragHandleProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'className'>;
onClick?: React.DOMAttributes<HTMLDivElement>['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 (
<div style={{ ...style }} className='items-center rounded bg-white'>
@@ -42,7 +43,7 @@ export default function ScheduleListItem({ style, name, dragHandleProps, onClick
}
)}
/>
<Text variant='p'>{name}</Text>
<Text variant='p'>{schedule.name}</Text>
</div>
</div>
</li>

View File

@@ -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<HTMLDivElement>(null);
const [activeSchedule] = useSchedules();
return (
<Dialog className='max-w-[780px] px-6' {...rest} initialFocus={emptyRef}>

View File

@@ -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 });
}
};

View File

@@ -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(),

View File

@@ -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<UserSchedule[]>(schedulesCache);
const [activeIndex, setActiveIndex] = useState<number>(activeIndexCache);
const [activeSchedule, setActiveSchedule] = useState<UserSchedule>(schedules[activeIndex]);
const [activeSchedule, setActiveSchedule] = useState<UserSchedule>(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<void> {
export async function switchSchedule(id: string): Promise<void> {
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);
}