Forked from an inaccessible project.
-
Isaac Evavold authoredIsaac Evavold authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
reducer.ts 10.93 KiB
import { UIActionTypes } from './actions/ui.actions';
import {
DegreePlannerState,
INITIAL_DEGREE_PLANNER_STATE,
} from '@app/degree-planner/store/state';
import {
PlanActionTypes,
InitialLoadSuccess,
SwitchPlanSuccess,
MakePlanPrimary,
MakePlanPrimarySuccess,
MakePlanPrimaryFailure,
ChangePlanNameSuccess,
ChangePlanNameFailure,
CreatePlan,
CreatePlanSuccess,
} from '@app/degree-planner/store/actions/plan.actions';
import {
CourseActionTypes,
RemoveCourseSuccess,
MoveCourseBetweenTermsSuccess,
AddCourseSuccess,
RemoveSaveForLaterSuccess,
AddSaveForLaterSuccess,
} from '@app/degree-planner/store/actions/course.actions';
import {
NoteActionTypes,
WriteNoteSuccess,
DeleteNoteSuccess,
} from '@app/degree-planner/store/actions/note.actions';
import {
AddAcademicYearActionTypes,
AddAcademicYearRequest,
} from '@app/degree-planner/store/actions/addAcademicYear.actions';
import { ToggleAcademicYear } from '@app/degree-planner/store/actions/ui.actions';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
import { DegreePlan } from '@app/core/models/degree-plan';
type SupportedActions =
| InitialLoadSuccess
| SwitchPlanSuccess
| WriteNoteSuccess
| DeleteNoteSuccess
| MoveCourseBetweenTermsSuccess
| RemoveCourseSuccess
| AddCourseSuccess
| RemoveSaveForLaterSuccess
| AddSaveForLaterSuccess
| AddAcademicYearRequest
| CreatePlanSuccess
| MakePlanPrimary
| MakePlanPrimarySuccess
| MakePlanPrimaryFailure
| ChangePlanNameSuccess
| ChangePlanNameFailure
| ToggleAcademicYear;
export function degreePlannerReducer(
state = INITIAL_DEGREE_PLANNER_STATE,
action: SupportedActions,
): DegreePlannerState {
switch (action.type) {
/**
* 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.InitialLoadSuccess: {
return { ...action.payload };
}
/**
* 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.
*/
case PlanActionTypes.SwitchPlanSuccess: {
return { ...state, ...action.payload };
}
/**
* The `AddAcademicYearRequest` action is triggered after `addAcademicYear()`
* function runs. A new academic year container with three terms will be created.
*/
case AddAcademicYearActionTypes.AddAcademicYearRequest: {
const originalTerms = state.visibleTerms.map(term => {
return parseInt(term.termCode.substr(0, 3), 10);
});
const newAcademicYearCode = Math.max(...originalTerms) + 1;
const newVisibleTerms = [
...state.visibleTerms,
{ termCode: `${newAcademicYearCode}2`, courses: [] },
{ termCode: `${newAcademicYearCode}4`, courses: [] },
{ termCode: `${newAcademicYearCode}6`, courses: [] },
];
return { ...state, visibleTerms: newVisibleTerms };
}
/**
* The `ToggleAcademicYear` action toggles the expand/collapse UI state
*/
case UIActionTypes.ToggleAcademicYear: {
return { ...state };
}
/**
* 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.
*/
case NoteActionTypes.WriteNoteSuccess: {
const updatedNote = action.payload.updatedNote;
const updatedTermCode = updatedNote.termCode;
const originalTerms = state.visibleTerms;
if (termCodeExists(updatedTermCode, originalTerms)) {
/**
* If a term with the given `termCode` *DOES exist* in the state,
* replace just that term with the new data inside the action.
*/
const newVisibleTerms = originalTerms.map(term => {
if (term.termCode === updatedTermCode) {
return { ...term, note: updatedNote };
} else {
return term;
}
});
return { ...state, visibleTerms: newVisibleTerms };
} else {
/**
* If a term with the given `termCode` *DOES NOT exist*
* in the state, add it to the end of the term list.
*/
const newVisibleTerms = originalTerms.concat({
termCode: updatedTermCode,
note: updatedNote,
courses: [],
});
return { ...state, visibleTerms: newVisibleTerms };
}
}
/**
* 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.
*/
case NoteActionTypes.DeleteNoteSuccess: {
const deletedTermCode = action.payload.termCode;
const originalTerms = state.visibleTerms;
if (termCodeExists(deletedTermCode, originalTerms)) {
/**
* If a term with the given `termCode` *DOES EXIST* in the state,
* remove that term's note.
*/
const newVisibleTerms = originalTerms.map(term => {
if (term.termCode === deletedTermCode) {
return { ...term, note: undefined };
} else {
return term;
}
});
return { ...state, visibleTerms: newVisibleTerms };
} else {
return state;
}
}
case CourseActionTypes.MoveCourseBetweenTermsSuccess: {
const { to, from, id } = action.payload;
const t = state.visibleTerms.find(term => term.termCode === from);
if (t) {
const course = t.courses.find(c => c.id === id);
if (course) {
course.termCode = to;
// Create new visibleTerms array
const newVisibleTerms = state.visibleTerms.map(term => {
if (term.termCode === from) {
// Remove the course from the previous term
term.courses = term.courses.filter(c => c.id !== id);
} else if (term.termCode === to) {
// Add the new course to this term
term.courses = [...term.courses, course];
}
return term;
});
return { ...state, visibleTerms: newVisibleTerms };
}
return state;
}
return state;
}
case CourseActionTypes.AddCourseSuccess: {
const { course } = action.payload;
const newVisibleTerms = state.visibleTerms.map(term => {
if (term.termCode === course.termCode) {
term.courses.push(course);
}
return term;
});
return { ...state, visibleTerms: newVisibleTerms };
// return {...state, visibleTerms: newVisibleTerms};
// return state;
}
case CourseActionTypes.RemoveCourseSuccess: {
const { recordId: id } = action.payload;
// Create new visibleTerms array
const newVisibleTerms = state.visibleTerms.map(term => {
term.courses = term.courses.filter(course => {
return course.id !== id;
});
return term;
});
return { ...state, visibleTerms: newVisibleTerms };
}
case CourseActionTypes.RemoveSaveForLaterSuccess: {
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 };
}
case CourseActionTypes.AddSaveForLaterSuccess: {
const newSavedForLater: SavedForLaterCourse[] = [
...state.savedForLaterCourses,
{
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],
},
];
return { ...state, savedForLaterCourses: newSavedForLater };
}
case PlanActionTypes.CreatePlanSuccess: {
const { newPlan } = action.payload;
return {
...state,
allDegreePlans: state.allDegreePlans.concat(newPlan),
};
}
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 = {
...(state.visibleDegreePlan as DegreePlan),
name: action.payload.oldName,
};
return {
...state,
allDegreePlans: state.allDegreePlans.map(plan => {
if (plan.roadmapId === action.payload.roadmapId) {
return { ...plan, name: action.payload.oldName };
} else {
return plan;
}
}),
visibleDegreePlan,
};
}
/**
* 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);
};