Forked from an inaccessible project.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
plan.effects.ts 10.36 KiB
// Libraries
import { Injectable } from '@angular/core';
import { ROOT_EFFECTS_INIT, Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, forkJoin, of } from 'rxjs';
import { map, flatMap, withLatestFrom, filter } from 'rxjs/operators';
import { GlobalState } from '@app/core/state';
import { Store } from '@ngrx/store';
// Services
import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
import { getDegreePlannerState } from '@app/degree-planner/store/selectors';
// Actions
import {
InitialPlanLoadResponse,
ChangeVisiblePlanRequest,
ChangeVisiblePlanResponse,
PlanActionTypes,
ChangeCourseTermResponse,
AddCourseResponse,
RemoveCourseResponse,
RemoveSavedForLaterResponse,
AddSavedForLaterResponse,
AddCourseRequest,
RemoveCourseRequest,
} from '@app/degree-planner/store/actions/plan.actions';
// Models
import { DegreePlan } from '@app/core/models/degree-plan';
import { PlannedTerm } from '@app/core/models/planned-term';
import { Course, SubjectMapping, CourseBase } from '@app/core/models/course';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
@Injectable()
export class DegreePlanEffects {
constructor(
private actions$: Actions,
private api: DegreePlannerApiService,
private store$: Store<GlobalState>,
) {}
@Effect()
init$: Observable<InitialPlanLoadResponse> = this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
// Load the list of degree plans and data used by all degree plans.
flatMap(() => {
const activeTermCodes = this.api
.getActiveTerms()
.pipe(map(terms => terms.map(term => term.termCode)));
return forkJoinWithKeys({
allDegreePlans: this.api.getAllDegreePlans(),
subjects: this.api.getAllSubjects(),
activeTermCodes,
});
}),
// Load data specific to the primary degree plan.
flatMap(({ allDegreePlans, subjects, activeTermCodes }) => {
const savedForLaterCourses = this.loadSavedForLaterCourses(subjects);
const visibleDegreePlan = pickPrimaryDegreePlan(allDegreePlans);
const visibleTerms = loadPlanTerms(
this.api,
visibleDegreePlan,
subjects,
activeTermCodes,
);
return forkJoinWithKeys({
visibleDegreePlan: of(visibleDegreePlan),
visibleTerms,
savedForLaterCourses,
activeTermCodes: of(activeTermCodes),
allDegreePlans: of(allDegreePlans),
subjects: of(subjects),
});
}),
map(payload => new InitialPlanLoadResponse(payload)),
);
@Effect()
switch$: Observable<ChangeVisiblePlanResponse> = this.actions$.pipe(
ofType<ChangeVisiblePlanRequest>(PlanActionTypes.ChangeVisiblePlanRequest),
withLatestFrom(this.store$.select(getDegreePlannerState)),
flatMap(([action, state]) => {
const visibleDegreePlan = state.allDegreePlans.find(plan => {
return plan.roadmapId === action.payload.newVisibleRoadmapId;
}) as DegreePlan;
const visibleTerms = loadPlanTerms(
this.api,
visibleDegreePlan,
state.subjects,
state.activeTermCodes,
);
return forkJoinWithKeys({
visibleDegreePlan: of(visibleDegreePlan),
visibleTerms,
});
}),
map(payload => new ChangeVisiblePlanResponse(payload)),
);
@Effect()
MoveCourseBetweenTerms$ = this.actions$.pipe(
ofType<any>(PlanActionTypes.ChangeCourseTermRequest),
withLatestFrom(this.store$.select(getDegreePlannerState)),
filter(([_, state]) => typeof state.visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([action, state]) => {
// TODO error handle the API calls
return this.api
.updateCourseTerm(
(state.visibleDegreePlan as DegreePlan).roadmapId,
action.payload.id,
action.payload.to,
)
.pipe(
map(response => {
return {
response,
action,
};
}),
);
}),
// // Wrap data in an Action for dispatch
map(({ response, action }) => {
if (response === 1) {
return new ChangeCourseTermResponse({
id: action.payload.id,
from: action.payload.from,
to: action.payload.to,
});
}
return;
}),
);
@Effect()
AddCourse$ = this.actions$.pipe(
ofType<AddCourseRequest>(PlanActionTypes.AddCourseRequest),
withLatestFrom(this.store$.select(getDegreePlannerState)),
filter(([_, state]) => state.visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([action, state]) => {
// TODO error handle the API calls
const roadmapId = (state.visibleDegreePlan as DegreePlan).roadmapId;
const { subjectCode, termCode, courseId } = action.payload;
const addCourse$ = this.api.addCourse(
roadmapId,
subjectCode,
courseId,
termCode,
);
const courseBaseToCourse$ = addCourse$.pipe(
map<CourseBase, Course>(courseBase => ({
...courseBase,
subject: state.subjects[courseBase.subjectCode],
})),
);
const toSuccessAction$ = courseBaseToCourse$.pipe(
map(course => new AddCourseResponse({ course })),
);
return toSuccessAction$;
}),
);
@Effect()
RemoveCourse$ = this.actions$.pipe(
ofType<RemoveCourseRequest>(PlanActionTypes.RemoveCourseRequest),
withLatestFrom(this.store$.select(getDegreePlannerState)),
filter(([_, state]) => state.visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([action, state]) => {
const roadmapId = (state.visibleDegreePlan as DegreePlan).roadmapId;
const recordId = action.payload.recordId;
const removeCourse$ = this.api.removeCourse(roadmapId, recordId);
const toSuccessAction$ = removeCourse$.pipe(
map(() => new RemoveCourseResponse({ recordId })),
);
return toSuccessAction$;
}),
);
@Effect()
RemoveSavedForLater$ = this.actions$.pipe(
ofType<any>(PlanActionTypes.RemoveSavedForLaterRequest),
withLatestFrom(this.store$.select(getDegreePlannerState)),
filter(([_, state]) => state.visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([action, state]) => {
// TODO error handle the API calls
return this.api
.removeSavedForLater(
action.payload.subjectCode,
action.payload.courseId,
)
.pipe(
map(response => {
return {
response,
action,
};
}),
);
}),
// // Wrap data in an Action for dispatch
map(({ response, action }) => {
if (response === null) {
const { courseId, subjectCode } = action.payload;
return new RemoveSavedForLaterResponse({ courseId, subjectCode });
// TODO Update UI and remove saved response
}
return;
}),
);
@Effect()
SaveForLater$ = this.actions$.pipe(
ofType<any>(PlanActionTypes.AddSavedForLaterRequest),
withLatestFrom(this.store$.select(getDegreePlannerState)),
filter(([_, state]) => state.visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([action, state]) => {
// TODO error handle the API calls
return this.api
.saveForLater(action.payload.subjectCode, action.payload.courseId)
.pipe(
map(response => {
return {
response,
action,
};
}),
);
}),
// // // Wrap data in an Action for dispatch
map(({ response, action }) => {
if (response === null) {
return new AddSavedForLaterResponse(action.payload);
}
// return;
return;
}),
);
private loadSavedForLaterCourses(subjects: SubjectMapping) {
return this.api.getSavedForLaterCourses().pipe(
map(courseBases => {
return courseBases.map<SavedForLaterCourse>(base => {
return {
...base,
subject: subjects[base.subjectCode] as string,
};
});
}),
);
}
}
const loadPlanTerms = (
api: DegreePlannerApiService,
visibleDegreePlan: DegreePlan,
subjects: SubjectMapping,
activeTermCodes: string[],
): Observable<PlannedTerm[]> => {
const notesAndTerms$ = forkJoinWithKeys({
notes: api.getAllNotes(visibleDegreePlan.roadmapId),
terms: api.getAllTermCourses(visibleDegreePlan.roadmapId),
});
const uniqueTerms$ = notesAndTerms$.pipe(
map(({ notes, terms }) => {
const noteTermCodes = notes.map(note => note.termCode);
const courseTermCodes = terms.map(term => term.termCode);
const uniqueTermCodes = unique([
...noteTermCodes,
...courseTermCodes,
...activeTermCodes,
]);
return uniqueTermCodes.sort();
}),
);
const visibleTerms$ = forkJoin(uniqueTerms$, notesAndTerms$).pipe(
map(([uniqueTerms, { notes, terms }]) => {
return uniqueTerms.map(termCode => {
const note = notes.find(matchesTermCode(termCode));
const term = terms.find(matchesTermCode(termCode));
const courses = (term ? term.courses : []).map(course => ({
...course,
subject: subjects[course.subjectCode],
}));
return { termCode, note, courses } as PlannedTerm;
});
}),
);
return visibleTerms$;
};
type SimpleMap = { [name: string]: any };
type ObservableMap<T = SimpleMap> = { [K in keyof T]: Observable<T[K]> };
const forkJoinWithKeys = <T = SimpleMap>(pairs: ObservableMap<T>) => {
const keys = Object.keys(pairs);
const observables = keys.map(key => pairs[key]);
return forkJoin(observables).pipe(
map<any[], T>(values => {
const valueMapping = {} as T;
keys.forEach((key, index) => {
valueMapping[key] = values[index];
});
return valueMapping;
}),
);
};
const unique = <T>(things: T[]): T[] => {
return things.filter((thing, index, all) => all.indexOf(thing) === index);
};
const matchesTermCode = (termCode: string) => (thing: { termCode: string }) => {
return thing.termCode === termCode;
};
const pickPrimaryDegreePlan = (plans: DegreePlan[]): DegreePlan => {
const primary = plans.find(plan => plan.primary);
return primary ? primary : plans[0];
};