diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts index be19b723aab6f1a13bdc775496cb4ec4cb4adf46..cefbda2820907ce817fc9eb1da047ee56803c81e 100644 --- a/src/app/degree-planner/store/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -1,8 +1,8 @@ // Libraries import { Injectable } from '@angular/core'; import { ROOT_EFFECTS_INIT, Actions, Effect, ofType } from '@ngrx/effects'; -import { Observable, forkJoin } from 'rxjs'; -import { map, flatMap, tap, withLatestFrom, filter } from 'rxjs/operators'; +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'; @@ -41,101 +41,68 @@ export class DegreePlanEffects { init$: Observable<InitialPlanLoadResponse> = this.actions$.pipe( ofType(ROOT_EFFECTS_INIT), - // Load the data that is not specific to a particular degree plan. Also pick - // the primary degree plan as the first visible degree plan. + // Load the list of degree plans and data used by all degree plans. flatMap(() => { - return forkJoin( - this.api.getAllDegreePlans(), - this.api.getAllSubjects(), - this.api.getActiveTerms(), - ).pipe( - map(([allDegreePlans, subjects, activeTerms]) => { - const visibleDegreePlan = - allDegreePlans.find(plan => plan.primary) || allDegreePlans[0]; - - return { - allDegreePlans, - subjects, - activeTermCodes: activeTerms.map(term => term.termCode), - visibleDegreePlan, - }; - }), - ); + const activeTermCodes = this.api + .getActiveTerms() + .pipe(map(terms => terms.map(term => term.termCode))); + + return forkJoinWithKeys({ + allDegreePlans: this.api.getAllDegreePlans(), + subjects: this.api.getAllSubjects(), + activeTermCodes, + }); }), - // Get term data for the degree plan specified by the roadmap ID and any - // courses that were 'saved for later' by the user. - flatMap(stdin => { - return forkJoin( - this.loadPlanTerms( - stdin.visibleDegreePlan, - stdin.subjects, - stdin.activeTermCodes, - ), - this.loadSavedForLaterCourses(stdin.subjects), - ).pipe( - map(([visibleTerms, savedForLater]) => { - const savedForLaterCourses = savedForLater.map(course => { - course.subject = stdin.subjects[course.subjectCode]; - return course; - }); - - return new InitialPlanLoadResponse({ - visibleDegreePlan: stdin.visibleDegreePlan, - visibleTerms, - savedForLaterCourses, - activeTermCodes: stdin.activeTermCodes, - allDegreePlans: stdin.allDegreePlans, - subjects: stdin.subjects, - }); - }), + // 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), - // Get the most recent Degree Planner state object from the store. This is - // used to decide to fire either the `updateNote` API or `createNote` API. withLatestFrom(this.store$.select(getDegreePlannerState)), - // Get the roadmap ID from the action and use that ID to lookup the - // corresponding degree plan object. - map(([action, state]) => { - return { - subjects: state.subjects, - visibleDegreePlan: state.allDegreePlans.find(plan => { - return plan.roadmapId === action.payload.newVisibleRoadmapId; - }) as DegreePlan, - activeTermCodes: state.activeTermCodes, - }; - }), - - // Get term data for the degree plan specified by the roadmap ID. - flatMap(oldData => { - return this.loadPlanTerms( - oldData.visibleDegreePlan, - oldData.subjects, - oldData.activeTermCodes, - ).pipe( - map(visibleTerms => { - return { - visibleTerms, - visibleDegreePlan: oldData.visibleDegreePlan, - }; - }), + 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, ); - }), - // Wrap data in an Action for dispatch - map(data => { - return new ChangeVisiblePlanResponse({ - visibleDegreePlan: data.visibleDegreePlan, - visibleTerms: data.visibleTerms, + return forkJoinWithKeys({ + visibleDegreePlan: of(visibleDegreePlan), + visibleTerms, }); }), + + map(payload => new ChangeVisiblePlanResponse(payload)), ); @Effect() @@ -145,12 +112,6 @@ export class DegreePlanEffects { withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => typeof state.visibleDegreePlan !== undefined), - // Get the roadmap ID from the action. - tap(([action, state]) => { - console.log(action); - console.log(state); - }), - // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls @@ -190,12 +151,6 @@ export class DegreePlanEffects { withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => state.visibleDegreePlan !== undefined), - // Get the roadmap ID from the action. - tap(([action, state]) => { - console.log(action); - console.log(state); - }), - // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls @@ -227,12 +182,6 @@ export class DegreePlanEffects { withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => state.visibleDegreePlan !== undefined), - // Get the roadmap ID from the action. - // tap(([action, state]) => { - // console.log(action); - // console.log(state); - // }), - // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls @@ -264,15 +213,6 @@ export class DegreePlanEffects { withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => state.visibleDegreePlan !== undefined), - // tap(([action, state]) => { - // console.log('REMOVE SAVED FOR LATER ----------'); - - // console.log(action); - // console.log(state); - - // console.log('---------------------------------'); - // }), - // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls @@ -309,16 +249,6 @@ export class DegreePlanEffects { withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => state.visibleDegreePlan !== undefined), - // Get the roadmap ID from the action. - // tap(([action, state]) => { - // console.log('ADD SAVED FOR LATER ----------'); - - // console.log(action); - // console.log(state); - - // console.log('---------------------------------'); - // }), - // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls @@ -357,49 +287,69 @@ export class DegreePlanEffects { }), ); } +} - private loadPlanTerms( - visibleDegreePlan: DegreePlan, - subjects: SubjectMapping, - activeTermCodes: string[], - ) { - return forkJoin( - this.api.getAllNotes(visibleDegreePlan.roadmapId), - this.api.getAllTermCourses(visibleDegreePlan.roadmapId), - ).pipe( - // Combine courses and notes by term. - map(([notes, termCourses]) => { - const noteTermCodes = notes.map(note => note.termCode); - const courseTermCodes = termCourses.map(term => term.termCode); - - /** - * Using the notes & courses relevant to the current degree plan and - * the active terms, generate a sorted list of all unqiue term codes. - */ - const uniqueTermCodes = unique([ - ...noteTermCodes, - ...courseTermCodes, - ...activeTermCodes, - ]).sort(); - - /** - * Group the notes and courses into a list of visible terms. - */ - const visibleTerms: PlannedTerm[] = uniqueTermCodes.map(termCode => { - const note = notes.find(matchesTermCode(termCode)); - const termCourse = termCourses.find(matchesTermCode(termCode)); - const courses = (termCourse ? termCourse.courses : []).map(base => ({ - ...base, - subject: subjects[base.subjectCode], - })); - return { termCode, note, courses }; - }); +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(); + }), + ); - return visibleTerms; - }), - ); - } -} + 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); @@ -408,3 +358,8 @@ const unique = <T>(things: T[]): T[] => { 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]; +};