Skip to content

Commit 8ed8bca

Browse files
authored
Add the primary timetable feature (#1007)
* Add frontend UI - context window selection for primary timetable & star icon with tooltip * Complete frontend * Some preliminary backend work * Add primary timetable migration * Finish up frontend type annotations * Set the left timetable to primary if non-found for foward implication * Remove warning on reselecting - grey it out & add a tooltip instead * I am the 144 (bus) * Add undefined check to displayTimetables arr * I am the 144 (train) * school of computer science and engineering * Remove unused import that vscode added (poweruser alert)
1 parent 619d82f commit 8ed8bca

File tree

11 files changed

+67
-7
lines changed

11 files changed

+67
-7
lines changed

client/src/components/timetableTabs/TimetableTabContextMenu.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Close, Edit, EditNote, FileCopy, Save } from '@mui/icons-material';
1+
import { Close, Edit, EditNote, FileCopy, Star, Save } from '@mui/icons-material';
22
import {
33
Button,
44
Dialog,
@@ -81,12 +81,19 @@ const TimetableTabContextMenu: React.FC<TimetableTabContextMenuProps> = ({ ancho
8181
// Handler for deleting a timetable
8282
const handleDeleteTimetable = (targetIndex: number) => {
8383
if (displayTimetables[term].length > 1) {
84+
if (displayTimetables[term].findIndex((t: TimetableData, index: number) => t.isPrimary) === targetIndex) {
85+
setAlertMsg('You cannot delete the primary timetable.');
86+
setErrorVisibility(true);
87+
return;
88+
}
89+
8490
prevTimetables = {
8591
selected: selectedTimetable,
8692
timetables: displayTimetables[term].map((timetable: TimetableData) => {
8793
return {
8894
name: timetable.name,
8995
id: timetable.id,
96+
isPrimary: timetable.isPrimary,
9097
selectedCourses: timetable.selectedCourses,
9198
selectedClasses: duplicateClasses(timetable.selectedClasses),
9299
createdEvents: timetable.createdEvents,
@@ -99,7 +106,7 @@ const TimetableTabContextMenu: React.FC<TimetableTabContextMenuProps> = ({ ancho
99106

100107
const newDisplayTimetables = {
101108
...displayTimetables,
102-
[term]: displayTimetables[term].filter((timetable: TimetableData, index: number) => index !== targetIndex),
109+
[term]: displayTimetables[term].filter((_: TimetableData, index: number) => index !== targetIndex),
103110
};
104111
// Updating the timetables state to the new timetable index
105112
setDisplayTimetables(newDisplayTimetables);
@@ -128,7 +135,7 @@ const TimetableTabContextMenu: React.FC<TimetableTabContextMenuProps> = ({ ancho
128135
return;
129136
});
130137
} else {
131-
setAlertMsg('Must have at least 1 timetable');
138+
setAlertMsg('Must have at least 1 timetable.');
132139
setErrorVisibility(true);
133140
}
134141
};
@@ -183,6 +190,7 @@ const TimetableTabContextMenu: React.FC<TimetableTabContextMenuProps> = ({ ancho
183190
const handleDuplicateTimetable = () => {
184191
if (displayTimetables[term].length >= TIMETABLE_LIMIT) {
185192
setAlertMsg('Maximum timetables reached');
193+
186194
setErrorVisibility(true);
187195
} else {
188196
const currentTimetable = displayTimetables[term][selectedTimetable];
@@ -215,6 +223,24 @@ const TimetableTabContextMenu: React.FC<TimetableTabContextMenuProps> = ({ ancho
215223
}
216224
};
217225

226+
const handleSetPrimary = () => {
227+
if (displayTimetables[term].findIndex((t: TimetableData, _: number) => t.isPrimary) === selectedTimetable) {
228+
return;
229+
}
230+
displayTimetables[term].forEach((e) => {
231+
e.isPrimary = false;
232+
});
233+
displayTimetables[term][selectedTimetable].isPrimary = true;
234+
storage.set('timetables', displayTimetables);
235+
handleMenuClose();
236+
};
237+
238+
const isPrimarySelected = () => {
239+
return displayTimetables[term] !== undefined
240+
? displayTimetables[term].findIndex((t: TimetableData, _: number) => t.isPrimary) === selectedTimetable
241+
: false;
242+
};
243+
218244
/**
219245
* Menu shortcut (hotkey) event listeners
220246
*/
@@ -330,6 +356,14 @@ const TimetableTabContextMenu: React.FC<TimetableTabContextMenuProps> = ({ ancho
330356
onClose={handleMenuClose}
331357
autoFocus={false}
332358
>
359+
<Tooltip title={isPrimarySelected() ? 'This timetable is already primary' : ''}>
360+
<MenuItem onClick={handleSetPrimary} sx={{ opacity: isPrimarySelected() ? 0.5 : 1 }}>
361+
<ListItemIcon>
362+
<Star fontSize="small" />
363+
</ListItemIcon>
364+
<ListItemText>Set as primary</ListItemText>
365+
</MenuItem>
366+
</Tooltip>
333367
<MenuItem onClick={handleRenameOpen}>
334368
<ListItemIcon>
335369
<Edit fontSize="small" />

client/src/components/timetableTabs/TimetableTabs.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Add, MoreHoriz } from '@mui/icons-material';
2-
import { Box, Tooltip } from '@mui/material';
1+
import { Add, MoreHoriz, Star } from '@mui/icons-material';
2+
import { Box, Icon, Tooltip } from '@mui/material';
33
import React, { useContext, useEffect, useState } from 'react';
44
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
55
import { v4 as uuidv4 } from 'uuid';
@@ -87,6 +87,7 @@ const TimetableTabs: React.FC = () => {
8787
const newTimetable: TimetableData = {
8888
name: 'New Timetable',
8989
id: uuidv4(),
90+
isPrimary: false,
9091
selectedCourses: [],
9192
selectedClasses: {},
9293
createdEvents: {},
@@ -184,6 +185,11 @@ const TimetableTabs: React.FC = () => {
184185
{...props.dragHandleProps}
185186
sx={TabStyle(index, selectedTimetable)}
186187
>
188+
{displayTimetables[term][index].isPrimary && (
189+
<Tooltip title="A primary timetable is the timetable for social features.">
190+
<Star fontSize="small" className="pr-1.5"></Star>
191+
</Tooltip>
192+
)}
187193
{timetable.name}
188194
{selectedTimetable === index ? (
189195
<StyledSpan onClick={handleMenuClick}>

client/src/context/UserContext.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ const UserContextProvider = ({ children }: UserContextProviderProps) => {
7272
const timetables = await Promise.all(
7373
res.data.timetables.map((timetable: TimetableDTO) => parseTimetableDTO(timetable, year)),
7474
);
75-
7675
// Unpack timetables based on key
7776
const timetableMap: DisplayTimetablesMap = {};
7877

@@ -93,6 +92,10 @@ const UserContextProvider = ({ children }: UserContextProviderProps) => {
9392
timetableMap[term] = createDefaultTimetable(res.data.userID);
9493
}
9594
setDisplayTimetables({ ...timetableMap });
95+
96+
Object.keys(timetableMap).forEach(() =>{
97+
if (timetableMap[term].every((t) => !t.isPrimary)) timetableMap[term][0].isPrimary = true;
98+
})
9699

97100
// TODO: check if this conditional is necessary
98101
if (timetableMap[term] && timetableMap[term][0]) {

client/src/interfaces/Periods.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface ClassData {
4646
export interface TimetableData {
4747
name: string;
4848
id: string;
49+
isPrimary: boolean;
4950
selectedCourses: CourseData[];
5051
selectedClasses: SelectedClasses;
5152
createdEvents: CreatedEvents;
@@ -100,6 +101,7 @@ export interface ScrapedClassDTO {
100101
export interface TimetableDTO {
101102
id: string;
102103
name: string;
104+
isPrimary: boolean;
103105
selectedCourses: string[];
104106
selectedClasses: ScrapedClassDTO[];
105107
createdEvents: EventDTO[];

client/src/utils/syncTimetables.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const parseTimetableDTO = async (timetableDTO: TimetableDTO, currentYear: string
146146
const parsedTimetable: TimetableData = {
147147
id: timetableDTO.id,
148148
name: timetableDTO.name,
149+
isPrimary: timetableDTO.isPrimary,
149150
selectedCourses: courseInfo,
150151
selectedClasses: selectedClasses,
151152
createdEvents: createdEvents,

client/src/utils/timetableHelpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const createDefaultTimetable = (userID: string | undefined): TimetableData[] =>
149149
const defaultTimetable = {
150150
name: 'My timetable',
151151
id: uuidv4(),
152+
isPrimary: true,
152153
selectedCourses: [],
153154
selectedClasses: {},
154155
createdEvents: {},
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "timetables" ADD COLUMN "isPrimary" BOOLEAN NOT NULL DEFAULT false;

server/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ model Class {
8282
model Timetable {
8383
id String @id @unique @default(uuid())
8484
name String
85+
isPrimary Boolean @default(false)
8586
selectedCourses String[]
8687
selectedClasses Class[]
8788
createdEvents Event[]

server/src/user/dto/timetable.dto.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { IsArray, IsString } from 'class-validator';
1+
import { IsArray, IsBoolean, IsString } from 'class-validator';
22

33
export class TimetableDto {
44
@IsString()
55
id: string; // Randomly generated on the backend
6+
@IsBoolean()
7+
isPrimary: boolean;
68

79
@IsArray()
810
@IsString({ each: true })
@@ -16,6 +18,8 @@ export class TimetableDto {
1618
export class ReconstructedTimetableDto {
1719
@IsString()
1820
id: string; // Randomly generated on the backend
21+
@IsBoolean()
22+
isPrimary: boolean;
1923

2024
@IsArray()
2125
@IsString({ each: true })

server/src/user/user.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export class UserController {
116116
createUserTimetable(
117117
@Request() req,
118118
@Body('userId') userId: string,
119+
@Body('isPrimary') isPrimary: boolean,
119120
@Body('selectedCourses') selectedCourses: string[],
120121
@Body('selectedClasses') selectedClasses: ClassDto[],
121122
@Body('createdEvents') createdEvents: EventDto[],
@@ -132,6 +133,7 @@ export class UserController {
132133
return this.userService
133134
.createUserTimetable(
134135
userId,
136+
isPrimary,
135137
selectedCourses,
136138
selectedClasses,
137139
createdEvents,

0 commit comments

Comments
 (0)