diff --git a/website/src/selectors/planner.test.ts b/website/src/selectors/planner.test.ts index 12ebcbbd12..de49e3671a 100644 --- a/website/src/selectors/planner.test.ts +++ b/website/src/selectors/planner.test.ts @@ -153,7 +153,7 @@ describe(getAcadYearModules, () => { expect(getAcadYearModules(state)).toHaveProperty('2018/2019.2.0', { id: '0', moduleCode: 'CS3216', - conflict: { type: 'semester', semestersOffered: [1] }, + conflicts: [{ type: 'semester', semestersOffered: [1] }], }); }); @@ -177,10 +177,45 @@ describe(getAcadYearModules, () => { id: '0', moduleCode: 'CS3216', moduleInfo: CS3216, - conflict: { - type: 'prereq', - unfulfilledPrereqs: ['CS2103'], + conflicts: [ + { + type: 'prereq', + unfulfilledPrereqs: ['CS2103'], + }, + ], + }); + }); + + test('should return both module prereq conflicts and semester conflict', () => { + const planner: PlannerState = { + ...defaultState, + modules: { + // CS3216 requires CS2103 and is only offered in sem1 + 0: { id: '0', moduleCode: 'CS3216', year: '2018/2019', semester: 2, index: 0 }, }, + }; + + const moduleBank = { + modules: { CS3216 }, + moduleCodes: { CS3216: { semesters: [1] } }, + }; + + const state: any = { planner, moduleBank }; + + expect(getAcadYearModules(state)).toHaveProperty('2018/2019.2.0', { + id: '0', + moduleCode: 'CS3216', + moduleInfo: CS3216, + conflicts: [ + { + type: 'semester', + semestersOffered: [1], + }, + { + type: 'prereq', + unfulfilledPrereqs: ['CS2103'], + }, + ], }); }); @@ -211,24 +246,28 @@ describe(getAcadYearModules, () => { id: '0', moduleCode: 'CS1010X', moduleInfo: CS1010X, - conflict: { - type: 'exam', - conflictModules: ['CS1010X', 'CS1010S'], - }, + conflicts: [ + { + type: 'exam', + conflictModules: ['CS1010X', 'CS1010S'], + }, + ], }, { id: '1', moduleCode: 'CS1010S', moduleInfo: CS1010S, - conflict: { - type: 'exam', - conflictModules: ['CS1010X', 'CS1010S'], - }, + conflicts: [ + { + type: 'exam', + conflictModules: ['CS1010X', 'CS1010S'], + }, + ], }, ]); }); - test('should return duplicate conflicts', () => { + test('should return duplicate and prereq conflicts', () => { const planner: PlannerState = { ...defaultState, modules: { @@ -256,13 +295,29 @@ describe(getAcadYearModules, () => { id: '0', moduleCode: 'CS3216', moduleInfo: CS3216, - conflict: { type: 'duplicate' }, + conflicts: [ + { + type: 'duplicate', + }, + { + type: 'prereq', + unfulfilledPrereqs: ['CS2103'], + }, + ], }, { id: '1', moduleCode: 'CS3216', moduleInfo: CS3216_DUPLICATE, - conflict: { type: 'duplicate' }, + conflicts: [ + { + type: 'duplicate', + }, + { + type: 'prereq', + unfulfilledPrereqs: ['CS2103'], + }, + ], }, ]); @@ -271,13 +326,13 @@ describe(getAcadYearModules, () => { id: '2', moduleCode: 'CS1010S', moduleInfo: CS1010S, - conflict: { type: 'duplicate' }, + conflicts: [{ type: 'duplicate' }], }, { id: '3', moduleCode: 'CS1010S', moduleInfo: CS1010S_DUPLICATE, - conflict: { type: 'duplicate' }, + conflicts: [{ type: 'duplicate' }], }, ]); }); @@ -306,7 +361,7 @@ describe(getAcadYearModules, () => { id: '0', moduleCode: 'CS1010S', moduleInfo: CS1010S, - conflict: null, + conflicts: null, }, ]); @@ -315,7 +370,7 @@ describe(getAcadYearModules, () => { id: '1', moduleCode: 'CS1010S', moduleInfo: CS1010S_DUPLICATE, - conflict: null, + conflicts: null, }, ]); }); @@ -347,13 +402,13 @@ describe(getAcadYearModules, () => { id: '0', moduleCode: 'CS1010X', moduleInfo: CS1010X, - conflict: null, + conflicts: null, }, { id: '1', moduleCode: 'CS1010S', moduleInfo: CS1010S, - conflict: null, + conflicts: null, }, ]); }); @@ -390,7 +445,7 @@ describe(getAcadYearModules, () => { id: '1', moduleCode: 'CS3216', moduleInfo: CS3216, - conflict: null, + conflicts: null, }); }); @@ -434,7 +489,7 @@ describe(getAcadYearModules, () => { title: 'Algorithms and Data Structure Accelerated', moduleCredit: 6, }, - conflict: null, + conflicts: null, }); expect(cs3216).toMatchObject({ @@ -452,9 +507,11 @@ describe(getAcadYearModules, () => { }, }); - expect(getAcadYearModules(state)).toHaveProperty('2018/2019.1.0.conflict', { - type: 'noInfo', - }); + expect(getAcadYearModules(state)).toHaveProperty('2018/2019.1.0.conflicts', [ + { + type: 'noInfo', + }, + ]); }); test('should not show no data conflicts for modules with custom info', () => { @@ -471,6 +528,6 @@ describe(getAcadYearModules, () => { }, }); - expect(getAcadYearModules(state)).toHaveProperty('2018/2019.1.0.conflict', null); + expect(getAcadYearModules(state)).toHaveProperty('2018/2019.1.0.conflicts', null); }); }); diff --git a/website/src/selectors/planner.ts b/website/src/selectors/planner.ts index 0bb1c05def..00c5f05a67 100644 --- a/website/src/selectors/planner.ts +++ b/website/src/selectors/planner.ts @@ -125,7 +125,7 @@ function mapModuleToInfo( const moduleInfo: PlannerModuleInfo = { id, moduleCode, - conflict: null, + conflicts: null, }; if (placeholderId) { @@ -136,12 +136,9 @@ function mapModuleToInfo( } if (moduleCode) { - // Only continue checking until the first conflict is found - let index = 0; - while (!moduleInfo.conflict && index < conflictChecks.length) { - moduleInfo.conflict = conflictChecks[index](moduleCode); - index += 1; - } + // Check every conflict + const conflicts = conflictChecks.flatMap((check) => check(moduleCode) ?? []); + moduleInfo.conflicts = conflicts.length > 0 ? conflicts : null; // Insert customInfo and moduleInfo moduleInfo.moduleInfo = modulesMap[moduleCode]; diff --git a/website/src/types/planner.ts b/website/src/types/planner.ts index d34bb73bf9..b7fb322bfc 100644 --- a/website/src/types/planner.ts +++ b/website/src/types/planner.ts @@ -55,7 +55,7 @@ export type PlannerModuleInfo = { // Custom info added by the student to override our data or to fill in the blanks // This is a separate field for easier typing customInfo?: CustomModule | null; - conflict?: Conflict | null; + conflicts?: Conflict[] | null; moduleCode?: ModuleCode; placeholder?: PlannerPlaceholder; }; diff --git a/website/src/views/planner/PlannerSemester.test.tsx b/website/src/views/planner/PlannerSemester.test.tsx index 490254eedf..69fcab7ec6 100644 --- a/website/src/views/planner/PlannerSemester.test.tsx +++ b/website/src/views/planner/PlannerSemester.test.tsx @@ -45,16 +45,18 @@ function makePlannerSemester(year: string, semester: number, modules: PlannerMod } test('should show conflicts for current year', () => { - const conflict: Conflict = { - type: 'semester', - semestersOffered: [], - }; + const conflicts: Conflict[] = [ + { + type: 'semester', + semestersOffered: [], + }, + ]; const modules: PlannerModuleInfo[] = [ { id: '0', moduleCode: 'UTC1702G', - conflict, + conflicts, }, ]; @@ -63,16 +65,18 @@ test('should show conflicts for current year', () => { }); test('should not show conflicts for non-current years', () => { - const conflict: Conflict = { - type: 'semester', - semestersOffered: [], - }; + const conflicts: Conflict[] = [ + { + type: 'semester', + semestersOffered: [], + }, + ]; const modules: PlannerModuleInfo[] = [ { id: '0', moduleCode: 'UTC1702G', - conflict, + conflicts, }, ]; diff --git a/website/src/views/planner/PlannerSemester.tsx b/website/src/views/planner/PlannerSemester.tsx index 37388efb61..377f6d37b8 100644 --- a/website/src/views/planner/PlannerSemester.tsx +++ b/website/src/views/planner/PlannerSemester.tsx @@ -4,7 +4,7 @@ import AddCalendarIcon from 'img/icons/add-calendar.svg'; import classnames from 'classnames'; import { Semester, ModuleCode } from 'types/modules'; -import { AddModuleData, PlannerModuleInfo } from 'types/planner'; +import { AddModuleData, PlannerModuleInfo, Conflict } from 'types/planner'; import { Dispatch } from 'types/redux'; import config from 'config'; import { getExamDate, renderMCs } from 'utils/modules'; @@ -78,16 +78,22 @@ const PlannerSemester: React.FC = ({ const dispatch = useDispatch(); const renderModule = (plannerModule: PlannerModuleInfo, index: number) => { - const { id, moduleCode, moduleInfo, conflict, placeholder } = plannerModule; + const { id, moduleCode, moduleInfo, conflicts, placeholder } = plannerModule; const showExamDate = showModuleMeta && config.academicYear === year; const isModuleInTimetable = moduleCode !== undefined && moduleCode in timetable; - const displayedConflict = - year === config.academicYear || (conflict && ['prereq', 'duplicate'].includes(conflict.type)) - ? conflict - : null; + let displayedConflict: Conflict | null = null; + + if (conflicts?.length) { + displayedConflict = + year === config.academicYear + ? conflicts[0] // Topmost conflict if correc year + : conflicts.find((c) => c.type === 'prereq') || + conflicts.find((c) => c.type === 'duplicate') || + null; // either prereq or duplicate or none if not same year + } return (