diff --git a/src/app/degree-planner/degree-planner.component.html b/src/app/degree-planner/degree-planner.component.html index 0cb8a39e38844a7d70471e9a731b7b392e40898f..83d91f44622458dd7b8f5362e3efa3d50eec226b 100644 --- a/src/app/degree-planner/degree-planner.component.html +++ b/src/app/degree-planner/degree-planner.component.html @@ -3,44 +3,43 @@ <mat-sidenav #addMenu position="end" mode="over" [opened]="isCourseSearchOpen$ | async"> <mat-toolbar color="primary" class="dialog-toolbar"> <span class="dialog-toolbar-title">Course Search</span> - <button mat-button class="close-btn" (click)="closeCourseSearch();"><i class="material-icons">clear</i></button> + <button mat-button class="close-btn" (click)="closeCourseSearch();"><i class="material-icons">keyboard_arrow_right</i></button> </mat-toolbar> - - <div [formGroup]='courseSearchForm' class="add-course-form" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> + + <form [formGroup]='courseSearchForm' (ngSubmit)="search()" 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-select placeholder="Term" formControlName="term"> + <mat-option value="0000" selected>All</mat-option> + <mat-option *ngFor="let term of activeTerms$ | async" [value]="term">{{term | getTermDescription}}</mat-option> + </mat-select> </mat-form-field> <mat-form-field> - <input matInput placeholder="Subject" formControlName='coursesInput' required> - <!-- <mat-error *ngIf="coursesInput.invalid">Please select an existing Subject or 'All'.</mat-error> --> + <mat-select placeholder="Subject" formControlName="subject" required> + <mat-option value="all">All</mat-option> + <mat-option *ngFor="let subject of (subjects$ | async) | keyvalue" [value]="subject.key"> + {{subject.value}} + </mat-option> + </mat-select> </mat-form-field> - <mat-form-field class="example-full-width"> - <input matInput placeholder="Keyword, number" value=""> + <mat-form-field> + <div class="search-input-wrapper"> + <input id="keyword-field" matInput placeholder="Keyword, number" formControlName="search" value=""> + <button id="search-button" mat-icon-button aria-label="Search" matSuffix style="margin-top: -10px;"><i class="material-icons">search</i></button> + </div> </mat-form-field> + </form> - <button mat-button (click)="search()">Search <i class="material-icons">search</i></button> - - <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 *ngIf="hasResults || isLoading"> + <div class="search-results-toolbar mat-typography" fxLayout="row" fxLayoutAlign="space-between center" style="padding: 12px 22px; background-color: #EDF1F3; min-height: 60px;"> + <h3 *ngIf="isLoading" style="margin: 0px;">Searching for courses...</h3> + <h3 *ngIf="hasResults" style="margin: 0px;">{{queriedCourses.length}} result(s)</h3> + <button *ngIf="hasResults" mat-button (click)="resetSearch()">Reset Search</button> </div> - + <mat-progress-bar mode="indeterminate" *ngIf="isLoading"></mat-progress-bar> + <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 cdkDropList id="queried-courses-list" diff --git a/src/app/degree-planner/degree-planner.component.scss b/src/app/degree-planner/degree-planner.component.scss index a86efb37cb46f307a6094fc7e17820f1635826a1..191df4e4c51da1ba5f875d99f90561401578276c 100644 --- a/src/app/degree-planner/degree-planner.component.scss +++ b/src/app/degree-planner/degree-planner.component.scss @@ -6,10 +6,6 @@ mat-sidenav { width: 340px; } -#degree-plan-wrapper { - // margin-right: 0 !important; -} - #menu-toggle-btn { position: absolute; right: 0px; @@ -52,3 +48,10 @@ mat-sidenav { padding: 12px; } } + +// Course search +.search-input-wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/src/app/degree-planner/degree-planner.component.ts b/src/app/degree-planner/degree-planner.component.ts index 1050db439dfde6a6979dcb343db3e0a57be43a8b..56f961e6e8ee159111e5122caea48c1b1091abc3 100644 --- a/src/app/degree-planner/degree-planner.component.ts +++ b/src/app/degree-planner/degree-planner.component.ts @@ -41,6 +41,8 @@ import { hasLoadedDegreePlan, getDropZones, isCourseSearchOpen, + getSubjects, + getActiveTerms, } from '@app/degree-planner/store/selectors'; // Actions @@ -77,9 +79,15 @@ export class DegreePlannerComponent implements OnInit { public allDegreePlans$: Observable<DegreePlan[]>; public firstActiveTermCode$: Observable<string | undefined>; public termsByYear$: Observable<Year[]>; + public isCourseSearchOpen$: Observable<boolean>; + public subjects$: Observable<object>; + public activeTerms$: Observable<string[]>; + + // Search variables public queriedCourses: Course[]; + public hasResults: boolean; + public isLoading: boolean; public courseSearchForm: FormGroup; - public isCourseSearchOpen$: Observable<boolean>; constructor( private store: Store<GlobalState>, @@ -103,8 +111,20 @@ export class DegreePlannerComponent implements OnInit { this.termsByYear$ = this.store.pipe(select(getAllVisibleTermsByYear)); this.dropZones$ = this.store.pipe(select(getDropZones)); + // State attributes needed to create the search form + this.subjects$ = this.store.pipe(select(getSubjects)); + this.activeTerms$ = this.store.pipe(select(getActiveTerms)); + + // Internal values used to manage loading state + this.queriedCourses = []; + this.hasResults = false; + this.isLoading = false; + + // Deafults for the search form this.courseSearchForm = this.fb.group({ - coursesInput: null, + term: '0000', + subject: 'all', + search: '', }); this.isCourseSearchOpen$ = this.store.pipe(select(isCourseSearchOpen)); @@ -225,13 +245,30 @@ export class DegreePlannerComponent implements OnInit { } public search() { - // console.log('here'); - // console.log(this.coursesForm.value.subjectCode); + // Get the form field values + const { search, term, subject } = this.courseSearchForm.value; + + // Set the internal UI state + this.isLoading = true; + this.hasResults = false; + this.queriedCourses = []; + + // Hit the search API this.api - .searchCourses('test', 'test') + .searchCourses({ + subjectCode: subject, + searchText: search, + termCode: term === '' ? '0000' : term, + }) .toPromise() .then(res => { - console.log(res.hits); + // TODO add error handeling + + // Update the internal state + this.hasResults = true; + this.isLoading = false; + + // Map out the results and update the course object to match the needed structure this.queriedCourses = res.hits.map(course => { return { ...course, @@ -242,6 +279,16 @@ export class DegreePlannerComponent implements OnInit { }) .catch(console.log); } + + public resetSearch() { + // Reset the internal state and form values + this.queriedCourses = []; + this.hasResults = false; + this.courseSearchForm.setValue({ + subject: 'all', + search: '', + }); + } } const isntUndefined = <T>(anything: T | undefined): anything is T => { diff --git a/src/app/degree-planner/services/api.service.ts b/src/app/degree-planner/services/api.service.ts index dbd233f272c0768de5fbda31a1660d9ad9da3274..63a681a57b3fa57d402929db133d115c359ea523 100644 --- a/src/app/degree-planner/services/api.service.ts +++ b/src/app/degree-planner/services/api.service.ts @@ -4,7 +4,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Location } from '@angular/common'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, filter } from 'rxjs/operators'; // Services import { ConfigService } from '@app/core/config.service'; @@ -106,7 +106,11 @@ export class DegreePlannerApiService { return this.http.post('/api/search/v1/autocomplete', data, HTTP_OPTIONS); } - public searchCourses(search: string, subjectCode: string): Observable<any> { + public searchCourses(config: { + subjectCode: string; + searchText?: string; + termCode?: string; + }): Observable<any> { // const data = { // filters: [ // { term: { 'subject.subjectCode': '266' } }, @@ -128,43 +132,44 @@ export class DegreePlannerApiService { // sortOrder: 'SCORE', // }; - // Used to search all terms - const data = { - filters: [{ term: { 'subject.subjectCode': '266' } }], - page: 1, - pageSize: 10, - queryString: 'Programming', - selectedTerm: '0000', - sortOrder: 'SCORE', - }; + const { subjectCode, termCode, searchText } = config; - // Used to search a specific term - const test = { - selectedTerm: '1194', - queryString: 'programing', + const payload: any = { + selectedTerm: termCode, + queryString: searchText === '' ? '*' : searchText, // Filters are use to build the elastic search query - filters: [ - { term: { 'subject.subjectCode': '266' } }, - { - has_child: { - type: 'enrollmentPackage', - query: { - // We want to make sure we search for ALL classes regardless of status - match: { - 'packageEnrollmentStatus.status': 'OPEN WAITLISTED CLOSED', - }, - }, - }, - }, - ], + filters: [], // These options control how much data we get back page: 1, - pageSize: 20, + pageSize: 25, + sortOrder: 'SCORE', }; - return this.http.post('/api/search/v1', data, HTTP_OPTIONS); + // If we have a specific subject code, add a fitler for it + if (subjectCode !== 'all') { + payload.filters.push({ term: { 'subject.subjectCode': subjectCode } }); + } + + // 0000 is used to search all courses + // Any other term code we can assuem is an active term + if (termCode !== '0000') { + // Used to search a specific term + payload.filters.push({ + has_child: { + type: 'enrollmentPackage', + query: { + // We want to make sure we search for ALL classes regardless of status + match: { + 'packageEnrollmentStatus.status': 'OPEN WAITLISTED CLOSED', + }, + }, + }, + }); + } + + return this.http.post('/api/search/v1', payload, HTTP_OPTIONS); } public addCourse( diff --git a/src/app/degree-planner/store/selectors.ts b/src/app/degree-planner/store/selectors.ts index 15c890d4ff495cd52de71f0bf8d2dcf763605ca1..01ce5daa0c87da0c427975d502d2c6863ae387ef 100644 --- a/src/app/degree-planner/store/selectors.ts +++ b/src/app/degree-planner/store/selectors.ts @@ -69,6 +69,8 @@ export const getDropableTerms = createSelector( export const getAllVisibleTermsByYear = createSelector( getDegreePlannerState, state => { + console.log('getting visible terms'); + const unqiueYears = state.visibleTerms .map(term => term.termCode.slice(0, 3)) .filter(year => year.match(/^\d{3}/)) @@ -117,6 +119,20 @@ export const isCourseSearchOpen = createSelector( }, ); +export const getSubjects = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + return state.subjects; + }, +); + +export const getActiveTerms = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + return state.activeTermCodes; + }, +); + export const isPastTerm = (termCode: string) => createSelector( getDegreePlannerState, diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index bbe21af1e478ee527f97482b237bf9cecbba0bb0..648ca93bd4f2b8872afe12a1e7b464ed6d949990 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -16,6 +16,7 @@ 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 { MatProgressBarModule } from '@angular/material/progress-bar'; import { GetTermDescriptionPipe } from './pipes/get-term-description.pipe'; import { AcademicYearStatePipe } from './pipes/academic-year-state.pipe'; @@ -45,6 +46,7 @@ const modules = [ MatAutocompleteModule, MatFormFieldModule, MatSnackBarModule, + MatProgressBarModule, ]; const pipes = [ GetTermDescriptionPipe,