From 94be34883f72f0f5f4971887e37f2f8f1015b8e8 Mon Sep 17 00:00:00 2001 From: Scott Berg <scott.berg@wisc.edu> Date: Wed, 20 Feb 2019 17:07:30 +0000 Subject: [PATCH] Add selected term tracking. Separate search into it's own component --- .../course-search.component.html | 57 +++++ .../course-search.component.scss | 6 + .../course-search/course-search.component.ts | 228 ++++++++++++++++++ .../degree-planner.component.html | 101 +------- .../degree-planner.component.scss | 7 - .../degree-planner.component.ts | 150 +----------- .../degree-planner/degree-planner.module.ts | 2 + .../store/actions/ui.actions.ts | 11 +- .../store/effects/plan.effects.ts | 1 - src/app/degree-planner/store/reducer.ts | 46 +++- src/app/degree-planner/store/selectors.ts | 22 +- src/app/degree-planner/store/state.ts | 4 +- .../term-container.component.html | 2 +- .../term-container.component.ts | 9 +- 14 files changed, 375 insertions(+), 271 deletions(-) create mode 100644 src/app/degree-planner/course-search/course-search.component.html create mode 100644 src/app/degree-planner/course-search/course-search.component.scss create mode 100644 src/app/degree-planner/course-search/course-search.component.ts diff --git a/src/app/degree-planner/course-search/course-search.component.html b/src/app/degree-planner/course-search/course-search.component.html new file mode 100644 index 0000000..2b908f4 --- /dev/null +++ b/src/app/degree-planner/course-search/course-search.component.html @@ -0,0 +1,57 @@ +<form [formGroup]='courseSearchForm' (ngSubmit)="formSubmit()" class="add-course-form" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> + + <mat-form-field> + <mat-select placeholder="Term" aria-label="Term" formControlName="term"> + <mat-option value="0000">All</mat-option> + <mat-option *ngFor="let yearCode of activeTerms$ | async" [value]="yearCode">{{yearCode | getTermDescription}}</mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field class="example-full-width"> + <input type="text" placeholder="Subject" aria-label="Subject" matInput formControlName="subject" [matAutocomplete]="subject"> + <mat-autocomplete autoActiveFirstOption #subject="matAutocomplete"> + <mat-option *ngFor="let subject of filteredSubjects | keyvalue" [value]="subject.value | titlecase">{{subject.value | titlecase}}</mat-option> + </mat-autocomplete> + </mat-form-field> + + <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> + <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)="resetSearchForm()">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="margin-top: 20px; padding: 12px 22px;"> + <div *ngIf="hasResults && queriedCourses.length === 0" class="mat-typography" style="text-align: center;"> + <img style="width: 50%;" src="../../assets/img/bucky-sad.svg" alt="No results found"> + <h3>No search results found.</h3> + </div> + + <div + cdkDropList + id="queried-courses-list" + [cdkDropListData]="queriedCourses" + [cdkDropListConnectedTo]="dropZoneIds$ | async" + > + <div + class="course-wrapper" + cdkDrag + [cdkDragData]="course" + *ngFor="let course of queriedCourses" + > + <div class="course-wrapper-inner"> + <cse-course-item [course]="course" type="search"></cse-course-item> + </div> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/app/degree-planner/course-search/course-search.component.scss b/src/app/degree-planner/course-search/course-search.component.scss new file mode 100644 index 0000000..103d8a4 --- /dev/null +++ b/src/app/degree-planner/course-search/course-search.component.scss @@ -0,0 +1,6 @@ +// Course search +.search-input-wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/src/app/degree-planner/course-search/course-search.component.ts b/src/app/degree-planner/course-search/course-search.component.ts new file mode 100644 index 0000000..6db84a0 --- /dev/null +++ b/src/app/degree-planner/course-search/course-search.component.ts @@ -0,0 +1,228 @@ +import { Observable, Subscription } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; +import { Store, select } from '@ngrx/store'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { GlobalState } from '@app/core/state'; +import * as utils from '@app/degree-planner/shared/utils'; +import * as selectors from '@app/degree-planner/store/selectors'; + +// API +import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; + +// Forms +import { FormBuilder, FormGroup, AbstractControl } from '@angular/forms'; + +// snackBar +import { MatSnackBar } from '@angular/material'; + +// Data modles +import { Course, SubjectMapping } from '@app/core/models/course'; + +@Component({ + selector: 'cse-course-search', + templateUrl: './course-search.component.html', + styleUrls: ['./course-search.component.scss'], +}) +export class CourseSearchComponent implements OnInit, OnDestroy { + // Internal variables used to store search info + public queriedCourses: Course[]; + public hasResults: boolean; + public isLoading: boolean; + public courseSearchForm: FormGroup; + + // Observable used for drag and drop and for populating term select + public dropZoneIds$: Observable<string[]>; + public activeTerms$: Observable<string[]>; + public activeSelectedSearchTerm$: Observable<string>; + public isCourseSearchOpen$: Observable<boolean>; + + // Internal variables to store subjects and filtered subjects list + public subjects: SubjectMapping; + public filteredSubjects: SubjectMapping; + + public termSubscription: Subscription; + public searchOpenSubscribe: Subscription; + + constructor( + private store: Store<GlobalState>, + private fb: FormBuilder, + private api: DegreePlannerApiService, + private snackBar: MatSnackBar, + ) {} + + public ngOnInit(): void { + // Internal values used to manage loading state + this.queriedCourses = []; + this.hasResults = false; + this.isLoading = false; + + // Get active term drop zones + this.dropZoneIds$ = this.store.pipe( + select(selectors.selectAllVisibleYears), + utils.yearsToDropZoneIds(), + distinctUntilChanged(utils.compareStringArrays), + ); + + // Get active terms to populate term select box + this.activeTerms$ = this.store.pipe(select(selectors.getActiveTerms)); + + // TODO unsubscribe on disconnect + // Get observable for the search open state + this.isCourseSearchOpen$ = this.store.pipe( + select(selectors.isCourseSearchOpen), + ); + + // Reset serach form when closed + this.searchOpenSubscribe = this.isCourseSearchOpen$.subscribe(openState => { + if (!openState) { + this.resetSearchForm(); + } + }); + + // Get all subjects and set them to internal values + this.store + .pipe(select(selectors.getSubjectDescriptions)) + .subscribe(subjects => { + this.subjects = { [-1]: 'All', ...subjects }; + this.filteredSubjects = { [-1]: 'All', ...subjects }; + }); + + // Deafults for the search form + this.courseSearchForm = this.fb.group({ + term: '0000', + subject: 'All', + search: '', + }); + + // TODO Unsubscribe from this on component disconnect + // Get observable for currently active term + this.activeSelectedSearchTerm$ = this.store.pipe( + select(selectors.getActiveSelectedSearchTerm), + ); + + // Listen for changes on the active selected term and update the form when it changes + this.termSubscription = this.activeSelectedSearchTerm$.subscribe( + termCode => { + const values = this.courseSearchForm.value; + this.courseSearchForm.setValue({ ...values, term: termCode }); + }, + ); + + // Listen for changes to the term value + (this.courseSearchForm.get( + 'term', + ) as AbstractControl).valueChanges.subscribe(term => { + if (this.hasResults) { + const { search, subject } = this.courseSearchForm.value; + this.search(search, term, subject); + } + }); + + // Listen for changes in the subject input + (this.courseSearchForm.get( + 'subject', + ) as AbstractControl).valueChanges.subscribe(value => { + // If the subject value is blank, reset the filtered subjects + if (value === '') { + this.filteredSubjects = this.subjects; + return; + } + + // Create an object to store fitlered subjects + const filtered = {}; + + // Filter the terms based on users search + Object.entries(this.subjects).map(subject => { + const [key, name] = subject; + + const search = name.replace(/\s/g, ''); + + if (search.toLowerCase().indexOf(value.toLowerCase()) === 0) { + filtered[key] = name; + } + }); + + this.filteredSubjects = filtered; + }); + } + + // Unsubscribe from subs to prevent memeory leaks + public ngOnDestroy() { + this.termSubscription.unsubscribe(); + this.searchOpenSubscribe.unsubscribe(); + } + + // Function that runs on form submit + // Get form values and run a search + public formSubmit() { + const { search, term, subject } = this.courseSearchForm.value; + this.search(search, term, subject); + } + + // Run a search and display all results if the serach is valid + public search(search, term, subject) { + // Get the form field values + let subjectCode; + + // Check if subject is valid + Object.entries(this.subjects).forEach(option => { + const [key, value] = option; + if (value.toLowerCase() === subject.toLowerCase()) { + subjectCode = key; + } + }); + + if (!subjectCode) { + this.snackBar.open('Please select a valid subject', undefined, { + duration: 6000, + }); + return; + } + + // Set the internal UI state + this.isLoading = true; + this.hasResults = false; + this.queriedCourses = []; + + // Hit the search API + this.api + .searchCourses({ + subjectCode, + searchText: search, + termCode: term === '' ? '0000' : term, + }) + .toPromise() + .then(res => { + // 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, + subject: course.subject.description, + subjectCode: course.subject.subjectCode, + }; + }); + }) + .catch(console.log); + } + + // Reset the search form values and clear results + public resetSearchForm() { + this.queriedCourses = []; + this.hasResults = false; + + // Ensure that the search form has been created before attempting to set values + if (this.courseSearchForm) { + this.courseSearchForm.setValue({ + subject: 'All', + search: '', + term: '0000', + }); + } + } +} diff --git a/src/app/degree-planner/degree-planner.component.html b/src/app/degree-planner/degree-planner.component.html index 4ecd026..64375e7 100644 --- a/src/app/degree-planner/degree-planner.component.html +++ b/src/app/degree-planner/degree-planner.component.html @@ -5,108 +5,9 @@ <button mat-button class="close-btn" (click)="closeCourseSearch();"><i class="material-icons">keyboard_arrow_right</i></button> </mat-toolbar> - <form [formGroup]='courseSearchForm' (ngSubmit)="search()" class="add-course-form" fxLayout="column" fxLayoutAlign="space-around none" style="padding: 12px 22px;"> - - <mat-form-field> - <mat-select placeholder="Term" aria-label="Term" formControlName="term"> - <mat-option value="0000">All</mat-option> - <mat-option *ngFor="let yearCode of activeTerms$ | async" [value]="yearCode">{{yearCode | getTermDescription}}</mat-option> - </mat-select> - </mat-form-field> - - <mat-form-field class="example-full-width"> - <input type="text" placeholder="Subject" aria-label="Subject" matInput formControlName="subject" [matAutocomplete]="subject"> - <mat-autocomplete autoActiveFirstOption #subject="matAutocomplete"> - <mat-option *ngFor="let subject of filteredSubjects | keyvalue" [value]="subject.value">{{subject.value}}</mat-option> - </mat-autocomplete> - </mat-form-field> - - <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> - <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="margin-top: 20px; padding: 12px 22px;"> - <div *ngIf="hasResults && queriedCourses.length === 0" class="mat-typography" style="text-align: center;"> - <img style="width: 50%;" src="../../assets/img/bucky-sad.svg" alt="No results found"> - <h3>No search results found.</h3> - </div> - - <div - cdkDropList - id="queried-courses-list" - [cdkDropListData]="queriedCourses" - [cdkDropListConnectedTo]="dropZoneIds$ | async" - > - <div - class="course-wrapper" - cdkDrag - [cdkDragData]="course" - *ngFor="let course of queriedCourses" - > - <div class="course-wrapper-inner"> - <cse-course-item [course]="course" type="search"></cse-course-item> - </div> - </div> - </div> - </div> - </div> + <cse-course-search></cse-course-search> </mat-sidenav> - <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;"> - <div - cdkDropList - id="queried-courses-list" - [cdkDropListData]="queriedCourses" - [cdkDropListConnectedTo]="getTermDropZone()"> - <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> - </div> - </div> - </div> - </div> - - <div - cdkDropList - id="queried-courses-list" - [cdkDropListData]="queriedCourses" - [cdkDropListConnectedTo]="dropZoneIds$ | 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> - </div> - </div> - <mat-sidenav-content> <mat-sidenav-container id="plans-container"> <!-- Menu side nav --> diff --git a/src/app/degree-planner/degree-planner.component.scss b/src/app/degree-planner/degree-planner.component.scss index 191df4e..4dfac0a 100644 --- a/src/app/degree-planner/degree-planner.component.scss +++ b/src/app/degree-planner/degree-planner.component.scss @@ -48,10 +48,3 @@ 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 5f4f04e..2a4c91b 100644 --- a/src/app/degree-planner/degree-planner.component.ts +++ b/src/app/degree-planner/degree-planner.component.ts @@ -16,11 +16,8 @@ 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 { FormBuilder, FormGroup } from '@angular/forms'; -import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; import * as selectors from '@app/degree-planner/store/selectors'; import * as utils from '@app/degree-planner/shared/utils'; -import { Course, SubjectMapping } from '@app/core/models/course'; // Actions import { @@ -36,10 +33,7 @@ import { DialogMode, } from './dialogs/modify-plan-dialog/modify-plan-dialog.component'; import { PromptDialogComponent } from '@app/shared/dialogs/prompt-dialog/prompt-dialog.component'; -import { - ToggleAcademicYear, - CloseCourseSearch, -} from './store/actions/ui.actions'; +import { CloseCourseSearch } from './store/actions/ui.actions'; @Component({ selector: 'cse-degree-planner', @@ -55,29 +49,14 @@ 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[]>; public yearCodes$: Observable<string[]>; - public dropZoneIds$: Observable<string[]>; - - // Search variables - public queriedCourses: Course[]; - public hasResults: boolean; - public isLoading: boolean; - public courseSearchForm: FormGroup; - public isCourseSearchVisible$: Observable<boolean>; - - public subjects: SubjectMapping; - public filteredSubjects: SubjectMapping; + 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)'); } @@ -92,70 +71,16 @@ export class DegreePlannerComponent implements OnInit { select(selectors.selectAllDegreePlans), ); - this.isCourseSearchVisible$ = this.store.pipe( - select(selectors.selectCourseSearchVisibility), + // Get observable for the search open state + this.isCourseSearchOpen$ = this.store.pipe( + select(selectors.isCourseSearchOpen), ); - // State attributes needed to create the search form - this.subjects$ = this.store.pipe(select(selectors.getSubjects)); - this.activeTerms$ = this.store.pipe(select(selectors.getActiveTerms)); - - // Internal values used to manage loading state - this.queriedCourses = []; - this.hasResults = false; - this.isLoading = false; - this.yearCodes$ = this.store.pipe( select(selectors.selectAllVisibleYears), map(years => Object.keys(years)), distinctUntilChanged(utils.compareStringArrays), ); - - this.store - .pipe(select(selectors.getSubjectDescriptions)) - .subscribe(subjects => { - this.subjects = { [-1]: 'All', ...subjects }; - this.filteredSubjects = { [-1]: 'All', ...subjects }; - }); - - this.dropZoneIds$ = this.store.pipe( - select(selectors.selectAllVisibleYears), - utils.yearsToDropZoneIds(), - distinctUntilChanged(utils.compareStringArrays), - ); - - // Deafults for the search form - this.courseSearchForm = this.fb.group({ - term: '0000', - subject: 'All', - search: '', - }); - - this.courseSearchForm.valueChanges.subscribe(values => { - if (values.subject === '') { - this.filteredSubjects = this.subjects; - return; - } - - const filtered = {}; - - // Filter the terms based on users search - Object.entries(this.subjects).map(subject => { - const [key, name] = subject; - - const search = name.replace(/\s/g, ''); - - if (search.toLowerCase().indexOf(values.subject.toLowerCase()) === 0) { - filtered[key] = name; - } - }); - - this.filteredSubjects = filtered; - }); - - this.isCourseSearchOpen$ = this.store.pipe( - select(selectors.isCourseSearchOpen), - ); } public handleAcademicYearToggle(year: Year): void { @@ -279,71 +204,6 @@ export class DegreePlannerComponent implements OnInit { public closeCourseSearch() { this.store.dispatch(new CloseCourseSearch()); } - - public search() { - // Get the form field values - const { search, term, subject } = this.courseSearchForm.value; - let subjectCode; - - // Check if subject is valid - Object.entries(this.subjects).forEach(option => { - const [key, value] = option; - if (value === subject) { - subjectCode = key; - } - }); - - if (!subjectCode) { - this.snackBar.open('Please select a valid subject', undefined, { - duration: 6000, - }); - return; - } - - console.log(search, term, subject, subjectCode); - - // Set the internal UI state - this.isLoading = true; - this.hasResults = false; - this.queriedCourses = []; - - // Hit the search API - this.api - .searchCourses({ - subjectCode, - searchText: search, - termCode: term === '' ? '0000' : term, - }) - .toPromise() - .then(res => { - // 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, - subject: course.subject.description, - subjectCode: course.subject.subjectCode, - }; - }); - }) - .catch(console.log); - } - - public resetSearch() { - // Reset the internal state and form values - this.queriedCourses = []; - this.hasResults = false; - this.courseSearchForm.setValue({ - subject: 'All', - search: '', - term: '0000', - }); - } } const isntUndefined = <T>(anything: T | undefined): anything is T => { diff --git a/src/app/degree-planner/degree-planner.module.ts b/src/app/degree-planner/degree-planner.module.ts index 82ea27c..c38fd13 100644 --- a/src/app/degree-planner/degree-planner.module.ts +++ b/src/app/degree-planner/degree-planner.module.ts @@ -12,6 +12,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { RemoveCourseConfirmDialogComponent } from './dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component'; import { ModifyPlanDialogComponent } from './dialogs/modify-plan-dialog/modify-plan-dialog.component'; import { YearContainerComponent } from '@app/degree-planner/year-container/year-container.component'; +import { CourseSearchComponent } from '@app/degree-planner/course-search/course-search.component'; @NgModule({ imports: [SharedModule, DragDropModule, DegreePlannerRoutingModule], @@ -26,6 +27,7 @@ import { YearContainerComponent } from '@app/degree-planner/year-container/year- RemoveCourseConfirmDialogComponent, ModifyPlanDialogComponent, YearContainerComponent, + CourseSearchComponent, ], entryComponents: [ NotesDialogComponent, diff --git a/src/app/degree-planner/store/actions/ui.actions.ts b/src/app/degree-planner/store/actions/ui.actions.ts index 1a134ab..59b3fc3 100644 --- a/src/app/degree-planner/store/actions/ui.actions.ts +++ b/src/app/degree-planner/store/actions/ui.actions.ts @@ -8,6 +8,8 @@ export enum UIActionTypes { OpenCourseSearch = '[UI] Open Course Search', CloseCourseSearch = '[UI] Close Course Search', ToggleCourseSearch = '[UI] Toggle Course Search', + + UpdateSearchTermCode = '[UI] Change Search Term Code', } export class ToggleAcademicYear implements Action { @@ -27,7 +29,7 @@ export class CollapseAcademicYear implements Action { export class OpenCourseSearch implements Action { public readonly type = UIActionTypes.OpenCourseSearch; - constructor() {} + constructor(public termCode?: string) {} } export class CloseCourseSearch implements Action { @@ -37,5 +39,10 @@ export class CloseCourseSearch implements Action { export class ToggleCourseSearch implements Action { public readonly type = UIActionTypes.ToggleCourseSearch; - constructor() {} + constructor(public termCode?: string) {} +} + +export class UpdateSearchTermCode implements Action { + public readonly type = UIActionTypes.UpdateSearchTermCode; + constructor(public termCode: string) {} } diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts index 11daebd..d737d6d 100644 --- a/src/app/degree-planner/store/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -103,7 +103,6 @@ export class DegreePlanEffects { allDegreePlans: of(allDegreePlans), subjects: of(subjects), expandedYears: of([] as string[]), - isCourseSearchVisible: of(false), subjectDescriptions: of(descriptions), }); }, diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts index 49af12e..393ef74 100644 --- a/src/app/degree-planner/store/reducer.ts +++ b/src/app/degree-planner/store/reducer.ts @@ -40,6 +40,7 @@ import { OpenCourseSearch, CloseCourseSearch, ToggleCourseSearch, + UpdateSearchTermCode, } 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'; @@ -72,7 +73,8 @@ type SupportedActions = | CollapseAcademicYear | OpenCourseSearch | CloseCourseSearch - | ToggleCourseSearch; + | ToggleCourseSearch + | UpdateSearchTermCode; export function degreePlannerReducer( state = INITIAL_DEGREE_PLANNER_STATE, @@ -144,21 +146,57 @@ export function degreePlannerReducer( * The `ToggleCourseSearch` action toggles the open and close state of the course search side nav */ case UIActionTypes.ToggleCourseSearch: { - return { ...state, isCourseSearchVisible: !state.isCourseSearchVisible }; + const newSearchState = { + ...state.search, + visible: !state.search.visible, + }; + + // If a term was passed into the action + if (action.termCode) { + newSearchState.selectedTerm = action.termCode; + } + + return { + ...state, + search: newSearchState, + }; } /** * The `ToggleCourseSearch` action opens the course search side nav */ case UIActionTypes.OpenCourseSearch: { - return { ...state, isCourseSearchVisible: true }; + const newSearchState = { + ...state.search, + visible: true, + }; + + // If a term was passed into the action + if (action.termCode) { + newSearchState.selectedTerm = action.termCode; + } + + return { ...state, search: newSearchState }; } /** * The `ToggleCourseSearch` action closes the course search side nav */ case UIActionTypes.CloseCourseSearch: { - return { ...state, isCourseSearchVisible: false }; + return { + ...state, + search: { ...state.search, visible: false, selectedTerm: '0000' }, + }; + } + + /** + * The `UpdateSearchTermCode` action changes the active seach term code. + */ + case UIActionTypes.UpdateSearchTermCode: { + return { + ...state, + search: { ...state.search, selectedTerm: action.termCode }, + }; } /** diff --git a/src/app/degree-planner/store/selectors.ts b/src/app/degree-planner/store/selectors.ts index 3a2d5d7..5aa3813 100644 --- a/src/app/degree-planner/store/selectors.ts +++ b/src/app/degree-planner/store/selectors.ts @@ -24,11 +24,6 @@ export const selectAllDegreePlans = createSelector( state => state.allDegreePlans, ); -export const selectCourseSearchVisibility = createSelector( - (state: GlobalState) => state.degreePlanner, - state => state.isCourseSearchVisible, -); - export const getSavedForLaterCourses = createSelector( (state: GlobalState) => state.degreePlanner, state => state.savedForLaterCourses, @@ -89,7 +84,22 @@ export const getSubjectDescriptions = createSelector( export const isCourseSearchOpen = createSelector( getDegreePlannerState, (state: DegreePlannerState) => { - return state.isCourseSearchVisible; + return state.search.visible; + }, +); + +export const getSelectedSearchTerm = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + return state.search.selectedTerm; + }, +); + +export const getActiveSelectedSearchTerm = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + const { selectedTerm } = state.search; + return state.activeTermCodes.includes(selectedTerm) ? selectedTerm : '0000'; }, ); diff --git a/src/app/degree-planner/store/state.ts b/src/app/degree-planner/store/state.ts index 45ccb17..2ff30aa 100644 --- a/src/app/degree-planner/store/state.ts +++ b/src/app/degree-planner/store/state.ts @@ -13,7 +13,7 @@ export interface DegreePlannerState { subjects: SubjectMapping; subjectDescriptions: SubjectMapping; expandedYears: string[]; - isCourseSearchVisible: boolean; + search: { visible: boolean; selectedTerm: string }; } export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { @@ -25,5 +25,5 @@ export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { subjects: {}, subjectDescriptions: {}, expandedYears: [], - isCourseSearchVisible: false, + search: { visible: false, selectedTerm: '0000' }, }; 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 87a1dc0..28e20d0 100644 --- a/src/app/degree-planner/term-container/term-container.component.html +++ b/src/app/degree-planner/term-container/term-container.component.html @@ -70,7 +70,7 @@ <button mat-raised-button class="add-course-button" - (click)="toggleCourseSearch()"> + (click)="openCourseSearch()"> + Add Course </button> </div> 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 3bd6cd1..5874b5b 100644 --- a/src/app/degree-planner/term-container/term-container.component.ts +++ b/src/app/degree-planner/term-container/term-container.component.ts @@ -5,7 +5,10 @@ import { Observable } from 'rxjs'; import { filter, map, distinctUntilChanged } from 'rxjs/operators'; import { Store, select } from '@ngrx/store'; import { DegreePlannerState } from '@app/degree-planner/store/state'; -import { ToggleCourseSearch } from '@app/degree-planner/store/actions/ui.actions'; +import { + ToggleCourseSearch, + OpenCourseSearch, +} from '@app/degree-planner/store/actions/ui.actions'; // Models import * as actions from '@app/degree-planner/store/actions/course.actions'; @@ -105,8 +108,8 @@ export class TermContainerComponent implements OnInit { this.dialog.open(NotesDialogComponent, { data }); } - toggleCourseSearch() { - this.store.dispatch(new ToggleCourseSearch()); + openCourseSearch() { + this.store.dispatch(new OpenCourseSearch(this.termCode)); } drop(event: CdkDragDrop<string>) { -- GitLab