From cf6703f70fb7c299321571c8a20aa4eb78a6fe65 Mon Sep 17 00:00:00 2001
From: ievavold <ievavold@wisc.edu>
Date: Tue, 19 Feb 2019 14:31:39 -0600
Subject: [PATCH] ROENROLL-1373

---
 src/app/core/models/planned-term.ts           |  7 +++-
 .../notes-dialog/notes-dialog.component.ts    | 21 ++++++++--
 .../store/actions/note.actions.ts             |  6 ++-
 .../store/effects/note.effects.ts             | 24 ++++-------
 .../store/effects/plan.effects.ts             | 12 ++++--
 src/app/degree-planner/store/reducer.ts       | 42 ++++++++++++++++---
 .../term-container.component.html             | 33 +++++++++------
 .../term-container.component.scss             | 11 +++++
 .../term-container.component.ts               | 23 ++++++----
 src/app/shared/shared.module.ts               |  2 +
 10 files changed, 128 insertions(+), 53 deletions(-)

diff --git a/src/app/core/models/planned-term.ts b/src/app/core/models/planned-term.ts
index 1ec2722..0446370 100644
--- a/src/app/core/models/planned-term.ts
+++ b/src/app/core/models/planned-term.ts
@@ -1,11 +1,14 @@
-import { Note } from '@app/core/models/note';
 import { Course } from '@app/core/models/course';
 
 export type PlannedTermEra = 'past' | 'active' | 'future';
 
+export type PlannedTermNote =
+  | { isLoaded: true; text: string; id: number }
+  | { isLoaded: false; text: string };
+
 export interface PlannedTerm {
   termCode: string;
   era: PlannedTermEra;
-  note?: Note;
+  note?: PlannedTermNote;
   courses: Course[];
 }
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 caade24..c807f0c 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
@@ -12,8 +12,18 @@ import {
 } from '@app/degree-planner/store/actions/note.actions';
 
 export type NotesDialogData =
