diff --git a/src/app/app.component.html b/src/app/app.component.html index 08abe4f4bc58cea6e9d3c55a8287ce0f30e0f818..f2f6974ac8eb26506316a71960e0d15e5073c5c1 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -3,63 +3,5 @@ <cse-navigation></cse-navigation> </header> <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> - - <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> - - <mat-form-field> - <input matInput placeholder="Subject" [matAutocomplete]="auto" formControlName='coursesInput' required> - <mat-autocomplete #auto="matAutocomplete"> - <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 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> - - <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> - </cse-course-item> - </div> - </div> - </mat-sidenav> - - <mat-sidenav-content> - <router-outlet></router-outlet> - </mat-sidenav-content> - - </mat-sidenav-container> + <router-outlet></router-outlet> </main> \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8d72df0e26da92497bae4c755cee2eb74a743716..9422145c2b40a2f4383f36ec3547d2ef7fcba7d4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -7,7 +7,7 @@ import { FormControl, Validators, } from '@angular/forms'; - +import { Course } from '@app/core/models/course'; import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; import { SidenavService } from '@app/core/service/sidenav.service'; import { CourseDetailsDialogComponent } from '@app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; @@ -20,9 +20,9 @@ import { CourseDetailsDialogComponent } from '@app/degree-planner/dialogs/course export class AppComponent implements OnInit { coursesData$: any; selectedDegreePlan: number; - courses: Observable<any>; + courses: Course[]; coursesForm: FormGroup; - coursesInput = {}; + subjectCode: FormControl; @ViewChild('rightAddCourse') public rightAddCourse: MatSidenav; constructor( @@ -31,11 +31,12 @@ export class AppComponent implements OnInit { private fb: FormBuilder, private api: DegreePlannerApiService, ) { - this.coursesInput = new FormControl('', [Validators.required]); + this.subjectCode = new FormControl('', [Validators.required]); this.selectedDegreePlan = 520224; this.coursesForm = this.fb.group({ coursesInput: null, }); + // Uncomment following code when working on Course Search form in +Add Course // It will need to be modified to fix error: TS2531: Object is possibly 'null' // this.courses = this.coursesForm.get('coursesInput').valueChanges @@ -47,7 +48,7 @@ export class AppComponent implements OnInit { } ngOnInit() { - this.sidenavService.setSidenav(this.rightAddCourse); + // this.sidenavService.setSidenav(this.rightAddCourse); } openCourseDetailsDialog(course) { diff --git a/src/app/core/service/sidenav.service.ts b/src/app/core/service/sidenav.service.ts index 12ddbf060e1425c16867e701d0206e896fa640da..ee5270156d76a9c729de9fb669ef1962de87afe3 100644 --- a/src/app/core/service/sidenav.service.ts +++ b/src/app/core/service/sidenav.service.ts @@ -3,22 +3,21 @@ import { MatSidenav } from '@angular/material'; @Injectable() export class SidenavService { - private sidenav: MatSidenav; + private sidenav: MatSidenav; + public setSidenav(sidenav: MatSidenav) { + this.sidenav = sidenav; + } - public setSidenav(sidenav: MatSidenav) { - this.sidenav = sidenav; - } + public open() { + return this.sidenav.open(); + } - public open() { - return this.sidenav.open(); - } + public close() { + return this.sidenav.close(); + } - public close() { - return this.sidenav.close(); - } - - public toggle(): void { - this.sidenav.toggle(); - } + public toggle(): void { + this.sidenav.toggle(); + } } diff --git a/src/app/degree-planner/degree-planner.component.html b/src/app/degree-planner/degree-planner.component.html index 404f0293048dbe9a539aa23bf3b104595d24453b..b708de5a2510c05b0505e5ac2ef962aca59eb045 100644 --- a/src/app/degree-planner/degree-planner.component.html +++ b/src/app/degree-planner/degree-planner.component.html @@ -1,73 +1,139 @@ -<mat-sidenav-container id="plans-container" *ngIf="(visibleDegreePlan$ | async) as degreePlan"> - <!-- Menu side nav --> - <mat-sidenav #rightMenu position="end" [mode]="mobileView.matches ? 'over' : 'side'" [opened]="mobileView.matches ? false : true"> - <cse-sidenav-menu-item></cse-sidenav-menu-item> - </mat-sidenav> +<mat-sidenav-container hasBackdrop="false"> - <mat-sidenav-content> - <div fxLayout="row" fxLayout.lt-sm="column" fxLayoutGap="20px" fxLayoutAlign="start center" style="margin: 24px 0px 24px 24px;"> + <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> + </mat-toolbar> + + <div [formGroup]='courseSearchForm' class="add-course-form" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> <mat-form-field> - <mat-select - placeholder="Degree Plans" - class="degree-plan-selector" - [value]="visibleRoadmapId$ | async" - [disableOptionCentering]="true" - (selectionChange)="handleDegreePlanChange($event)"> + <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> - <!-- Render the name of the currently visible degree plan. --> - <mat-select-trigger> - <mat-icon class="primary-star" *ngIf="degreePlan.primary">star_rate</mat-icon> - <span class="plan-name">{{degreePlan.name}}</span> - </mat-select-trigger> + <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-form-field> - <!-- Show all degree plans in the dropdown list and ddd a star next to the user's primary plan. --> - <mat-option *ngFor="let degreePlan of (allDegreePlans$ | async)" [value]="degreePlan.roadmapId"> - <mat-icon class="primary-star" *ngIf="degreePlan.primary">star_rate</mat-icon> - <span class="plan-name">{{degreePlan.name}}</span> - </mat-option> - </mat-select> + <mat-form-field class="example-full-width"> + <input matInput placeholder="Keyword, number" value=""> </mat-form-field> - <button mat-icon-button [matMenuTriggerFor]="degreePlanMenu"> - <mat-icon>settings</mat-icon> - </button> - <mat-menu #degreePlanMenu="matMenu"> - <button mat-menu-item (click)="onCreatePlanClick()">Create new plan</button> - <button mat-menu-item (click)="onRenamePlanClick(degreePlan)">Rename plan</button> - <button mat-menu-item (click)="onDeletePlanClick(degreePlan)" [disabled]="degreePlan.primary">Delete plan</button> - <button mat-menu-item (click)="onMakePrimayClick(degreePlan)" [disabled]="degreePlan.primary">Make primary</button> - <hr> - <button mat-menu-item (click)="onPrintPlanClick(degreePlan)">Print plan</button> - <button mat-menu-item (click)="onSharePlanClick(degreePlan)">Share plan</button> - </mat-menu> - </div> + <button mat-button (click)="search()">Search <i class="material-icons">search</i></button> - <div fxLayout="column" fxLayoutGap="20px" fxLayoutAlign="start stretch" style="margin: 24px"> - <mat-accordion multi="true" *ngIf="(firstActiveTermCode$ | async) as cutoffTermCode"> - <ng-container *ngFor="let year of termsByYear$ | async"> - <mat-expansion-panel class="year-container" [expanded]="year.expandedState" (opened)="handleAcademicYearToggle(year)" (closed)="handleAcademicYearToggle(year)"> - <mat-expansion-panel-header> - <mat-panel-title> - {{ year.twoDigitYearCode | academicYearState:cutoffTermCode }} - </mat-panel-title> - </mat-expansion-panel-header> - <div fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutAlign="start stretch" class="term-container-wrapper"> - <cse-term-container - id="term-{{year.fall.termCode}}" - fxFlex="33%" - [term]="year.fall"></cse-term-container> - <cse-term-container - id="term-{{year.spring.termCode}}" - fxFlex="33%" - [term]="year.spring"></cse-term-container> - <cse-term-container - id="term-{{year.summer.termCode}}" - fxFlex="33%" - [term]="year.summer"></cse-term-container> + <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> + + <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" + [cdkDropListData]="queriedCourses" + [cdkDropListConnectedTo]="dropZones$ | async"> + <div + class="course-wrapper" + cdkDrag + [cdkDragData]="course" + *ngFor="let course of this.queriedCourses"> + <div class="course-wrapper-inner"> + <cse-course-item [course]="course" type="course"></cse-course-item> </div> - </mat-expansion-panel> - </ng-container> - </mat-accordion> + </div> + </div> + </div> </div> + </mat-sidenav> + + + <mat-sidenav-content> + <mat-sidenav-container id="plans-container" *ngIf="(visibleDegreePlan$ | async) as degreePlan"> + <!-- Menu side nav --> + <mat-sidenav #rightMenu position="end" [mode]="mobileView.matches ? 'over' : 'side'" [opened]="mobileView.matches ? false : true"> + <cse-sidenav-menu-item></cse-sidenav-menu-item> + </mat-sidenav> + + <mat-sidenav-content id="degree-plan-wrapper"> + <div fxLayout="row" fxLayout.lt-sm="column" fxLayoutGap="20px" fxLayoutAlign="start center" style="margin: 24px 0px 24px 24px;"> + <mat-form-field> + <mat-select + placeholder="Degree Plans" + class="degree-plan-selector" + [value]="visibleRoadmapId$ | async" + [disableOptionCentering]="true" + (selectionChange)="handleDegreePlanChange($event)"> + + <!-- Render the name of the currently visible degree plan. --> + <mat-select-trigger *ngIf="(visibleDegreePlan$ | async) as degreePlan"> + <mat-icon class="primary-star" *ngIf="degreePlan.primary">star_rate</mat-icon> + <span class="plan-name">{{degreePlan.name}}</span> + </mat-select-trigger> + + <!-- Show all degree plans in the dropdown list and ddd a star next to the user's primary plan. --> + <mat-option *ngFor="let degreePlan of (allDegreePlans$ | async)" [value]="degreePlan.roadmapId"> + <mat-icon class="primary-star" *ngIf="degreePlan.primary">star_rate</mat-icon> + <span class="plan-name">{{degreePlan.name}}</span> + </mat-option> + </mat-select> + </mat-form-field> + + <button mat-icon-button [matMenuTriggerFor]="degreePlanMenu"> + <mat-icon>settings</mat-icon> + </button> + <mat-menu #degreePlanMenu="matMenu"> + <button mat-menu-item (click)="onCreatePlanClick()">Create new plan</button> + <button mat-menu-item (click)="onRenamePlanClick()">Rename plan</button> + <button mat-menu-item (click)="onDeletePlanClick()">Delete plan</button> + <button mat-menu-item (click)="onMakePrimayClick()">Make primary</button> + <hr> + <button mat-menu-item (click)="onPrintPlanClick()">Print plan</button> + <button mat-menu-item (click)="onSharePlanClick()">Share plan</button> + </mat-menu> + </div> + + <div fxLayout="column" fxLayoutGap="20px" fxLayoutAlign="start stretch" style="margin: 24px"> + <mat-accordion multi="true" *ngIf="(firstActiveTermCode$ | async) as cutoffTermCode"> + <ng-container *ngFor="let year of termsByYear$ | async"> + <mat-expansion-panel class="year-container" [expanded]="year.expandedState" (opened)="handleAcademicYearToggle(year)" (closed)="handleAcademicYearToggle(year)"> + <mat-expansion-panel-header> + <mat-panel-title> + {{ year.twoDigitYearCode | academicYearState:cutoffTermCode }} + </mat-panel-title> + </mat-expansion-panel-header> + <div fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutAlign="start stretch" class="term-container-wrapper"> + <cse-term-container + id="term-{{year.fall.termCode}}" + fxFlex="33%" + [term]="year.fall"></cse-term-container> + <cse-term-container + id="term-{{year.spring.termCode}}" + fxFlex="33%" + [term]="year.spring"></cse-term-container> + <cse-term-container + id="term-{{year.summer.termCode}}" + fxFlex="33%" + [term]="year.summer"></cse-term-container> + </div> + </mat-expansion-panel> + </ng-container> + </mat-accordion> + </div> + </mat-sidenav-content> + </mat-sidenav-container> </mat-sidenav-content> </mat-sidenav-container> diff --git a/src/app/degree-planner/degree-planner.component.scss b/src/app/degree-planner/degree-planner.component.scss index 4dfac0a8381931bc888d7ae731fc1d176bb5929b..a86efb37cb46f307a6094fc7e17820f1635826a1 100644 --- a/src/app/degree-planner/degree-planner.component.scss +++ b/src/app/degree-planner/degree-planner.component.scss @@ -6,6 +6,10 @@ mat-sidenav { width: 340px; } +#degree-plan-wrapper { + // margin-right: 0 !important; +} + #menu-toggle-btn { position: absolute; right: 0px; diff --git a/src/app/degree-planner/degree-planner.component.ts b/src/app/degree-planner/degree-planner.component.ts index baaf6aac2ae56ef7ae95c62d66a8ca83d35039c9..1050db439dfde6a6979dcb343db3e0a57be43a8b 100644 --- a/src/app/degree-planner/degree-planner.component.ts +++ b/src/app/degree-planner/degree-planner.component.ts @@ -1,6 +1,6 @@ import { tap, withLatestFrom } from 'rxjs/operators'; // Libraries -import { OnInit } from '@angular/core'; +import { OnInit, ViewChild } from '@angular/core'; import { Observable } from 'rxjs'; import { select } from '@ngrx/store'; import { Component } from '@angular/core'; @@ -8,6 +8,7 @@ import { MatSelectChange, MatExpansionPanelState, MatExpansionPanel, + MatSidenav, } from '@angular/material'; import { MatDialog } from '@angular/material'; import { Store } from '@ngrx/store'; @@ -19,6 +20,16 @@ import { MatSnackBar } from '@angular/material'; import { GlobalState } from '@app/core/state'; import { DegreePlan } from '@app/core/models/degree-plan'; import { Year } from '@app/core/models/year'; +import { Course } from '@app/core/models/course'; + +// Form imports +import { + FormBuilder, + FormGroup, + FormControl, + Validators, +} from '@angular/forms'; +import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; // Selectors import { @@ -27,6 +38,9 @@ import { firstActiveTermCode, getAllVisibleTermsByYear, getVisibleDegreePlan, + hasLoadedDegreePlan, + getDropZones, + isCourseSearchOpen, } from '@app/degree-planner/store/selectors'; // Actions @@ -42,7 +56,10 @@ import { ModifyPlanDialogComponent, DialogMode, } from './dialogs/modify-plan-dialog/modify-plan-dialog.component'; -import { ToggleAcademicYear } from './store/actions/ui.actions'; +import { + ToggleAcademicYear, + CloseCourseSearch, +} from './store/actions/ui.actions'; @Component({ selector: 'cse-degree-planner', @@ -53,18 +70,24 @@ export class DegreePlannerComponent implements OnInit { public termsByAcademicYear: Object; public mobileView: MediaQueryList; public coursesData$: any; + public dropZones$: Observable<String[]>; public visibleRoadmapId$: Observable<number | undefined>; public visibleDegreePlan$: Observable<DegreePlan>; public allDegreePlans$: Observable<DegreePlan[]>; public firstActiveTermCode$: Observable<string | undefined>; public termsByYear$: Observable<Year[]>; + public queriedCourses: Course[]; + public courseSearchForm: FormGroup; + public isCourseSearchOpen$: Observable<boolean>; constructor( private store: Store<GlobalState>, public mediaMatcher: MediaMatcher, public dialog: MatDialog, private snackBar: MatSnackBar, + private fb: FormBuilder, + private api: DegreePlannerApiService, ) { this.mobileView = mediaMatcher.matchMedia('(max-width: 900px)'); } @@ -78,6 +101,13 @@ export class DegreePlannerComponent implements OnInit { this.allDegreePlans$ = this.store.pipe(select(getAllDegreePlans)); this.firstActiveTermCode$ = this.store.pipe(select(firstActiveTermCode)); this.termsByYear$ = this.store.pipe(select(getAllVisibleTermsByYear)); + this.dropZones$ = this.store.pipe(select(getDropZones)); + + this.courseSearchForm = this.fb.group({ + coursesInput: null, + }); + + this.isCourseSearchOpen$ = this.store.pipe(select(isCourseSearchOpen)); } public handleAcademicYearToggle(year: Year): void { @@ -189,6 +219,29 @@ export class DegreePlannerComponent implements OnInit { return termCodes; } + + public closeCourseSearch() { + this.store.dispatch(new CloseCourseSearch()); + } + + public search() { + // console.log('here'); + // console.log(this.coursesForm.value.subjectCode); + this.api + .searchCourses('test', 'test') + .toPromise() + .then(res => { + console.log(res.hits); + this.queriedCourses = res.hits.map(course => { + return { + ...course, + subject: course.subject.description, + subjectCode: course.subject.subjectCode, + }; + }); + }) + .catch(console.log); + } } 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 f2203e481590197b5923db8eadb359c5bc76f598..dbd233f272c0768de5fbda31a1660d9ad9da3274 100644 --- a/src/app/degree-planner/services/api.service.ts +++ b/src/app/degree-planner/services/api.service.ts @@ -106,6 +106,67 @@ export class DegreePlannerApiService { return this.http.post('/api/search/v1/autocomplete', data, HTTP_OPTIONS); } + public searchCourses(search: string, subjectCode: string): Observable<any> { + // const data = { + // filters: [ + // { term: { 'subject.subjectCode': '266' } }, + // { + // has_child: { + // type: 'enrollmentPackage', + // query: { + // match: { + // 'packageEnrollmentStatus.status': 'OPEN WAITLISTED CLOSED', + // }, + // }, + // }, + // }, + // ], + // page: 1, + // pageSize: 500, + // queryString: 'programing', + // selectedTerm: '1194', + // sortOrder: 'SCORE', + // }; + + // Used to search all terms + const data = { + filters: [{ term: { 'subject.subjectCode': '266' } }], + page: 1, + pageSize: 10, + queryString: 'Programming', + selectedTerm: '0000', + sortOrder: 'SCORE', + }; + + // Used to search a specific term + const test = { + selectedTerm: '1194', + queryString: 'programing', + + // 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', + }, + }, + }, + }, + ], + + // These options control how much data we get back + page: 1, + pageSize: 20, + }; + + return this.http.post('/api/search/v1', data, HTTP_OPTIONS); + } + public addCourse( planId: number, subjectCode: string, diff --git a/src/app/degree-planner/store/actions/plan.actions.ts b/src/app/degree-planner/store/actions/plan.actions.ts index 7f3662489808b46b0038090f88387a9a428b786b..22acd35f46db38a42ee8e9d3c16b9d2f9103f8a8 100644 --- a/src/app/degree-planner/store/actions/plan.actions.ts +++ b/src/app/degree-planner/store/actions/plan.actions.ts @@ -68,7 +68,9 @@ export class DeletePlanSuccess implements Action { export class PlanError implements Action { public readonly type = PlanActionTypes.PlanError; - constructor(public payload: { message: string; duration: number; error: any }) {} + constructor( + public payload: { message: string; duration: number; error: any }, + ) {} } export class MakePlanPrimary implements Action { diff --git a/src/app/degree-planner/store/actions/ui.actions.ts b/src/app/degree-planner/store/actions/ui.actions.ts index 21964d32eabe4a0f698ef68849a1eea84ab31977..af6f193ca07520294dfb644eba8ff4aff5a45829 100644 --- a/src/app/degree-planner/store/actions/ui.actions.ts +++ b/src/app/degree-planner/store/actions/ui.actions.ts @@ -2,9 +2,28 @@ import { Action } from '@ngrx/store'; export enum UIActionTypes { ToggleAcademicYear = '[UI] Toggle Academic Year', + + OpenCourseSearch = '[UI] Open Course Search', + CloseCourseSearch = '[UI] Close Course Search', + ToggleCourseSearch = '[UI] Toggle Course Search', } export class ToggleAcademicYear implements Action { public readonly type = UIActionTypes.ToggleAcademicYear; constructor(public payload: { year: string }) {} } + +export class OpenCourseSearch implements Action { + public readonly type = UIActionTypes.OpenCourseSearch; + constructor() {} +} + +export class CloseCourseSearch implements Action { + public readonly type = UIActionTypes.CloseCourseSearch; + constructor() {} +} + +export class ToggleCourseSearch implements Action { + public readonly type = UIActionTypes.ToggleCourseSearch; + constructor() {} +} diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts index dfac9ebf77c861d0cc66d08e3353297afe406f30..3b9021567b3243b5f58f194d11f7723586c004ee 100644 --- a/src/app/degree-planner/store/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -42,7 +42,10 @@ import { DegreePlan } from '@app/core/models/degree-plan'; import { PlannedTerm } from '@app/core/models/planned-term'; import { SubjectMapping } from '@app/core/models/course'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; -import { DegreePlannerState } from '@app/degree-planner/store/state'; +import { + DegreePlannerState, + INITIAL_DEGREE_PLANNER_STATE, +} from '@app/degree-planner/store/state'; @Injectable() export class DegreePlanEffects { @@ -50,7 +53,7 @@ export class DegreePlanEffects { private actions$: Actions, private api: DegreePlannerApiService, private store$: Store<GlobalState>, - private snackBar: MatSnackBar + private snackBar: MatSnackBar, ) {} @Effect() @@ -103,10 +106,19 @@ export class DegreePlanEffects { return { ...payload, expandedYears }; }), - map(payload => new InitialLoadSuccess(payload)), + map( + payload => + new InitialLoadSuccess({ ...INITIAL_DEGREE_PLANNER_STATE, ...payload }), + ), catchError(error => { - return of(new PlanError({ message: 'Something went wrong', duration: 2000, error })); + return of( + new PlanError({ + message: 'Something went wrong', + duration: 2000, + error, + }), + ); }), ); @@ -136,14 +148,20 @@ export class DegreePlanEffects { map(payload => new SwitchPlanSuccess(payload)), - tap((state) => { + tap(state => { const touchedPlan = state.payload.visibleDegreePlan.name; const message = `Switched to ${touchedPlan}`; - this.snackBar.open(message, undefined, {duration: 2000}); + this.snackBar.open(message, undefined, { duration: 2000 }); }), catchError(error => { - return of(new PlanError({ message: 'Unable to switch plan', duration: 2000, error })); + return of( + new PlanError({ + message: 'Unable to switch plan', + duration: 2000, + error, + }), + ); }), ); @@ -189,7 +207,7 @@ export class DegreePlanEffects { }), tap(() => { const message = `Plan name has been changed to ${newName}`; - this.snackBar.open(message, undefined, {duration: 2000}); + this.snackBar.open(message, undefined, { duration: 2000 }); }), catchError(() => { return of(new ChangePlanNameFailure({ roadmapId, oldName })); diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts index 9e82926547961ac9331cdbbc8a14b73c03003579..52b2be499f4c6c0ac856da4c0a4c9e924be2cdff 100644 --- a/src/app/degree-planner/store/reducer.ts +++ b/src/app/degree-planner/store/reducer.ts @@ -32,7 +32,12 @@ import { AddAcademicYearActionTypes, AddAcademicYearRequest, } from '@app/degree-planner/store/actions/addAcademicYear.actions'; -import { ToggleAcademicYear } from '@app/degree-planner/store/actions/ui.actions'; +import { + ToggleAcademicYear, + OpenCourseSearch, + CloseCourseSearch, + ToggleCourseSearch, +} from '@app/degree-planner/store/actions/ui.actions'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; import { DegreePlan } from '@app/core/models/degree-plan'; @@ -54,7 +59,10 @@ type SupportedActions = | ChangePlanNameSuccess | ChangePlanNameFailure | DeletePlanSuccess - | ToggleAcademicYear; + | ToggleAcademicYear + | OpenCourseSearch + | CloseCourseSearch + | ToggleCourseSearch; export function degreePlannerReducer( state = INITIAL_DEGREE_PLANNER_STATE, @@ -107,6 +115,27 @@ export function degreePlannerReducer( return { ...state }; } + /** + * The `ToggleCourseSearch` action toggles the open and close state of the course search side nav + */ + case UIActionTypes.ToggleCourseSearch: { + return { ...state, isCourseSearchVisible: !state.isCourseSearchVisible }; + } + + /** + * The `ToggleCourseSearch` action opens the course search side nav + */ + case UIActionTypes.OpenCourseSearch: { + return { ...state, isCourseSearchVisible: true }; + } + + /** + * The `ToggleCourseSearch` action closes the course search side nav + */ + case UIActionTypes.CloseCourseSearch: { + return { ...state, isCourseSearchVisible: false }; + } + /** * The `WriteNoteResponse` action is dispatched by the `Note.write$` effect * upon a successful response from the `updateNote` or `createNote` API diff --git a/src/app/degree-planner/store/selectors.ts b/src/app/degree-planner/store/selectors.ts index d13ce80112ffb9972a3b5f9b4e5df852c3cc515e..15c890d4ff495cd52de71f0bf8d2dcf763605ca1 100644 --- a/src/app/degree-planner/store/selectors.ts +++ b/src/app/degree-planner/store/selectors.ts @@ -110,6 +110,13 @@ export const getDropZones = createSelector( }, ); +export const isCourseSearchOpen = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + return state.isCourseSearchVisible; + }, +); + export const isPastTerm = (termCode: string) => createSelector( getDegreePlannerState, diff --git a/src/app/degree-planner/store/state.ts b/src/app/degree-planner/store/state.ts index 89d40628b7783de0e6887b83733ce3a1b2b7b709..5adcaf3effbf137c4ca740963f61d5ffc65d5e2d 100644 --- a/src/app/degree-planner/store/state.ts +++ b/src/app/degree-planner/store/state.ts @@ -12,6 +12,7 @@ export interface DegreePlannerState { allDegreePlans: DegreePlan[]; subjects: SubjectMapping; expandedYears: string[]; + isCourseSearchVisible: boolean; } export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { @@ -22,4 +23,5 @@ export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { allDegreePlans: [], subjects: {}, expandedYears: [], + isCourseSearchVisible: false, }; 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 97a445d1b0c0db68e15520ae8e2cceeee4ae92f2..82a5eaf8ed09a44b9e7b11fff22101e4b7752a57 100644 --- a/src/app/degree-planner/term-container/term-container.component.html +++ b/src/app/degree-planner/term-container/term-container.component.html @@ -58,6 +58,6 @@ </div> </div> <div class="add-new-wrapper" *ngIf="!(isPastTerm$ | async)"> - <button mat-raised-button (click)="openAddSidenav()" class="add-course-button">+ Add Course</button> + <button mat-raised-button (click)="toggleCourseSearch()" class="add-course-button">+ Add Course</button> </div> </mat-card> \ No newline at end of file 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 15a64bba5e4b9ba43c88728fc22b69a3add2e5e7..d856aff287ce331f6a4152c51b44ce7e450986e2 100644 --- a/src/app/degree-planner/term-container/term-container.component.ts +++ b/src/app/degree-planner/term-container/term-container.component.ts @@ -18,6 +18,12 @@ import { RemoveSaveForLater, } from '@app/degree-planner/store/actions/course.actions'; +import { + ToggleCourseSearch, + OpenCourseSearch, + CloseCourseSearch, +} from '@app/degree-planner/store/actions/ui.actions'; + // Selectors import { getDropZones, isPastTerm } from '@app/degree-planner/store/selectors'; @@ -70,6 +76,10 @@ export class TermContainerComponent implements OnInit { }, 0); } + toggleCourseSearch() { + this.store.dispatch(new ToggleCourseSearch()); + } + drop(event: CdkDragDrop<string[]>) { const newContainer = event.container.id; const previousContainer = event.previousContainer.id; @@ -98,6 +108,13 @@ export class TermContainerComponent implements OnInit { // Dispatch the add event this.store.dispatch(new AddCourse({ subjectCode, courseId, termCode })); this.store.dispatch(new RemoveSaveForLater({ subjectCode, courseId })); + } else if ( + previousContainer === 'queried-courses-list' && + newContainer.indexOf('term-') === 0 + ) { + const termCode = newContainer.substr(5); + const { subjectCode, courseId } = event.item.data; + this.store.dispatch(new AddCourse({ subjectCode, courseId, termCode })); } } }