Newer
Older
import { 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,
PlannedTermNote,
} from '@app/core/models/planned-term';
import { TermCode, YearCode } from '@app/core/models/termcode';
type SupportedActions =
| ToggleCourseSearch
| UpdateSearchTermCode;
export function degreePlannerReducer(
state = INITIAL_DEGREE_PLANNER_STATE,
action: SupportedActions,
): DegreePlannerState {
case PlanActionTypes.PlanError: {
return { ...state, isLoadingPlan: false };
}
case PlanActionTypes.CreatePlan: {
return { ...state, isLoadingPlan: true };
}
/**
* 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 = new YearCode(`${largestYearCode + 1}`);
const nextYear = emptyYear(nextYearCode, state.activeTermCodes);
const visibleYears: YearMapping = {
...state.visibleYears,
case UIActionTypes.ExpandAcademicYear: {
const yearCode = action.payload.yearCode;
return {
...state,
visibleYears: {
...state.visibleYears,
[yearCode.toString()]: {
...state.visibleYears[yearCode.toString()],
isExpanded: true,
},
case UIActionTypes.CollapseAcademicYear: {
const yearCode = action.payload.yearCode;
return {
...state,
visibleYears: {
...state.visibleYears,
[yearCode.toString()]: {
...state.visibleYears[yearCode.toString()],
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,
};
}
/**
* The `UpdateSearchTermCode` action changes the active seach term code.
*/
case UIActionTypes.UpdateSearchTermCode: {
return {
...state,
search: { ...state.search, selectedTerm: action.termCode },
};
case NoteActionTypes.WriteNote: {
const { termCode, noteText } = action.payload;
const { yearCode, termName } = termCode;
const year = state.visibleYears[yearCode.toString()];
const existingNote = year ? year[termName].note : undefined;
const newNote: PlannedTermNote =
existingNote && existingNote.isLoaded
? { isLoaded: true, text: noteText, id: existingNote.id }
: { isLoaded: false, text: noteText };
const visibleYears: YearMapping = {
...state.visibleYears,
),
};
return { ...state, visibleYears };
}
/**
* 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 termCode = new TermCode(updatedNote.termCode);
const { yearCode } = termCode;
const visibleYears: YearMapping = {
...state.visibleYears,
{ isLoaded: true, text: updatedNote.note, id: updatedNote.id },
}
/**
* 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 visibleYears: YearMapping = {
...state.visibleYears,
case CourseActionTypes.MoveCourseInsideTerm: {
const { termCode, recordId, newIndex } = action.payload;
const { yearCode, termName } = termCode;
const year = state.visibleYears[yearCode.toString()];
const courses = year[termName].plannedCourses;
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.toString()]: {
...state.visibleYears[yearCode.toString()],
...state.visibleYears[yearCode.toString()][termName],
plannedCourses: newCourses,
},
},
};
return { ...state, visibleYears };
}
}
return state;
}
const {
to: toTermCode,
from: fromTermCode,
id,
newIndex,
} = action.payload;
const { yearCode: fromYearCode } = fromTermCode;
const { yearCode: toYearCode } = toTermCode;
const course = findCourse(state.visibleYears, fromTermCode, id);
if (course && course.id !== null) {
const fromYear = createYearWithoutCourse(
fromTermCode,
course.id,
state.activeTermCodes,
);
const toYear = createYearWithCourse(
toTermCode,
);
const visibleYears = {
...state.visibleYears,
[fromYearCode.toString()]: fromYear,
[toYearCode.toString()]: toYear,
case CourseActionTypes.AddCourse: {
const { termCode, subjectCode, newIndex } = action.payload;
const course = {
...action.payload,
id: null,
termCode: action.payload.termCode.toString(),
subject: state.subjects[subjectCode],
} as Course;
const year: Year = createYearWithCourse(
termCode,
course,
state.activeTermCodes,
newIndex,
);
const visibleYears: YearMapping = {
...state.visibleYears,
const { course, newIndex } = action.payload;
const termCode = new TermCode(course.termCode);
const { yearCode } = termCode;
const year: Year = createYearWithCourse(
termCode,
course,
state.activeTermCodes,
const year: Year = createYearWithoutCourse(
fromTermCode,
recordId,
state.activeTermCodes,
);
const visibleYears = {
...state.visibleYears,
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 };
const { newPlan, newYears } = action.payload;
visibleDegreePlan: newPlan,
visibleYears: newYears,
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
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 emptyTerm = (termCode: TermCode, era: PlannedTermEra): PlannedTerm => {
return { termCode, era, plannedCourses: [], enrolledCourses: [] };
const emptyYear = (yearCode: YearCode, activeTermCodes: TermCode[]): Year => {
return {
yearCode,
isExpanded: utils.pickYearEra(yearCode, activeTermCodes) !== 'past',
fall: emptyTerm(yearCode.fall, pickTermEra(yearCode.fall, activeTermCodes)),
spring: emptyTerm(
yearCode.spring,
pickTermEra(yearCode.spring, activeTermCodes),
),
summer: emptyTerm(
yearCode.summer,
pickTermEra(yearCode.summer, activeTermCodes),
),
termCode: TermCode,
activeTermCodes: TermCode[],
return emptyYear(termCode.yearCode, activeTermCodes);
year = generateYearForTermCode(termCode, activeTermCodes),
): Year => {
const term = year[termCode.termName];
return { ...year, [termCode.termName]: { ...term, note } };
termCode: TermCode,
activeTermCodes: TermCode[],
year?: Year,
) => {
return createYearWithNote(termCode, undefined, activeTermCodes, year);
};
const findCourse = (
years: YearMapping,
termCode: TermCode,
recordId: number,
) => {
const year = years[termCode.yearCode.toString()];
return term.plannedCourses.find(course => course.id === recordId);
year = generateYearForTermCode(termCode, activeTermCodes),
const courses = term.plannedCourses.filter(
c => c.courseId !== course.courseId,
);
newIndex = newIndex !== undefined ? newIndex : courses.length;
courses.splice(newIndex, 0, course);
return { ...year, [termCode.termName]: { ...term, plannedCourses: courses } };
year = generateYearForTermCode(termCode, activeTermCodes),
): Year => {
const courses = term.plannedCourses.filter(course => course.id !== recordId);
return {
...year,
[termCode.termName]: { ...term, plannedCourses: courses },