Skip to content

Commit 3e1ce70

Browse files
leslieyip02ravern
andauthored
Add menu button to add module from planner to timetable (#3729)
* Add menu button to add module from planner to timetable * Add menu button to add module from planner to timetable * Add to timetable becomes view in timetable after adding module * Fix nit --------- Co-authored-by: Ravern Koh <[email protected]>
1 parent beef289 commit 3e1ce70

File tree

6 files changed

+82
-6
lines changed

6 files changed

+82
-6
lines changed

website/src/views/planner/ModuleMenu.test.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ import ModuleMenu from './ModuleMenu';
33

44
import styles from './PlannerModule.scss';
55

6-
function makeModuleMenu() {
6+
function makeModuleMenu(isInTimetable: boolean) {
77
const removeModule = jest.fn();
88
const editCustomData = jest.fn();
9+
const addModuleToTimetable = jest.fn();
10+
const viewSemesterTimetable = jest.fn();
911
return {
1012
removeModule,
1113
editCustomData,
12-
wrapper: mount(<ModuleMenu removeModule={removeModule} editCustomData={editCustomData} />),
14+
addModuleToTimetable,
15+
wrapper: mount(
16+
<ModuleMenu
17+
isInTimetable={isInTimetable}
18+
removeModule={removeModule}
19+
editCustomData={editCustomData}
20+
addModuleToTimetable={addModuleToTimetable}
21+
viewSemesterTimetable={viewSemesterTimetable}
22+
/>,
23+
),
1324
};
1425
}
1526

@@ -26,16 +37,32 @@ function isMenuRight(wrapper: ReactWrapper) {
2637
}
2738

2839
test('should show button only on render', () => {
29-
const { wrapper } = makeModuleMenu();
40+
const { wrapper } = makeModuleMenu(false);
3041
expect(findButton(wrapper).exists()).toBe(true);
3142
});
3243

3344
test('should show dropdown when button is clicked', () => {
34-
const { wrapper } = makeModuleMenu();
45+
const { wrapper } = makeModuleMenu(false);
3546
wrapper.find('button').at(0).simulate('click');
3647
expect(isExpanded(wrapper).exists()).toBe(true);
3748
});
3849

50+
test('should show add to timetable if module is not included', () => {
51+
const { wrapper } = makeModuleMenu(false);
52+
wrapper.find('button').at(0).simulate('click');
53+
expect(wrapper.text()).toContain('Edit Unit and Title');
54+
expect(wrapper.text()).toContain('Add to Timetable');
55+
expect(wrapper.text()).toContain('Remove');
56+
});
57+
58+
test('should show view in timetable if module is included', () => {
59+
const { wrapper } = makeModuleMenu(true);
60+
wrapper.find('button').at(0).simulate('click');
61+
expect(wrapper.text()).toContain('Edit Unit and Title');
62+
expect(wrapper.text()).toContain('View in Timetable');
63+
expect(wrapper.text()).toContain('Remove');
64+
});
65+
3966
test('should fix its overflow given a small window innerwidth', () => {
4067
Object.defineProperty(window, 'innerHeight', {
4168
writable: true,
@@ -44,7 +71,7 @@ test('should fix its overflow given a small window innerwidth', () => {
4471
});
4572
window.dispatchEvent(new Event('resize'));
4673

47-
const { wrapper } = makeModuleMenu();
74+
const { wrapper } = makeModuleMenu(false);
4875
wrapper.find('button').at(0).simulate('click');
4976
expect(isMenuRight(wrapper).exists()).toBe(true);
5077
});

website/src/views/planner/ModuleMenu.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import classnames from 'classnames';
66
import styles from './PlannerModule.scss';
77

88
type Props = {
9+
readonly isInTimetable?: boolean;
10+
911
readonly removeModule: () => void;
1012
readonly editCustomData: () => void;
13+
readonly addModuleToTimetable: () => void;
14+
readonly viewSemesterTimetable: () => void;
1115
};
1216

1317
type MenuItem = {
@@ -23,6 +27,9 @@ const ModuleMenu = memo((props: Props) => {
2327

2428
const menuItems: MenuItem[] = [
2529
{ label: 'Edit Unit and Title', action: props.editCustomData },
30+
props.isInTimetable
31+
? { label: 'View in Timetable', action: props.viewSemesterTimetable }
32+
: { label: 'Add to Timetable', action: props.addModuleToTimetable },
2633
{ label: 'Remove', action: props.removeModule, className: 'dropdown-item-danger' },
2734
];
2835

website/src/views/planner/PlannerContainer/PlannerContainer.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from 'actions/planner';
2626
import { toggleFeedback } from 'actions/app';
2727
import { fetchModule } from 'actions/moduleBank';
28+
import { addModule as addModuleToTimetable } from 'actions/timetables';
2829
import { getAcadYearModules, getExemptions, getIBLOCs, getPlanToTake } from 'selectors/planner';
2930
import Title from 'views/components/Title';
3031
import LoadingSpinner from 'views/components/LoadingSpinner';
@@ -52,6 +53,7 @@ export type Props = Readonly<{
5253
moveModule: (id: string, year: string, semester: Semester, index: number) => void;
5354
removeModule: (id: string) => void;
5455
setPlaceholderModule: (id: string, moduleCode: ModuleCode) => void;
56+
addModuleToTimetable: (semester: Semester, module: ModuleCode) => void;
5557
}>;
5658

5759
type SemesterModules = { [semester: string]: PlannerModuleInfo[] };
@@ -131,6 +133,9 @@ export class PlannerContainerComponent extends PureComponent<Props, State> {
131133
this.props.fetchModule(moduleCode);
132134
};
133135

136+
onAddModuleToTimetable = (semester: Semester, module: ModuleCode) =>
137+
this.props.fetchModule(module).then(() => this.props.addModuleToTimetable(semester, module));
138+
134139
closeAddCustomData = () => this.setState({ showCustomModule: null });
135140

136141
closeSettingsModal = () => this.setState({ showSettings: false });
@@ -194,6 +199,7 @@ export class PlannerContainerComponent extends PureComponent<Props, State> {
194199
addCustomData: this.onAddCustomData,
195200
setPlaceholderModule: this.onSetPlaceholderModule,
196201
removeModule: this.props.removeModule,
202+
addModuleToTimetable: this.onAddModuleToTimetable,
197203
};
198204

199205
return (
@@ -306,6 +312,7 @@ const PlannerContainer = connect(mapStateToProps, {
306312
addModule: addPlannerModule,
307313
moveModule: movePlannerModule,
308314
removeModule: removePlannerModule,
315+
addModuleToTimetable,
309316
})(PlannerContainerComponent);
310317

311318
export default PlannerContainer;

website/src/views/planner/PlannerModule.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Props = Readonly<{
2727
placeholder?: PlannerPlaceholder;
2828
conflict?: Conflict | null;
2929
semester?: Semester;
30+
isInTimetable?: boolean;
3031

3132
// For draggable
3233
id: string;
@@ -36,6 +37,8 @@ type Props = Readonly<{
3637
removeModule: (id: string) => void;
3738
addCustomData: (moduleCode: ModuleCode) => void;
3839
setPlaceholderModule: (id: string, moduleCode: ModuleCode) => void;
40+
addModuleToTimetable: (semester: Semester, module: ModuleCode) => void;
41+
viewSemesterTimetable: () => void;
3942
}>;
4043

4144
/**
@@ -50,6 +53,11 @@ const PlannerModule = memo<Props>((props) => {
5053
if (props.moduleCode) props.addCustomData(props.moduleCode);
5154
};
5255

56+
const addModuleToTimetable = () => {
57+
if (props.semester && props.moduleCode)
58+
props.addModuleToTimetable(props.semester, props.moduleCode);
59+
};
60+
5361
const renderConflict = (conflict: Conflict) => {
5462
switch (conflict.type) {
5563
case 'noInfo':
@@ -192,7 +200,13 @@ const PlannerModule = memo<Props>((props) => {
192200
{...provided.draggableProps}
193201
{...provided.dragHandleProps}
194202
>
195-
<ModuleMenu removeModule={removeModule} editCustomData={editCustomData} />
203+
<ModuleMenu
204+
isInTimetable={props.isInTimetable}
205+
removeModule={removeModule}
206+
editCustomData={editCustomData}
207+
addModuleToTimetable={addModuleToTimetable}
208+
viewSemesterTimetable={props.viewSemesterTimetable}
209+
/>
196210

197211
<div className={styles.moduleInfo}>
198212
<div className={styles.moduleName}>

website/src/views/planner/PlannerSemester.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
getSemesterName,
1414
getTotalMC,
1515
} from 'utils/planner';
16+
import { useSelector } from 'react-redux';
17+
import { getSemesterTimetableLessons } from 'selectors/timetables';
18+
import { useHistory } from 'react-router-dom';
19+
import { timetablePage } from 'views/routes/paths';
1620
import PlannerModule from './PlannerModule';
1721
import AddModule from './AddModule';
1822
import styles from './PlannerSemester.scss';
@@ -29,6 +33,7 @@ type Props = Readonly<{
2933
removeModule: (id: string) => void;
3034
addCustomData: (moduleCode: ModuleCode) => void;
3135
setPlaceholderModule: (id: string, moduleCode: ModuleCode) => void;
36+
addModuleToTimetable: (semester: Semester, module: ModuleCode) => void;
3237
}>;
3338

3439
function renderSemesterMeta(plannerModules: PlannerModuleInfo[]) {
@@ -57,12 +62,23 @@ const PlannerSemester: React.FC<Props> = ({
5762
removeModule,
5863
addCustomData,
5964
setPlaceholderModule,
65+
addModuleToTimetable,
6066
}) => {
67+
const timetable = useSelector(getSemesterTimetableLessons)(semester);
68+
69+
const history = useHistory();
70+
const viewSemesterTimetable = () => {
71+
const timetablePath = timetablePage(semester);
72+
history.push(timetablePath);
73+
};
74+
6175
const renderModule = (plannerModule: PlannerModuleInfo, index: number) => {
6276
const { id, moduleCode, moduleInfo, conflict, placeholder } = plannerModule;
6377

6478
const showExamDate = showModuleMeta && config.academicYear === year;
6579

80+
const isInTimetable = moduleCode !== undefined && moduleCode in timetable;
81+
6682
return (
6783
<PlannerModule
6884
key={id}
@@ -75,8 +91,11 @@ const PlannerSemester: React.FC<Props> = ({
7591
moduleCredit={showModuleMeta ? getModuleCredit(plannerModule) : null}
7692
conflict={conflict}
7793
semester={semester}
94+
isInTimetable={isInTimetable}
7895
removeModule={removeModule}
7996
addCustomData={addCustomData}
97+
addModuleToTimetable={addModuleToTimetable}
98+
viewSemesterTimetable={viewSemesterTimetable}
8099
setPlaceholderModule={setPlaceholderModule}
81100
/>
82101
);

website/src/views/planner/PlannerYear.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Props = Readonly<{
2020
removeModule: (id: string) => void;
2121
addCustomData: (moduleCode: ModuleCode) => void;
2222
setPlaceholderModule: (id: string, moduleCode: ModuleCode) => void;
23+
addModuleToTimetable: (semester: Semester, module: ModuleCode) => void;
2324
}>;
2425

2526
type State = {
@@ -92,6 +93,7 @@ export default class PlannerYear extends PureComponent<Props, State> {
9293
removeModule={this.props.removeModule}
9394
addCustomData={this.props.addCustomData}
9495
setPlaceholderModule={this.props.setPlaceholderModule}
96+
addModuleToTimetable={this.props.addModuleToTimetable}
9597
/>
9698
</div>
9799
))}

0 commit comments

Comments
 (0)