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