-  | { termCode: string; hasExistingNote: true; initialText: string }
-  | { termCode: string; hasExistingNote: false; initialText?: never };
+  | {
+      termCode: string;
+      hasExistingNote: true;
+      initialText: string;
+      noteId: number;
+    }
+  | {
+      termCode: string;
+      hasExistingNote: false;
+      initialText?: never;
+      noteId?: never;
+    };
 
 @Component({
   selector: 'cse-notes-dialog',
@@ -51,7 +61,10 @@ export class NotesDialogComponent implements OnInit {
 
   public deleteNote() {
     const termCode = this.data.termCode;
-    this.store.dispatch(new DeleteNote({ termCode }));
-    this.dialogRef.close({ event: 'remove' });
+    if (this.data.noteId !== undefined) {
+      const noteId = this.data.noteId;
+      this.store.dispatch(new DeleteNote({ termCode, noteId }));
+      this.dialogRef.close({ event: 'remove' });
+    }
   }
 }
diff --git a/src/app/degree-planner/store/actions/note.actions.ts b/src/app/degree-planner/store/actions/note.actions.ts
index 48a4db8..567acf6 100644
--- a/src/app/degree-planner/store/actions/note.actions.ts
+++ b/src/app/degree-planner/store/actions/note.actions.ts
@@ -23,7 +23,7 @@ export class WriteNoteSuccess implements Action {
 
 export class DeleteNote implements Action {
   public readonly type = NoteActionTypes.DeleteNote;
-  constructor(public payload: { termCode: string }) {}
+  constructor(public payload: { termCode: string; noteId: number }) {}
 }
 
 export class DeleteNoteSuccess implements Action {
@@ -33,5 +33,7 @@ export class DeleteNoteSuccess implements Action {
 
 export class NoteError implements Action {
   public readonly type = NoteActionTypes.NoteError;
-  constructor(public payload: { message: string; duration: number; error: any }) {}
+  constructor(
+    public payload: { message: string; duration: number; error: any },
+  ) {}
 }
diff --git a/src/app/degree-planner/store/effects/note.effects.ts b/src/app/degree-planner/store/effects/note.effects.ts
index 072a0a7..6768178 100644
--- a/src/app/degree-planner/store/effects/note.effects.ts
+++ b/src/app/degree-planner/store/effects/note.effects.ts
@@ -2,7 +2,7 @@
 import { Injectable } from '@angular/core';
 import { Store } from '@ngrx/store';
 import { Actions, Effect, ofType } from '@ngrx/effects';
-import { Observable, of } from 'rxjs';
+import { of } from 'rxjs';
 import {
   mergeMap,
   withLatestFrom,
@@ -10,6 +10,7 @@ import {
   map,
   filter,
   catchError,
+  delay,
 } from 'rxjs/operators';
 import { MatSnackBar } from '@angular/material';
 
@@ -47,6 +48,8 @@ export class NoteEffects {
   write$ = this.actions$.pipe(
     ofType<WriteNote>(NoteActionTypes.WriteNote),
 
+    delay(5000),
+
     // Get the most recent Degree Planner state object from the store. This is
     // used to decide to fire either the `updateNote` API or `createNote` API.
     withLatestFrom(this.store$.select(selectors.selectVisibleDegreePlan)),
@@ -64,7 +67,7 @@ export class NoteEffects {
       const termCode = action.payload.termCode;
       const noteText = action.payload.noteText;
       const existingNote = getExistingNote(years, termCode);
-      if (existingNote !== undefined) {
+      if (existingNote !== undefined && existingNote.isLoaded) {
         // Since the term DOES have a note, update the existing note
         const noteId = existingNote.id;
         return this.api.updateNote(planId, termCode, noteText, noteId);
@@ -105,22 +108,11 @@ export class NoteEffects {
     // Only handle DeleteNote actions when a current plan ID is set.
     filter(([_, visibleDegreePlan]) => visibleDegreePlan !== undefined),
 
-    withLatestFrom(this.store$.select(selectors.selectAllVisibleYears)),
-
     // Using the action and State objects, fire the `deleteNote` API.
-    mergeMap(([[action, visibleDegreePlan], years]) => {
+    mergeMap(([action, visibleDegreePlan]) => {
       const planId = (visibleDegreePlan as DegreePlan).roadmapId;
-      const termCode = action.payload.termCode;
-      const existingNote = getExistingNote(years, termCode);
-      if (existingNote !== undefined) {
-        // Since the term DOES have a note, delete it
-        const noteId = existingNote.id;
-        return this.api.deleteNote(planId, noteId).pipe(map(() => termCode));
-      } else {
-        // Since the term DID NOT already have a note, return an action that
-        // makes no changes to the current state
-        return of(termCode);
-      }
+      const { termCode, noteId } = action.payload;
+      return this.api.deleteNote(planId, noteId).pipe(map(() => termCode));
     }),
 
     // Dispatch an `DeleteNoteSuccess` action so that the
diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts
index 012d51b..4cacf32 100644
--- a/src/app/degree-planner/store/effects/plan.effects.ts
+++ b/src/app/degree-planner/store/effects/plan.effects.ts
@@ -40,7 +40,7 @@ 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 { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term';
 import { SubjectMapping } from '@app/core/models/course';
 import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
 import {
@@ -341,7 +341,10 @@ const loadPlanTerms = (
   const visibleTerms$ = forkJoin(uniqueTerms$, notesAndTerms$).pipe(
     map(([uniqueTerms, { notes, terms }]) => {
       return uniqueTerms.map(termCode => {
-        const note = notes.find(matchesTermCode(termCode));
+        const baseNote = notes.find(matchesTermCode(termCode));
+        const note: PlannedTermNote | undefined = baseNote
+          ? { isLoaded: true, text: baseNote.note, id: baseNote.id }
+          : undefined;
         const term = terms.find(matchesTermCode(termCode));
         const courses = (term ? term.courses : []).map(course => ({
           ...course,
@@ -396,7 +399,10 @@ const buildTerm = (
   activeTermCodes: string[],
 ): PlannedTerm => {
   const termCode = yearCode + offset;
-  const note = notes.find(matchesTermCode(termCode));
+  const baseNote = notes.find(matchesTermCode(termCode));
+  const note: PlannedTermNote | undefined = baseNote
+    ? { isLoaded: true, text: baseNote.note, id: baseNote.id }
+    : undefined;
   const group = courses.find(matchesTermCode(termCode));
   const era = pickTermEra(termCode, activeTermCodes);
   return {
diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts
index 2f8e88b..02572ba 100644
--- a/src/app/degree-planner/store/reducer.ts
+++ b/src/app/degree-planner/store/reducer.ts
@@ -31,8 +31,9 @@ import {
 } from '@app/degree-planner/store/actions/course.actions';
 import {
   NoteActionTypes,
+  WriteNote,
   WriteNoteSuccess,
-  DeleteNoteSuccess,
+  DeleteNote,
 } from '@app/degree-planner/store/actions/note.actions';
 import {
   AddAcademicYearActionTypes,
@@ -51,7 +52,11 @@ import { DegreePlan } from '@app/core/models/degree-plan';
 import { Year, YearMapping } from '@app/core/models/year';
 import { Course } from '@app/core/models/course';
 import { Note } from '@app/core/models/note';
-import { PlannedTerm, PlannedTermEra } from '@app/core/models/planned-term';
+import {
+  PlannedTerm,
+  PlannedTermEra,
+  PlannedTermNote,
+} from '@app/core/models/planned-term';
 import * as utils from '@app/degree-planner/shared/utils';
 
 type SupportedActions =
@@ -59,8 +64,9 @@ type SupportedActions =
   | InitialLoadSuccess
   | SwitchPlan
   | SwitchPlanSuccess
+  | WriteNote
   | WriteNoteSuccess
-  | DeleteNoteSuccess
+  | DeleteNote
   | MoveCourseInsideTerm
   | MoveCourseBetweenTerms
   | RemoveCourse
@@ -217,6 +223,30 @@ export function degreePlannerReducer(
       };
     }
 
+    case NoteActionTypes.WriteNote: {
+      const { termCode, noteText } = action.payload;
+      const { yearCode, termName } = parseTermCode(termCode);
+      const year = state.visibleYears[yearCode];
+      const existingNote = year ? year[termName].note : undefined;
+
+      const newNote: PlannedTermNote =
+        existingNote && existingNote.isLoaded
+          ? { isLoaded: true, text: noteText, id: existingNote.id }
+          : { isLoaded: false, text: noteText };
+
+      const visibleYears: YearMapping = {
+        ...state.visibleYears,
+        [yearCode]: createYearWithNote(
+          termCode,
+          newNote,
+          state.activeTermCodes,
+          state.visibleYears[yearCode],
+        ),
+      };
+
+      return { ...state, visibleYears };
+    }
+
     /**
      * The `WriteNoteResponse` action is dispatched by the `Note.write$` effect
      * upon a successful response from the `updateNote` or `createNote` API
@@ -235,7 +265,7 @@ export function degreePlannerReducer(
         ...state.visibleYears,
         [yearCode]: createYearWithNote(
           termCode,
-          updatedNote,
+          { isLoaded: true, text: updatedNote.note, id: updatedNote.id },
           state.activeTermCodes,
           state.visibleYears[yearCode],
         ),
@@ -249,7 +279,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.DeleteNoteSuccess: {
+    case NoteActionTypes.DeleteNote: {
       const { termCode } = action.payload;
       const { yearCode } = parseTermCode(termCode);
 
@@ -541,7 +571,7 @@ const generateYearForTermCode = (
 
 const createYearWithNote = (
   termCode: string,
-  note: Note | undefined,
+  note: PlannedTermNote | undefined,
   activeTermCodes: string[],
   year = generateYearForTermCode(termCode, activeTermCodes),
 ): Year => {
diff --git a/src/app/degree-planner/term-container/term-container.component.html b/src/app/degree-planner/term-container/term-container.component.html
index 0f75aab..100368f 100644
--- a/src/app/degree-planner/term-container/term-container.component.html
+++ b/src/app/degree-planner/term-container/term-container.component.html
@@ -4,28 +4,28 @@
       <h2>{{ termCode | getTermDescription }}</h2>
       <div fxLayout="row" fxLayoutAlign="space-between center">
         <p class="text-right semi-bold credits">{{ credits$ | async }} Cr</p>
-        <button mat-icon-button>
-          <ng-container *ngIf="(note$ | async) as note; else newNote">
+        <ng-container *ngIf="(note$ | async) as note; else newNote">
+          <button mat-icon-button [disabled]="note.isLoaded === false" (click)="openNotesDialog(note)">
             <mat-icon
               aria-label="Open dialog with notes in this term"
               color="primary"
               matTooltip="Edit Note"
-              (click)="openNotesDialog(note)"
               matTooltipPosition="above">
               insert_drive_file
             </mat-icon>
-          </ng-container>
-          <ng-template #newNote>
+          </button>
+        </ng-container>
+        <ng-template #newNote>
+          <button mat-icon-button (click)="openNotesDialog()">
             <mat-icon
               aria-label="Open dialog with notes in this term"
               color="primary"
               matTooltip="Add Note"
-              (click)="openNotesDialog()"
               matTooltipPosition="above">
               note_add
             </mat-icon>
-          </ng-template>
-         </button>
+          </button>
+        </ng-template>
       </div>
     </div>
     <div id="term-{{termCode}}" class="term-body" cdkDropList
@@ -35,10 +35,19 @@
 
       <!-- Render term note (if it exists) -->
       <ng-container *ngIf="note$ | async as note">
-        <div class="note-item" (click)="openNotesDialog(note)">
-          <p class="semi-bold">Note</p>
-          <p class="note-excerpt">{{ note.note }}</p>
-        </div>
+        <ng-container *ngIf="note.isLoaded; else noteIsLoading">
+          <div class="note-item" (click)="openNotesDialog(note)">
+            <p class="semi-bold">Note</p>
+            <p class="note-excerpt">{{ note.text }}</p>
+          </div>
+        </ng-container>
+        <ng-template #noteIsLoading>
+          <div class="note-item note-item-loading">
+            <p class="semi-bold">Note</p>
+            <p class="note-excerpt">{{ note.text }}</p>
+            <mat-progress-spinner mode="indeterminate" diameter="24"></mat-progress-spinner>
+          </div>
+        </ng-template>
       </ng-container>
 
       <!-- Render list of courses in this term -->
diff --git a/src/app/degree-planner/term-container/term-container.component.scss b/src/app/degree-planner/term-container/term-container.component.scss
index 7a2fd22..6429a25 100644
--- a/src/app/degree-planner/term-container/term-container.component.scss
+++ b/src/app/degree-planner/term-container/term-container.component.scss
@@ -100,6 +100,17 @@
   }
 }
 
+.note-item-loading {
+  background: #eee;
+  border-color: #888;
+
+  mat-progress-spinner {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+  }
+}
+
 .no-courses {
   text-align: center;
   padding: 25px 0;
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 5874b5b..12b0b87 100644
--- a/src/app/degree-planner/term-container/term-container.component.ts
+++ b/src/app/degree-planner/term-container/term-container.component.ts
@@ -13,7 +13,7 @@ import {
 // Models
 import * as actions from '@app/degree-planner/store/actions/course.actions';
 import * as selectors from '@app/degree-planner/store/selectors';
-import { PlannedTerm } from '@app/core/models/planned-term';
+import { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term';
 import { Note } from '@app/core/models/note';
 import { Course } from '@app/core/models/course';
 import {
@@ -43,7 +43,7 @@ export class TermContainerComponent implements OnInit {
 
   public termCode: string;
   public term$: Observable<PlannedTerm>;
-  public note$: Observable<Note | undefined>;
+  public note$: Observable<PlannedTermNote | undefined>;
   public courses$: Observable<Course[]>;
   public credits$: Observable<number>;
   public isPastTerm$: Observable<boolean>;
@@ -100,12 +100,19 @@ export class TermContainerComponent implements OnInit {
     );
   }
 
-  openNotesDialog(note?: Note) {
-    const termCode = this.termCode;
-    const data: NotesDialogData = note
-      ? { termCode, hasExistingNote: true, initialText: note.note }
-      : { termCode, hasExistingNote: false };
-    this.dialog.open(NotesDialogComponent, { data });
+  openNotesDialog(note?: PlannedTermNote) {
+    if (note === undefined || note.isLoaded) {
+      const termCode = this.termCode;
+      const data: NotesDialogData = note
+        ? {
+            termCode,
+            hasExistingNote: true,
+            initialText: note.text,
+            noteId: note.id,
+          }
+        : { termCode, hasExistingNote: false };
+      this.dialog.open(NotesDialogComponent, { data });
+    }
   }
 
   openCourseSearch() {
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 4eea8fc..1f5827e 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -28,6 +28,7 @@ import { CourseDetailsDialogComponent } from '../degree-planner/dialogs/course-d
 
 import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
 import { PromptDialogComponent } from './dialogs/prompt-dialog/prompt-dialog.component';
+import { MatProgressSpinnerModule } from '@angular/material';
 
 const modules = [
   CommonModule,
@@ -50,6 +51,7 @@ const modules = [
   MatFormFieldModule,
   MatSnackBarModule,
   MatProgressBarModule,
+  MatProgressSpinnerModule,
 ];
 const pipes = [
   GetTermDescriptionPipe,
-- 
GitLab