diff --git a/src/app/core/models/alert.ts b/src/app/core/models/alert.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc1f4eb562f3cfdc94df8b6a51e18de4b6d541a7 --- /dev/null +++ b/src/app/core/models/alert.ts @@ -0,0 +1,6 @@ +export interface Alert { + key: string; + title: string; + message: string; + callback?: () => void; +} diff --git a/src/app/degree-planner/alert-container/alert-container.component.html b/src/app/degree-planner/alert-container/alert-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..242aca4271f8a87d1c56e1fbf04488168a87a888 --- /dev/null +++ b/src/app/degree-planner/alert-container/alert-container.component.html @@ -0,0 +1,14 @@ +<div class="cse-alert-container"> + <div *ngFor="let alert of (alerts$ | async)" class="cse-alert"> + <div class="alert-icon"> + <mat-icon>warning</mat-icon> + </div> + <div class="alert-body"> + <p class="alert-title mat-h4"><strong>{{ alert.title }}</strong></p> + <p class="alert-message mat-body">{{ alert.message }}</p> + </div> + <div class="alert-close"> + <button mat-icon-button (click)="dismissAlert(alert.key, alert.callback)"><mat-icon>clear</mat-icon></button> + </div> + </div> +</div> diff --git a/src/app/degree-planner/alert-container/alert-container.component.scss b/src/app/degree-planner/alert-container/alert-container.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..919a51b37ff5ab7f5a1bea6ac6aed8b8e75f2d98 --- /dev/null +++ b/src/app/degree-planner/alert-container/alert-container.component.scss @@ -0,0 +1,35 @@ +.cse-alert { + margin: 1.5rem; + padding: 0.5rem 0; + display: flex; + flex-direction: row; + border: 1px solid silver; + border-left: 0.4rem solid #ef6c00; + border-radius: 4px; + + .alert-icon, + .alert-close { + margin: 0 1.5rem; + flex-grow: 0; + flex-shrink: 0; + display: flex; + align-items: center; + } + + .alert-icon mat-icon { + color: #ef6c00; + display: block; + } + + .alert-body { + flex-grow: 1; + } + + .alert-title { + margin: 0; + } + + .alert-message { + margin: 0; + } +} diff --git a/src/app/degree-planner/alert-container/alert-container.component.ts b/src/app/degree-planner/alert-container/alert-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cacdb402963c5747d9de4f9c31d3ef27148abb3 --- /dev/null +++ b/src/app/degree-planner/alert-container/alert-container.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { Store, select } from '@ngrx/store'; +import { GlobalState } from '@app/core/state'; +import * as selectors from '@app/degree-planner/store/selectors'; +import { Alert } from '@app/core/models/alert'; +import { Observable } from 'rxjs'; +import { DismissAlert } from '../store/actions/ui.actions'; + +@Component({ + selector: 'cse-alert-container', + templateUrl: './alert-container.component.html', + styleUrls: ['./alert-container.component.scss'], +}) +export class AlertContainerComponent { + public alerts$: Observable<Alert[]>; + + constructor(private store: Store<GlobalState>) { + this.alerts$ = store.pipe(select(selectors.getAlerts)); + } + + public dismissAlert(key: string, callback?: () => void) { + this.store.dispatch(new DismissAlert({ key })); + console.log(callback); + + if (typeof callback === 'function') { + callback(); + } + } +} diff --git a/src/app/degree-planner/degree-planner.component.html b/src/app/degree-planner/degree-planner.component.html index 7a7d22b65c6640e83b96cce3cda7b436c7a3ae9a..361fbd480ac753a2e7e5c88e607ca124d2adf4ee 100644 --- a/src/app/degree-planner/degree-planner.component.html +++ b/src/app/degree-planner/degree-planner.component.html @@ -8,6 +8,9 @@ cdkFocusRegionStart [ngClass]="{ isLoadingPlan: isLoadingPlan$ | async }" *ngIf="(degreePlan$ | async) as degreePlan"> + + <cse-alert-container></cse-alert-container> + <div id="maincontent" fxLayout="row" @@ -128,7 +131,6 @@ <i class="material-icons" alt="Close course search sidenav" matTooltip="Close sidenav" matTooltipPosition="above">keyboard_arrow_right</i> </button> </mat-toolbar> - <cse-course-search></cse-course-search> </mat-sidenav> </mat-sidenav-container> diff --git a/src/app/degree-planner/degree-planner.module.ts b/src/app/degree-planner/degree-planner.module.ts index 583c507feb617943a41a6709e6f7640b848364ea..0633b86e92113dc0235202835248d8008f243754 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 { YearContainerComponent } from '@app/degree-planner/year-container/year-container.component'; import { CourseSearchComponent } from '@app/degree-planner/course-search/course-search.component'; +import { AlertContainerComponent } from './alert-container/alert-container.component'; @NgModule({ imports: [SharedModule, DragDropModule, DegreePlannerRoutingModule], @@ -25,6 +26,7 @@ import { CourseSearchComponent } from '@app/degree-planner/course-search/course- NotesDialogComponent, RemoveCourseConfirmDialogComponent, YearContainerComponent, + AlertContainerComponent, CourseSearchComponent, ], entryComponents: [NotesDialogComponent, RemoveCourseConfirmDialogComponent], diff --git a/src/app/degree-planner/store/actions/ui.actions.ts b/src/app/degree-planner/store/actions/ui.actions.ts index 1e208197b4e7753f99b00264e7484f39d31b7c60..e776dc5d0636c8ea82068eb9f505d7c2d58af166 100644 --- a/src/app/degree-planner/store/actions/ui.actions.ts +++ b/src/app/degree-planner/store/actions/ui.actions.ts @@ -14,6 +14,8 @@ export enum UIActionTypes { OpenSidenav = '[UI] Open Sidenav', CloseSidenav = '[UI] Close Sidenav', + + DismissAlert = '[UI] Dismiss Disclaimer Alert', } export class ToggleAcademicYear implements Action { @@ -58,3 +60,8 @@ export class OpenSidenav implements Action { export class CloseSidenav implements Action { public readonly type = UIActionTypes.CloseSidenav; } + +export class DismissAlert implements Action { + public readonly type = UIActionTypes.DismissAlert; + constructor(public payload: { key: string }) {} +} diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts index aff285a876347c09d6dce914876bed9bbf5bec48..9f824e6d505fca2947677a64693e8eb6567032bf 100644 --- a/src/app/degree-planner/store/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -41,6 +41,7 @@ import { CourseBase, Course } from '@app/core/models/course'; import { pickTermEra } from '@app/degree-planner/shared/utils'; import { TermCode, YearCode } from '@app/core/models/termcode'; import { ConstantsService } from '@app/degree-planner/services/constants.service'; +import { Alert } from '@app/core/models/alert'; @Injectable() export class DegreePlanEffects { @@ -78,11 +79,37 @@ export class DegreePlanEffects { this.constants, ); + const alerts: Alert[] = []; + + if (userPreferences.degreePlannerHasDismissedDisclaimer !== true) { + const key = 'disclaimerAlert'; + alerts.push({ + key, + title: 'This is a planning tool.', + message: + 'If you have questions about your plan or degree, please contact your advisor.', + callback: () => { + this.api + .getUserPreferences() + .toPromise() + .then(prefs => + this.api + .updateUserPreferences({ + ...prefs, + degreePlannerHasDismissedDisclaimer: true, + }) + .toPromise(), + ); + }, + }); + } + return forkJoinWithKeys({ visibleDegreePlan: of(visibleDegreePlan), visibleYears, savedForLaterCourses, allDegreePlans: of(allDegreePlans), + alerts: of(alerts), }); }), map(payload => { diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts index 16868620f5cb10ceabbf36f603786c4689e608ff..d1f1354727268f195fff4b853d7fa114ae9add86 100644 --- a/src/app/degree-planner/store/reducer.ts +++ b/src/app/degree-planner/store/reducer.ts @@ -43,6 +43,7 @@ import { import { ExpandAcademicYear, CollapseAcademicYear, + DismissAlert, OpenCourseSearch, CloseCourseSearch, ToggleCourseSearch, @@ -87,6 +88,7 @@ type SupportedActions = | DeletePlanSuccess | ExpandAcademicYear | CollapseAcademicYear + | DismissAlert | OpenCourseSearch | CloseCourseSearch | ToggleCourseSearch @@ -179,6 +181,12 @@ export function degreePlannerReducer( return newState; } + case UIActionTypes.DismissAlert: { + const keyToRemove = action.payload.key; + const newAlerts = state.alerts.filter(({ key }) => key !== keyToRemove); + return { ...state, alerts: newAlerts }; + } + /** * The `ToggleCourseSearch` action toggles the open and close state of the course search side nav */ diff --git a/src/app/degree-planner/store/selectors.ts b/src/app/degree-planner/store/selectors.ts index c227d553b1de6d625f3b285f77cd427f94f6db5e..d70219501e5283ac68a37e1e1ead0c4451d16c69 100644 --- a/src/app/degree-planner/store/selectors.ts +++ b/src/app/degree-planner/store/selectors.ts @@ -88,6 +88,13 @@ export const getActiveSelectedSearchTerm = createSelector( }, ); +export const getAlerts = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + return state.alerts; + }, +); + export const isLoadingPlan = createSelector( getDegreePlannerState, (state: DegreePlannerState) => state.isLoadingPlan, diff --git a/src/app/degree-planner/store/state.ts b/src/app/degree-planner/store/state.ts index 0f3ff34ef83d835085a591fcf777f8639da57be9..3fd7085c824257ddae28b22f1e0f12925852eae6 100644 --- a/src/app/degree-planner/store/state.ts +++ b/src/app/degree-planner/store/state.ts @@ -3,6 +3,7 @@ import { DegreePlan } from '@app/core/models/degree-plan'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; import { SubjectCodesTo, SubjectDescription } from '@app/core/models/course'; import { TermCode } from '@app/core/models/termcode'; +import { Alert } from '@app/core/models/alert'; export interface DegreePlannerState { visibleDegreePlan: DegreePlan | undefined; @@ -14,6 +15,7 @@ export interface DegreePlannerState { search: { visible: boolean; selectedTerm?: TermCode }; isLoadingPlan: boolean; isSidenavOpen: 'defer' | boolean; + alerts: Alert[]; } export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { @@ -26,4 +28,5 @@ export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { search: { visible: false }, isLoadingPlan: true, isSidenavOpen: 'defer', + alerts: [], };