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