Newer
Older
import { parseTermCode, pickTermEra } from '@app/degree-planner/shared/utils';
MakePlanPrimary,
MakePlanPrimarySuccess,
MakePlanPrimaryFailure,
} from '@app/degree-planner/store/actions/plan.actions';
import {
} from '@app/degree-planner/store/actions/course.actions';
} from '@app/degree-planner/store/actions/note.actions';
import {
AddAcademicYearActionTypes,
AddAcademicYearRequest,
} from '@app/degree-planner/store/actions/addAcademicYear.actions';
OpenCourseSearch,
CloseCourseSearch,
ToggleCourseSearch,
UpdateSearchTermCode,
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
import { Year, YearMapping } from '@app/core/models/year';
import { Course } from '@app/core/models/course';
import { Note } from '@app/core/models/note';
import { PlannedTerm, PlannedTermEra } from '@app/core/models/planned-term';
import * as utils from '@app/degree-planner/shared/utils';
type SupportedActions =
| SwitchPlanSuccess
| WriteNoteSuccess
| DeleteNoteSuccess
| ToggleCourseSearch
| UpdateSearchTermCode;
export function degreePlannerReducer(
state = INITIAL_DEGREE_PLANNER_STATE,
action: SupportedActions,
): DegreePlannerState {
case PlanActionTypes.PlanError: {
return { ...state, isLoadingPlan: false };
}
/**
* The `InitialPlanLoadResponse` action is triggered on initial Degree
* Planner app load. It downloads a list of the user's degree plans and
* picks the primary plan from that list to load as the first visible plan.
*/
case PlanActionTypes.SwitchPlan: {
return { ...state, isLoadingPlan: true };
}
* The `SwitchPlanSuccess` action is triggered whenever the UI needs
* to switch which degree plan is being shown and load the data associated
* with that degree plan. The reducer extracts that downloaded data from the
* action payload and builds a new state using that data.
*/
return { ...state, ...action.payload, isLoadingPlan: false };
/**
* The `AddAcademicYearRequest` action is triggered after `addAcademicYear()`
* function runs. A new academic year container with three terms will be created.
case AddAcademicYearActionTypes.AddAcademicYearRequest: {
const currentYearCodes = Object.keys(state.visibleYears);
const largestYearCode = Math.max(
...currentYearCodes.map(yearCode => {
return parseInt(yearCode, 10);
}),
);
const nextYearCode = `${largestYearCode + 1}`;
const nextYear = emptyYear(nextYearCode, state.activeTermCodes);
const visibleYears: YearMapping = {
...state.visibleYears,
[nextYearCode]: nextYear,
};
return { ...state, visibleYears };
}
case UIActionTypes.ExpandAcademicYear: {
const yearCode = action.payload.yearCode;
return {
...state,
visibleYears: {
...state.visibleYears,
[yearCode]: { ...state.visibleYears[yearCode], isExpanded: true },
},
};
case UIActionTypes.CollapseAcademicYear: {
const yearCode = action.payload.yearCode;
return {
...state,
visibleYears: {
...state.visibleYears,
[yearCode]: { ...state.visibleYears[yearCode], isExpanded: false },
},
};
/**
* The `ToggleCourseSearch` action toggles the open and close state of the course search side nav
*/
case UIActionTypes.ToggleCourseSearch: {
const newSearchState = {
...state.search,
visible: !state.search.visible,
};
// If a term was passed into the action
if (action.termCode) {
newSearchState.selectedTerm = action.termCode;
}
return {
...state,
search: newSearchState,
};
}
/**
* The `ToggleCourseSearch` action opens the course search side nav
*/
case UIActionTypes.OpenCourseSearch: {
const newSearchState = {
...state.search,
visible: true,
};
// If a term was passed into the action
if (action.termCode) {
newSearchState.selectedTerm = action.termCode;
}
return { ...state, search: newSearchState };
}
/**
* The `ToggleCourseSearch` action closes the course search side nav
*/
case UIActionTypes.CloseCourseSearch: {
return {
...state,
search: { ...state.search, visible: false, selectedTerm: '0000' },
};
}
/**
* The `UpdateSearchTermCode` action changes the active seach term code.
*/
case UIActionTypes.UpdateSearchTermCode: {
return {
...state,
search: { ...state.search, selectedTerm: action.termCode },
};
/**
* The `WriteNoteResponse` action is dispatched by the `Note.write$` effect
* upon a successful response from the `updateNote` or `createNote` API
* endpoints. The reducer in this case either:
*
* - Replaces a note on a term that already had a note.
* - *OR* adds a note to a term that didn't previously have a note.
* - *OR* adds a new term with the given note if no term exists with the note's termCode.
*/
const { updatedNote } = action.payload;
const { termCode } = updatedNote;
const { yearCode } = parseTermCode(termCode);
const visibleYears: YearMapping = {
...state.visibleYears,
[yearCode]: createYearWithNote(
termCode,
updatedNote,
state.activeTermCodes,
state.visibleYears[yearCode],
),
};
return { ...state, visibleYears };
}
/**
* The `DeleteNoteResponse` action is dispatched after the `deleteNote` API
* has been called and it is okay to remote the note with the given
* termCode from the degree planner state.
*/
const { termCode } = action.payload;
const { yearCode } = parseTermCode(termCode);
const visibleYears: YearMapping = {
...state.visibleYears,
[yearCode]: createYearWithoutNote(
termCode,
state.activeTermCodes,
state.visibleYears[yearCode],
),
};
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
case CourseActionTypes.MoveCourseInsideTerm: {
const { termCode, recordId, newIndex } = action.payload;
const { yearCode, termName } = parseTermCode(termCode);
const year = state.visibleYears[yearCode];
if (year) {
const courses = year[termName].courses;
const course = courses.find(course => course.id === recordId);
const oldIndex = courses.findIndex(course => course.id === recordId);
if (course) {
const newCourses = courses.slice();
newCourses.splice(oldIndex, 1);
newCourses.splice(newIndex, 0, course);
const visibleYears = {
...state.visibleYears,
[yearCode]: {
...state.visibleYears[yearCode],
[termName]: {
...state.visibleYears[yearCode][termName],
courses: newCourses,
},
},
};
return { ...state, visibleYears };
}
}
return state;
}
const {
to: toTermCode,
from: fromTermCode,
id,
newIndex,
} = action.payload;
const { yearCode: fromYearCode } = parseTermCode(fromTermCode);
const { yearCode: toYearCode } = parseTermCode(toTermCode);
const course = findCourse(state.visibleYears, fromTermCode, id);
if (course && course.id !== null) {
const fromYear = createYearWithoutCourse(
fromTermCode,
course.id,
state.activeTermCodes,
state.visibleYears[fromYearCode],
);
const toYear = createYearWithCourse(
toTermCode,
course,
state.activeTermCodes,
fromYearCode === toYearCode
? fromYear
: state.visibleYears[toYearCode],
);
const visibleYears = {
...state.visibleYears,
[fromYearCode]: fromYear,
[toYearCode]: toYear,
};
return { ...state, visibleYears };
const { course, newIndex } = action.payload;
const { termCode } = course;
const { yearCode } = parseTermCode(termCode);
const year: Year = createYearWithCourse(
termCode,
course,
state.activeTermCodes,
state.visibleYears[yearCode],
);
const visibleYears = {
...state.visibleYears,
[yearCode]: year,
};
const { recordId, fromTermCode } = action.payload;
const { yearCode } = parseTermCode(fromTermCode);
const year: Year = createYearWithoutCourse(
fromTermCode,
recordId,
state.activeTermCodes,
state.visibleYears[yearCode],
);
const visibleYears = {
...state.visibleYears,
[yearCode]: year,
};
const { courseId, subjectCode } = action.payload;
// // Create new saved for later array
const newSavedForLater = state.savedForLaterCourses.filter(
course =>
course.subjectCode !== subjectCode && course.courseId !== courseId,
);
return { ...state, savedForLaterCourses: newSavedForLater };
}
const { newIndex } = action.payload;
const newSavedCourse: SavedForLaterCourse = {
id: null,
courseId: action.payload.courseId,
termCode: '0000',
topicId: 0,
subjectCode: action.payload.subjectCode,
title: action.payload.title,
catalogNumber: action.payload.catalogNumber,
courseOrder: 0,
subject: state.subjects[action.payload.subjectCode],
};
const savedForLaterCoursesArr = state.savedForLaterCourses.slice();
savedForLaterCoursesArr.splice(newIndex, 0, newSavedCourse);
return { ...state, savedForLaterCourses: savedForLaterCoursesArr };
case PlanActionTypes.CreatePlanSuccess: {
const { newPlan } = action.payload;
return {
...state,
allDegreePlans: state.allDegreePlans.concat(newPlan),
};
}
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
case PlanActionTypes.MakePlanPrimary: {
// TODO add global loading state
return state;
}
case PlanActionTypes.MakePlanPrimarySuccess: {
// Get current roadmap ID
const roadmapId = (state.visibleDegreePlan as DegreePlan).roadmapId;
// Update the visible term object
const newVisibleDegreePlan = {
...(state.visibleDegreePlan as DegreePlan),
primary: true,
};
// Update all plans to only have the current one be primary
const newPlans = state.allDegreePlans.map(plan => {
if (plan.roadmapId !== roadmapId) {
return { ...plan, primary: false };
}
return { ...plan, primary: true };
});
// Return the new state
return {
...state,
visibleDegreePlan: newVisibleDegreePlan,
allDegreePlans: newPlans,
};
}
case PlanActionTypes.MakePlanPrimaryFailure: {
// TODO add error message
return state;
}
case PlanActionTypes.ChangePlanNameSuccess: {
const visibleDegreePlan = {
...(state.visibleDegreePlan as DegreePlan),
name: action.payload.newName,
};
return {
...state,
allDegreePlans: state.allDegreePlans.map(plan => {
if (plan.roadmapId === action.payload.roadmapId) {
return { ...plan, name: action.payload.newName };
} else {
return plan;
}
}),
visibleDegreePlan,
};
}
case PlanActionTypes.ChangePlanNameFailure: {
const visibleDegreePlan = {
return {
...state,
allDegreePlans: state.allDegreePlans.map(plan => {
if (plan.roadmapId === action.payload.roadmapId) {
return { ...plan, name: action.payload.oldName };
} else {
return plan;
}
}),
visibleDegreePlan,
};
case PlanActionTypes.DeletePlanSuccess: {
const allDegreePlans = state.allDegreePlans.filter(plan => {
return plan.roadmapId !== action.payload.roadmapId;
});
return { ...state, allDegreePlans };
}
/**
* It's okay if the action didn't match any of the cases above. If that's
* the case, just return the existing state object.
*/
default:
return state;
}
}
const termCodeExists = (termCode: string, things: { termCode: string }[]) => {
return things.some(thing => thing.termCode === termCode);
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
const emptyTerm = (termCode: string, era: PlannedTermEra): PlannedTerm => {
return { termCode, era, courses: [] };
};
const emptyYear = (yearCode: string, activeTermCodes: string[]): Year => {
const fall = `${yearCode}2`;
const spring = `${yearCode}4`;
const summer = `${yearCode}6`;
return {
yearCode,
isExpanded: utils.pickYearEra(yearCode, activeTermCodes) !== 'past',
fall: emptyTerm(fall, pickTermEra(fall, activeTermCodes)),
spring: emptyTerm(spring, pickTermEra(spring, activeTermCodes)),
summer: emptyTerm(summer, pickTermEra(summer, activeTermCodes)),
};
};
const generateYearForTermCode = (
termCode: string,
activeTermCodes: string[],
): Year => {
const { yearCode } = parseTermCode(termCode);
return emptyYear(yearCode, activeTermCodes);
};
const createYearWithNote = (
termCode: string,
note: Note | undefined,
activeTermCodes: string[],
year = generateYearForTermCode(termCode, activeTermCodes),
): Year => {
const { termName } = parseTermCode(termCode);
const term = year[termName];
return { ...year, [termName]: { ...term, note } };
};
const createYearWithoutNote = (
termCode: string,
activeTermCodes: string[],
year?: Year,
) => {
return createYearWithNote(termCode, undefined, activeTermCodes, year);
};
const findCourse = (years: YearMapping, termCode: string, recordId: number) => {
const { yearCode, termName } = parseTermCode(termCode);
const year = years[yearCode];
if (year) {
const term = year[termName];
return term.courses.find(course => course.id === recordId);
}
};
const createYearWithCourse = (
termCode: string,
course: Course,
newIndex: number,
activeTermCodes: string[],
year = generateYearForTermCode(termCode, activeTermCodes),
): Year => {
const { termName } = parseTermCode(termCode);
const term = year[termName];
const courses = term.courses.slice();
courses.splice(newIndex, 0, course);
return { ...year, [termName]: { ...term, courses } };
};
const createYearWithoutCourse = (
termCode: string,
recordId: number,
activeTermCodes: string[],
year = generateYearForTermCode(termCode, activeTermCodes),
): Year => {
const { termName } = parseTermCode(termCode);
const term = year[termName];
const courses = term.courses.filter(course => course.id !== recordId);
return { ...year, [termName]: { ...term, courses } };
};