Skip to content

Commit

Permalink
Add support for Timetable for TAs (#3434)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zhoukerrr authored and zwliew committed Aug 4, 2023
1 parent 48070a1 commit 4ce23c1
Show file tree
Hide file tree
Showing 17 changed files with 352 additions and 47 deletions.
1 change: 1 addition & 0 deletions website/src/actions/__snapshots__/timetables.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exports[`cancelModifyLesson should not have payload 1`] = `
exports[`changeLesson should return updated information to change lesson 1`] = `
{
"payload": {
"activeLesson": "1",
"classNo": "1",
"lessonType": "Recitation",
"moduleCode": "CS1010S",
Expand Down
2 changes: 1 addition & 1 deletion website/src/actions/timetables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test('modifyLesson should return lesson payload', () => {
test('changeLesson should return updated information to change lesson', () => {
const semester: Semester = 1;
const lesson: Lesson = lessons[1];
expect(actions.changeLesson(semester, lesson)).toMatchSnapshot();
expect(actions.changeLesson(semester, lesson, lesson.classNo)).toMatchSnapshot();
});

test('cancelModifyLesson should not have payload', () => {
Expand Down
83 changes: 81 additions & 2 deletions website/src/actions/timetables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,24 @@ export function modifyLesson(activeLesson: Lesson) {
};
}

export const CUSTOMISE_MODULE = 'CUSTOMISE_LESSON' as const;
export function customiseLesson(semester: Semester, moduleCode: ModuleCode) {
return {
type: CUSTOMISE_MODULE,
payload: {
semester,
moduleCode,
},
};
}

export const CHANGE_LESSON = 'CHANGE_LESSON' as const;
export function setLesson(
semester: Semester,
moduleCode: ModuleCode,
lessonType: LessonType,
classNo: ClassNo,
activeLesson: ClassNo,
) {
return {
type: CHANGE_LESSON,
Expand All @@ -104,12 +116,71 @@ export function setLesson(
moduleCode,
lessonType,
classNo,
activeLesson,
},
};
}

export function changeLesson(semester: Semester, lesson: Lesson) {
return setLesson(semester, lesson.moduleCode, lesson.lessonType, lesson.classNo);
export const ADD_CUSTOM_MODULE = 'ADD_CUSTOM_MODULE' as const;
export function addCustomModule(semester: Semester, moduleCode: ModuleCode) {
return {
type: ADD_CUSTOM_MODULE,
payload: {
semester,
moduleCode,
},
};
}

export const REMOVE_CUSTOM_MODULE = 'REMOVE_CUSTOM_MODULE' as const;
export function removeCustomModule(semester: Semester, moduleCode: ModuleCode) {
return {
type: REMOVE_CUSTOM_MODULE,
payload: {
semester,
moduleCode,
},
};
}

export const ADD_LESSON = 'ADD_LESSON' as const;
export function addLesson(
semester: Semester,
moduleCode: ModuleCode,
lessonType: LessonType,
classNo: ClassNo,
) {
return {
type: ADD_LESSON,
payload: {
semester,
moduleCode,
lessonType,
classNo,
},
};
}

export const REMOVE_LESSON = 'REMOVE_LESSON' as const;
export function removeLesson(
semester: Semester,
moduleCode: ModuleCode,
lessonType: LessonType,
classNo: ClassNo,
) {
return {
type: REMOVE_LESSON,
payload: {
semester,
moduleCode,
lessonType,
classNo,
},
};
}

export function changeLesson(semester: Semester, lesson: Lesson, activeLesson: ClassNo) {
return setLesson(semester, lesson.moduleCode, lesson.lessonType, lesson.classNo, activeLesson);
}

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

// If the module is customised, we do not validate it
if (
timetables.customisedModules &&
timetables.customisedModules[semester] &&
timetables.customisedModules[semester].includes(moduleCode)
) {
return;
}
const [validatedLessonConfig, changedLessonTypes] = validateModuleLessons(
semester,
lessonConfig,
Expand Down
3 changes: 2 additions & 1 deletion website/src/reducers/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const appInitialState: AppState = {
isFeedbackModalOpen: false,
promptRefresh: false,
notifications: [],
customiseModule: '',
};
const appHasSemesterTwoState: AppState = { ...appInitialState, activeSemester: anotherSemester };
const appHasActiveLessonState: AppState = { ...appInitialState, activeLesson: lesson };
Expand Down Expand Up @@ -55,7 +56,7 @@ test('app should set active lesson', () => {
});

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

expect(nextState).toEqual(appInitialState);
Expand Down
13 changes: 12 additions & 1 deletion website/src/reducers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { Actions } from 'types/actions';
import config from 'config';

import { forceRefreshPrompt } from 'utils/debug';
import { MODIFY_LESSON, CHANGE_LESSON, CANCEL_MODIFY_LESSON } from 'actions/timetables';
import {
MODIFY_LESSON,
CHANGE_LESSON,
CANCEL_MODIFY_LESSON,
CUSTOMISE_MODULE,
} from 'actions/timetables';
import { SELECT_SEMESTER } from 'actions/settings';
import {
OPEN_NOTIFICATION,
Expand All @@ -18,6 +23,7 @@ const defaultAppState = (): AppState => ({
activeSemester: config.semester,
// The lesson being modified on the timetable.
activeLesson: null,
customiseModule: '',
isOnline: navigator.onLine,
isFeedbackModalOpen: false,
promptRefresh: forceRefreshPrompt(),
Expand All @@ -37,6 +43,11 @@ function app(state: AppState = defaultAppState(), action: Actions): AppState {
...state,
activeLesson: action.payload.activeLesson,
};
case CUSTOMISE_MODULE:
return {
...state,
customiseModule: action.payload.moduleCode,
};
case CANCEL_MODIFY_LESSON:
case CHANGE_LESSON:
return {
Expand Down
1 change: 1 addition & 0 deletions website/src/reducers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ test('reducers should set export data state', () => {
PC1222: 2,
},
},
customisedModules: {},
hidden: { [1]: ['PC1222'] },
academicYear: expect.any(String),
archive: {},
Expand Down
3 changes: 3 additions & 0 deletions website/src/reducers/timetables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ describe('stateReconciler', () => {
},
academicYear: config.academicYear,
archive: oldArchive,
customisedModules: {},
};

const { stateReconciler } = persistConfig;
Expand Down Expand Up @@ -259,6 +260,7 @@ describe('redux schema migration', () => {
hidden: {},
academicYear: '2022/2023',
archive: {},
customisedModules: {},
_persist: {
version: 1,
rehydrated: false,
Expand All @@ -283,6 +285,7 @@ describe('redux schema migration', () => {
hidden: {},
academicYear: '2022/2023',
archive: {},
customisedModules: {},
_persist: {
version: 1, // version kept the same because the framework does not support it in unit tests
rehydrated: false,
Expand Down
58 changes: 54 additions & 4 deletions website/src/reducers/timetables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import config from 'config';
import {
ADD_MODULE,
CHANGE_LESSON,
ADD_LESSON,
REMOVE_LESSON,
HIDE_LESSON_IN_TIMETABLE,
REMOVE_MODULE,
SELECT_MODULE_COLOR,
SET_LESSON_CONFIG,
SET_TIMETABLE,
SHOW_LESSON_IN_TIMETABLE,
ADD_CUSTOM_MODULE,
REMOVE_CUSTOM_MODULE,
} from 'actions/timetables';
import { getNewColor } from 'utils/colors';
import { SET_EXPORTED_DATA } from 'actions/constants';
Expand All @@ -42,7 +46,6 @@ export function migrateV1toV2(
const lessonArray = [lessonValue];
newSemester[moduleCode][lessonType] = lessonArray;
});

if (!newLessons[semester]) {
newLessons[semester] = {};
}
Expand Down Expand Up @@ -119,12 +122,30 @@ function moduleLessonConfig(
if (!(classNo && lessonType)) return state;
return {
...state,
[lessonType]: [classNo],
[lessonType]: [
...state[lessonType].filter((lesson) => lesson !== action.payload.activeLesson),
action.payload.classNo,
],
};
}
case SET_LESSON_CONFIG:
return action.payload.lessonConfig;

case ADD_LESSON: {
const { classNo, lessonType } = action.payload;
if (!(classNo && lessonType)) return state;
return {
...state,
[lessonType]: [...state[lessonType], classNo],
};
}
case REMOVE_LESSON: {
const { classNo, lessonType } = action.payload;
if (!(classNo && lessonType)) return state;
return {
...state,
[lessonType]: state[lessonType].filter((lesson) => lesson !== classNo),
};
}
default:
return state;
}
Expand All @@ -148,6 +169,8 @@ function semTimetable(
case REMOVE_MODULE:
return omit(state, [moduleCode]);
case CHANGE_LESSON:
case ADD_LESSON:
case REMOVE_LESSON:
case SET_LESSON_CONFIG:
return {
...state,
Expand Down Expand Up @@ -203,12 +226,31 @@ function semHiddenModules(state = defaultHiddenState, action: Actions) {
}
}

// Map of CustomisedModules
const defaultCustomisedModulesState: ModuleCode[] = [];
function customisedModules(state = defaultCustomisedModulesState, action: Actions) {
if (!action.payload) {
return state;
}

switch (action.type) {
case ADD_CUSTOM_MODULE:
if (state.includes(action.payload.moduleCode)) return state;
return [...state, action.payload.moduleCode];
case REMOVE_CUSTOM_MODULE:
return state.filter((c) => c !== action.payload.moduleCode);
default:
return state;
}
}

export const defaultTimetableState: TimetablesState = {
lessons: {},
colors: {},
hidden: {},
academicYear: config.academicYear,
archive: {},
customisedModules: {},
};

function timetables(
Expand All @@ -234,15 +276,23 @@ function timetables(
case REMOVE_MODULE:
case SELECT_MODULE_COLOR:
case CHANGE_LESSON:
case ADD_LESSON:
case REMOVE_LESSON:
case SET_LESSON_CONFIG:
case HIDE_LESSON_IN_TIMETABLE:
case SHOW_LESSON_IN_TIMETABLE: {
case SHOW_LESSON_IN_TIMETABLE:
case ADD_CUSTOM_MODULE:
case REMOVE_CUSTOM_MODULE: {
const { semester } = action.payload;

return produce(state, (draft) => {
draft.lessons[semester] = semTimetable(draft.lessons[semester], action);
draft.colors[semester] = semColors(state.colors[semester], action);
draft.hidden[semester] = semHiddenModules(state.hidden[semester], action);
draft.customisedModules[semester] = customisedModules(
state.customisedModules[semester],
action,
);
});
}

Expand Down
5 changes: 5 additions & 0 deletions website/src/selectors/timetables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ export const getSemesterTimetableColors = createSelector(
(colors) => (semester: Semester | null) =>
semester === null ? EMPTY_OBJECT : colors[semester] ?? EMPTY_OBJECT,
);

export const getCustomisingLesson = createSelector(
({ app }: State) => app.customiseModule,
(customiseModule) => customiseModule ?? null,
);
3 changes: 3 additions & 0 deletions website/src/types/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type NotificationData = { readonly message: string } & NotificationOption
export type AppState = {
readonly activeSemester: Semester;
readonly activeLesson: Lesson | null;
readonly customiseModule: ModuleCode;
readonly isOnline: boolean;
readonly isFeedbackModalOpen: boolean;
readonly notifications: NotificationData[];
Expand Down Expand Up @@ -112,6 +113,7 @@ export type SettingsState = {
export type ColorMapping = { [moduleCode: string]: ColorIndex };
export type SemesterColorMap = { [semester: string]: ColorMapping };
export type HiddenModulesMap = { [semester: string]: ModuleCode[] };
export type CustomisedModulesMap = { [semester: string]: ModuleCode[] };

export type TimetablesState = {
readonly lessons: TimetableConfig;
Expand All @@ -120,6 +122,7 @@ export type TimetablesState = {
readonly academicYear: string;
// Mapping of academic year to old timetable config
readonly archive: { [key: string]: TimetableConfig };
readonly customisedModules: CustomisedModulesMap;
};

/* venueBank.js */
Expand Down
3 changes: 3 additions & 0 deletions website/src/views/timetable/Timetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import elements from 'views/elements';
import withTimer, { TimerData } from 'views/hocs/withTimer';

import { TimePeriod } from 'types/venues';
import { ModuleCode } from 'types/modules';
import styles from './Timetable.scss';
import TimetableTimings from './TimetableTimings';
import TimetableDay from './TimetableDay';
Expand All @@ -29,6 +30,7 @@ type Props = TimerData & {
showTitle?: boolean;
onModifyCell?: OnModifyCell;
highlightPeriod?: TimePeriod;
customisedModules?: ModuleCode[];
};

type State = {
Expand Down Expand Up @@ -108,6 +110,7 @@ class Timetable extends React.PureComponent<Props, State> {
highlightPeriod={
highlightPeriod && index === highlightPeriod.day ? highlightPeriod : undefined
}
customisedModules={this.props.customisedModules}
/>
))}
</ol>
Expand Down
Loading

0 comments on commit 4ce23c1

Please sign in to comment.