Skip to content

Commit 4ce23c1

Browse files
zhoukerrrzwliew
authored andcommitted
Add support for Timetable for TAs (#3434)
* feat: fix lint * feat: add customise module button * feat: add redux actions * feat: connect button to redux action * feat: enter and edit module * feat: exit customising state * cahnge customisemod default state to empty string * feat: add timetable state for custimised mods * feat: remove validation for custom mods * feat: add TA label to cells * feat: hide and disable other edit buttons when editing * feat: add support for changing lessons after customisation * chore: fix test cases * chore: fox lint * chore: fix schema migration test
1 parent 48070a1 commit 4ce23c1

17 files changed

+352
-47
lines changed

website/src/actions/__snapshots__/timetables.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ exports[`cancelModifyLesson should not have payload 1`] = `
1010
exports[`changeLesson should return updated information to change lesson 1`] = `
1111
{
1212
"payload": {
13+
"activeLesson": "1",
1314
"classNo": "1",
1415
"lessonType": "Recitation",
1516
"moduleCode": "CS1010S",

website/src/actions/timetables.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test('modifyLesson should return lesson payload', () => {
3636
test('changeLesson should return updated information to change lesson', () => {
3737
const semester: Semester = 1;
3838
const lesson: Lesson = lessons[1];
39-
expect(actions.changeLesson(semester, lesson)).toMatchSnapshot();
39+
expect(actions.changeLesson(semester, lesson, lesson.classNo)).toMatchSnapshot();
4040
});
4141

4242
test('cancelModifyLesson should not have payload', () => {

website/src/actions/timetables.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,24 @@ export function modifyLesson(activeLesson: Lesson) {
9090
};
9191
}
9292

93+
export const CUSTOMISE_MODULE = 'CUSTOMISE_LESSON' as const;
94+
export function customiseLesson(semester: Semester, moduleCode: ModuleCode) {
95+
return {
96+
type: CUSTOMISE_MODULE,
97+
payload: {
98+
semester,
99+
moduleCode,
100+
},
101+
};
102+
}
103+
93104
export const CHANGE_LESSON = 'CHANGE_LESSON' as const;
94105
export function setLesson(
95106
semester: Semester,
96107
moduleCode: ModuleCode,
97108
lessonType: LessonType,
98109
classNo: ClassNo,
110+
activeLesson: ClassNo,
99111
) {
100112
return {
101113
type: CHANGE_LESSON,
@@ -104,12 +116,71 @@ export function setLesson(
104116
moduleCode,
105117
lessonType,
106118
classNo,
119+
activeLesson,
107120
},
108121
};
109122
}
110123

111-
export function changeLesson(semester: Semester, lesson: Lesson) {
112-
return setLesson(semester, lesson.moduleCode, lesson.lessonType, lesson.classNo);
124+
export const ADD_CUSTOM_MODULE = 'ADD_CUSTOM_MODULE' as const;
125+
export function addCustomModule(semester: Semester, moduleCode: ModuleCode) {
126+
return {
127+
type: ADD_CUSTOM_MODULE,
128+
payload: {
129+
semester,
130+
moduleCode,
131+
},
132+
};
133+
}
134+
135+
export const REMOVE_CUSTOM_MODULE = 'REMOVE_CUSTOM_MODULE' as const;
136+
export function removeCustomModule(semester: Semester, moduleCode: ModuleCode) {
137+
return {
138+
type: REMOVE_CUSTOM_MODULE,
139+
payload: {
140+
semester,
141+
moduleCode,
142+
},
143+
};
144+
}
145+
146+
export const ADD_LESSON = 'ADD_LESSON' as const;
147+
export function addLesson(
148+
semester: Semester,
149+
moduleCode: ModuleCode,
150+
lessonType: LessonType,
151+
classNo: ClassNo,
152+
) {
153+
return {
154+
type: ADD_LESSON,
155+
payload: {
156+
semester,
157+
moduleCode,
158+
lessonType,
159+
classNo,
160+
},
161+
};
162+
}
163+
164+
export const REMOVE_LESSON = 'REMOVE_LESSON' as const;
165+
export function removeLesson(
166+
semester: Semester,
167+
moduleCode: ModuleCode,
168+
lessonType: LessonType,
169+
classNo: ClassNo,
170+
) {
171+
return {
172+
type: REMOVE_LESSON,
173+
payload: {
174+
semester,
175+
moduleCode,
176+
lessonType,
177+
classNo,
178+
},
179+
};
180+
}
181+
182+
export function changeLesson(semester: Semester, lesson: Lesson, activeLesson: ClassNo) {
183+
return setLesson(semester, lesson.moduleCode, lesson.lessonType, lesson.classNo, activeLesson);
113184
}
114185

115186
export const SET_LESSON_CONFIG = 'SET_LESSON_CONFIG' as const;
@@ -165,6 +236,14 @@ export function validateTimetable(semester: Semester) {
165236
const module = moduleBank.modules[moduleCode];
166237
if (!module) return;
167238

239+
// If the module is customised, we do not validate it
240+
if (
241+
timetables.customisedModules &&
242+
timetables.customisedModules[semester] &&
243+
timetables.customisedModules[semester].includes(moduleCode)
244+
) {
245+
return;
246+
}
168247
const [validatedLessonConfig, changedLessonTypes] = validateModuleLessons(
169248
semester,
170249
lessonConfig,

website/src/reducers/app.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const appInitialState: AppState = {
2020
isFeedbackModalOpen: false,
2121
promptRefresh: false,
2222
notifications: [],
23+
customiseModule: '',
2324
};
2425
const appHasSemesterTwoState: AppState = { ...appInitialState, activeSemester: anotherSemester };
2526
const appHasActiveLessonState: AppState = { ...appInitialState, activeLesson: lesson };
@@ -55,7 +56,7 @@ test('app should set active lesson', () => {
5556
});
5657

5758
test('app should accept lesson change and unset active lesson', () => {
58-
const action = changeLesson(semester, lesson);
59+
const action = changeLesson(semester, lesson, lesson.classNo);
5960
const nextState: AppState = reducer(appInitialState, action);
6061

6162
expect(nextState).toEqual(appInitialState);

website/src/reducers/app.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { Actions } from 'types/actions';
33
import config from 'config';
44

55
import { forceRefreshPrompt } from 'utils/debug';
6-
import { MODIFY_LESSON, CHANGE_LESSON, CANCEL_MODIFY_LESSON } from 'actions/timetables';
6+
import {
7+
MODIFY_LESSON,
8+
CHANGE_LESSON,
9+
CANCEL_MODIFY_LESSON,
10+
CUSTOMISE_MODULE,
11+
} from 'actions/timetables';
712
import { SELECT_SEMESTER } from 'actions/settings';
813
import {
914
OPEN_NOTIFICATION,
@@ -18,6 +23,7 @@ const defaultAppState = (): AppState => ({
1823
activeSemester: config.semester,
1924
// The lesson being modified on the timetable.
2025
activeLesson: null,
26+
customiseModule: '',
2127
isOnline: navigator.onLine,
2228
isFeedbackModalOpen: false,
2329
promptRefresh: forceRefreshPrompt(),
@@ -37,6 +43,11 @@ function app(state: AppState = defaultAppState(), action: Actions): AppState {
3743
...state,
3844
activeLesson: action.payload.activeLesson,
3945
};
46+
case CUSTOMISE_MODULE:
47+
return {
48+
...state,
49+
customiseModule: action.payload.moduleCode,
50+
};
4051
case CANCEL_MODIFY_LESSON:
4152
case CHANGE_LESSON:
4253
return {

website/src/reducers/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ test('reducers should set export data state', () => {
7272
PC1222: 2,
7373
},
7474
},
75+
customisedModules: {},
7576
hidden: { [1]: ['PC1222'] },
7677
academicYear: expect.any(String),
7778
archive: {},

website/src/reducers/timetables.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ describe('stateReconciler', () => {
207207
},
208208
academicYear: config.academicYear,
209209
archive: oldArchive,
210+
customisedModules: {},
210211
};
211212

212213
const { stateReconciler } = persistConfig;
@@ -259,6 +260,7 @@ describe('redux schema migration', () => {
259260
hidden: {},
260261
academicYear: '2022/2023',
261262
archive: {},
263+
customisedModules: {},
262264
_persist: {
263265
version: 1,
264266
rehydrated: false,
@@ -283,6 +285,7 @@ describe('redux schema migration', () => {
283285
hidden: {},
284286
academicYear: '2022/2023',
285287
archive: {},
288+
customisedModules: {},
286289
_persist: {
287290
version: 1, // version kept the same because the framework does not support it in unit tests
288291
rehydrated: false,

website/src/reducers/timetables.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ import config from 'config';
1111
import {
1212
ADD_MODULE,
1313
CHANGE_LESSON,
14+
ADD_LESSON,
15+
REMOVE_LESSON,
1416
HIDE_LESSON_IN_TIMETABLE,
1517
REMOVE_MODULE,
1618
SELECT_MODULE_COLOR,
1719
SET_LESSON_CONFIG,
1820
SET_TIMETABLE,
1921
SHOW_LESSON_IN_TIMETABLE,
22+
ADD_CUSTOM_MODULE,
23+
REMOVE_CUSTOM_MODULE,
2024
} from 'actions/timetables';
2125
import { getNewColor } from 'utils/colors';
2226
import { SET_EXPORTED_DATA } from 'actions/constants';
@@ -42,7 +46,6 @@ export function migrateV1toV2(
4246
const lessonArray = [lessonValue];
4347
newSemester[moduleCode][lessonType] = lessonArray;
4448
});
45-
4649
if (!newLessons[semester]) {
4750
newLessons[semester] = {};
4851
}
@@ -119,12 +122,30 @@ function moduleLessonConfig(
119122
if (!(classNo && lessonType)) return state;
120123
return {
121124
...state,
122-
[lessonType]: [classNo],
125+
[lessonType]: [
126+
...state[lessonType].filter((lesson) => lesson !== action.payload.activeLesson),
127+
action.payload.classNo,
128+
],
123129
};
124130
}
125131
case SET_LESSON_CONFIG:
126132
return action.payload.lessonConfig;
127-
133+
case ADD_LESSON: {
134+
const { classNo, lessonType } = action.payload;
135+
if (!(classNo && lessonType)) return state;
136+
return {
137+
...state,
138+
[lessonType]: [...state[lessonType], classNo],
139+
};
140+
}
141+
case REMOVE_LESSON: {
142+
const { classNo, lessonType } = action.payload;
143+
if (!(classNo && lessonType)) return state;
144+
return {
145+
...state,
146+
[lessonType]: state[lessonType].filter((lesson) => lesson !== classNo),
147+
};
148+
}
128149
default:
129150
return state;
130151
}
@@ -148,6 +169,8 @@ function semTimetable(
148169
case REMOVE_MODULE:
149170
return omit(state, [moduleCode]);
150171
case CHANGE_LESSON:
172+
case ADD_LESSON:
173+
case REMOVE_LESSON:
151174
case SET_LESSON_CONFIG:
152175
return {
153176
...state,
@@ -203,12 +226,31 @@ function semHiddenModules(state = defaultHiddenState, action: Actions) {
203226
}
204227
}
205228

229+
// Map of CustomisedModules
230+
const defaultCustomisedModulesState: ModuleCode[] = [];
231+
function customisedModules(state = defaultCustomisedModulesState, action: Actions) {
232+
if (!action.payload) {
233+
return state;
234+
}
235+
236+
switch (action.type) {
237+
case ADD_CUSTOM_MODULE:
238+
if (state.includes(action.payload.moduleCode)) return state;
239+
return [...state, action.payload.moduleCode];
240+
case REMOVE_CUSTOM_MODULE:
241+
return state.filter((c) => c !== action.payload.moduleCode);
242+
default:
243+
return state;
244+
}
245+
}
246+
206247
export const defaultTimetableState: TimetablesState = {
207248
lessons: {},
208249
colors: {},
209250
hidden: {},
210251
academicYear: config.academicYear,
211252
archive: {},
253+
customisedModules: {},
212254
};
213255

214256
function timetables(
@@ -234,15 +276,23 @@ function timetables(
234276
case REMOVE_MODULE:
235277
case SELECT_MODULE_COLOR:
236278
case CHANGE_LESSON:
279+
case ADD_LESSON:
280+
case REMOVE_LESSON:
237281
case SET_LESSON_CONFIG:
238282
case HIDE_LESSON_IN_TIMETABLE:
239-
case SHOW_LESSON_IN_TIMETABLE: {
283+
case SHOW_LESSON_IN_TIMETABLE:
284+
case ADD_CUSTOM_MODULE:
285+
case REMOVE_CUSTOM_MODULE: {
240286
const { semester } = action.payload;
241287

242288
return produce(state, (draft) => {
243289
draft.lessons[semester] = semTimetable(draft.lessons[semester], action);
244290
draft.colors[semester] = semColors(state.colors[semester], action);
245291
draft.hidden[semester] = semHiddenModules(state.hidden[semester], action);
292+
draft.customisedModules[semester] = customisedModules(
293+
state.customisedModules[semester],
294+
action,
295+
);
246296
});
247297
}
248298

website/src/selectors/timetables.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,8 @@ export const getSemesterTimetableColors = createSelector(
3838
(colors) => (semester: Semester | null) =>
3939
semester === null ? EMPTY_OBJECT : colors[semester] ?? EMPTY_OBJECT,
4040
);
41+
42+
export const getCustomisingLesson = createSelector(
43+
({ app }: State) => app.customiseModule,
44+
(customiseModule) => customiseModule ?? null,
45+
);

website/src/types/reducers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type NotificationData = { readonly message: string } & NotificationOption
5151
export type AppState = {
5252
readonly activeSemester: Semester;
5353
readonly activeLesson: Lesson | null;
54+
readonly customiseModule: ModuleCode;
5455
readonly isOnline: boolean;
5556
readonly isFeedbackModalOpen: boolean;
5657
readonly notifications: NotificationData[];
@@ -112,6 +113,7 @@ export type SettingsState = {
112113
export type ColorMapping = { [moduleCode: string]: ColorIndex };
113114
export type SemesterColorMap = { [semester: string]: ColorMapping };
114115
export type HiddenModulesMap = { [semester: string]: ModuleCode[] };
116+
export type CustomisedModulesMap = { [semester: string]: ModuleCode[] };
115117

116118
export type TimetablesState = {
117119
readonly lessons: TimetableConfig;
@@ -120,6 +122,7 @@ export type TimetablesState = {
120122
readonly academicYear: string;
121123
// Mapping of academic year to old timetable config
122124
readonly archive: { [key: string]: TimetableConfig };
125+
readonly customisedModules: CustomisedModulesMap;
123126
};
124127

125128
/* venueBank.js */

0 commit comments

Comments
 (0)