From a9ee3c4afd7424f69c85d77664c0cb611dea8c37 Mon Sep 17 00:00:00 2001 From: pnogal <paulina.nogal@wisc.edu> Date: Wed, 6 Feb 2019 13:56:24 -0600 Subject: [PATCH] Include Typeahead api --- src/app/app.component.html | 184 ++++++------------ src/app/app.component.scss | 9 +- src/app/app.component.ts | 91 ++++++--- src/app/app.module.ts | 2 + src/app/core/data.service.ts | 141 ++++++++++++++ .../degree-planner/degree-planner.module.ts | 33 ++-- src/app/shared/shared.module.ts | 9 +- 7 files changed, 282 insertions(+), 187 deletions(-) create mode 100644 src/app/core/data.service.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index 9e8cf98..01dffad 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,138 +1,64 @@ <header> - <cse-header></cse-header> - <cse-navigation></cse-navigation> + <cse-header></cse-header> + <cse-navigation></cse-navigation> </header> - <main> - <mat-sidenav-container class="example-container" hasBackdrop="false" style="height: 100vh;"> - <mat-sidenav mode="over" position="end" #rightAddCourse> - <mat-sidenav-container> - <mat-sidenav mode="side" position="start" #rightAddCourse2 class="course-details-pane"> - <mat-toolbar color="primary" class="dialog-toolbar"> - <button mat-button class="close-btn" (click)="rightAddCourse2.close()"><i class="material-icons">clear</i></button> - <span class="dialog-toolbar-title">Add course to degree plan</span> - </mat-toolbar> - <div id="course-details-content" class="mat-typography"> - <div fxLayout="row"> - <div fxLayout="row" fxLayout.lt-md="column" fxFlex="100" fxLayoutGap="10px" class="course-details-header"> - <div fxFlex="50" class="course-detail-title" fxLayoutAlign="start start"> - <h3>English 162<span class="course-detail-subtitle">Shakespeare</span></h3> - </div> - <div fxFlex="50" class="course-detail-title" fxLayoutAlign="end start"> - <button mat-raised-button class="btn-primary mat-button" aria-label="Add course to plan">Add course</button> - </div> - </div> - </div> - <div> - <p>Introduction to several of Shakespare's most popular plays and their relation to other works of English and American literature.</p> - <p><span class="semi-bold">Enroll Info: </span>None</p> - <p><span class="semi-bold">Requisites: </span>None</p> - <ul class="courseDetails-list"> - <li><span class="semi-bold">Credits:</span> 3</li> - <li><span class="semi-bold">Level: </span> - <span>Elementary</span> - </li> - <li><span class="semi-bold">Breadth: </span> - <span>Literature</span> - </li> - <li><span class="semi-bold">L&S Credit Type:</span> - Counts as LAS credit (L&S) - </li> - </ul> - <ul class="courseDetails-list"> - <li><span class="semi-bold">Last Taught: </span>Fall 2018</li> - <li><span class="semi-bold">Typically Offered: </span>Fall, Spring, Summer</li> - </ul> +<main> + <mat-sidenav-container class="example-container" hasBackdrop="false" style="height: 100vh;"> + <mat-sidenav mode="over" position="end" #rightAddCourse id="course-search-sidenav"> + <mat-toolbar color="primary" class="dialog-toolbar"> + <span class="dialog-toolbar-title">Course Search</span> + <button mat-button class="close-btn" (click)="rightAddCourse.close();"><i class="material-icons">clear</i></button> + </mat-toolbar> - <p><span class="semi-bold">Subject Notes:</span><br> - <span class="subject-notes"></span></p> - </div> + <div [formGroup]='coursesForm' class="add-course-form" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> + <mat-form-field> + <input type="text" placeholder="Term" aria-label="Term" matInput [formControl]="" [matAutocomplete]="term"> + <mat-autocomplete #term="matAutocomplete"> + <mat-option *ngFor="let term of (coursesData$ | async)" [value]="term[0].termCode | getTermDescription"> + {{ term[0].termCode | getTermDescription }} + </mat-option> + </mat-autocomplete> + </mat-form-field> - <div class="course-details-footer"> - <p class="semi-bold course-detail-title">English Information:</p> - <div fxLayout="row"> - <div fxLayout="row" fxFlex="100" fxLayoutGap="10px"> - <div fxFlex="100" fxLayout.lt-sm="column" class="mat-dialog-actions" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="start start"> - <a class="md-primary btn-link mat-button" href="">Website</a> - <a class="md-primary btn-link mat-button" href="">Undergraduate Info</a> - <a class="md-primary btn-link mat-button" href="">Graduate Info</a> - </div> - </div> - </div> - </div> - </div> - </mat-sidenav> - <mat-sidenav-content class="course-search-pane"> - <mat-toolbar color="primary" class="dialog-toolbar"> - <span class="dialog-toolbar-title">Course Search</span> - <button mat-button class="close-btn" (click)="rightAddCourse.close(); rightAddCourse2.close()"><i class="material-icons">clear</i></button> - </mat-toolbar> - - <form class="add-course-form" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> - <mat-form-field> - <input type="text" placeholder="Term" aria-label="Term" matInput [formControl]="" [matAutocomplete]="term"> - <mat-autocomplete #term="matAutocomplete"> - <mat-option *ngFor="let term of coursesData$ | async" [value]="term[0].termCode | getTermDescription"> - {{ term[0].termCode | getTermDescription }} - </mat-option> - </mat-autocomplete> - </mat-form-field> - - <mat-form-field> - <input type="text" placeholder="Subject" aria-label="Subject" matInput [formControl]="" [matAutocomplete]="subject"> - <mat-autocomplete #subject="matAutocomplete"> - <mat-option>Afro-American Studies</mat-option> - <mat-option>Agricultural and Applied Economics</mat-option> - <mat-option>Agroecology</mat-option> - <mat-option>Agronomy</mat-option> - <mat-option>Air Force Aerospace Studies</mat-option> - <mat-option>American Indian Studies</mat-option> - <mat-option>Anatomy</mat-option> - <mat-option>Anatomy & Physiology</mat-option> - </mat-autocomplete> - <!-- <mat-error *ngIf="terms.hasError('required')">Please select an existing subject or 'All'.</mat-error> --> - </mat-form-field> - - <mat-form-field class="example-full-width"> - <input matInput placeholder="Subject, number" value=""> - </mat-form-field> - </form> - - <div class="search-results-toolbar mat-typography" fxLayout="row" fxLayoutAlign="space-between center" style="padding: 12px 22px; background-color: #EDF1F3"> - <h3 style="margin: 0px;">7 of 7 results</h3> - <span class="mat-button">Reset Search</span> - </div> + <mat-form-field> + <input matInput placeholder="Subject" [matAutocomplete]="auto" formControlName='coursesInput' required> + <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn"> + <mat-option *ngFor="let course of (courses | async)" [value]="course.textSuggest"> + <span>{{ course.textSuggest }} </span> + </mat-option> + </mat-autocomplete> + <mat-error *ngIf="coursesInput.invalid">Please select an existing Subject or 'All'.</mat-error> + </mat-form-field> + <mat-form-field class="example-full-width"> + <input matInput placeholder="Keyword, number" value=""> + </mat-form-field> - <div id="course-search-results" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> - <mat-form-field> - <mat-select placeholder="Order by"> - <mat-option>Relevance</mat-option> - <mat-option>Subject</mat-option> - <mat-option>Catalog Number</mat-option> - </mat-select> - </mat-form-field> + <div class="search-results-toolbar mat-typography" fxLayout="row" fxLayoutAlign="space-between center" style="padding: 12px 22px; background-color: #EDF1F3"> + <h3 style="margin: 0px;">15 results</h3> + <span class="mat-button">Reset Search</span> + </div> - <cse-course-item (click)="rightAddCourse2.toggle()"> - <div class="course-item"> - <p class="course-number">ENGL 162</p> - <p class="course-title">Shakespeare</p> - </div> - <div class="course-item"> - <p class="course-number">PHILOS 101</p> - <p class="course-title">Introduction to Philosophy</p> - </div> - <div class="course-item"> - <p class="course-number">Math 135</p> - <p class="course-title">Algebraic Reasoning for Teaching Math</p> - </div> - </cse-course-item> + <div id="course-search-results" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> + <mat-form-field> + <mat-select placeholder="Order by"> + <mat-option>Relevance</mat-option> + <mat-option>Subject</mat-option> + <mat-option>Catalog Number</mat-option> + </mat-select> + </mat-form-field> + <cse-course-item *ngFor="let course of (courses | async)" (click)="openCourseDetailsDialog(course)"> + <div class="course-item"> + <p class="course-number">{{ course.payload.subject.shortDescription }} {{ course.payload.catalogNumber }}</p> + <p class="course-title">{{ course.textSuggest }}</p> </div> - </mat-sidenav-content> - </mat-sidenav-container> - </mat-sidenav> + </cse-course-item> + </div> + </div> + </mat-sidenav> - <mat-sidenav-content> - <router-outlet></router-outlet> - </mat-sidenav-content> + <mat-sidenav-content> + <router-outlet></router-outlet> + </mat-sidenav-content> - </mat-sidenav-container> - </main> + </mat-sidenav-container> +</main> \ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 99fcee6..114936e 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -20,13 +20,8 @@ main { overflow: hidden; } -#course-details-content { - padding: 0 25px 25px 25px; -} - -.course-search-pane, -.course-details-pane { +#course-search-sidenav { max-width: 360px; min-width: 360px; height: 100vh; -} +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 41e6b47..3ed5956 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,48 +1,75 @@ -import { SidenavService } from './core/service/sidenav.service'; import { DataService } from './core/data.service'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { SidenavService } from './core/service/sidenav.service'; import { Component, ViewChild, OnInit } from '@angular/core'; import { MatSidenav } from '@angular/material'; import { ActivatedRoute } from '@angular/router'; import { DegreePlannerDataService } from './core/service/degree-planner-data.service'; -// import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { debounceTime, switchMap, tap } from 'rxjs/operators'; +import { MatDialog } from '@angular/material'; +import { CourseDetailsDialogComponent } from './degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { FormControl, FormGroupDirective, NgForm, Validators } from '@angular/forms'; @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) {} + coursesData$: any; + selectedDegreePlan: number; + courses: Observable<any>; + coursesForm: FormGroup; + coursesInput: any; - ngOnInit() { - this.sidenavService.setSidenav(this.rightAddCourse); - } - coursesData$: any; - selectedDegreePlan: number; + @ViewChild('rightAddCourse') public rightAddCourse: MatSidenav; + constructor( + public dialog: MatDialog, + private dataService: DataService, + private route: ActivatedRoute, + private sidenavService: SidenavService, + private degreePlannerDataSvc: DegreePlannerDataService, + private fb: FormBuilder) { + this.coursesInput = new FormControl('', [Validators.required]); + this.selectedDegreePlan = 520224; + this.coursesData$ = this.degreePlannerDataSvc.getDegreePlanDataById(this.selectedDegreePlan); + } - @ViewChild('rightAddCourse') public rightAddCourse: MatSidenav; - constructor( - private dataService: DataService, - private route: ActivatedRoute, - private sidenavService: SidenavService, - private degreePlannerDataSvc: DegreePlannerDataService) { - this.selectedDegreePlan = 520224; - this.coursesData$ = this.degreePlannerDataSvc.getDegreePlanDataById(this.selectedDegreePlan); - } + ngOnInit() { + this.sidenavService.setSidenav(this.rightAddCourse); + this.coursesForm = this.fb.group({ + coursesInput: null + }); + this.courses = this.coursesForm.get('coursesInput').valueChanges + .pipe( + debounceTime(300), + switchMap(value => this.dataService.autocomplete(value)), + tap(x => console.log( x)) + ); + } - ngOnInit() { - this.sidenavService.setSidenav(this.rightAddCourse); - } + openCourseDetailsDialog(course) { + this.dataService.getCourseDetails(course.termCode, course.payload.subject.subjectCode, course.payload.courseId) + .subscribe(courseDetails => { + const dialogRef = this.dialog.open(CourseDetailsDialogComponent, { + data: { courseDetails: courseDetails } + }); + }); + } + +} } 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 b7d80fb..5188be0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,6 +16,7 @@ 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 { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { CourseDetailsDialogComponent } from './degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; @NgModule({ imports: [ @@ -38,6 +39,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete'; AppComponent, HeaderComponent ], + entryComponents: [CourseDetailsDialogComponent], providers: [ SidenavService ], bootstrap: [ AppComponent ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ] diff --git a/src/app/core/data.service.ts b/src/app/core/data.service.ts new file mode 100644 index 0000000..51eef86 --- /dev/null +++ b/src/app/core/data.service.ts @@ -0,0 +1,141 @@ +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { throwError, Observable, forkJoin } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { ConfigService } from './config.service'; +import { Course } from './models/course'; +import { DegreePlan } from './models/degree-plan'; +import { Term } from './models/term'; +import { SavedForLaterCourse } from './models/saved-for-later-course'; +import { CourseDetails } from './models/course-details'; +import { Note } from './models/note'; + +const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) +}; + +@Injectable() +export class DataService { + private plannerApiUrl: string; + private searchApiUrl: string; + + constructor(private http: HttpClient, private configService: ConfigService) { + this.plannerApiUrl = this.configService.apiPlannerUrl; + this.searchApiUrl = this.configService.apiSearchUrl; + } + + getAllPlanData(roadmapId: number) { + return forkJoin( + this.getDegreePlannerCourseData(roadmapId), + this.getTerms() + ); + } + + getDegreePlans(): Observable<DegreePlan[]> { + return this.http.get<DegreePlan[]>(this.plannerApiUrl + '/degreePlan') + .pipe(catchError(this.errorHandler)); + } + + getDegreePlannerCourseData(roadmapId: number): Observable<Course[]> { + return this.http.get<Course[]>(this.plannerApiUrl + '/degreePlan/' + roadmapId + '/courses') + .pipe(catchError(this.errorHandler)); + } + + getDegreePlannerCourseData2(roadmapId: number): Observable<Course> { + return this.http.get<Course>(this.plannerApiUrl + '/degreePlan/' + roadmapId + '/courses') + .pipe(catchError(this.errorHandler)); + } + + getTerms(): Observable<Term[]> { + return this.http.get<Term[]>(this.searchApiUrl + '/terms') + .pipe(catchError(this.errorHandler)); + } + + getCourseDetails(termCode: string, subjectCode: string, courseId: string): Observable<CourseDetails[]> { + return this.http.get<CourseDetails[]>(this.searchApiUrl + '/course/0000/' + subjectCode + '/' + courseId, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + getSubjectsMap(): Observable<Object> { + return this.http.get(this.searchApiUrl + '/subjectsMap/0000', httpOptions) + .pipe(catchError(this.errorHandler)); + } + + getFavoriteCourses(): Observable<SavedForLaterCourse[]> { + return this.http.get<SavedForLaterCourse[]>(this.plannerApiUrl + '/favorites') + .pipe(catchError(this.errorHandler)); + } + + getAllNotes(planId: number): Observable<Note[]> { + return this.http.get<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes') + .pipe(catchError(this.errorHandler)); + } + + getNote(planId, noteId): Observable<Note[]> { + return this.http.get<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/' + noteId) + .pipe(catchError(this.errorHandler)); + } + + updateNote(planId, note): Observable<Note[]> { + return this.http.put<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/' + note.id, note, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + createNote(planId, note): Observable<Note[]> { + return this.http.post<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/', note, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + removeNote(planId, noteId): Observable<Note[]> { + return this.http.delete<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/' + noteId, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + saveFavoriteCourse(subjectCode: string, courseId: string): Observable<SavedForLaterCourse> { + return this.http.post<SavedForLaterCourse>(this.plannerApiUrl + '/favorites/' + subjectCode + '/' + courseId, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + removeFavoriteCourse(subjectCode, courseId): Observable<SavedForLaterCourse> { + return this.http.delete<SavedForLaterCourse>(this.plannerApiUrl + '/favorites/' + subjectCode + '/' + courseId, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + autocomplete(search: string) { + const data = { + subjectCode: '104', + termCode: '0000', + matchText: search + }; + + return this.http.post('/api/search/v1/autocomplete', data, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + addCourse(planId, subjectCode, courseId, termCode) { + return this.http.post(this.plannerApiUrl + '/degreePlan/' + planId + '/courses', {subjectCode, courseId, termCode }, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + removeCourse(planId, recordId) { + return this.http.delete(this.plannerApiUrl + '/degreePlan/' + planId + '/courses/' + recordId, httpOptions) + .pipe(catchError(this.errorHandler)); + } + + updateCourseTerm(planId, recordId, termCode): Observable<Course> { + return this.http.put<Course>(this.plannerApiUrl + '/degreePlan/' + planId + '/courses/' + recordId + '?termCode=' + termCode, httpOptions) + .pipe(catchError(this.errorHandler)); + } + +test() { +// return this.http.delete(this.plannerApiUrl + '/degreePlan/519260/courses/259445', httpOptions) +return this.http.put(this.plannerApiUrl + '/degreePlan/519260/courses/259465?termCode=1174', httpOptions) + .pipe(catchError(this.errorHandler)); +} + + private errorHandler(error: HttpErrorResponse) { + return throwError(error || 'Server Error'); + } +} diff --git a/src/app/degree-planner/degree-planner.module.ts b/src/app/degree-planner/degree-planner.module.ts index cd9a4a4..12364b4 100644 --- a/src/app/degree-planner/degree-planner.module.ts +++ b/src/app/degree-planner/degree-planner.module.ts @@ -13,22 +13,21 @@ 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, + NotesDialogComponent, + RemoveCourseConfirmDialogComponent + ], + entryComponents: [ + CourseDetailsDialogComponent, + NotesDialogComponent, + RemoveCourseConfirmDialogComponent + ] }) export class DegreePlannerModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 8d954b1..bb40482 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -20,6 +20,9 @@ import { GetTermDescriptionPipe } from './pipes/get-term-description.pipe'; import { AcademicYearStatePipe } from './pipes/academic-year-state.pipe'; import { AcademicYearRangePipe } from './pipes/academic-year-range.pipe'; import { CourseDetailsComponent } from './components/course-details/course-details.component'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { CourseDetailsDialogComponent } from '../degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; const modules = [ CommonModule, @@ -37,7 +40,9 @@ const modules = [ MatToolbarModule, MatDialogModule, MatInputModule, - MatTooltipModule + MatTooltipModule, + MatAutocompleteModule, + MatFormFieldModule ]; const pipes = [ GetTermDescriptionPipe, AcademicYearStatePipe, AcademicYearRangePipe @@ -46,7 +51,7 @@ const pipes = [ @NgModule({ imports: [ modules ], exports: [ modules, pipes, CourseDetailsComponent ], - declarations: [ pipes, CourseDetailsComponent ] + declarations: [ pipes, CourseDetailsComponent, CourseDetailsDialogComponent ] }) export class SharedModule { } -- GitLab