diff --git a/src/app/core/models/user-preferences.ts b/src/app/core/models/user-preferences.ts new file mode 100644 index 0000000000000000000000000000000000000000..cba6c97093cbf85aa44e235e7fa485c9045822f4 --- /dev/null +++ b/src/app/core/models/user-preferences.ts @@ -0,0 +1,4 @@ +export interface UserPreferences { + degreePlannerGradesVisibility?: boolean; + degreePlannerSelectedPlan?: number; +} diff --git a/src/app/degree-planner/degree-planner.component.html b/src/app/degree-planner/degree-planner.component.html index 361fbd480ac753a2e7e5c88e607ca124d2adf4ee..2fa3e899d1a66e5297009c9bf51c943aa693f340 100644 --- a/src/app/degree-planner/degree-planner.component.html +++ b/src/app/degree-planner/degree-planner.component.html @@ -101,10 +101,22 @@ fxLayoutAlign="start stretch" style="margin:24px 24px 72px 24px"> <div id="year-mask" *ngIf="(isLoadingPlan$ | async)"></div> - <div id="accordion-controls"> - <button mat-button color="primary" (click)="toggleAllYears(true)" aria-label="Expand All Years">Expand All</button> - <span>|</span> - <button mat-button color="primary" (click)="toggleAllYears(false)" aria-label="Collapse All Years">Collapse All</button> + <div id="plan-controls"> + <div> + <mat-slide-toggle + color="primary" + labelPosition="before" + [checked]="showGrades$ | async" + (change)="changeGradeVisibility($event)"> + Show Grades + </mat-slide-toggle> + </div> + + <div class="expand-collapse"> + <button mat-button color="primary" (click)="toggleAllYears(true)" aria-label="Expand All Years">Expand All</button> + <span>|</span> + <button mat-button color="primary" (click)="toggleAllYears(false)" aria-label="Collapse All Years">Collapse All</button> + </div> </div> <mat-accordion multi="true"> <cse-year-container diff --git a/src/app/degree-planner/degree-planner.component.scss b/src/app/degree-planner/degree-planner.component.scss index 08606287801b00bbc347411266af16d4c50ecbaa..88ec14249faabdf0eaf3e9c7228e340625804aac 100644 --- a/src/app/degree-planner/degree-planner.component.scss +++ b/src/app/degree-planner/degree-planner.component.scss @@ -112,7 +112,12 @@ mat-sidenav { display: none; } -#accordion-controls { - text-align: right; +#plan-controls { + display: flex; + justify-content: space-between; + align-items: center; +} + +.expand-collapse { color: map-get($uw-primary, 500); } diff --git a/src/app/degree-planner/degree-planner.component.ts b/src/app/degree-planner/degree-planner.component.ts index e98ae348e975bbb102ce62c0bbc187db42ec32e4..7e73bc19c0cad9f8ce27a50b255b2619de4e1f57 100644 --- a/src/app/degree-planner/degree-planner.component.ts +++ b/src/app/degree-planner/degree-planner.component.ts @@ -8,7 +8,7 @@ import { OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { select } from '@ngrx/store'; import { Component } from '@angular/core'; -import { MatSelectChange } from '@angular/material'; +import { MatSelectChange, MatSlideToggleChange } from '@angular/material'; import { MatDialog } from '@angular/material'; import { Store } from '@ngrx/store'; import { MediaMatcher } from '@angular/cdk/layout'; @@ -24,6 +24,7 @@ import { CreatePlan, ChangePlanName, DeletePlan, + ChangeGradeVisibility, } from '@app/degree-planner/store/actions/plan.actions'; import { PromptDialogComponent } from '@app/shared/dialogs/prompt-dialog/prompt-dialog.component'; import { ConfirmDialogComponent } from '@app/shared/dialogs/confirm-dialog/confirm-dialog.component'; @@ -47,6 +48,7 @@ export class DegreePlannerComponent implements OnInit { public termsByAcademicYear: Object; public mobileView: MediaQueryList; public coursesData$: any; + public showGrades$: Observable<boolean>; public degreePlan$: Observable<DegreePlan | undefined>; public allDegreePlans$: Observable<ReadonlyArray<DegreePlan>>; public termsByYear$: Observable<ReadonlyArray<Year>>; @@ -73,6 +75,8 @@ export class DegreePlannerComponent implements OnInit { filter(isntUndefined), ); + this.showGrades$ = this.store.pipe(select(selectors.selectGradeVisibility)); + this.allDegreePlans$ = this.store.pipe( select(selectors.selectAllDegreePlans), ); @@ -248,6 +252,10 @@ export class DegreePlannerComponent implements OnInit { return termCodes; } + public changeGradeVisibility(event: MatSlideToggleChange) { + this.store.dispatch(new ChangeGradeVisibility(event.checked)); + } + public closeCourseSearch() { this.store.dispatch(new CloseCourseSearch()); } diff --git a/src/app/degree-planner/shared/course-item/course-item.component.html b/src/app/degree-planner/shared/course-item/course-item.component.html index 8a7430e917ac739d789a328f775244b927e30105..053a4e9c3d586274aa3a86ce177e15d65e695a31 100644 --- a/src/app/degree-planner/shared/course-item/course-item.component.html +++ b/src/app/degree-planner/shared/course-item/course-item.component.html @@ -120,7 +120,8 @@ </mat-menu> </div> <div *ngIf="disabled" fxLayout="row" fxLayoutAlign="end center"> - <p attr.aria-label="grade {{ course.grade }}">{{ course.grade || ' ' }}</p> + <p *ngIf="!(showGrades$ | async)">{{ ' ' }}</p> + <p *ngIf="showGrades$ | async" attr.aria-label="grade {{ course.grade }}">{{ course.grade || ' ' }}</p> </div> <div fxLayout="row" fxLayoutAlign="end center"> <p *ngIf="type !== 'saved'" diff --git a/src/app/degree-planner/shared/course-item/course-item.component.ts b/src/app/degree-planner/shared/course-item/course-item.component.ts index 5171c9743531d7b1f4da3f4fbe3f13b542b5e200..a464f25c9a0d69b85a0a9310fde1ad6205cc104c 100644 --- a/src/app/degree-planner/shared/course-item/course-item.component.ts +++ b/src/app/degree-planner/shared/course-item/course-item.component.ts @@ -54,6 +54,7 @@ export class CourseItemComponent implements OnInit { public plannedCourses: ReadonlyArray<Course>; public toActiveTerm: boolean; public mobileView: MediaQueryList; + public showGrades$: Observable<boolean>; constructor( private api: DegreePlannerApiService, @@ -93,6 +94,8 @@ export class CourseItemComponent implements OnInit { } else { this.status = 'Normal'; } + + this.showGrades$ = this.store.pipe(select(selectors.selectGradeVisibility)); } onMenuOpen() { diff --git a/src/app/degree-planner/store/actions/plan.actions.ts b/src/app/degree-planner/store/actions/plan.actions.ts index 60376326f8202a41cfb454b3647ccb3b7131f89d..55d2b5a189ec095728244563d9c733ddf556a9ab 100644 --- a/src/app/degree-planner/store/actions/plan.actions.ts +++ b/src/app/degree-planner/store/actions/plan.actions.ts @@ -25,6 +25,8 @@ export enum PlanActionTypes { ChangePlanName = '[Plan] Change Plan Name', ChangePlanNameSuccess = '[Plan] Change Plan Name (Success)', ChangePlanNameFailure = '[Plan] CHange Plan Name (Failure)', + + ChangeGradeVisibility = '[Plan] Change Grade Visibility', } export class InitialLoadSuccess implements Action { @@ -37,6 +39,11 @@ export class SwitchPlan implements Action { constructor(public payload: { newVisibleRoadmapId: number }) {} } +export class ChangeGradeVisibility implements Action { + public readonly type = PlanActionTypes.ChangeGradeVisibility; + constructor(public visibility: boolean) {} +} + export class SwitchPlanSuccess implements Action { public readonly type = PlanActionTypes.SwitchPlanSuccess; constructor( diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts index 9f824e6d505fca2947677a64693e8eb6567032bf..9948bf01c0b07d102b892acfed4255ef0d21d835 100644 --- a/src/app/degree-planner/store/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -30,12 +30,14 @@ import { CreatePlanSuccess, DeletePlan, DeletePlanSuccess, + ChangeGradeVisibility, } from '@app/degree-planner/store/actions/plan.actions'; import * as utils from '@app/degree-planner/shared/utils'; import { DegreePlan } from '@app/core/models/degree-plan'; import { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term'; import { INITIAL_DEGREE_PLANNER_STATE } from '@app/degree-planner/store/state'; import { YearMapping, MutableYearMapping } from '@app/core/models/year'; +import { UserPreferences } from '@app/core/models/user-preferences'; import { Note } from '@app/core/models/note'; import { CourseBase, Course } from '@app/core/models/course'; import { pickTermEra } from '@app/degree-planner/shared/utils'; @@ -104,7 +106,13 @@ export class DegreePlanEffects { }); } + const showGrades = + userPreferences.degreePlannerGradesVisibility !== undefined + ? userPreferences.degreePlannerGradesVisibility + : true; + return forkJoinWithKeys({ + showGrades: of(showGrades), visibleDegreePlan: of(visibleDegreePlan), visibleYears, savedForLaterCourses, @@ -158,7 +166,9 @@ export class DegreePlanEffects { this.snackBar.open(message, undefined, {}); // Get the users current preferences and update the selected roadmapId - this.updateSelectedPlan(state.payload.visibleDegreePlan.roadmapId); + this.setUserPreferences({ + degreePlannerSelectedPlan: state.payload.visibleDegreePlan.roadmapId, + }); }), catchError(error => { return of( @@ -171,6 +181,27 @@ export class DegreePlanEffects { }), ); + @Effect({ dispatch: false }) + gradeVisibility$ = this.actions$.pipe( + ofType<ChangeGradeVisibility>(PlanActionTypes.ChangeGradeVisibility), + withLatestFrom(this.store$), + map(([change, state]) => { + this.setUserPreferences({ + degreePlannerGradesVisibility: change.visibility, + }); + return state; + }), + catchError(error => { + return of( + new PlanError({ + message: 'Unable to change grade visibility', + duration: 2000, + error, + }), + ); + }), + ); + @Effect() MakePlanPrimary$ = this.actions$.pipe( ofType<MakePlanPrimary>(PlanActionTypes.MakePlanPrimary), @@ -251,7 +282,9 @@ export class DegreePlanEffects { }); }), map(({ newPlan, newYears }) => { - this.updateSelectedPlan(newPlan.roadmapId); + this.setUserPreferences({ + degreePlannerSelectedPlan: newPlan.roadmapId, + }); return new CreatePlanSuccess({ newPlan, newYears }); }), tap(() => { @@ -295,7 +328,7 @@ export class DegreePlanEffects { }), ); - private updateSelectedPlan(roadmapId: number) { + private setUserPreferences(changes: UserPreferences) { // Get the users current preferences and update the selected roadmapId this.api @@ -305,7 +338,7 @@ export class DegreePlanEffects { this.api .updateUserPreferences({ ...prefs, - degreePlannerSelectedPlan: roadmapId, + ...changes, }) .toPromise(); // We have to .toPromise this to actually fire the API call diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts index d1f1354727268f195fff4b853d7fa114ae9add86..d47f60de82e700e86c27dafbdc881b8b61446393 100644 --- a/src/app/degree-planner/store/reducer.ts +++ b/src/app/degree-planner/store/reducer.ts @@ -18,6 +18,7 @@ import { CreatePlanSuccess, DeletePlanSuccess, PlanError, + ChangeGradeVisibility, } from '@app/degree-planner/store/actions/plan.actions'; import { MoveCourseInsideTerm, @@ -94,7 +95,8 @@ type SupportedActions = | ToggleCourseSearch | OpenSidenav | CloseSidenav - | UpdateSearchTermCode; + | UpdateSearchTermCode + | ChangeGradeVisibility; export function degreePlannerReducer( state = INITIAL_DEGREE_PLANNER_STATE, @@ -606,6 +608,10 @@ export function degreePlannerReducer( return { ...state, allDegreePlans }; } + case PlanActionTypes.ChangeGradeVisibility: { + return { ...state, showGrades: action.visibility }; + } + /** * It's okay if the action didn't match any of the cases above. If that's * the case, just return the existing state object. diff --git a/src/app/degree-planner/store/selectors.ts b/src/app/degree-planner/store/selectors.ts index d70219501e5283ac68a37e1e1ead0c4451d16c69..adc081fc4cf69ad49aa16f58760e8f1bd7bc1029 100644 --- a/src/app/degree-planner/store/selectors.ts +++ b/src/app/degree-planner/store/selectors.ts @@ -59,6 +59,11 @@ export const selectVisibleTerm = createSelector( }, ); +export const selectGradeVisibility = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => state.showGrades, +); + export const isCourseSearchOpen = createSelector( getDegreePlannerState, (state: DegreePlannerState) => { diff --git a/src/app/degree-planner/store/state.ts b/src/app/degree-planner/store/state.ts index 3fd7085c824257ddae28b22f1e0f12925852eae6..608177c9267a1a953358494f096fb5824a8d7aad 100644 --- a/src/app/degree-planner/store/state.ts +++ b/src/app/degree-planner/store/state.ts @@ -16,6 +16,7 @@ export interface DegreePlannerState { isLoadingPlan: boolean; isSidenavOpen: 'defer' | boolean; alerts: Alert[]; + showGrades: boolean; } export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { @@ -29,4 +30,5 @@ export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { isLoadingPlan: true, isSidenavOpen: 'defer', alerts: [], + showGrades: true, }; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 4a9ab01286568b0a696ed0ee87eb43ac76f28a8b..9c21d43342dbb42e0f98efba0c1b7f6023387717 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -9,6 +9,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatTabsModule } from '@angular/material/tabs'; import { MatCardModule } from '@angular/material/card'; import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatListModule } from '@angular/material/list'; @@ -53,6 +54,7 @@ const modules = [ MatSelectModule, FlexLayoutModule, MatSidenavModule, + MatSlideToggleModule, MatListModule, MatToolbarModule, MatDialogModule,