Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/drag-and-drop schedules #1089

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions apps/antalmanac/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions apps/antalmanac/src/actions/ActionTypesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ export interface CopyScheduleAction {
newScheduleName: string;
}

export interface ReorderScheduleAction {
type: 'reorderSchedule';
from: number;
to: number;
}

export interface ChangeCourseColorAction {
type: 'changeCourseColor';
sectionCode: string;
Expand All @@ -98,6 +104,7 @@ export type ActionType =
| RenameScheduleAction
| DeleteScheduleAction
| CopyScheduleAction
| ReorderScheduleAction
| ChangeCourseColorAction
| UndoAction;

Expand Down Expand Up @@ -190,6 +197,9 @@ class ActionTypesStore extends EventEmitter {
case 'deleteSchedule':
AppStore.schedule.deleteSchedule(action.scheduleIndex);
break;
case 'reorderSchedule':
AppStore.schedule.reorderSchedule(action.from, action.to);
break;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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);
Expand All @@ -60,14 +53,6 @@ export const CalendarToolbar = memo((props: CalendarPaneToolbarProps) => {
};
}, []);

useEffect(() => {
AppStore.on('scheduleNamesChange', handleScheduleNamesChange);

return () => {
AppStore.off('scheduleNamesChange', handleScheduleNamesChange);
};
}, [handleScheduleNamesChange]);

return (
<Paper
elevation={0}
Expand All @@ -82,7 +67,7 @@ export const CalendarToolbar = memo((props: CalendarPaneToolbarProps) => {
}}
>
<Box gap={1} display="flex" alignItems="center">
<SelectSchedulePopover scheduleNames={skeletonMode ? skeletonScheduleNames : scheduleNames} />
<SelectSchedulePopover />
<Tooltip title="Toggle showing finals schedule">
<Button
color={showFinalsSchedule ? 'primary' : 'inherit'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,28 @@ import { Box, Button, Popover, Typography, useTheme, Tooltip } from '@mui/materi
import { useCallback, useEffect, useMemo, useState } from 'react';

import { changeCurrentSchedule } from '$actions/AppStoreActions';
import { SortableList } from '$components/Calendar/Toolbar/ScheduleSelect/drag-and-drop/SortableList';
import { AddScheduleButton } from '$components/Calendar/Toolbar/ScheduleSelect/schedule-select-buttons/AddScheduleButton';
import { DeleteScheduleButton } from '$components/Calendar/Toolbar/ScheduleSelect/schedule-select-buttons/DeleteScheduleButton';
import { RenameScheduleButton } from '$components/Calendar/Toolbar/ScheduleSelect/schedule-select-buttons/RenameScheduleButton';
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,
Expand All @@ -32,11 +47,16 @@ function createScheduleSelector(index: number) {
*
* Can select a schedule, and also control schedule settings with buttons.
*/
export function SelectSchedulePopover(props: { scheduleNames: string[] }) {
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());
const [skeletonScheduleMapping, setSkeletonScheduleMapping] = useState(
getScheduleItems(AppStore.getSkeletonScheduleNames())
);

const [anchorEl, setAnchorEl] = useState<HTMLElement>();

// TODO: maybe these widths should be dynamic based on i.e. the viewport width?
Expand All @@ -45,10 +65,6 @@ export function SelectSchedulePopover(props: { scheduleNames: string[] }) {

const open = useMemo(() => Boolean(anchorEl), [anchorEl]);

const currentScheduleName = useMemo(() => {
return props.scheduleNames[currentScheduleIndex];
}, [props.scheduleNames, currentScheduleIndex]);

const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
}, []);
Expand All @@ -61,29 +77,63 @@ export function SelectSchedulePopover(props: { scheduleNames: string[] }) {
setCurrentScheduleIndex(AppStore.getCurrentScheduleIndex());
}, []);

const handleSkeletonModeChange = () => {
setSkeletonMode(AppStore.getSkeletonMode());
};

useEffect(() => {
AppStore.on('addedCoursesChange', handleScheduleIndexChange);
AppStore.on('customEventsChange', handleScheduleIndexChange);
AppStore.on('colorChange', handleScheduleIndexChange);
AppStore.on('currentScheduleIndexChange', handleScheduleIndexChange);
AppStore.on('skeletonModeChange', handleSkeletonModeChange);

return () => {
AppStore.off('addedCoursesChange', handleScheduleIndexChange);
AppStore.off('customEventsChange', handleScheduleIndexChange);
AppStore.off('colorChange', handleScheduleIndexChange);
AppStore.off('currentScheduleIndexChange', handleScheduleIndexChange);
AppStore.off('skeletonModeChange', handleSkeletonModeChange);
};
}, [handleScheduleIndexChange]);

useEffect(() => {
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);
};
}, []);

