From c016b33e003ebd7101fddcb732dc320d132fe8e3 Mon Sep 17 00:00:00 2001 From: ievavold <ievavold@wisc.edu> Date: Tue, 12 Feb 2019 09:59:31 -0600 Subject: [PATCH] ROENROLL-1349 --- src/app/app.module.ts | 8 +- .../degree-planner.component.ts | 6 +- .../notes-dialog/notes-dialog.component.ts | 8 +- .../remove-course-confirm-dialog.component.ts | 4 +- .../favorites-container.component.ts | 8 +- .../store/actions/course.actions.ts | 71 ++++--- .../store/actions/note.actions.ts | 32 +-- .../store/actions/plan.actions.ts | 25 ++- .../store/effects/course.effects.ts | 187 ++++++++---------- .../store/effects/error.effects.ts | 37 ++++ .../store/effects/note.effects.ts | 47 +++-- .../store/effects/plan.effects.ts | 29 ++- src/app/degree-planner/store/reducer.ts | 56 +++--- .../term-container.component.ts | 16 +- src/app/shared/shared.module.ts | 51 ++--- 15 files changed, 332 insertions(+), 253 deletions(-) create mode 100644 src/app/degree-planner/store/effects/error.effects.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 006f8f0..0d71beb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,6 +16,7 @@ import { degreePlannerReducer } from '@app/degree-planner/store/reducer'; import { DegreePlanEffects } from '@app/degree-planner/store/effects/plan.effects'; import { NoteEffects } from '@app/degree-planner/store/effects/note.effects'; import { CourseEffects } from '@app/degree-planner/store/effects/course.effects'; +import { ErrorEffects } from '@app/degree-planner/store/effects/error.effects'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { CourseDetailsDialogComponent } from './degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; @@ -24,7 +25,12 @@ import { CourseDetailsDialogComponent } from './degree-planner/dialogs/course-de StoreModule.forRoot({ degreePlanner: degreePlannerReducer, }), - EffectsModule.forRoot([DegreePlanEffects, NoteEffects, CourseEffects]), + EffectsModule.forRoot([ + DegreePlanEffects, + NoteEffects, + CourseEffects, + ErrorEffects, + ]), BrowserModule, BrowserAnimationsModule, HttpClientModule, diff --git a/src/app/degree-planner/degree-planner.component.ts b/src/app/degree-planner/degree-planner.component.ts index 7ed5ebc..78899b7 100644 --- a/src/app/degree-planner/degree-planner.component.ts +++ b/src/app/degree-planner/degree-planner.component.ts @@ -23,7 +23,7 @@ import { } from '@app/degree-planner/store/selectors'; // Actions -import { ChangeVisiblePlanRequest } from '@app/degree-planner/store/actions/plan.actions'; +import { SwitchPlan } from '@app/degree-planner/store/actions/plan.actions'; import { ModifyPlanDialogComponent } from './dialogs/modify-plan-dialog/modify-plan-dialog.component'; @@ -61,9 +61,7 @@ export class DegreePlannerComponent implements OnInit { public handleDegreePlanChange(event: MatSelectChange): void { if (typeof event.value === 'number') { - this.store.dispatch( - new ChangeVisiblePlanRequest({ newVisibleRoadmapId: event.value }), - ); + this.store.dispatch(new SwitchPlan({ newVisibleRoadmapId: event.value })); } } diff --git a/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts b/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts index e392742..caade24 100644 --- a/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts +++ b/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts @@ -7,8 +7,8 @@ import { Store } from '@ngrx/store'; // State management import { GlobalState } from '@app/core/state'; import { - WriteNoteRequest, - DeleteNoteRequest, + WriteNote, + DeleteNote, } from '@app/degree-planner/store/actions/note.actions'; export type NotesDialogData = @@ -45,13 +45,13 @@ export class NotesDialogComponent implements OnInit { typeof this.form.value.textarea === 'string' ? this.form.value.textarea : ''; - this.store.dispatch(new WriteNoteRequest({ termCode, noteText })); + this.store.dispatch(new WriteNote({ termCode, noteText })); this.dialogRef.close({ event: 'save' }); } public deleteNote() { const termCode = this.data.termCode; - this.store.dispatch(new DeleteNoteRequest({ termCode })); + this.store.dispatch(new DeleteNote({ termCode })); this.dialogRef.close({ event: 'remove' }); } } diff --git a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts index 0674693..a6fbbe6 100644 --- a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts +++ b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts @@ -7,7 +7,7 @@ import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; import { DegreePlannerState } from '@app/degree-planner/store/state'; import { Store } from '@ngrx/store'; -import { RemoveCourseRequest } from '@app/degree-planner/store/actions/course.actions'; +import { RemoveCourse } from '@app/degree-planner/store/actions/course.actions'; @Component({ selector: 'cse-remove-course-confirm-dialog', @@ -43,7 +43,7 @@ export class RemoveCourseConfirmDialogComponent implements OnInit { console.log(this.course); const id = this.course.id; if (typeof id === 'number') { - this.store.dispatch(new RemoveCourseRequest({ recordId: id })); + this.store.dispatch(new RemoveCourse({ recordId: id })); } else { throw new Error('cannot remove a course that does not have an ID'); } diff --git a/src/app/degree-planner/favorites-container/favorites-container.component.ts b/src/app/degree-planner/favorites-container/favorites-container.component.ts index fc688d9..4267330 100644 --- a/src/app/degree-planner/favorites-container/favorites-container.component.ts +++ b/src/app/degree-planner/favorites-container/favorites-container.component.ts @@ -9,8 +9,8 @@ import { Course } from '@app/core/models/course'; // rsjx / ngrx import { DegreePlannerState } from '@app/degree-planner/store/state'; import { - AddSavedForLaterRequest, - RemoveCourseRequest, + AddSaveForLater, + RemoveCourse, } from '@app/degree-planner/store/actions/course.actions'; // Selectors @@ -42,7 +42,7 @@ export class SavedForLaterContainerComponent implements OnInit { const course = event.item.data as Course; this.store.dispatch( - new AddSavedForLaterRequest({ + new AddSaveForLater({ courseId: course.courseId, subjectCode: course.subjectCode, title: course.title, @@ -51,7 +51,7 @@ export class SavedForLaterContainerComponent implements OnInit { ); this.store.dispatch( - new RemoveCourseRequest({ + new RemoveCourse({ recordId: course.id as number, }), ); diff --git a/src/app/degree-planner/store/actions/course.actions.ts b/src/app/degree-planner/store/actions/course.actions.ts index 683ba30..c1e5fbe 100644 --- a/src/app/degree-planner/store/actions/course.actions.ts +++ b/src/app/degree-planner/store/actions/course.actions.ts @@ -2,59 +2,61 @@ import { Action } from '@ngrx/store'; import { Course } from '@app/core/models/course'; export enum CourseActionTypes { - AddCourseRequest = '[Course] Add Course Request', - AddCourseResponse = '[Course] Add Course Response', + AddCourse = '[Course] Add', + AddCourseSuccess = '[Course] Add (Success)', - RemoveCourseRequest = '[Course] Remove Course Request', - RemoveCourseResponse = '[Course] Remove Course Response', + RemoveCourse = '[Course] Remove', + RemoveCourseSuccess = '[Course] Remove (Success)', - ChangeCourseTermRequest = '[Course] Change Course Term Request', - ChangeCourseTermResponse = '[Course] Change Course Term Response', + MoveCourseBetweenTerms = '[Course] Move Between Terms', + MoveCourseBetweenTermsSuccess = '[Course] Move Between Terms (Success)', - AddSavedForLaterRequest = '[Course] Add Saved For Later Request', - AddSavedForLaterResponse = '[Course] Add Saved For Later Response', + AddSaveForLater = '[Course] Add Save for Later', + AddSaveForLaterSuccess = '[Course] Add Save for Later (Success)', - RemoveSavedForLaterRequest = '[Course] Remove Saved For Later Request', - RemoveSavedForLaterResponse = '[Course] Remove Saved For Later Response', + RemoveSaveForLater = '[Course] Remove Save for Later', + RemoveSaveForLaterSuccess = '[Course] Remove Save for Later (Success)', - MoveFromSavedToTermRequest = '[Course] Move Course From Saved to Term Request', - MoveFromSavedToTermResponse = '[Course] Move Course From Saved to Term Response', + MoveSaveForLaterToTerm = '[Course] Save for Later to Term', + MoveSaveForLaterToTermSuccess = '[Course] Save for Later to Term (Success)', + + CourseError = '[Course] Error', } -export class ChangeCourseTermRequest implements Action { - public readonly type = CourseActionTypes.ChangeCourseTermRequest; +export class MoveCourseBetweenTerms implements Action { + public readonly type = CourseActionTypes.MoveCourseBetweenTerms; constructor(public payload: { to: string; from: string; id: number }) {} } -export class ChangeCourseTermResponse implements Action { - public readonly type = CourseActionTypes.ChangeCourseTermResponse; +export class MoveCourseBetweenTermsSuccess implements Action { + public readonly type = CourseActionTypes.MoveCourseBetweenTermsSuccess; constructor(public payload: { to: string; from: string; id: number }) {} } -export class AddCourseRequest implements Action { - public readonly type = CourseActionTypes.AddCourseRequest; +export class AddCourse implements Action { + public readonly type = CourseActionTypes.AddCourse; constructor( public payload: { subjectCode: string; courseId: string; termCode: string }, ) {} } -export class AddCourseResponse implements Action { - public readonly type = CourseActionTypes.AddCourseResponse; +export class AddCourseSuccess implements Action { + public readonly type = CourseActionTypes.AddCourseSuccess; constructor(public payload: { course: Course }) {} } -export class RemoveCourseRequest implements Action { - public readonly type = CourseActionTypes.RemoveCourseRequest; +export class RemoveCourse implements Action { + public readonly type = CourseActionTypes.RemoveCourse; constructor(public payload: { recordId: number }) {} } -export class RemoveCourseResponse implements Action { - public readonly type = CourseActionTypes.RemoveCourseResponse; +export class RemoveCourseSuccess implements Action { + public readonly type = CourseActionTypes.RemoveCourseSuccess; constructor(public payload: { recordId: number }) {} } -export class AddSavedForLaterRequest implements Action { - public readonly type = CourseActionTypes.AddSavedForLaterRequest; +export class AddSaveForLater implements Action { + public readonly type = CourseActionTypes.AddSaveForLater; constructor( public payload: { subjectCode: string; @@ -65,8 +67,8 @@ export class AddSavedForLaterRequest implements Action { ) {} } -export class AddSavedForLaterResponse implements Action { - public readonly type = CourseActionTypes.AddSavedForLaterResponse; +export class AddSaveForLaterSuccess implements Action { + public readonly type = CourseActionTypes.AddSaveForLaterSuccess; constructor( public payload: { subjectCode: string; @@ -77,12 +79,17 @@ export class AddSavedForLaterResponse implements Action { ) {} } -export class RemoveSavedForLaterRequest implements Action { - public readonly type = CourseActionTypes.RemoveSavedForLaterRequest; +export class RemoveSaveForLater implements Action { + public readonly type = CourseActionTypes.RemoveSaveForLater; constructor(public payload: { subjectCode: string; courseId: string }) {} } -export class RemoveSavedForLaterResponse implements Action { - public readonly type = CourseActionTypes.RemoveSavedForLaterResponse; +export class RemoveSaveForLaterSuccess implements Action { + public readonly type = CourseActionTypes.RemoveSaveForLaterSuccess; constructor(public payload: { subjectCode: string; courseId: string }) {} } + +export class CourseError implements Action { + public readonly type = CourseActionTypes.CourseError; + constructor(public payload: { message: string; error: any }) {} +} diff --git a/src/app/degree-planner/store/actions/note.actions.ts b/src/app/degree-planner/store/actions/note.actions.ts index 9c3bba9..7e80e4a 100644 --- a/src/app/degree-planner/store/actions/note.actions.ts +++ b/src/app/degree-planner/store/actions/note.actions.ts @@ -2,28 +2,36 @@ import { Action } from '@ngrx/store'; import { Note } from '@app/core/models/note'; export enum NoteActionTypes { - WriteNoteRequest = '[Note] Write Request', - WriteNoteResponse = '[Note] Write Response', - DeleteNoteRequest = '[Note] Delete Request', - DeleteNoteResponse = '[Note] Delete Response', + WriteNote = '[Note] Write', + WriteNoteSuccess = '[Note] Write (Success)', + + DeleteNote = '[Note] Delete', + DeleteNoteSuccess = '[Note] Delete (Success)', + + NoteError = '[Note] Error', } -export class WriteNoteRequest implements Action { - public readonly type = NoteActionTypes.WriteNoteRequest; +export class WriteNote implements Action { + public readonly type = NoteActionTypes.WriteNote; constructor(public payload: { termCode: string; noteText: string }) {} } -export class WriteNoteResponse implements Action { - public readonly type = NoteActionTypes.WriteNoteResponse; +export class WriteNoteSuccess implements Action { + public readonly type = NoteActionTypes.WriteNoteSuccess; constructor(public payload: { updatedNote: Note }) {} } -export class DeleteNoteRequest implements Action { - public readonly type = NoteActionTypes.DeleteNoteRequest; +export class DeleteNote implements Action { + public readonly type = NoteActionTypes.DeleteNote; constructor(public payload: { termCode: string }) {} } -export class DeleteNoteResponse implements Action { - public readonly type = NoteActionTypes.DeleteNoteResponse; +export class DeleteNoteSuccess implements Action { + public readonly type = NoteActionTypes.DeleteNoteSuccess; constructor(public payload: { termCode: string }) {} } + +export class NoteError implements Action { + public readonly type = NoteActionTypes.NoteError; + constructor(public payload: { message: string; error: any }) {} +} diff --git a/src/app/degree-planner/store/actions/plan.actions.ts b/src/app/degree-planner/store/actions/plan.actions.ts index adf2f83..2d4da0b 100644 --- a/src/app/degree-planner/store/actions/plan.actions.ts +++ b/src/app/degree-planner/store/actions/plan.actions.ts @@ -4,24 +4,26 @@ import { PlannedTerm } from '@app/core/models/planned-term'; import { DegreePlannerState } from '@app/degree-planner/store/state'; export enum PlanActionTypes { - InitialPlanLoadResponse = '[Plan] Initial Load Response', + InitialLoadSuccess = '[Plan] Initial Load (Success)', - ChangeVisiblePlanRequest = '[Plan] Change Visible Request', - ChangeVisiblePlanResponse = '[Plan] Change Visible Response', + SwitchPlan = '[Plan] Switch', + SwitchPlanSuccess = '[Plan] Switch (Success)', + + PlanError = '[Plan] Error', } -export class InitialPlanLoadResponse implements Action { - public readonly type = PlanActionTypes.InitialPlanLoadResponse; +export class InitialLoadSuccess implements Action { + public readonly type = PlanActionTypes.InitialLoadSuccess; constructor(public payload: DegreePlannerState) {} } -export class ChangeVisiblePlanRequest implements Action { - public readonly type = PlanActionTypes.ChangeVisiblePlanRequest; +export class SwitchPlan implements Action { + public readonly type = PlanActionTypes.SwitchPlan; constructor(public payload: { newVisibleRoadmapId: number }) {} } -export class ChangeVisiblePlanResponse implements Action { - public readonly type = PlanActionTypes.ChangeVisiblePlanResponse; +export class SwitchPlanSuccess implements Action { + public readonly type = PlanActionTypes.SwitchPlanSuccess; constructor( public payload: { visibleDegreePlan: DegreePlan; @@ -29,3 +31,8 @@ export class ChangeVisiblePlanResponse implements Action { }, ) {} } + +export class PlanError implements Action { + public readonly type = PlanActionTypes.PlanError; + constructor(public payload: { message: string; error: any }) {} +} diff --git a/src/app/degree-planner/store/effects/course.effects.ts b/src/app/degree-planner/store/effects/course.effects.ts index 3105a0a..b6c0843 100644 --- a/src/app/degree-planner/store/effects/course.effects.ts +++ b/src/app/degree-planner/store/effects/course.effects.ts @@ -1,7 +1,14 @@ // Libraries import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { map, flatMap, withLatestFrom, filter } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { + map, + flatMap, + withLatestFrom, + filter, + catchError, +} from 'rxjs/operators'; import { GlobalState } from '@app/core/state'; import { Store } from '@ngrx/store'; @@ -11,20 +18,23 @@ import { getDegreePlannerState } from '@app/degree-planner/store/selectors'; // Actions import { + AddCourse, + AddCourseSuccess, + AddSaveForLater, + AddSaveForLaterSuccess, CourseActionTypes, - ChangeCourseTermResponse, - AddCourseResponse, - RemoveCourseResponse, - RemoveSavedForLaterResponse, - AddSavedForLaterResponse, - AddCourseRequest, - RemoveCourseRequest, + CourseError, + MoveCourseBetweenTerms, + MoveCourseBetweenTermsSuccess, + RemoveCourse, + RemoveCourseSuccess, + RemoveSaveForLater, + RemoveSaveForLaterSuccess, } from '@app/degree-planner/store/actions/course.actions'; // Models import { DegreePlan } from '@app/core/models/degree-plan'; -import { Course, SubjectMapping, CourseBase } from '@app/core/models/course'; -import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; +import { Course, CourseBase } from '@app/core/models/course'; @Injectable() export class CourseEffects { @@ -36,46 +46,36 @@ export class CourseEffects { @Effect() MoveCourseBetweenTerms$ = this.actions$.pipe( - ofType<any>(CourseActionTypes.ChangeCourseTermRequest), + ofType<MoveCourseBetweenTerms>(CourseActionTypes.MoveCourseBetweenTerms), 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 + const roadmapId = (state.visibleDegreePlan as DegreePlan).roadmapId; + const recordId = action.payload.id; + const termCode = action.payload.to; return this.api - .updateCourseTerm( - (state.visibleDegreePlan as DegreePlan).roadmapId, - action.payload.id, - action.payload.to, - ) - .pipe( - map(response => { - return { - response, - action, - }; - }), - ); + .updateCourseTerm(roadmapId, recordId, termCode) + .pipe(map(() => 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; + map(action => new MoveCourseBetweenTermsSuccess(action.payload)), + + catchError(error => { + return of( + new CourseError({ + message: 'Error moving course', + error, + }), + ); }), ); @Effect() AddCourse$ = this.actions$.pipe( - ofType<AddCourseRequest>(CourseActionTypes.AddCourseRequest), + ofType<AddCourse>(CourseActionTypes.AddCourse), withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => state.visibleDegreePlan !== undefined), @@ -101,16 +101,25 @@ export class CourseEffects { ); const toSuccessAction$ = courseBaseToCourse$.pipe( - map(course => new AddCourseResponse({ course })), + map(course => new AddCourseSuccess({ course })), ); return toSuccessAction$; }), + + catchError(error => { + return of( + new CourseError({ + message: 'Error adding course', + error, + }), + ); + }), ); @Effect() RemoveCourse$ = this.actions$.pipe( - ofType<RemoveCourseRequest>(CourseActionTypes.RemoveCourseRequest), + ofType<RemoveCourse>(CourseActionTypes.RemoveCourse), withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => state.visibleDegreePlan !== undefined), @@ -123,91 +132,65 @@ export class CourseEffects { const removeCourse$ = this.api.removeCourse(roadmapId, recordId); const toSuccessAction$ = removeCourse$.pipe( - map(() => new RemoveCourseResponse({ recordId })), + map(() => new RemoveCourseSuccess({ recordId })), ); return toSuccessAction$; }), + + catchError(error => { + return of( + new CourseError({ + message: 'Error removing course', + error, + }), + ); + }), ); @Effect() RemoveSavedForLater$ = this.actions$.pipe( - ofType<any>(CourseActionTypes.RemoveSavedForLaterRequest), - - withLatestFrom(this.store$.select(getDegreePlannerState)), - filter(([_, state]) => state.visibleDegreePlan !== undefined), + ofType<RemoveSaveForLater>(CourseActionTypes.RemoveSaveForLater), - // Get term data for the degree plan specified by the roadmap ID. - flatMap(([action, state]) => { - // TODO error handle the API calls + flatMap(action => { + const { subjectCode, courseId } = action.payload; return this.api - .removeSavedForLater( - action.payload.subjectCode, - action.payload.courseId, - ) - .pipe( - map(response => { - return { - response, - action, - }; - }), - ); + .removeSavedForLater(subjectCode, courseId) + .pipe(map(() => 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; + map(action => new RemoveSaveForLaterSuccess(action.payload)), + + catchError(error => { + return of( + new CourseError({ + message: 'Error removing saved-for-later course', + error, + }), + ); }), ); @Effect() SaveForLater$ = this.actions$.pipe( - ofType<any>(CourseActionTypes.AddSavedForLaterRequest), - - withLatestFrom(this.store$.select(getDegreePlannerState)), - filter(([_, state]) => state.visibleDegreePlan !== undefined), + ofType<AddSaveForLater>(CourseActionTypes.AddSaveForLater), - // Get term data for the degree plan specified by the roadmap ID. - flatMap(([action, state]) => { - // TODO error handle the API calls + flatMap(action => { + const { subjectCode, courseId } = action.payload; return this.api - .saveForLater(action.payload.subjectCode, action.payload.courseId) - .pipe( - map(response => { - return { - response, - action, - }; - }), - ); + .saveForLater(subjectCode, courseId) + .pipe(map(() => action)); }), - // // // Wrap data in an Action for dispatch - map(({ response, action }) => { - if (response === null) { - return new AddSavedForLaterResponse(action.payload); - } - // return; - return; + map(action => new AddSaveForLaterSuccess(action.payload)), + + catchError(error => { + return of( + new CourseError({ + message: 'Error saving course for later', + 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, - }; - }); - }), - ); - } } diff --git a/src/app/degree-planner/store/effects/error.effects.ts b/src/app/degree-planner/store/effects/error.effects.ts new file mode 100644 index 0000000..c7fa977 --- /dev/null +++ b/src/app/degree-planner/store/effects/error.effects.ts @@ -0,0 +1,37 @@ +import { + PlanError, + PlanActionTypes, +} from '@app/degree-planner/store/actions/plan.actions'; +import { + NoteError, + NoteActionTypes, +} from '@app/degree-planner/store/actions/note.actions'; +import { + CourseError, + CourseActionTypes, +} from '@app/degree-planner/store/actions/course.actions'; +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { tap } from 'rxjs/operators'; +import { MatSnackBar } from '@angular/material'; + +@Injectable() +export class ErrorEffects { + constructor(private actions$: Actions, private snackBar: MatSnackBar) {} + + @Effect({ dispatch: false }) + error$ = this.actions$.pipe( + ofType<PlanError | NoteError | CourseError>( + PlanActionTypes.PlanError, + NoteActionTypes.NoteError, + CourseActionTypes.CourseError, + ), + + tap(action => { + const message = action.payload.message; + const err = action.payload.error; + this.snackBar.open(message); + console.error('(error)', err); + }), + ); +} diff --git a/src/app/degree-planner/store/effects/note.effects.ts b/src/app/degree-planner/store/effects/note.effects.ts index 387f658..a761fe6 100644 --- a/src/app/degree-planner/store/effects/note.effects.ts +++ b/src/app/degree-planner/store/effects/note.effects.ts @@ -3,7 +3,13 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Observable, of } from 'rxjs'; -import { mergeMap, withLatestFrom, map, filter } from 'rxjs/operators'; +import { + mergeMap, + withLatestFrom, + map, + filter, + catchError, +} from 'rxjs/operators'; // Models import { Note } from '@app/core/models/note'; @@ -15,10 +21,11 @@ import { DegreePlannerApiService } from '@app/degree-planner/services/api.servic // State management import { NoteActionTypes, - WriteNoteRequest, - WriteNoteResponse, - DeleteNoteRequest, - DeleteNoteResponse, + WriteNote, + WriteNoteSuccess, + DeleteNote, + DeleteNoteSuccess, + NoteError, } from '@app/degree-planner/store/actions/note.actions'; import { getDegreePlannerState } from '@app/degree-planner/store/selectors'; import { GlobalState } from '@app/core/state'; @@ -33,8 +40,8 @@ export class NoteEffects { ) {} @Effect() - writeNote$: Observable<WriteNoteResponse> = this.actions$.pipe( - ofType<WriteNoteRequest>(NoteActionTypes.WriteNoteRequest), + write$ = this.actions$.pipe( + ofType<WriteNote>(NoteActionTypes.WriteNote), // 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. @@ -63,12 +70,21 @@ export class NoteEffects { // Dispatch an `WriteNoteSuccess` action so that the State // object can be updated with the new Note data. - map(updatedNote => new WriteNoteResponse({ updatedNote })), + map(updatedNote => new WriteNoteSuccess({ updatedNote })), + + catchError(error => { + return of( + new NoteError({ + message: 'Error writing note', + error, + }), + ); + }), ); @Effect() - deleteNote$: Observable<DeleteNoteResponse> = this.actions$.pipe( - ofType<DeleteNoteRequest>(NoteActionTypes.DeleteNoteRequest), + delete$ = this.actions$.pipe( + ofType<DeleteNote>(NoteActionTypes.DeleteNote), // Get the most recent Degree Planner state object. // This is used to lookup the Note ID. @@ -95,7 +111,16 @@ export class NoteEffects { // Dispatch an `DeleteNoteSuccess` action so that the // State object can be updated with the note removed. - map(termCode => new DeleteNoteResponse({ termCode })), + map(termCode => new DeleteNoteSuccess({ termCode })), + + catchError(error => { + return of( + new NoteError({ + message: 'Error deleting note', + error, + }), + ); + }), ); } diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts index a58e72c..bb18281 100644 --- a/src/app/degree-planner/store/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -2,7 +2,7 @@ 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 } from 'rxjs/operators'; +import { map, flatMap, withLatestFrom, catchError } from 'rxjs/operators'; import { GlobalState } from '@app/core/state'; import { Store } from '@ngrx/store'; @@ -12,16 +12,17 @@ import { getDegreePlannerState } from '@app/degree-planner/store/selectors'; // Actions import { - InitialPlanLoadResponse, - ChangeVisiblePlanRequest, - ChangeVisiblePlanResponse, + InitialLoadSuccess, + SwitchPlan, + SwitchPlanSuccess, PlanActionTypes, + PlanError, } 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 { SubjectMapping } from '@app/core/models/course'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; @Injectable() @@ -33,7 +34,7 @@ export class DegreePlanEffects { ) {} @Effect() - init$: Observable<InitialPlanLoadResponse> = this.actions$.pipe( + init$ = this.actions$.pipe( ofType(ROOT_EFFECTS_INIT), // Load the list of degree plans and data used by all degree plans. @@ -70,12 +71,16 @@ export class DegreePlanEffects { }); }), - map(payload => new InitialPlanLoadResponse(payload)), + map(payload => new InitialLoadSuccess(payload)), + + catchError(error => { + return of(new PlanError({ message: '', error })); + }), ); @Effect() - switch$: Observable<ChangeVisiblePlanResponse> = this.actions$.pipe( - ofType<ChangeVisiblePlanRequest>(PlanActionTypes.ChangeVisiblePlanRequest), + switch$ = this.actions$.pipe( + ofType<SwitchPlan>(PlanActionTypes.SwitchPlan), withLatestFrom(this.store$.select(getDegreePlannerState)), @@ -97,7 +102,11 @@ export class DegreePlanEffects { }); }), - map(payload => new ChangeVisiblePlanResponse(payload)), + map(payload => new SwitchPlanSuccess(payload)), + + catchError(error => { + return of(new PlanError({ message: '', error })); + }), ); private loadSavedForLaterCourses(subjects: SubjectMapping) { diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts index e52c70a..72a0960 100644 --- a/src/app/degree-planner/store/reducer.ts +++ b/src/app/degree-planner/store/reducer.ts @@ -4,34 +4,34 @@ import { } from '@app/degree-planner/store/state'; import { PlanActionTypes, - InitialPlanLoadResponse, - ChangeVisiblePlanResponse, + InitialLoadSuccess, + SwitchPlanSuccess, } from '@app/degree-planner/store/actions/plan.actions'; import { CourseActionTypes, - RemoveCourseResponse, - ChangeCourseTermResponse, - AddCourseResponse, - RemoveSavedForLaterResponse, - AddSavedForLaterResponse, + RemoveCourseSuccess, + MoveCourseBetweenTermsSuccess, + AddCourseSuccess, + RemoveSaveForLaterSuccess, + AddSaveForLaterSuccess, } from '@app/degree-planner/store/actions/course.actions'; import { NoteActionTypes, - WriteNoteResponse, - DeleteNoteResponse, + WriteNoteSuccess, + DeleteNoteSuccess, } from '@app/degree-planner/store/actions/note.actions'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; type SupportedActions = - | InitialPlanLoadResponse - | ChangeVisiblePlanResponse - | WriteNoteResponse - | DeleteNoteResponse - | ChangeCourseTermResponse - | RemoveCourseResponse - | AddCourseResponse - | RemoveSavedForLaterResponse - | AddSavedForLaterResponse; + | InitialLoadSuccess + | SwitchPlanSuccess + | WriteNoteSuccess + | DeleteNoteSuccess + | MoveCourseBetweenTermsSuccess + | RemoveCourseSuccess + | AddCourseSuccess + | RemoveSaveForLaterSuccess + | AddSaveForLaterSuccess; export function degreePlannerReducer( state = INITIAL_DEGREE_PLANNER_STATE, @@ -43,17 +43,17 @@ export function degreePlannerReducer( * 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.InitialPlanLoadResponse: { + case PlanActionTypes.InitialLoadSuccess: { return { ...action.payload }; } /** - * The `ChangeVisiblePlanResponse` action is triggered whenever the UI needs + * 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.ChangeVisiblePlanResponse: { + case PlanActionTypes.SwitchPlanSuccess: { return { ...state, ...action.payload }; } @@ -66,7 +66,7 @@ export function degreePlannerReducer( * - *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.WriteNoteResponse: { + case NoteActionTypes.WriteNoteSuccess: { const updatedNote = action.payload.updatedNote; const updatedTermCode = updatedNote.termCode; const originalTerms = state.visibleTerms; @@ -104,7 +104,7 @@ export function degreePlannerReducer( * has been called and it is okay to remote the note with the given * termCode from the degree planner state. */ - case NoteActionTypes.DeleteNoteResponse: { + case NoteActionTypes.DeleteNoteSuccess: { const deletedTermCode = action.payload.termCode; const originalTerms = state.visibleTerms; if (termCodeExists(deletedTermCode, originalTerms)) { @@ -126,7 +126,7 @@ export function degreePlannerReducer( } } - case CourseActionTypes.ChangeCourseTermResponse: { + case CourseActionTypes.MoveCourseBetweenTermsSuccess: { const { to, from, id } = action.payload; const t = state.visibleTerms.find(term => term.termCode === from); @@ -156,7 +156,7 @@ export function degreePlannerReducer( return state; } - case CourseActionTypes.AddCourseResponse: { + case CourseActionTypes.AddCourseSuccess: { const { course } = action.payload; const newVisibleTerms = state.visibleTerms.map(term => { @@ -172,7 +172,7 @@ export function degreePlannerReducer( // return state; } - case CourseActionTypes.RemoveCourseResponse: { + case CourseActionTypes.RemoveCourseSuccess: { const { recordId: id } = action.payload; // Create new visibleTerms array @@ -186,7 +186,7 @@ export function degreePlannerReducer( return { ...state, visibleTerms: newVisibleTerms }; } - case CourseActionTypes.RemoveSavedForLaterResponse: { + case CourseActionTypes.RemoveSaveForLaterSuccess: { const { courseId, subjectCode } = action.payload; // // Create new saved for later array @@ -198,7 +198,7 @@ export function degreePlannerReducer( return { ...state, savedForLaterCourses: newSavedForLater }; } - case CourseActionTypes.AddSavedForLaterResponse: { + case CourseActionTypes.AddSaveForLaterSuccess: { const newSavedForLater: SavedForLaterCourse[] = [ ...state.savedForLaterCourses, { diff --git a/src/app/degree-planner/term-container/term-container.component.ts b/src/app/degree-planner/term-container/term-container.component.ts index 622a506..2f2eb56 100644 --- a/src/app/degree-planner/term-container/term-container.component.ts +++ b/src/app/degree-planner/term-container/term-container.component.ts @@ -13,9 +13,9 @@ import { Observable } from 'rxjs'; import { Store, select } from '@ngrx/store'; import { DegreePlannerState } from '@app/degree-planner/store/state'; import { - ChangeCourseTermRequest, - AddCourseRequest, - RemoveSavedForLaterRequest, + MoveCourseBetweenTerms, + AddCourse, + RemoveSaveForLater, } from '@app/degree-planner/store/actions/course.actions'; // Selectors @@ -90,7 +90,7 @@ export class TermContainerComponent implements OnInit { const { termCode: from, id } = event.item.data; // Dispatch a new change request - this.store.dispatch(new ChangeCourseTermRequest({ to, from, id })); + this.store.dispatch(new MoveCourseBetweenTerms({ to, from, id })); } else if (previousContainer === 'saved-courses') { // If moving from saved courses to term @@ -101,12 +101,8 @@ export class TermContainerComponent implements OnInit { const { subjectCode, courseId } = event.item.data; // Dispatch the add event - this.store.dispatch( - new AddCourseRequest({ subjectCode, courseId, termCode }), - ); - this.store.dispatch( - new RemoveSavedForLaterRequest({ subjectCode, courseId }), - ); + this.store.dispatch(new AddCourse({ subjectCode, courseId, termCode })); + this.store.dispatch(new RemoveSaveForLater({ subjectCode, courseId })); } } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index bb40482..bbe21af 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -15,6 +15,7 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { MatDialogModule } from '@angular/material/dialog'; import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { GetTermDescriptionPipe } from './pipes/get-term-description.pipe'; import { AcademicYearStatePipe } from './pipes/academic-year-state.pipe'; @@ -25,33 +26,35 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { CourseDetailsDialogComponent } from '../degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; const modules = [ - CommonModule, - FormsModule, - ReactiveFormsModule, - MatButtonModule, - MatMenuModule, - MatIconModule, - MatTabsModule, - MatExpansionModule, - MatCardModule, - MatSelectModule, - FlexLayoutModule, - MatSidenavModule, - MatToolbarModule, - MatDialogModule, - MatInputModule, - MatTooltipModule, - MatAutocompleteModule, - MatFormFieldModule + CommonModule, + FormsModule, + ReactiveFormsModule, + MatButtonModule, + MatMenuModule, + MatIconModule, + MatTabsModule, + MatExpansionModule, + MatCardModule, + MatSelectModule, + FlexLayoutModule, + MatSidenavModule, + MatToolbarModule, + MatDialogModule, + MatInputModule, + MatTooltipModule, + MatAutocompleteModule, + MatFormFieldModule, + MatSnackBarModule, ]; const pipes = [ - GetTermDescriptionPipe, AcademicYearStatePipe, AcademicYearRangePipe + GetTermDescriptionPipe, + AcademicYearStatePipe, + AcademicYearRangePipe, ]; @NgModule({ - imports: [ modules ], - exports: [ modules, pipes, CourseDetailsComponent ], - declarations: [ pipes, CourseDetailsComponent, CourseDetailsDialogComponent ] + imports: [modules], + exports: [modules, pipes, CourseDetailsComponent], + declarations: [pipes, CourseDetailsComponent, CourseDetailsDialogComponent], }) - -export class SharedModule { } +export class SharedModule {} -- GitLab