Skip to content

Commit

Permalink
Implement planner placeholder (#1635)
Browse files Browse the repository at this point in the history
* Add placeholder reducer state and convert ModuleTime to object

* Add placeholders for GEM and CS

* Switch to new planner data shape

The new shape makes it easier to typecheck and work with in the reducer

* Add category select dropdown to AddModule

* Update action and drilldown props for add placeholder

* Add UI scaffold for editing placeholders

* Fix types

* Add PlannerModuleSelect component

* Add autocomplete to planner

* Add set placeholder action and UI

* Fix nextId returning -Infinity when modules is empty

* Increase PlannerSemester min height for new add module UI

* Fix lint

* Fix lint-ish

* Fix more merge issues

* Tweak styles

Co-authored-by: E-Liang Tan <[email protected]>
  • Loading branch information
ZhangYiJiang and taneliang authored Jul 13, 2020
1 parent 312b11a commit 9d56881
Show file tree
Hide file tree
Showing 31 changed files with 1,090 additions and 373 deletions.
35 changes: 18 additions & 17 deletions website/src/actions/planner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ModuleCode, Semester } from 'types/modules';
import { CustomModule } from 'types/planner';
import { AddModuleData } from 'types/planner';
import { CustomModule } from 'types/reducers';

export const SET_PLANNER_MIN_YEAR = 'SET_PLANNER_MIN_YEAR' as const;
export function setPlannerMinYear(year: string) {
Expand All @@ -26,46 +27,46 @@ export function setPlannerIBLOCs(iblocs: boolean) {
}

export const ADD_PLANNER_MODULE = 'ADD_PLANNER_MODULE' as const;
export function addPlannerModule(
moduleCode: ModuleCode,
year: string,
semester: Semester,
index: number | null = null,
) {
export function addPlannerModule(year: string, semester: Semester, module: AddModuleData) {
return {
type: ADD_PLANNER_MODULE,
payload: {
year,
semester,
moduleCode,
index,
...module,
},
};
}

export const MOVE_PLANNER_MODULE = 'MOVE_PLANNER_MODULE' as const;
export function movePlannerModule(
moduleCode: ModuleCode,
year: string,
semester: Semester,
index: number | null = null,
) {
export function movePlannerModule(id: string, year: string, semester: Semester, index: number) {
return {
type: MOVE_PLANNER_MODULE,
payload: {
id,
year,
semester,
moduleCode,
index,
},
};
}

export const REMOVE_PLANNER_MODULE = 'REMOVE_PLANNER_MODULE' as const;
export function removePlannerModule(moduleCode: ModuleCode) {
export function removePlannerModule(id: string) {
return {
type: REMOVE_PLANNER_MODULE,
payload: {
id,
},
};
}

export const SET_PLACEHOLDER_MODULE = 'SET_PLACEHOLDER_MODULE' as const;
export function setPlaceholderModule(id: string, moduleCode: ModuleCode) {
return {
type: SET_PLACEHOLDER_MODULE,
payload: {
id,
moduleCode,
},
};
Expand Down
4 changes: 2 additions & 2 deletions website/src/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import venueBankReducer, { persistConfig as venueBankPersistConfig } from './ven
import timetablesReducer, { persistConfig as timetablesPersistConfig } from './timetables';
import themeReducer from './theme';
import settingsReducer, { persistConfig as settingsPersistConfig } from './settings';
import plannerReducer from './planner';
import plannerReducer, { persistConfig as plannerPersistConfig } from './planner';

// Persist reducers
const moduleBank = persistReducer('moduleBank', moduleBankReducer, moduleBankPersistConfig);
const venueBank = persistReducer('venueBank', venueBankReducer, venueBankPersistConfig);
const timetables = persistReducer('timetables', timetablesReducer, timetablesPersistConfig);
const theme = persistReducer('theme', themeReducer);
const settings = persistReducer('settings', settingsReducer, settingsPersistConfig);
const planner = persistReducer('planner', plannerReducer);
const planner = persistReducer('planner', plannerReducer, plannerPersistConfig);

// State default is delegated to its child reducers.
const defaultState = ({} as unknown) as State;
Expand Down
137 changes: 86 additions & 51 deletions website/src/reducers/planner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
setPlannerIBLOCs,
} from 'actions/planner';
import { PlannerState } from 'types/reducers';
import reducer from './planner';
import reducer, { migrateV0toV1, nextId } from './planner';

const defaultState: PlannerState = {
minYear: '2017/2018',
Expand All @@ -23,6 +23,15 @@ const defaultState: PlannerState = {
custom: {},
};

describe(nextId, () => {
const module: any = {};

test('should produce next ID', () => {
expect(nextId({})).toEqual('0');
expect(nextId({ 0: module })).toEqual('1');
});
});

describe(SET_PLANNER_MIN_YEAR, () => {
test('should set min year', () => {
expect(reducer(defaultState, setPlannerMinYear('2016/2017'))).toEqual({
Expand Down Expand Up @@ -71,37 +80,41 @@ describe(SET_PLANNER_IBLOCS, () => {
describe(ADD_PLANNER_MODULE, () => {
const initial: PlannerState = {
...defaultState,
modules: { CS1010S: ['2018/2019', 1, 0] },
modules: {
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
},
};

test('should add module to semester and year', () => {
expect(reducer(initial, addPlannerModule('CS2030', '2018/2019', 2)).modules).toEqual({
CS1010S: ['2018/2019', 1, 0],
CS2030: ['2018/2019', 2, 0],
});

// Add module code in uppercase
expect(reducer(initial, addPlannerModule('cs2030', '2018/2019', 2)).modules).toEqual({
CS1010S: ['2018/2019', 1, 0],
CS2030: ['2018/2019', 2, 0],
expect(
reducer(initial, addPlannerModule('2018/2019', 2, { type: 'module', moduleCode: 'CS2030' }))
.modules,
).toEqual({
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 0 },
});

// Inserts new module in correct position
expect(reducer(initial, addPlannerModule('CS2030', '2018/2019', 1)).modules).toEqual({
CS1010S: ['2018/2019', 1, 0],
CS2030: ['2018/2019', 1, 1],
expect(
reducer(initial, addPlannerModule('2018/2019', 1, { type: 'module', moduleCode: 'CS2030' }))
.modules,
).toEqual({
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 1, index: 1 },
});
});

test('should not add duplicate modules', () => {
expect(
Object.keys(reducer(initial, addPlannerModule('CS1010S', '2018/2019', 1)).modules),
).toHaveLength(1);
test('can insert multiple modules in order', () => {
const insertCS1010S = addPlannerModule('2018/2019', 1, {
type: 'module',
moduleCode: 'CS1010S',
});
const insertCS1231 = addPlannerModule('2018/2019', 1, { type: 'module', moduleCode: 'CS1231' });

// Also deduplicate lowercase module codes
expect(
Object.keys(reducer(initial, addPlannerModule('cs1010s', '2018/2019', 1)).modules),
).toHaveLength(1);
expect(reducer(reducer(defaultState, insertCS1010S), insertCS1231).modules).toEqual({
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
1: { id: '1', moduleCode: 'CS1231', year: '2018/2019', semester: 1, index: 1 },
});
});
});

Expand All @@ -110,56 +123,56 @@ describe(MOVE_PLANNER_MODULE, () => {
...defaultState,

modules: {
CS1010S: ['2018/2019', 1, 0],
CS2030: ['2018/2019', 2, 0],
CS2105: ['2018/2019', 2, 1],
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 0 },
2: { id: '2', moduleCode: 'CS2105', year: '2018/2019', semester: 2, index: 1 },
},
};

test('should move modules between same semester', () => {
expect(reducer(initial, movePlannerModule('CS2105', '2018/2019', 2, 0)).modules).toEqual({
CS1010S: ['2018/2019', 1, 0],
CS2030: ['2018/2019', 2, 1],
CS2105: ['2018/2019', 2, 0],
expect(reducer(initial, movePlannerModule('2', '2018/2019', 2, 0)).modules).toEqual({
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 1 },
2: { id: '2', moduleCode: 'CS2105', year: '2018/2019', semester: 2, index: 0 },
});

expect(
reducer(
{
...initial,
...defaultState,
modules: {
CS1010S: ['2018/2019', 2, 0],
CS2030: ['2018/2019', 2, 1],
CS2105: ['2018/2019', 2, 2],
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 2, index: 0 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 1 },
2: { id: '2', moduleCode: 'CS2105', year: '2018/2019', semester: 2, index: 2 },
},
},
movePlannerModule('CS2105', '2018/2019', 2, 1),
movePlannerModule('2', '2018/2019', 2, 1),
).modules,
).toEqual({
CS1010S: ['2018/2019', 2, 0],
CS2105: ['2018/2019', 2, 1],
CS2030: ['2018/2019', 2, 2],
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 2, index: 0 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 2 },
2: { id: '2', moduleCode: 'CS2105', year: '2018/2019', semester: 2, index: 1 },
});
});

test('should move module to other acad year or semester', () => {
// Move CS2105 from sem 2 to 1
expect(reducer(initial, movePlannerModule('CS2105', '2018/2019', 1, 0)).modules).toEqual({
CS1010S: ['2018/2019', 1, 1],
CS2030: ['2018/2019', 2, 0],
CS2105: ['2018/2019', 1, 0],
expect(reducer(initial, movePlannerModule('2', '2018/2019', 1, 0)).modules).toEqual({
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 1 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 0 },
2: { id: '2', moduleCode: 'CS2105', year: '2018/2019', semester: 1, index: 0 },
});

expect(reducer(initial, movePlannerModule('CS1010S', '2017/2018', 2, 0)).modules).toEqual({
CS1010S: ['2017/2018', 2, 0],
CS2030: ['2018/2019', 2, 0],
CS2105: ['2018/2019', 2, 1],
expect(reducer(initial, movePlannerModule('0', '2017/2018', 2, 0)).modules).toEqual({
0: { id: '0', moduleCode: 'CS1010S', year: '2017/2018', semester: 2, index: 0 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 0 },
2: { id: '2', moduleCode: 'CS2105', year: '2018/2019', semester: 2, index: 1 },
});

expect(reducer(initial, movePlannerModule('CS1010S', '2018/2019', 2, 1)).modules).toEqual({
CS1010S: ['2018/2019', 2, 1],
CS2030: ['2018/2019', 2, 0],
CS2105: ['2018/2019', 2, 2],
expect(reducer(initial, movePlannerModule('0', '2018/2019', 2, 1)).modules).toEqual({
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 2, index: 1 },
1: { id: '1', moduleCode: 'CS2030', year: '2018/2019', semester: 2, index: 0 },
2: { id: '2', moduleCode: 'CS2105', year: '2018/2019', semester: 2, index: 2 },
});
});
});
Expand All @@ -168,10 +181,32 @@ describe(REMOVE_PLANNER_MODULE, () => {
const initial: PlannerState = {
...defaultState,

modules: { CS1010S: ['2018/2019', 1, 0] },
modules: {
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
},
};

test('should remove the specified module', () => {
expect(reducer(initial, removePlannerModule('CS1010S')).modules).toEqual({});
expect(reducer(initial, removePlannerModule('0')).modules).toEqual({});
});
});

describe(migrateV0toV1, () => {
test('should migrate old modules state to new modules state', () => {
expect(
migrateV0toV1({
_persist: {} as any,
...defaultState,
modules: {
CS1010S: ['2018/2019', 1, 0],
MA1101R: ['2018/2019', 1, 1],
CS1231: ['2018/2019', 2, 0],
},
}),
).toHaveProperty('modules', {
0: { id: '0', moduleCode: 'CS1010S', year: '2018/2019', semester: 1, index: 0 },
1: { id: '1', moduleCode: 'MA1101R', year: '2018/2019', semester: 1, index: 1 },
2: { id: '2', moduleCode: 'CS1231', year: '2018/2019', semester: 2, index: 0 },
});
});
});
Loading

0 comments on commit 9d56881

Please sign in to comment.