Newer
Older
// Libraries
import { Injectable } from '@angular/core';
import { ROOT_EFFECTS_INIT, Actions, Effect, ofType } from '@ngrx/effects';
map,
flatMap,
withLatestFrom,
catchError,
filter,
} from 'rxjs/operators';

Scott Berg
committed
import { GlobalState } from '@app/core/state';
// Services
import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
import * as selectors from '@app/degree-planner/store/selectors';
// Actions
import {
CreatePlan,
CreatePlanSuccess,
DeletePlan,
DeletePlanSuccess,
} from '@app/degree-planner/store/actions/plan.actions';
import { DegreePlan } from '@app/core/models/degree-plan';
import { PlannedTerm, PlannedTermEra } from '@app/core/models/planned-term';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
import {
DegreePlannerState,
INITIAL_DEGREE_PLANNER_STATE,
} from '@app/degree-planner/store/state';
import { YearMapping } from '@app/core/models/year';
import { Note } from '@app/core/models/note';
import { CourseBase } from '@app/core/models/course';
import { pickTermEra } from '@app/degree-planner/shared/utils';
@Injectable()
export class DegreePlanEffects {
constructor(
private actions$: Actions,
private api: DegreePlannerApiService,
private store$: Store<GlobalState>,
// Load the list of degree plans and data used by all degree plans.
const activeTermCodes = this.api
.getActiveTerms()
.pipe(map(terms => terms.map(term => term.termCode)));
return forkJoinWithKeys({
allDegreePlans: this.api.getAllDegreePlans(),
subjects: this.api.getAllSubjects(),
subjectDescriptions: this.api.getAllSubjectDescriptions(),
// Load data specific to the primary degree plan.
flatMap(
({ allDegreePlans, subjects, subjectDescriptions, activeTermCodes }) => {
const savedForLaterCourses = this.loadSavedForLaterCourses(subjects);
const visibleDegreePlan = pickPrimaryDegreePlan(allDegreePlans);
const visibleYears = loadPlanYears(
this.api,
visibleDegreePlan.roadmapId,
subjects,
activeTermCodes,
);
const descriptions = {};
subjectDescriptions['0000'].map(subject => {
descriptions[subject.subjectCode] = subject.formalDescription;
});
return forkJoinWithKeys({
visibleDegreePlan: of(visibleDegreePlan),
visibleYears,
savedForLaterCourses,
activeTermCodes: of(activeTermCodes),
allDegreePlans: of(allDegreePlans),
subjects: of(subjects),
expandedYears: of([] as string[]),
subjectDescriptions: of(descriptions),
});
},
),
// map(payload => {
// const allTerms = payload.visibleYears.map(term => term.termCode);
// const currentIndex = allTerms.indexOf(payload.activeTermCodes[0]);
// const expandedTerms = allTerms.slice(currentIndex - allTerms.length);
// const expandedYearsDups = expandedTerms.map(term => term.substr(1, 2));
// const expandedYears = expandedYearsDups.filter(function(item, pos, self) {
// return self.indexOf(item) === pos;
// });
// return { ...payload, expandedYears };
// }),
return new InitialLoadSuccess({
...INITIAL_DEGREE_PLANNER_STATE,
...payload,
return of(
new PlanError({
message: 'Something went wrong',
duration: 2000,
error,
}),
);
switch$ = this.actions$.pipe(
ofType<SwitchPlan>(PlanActionTypes.SwitchPlan),
withLatestFrom(this.store$.select(selectors.selectAllDegreePlans)),
withLatestFrom(this.store$.select(selectors.selectSubjects)),
withLatestFrom(this.store$.select(selectors.selectActiveTermCodes)),
flatMap(([[[action, allDegreePlans], subjects], activeTermCodes]) => {
const visibleDegreePlan = allDegreePlans.find(plan => {
return plan.roadmapId === action.payload.newVisibleRoadmapId;
}) as DegreePlan;
visibleDegreePlan.roadmapId,
subjects,
activeTermCodes,
return forkJoinWithKeys({
visibleDegreePlan: of(visibleDegreePlan),
const touchedPlan = state.payload.visibleDegreePlan.name;
const message = `Switched to ${touchedPlan}`;
this.snackBar.open(message, undefined, { duration: 2000 });
return of(
new PlanError({
message: 'Unable to switch plan',
duration: 2000,
error,
}),
);
withLatestFrom(this.store$.select(selectors.selectVisibleDegreePlan)),
filter(([_, visibleDegreePlan]) => visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([_action, visibleDegreePlan]) => {
const { roadmapId, name } = visibleDegreePlan as DegreePlan;
if (response === 1) {
return new MakePlanPrimarySuccess();
} else {
return new MakePlanPrimaryFailure();
}
}),
const message = 'This plan has been set as the primary plan';
this.snackBar.open(message, undefined, { duration: 2000 });
}),
catchError(error => {
return of(
new PlanError({
message: 'Unable to make this plan primary',
duration: 2000,
error,
}),
);
}),
withLatestFrom(this.store$.select(selectors.selectAllDegreePlans)),
flatMap(([action, allDegreePlans]) => {
return plan.roadmapId === roadmapId;
}) as DegreePlan;
const oldName = oldDegreePlan.name;
return this.api
.updatePlan(roadmapId, newName, oldDegreePlan.primary)
.pipe(
map(() => {
return new ChangePlanNameSuccess({ roadmapId, newName });
}),
const message = `Plan has been renamed to ${newName}`;
this.snackBar.open(message, undefined, { duration: 2000 });
catchError(() => {
return of(new ChangePlanNameFailure({ roadmapId, oldName }));
}),
);
@Effect()
createPlan$ = this.actions$.pipe(
ofType<CreatePlan>(PlanActionTypes.CreatePlan),
flatMap(action => {
const { name, primary } = action.payload;
return this.api.createDegreePlan(name, primary).pipe(
map(newPlan => new CreatePlanSuccess({ newPlan })),
tap(() => {
const message = `New plan has been created`;
this.snackBar.open(message, undefined, { duration: 2000 });
}),
catchError(error => {
return of(
new PlanError({
message: 'Unable to create new plan',
duration: 2000,
error,
}),
);
}),
);
}),
);
@Effect()
deletePlan$ = this.actions$.pipe(
ofType<DeletePlan>(PlanActionTypes.DeletePlan),
flatMap(action => {
const { roadmapId } = action.payload;
return this.api.deleteDegreePlan(roadmapId).pipe(
map(() => new DeletePlanSuccess({ roadmapId })),
catchError(error => {
return of(
new PlanError({
message: 'Unable to delete plan',
duration: 2000,
error,
}),
);
}),
);
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();
}),
);
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
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;
};
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
const toYearCode = (termCode: string) => {
return termCode.substr(0, 3);
};
const buildTerm = (
yearCode: string,
offset: string,
notes: Note[],
subjects: SubjectMapping,
courses: { termCode: string; courses: CourseBase[] }[],
activeTermCodes: string[],
): PlannedTerm => {
const termCode = yearCode + offset;
const note = notes.find(matchesTermCode(termCode));
const group = courses.find(matchesTermCode(termCode));
const era = pickTermEra(termCode, activeTermCodes);
return {
termCode,
era,
note,
courses: (group ? group.courses : []).map(course => {
return { ...course, termCode, subject: subjects[course.subjectCode] };
}),
};
};
const loadPlanYears = (
api: DegreePlannerApiService,
roadmapId: number,
subjects: SubjectMapping,
activeTermCodes: string[],
): Observable<YearMapping> => {
const notesAndCourses$ = forkJoinWithKeys({
notes: api.getAllNotes(roadmapId),
courses: api.getAllTermCourses(roadmapId),
});
const uniqueYearCodes$ = notesAndCourses$.pipe(
map(({ notes, courses }) => {
const noteTermCodes = notes.map(note => note.termCode);
const courseTermCodes = courses.map(course => course.termCode);
const allTermCodes = [
...noteTermCodes,
...courseTermCodes,
...activeTermCodes,
];
const uniqueYearCodes = unique(allTermCodes.map(toYearCode)).sort();
return {
uniqueYearCodes,
notes,
courses,
};
const visibleYears$ = uniqueYearCodes$.pipe(
map(({ uniqueYearCodes, notes, courses }) => {
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
451
452
453
454
455
456
457
458
459
460
461
462
463
const mapping: YearMapping = {};
uniqueYearCodes.forEach(yearCode => {
mapping[yearCode] = {
yearCode,
isExpanded: utils.pickYearEra(yearCode, activeTermCodes) !== 'past',
fall: buildTerm(
yearCode,
'2',
notes,
subjects,
courses,
activeTermCodes,
),
spring: buildTerm(
yearCode,
'4',
notes,
subjects,
courses,
activeTermCodes,
),
summer: buildTerm(
yearCode,
'6',
notes,
subjects,
courses,
activeTermCodes,
),
};
});
return mapping;
}),
);
return visibleYears$;
};
const pickPrimaryDegreePlan = (plans: DegreePlan[]): DegreePlan => {
const primary = plans.find(plan => plan.primary);
return primary ? primary : plans[0];
};
const checkExpanded = (activeTermCodes, visibleTerms) => {
console.log(visibleTerms);
};
const hasVisibleDegreePlan = <T extends Action>(
pair: [T, DegreePlannerState],
): pair is [T, { visibleDegreePlan: DegreePlan } & DegreePlannerState] => {
return pair[1].visibleDegreePlan !== undefined;
};