const scheduleMappingToUse = skeletonMode ? skeletonScheduleMapping : scheduleMapping;

return (
<Box>
<Tooltip title={currentScheduleName} enterDelay={200} disableInteractive>
<Tooltip
title={scheduleMappingToUse[currentScheduleIndex]?.name}
enterDelay={200}
slotProps={{
popper: {
modifiers: [
{
name: 'offset',
options: {
offset: [-2, -10],
},
},
],
},
}}
placement="bottom-start"
disableInteractive
>
<Button
size="small"
color="inherit"
Expand All @@ -92,7 +142,7 @@ export function SelectSchedulePopover(props: { scheduleNames: string[] }) {
sx={{ minWidth, maxWidth, justifyContent: 'space-between' }}
>
<Typography whiteSpace="nowrap" textOverflow="ellipsis" overflow="hidden" textTransform="none">
{currentScheduleName}
{scheduleMappingToUse[currentScheduleIndex]?.name || null}
</Typography>
<ArrowDropDownIcon />
</Button>
Expand All @@ -105,46 +155,78 @@ export function SelectSchedulePopover(props: { scheduleNames: string[] }) {
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Box padding={1}>
{props.scheduleNames.map((name, index) => (
<Box key={index} display="flex" alignItems="center" gap={1}>
<Box flexGrow={1}>
<Tooltip title={name} enterDelay={200} disableInteractive>
<Button
color="inherit"
sx={{
minWidth,
maxWidth,
width: '100%',
display: 'flex',
justifyContent: 'flex-start',
background:
index === currentScheduleIndex
? theme.palette.action.selected
: undefined,
}}
onClick={createScheduleSelector(index)}
<SortableList
items={scheduleMappingToUse}
onChange={setScheduleMapping}
renderItem={(item) => {
const index = scheduleMappingToUse.indexOf(item);
return (
<SortableList.Item id={item.id}>
<Box
display="flex"
gap={1}
justifyContent="space-between"
alignItems="center"
flexGrow={1}
>
<Typography
overflow="hidden"
whiteSpace="nowrap"
textTransform="none"
textOverflow="ellipsis"
>
{name}
</Typography>
</Button>
</Tooltip>
</Box>
<Box display="flex" alignItems="center" gap={0.5}>
<CopyScheduleButton index={index} disabled={skeletonMode} />
<RenameScheduleButton index={index} disabled={skeletonMode} />
<DeleteScheduleButton index={index} disabled={skeletonMode} />
</Box>
</Box>
))}

<SortableList.DragHandle disabled={skeletonMode} />
<Box flexGrow={1}>
<Tooltip
title={item.name}
enterDelay={200}
slotProps={{
popper: {
modifiers: [
{
name: 'offset',
options: {
offset: [-2, -10],
},
},
],
},
}}
placement="bottom-start"
disableInteractive
>
<Button
color="inherit"
sx={{
minWidth,
maxWidth,
width: '100%',
display: 'flex',
justifyContent: 'flex-start',
background:
index === currentScheduleIndex
? theme.palette.action.selected
: undefined,
}}
onClick={() => createScheduleSelector(index)()}
>
<Typography
overflow="hidden"
whiteSpace="nowrap"
textTransform="none"
textOverflow="ellipsis"
>
{item.name}
</Typography>
</Button>
</Tooltip>
</Box>

<Box display="flex" alignItems="center" gap={0.5}>
<CopyScheduleButton index={index} disabled={skeletonMode} />
<RenameScheduleButton index={index} disabled={skeletonMode} />
<DeleteScheduleButton index={index} disabled={skeletonMode} />
</Box>
</Box>
</SortableList.Item>
);
}}
/>
<Box marginY={1} />

<AddScheduleButton disabled={skeletonMode} />
</Box>
</Popover>
Expand Down
Loading
Loading