Forked from an inaccessible project.
-
Isaac Evavold authoredIsaac Evavold authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
plan.effects.ts 13.78 KiB
// Libraries
import { Injectable } from '@angular/core';
import { ROOT_EFFECTS_INIT, Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, forkJoin, of } from 'rxjs';
import {
tap,
map,
flatMap,
withLatestFrom,
catchError,
filter,
} from 'rxjs/operators';
import { GlobalState } from '@app/core/state';
import { Store, Action } from '@ngrx/store';
import { MatSnackBar } from '@angular/material';
// Services
import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
import * as selectors from '@app/degree-planner/store/selectors';
// Actions
import {
InitialLoadSuccess,
SwitchPlan,
SwitchPlanSuccess,
PlanActionTypes,
PlanError,
MakePlanPrimary,
MakePlanPrimarySuccess,
MakePlanPrimaryFailure,
ChangePlanName,
ChangePlanNameSuccess,
ChangePlanNameFailure,
CreatePlan,
CreatePlanSuccess,
DeletePlan,
DeletePlanSuccess,
} from '@app/degree-planner/store/actions/plan.actions';
import * as utils from '@app/degree-planner/shared/utils';
// Models
import { DegreePlan } from '@app/core/models/degree-plan';
import { PlannedTerm, PlannedTermEra } from '@app/core/models/planned-term';
import { SubjectMapping } from '@app/core/models/course';
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>,
private snackBar: MatSnackBar,
) {}
@Effect()
init$ = 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(),
subjectDescriptions: this.api.getAllSubjectDescriptions(),
activeTermCodes,
});
}),
// 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 };
// }),
map(payload => {
return new InitialLoadSuccess({
...INITIAL_DEGREE_PLANNER_STATE,
...payload,
isLoadingPlan: false,
});
}),
catchError(error => {
return of(
new PlanError({
message: 'Something went wrong',
duration: 2000,
error,
}),
);
}),
);
@Effect()
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;
const visibleYears = loadPlanYears(
this.api,
visibleDegreePlan.roadmapId,
subjects,
activeTermCodes,
);
return forkJoinWithKeys({
visibleDegreePlan: of(visibleDegreePlan),
visibleYears,
});
}),
map(payload => new SwitchPlanSuccess(payload)),
tap(state => {
const touchedPlan = state.payload.visibleDegreePlan.name;
const message = `Switched to ${touchedPlan}`;
this.snackBar.open(message, undefined, { duration: 2000 });
}),
catchError(error => {
return of(
new PlanError({
message: 'Unable to switch plan',
duration: 2000,
error,
}),
);
}),
);
@Effect()
MakePlanPrimary$ = this.actions$.pipe(
ofType<MakePlanPrimary>(PlanActionTypes.MakePlanPrimary),
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;
return this.api.updatePlan(roadmapId, name, true);
}),
// // Wrap data in an Action for dispatch
map(response => {
if (response === 1) {
return new MakePlanPrimarySuccess();
} else {
return new MakePlanPrimaryFailure();
}
}),
tap(() => {
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,
}),
);
}),
);
@Effect()
ChangePlanName$ = this.actions$.pipe(
ofType<ChangePlanName>(PlanActionTypes.ChangePlanName),
withLatestFrom(this.store$.select(selectors.selectAllDegreePlans)),
flatMap(([action, allDegreePlans]) => {
const { roadmapId, newName } = action.payload;
const oldDegreePlan = allDegreePlans.find(plan => {
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 });
}),
tap(() => {
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();
}),
);
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 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 }) => {
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;
};