diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4501fdf66bc70fe63cd767152d57724b9e28bf44..d1da7959558ac4be0af32a2eb8910b89e547c67a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,26 +3,26 @@ import { Component, ViewChild, OnInit } from '@angular/core'; import { MatSidenav } from '@angular/material'; @Component({ - selector: 'cse-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + selector: 'cse-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], }) export class AppComponent implements OnInit { - @ViewChild('rightAddCourse') public rightAddCourse: MatSidenav; - constructor(private sidenavService: SidenavService) { } + @ViewChild('rightAddCourse') public rightAddCourse: MatSidenav; + constructor(private sidenavService: SidenavService) {} - ngOnInit() { - this.sidenavService.setSidenav(this.rightAddCourse); - } + ngOnInit() { + this.sidenavService.setSidenav(this.rightAddCourse); + } } document.addEventListener('WebComponentsReady', function() { - const customEvent = new CustomEvent('myuw-login', { - detail: { - person: { - // 'firstName': 'Bucky' - } - } - }); - document.dispatchEvent(customEvent); + const customEvent = new CustomEvent('myuw-login', { + detail: { + person: { + // 'firstName': 'Bucky' + }, + }, + }); + document.dispatchEvent(customEvent); }); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 75bdd82109c5dce49f8b8ac600c75ba2d70239e6..83f523b88a51128a4426c1c92afabaea61418bdd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,32 +12,29 @@ import { CoreModule } from '@app/core/core.module'; import { SharedModule } from '@app/shared/shared.module'; import { HeaderComponent } from './core/header/header.component'; import { SidenavService } from './core/service/sidenav.service'; -import { degreePlannerReducer } from '@app/degree-planner/reducer'; -import { DegreePlanEffects } from '@app/degree-planner/effects/plan.effects'; -import { NoteEffects } from '@app/degree-planner/effects/note.effects'; +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'; @NgModule({ - imports: [ - StoreModule.forRoot({ - degreePlanner: degreePlannerReducer - }), - EffectsModule.forRoot([DegreePlanEffects, NoteEffects]), - BrowserModule, - BrowserAnimationsModule, - HttpClientModule, - CoreModule, - SharedModule, - AppRoutingModule, - StoreDevtoolsModule.instrument({ - maxAge: 5 - }), - ], - declarations: [ - AppComponent, - HeaderComponent - ], - providers: [ SidenavService ], - bootstrap: [ AppComponent ], - schemas: [ CUSTOM_ELEMENTS_SCHEMA ] + imports: [ + StoreModule.forRoot({ + degreePlanner: degreePlannerReducer, + }), + EffectsModule.forRoot([DegreePlanEffects, NoteEffects]), + BrowserModule, + BrowserAnimationsModule, + HttpClientModule, + CoreModule, + SharedModule, + AppRoutingModule, + StoreDevtoolsModule.instrument({ + maxAge: 5, + }), + ], + declarations: [AppComponent, HeaderComponent], + providers: [SidenavService], + bootstrap: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/app.routing.module.ts b/src/app/app.routing.module.ts index c4fc6c324731215b8e9af4bcd81a32bdea30490a..585d241f00794e89e229f17e5c2127acb077c139 100644 --- a/src/app/app.routing.module.ts +++ b/src/app/app.routing.module.ts @@ -1,17 +1,15 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { AppComponent } from './app.component'; - const routes: Routes = [ - { - path: '', - loadChildren: './degree-planner/degree-planner.module#DegreePlannerModule' - } + { + path: '', + loadChildren: './degree-planner/degree-planner.module#DegreePlannerModule', + }, ]; @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], }) -export class AppRoutingModule { } +export class AppRoutingModule {} diff --git a/src/app/core/state.ts b/src/app/core/state.ts index 51a17639c2f13161ce526b8b3b69661ad5a53a30..a4d7e03c8d375ca1f3d6ec0fff176904974eef04 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -1,5 +1,5 @@ -import { DegreePlannerState } from '@app/degree-planner/state'; +import { DegreePlannerState } from '@app/degree-planner/store/state'; export interface GlobalState { - degreePlanner: DegreePlannerState; + degreePlanner: DegreePlannerState; } diff --git a/src/app/degree-planner/actions/note.actions.ts b/src/app/degree-planner/actions/note.actions.ts deleted file mode 100644 index 8397c027ecdf2ba5e70e64707219f2e7383f5db9..0000000000000000000000000000000000000000 --- a/src/app/degree-planner/actions/note.actions.ts +++ /dev/null @@ -1,29 +0,0 @@ -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' -} - -export class WriteNoteRequest implements Action { - public readonly type = NoteActionTypes.WriteNoteRequest; - constructor(public payload: { termCode: string; noteText: string }) {} -} - -export class WriteNoteResponse implements Action { - public readonly type = NoteActionTypes.WriteNoteResponse; - constructor(public payload: { updatedNote: Note }) {} -} - -export class DeleteNoteRequest implements Action { - public readonly type = NoteActionTypes.DeleteNoteRequest; - constructor(public payload: { termCode: string }) {} -} - -export class DeleteNoteResponse implements Action { - public readonly type = NoteActionTypes.DeleteNoteResponse; - constructor(public payload: { termCode: string }) {} -} diff --git a/src/app/degree-planner/degree-planner.component.ts b/src/app/degree-planner/degree-planner.component.ts index 58d2f4d8c3712c7e2e9b29a42850a03f5de2876b..444465958f2f3a0b0021977b46f65e99200aa924 100644 --- a/src/app/degree-planner/degree-planner.component.ts +++ b/src/app/degree-planner/degree-planner.component.ts @@ -18,10 +18,10 @@ import { getVisibleRoadmapId, firstActiveTermCode, getAllVisibleTermsByYear, -} from '@app/degree-planner/selectors'; +} from '@app/degree-planner/store/selectors'; // Actions -import { ChangeVisiblePlanRequest } from '@app/degree-planner/actions/plan.actions'; +import { ChangeVisiblePlanRequest } from '@app/degree-planner/store/actions/plan.actions'; @Component({ selector: 'cse-degree-planner', diff --git a/src/app/degree-planner/degree-planner.module.ts b/src/app/degree-planner/degree-planner.module.ts index 5600c87d3fd5764b8eb6c4e82cf25aab49cfa99e..cd9a4a46a533f8f11de32cf1d94473ea7ad9f9ed 100644 --- a/src/app/degree-planner/degree-planner.module.ts +++ b/src/app/degree-planner/degree-planner.module.ts @@ -13,22 +13,22 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { RemoveCourseConfirmDialogComponent } from './dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component'; @NgModule({ - imports: [SharedModule, DragDropModule, DegreePlannerRoutingModule], - exports: [DragDropModule], - declarations: [ - DegreePlannerComponent, - TermContainerComponent, - CourseItemComponent, - SidenavMenuItemComponent, - SavedForLaterContainerComponent, - CourseDetailsDialogComponent, - NotesDialogComponent, - RemoveCourseConfirmDialogComponent - ], - entryComponents: [ - CourseDetailsDialogComponent, - NotesDialogComponent, - RemoveCourseConfirmDialogComponent - ] + imports: [SharedModule, DragDropModule, DegreePlannerRoutingModule], + exports: [DragDropModule], + declarations: [ + DegreePlannerComponent, + TermContainerComponent, + CourseItemComponent, + SidenavMenuItemComponent, + SavedForLaterContainerComponent, + CourseDetailsDialogComponent, + NotesDialogComponent, + RemoveCourseConfirmDialogComponent, + ], + entryComponents: [ + CourseDetailsDialogComponent, + NotesDialogComponent, + RemoveCourseConfirmDialogComponent, + ], }) export class DegreePlannerModule {} diff --git a/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.html b/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.html deleted file mode 100644 index 35b902da77b74d0a7cb4c55c9323ed7233e7d7ae..0000000000000000000000000000000000000000 --- a/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.html +++ /dev/null @@ -1,8 +0,0 @@ -<mat-toolbar color="primary" class="dialog-toolbar"> - <span class="dialog-toolbar-title">Course Details</span> - <button mat-button mat-dialog-close class="close-btn"><i class="material-icons">clear</i></button> -</mat-toolbar> - -<mat-dialog-content class="mat-typography dialog-with-toolbar"> - <cse-course-details></cse-course-details> -</mat-dialog-content> \ No newline at end of file diff --git a/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.scss b/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.scss deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.spec.ts b/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.spec.ts deleted file mode 100644 index 6a866893a532e50e90de66e204b56d2d78a26aa5..0000000000000000000000000000000000000000 --- a/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -// import { SharedModule } from '@app/shared/shared.module'; -// import { CoreModule } from '@app/core/core.module'; -// import { DataService } from '@app/core/data.service'; -// import { HttpClient } from '@angular/common/http'; -// import { HttpHandler } from '@angular/common/http'; -// import { CourseDetailsDialogComponent } from './course-details-dialog.component'; -// import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; - -// describe('CourseDetailsDialogComponent', () => { -// let component: CourseDetailsDialogComponent; -// let fixture: ComponentFixture<CourseDetailsDialogComponent>; - -// beforeEach(async(() => { -// TestBed.configureTestingModule({ -// imports: [CoreModule, SharedModule], -// declarations: [ CourseDetailsDialogComponent ], -// providers: [DataService, HttpClient, HttpHandler, { provide: MatDialogRef, useValue: {} }, { provide: MAT_DIALOG_DATA, useValue: {} }] -// }) -// .compileComponents(); -// })); - -// beforeEach(() => { -// fixture = TestBed.createComponent(CourseDetailsDialogComponent); -// component = fixture.componentInstance; -// fixture.detectChanges(); -// }); - -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); diff --git a/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.ts b/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.ts index 782169c17e32626a762d5d248895fcabb3044181..8d3701f4de013149192205840d03c0026dbe944e 100644 --- a/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.ts +++ b/src/app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component.ts @@ -1,15 +1,22 @@ -import { CourseDetails } from './../../../core/models/course-details'; -import { Component, OnInit, Inject } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; @Component({ - selector: 'cse-course-details-dialog', - templateUrl: './course-details-dialog.component.html', - styleUrls: ['./course-details-dialog.component.scss'] -}) + selector: 'cse-course-details-dialog', + template: ` + <mat-toolbar color="primary" class="dialog-toolbar"> + <span class="dialog-toolbar-title">Course Details</span> + <button mat-button mat-dialog-close class="close-btn"> + <i class="material-icons">clear</i> + </button> + </mat-toolbar> + <mat-dialog-content class="mat-typography dialog-with-toolbar"> + <cse-course-details></cse-course-details> + </mat-dialog-content> + `, +}) export class CourseDetailsDialogComponent implements OnInit { - constructor() {} + constructor() {} - ngOnInit() { - } + ngOnInit() {} } 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 0896ccee413f4886cff5c14fddc033ab3d2e3609..e3927428861dd8bbacf8038ee308202dac501a66 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,51 +7,51 @@ import { Store } from '@ngrx/store'; // State management import { GlobalState } from '@app/core/state'; import { - WriteNoteRequest, - DeleteNoteRequest -} from '@app/degree-planner/actions/note.actions'; + WriteNoteRequest, + DeleteNoteRequest, +} 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 } + | { termCode: string; hasExistingNote: false; initialText?: never }; @Component({ - selector: 'cse-notes-dialog', - templateUrl: './notes-dialog.component.html', - styleUrls: ['./notes-dialog.component.scss'] + selector: 'cse-notes-dialog', + templateUrl: './notes-dialog.component.html', + styleUrls: ['./notes-dialog.component.scss'], }) export class NotesDialogComponent implements OnInit { - public form: FormGroup; - public MAX_LENGTH = 512; + public form: FormGroup; + public MAX_LENGTH = 512; - constructor( - private store: Store<GlobalState>, - private dialogRef: MatDialogRef<NotesDialogComponent>, - @Inject(MAT_DIALOG_DATA) public data: NotesDialogData - ) {} + constructor( + private store: Store<GlobalState>, + private dialogRef: MatDialogRef<NotesDialogComponent>, + @Inject(MAT_DIALOG_DATA) public data: NotesDialogData, + ) {} - public ngOnInit() { - this.form = new FormGroup({ - textarea: new FormControl(this.data.initialText || '', [ - Validators.maxLength(this.MAX_LENGTH), - Validators.required - ]) - }); - } + public ngOnInit() { + this.form = new FormGroup({ + textarea: new FormControl(this.data.initialText || '', [ + Validators.maxLength(this.MAX_LENGTH), + Validators.required, + ]), + }); + } - public writeNote() { - const termCode = this.data.termCode; - const noteText = - typeof this.form.value.textarea === 'string' - ? this.form.value.textarea - : ''; - this.store.dispatch(new WriteNoteRequest({ termCode, noteText })); - this.dialogRef.close({ event: 'save' }); - } + public writeNote() { + const termCode = this.data.termCode; + const noteText = + typeof this.form.value.textarea === 'string' + ? this.form.value.textarea + : ''; + this.store.dispatch(new WriteNoteRequest({ termCode, noteText })); + this.dialogRef.close({ event: 'save' }); + } - public deleteNote() { - const termCode = this.data.termCode; - this.store.dispatch(new DeleteNoteRequest({ termCode })); - this.dialogRef.close({ event: 'remove' }); - } + public deleteNote() { + const termCode = this.data.termCode; + this.store.dispatch(new DeleteNoteRequest({ 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 1381509c1ffcbbda32595b35e7bfdaba389d3583..d44598736a17cff3b249a6c1741c22aeead62541 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 @@ -4,10 +4,10 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; import { Course } from '@app/core/models/course'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; -import { DegreePlannerState } from '@app/degree-planner/state'; +import { DegreePlannerState } from '@app/degree-planner/store/state'; import { Store } from '@ngrx/store'; -import { RemoveCourseRequest } from '@app/degree-planner/actions/plan.actions'; +import { RemoveCourseRequest } from '@app/degree-planner/store/actions/plan.actions'; @Component({ selector: 'cse-remove-course-confirm-dialog', diff --git a/src/app/degree-planner/effects/note.effects.ts b/src/app/degree-planner/effects/note.effects.ts deleted file mode 100644 index 5dd812a7215bcd6b27dc461346ce15a0535551cf..0000000000000000000000000000000000000000 --- a/src/app/degree-planner/effects/note.effects.ts +++ /dev/null @@ -1,108 +0,0 @@ -// Libraries -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'; - -// Models -import { Note } from '@app/core/models/note'; - -// Services -import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; - -// State management -import { - NoteActionTypes, - WriteNoteRequest, - WriteNoteResponse, - DeleteNoteRequest, - DeleteNoteResponse -} from '@app/degree-planner/actions/note.actions'; -import { getDegreePlannerState } from '@app/degree-planner/selectors'; -import { GlobalState } from '@app/core/state'; -import { DegreePlannerState } from '@app/degree-planner/state'; - -@Injectable() -export class NoteEffects { - constructor( - private actions$: Actions, - private store$: Store<GlobalState>, - private api: DegreePlannerApiService - ) {} - - @Effect() - writeNote$: Observable<WriteNoteResponse> = this.actions$.pipe( - ofType<WriteNoteRequest>(NoteActionTypes.WriteNoteRequest), - - // 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(getDegreePlannerState)), - - // Only handle WriteNote actions when a current plan ID is set. - filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), - - // Using the action and State objects, determine whether to fire the - // `updateNote` or `createNote` API. Both of these API calls return - // an observable wrapper around the modified/created Note object. - mergeMap(([action, state]) => { - const planId = state.visibleRoadmapId as number; - const termCode = action.payload.termCode; - const noteText = action.payload.noteText; - const existingNote = getExistingNote(state, termCode); - if (existingNote !== undefined) { - // Since the term DOES have a note, update the existing note - const noteId = existingNote.id; - return this.api.updateNote(planId, termCode, noteText, noteId); - } else { - // Since the term DOES NOT have a note, create a new note - return this.api.createNote(planId, termCode, noteText); - } - }), - - // Dispatch an `WriteNoteSuccess` action so that the State - // object can be updated with the new Note data. - map(updatedNote => new WriteNoteResponse({ updatedNote })) - ); - - @Effect() - deleteNote$: Observable<DeleteNoteResponse> = this.actions$.pipe( - ofType<DeleteNoteRequest>(NoteActionTypes.DeleteNoteRequest), - - // Get the most recent Degree Planner state object. - // This is used to lookup the Note ID. - withLatestFrom(this.store$.select(getDegreePlannerState)), - - // Only handle DeleteNote actions when a current plan ID is set. - filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), - - // Using the action and State objects, fire the `deleteNote` API. - mergeMap(([action, state]) => { - const planId = state.visibleRoadmapId as number; - const termCode = action.payload.termCode; - const existingNote = getExistingNote(state, 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); - } - }), - - // Dispatch an `DeleteNoteSuccess` action so that the - // State object can be updated with the note removed. - map(termCode => new DeleteNoteResponse({ termCode })) - ); -} - -const getExistingNote = (state: DegreePlannerState, termCode: string) => { - for (const term of state.visibleTerms || []) { - if (term.termCode === termCode) { - return term.note; - } - } - return undefined; -}; 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 3e82e551bf84298813ea6299a99f1ad799a5f06d..a0a4e5b5f0cc4b7c638a11471aabdba1484b2c19 100644 --- a/src/app/degree-planner/favorites-container/favorites-container.component.ts +++ b/src/app/degree-planner/favorites-container/favorites-container.component.ts @@ -3,49 +3,49 @@ import { Observable } from 'rxjs'; import { Store, select } from '@ngrx/store'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; import { GlobalState } from '@app/core/state'; -import { getSavedForLaterCourses } from '@app/degree-planner/selectors'; +import { getSavedForLaterCourses } from '@app/degree-planner/store/selectors'; // rsjx / ngrx -import { DegreePlannerState } from '@app/degree-planner/state'; +import { DegreePlannerState } from '@app/degree-planner/store/state'; import { - AddSavedForLaterRequest, RemoveCourseRequest -} from '@app/degree-planner/actions/plan.actions'; + AddSavedForLaterRequest, + RemoveCourseRequest, +} from '@app/degree-planner/store/actions/plan.actions'; // Selectors -import { - getDropZones -} from '@app/degree-planner/selectors'; +import { getDropZones } from '@app/degree-planner/store/selectors'; @Component({ - selector: 'cse-favorites-container', - templateUrl: './favorites-container.component.html', - styleUrls: ['./favorites-container.component.scss'] + selector: 'cse-favorites-container', + templateUrl: './favorites-container.component.html', + styleUrls: ['./favorites-container.component.scss'], }) export class SavedForLaterContainerComponent implements OnInit { - public courses$: Observable<SavedForLaterCourse[]>; - public dropZones$: Observable<String[]>; - - constructor(private store: Store<{ degreePlanner: DegreePlannerState }>) {} - - public ngOnInit() { - this.dropZones$ = this.store.pipe(select(getDropZones)); - this.courses$ = this.store.pipe(select(getSavedForLaterCourses)); - } - - drop(event) { - const newContainer = event.container.id; - const previousContainer = event.previousContainer.id; - - if (newContainer === previousContainer) { - // If the user dropped a course into the same container do nothing - return; - - } else if (previousContainer.indexOf('term-') === 0) { - // If moving from term to to saved for later - - const {subjectCode, courseId, id} = event.item.data; - this.store.dispatch(new AddSavedForLaterRequest({subjectCode, courseId})); - this.store.dispatch(new RemoveCourseRequest({id})); - } - } + public courses$: Observable<SavedForLaterCourse[]>; + public dropZones$: Observable<String[]>; + + constructor(private store: Store<{ degreePlanner: DegreePlannerState }>) {} + + public ngOnInit() { + this.dropZones$ = this.store.pipe(select(getDropZones)); + this.courses$ = this.store.pipe(select(getSavedForLaterCourses)); + } + + drop(event) { + const newContainer = event.container.id; + const previousContainer = event.previousContainer.id; + + if (newContainer === previousContainer) { + // If the user dropped a course into the same container do nothing + return; + } else if (previousContainer.indexOf('term-') === 0) { + // If moving from term to to saved for later + + const { subjectCode, courseId, id } = event.item.data; + this.store.dispatch( + new AddSavedForLaterRequest({ subjectCode, courseId }), + ); + this.store.dispatch(new RemoveCourseRequest({ id })); + } + } } diff --git a/src/app/degree-planner/services/api.service.ts b/src/app/degree-planner/services/api.service.ts index af4f561d3d52220baa7667f5b37ef03815b11741..d1fbfde8fcde6b9112bdaaccb747e60981f4a076 100644 --- a/src/app/degree-planner/services/api.service.ts +++ b/src/app/degree-planner/services/api.service.ts @@ -1,3 +1,4 @@ +import { CourseDetails } from './../../core/models/course-details'; // Libraries import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; @@ -51,6 +52,16 @@ export class DegreePlannerApiService { return this.http.get<Note[]>(this.degreePlanEndpoint(roadmapId, 'notes')); } + getCourseDetails( + subjectCode: string, + courseId: string, + ): Observable<CourseDetails[]> { + return this.http.get<CourseDetails[]>( + this.config.apiSearchUrl + '/course/0000/' + subjectCode + '/' + courseId, + HTTP_OPTIONS, + ); + } + public getAllTermCourses( roadmapId: number, ): Observable<{ termCode: string; courses: Course[] }[]> { diff --git a/src/app/degree-planner/shared/course-item/course-item.component.ts b/src/app/degree-planner/shared/course-item/course-item.component.ts index cb84c36a3e3ff9c1f4c68a1c5a8e910c80896981..9c19e2d8b2ea7e32de5330a61a74cb8844e4d5d4 100644 --- a/src/app/degree-planner/shared/course-item/course-item.component.ts +++ b/src/app/degree-planner/shared/course-item/course-item.component.ts @@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material'; import { Course } from '@app/core/models/course'; +import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; // tslint:disable-next-line:max-line-length import { CourseDetailsDialogComponent } from '@app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; // tslint:disable-next-line:max-line-length @@ -14,19 +15,18 @@ import { RemoveCourseConfirmDialogComponent } from '@app/degree-planner/dialogs/ styleUrls: ['./course-item.component.scss'], }) export class CourseItemComponent implements OnInit { - dataService; // TODO refactor to use api service status; // TODO make this work k thx plz!? @Input() course: Course; @Input() disabled: string; @Input() type: 'saved' | 'course' | 'search'; - constructor(public dialog: MatDialog) {} + constructor(private api: DegreePlannerApiService, public dialog: MatDialog) {} ngOnInit() {} openCourseDetailsDialog(course) { - this.dataService - .getCourseDetails(course.termCode, course.subjectCode, course.courseId) + this.api + .getCourseDetails(course.subjectCode, course.courseId) .subscribe(courseDetails => { const dialogRef = this.dialog.open(CourseDetailsDialogComponent, { data: { courseDetails: courseDetails }, diff --git a/src/app/degree-planner/sidenav-menu-item/sidenav-menu-item.component.ts b/src/app/degree-planner/sidenav-menu-item/sidenav-menu-item.component.ts index 2aa55f20a3fa0bb1cf66889e269df4cd5edfd495..32afc442de96078b5c80ea8a5bc0e685374a2445 100644 --- a/src/app/degree-planner/sidenav-menu-item/sidenav-menu-item.component.ts +++ b/src/app/degree-planner/sidenav-menu-item/sidenav-menu-item.component.ts @@ -1,19 +1,12 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; @Component({ - selector: 'cse-sidenav-menu-item', - templateUrl: './sidenav-menu-item.component.html', - styleUrls: ['./sidenav-menu-item.component.scss'] + selector: 'cse-sidenav-menu-item', + templateUrl: './sidenav-menu-item.component.html', + styleUrls: ['./sidenav-menu-item.component.scss'], }) export class SidenavMenuItemComponent implements OnInit { - events: string[] = []; - opened: boolean; - @Input() favoriteDropZone: []; - // @Input() termsByAcademicYear: Object; - // @Input() favoriteCourses; + constructor() {} - constructor() { } - - ngOnInit() { - } + ngOnInit() {} } diff --git a/src/app/degree-planner/store/actions/note.actions.ts b/src/app/degree-planner/store/actions/note.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c3bba9be7e8f1b86fde6fdc272e6fd1fe546f40 --- /dev/null +++ b/src/app/degree-planner/store/actions/note.actions.ts @@ -0,0 +1,29 @@ +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', +} + +export class WriteNoteRequest implements Action { + public readonly type = NoteActionTypes.WriteNoteRequest; + constructor(public payload: { termCode: string; noteText: string }) {} +} + +export class WriteNoteResponse implements Action { + public readonly type = NoteActionTypes.WriteNoteResponse; + constructor(public payload: { updatedNote: Note }) {} +} + +export class DeleteNoteRequest implements Action { + public readonly type = NoteActionTypes.DeleteNoteRequest; + constructor(public payload: { termCode: string }) {} +} + +export class DeleteNoteResponse implements Action { + public readonly type = NoteActionTypes.DeleteNoteResponse; + constructor(public payload: { termCode: string }) {} +} diff --git a/src/app/degree-planner/actions/plan.actions.ts b/src/app/degree-planner/store/actions/plan.actions.ts similarity index 98% rename from src/app/degree-planner/actions/plan.actions.ts rename to src/app/degree-planner/store/actions/plan.actions.ts index c919236b6068ce904088169377995dd760b6cf52..7095e93cb7cc832bf6aacfc30065f7b74905a99e 100644 --- a/src/app/degree-planner/actions/plan.actions.ts +++ b/src/app/degree-planner/store/actions/plan.actions.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { PlannedTerm } from '@app/core/models/planned-term'; -import { DegreePlannerState } from '@app/degree-planner/state'; +import { DegreePlannerState } from '@app/degree-planner/store/state'; import { Course } from '@app/core/models/course'; export enum PlanActionTypes { diff --git a/src/app/degree-planner/store/effects/note.effects.ts b/src/app/degree-planner/store/effects/note.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb7a0ff58e7e88cc0d585c13bc572cef64ede3b1 --- /dev/null +++ b/src/app/degree-planner/store/effects/note.effects.ts @@ -0,0 +1,108 @@ +// Libraries +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'; + +// Models +import { Note } from '@app/core/models/note'; + +// Services +import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; + +// State management +import { + NoteActionTypes, + WriteNoteRequest, + WriteNoteResponse, + DeleteNoteRequest, + DeleteNoteResponse, +} from '@app/degree-planner/store/actions/note.actions'; +import { getDegreePlannerState } from '@app/degree-planner/store/selectors'; +import { GlobalState } from '@app/core/state'; +import { DegreePlannerState } from '@app/degree-planner/store/state'; + +@Injectable() +export class NoteEffects { + constructor( + private actions$: Actions, + private store$: Store<GlobalState>, + private api: DegreePlannerApiService, + ) {} + + @Effect() + writeNote$: Observable<WriteNoteResponse> = this.actions$.pipe( + ofType<WriteNoteRequest>(NoteActionTypes.WriteNoteRequest), + + // 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(getDegreePlannerState)), + + // Only handle WriteNote actions when a current plan ID is set. + filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), + + // Using the action and State objects, determine whether to fire the + // `updateNote` or `createNote` API. Both of these API calls return + // an observable wrapper around the modified/created Note object. + mergeMap(([action, state]) => { + const planId = state.visibleRoadmapId as number; + const termCode = action.payload.termCode; + const noteText = action.payload.noteText; + const existingNote = getExistingNote(state, termCode); + if (existingNote !== undefined) { + // Since the term DOES have a note, update the existing note + const noteId = existingNote.id; + return this.api.updateNote(planId, termCode, noteText, noteId); + } else { + // Since the term DOES NOT have a note, create a new note + return this.api.createNote(planId, termCode, noteText); + } + }), + + // Dispatch an `WriteNoteSuccess` action so that the State + // object can be updated with the new Note data. + map(updatedNote => new WriteNoteResponse({ updatedNote })), + ); + + @Effect() + deleteNote$: Observable<DeleteNoteResponse> = this.actions$.pipe( + ofType<DeleteNoteRequest>(NoteActionTypes.DeleteNoteRequest), + + // Get the most recent Degree Planner state object. + // This is used to lookup the Note ID. + withLatestFrom(this.store$.select(getDegreePlannerState)), + + // Only handle DeleteNote actions when a current plan ID is set. + filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), + + // Using the action and State objects, fire the `deleteNote` API. + mergeMap(([action, state]) => { + const planId = state.visibleRoadmapId as number; + const termCode = action.payload.termCode; + const existingNote = getExistingNote(state, 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); + } + }), + + // Dispatch an `DeleteNoteSuccess` action so that the + // State object can be updated with the note removed. + map(termCode => new DeleteNoteResponse({ termCode })), + ); +} + +const getExistingNote = (state: DegreePlannerState, termCode: string) => { + for (const term of state.visibleTerms || []) { + if (term.termCode === termCode) { + return term.note; + } + } + return undefined; +}; diff --git a/src/app/degree-planner/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts similarity index 98% rename from src/app/degree-planner/effects/plan.effects.ts rename to src/app/degree-planner/store/effects/plan.effects.ts index 0c68883d9532c35e65e325cb357c7d431e9a55ae..7f92568e1a50490638d11e45ff3d19434a16a3ce 100644 --- a/src/app/degree-planner/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -8,7 +8,7 @@ import { Store } from '@ngrx/store'; // Services import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; -import { getDegreePlannerState } from '@app/degree-planner/selectors'; +import { getDegreePlannerState } from '@app/degree-planner/store/selectors'; // Actions import { @@ -21,7 +21,7 @@ import { RemoveCourseResponse, RemoveSavedForLaterResponse, AddSavedForLaterResponse, -} from '@app/degree-planner/actions/plan.actions'; +} from '@app/degree-planner/store/actions/plan.actions'; // Models import { PlannedTerm } from '@app/core/models/planned-term'; diff --git a/src/app/degree-planner/reducer.ts b/src/app/degree-planner/store/reducer.ts similarity index 97% rename from src/app/degree-planner/reducer.ts rename to src/app/degree-planner/store/reducer.ts index 2ab6474ed37a0a7d479ff2c2172a79f8362477f9..7b99521660a9e2ecaf115e30a5e2fb4a8f6887e2 100644 --- a/src/app/degree-planner/reducer.ts +++ b/src/app/degree-planner/store/reducer.ts @@ -1,7 +1,7 @@ import { DegreePlannerState, INITIAL_DEGREE_PLANNER_STATE, -} from '@app/degree-planner/state'; +} from '@app/degree-planner/store/state'; import { PlanActionTypes, InitialPlanLoadResponse, @@ -11,12 +11,12 @@ import { AddCourseResponse, RemoveSavedForLaterResponse, AddSavedForLaterResponse, -} from '@app/degree-planner/actions/plan.actions'; +} from '@app/degree-planner/store/actions/plan.actions'; import { NoteActionTypes, WriteNoteResponse, DeleteNoteResponse, -} from '@app/degree-planner/actions/note.actions'; +} from '@app/degree-planner/store/actions/note.actions'; type SupportedActions = | InitialPlanLoadResponse diff --git a/src/app/degree-planner/selectors.ts b/src/app/degree-planner/store/selectors.ts similarity index 100% rename from src/app/degree-planner/selectors.ts rename to src/app/degree-planner/store/selectors.ts diff --git a/src/app/degree-planner/state.ts b/src/app/degree-planner/store/state.ts similarity index 100% rename from src/app/degree-planner/state.ts rename to src/app/degree-planner/store/state.ts 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 c7ae8e37abb40e3b414bab8330f6453a9c7ef479..fcb2eea2cbfc6bdf1f0a454c12c2678393bc013c 100644 --- a/src/app/degree-planner/term-container/term-container.component.ts +++ b/src/app/degree-planner/term-container/term-container.component.ts @@ -11,15 +11,18 @@ import { MatDialog } from '@angular/material'; // rsjx / ngrx import { Observable } from 'rxjs'; import { Store, select } from '@ngrx/store'; -import { DegreePlannerState } from '@app/degree-planner/state'; +import { DegreePlannerState } from '@app/degree-planner/store/state'; import { ChangeCourseTermRequest, AddCourseRequest, RemoveSavedForLaterRequest, -} from '@app/degree-planner/actions/plan.actions'; +} from '@app/degree-planner/store/actions/plan.actions'; // Selectors -import { getDropZones, isActiveTerm } from '@app/degree-planner/selectors'; +import { + getDropZones, + isActiveTerm, +} from '@app/degree-planner/store/selectors'; // Services import { SidenavService } from './../../core/service/sidenav.service'; diff --git a/src/app/shared/components/course-details/course-details.component.ts b/src/app/shared/components/course-details/course-details.component.ts index 998b88886a8ca283dee981565b554dbeef5c4ad9..bec3f7b0c10dc0b73b69fee84ad48b47e52c3d56 100644 --- a/src/app/shared/components/course-details/course-details.component.ts +++ b/src/app/shared/components/course-details/course-details.component.ts @@ -3,19 +3,16 @@ import { CourseDetails } from '../../../core/models/course-details'; import { MAT_DIALOG_DATA } from '@angular/material'; @Component({ - selector: 'cse-course-details', - templateUrl: './course-details.component.html', - styleUrls: ['./course-details.component.scss'] + selector: 'cse-course-details', + templateUrl: './course-details.component.html', + styleUrls: ['./course-details.component.scss'], }) - export class CourseDetailsComponent implements OnInit { - courseDetails: CourseDetails; - - constructor(@Inject(MAT_DIALOG_DATA) public data: any) { - this.courseDetails = data.courseDetails; - } + courseDetails: CourseDetails; - ngOnInit() { - } + constructor(@Inject(MAT_DIALOG_DATA) public data: any) { + this.courseDetails = data.courseDetails; + } + ngOnInit() {} }