From 088d69ef5395b46d0962b8697b329dbace03751e Mon Sep 17 00:00:00 2001 From: ievavold <ievavold@wisc.edu> Date: Wed, 8 May 2019 14:50:23 -0500 Subject: [PATCH] ROENROLL-1724 attach era to all termCodes --- src/app/core/models/planned-term.ts | 5 +- src/app/core/models/year.ts | 2 +- .../course-search.component.html | 2 +- .../course-search/course-search.component.ts | 8 +- .../degree-planner.component.ts | 9 +- .../credit-overload-dialog.component.ts | 2 +- .../notes-dialog/notes-dialog.component.ts | 2 +- .../remove-course-confirm-dialog.component.ts | 7 +- .../saved-for-later-container.component.ts | 6 +- .../degree-planner/services/api.service.ts | 5 +- .../services/constants.service.ts | 18 --- .../services/termcode.factory.ts | 74 +++++++++++ .../course-item/course-item.component.ts | 31 ++--- .../shared/term-codes/termcode.ts | 31 +++++ .../shared/term-codes/without-era.ts} | 122 +++++++++--------- .../shared/term-codes/yearcode.ts | 23 ++++ src/app/degree-planner/shared/utils.ts | 88 ++----------- .../store/actions/course.actions.ts | 6 +- .../store/actions/note.actions.ts | 4 +- .../store/actions/ui.actions.ts | 3 +- .../store/effects/course.effects.ts | 15 +-- .../store/effects/note.effects.ts | 18 ++- .../store/effects/plan.effects.ts | 58 +++------ src/app/degree-planner/store/reducer.ts | 90 ++++--------- src/app/degree-planner/store/selectors.ts | 11 +- src/app/degree-planner/store/state.ts | 4 +- .../term-container.component.html | 18 +-- .../term-container.component.ts | 17 +-- .../year-container.component.html | 6 +- .../year-container.component.ts | 2 +- .../course-details.component.html | 1 + .../course-details.component.ts | 51 ++++---- .../shared/pipes/academic-year-state.pipe.ts | 24 ++-- .../shared/pipes/get-term-description.pipe.ts | 10 +- 34 files changed, 382 insertions(+), 391 deletions(-) create mode 100644 src/app/degree-planner/services/termcode.factory.ts create mode 100644 src/app/degree-planner/shared/term-codes/termcode.ts rename src/app/{core/models/termcode.ts => degree-planner/shared/term-codes/without-era.ts} (60%) create mode 100644 src/app/degree-planner/shared/term-codes/yearcode.ts diff --git a/src/app/core/models/planned-term.ts b/src/app/core/models/planned-term.ts index bc5bad3..f5b732c 100644 --- a/src/app/core/models/planned-term.ts +++ b/src/app/core/models/planned-term.ts @@ -1,7 +1,5 @@ import { Course } from '@app/core/models/course'; -import { TermCode } from '@app/core/models/termcode'; - -export type PlannedTermEra = 'past' | 'active' | 'future'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; export type PlannedTermNote = | { isLoaded: true; text: string; id: number } @@ -10,7 +8,6 @@ export type PlannedTermNote = export interface PlannedTerm { roadmapId: number; termCode: TermCode; - era: PlannedTermEra; note?: PlannedTermNote; plannedCourses: ReadonlyArray<Course>; enrolledCourses: ReadonlyArray<Course>; diff --git a/src/app/core/models/year.ts b/src/app/core/models/year.ts index c8dcef8..8b72712 100644 --- a/src/app/core/models/year.ts +++ b/src/app/core/models/year.ts @@ -1,5 +1,5 @@ import { PlannedTerm } from '@app/core/models/planned-term'; -import { YearCode } from '@app/core/models/termcode'; +import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode'; export interface Year { yearCode: YearCode; diff --git a/src/app/degree-planner/course-search/course-search.component.html b/src/app/degree-planner/course-search/course-search.component.html index 54c88a3..293bc81 100644 --- a/src/app/degree-planner/course-search/course-search.component.html +++ b/src/app/degree-planner/course-search/course-search.component.html @@ -10,7 +10,7 @@ <mat-select placeholder="Term" aria-label="Term" [disableOptionCentering]="true" formControlName="term"> <mat-option value="0000">All courses</mat-option> <mat-option - *ngFor="let termCode of constants.activeTermCodes()" + *ngFor="let termCode of termCodeFactory.active" [value]="termCode.toString()" >{{ termCode | getTermDescription }}</mat-option> </mat-select> diff --git a/src/app/degree-planner/course-search/course-search.component.ts b/src/app/degree-planner/course-search/course-search.component.ts index f5bff94..7151bca 100644 --- a/src/app/degree-planner/course-search/course-search.component.ts +++ b/src/app/degree-planner/course-search/course-search.component.ts @@ -21,9 +21,10 @@ import { SubjectCodesTo, SubjectDescription, } from '@app/core/models/course'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; import { MediaMatcher } from '@angular/cdk/layout'; import { ConstantsService } from '../services/constants.service'; +import { TermCodeFactory } from '../services/termcode.factory'; @Component({ selector: 'cse-course-search', @@ -41,7 +42,7 @@ export class CourseSearchComponent implements OnInit, OnDestroy { // Observable used for drag and drop and for populating term select public dropZoneIds$: Observable<string[]>; - public activeTerms$: Observable<ReadonlyArray<TermCode>>; + public activeTerms: ReadonlyArray<TermCode>; public activeSelectedSearchTerm$: Observable<TermCode | undefined>; public isCourseSearchOpen$: Observable<boolean>; @@ -61,6 +62,7 @@ export class CourseSearchComponent implements OnInit, OnDestroy { private fb: FormBuilder, private api: DegreePlannerApiService, private snackBar: MatSnackBar, + public termCodeFactory: TermCodeFactory, mediaMatcher: MediaMatcher, public constants: ConstantsService, ) { @@ -74,6 +76,8 @@ export class CourseSearchComponent implements OnInit, OnDestroy { this.hasResults = false; this.isLoading = false; + this.activeTerms = this.termCodeFactory.active; + // Get active term drop zones this.dropZoneIds$ = this.store.pipe( select(selectors.selectAllVisibleYears), diff --git a/src/app/degree-planner/degree-planner.component.ts b/src/app/degree-planner/degree-planner.component.ts index 2bfea52..cdf7fdd 100644 --- a/src/app/degree-planner/degree-planner.component.ts +++ b/src/app/degree-planner/degree-planner.component.ts @@ -35,10 +35,11 @@ import { ExpandAcademicYear, CollapseAcademicYear, } from './store/actions/ui.actions'; -import { YearCode } from '@app/core/models/termcode'; +import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode'; import { ConstantsService } from './services/constants.service'; import { AddAcademicYearRequest } from './store/actions/addAcademicYear.actions'; import { UserPreferences } from '@app/core/models/user-preferences'; +import { TermCodeFactory } from './services/termcode.factory'; @Component({ selector: 'cse-degree-planner', @@ -62,9 +63,11 @@ export class DegreePlannerComponent implements OnInit { constructor( private store: Store<GlobalState>, private constants: ConstantsService, + private termCodeFactory: TermCodeFactory, public mediaMatcher: MediaMatcher, public dialog: MatDialog, private snackBar: MatSnackBar, + private termCodeService: TermCodeFactory, ) { this.mobileView = mediaMatcher.matchMedia('(max-width: 959px)'); this.version = constants.getVersion(); @@ -105,7 +108,7 @@ export class DegreePlannerComponent implements OnInit { select(selectors.selectAllVisibleYears), map(years => Object.keys(years)), distinctUntilChanged(utils.compareStringArrays), - map(yearCodes => yearCodes.map(YearCode.fromString)), + map(ycs => ycs.map(yc => this.termCodeFactory.fromRawYearCode(yc))), ); } @@ -180,7 +183,7 @@ export class DegreePlannerComponent implements OnInit { const text = `This will change your primary plan and replace the current ` + `courses in your cart with the courses in this plan's ` + - `${this.constants.firstActiveTermCode().description} term.`; + `${this.termCodeService.first().description} term.`; this.dialog .open(ConfirmDialogComponent, { diff --git a/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts b/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts index f39441a..f8070e1 100644 --- a/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts +++ b/src/app/degree-planner/dialogs/credit-overload-dialog/credit-overload-dialog.component.ts @@ -1,5 +1,5 @@ import { Component, Input, Inject } from '@angular/core'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; import { MAT_DIALOG_DATA } from '@angular/material'; @Component({ diff --git a/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts b/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts index c045f30..f11adef 100644 --- a/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts +++ b/src/app/degree-planner/dialogs/notes-dialog/notes-dialog.component.ts @@ -10,7 +10,7 @@ import { WriteNote, DeleteNote, } from '@app/degree-planner/store/actions/note.actions'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; export type NotesDialogData = | { diff --git a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts index e2e2d3b..a26cd0e 100644 --- a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts +++ b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts @@ -8,7 +8,7 @@ import { DegreePlannerState } from '@app/degree-planner/store/state'; import { Store } from '@ngrx/store'; import { RemoveCourse } from '@app/degree-planner/store/actions/course.actions'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCodeFactory } from '@app/degree-planner/services/termcode.factory'; @Component({ selector: 'cse-remove-course-confirm-dialog', @@ -23,6 +23,7 @@ export class RemoveCourseConfirmDialogComponent implements OnInit { constructor( private dialogRef: MatDialogRef<RemoveCourseConfirmDialogComponent>, + private termCodeService: TermCodeFactory, private store: Store<{ degreePlanner: DegreePlannerState }>, @Inject(MAT_DIALOG_DATA) data: any, ) { @@ -43,7 +44,9 @@ export class RemoveCourseConfirmDialogComponent implements OnInit { console.log(this.course); const id = this.course.id; if (typeof id === 'number') { - const fromTermCode = new TermCode(this.course.termCode); + const fromTermCode = this.termCodeService.fromString( + this.course.termCode, + ); this.store.dispatch(new RemoveCourse({ fromTermCode, recordId: id })); } else { throw new Error('cannot remove a course that does not have an ID'); diff --git a/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts b/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts index 1963d37..13dc611 100644 --- a/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts +++ b/src/app/degree-planner/saved-for-later-container/saved-for-later-container.component.ts @@ -15,8 +15,9 @@ import { } from '@app/degree-planner/store/actions/course.actions'; import * as selectors from '@app/degree-planner/store/selectors'; import { distinctUntilChanged } from 'rxjs/operators'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; import { MediaMatcher } from '@angular/cdk/layout'; +import { TermCodeFactory } from '../services/termcode.factory'; @Component({ selector: 'cse-saved-for-later-container', @@ -31,6 +32,7 @@ export class SavedForLaterContainerComponent implements OnInit { constructor( private store: Store<{ degreePlanner: DegreePlannerState }>, + private termCodeService: TermCodeFactory, mediaMatcher: MediaMatcher, ) { this.mobileView = mediaMatcher.matchMedia('(max-width: 900px)'); @@ -68,7 +70,7 @@ export class SavedForLaterContainerComponent implements OnInit { this.store.dispatch( new RemoveCourse({ - fromTermCode: new TermCode(course.termCode), + fromTermCode: this.termCodeService.fromString(course.termCode), recordId: course.id as number, }), ); diff --git a/src/app/degree-planner/services/api.service.ts b/src/app/degree-planner/services/api.service.ts index e9b6823..20f9b7d 100644 --- a/src/app/degree-planner/services/api.service.ts +++ b/src/app/degree-planner/services/api.service.ts @@ -13,8 +13,9 @@ import { DegreePlan } from '@app/core/models/degree-plan'; import { Profile } from '@app/core/models/profile'; import { SavedForLaterCourseBase } from '@app/core/models/saved-for-later-course'; import { SearchResults } from '@app/core/models/search-results'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '../shared/term-codes/termcode'; import { StudentInfo } from '@app/core/models/student-info'; +import { RawTermCode } from '../shared/term-codes/without-era'; const HTTP_OPTIONS = { headers: new HttpHeaders({ @@ -111,7 +112,7 @@ export class DegreePlannerApiService { const url = `/api/search/v1/terms`; return this.http .get<Term[]>(url, HTTP_OPTIONS) - .pipe(map(terms => terms.map(TermCode.fromTerm))); + .pipe(map(terms => terms.map(term => new RawTermCode(term.termCode)))); } public getAllNotes(roadmapId: number): Observable<Note[]> { diff --git a/src/app/degree-planner/services/constants.service.ts b/src/app/degree-planner/services/constants.service.ts index 7adef79..be64957 100644 --- a/src/app/degree-planner/services/constants.service.ts +++ b/src/app/degree-planner/services/constants.service.ts @@ -4,12 +4,10 @@ import { environment } from './../../../environments/environment'; import { forkJoinWithKeys } from '@app/degree-planner/shared/utils'; import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; import { map, tap } from 'rxjs/operators'; -import { TermCode } from '@app/core/models/termcode'; import { SubjectDescription, SubjectCodesTo } from '@app/core/models/course'; import { StudentInfo } from '@app/core/models/student-info'; export interface ConstantData { - activeTermCodes: TermCode[]; studentInfo: Partial<StudentInfo>; subjectDescriptions: SubjectCodesTo<SubjectDescription>; } @@ -18,25 +16,12 @@ export interface ConstantData { export class ConstantsService implements Resolve<ConstantData> { private version = environment.version; private constants: ConstantData = { - activeTermCodes: [], studentInfo: {}, subjectDescriptions: {}, }; constructor(private api: DegreePlannerApiService) {} - public firstActiveTermCode(): TermCode { - if (this.constants.activeTermCodes.length === 0) { - throw new Error(`tried to use the active term before it was loaded`); - } - - return this.constants.activeTermCodes[0]; - } - - public activeTermCodes(): ReadonlyArray<TermCode> { - return this.constants.activeTermCodes; - } - public getStudentInfo() { return this.constants.studentInfo; } @@ -69,8 +54,6 @@ export class ConstantsService implements Resolve<ConstantData> { } public resolve() { - const activeTermCodes = this.api.getActiveTermCodes(); - const studentInfo = this.api.getStudentInfo(); const subjectDescriptions = forkJoinWithKeys({ @@ -101,7 +84,6 @@ export class ConstantsService implements Resolve<ConstantData> { ); return forkJoinWithKeys({ - activeTermCodes, studentInfo, subjectDescriptions, }).pipe(tap(constants => (this.constants = constants))); diff --git a/src/app/degree-planner/services/termcode.factory.ts b/src/app/degree-planner/services/termcode.factory.ts new file mode 100644 index 0000000..bcdef14 --- /dev/null +++ b/src/app/degree-planner/services/termcode.factory.ts @@ -0,0 +1,74 @@ +import { Injectable } from '@angular/core'; +import { RawYearCode, RawTermCode } from '../shared/term-codes/without-era'; +import { YearCode } from '../shared/term-codes/yearcode'; +import { TermCode, Era } from '../shared/term-codes/termcode'; + +@Injectable({ providedIn: 'root' }) +export class TermCodeFactory { + private state: 'uninitialized' | 'initialized' = 'uninitialized'; + public active: ReadonlyArray<TermCode> = []; + + private static pickEra(code: RawTermCode, active: ReadonlyArray<TermCode>) { + const isActive = active.some(t => t.equals(code)); + if (isActive) { + return Era.Active; + } + + const beforeActive = active.every(t => t.comesAfter(code)); + if (beforeActive) { + return Era.Past; + } else { + return Era.Future; + } + } + + private requireInitialization() { + if (this.state !== 'initialized') { + throw new Error('cannot use TermCodeFactory without active terms'); + } + } + + public setActiveTermCodes(active: RawTermCode[]) { + if (this.state !== 'uninitialized') { + throw new Error('the TermCodeFactory was not uninitialized'); + } else if (active.length === 0) { + throw new Error('app cannot have 0 active terms, must have at least 1'); + } + + this.state = 'initialized'; + this.active = active + .sort(RawTermCode.sort) + .map(t => new TermCode(t, Era.Active, this.fromRawYearCode(t.yearCode))); + } + + public first() { + return this.active[0]; + } + + public fromString(str: string) { + this.requireInitialization(); + + const raw = new RawTermCode(str); + const era = TermCodeFactory.pickEra(raw, this.active); + return new TermCode(raw, era, this.fromRawYearCode(raw.yearCode)); + } + + public fromRawYearCode(raw: RawYearCode | string): YearCode { + if (typeof raw === 'string') { + raw = new RawYearCode(raw); + } + + const fall = TermCodeFactory.pickEra(raw.fall(), this.active); + const spring = TermCodeFactory.pickEra(raw.spring(), this.active); + const summer = TermCodeFactory.pickEra(raw.summer(), this.active); + return new YearCode(raw.toString(), fall, spring, summer); + } + + public fromYear(year: YearCode) { + return { + fall: this.fromString(`${year.toString()}2`), + spring: this.fromString(`${year.toString()}4`), + summer: this.fromString(`${year.toString()}6`), + }; + } +} 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 a464f25..b3e2b25 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 @@ -19,9 +19,10 @@ import { DegreePlannerApiService } from '@app/degree-planner/services/api.servic import { ConfirmDialogComponent } from '@app/shared/dialogs/confirm-dialog/confirm-dialog.component'; import { CourseDetailsDialogComponent } from '@app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; import { distinctUntilChanged, filter } from 'rxjs/operators'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode, Era } from '@app/degree-planner/shared/term-codes/termcode'; import { PlannedTerm } from '@app/core/models/planned-term'; import { ConstantsService } from '@app/degree-planner/services/constants.service'; +import { TermCodeFactory } from '@app/degree-planner/services/termcode.factory'; const isntUndefined = <T>(thing: T | undefined): thing is T => { return thing !== undefined; @@ -38,7 +39,7 @@ export class CourseItemComponent implements OnInit { @Input() isPastTerm: boolean; @Input() disabled: boolean; @Input() type: 'saved' | 'course' | 'search'; - @Input() era?: unknown; + @Input() era?: Era; visibleTerms: any; activeTerm: any; public status: @@ -48,8 +49,7 @@ export class CourseItemComponent implements OnInit { | 'NotOfferedInTerm' | 'DoesNotExist' | 'Normal'; - public visibleTermCodes$: Observable<string[]>; - public droppableTermCodes$: Observable<string[]>; + public droppableTermCodes$: Observable<TermCode[]>; public term$: Observable<PlannedTerm>; public plannedCourses: ReadonlyArray<Course>; public toActiveTerm: boolean; @@ -63,13 +63,14 @@ export class CourseItemComponent implements OnInit { private constants: ConstantsService, private snackBar: MatSnackBar, public mediaMatcher: MediaMatcher, + private termCodeFactory: TermCodeFactory, ) { this.mobileView = mediaMatcher.matchMedia('(max-width: 959px)'); } ngOnInit() { - const isActive = this.era === 'active'; - const isPast = this.era === 'past'; + const isActive = this.era === Era.Active; + const isPast = this.era === Era.Past; const isNotOffered = this.course.studentEnrollmentStatus === 'NOTOFFERED'; const doesNotExist = this.course.studentEnrollmentStatus === 'DOESNOTEXIST'; @@ -102,7 +103,7 @@ export class CourseItemComponent implements OnInit { this.droppableTermCodes$ = this.store.pipe( select(selectors.selectAllVisibleYears), utils.yearsToDroppableTermCodes(), - distinctUntilChanged(utils.compareStringArrays), + distinctUntilChanged(utils.compareArrays((a, b) => a.equals(b))), ); } @@ -123,8 +124,7 @@ export class CourseItemComponent implements OnInit { * Handle moving a course to different terms based on course type * */ - onMove(termCode: string) { - const toTermCode = new TermCode(termCode); + onMove(toTermCode: TermCode) { this.term$ = this.store.pipe( select(selectors.selectVisibleTerm, { termCode: toTermCode }), filter(isntUndefined), @@ -133,7 +133,7 @@ export class CourseItemComponent implements OnInit { this.term$.subscribe(term => { this.plannedCourses = term.plannedCourses; - this.toActiveTerm = term.era === 'active'; + this.toActiveTerm = term.termCode.era === Era.Active; }); const isCourseInPlannedCourses = this.plannedCourses.some( @@ -158,7 +158,7 @@ export class CourseItemComponent implements OnInit { case 'course': { const id = this.course.id as number; const { subjectCode, courseId } = this.course; - const from = new TermCode(this.course.termCode); + const from = this.termCodeFactory.fromString(this.course.termCode); this.store.dispatch( new MoveCourseBetweenTerms({ to: toTermCode, @@ -220,7 +220,7 @@ export class CourseItemComponent implements OnInit { case 'course': this.store.dispatch( new RemoveCourse({ - fromTermCode: new TermCode(termCode), + fromTermCode: this.termCodeFactory.fromString(termCode), recordId: this.course.id as number, }), ); @@ -255,7 +255,7 @@ export class CourseItemComponent implements OnInit { break; default: - if (this.era === 'future') { + if (this.era === Era.Future) { dialogOptions.text = `This will remove "${ this.course.title }" from your degree plan.`; @@ -284,7 +284,9 @@ export class CourseItemComponent implements OnInit { case 'course': this.store.dispatch( new RemoveCourse({ - fromTermCode: new TermCode(this.course.termCode), + fromTermCode: this.termCodeFactory.fromString( + this.course.termCode, + ), recordId: this.course.id as number, }), ); @@ -320,7 +322,6 @@ export class CourseItemComponent implements OnInit { this.snackBar.open(`'${short} ${catalogNumber}' no longer offered`); return; } - console.log('mobile view', this.mobileView); this.api .getCourseDetails(subjectCode, courseId) diff --git a/src/app/degree-planner/shared/term-codes/termcode.ts b/src/app/degree-planner/shared/term-codes/termcode.ts new file mode 100644 index 0000000..0cfe906 --- /dev/null +++ b/src/app/degree-planner/shared/term-codes/termcode.ts @@ -0,0 +1,31 @@ +import { RawTermCode } from './without-era'; +import { YearCode } from './yearcode'; + +export enum Era { + Past, + Active, + Future, +} + +export class TermCode extends RawTermCode { + public era: Era; + public yearCode: YearCode; + + constructor(from: RawTermCode | string, era: Era, yearCode: YearCode) { + super(from.toString()); + this.yearCode = yearCode; + this.era = era; + } + + public isPast() { + return this.era === Era.Past; + } + + public isActive() { + return this.era === Era.Active; + } + + public isFuture() { + return this.era === Era.Future; + } +} diff --git a/src/app/core/models/termcode.ts b/src/app/degree-planner/shared/term-codes/without-era.ts similarity index 60% rename from src/app/core/models/termcode.ts rename to src/app/degree-planner/shared/term-codes/without-era.ts index 6f51c5a..05be64c 100644 --- a/src/app/core/models/termcode.ts +++ b/src/app/degree-planner/shared/term-codes/without-era.ts @@ -1,52 +1,74 @@ -import { Term } from './term'; - const YEARCODE_PATTERN = /^[01]\d{2}$/i; const TERMCODE_PATTERN = /^[01]\d{2}[2346]$/i; -export class YearCode { +export class RawYearCode { public readonly centuryOffset: '0' | '1'; public readonly yearOffset: string; + public static isValid(str: string): boolean { + return YEARCODE_PATTERN.test(str); + } + + public static sort(a: RawYearCode, b: RawYearCode): -1 | 0 | 1 { + if (a.comesBefore(b)) { + return -1; + } + + if (a.comesAfter(b)) { + return 1; + } + + return 0; + } + + public static fromString(str: string): RawYearCode { + return new RawYearCode(str); + } + public get fromYear(): number { const century = 1900 + parseInt(this.centuryOffset, 10) * 100; const year = century + parseInt(this.yearOffset, 10) - 1; return year; } - public get fall(): TermCode { - return new TermCode(`${this.toString()}2`); + public get toYear(): number { + return this.fromYear + 1; } - public get spring(): TermCode { - return new TermCode(`${this.toString()}4`); + public fall() { + return new RawTermCode(`${this.toString()}2`); } - public get summer(): TermCode { - return new TermCode(`${this.toString()}6`); + public spring() { + return new RawTermCode(`${this.toString()}4`); } - public get toYear(): number { - return this.fromYear + 1; + public summer() { + return new RawTermCode(`${this.toString()}6`); } constructor(str: string) { - if (YearCode.isValid(str) === false) { + if (typeof str !== 'string') { + throw new Error(`constructor expected string, got ${typeof str}`); + } + + if (RawYearCode.isValid(str) === false) { throw new Error(`'${str}' is not a valid year code`); } - this.centuryOffset = str.substr(0, 1) as YearCode['centuryOffset']; - this.yearOffset = str.substr(1, 2) as YearCode['yearOffset']; + this.centuryOffset = str.substr(0, 1) as RawYearCode['centuryOffset']; + this.yearOffset = str.substr(1, 2) as RawYearCode['yearOffset']; } - public equals(other: YearCode): boolean { + public equals(other: RawYearCode): boolean { return this.toString() === other.toString(); } - public comesBefore(other: YearCode): boolean { + public comesBefore(other: RawYearCode): boolean { return this.toString() < other.toString(); } - public comesAfter(other: YearCode): boolean { + public comesAfter(other: RawYearCode): boolean { return this.toString() > other.toString(); } @@ -60,12 +82,21 @@ export class YearCode { public toString() { return `${this.centuryOffset}${this.yearOffset}`; } +} + +export class RawTermCode { + public readonly yearCode: RawYearCode; + public readonly termId: '2' | '3' | '4' | '6'; + + public static describe(str: string): string { + return new RawTermCode(str).description; + } public static isValid(str: string): boolean { - return YEARCODE_PATTERN.test(str); + return TERMCODE_PATTERN.test(str); } - public static sort(a: YearCode, b: YearCode): -1 | 0 | 1 { + public static sort(a: RawTermCode, b: RawTermCode): -1 | 0 | 1 { if (a.comesBefore(b)) { return -1; } @@ -77,15 +108,6 @@ export class YearCode { return 0; } - public static fromString(str: string): YearCode { - return new YearCode(str); - } -} - -export class TermCode { - public readonly yearCode: YearCode; - public readonly termId: '2' | '3' | '4' | '6'; - public get termName(): 'fall' | 'spring' | 'summer' { switch (this.termId) { case '2': @@ -111,23 +133,31 @@ export class TermCode { } constructor(str: string) { - if (TermCode.isValid(str) === false) { + if (typeof str !== 'string') { + throw new Error(`constructor expected string, got ${typeof str}`); + } + + if (RawTermCode.isValid(str) === false) { throw new Error(`'${str}' is not a valid term code`); } - this.yearCode = new YearCode(str.substr(0, 3)); - this.termId = str.substr(3, 1) as TermCode['termId']; + if (typeof str !== 'string') { + console.log({ str }); + } + + this.yearCode = new RawYearCode(str.substr(0, 3)); + this.termId = str.substr(3, 1) as RawTermCode['termId']; } - public equals(other: TermCode): boolean { + public equals(other: RawTermCode): boolean { return this.toString() === other.toString(); } - public comesBefore(other: TermCode): boolean { + public comesBefore(other: RawTermCode): boolean { return this.toString() < other.toString(); } - public comesAfter(other: TermCode): boolean { + public comesAfter(other: RawTermCode): boolean { return this.toString() > other.toString(); } @@ -141,28 +171,4 @@ export class TermCode { public toString(): string { return `${this.yearCode}${this.termId}`; } - - public static isValid(str: string): boolean { - return TERMCODE_PATTERN.test(str); - } - - public static sort(a: TermCode, b: TermCode): -1 | 0 | 1 { - if (a.comesBefore(b)) { - return -1; - } - - if (a.comesAfter(b)) { - return 1; - } - - return 0; - } - - public static fromString(str: string): TermCode { - return new TermCode(str); - } - - public static fromTerm(term: Term): TermCode { - return new TermCode(term.termCode); - } } diff --git a/src/app/degree-planner/shared/term-codes/yearcode.ts b/src/app/degree-planner/shared/term-codes/yearcode.ts new file mode 100644 index 0000000..c5c865c --- /dev/null +++ b/src/app/degree-planner/shared/term-codes/yearcode.ts @@ -0,0 +1,23 @@ +import { RawYearCode } from './without-era'; +import { Era, TermCode } from './termcode'; + +export class YearCode extends RawYearCode { + private eras: { fall: Era; spring: Era; summer: Era }; + + constructor(str: string, fall: Era, spring: Era, summer: Era) { + super(str); + this.eras = { fall, spring, summer }; + } + + public fall() { + return new TermCode(super.fall(), this.eras.fall, this); + } + + public spring() { + return new TermCode(super.spring(), this.eras.spring, this); + } + + public summer() { + return new TermCode(super.summer(), this.eras.summer, this); + } +} diff --git a/src/app/degree-planner/shared/utils.ts b/src/app/degree-planner/shared/utils.ts index f2be7be..190c2ea 100644 --- a/src/app/degree-planner/shared/utils.ts +++ b/src/app/degree-planner/shared/utils.ts @@ -1,78 +1,22 @@ import { Observable, forkJoin } from 'rxjs'; import { map } from 'rxjs/operators'; -import { PlannedTermEra, PlannedTerm } from '@app/core/models/planned-term'; import { YearMapping } from '@app/core/models/year'; -import { YearCode, TermCode } from '@app/core/models/termcode'; - -export const isValidTermCode = (anything: any): anything is string => { - return /^\d{4}$/.test(anything); -}; - -export const pickTermName = (termOffset: string) => { - switch (termOffset) { - case '2': - case '3': - return 'fall'; - case '4': - return 'spring'; - case '6': - return 'summer'; - default: - throw new Error(`'${termOffset}' is not a valid term offset`); - } -}; - -export const pickTermEra = ( - termCode: TermCode, - activeTermCodes: ReadonlyArray<TermCode>, -): PlannedTermEra => { - const noActiveTermCodes = activeTermCodes.length === 0; - const isActiveTermCode = activeTermCodes.some(tc => tc.equals(termCode)); - const beforeAllActiveTermCodes = activeTermCodes.every(tc => - tc.comesAfter(termCode), - ); - if (noActiveTermCodes || isActiveTermCode) { - return 'active'; - } else if (beforeAllActiveTermCodes) { - return 'past'; - } else { - return 'future'; - } -}; - -export const pickYearEra = ( - yearCode: YearCode, - activeTermCodes: ReadonlyArray<TermCode>, -): PlannedTermEra => { - const activeYearCodes = activeTermCodes.map(tc => tc.yearCode); - const noActiveYearCodes = activeYearCodes.length === 0; - const isActiveYearCode = activeYearCodes.some(yc => yc.equals(yearCode)); - const beforeAllActiveYearCodes = activeYearCodes.every(yc => - yc.comesAfter(yearCode), - ); - if (noActiveYearCodes || isActiveYearCode) { - return 'active'; - } else if (beforeAllActiveYearCodes) { - return 'past'; - } else { - return 'future'; - } -}; +import { TermCode, Era } from './term-codes/termcode'; export const yearsToDropZoneIds = () => (years$: Observable<YearMapping>) => { return years$.pipe( map(years => { const yearCodes = Object.keys(years); const termCodes = yearCodes.reduce<string[]>((acc, yearCode) => { - if (years[yearCode].fall.era !== 'past') { + if (years[yearCode].fall.termCode.era !== Era.Past) { acc = acc.concat(years[yearCode].fall.termCode.toString()); } - if (years[yearCode].spring.era !== 'past') { + if (years[yearCode].spring.termCode.era !== Era.Past) { acc = acc.concat(years[yearCode].spring.termCode.toString()); } - if (years[yearCode].summer.era !== 'past') { + if (years[yearCode].summer.termCode.era !== Era.Past) { acc = acc.concat(years[yearCode].summer.termCode.toString()); } @@ -100,33 +44,23 @@ export const compareArrays = <T>(cmp: (a: T, b: T) => boolean) => { export const compareStringArrays = compareArrays<string>((a, b) => a === b); -export const yearsToYearCodes = () => (years$: Observable<YearMapping>) => { - return years$.pipe( - map(years => { - const yearCodes = Object.keys(years); - const sortedYearCodes = yearCodes.sort(); - return sortedYearCodes; - }), - ); -}; - export const yearsToDroppableTermCodes = () => ( years$: Observable<YearMapping>, ) => { return years$.pipe( map(years => { const yearCodes = Object.keys(years); - return yearCodes.reduce<string[]>((acc, yearCode) => { - if (years[yearCode].fall.era !== 'past') { - acc = acc.concat(years[yearCode].fall.termCode.toString()); + return yearCodes.reduce<TermCode[]>((acc, yearCode) => { + if (years[yearCode].fall.termCode.era !== Era.Past) { + acc = acc.concat(years[yearCode].fall.termCode); } - if (years[yearCode].spring.era !== 'past') { - acc = acc.concat(years[yearCode].spring.termCode.toString()); + if (years[yearCode].spring.termCode.era !== Era.Past) { + acc = acc.concat(years[yearCode].spring.termCode); } - if (years[yearCode].summer.era !== 'past') { - acc = acc.concat(years[yearCode].summer.termCode.toString()); + if (years[yearCode].summer.termCode.era !== Era.Past) { + acc = acc.concat(years[yearCode].summer.termCode); } return acc; diff --git a/src/app/degree-planner/store/actions/course.actions.ts b/src/app/degree-planner/store/actions/course.actions.ts index 16bf00a..df0a07f 100644 --- a/src/app/degree-planner/store/actions/course.actions.ts +++ b/src/app/degree-planner/store/actions/course.actions.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store'; import { Course } from '@app/core/models/course'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; export enum CourseActionTypes { AddCourse = '[Course] Add', @@ -80,7 +80,9 @@ export class AddCourse implements Action { export class AddCourseSuccess implements Action { public readonly type = CourseActionTypes.AddCourseSuccess; - constructor(public payload: { course: Course; newIndex?: number }) {} + constructor( + public payload: { termCode: TermCode; course: Course; newIndex?: number }, + ) {} } export class RemoveCourse implements Action { diff --git a/src/app/degree-planner/store/actions/note.actions.ts b/src/app/degree-planner/store/actions/note.actions.ts index f1fed10..be7299f 100644 --- a/src/app/degree-planner/store/actions/note.actions.ts +++ b/src/app/degree-planner/store/actions/note.actions.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store'; import { Note } from '@app/core/models/note'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; export enum NoteActionTypes { WriteNote = '[Note] Write', @@ -19,7 +19,7 @@ export class WriteNote implements Action { export class WriteNoteSuccess implements Action { public readonly type = NoteActionTypes.WriteNoteSuccess; - constructor(public payload: { updatedNote: Note }) {} + constructor(public payload: { termCode: TermCode; updatedNote: Note }) {} } export class DeleteNote 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 76b57e7..90fbe30 100644 --- a/src/app/degree-planner/store/actions/ui.actions.ts +++ b/src/app/degree-planner/store/actions/ui.actions.ts @@ -1,6 +1,7 @@ import { Action } from '@ngrx/store'; -import { YearCode, TermCode } from '@app/core/models/termcode'; import { UserPreferences } from '@app/core/models/user-preferences'; +import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; export enum UIActionTypes { ToggleAcademicYear = '[UI] Toggle Academic Year', diff --git a/src/app/degree-planner/store/effects/course.effects.ts b/src/app/degree-planner/store/effects/course.effects.ts index ab36ab3..bb54b17 100644 --- a/src/app/degree-planner/store/effects/course.effects.ts +++ b/src/app/degree-planner/store/effects/course.effects.ts @@ -31,7 +31,7 @@ import { } from '@app/degree-planner/store/actions/course.actions'; import { DegreePlan } from '@app/core/models/degree-plan'; import { Course, CourseBase } from '@app/core/models/course'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; import { ConstantsService } from '@app/degree-planner/services/constants.service'; @Injectable() @@ -56,7 +56,6 @@ export class CourseEffects { // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, degreePlan]) => { const roadmapId = degreePlan.roadmapId; - const activeTerms = this.constants.activeTermCodes(); const { id: recordId, to: toTermCode, @@ -64,15 +63,13 @@ export class CourseEffects { courseId, } = action.payload; - const toActiveTerm = activeTerms.some(t => t.equals(toTermCode)); - const moveCourse = this.api.updateCourseTerm( roadmapId, recordId, toTermCode, ); - if (toActiveTerm) { + if (toTermCode.isActive()) { /** * The `updateCourseTerm` API won't force cart validation which we want * if we're adding a course to the cart. Calling the `addCourseToCart` @@ -119,13 +116,11 @@ export class CourseEffects { flatMap(([action, visibleDegreePlan]) => { // TODO error handle the API calls const roadmapId = (visibleDegreePlan as DegreePlan).roadmapId; - const activeTerms = this.constants.activeTermCodes(); const { subjectCode, termCode, courseId, newIndex } = action.payload; - const isActiveTerm = activeTerms.some(term => term.equals(termCode)); const isPrimaryPlan = (visibleDegreePlan as DegreePlan).primary; const addCourse$ = - isActiveTerm && isPrimaryPlan + termCode.isActive() && isPrimaryPlan ? this.api.addCourseToCart(subjectCode, courseId, termCode) : this.api.addCourse(roadmapId, subjectCode, courseId, termCode); @@ -137,7 +132,7 @@ export class CourseEffects { ); const toSuccessAction$ = courseBaseToCourse$.pipe( - map(course => new AddCourseSuccess({ course, newIndex })), + map(course => new AddCourseSuccess({ termCode, course, newIndex })), ); return toSuccessAction$; @@ -145,7 +140,7 @@ export class CourseEffects { tap(state => { const touchedCourse = state.payload.course; - const touchedTerm = new TermCode(touchedCourse.termCode).description; + const touchedTerm = TermCode.describe(touchedCourse.termCode); const subject = this.constants.subjectDescription( touchedCourse.subjectCode, ).short; diff --git a/src/app/degree-planner/store/effects/note.effects.ts b/src/app/degree-planner/store/effects/note.effects.ts index 30de10d..ebb3a95 100644 --- a/src/app/degree-planner/store/effects/note.effects.ts +++ b/src/app/degree-planner/store/effects/note.effects.ts @@ -31,7 +31,7 @@ import { } from '@app/degree-planner/store/actions/note.actions'; import * as selectors from '@app/degree-planner/store/selectors'; import { GlobalState } from '@app/core/state'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; @Injectable() export class NoteEffects { @@ -66,17 +66,21 @@ export class NoteEffects { if (existingNote !== undefined && existingNote.isLoaded) { // Since the term DOES have a note, update the existing note const noteId = existingNote.id; - return this.api.updateNote(planId, termCode, noteText, noteId); + return this.api + .updateNote(planId, termCode, noteText, noteId) + .pipe( + map(updatedNote => new WriteNoteSuccess({ termCode, updatedNote })), + ); } else { // Since the term DOES NOT have a note, create a new note - return this.api.createNote(planId, termCode, noteText); + return this.api + .createNote(planId, termCode, noteText) + .pipe( + map(updatedNote => new WriteNoteSuccess({ termCode, updatedNote })), + ); } }), - // Dispatch an `WriteNoteSuccess` action so that the State - // object can be updated with the new Note data. - map(updatedNote => new WriteNoteSuccess({ updatedNote })), - tap(() => { const message = 'Note has been saved'; this.snackBar.open(message, undefined, {}); diff --git a/src/app/degree-planner/store/effects/plan.effects.ts b/src/app/degree-planner/store/effects/plan.effects.ts index 33fc235..16722c5 100644 --- a/src/app/degree-planner/store/effects/plan.effects.ts +++ b/src/app/degree-planner/store/effects/plan.effects.ts @@ -33,18 +33,16 @@ import { 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 { Note } from '@app/core/models/note'; 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 { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; import { Alert } from '@app/core/models/alert'; import { UpdateUserPreferences } from '../actions/userPreferences.actions'; +import { TermCodeFactory } from '@app/degree-planner/services/termcode.factory'; @Injectable() export class DegreePlanEffects { @@ -53,7 +51,7 @@ export class DegreePlanEffects { private api: DegreePlannerApiService, private store$: Store<GlobalState>, private snackBar: MatSnackBar, - private constants: ConstantsService, + private termCodeService: TermCodeFactory, ) {} @Effect() @@ -64,11 +62,13 @@ export class DegreePlanEffects { console.log('loading all degree plans'); return forkJoinWithKeys({ allDegreePlans: this.api.getAllDegreePlans(), + activeTermCodes: this.api.getActiveTermCodes(), userPreferences: this.api.getUserPreferences(), }); }), - // Load data specific to the primary degree plan. - flatMap(({ allDegreePlans, userPreferences }) => { + + flatMap(({ allDegreePlans, activeTermCodes, userPreferences }) => { + this.termCodeService.setActiveTermCodes(activeTermCodes); const savedForLaterCourses = this.api.getSavedForLaterCourses(); const visibleDegreePlan = userPreferences.degreePlannerSelectedPlan ? pickDegreePlanById( @@ -79,7 +79,7 @@ export class DegreePlanEffects { const visibleYears = loadPlanYears( this.api, visibleDegreePlan.roadmapId, - this.constants, + this.termCodeService, ); const alerts: Alert[] = []; @@ -120,7 +120,6 @@ export class DegreePlanEffects { return new InitialLoadSuccess({ ...INITIAL_DEGREE_PLANNER_STATE, ...payload, - activeTermCodes: this.constants.activeTermCodes(), isLoadingPlan: false, }); }), @@ -147,7 +146,7 @@ export class DegreePlanEffects { const visibleYears = loadPlanYears( this.api, visibleDegreePlan.roadmapId, - this.constants, + this.termCodeService, ); return forkJoinWithKeys({ @@ -271,7 +270,7 @@ export class DegreePlanEffects { const newYears = loadPlanYears( this.api, newPlan.roadmapId, - this.constants, + this.termCodeService, ); return forkJoinWithKeys({ @@ -356,10 +355,6 @@ const matchesTermCode = (termCode: TermCode) => (thing: { return thing.termCode === termCode.toString(); }; -const toYearCode = (termCode: string) => { - return termCode.substr(0, 3); -}; - const buildTerm = ( roadmapId: number, termCode: TermCode, @@ -368,7 +363,6 @@ const buildTerm = ( termCode: string; courses: ReadonlyArray<CourseBase>; }>, - constants: ConstantsService, ): PlannedTerm => { const baseNote = notes.find(matchesTermCode(termCode)); const note: PlannedTermNote | undefined = baseNote @@ -390,11 +384,9 @@ const buildTerm = ( plannedCourses.push(course); }); - const era = pickTermEra(termCode, constants.activeTermCodes()); return { roadmapId, termCode, - era, note, plannedCourses, enrolledCourses, @@ -404,7 +396,7 @@ const buildTerm = ( const loadPlanYears = ( api: DegreePlannerApiService, roadmapId: number, - constants: ConstantsService, + termCodeService: TermCodeFactory, ): Observable<YearMapping> => { const notesAndCourses$ = forkJoinWithKeys({ notes: api.getAllNotes(roadmapId), @@ -418,11 +410,11 @@ const loadPlanYears = ( const allTermCodes = [ ...noteTermCodes, ...courseTermCodes, - ...constants.activeTermCodes().map(tc => tc.toString()), - ].map(TermCode.fromString); + ...termCodeService.active.map(t => t.toString()), + ].map(t => termCodeService.fromString(t)); const uniqueYearCodes = unique( allTermCodes.map(tc => tc.yearCode.toString()), - ).map(yearCodeStr => new YearCode(yearCodeStr)); + ).map(yearCodeStr => termCodeService.fromRawYearCode(yearCodeStr)); return { uniqueYearCodes, notes, @@ -435,25 +427,13 @@ const loadPlanYears = ( map(({ uniqueYearCodes, notes, courses }) => { const mapping: MutableYearMapping = {}; uniqueYearCodes.forEach(yearCode => { + const { fall, spring, summer } = termCodeService.fromYear(yearCode); mapping[yearCode.toString()] = { yearCode, - isExpanded: - utils.pickYearEra(yearCode, constants.activeTermCodes()) !== 'past', - fall: buildTerm(roadmapId, yearCode.fall, notes, courses, constants), - spring: buildTerm( - roadmapId, - yearCode.spring, - notes, - courses, - constants, - ), - summer: buildTerm( - roadmapId, - yearCode.summer, - notes, - courses, - constants, - ), + isExpanded: !(fall.isPast() && spring.isPast() && summer.isPast()), + fall: buildTerm(roadmapId, fall, notes, courses), + spring: buildTerm(roadmapId, spring, notes, courses), + summer: buildTerm(roadmapId, summer, notes, courses), }; }); diff --git a/src/app/degree-planner/store/reducer.ts b/src/app/degree-planner/store/reducer.ts index a0ed618..22ccb82 100644 --- a/src/app/degree-planner/store/reducer.ts +++ b/src/app/degree-planner/store/reducer.ts @@ -1,4 +1,3 @@ -import { pickTermEra } from '@app/degree-planner/shared/utils'; import { UIActionTypes, CloseSidenav, OpenSidenav } from './actions/ui.actions'; import { @@ -59,13 +58,9 @@ import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; import { DegreePlan } from '@app/core/models/degree-plan'; import { Year, YearMapping } from '@app/core/models/year'; import { Course } from '@app/core/models/course'; -import { - PlannedTerm, - PlannedTermEra, - PlannedTermNote, -} from '@app/core/models/planned-term'; -import * as utils from '@app/degree-planner/shared/utils'; -import { TermCode, YearCode } from '@app/core/models/termcode'; +import { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term'; +import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode'; +import { TermCode, Era } from '@app/degree-planner/shared/term-codes/termcode'; type SupportedActions = | PlanError @@ -150,11 +145,15 @@ export function degreePlannerReducer( return parseInt(yearCode, 10); }), ); - const nextYearCode = new YearCode(`${largestYearCode + 1}`); + const nextYearCode = new YearCode( + `${largestYearCode + 1}`, + Era.Future, + Era.Future, + Era.Future, + ); const nextYear = emptyYear( (state.visibleDegreePlan as DegreePlan).roadmapId, nextYearCode, - state.activeTermCodes, ); const visibleYears: YearMapping = { ...state.visibleYears, @@ -304,7 +303,6 @@ export function degreePlannerReducer( (state.visibleDegreePlan as DegreePlan).roadmapId, termCode, newNote, - state.activeTermCodes, state.visibleYears[yearCode.toString()], ), }; @@ -322,8 +320,7 @@ export function degreePlannerReducer( * - *OR* adds a new term with the given note if no term exists with the note's termCode. */ case NoteActionTypes.WriteNoteSuccess: { - const { updatedNote } = action.payload; - const termCode = new TermCode(updatedNote.termCode); + const { termCode, updatedNote } = action.payload; const { yearCode } = termCode; const visibleYears: YearMapping = { @@ -332,7 +329,6 @@ export function degreePlannerReducer( (state.visibleDegreePlan as DegreePlan).roadmapId, termCode, { isLoaded: true, text: updatedNote.note, id: updatedNote.id }, - state.activeTermCodes, state.visibleYears[yearCode.toString()], ), }; @@ -354,7 +350,6 @@ export function degreePlannerReducer( [yearCode.toString()]: createYearWithoutNote( (state.visibleDegreePlan as DegreePlan).roadmapId, termCode, - state.activeTermCodes, state.visibleYears[yearCode.toString()], ), }; @@ -426,7 +421,6 @@ export function degreePlannerReducer( (state.visibleDegreePlan as DegreePlan).roadmapId, fromTermCode, course.id, - state.activeTermCodes, state.visibleYears[fromYearCode.toString()], ); @@ -434,7 +428,6 @@ export function degreePlannerReducer( (state.visibleDegreePlan as DegreePlan).roadmapId, toTermCode, { ...course, termCode: toTermCode.toString() }, - state.activeTermCodes, fromYearCode.equals(toYearCode) ? fromYear : state.visibleYears[toYearCode.toString()], @@ -466,7 +459,6 @@ export function degreePlannerReducer( (state.visibleDegreePlan as DegreePlan).roadmapId, termCode, course, - state.activeTermCodes, state.visibleYears[termCode.yearCode.toString()], newIndex, ); @@ -480,15 +472,13 @@ export function degreePlannerReducer( } case CourseActionTypes.AddCourseSuccess: { - const { course, newIndex } = action.payload; - const termCode = new TermCode(course.termCode); + const { termCode, course, newIndex } = action.payload; const { yearCode } = termCode; const year: Year = createYearWithCourse( (state.visibleDegreePlan as DegreePlan).roadmapId, termCode, course, - state.activeTermCodes, state.visibleYears[yearCode.toString()], newIndex, ); @@ -509,7 +499,6 @@ export function degreePlannerReducer( (state.visibleDegreePlan as DegreePlan).roadmapId, fromTermCode, recordId, - state.activeTermCodes, state.visibleYears[yearCode.toString()], ); @@ -656,54 +645,36 @@ export function degreePlannerReducer( } } -const emptyTerm = ( - roadmapId: number, - termCode: TermCode, - era: PlannedTermEra, -): PlannedTerm => { - return { roadmapId, termCode, era, plannedCourses: [], enrolledCourses: [] }; +const emptyTerm = (roadmapId: number, termCode: TermCode): PlannedTerm => { + return { roadmapId, termCode, plannedCourses: [], enrolledCourses: [] }; }; -const emptyYear = ( - roadmapId: number, - yearCode: YearCode, - activeTermCodes: ReadonlyArray<TermCode>, -): Year => { +const emptyYear = (roadmapId: number, yearCode: YearCode): Year => { return { yearCode, - isExpanded: utils.pickYearEra(yearCode, activeTermCodes) !== 'past', - fall: emptyTerm( - roadmapId, - yearCode.fall, - pickTermEra(yearCode.fall, activeTermCodes), - ), - spring: emptyTerm( - roadmapId, - yearCode.spring, - pickTermEra(yearCode.spring, activeTermCodes), - ), - summer: emptyTerm( - roadmapId, - yearCode.summer, - pickTermEra(yearCode.summer, activeTermCodes), + isExpanded: !( + yearCode.fall().isPast() && + yearCode.spring().isPast() && + yearCode.summer().isPast() ), + fall: emptyTerm(roadmapId, yearCode.fall()), + spring: emptyTerm(roadmapId, yearCode.spring()), + summer: emptyTerm(roadmapId, yearCode.summer()), }; }; const generateYearForTermCode = ( roadmapId: number, termCode: TermCode, - activeTermCodes: ReadonlyArray<TermCode>, ): Year => { - return emptyYear(roadmapId, termCode.yearCode, activeTermCodes); + return emptyYear(roadmapId, termCode.yearCode); }; const createYearWithNote = ( roadmapId: number, termCode: TermCode, note: PlannedTermNote | undefined, - activeTermCodes: ReadonlyArray<TermCode>, - year = generateYearForTermCode(roadmapId, termCode, activeTermCodes), + year = generateYearForTermCode(roadmapId, termCode), ): Year => { const term = year[termCode.termName]; return { ...year, [termCode.termName]: { ...term, note } }; @@ -712,16 +683,9 @@ const createYearWithNote = ( const createYearWithoutNote = ( roadmapId: number, termCode: TermCode, - activeTermCodes: ReadonlyArray<TermCode>, year?: Year, ) => { - return createYearWithNote( - roadmapId, - termCode, - undefined, - activeTermCodes, - year, - ); + return createYearWithNote(roadmapId, termCode, undefined, year); }; const findCourse = ( @@ -740,8 +704,7 @@ const createYearWithCourse = ( roadmapId: number, termCode: TermCode, course: Course, - activeTermCodes: ReadonlyArray<TermCode>, - year = generateYearForTermCode(roadmapId, termCode, activeTermCodes), + year = generateYearForTermCode(roadmapId, termCode), newIndex?: number, ): Year => { const term = year[termCode.termName]; @@ -757,8 +720,7 @@ const createYearWithoutCourse = ( roadmapId: number, termCode: TermCode, recordId: number, - activeTermCodes: ReadonlyArray<TermCode>, - year = generateYearForTermCode(roadmapId, termCode, activeTermCodes), + year = generateYearForTermCode(roadmapId, termCode), ): Year => { const term = year[termCode.termName]; const courses = term.plannedCourses.filter(course => course.id !== recordId); diff --git a/src/app/degree-planner/store/selectors.ts b/src/app/degree-planner/store/selectors.ts index 3d44099..444768c 100644 --- a/src/app/degree-planner/store/selectors.ts +++ b/src/app/degree-planner/store/selectors.ts @@ -1,7 +1,8 @@ import { createSelector } from '@ngrx/store'; import { GlobalState } from '@app/core/state'; import { DegreePlannerState } from './state'; -import { TermCode, YearCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; +import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode'; import { YearMapping, Year } from '@app/core/models/year'; import { UserPreferences } from '@app/core/models/user-preferences'; @@ -95,12 +96,8 @@ export const getSelectedSearchTerm = createSelector( export const getActiveSelectedSearchTerm = createSelector( getDegreePlannerState, (state: DegreePlannerState) => { - const { selectedTerm } = state.search; - if ( - selectedTerm && - state.activeTermCodes.some(tc => tc.equals(selectedTerm)) - ) { - return selectedTerm; + if (state.search.selectedTerm && state.search.selectedTerm.isActive()) { + return state.search.selectedTerm; } else { return undefined; } diff --git a/src/app/degree-planner/store/state.ts b/src/app/degree-planner/store/state.ts index 1697917..0976a2a 100644 --- a/src/app/degree-planner/store/state.ts +++ b/src/app/degree-planner/store/state.ts @@ -2,7 +2,7 @@ import { YearMapping } from '@app/core/models/year'; 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 { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; import { Alert } from '@app/core/models/alert'; import { UserPreferences } from '@app/core/models/user-preferences'; @@ -10,7 +10,6 @@ export interface DegreePlannerState { visibleDegreePlan: DegreePlan | undefined; visibleYears: YearMapping; savedForLaterCourses: ReadonlyArray<SavedForLaterCourse>; - activeTermCodes: ReadonlyArray<TermCode>; allDegreePlans: ReadonlyArray<DegreePlan>; subjectDescriptions: SubjectCodesTo<SubjectDescription>; search: { visible: boolean; selectedTerm?: TermCode }; @@ -25,7 +24,6 @@ export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { visibleDegreePlan: undefined, visibleYears: {}, savedForLaterCourses: [], - activeTermCodes: [], allDegreePlans: [], subjectDescriptions: {}, search: { visible: 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 343124b..2ca5f0a 100644 --- a/src/app/degree-planner/term-container/term-container.component.html +++ b/src/app/degree-planner/term-container/term-container.component.html @@ -3,10 +3,10 @@ <div class="course-list"> <div class="course-list-inner"> <ng-container *ngIf="enrolledCourses.length === 0"> - <p *ngIf="(term$ | async).era === 'past'" class="no-courses"> + <p *ngIf="(term$ | async).termCode.isPast()" class="no-courses"> No courses taken </p> - <p *ngIf="(term$ | async).era === 'active'" class="no-courses"> + <p *ngIf="(term$ | async).termCode.isActive()" class="no-courses"> Not enrolled in any courses </p> </ng-container> @@ -39,13 +39,13 @@ <div class="course-list-inner term-body"> <ng-container *ngIf="plannedCourses.length === 0 && !hasItemDraggedOver"> - <p *ngIf="(term$ | async).era === 'active'" class="no-courses"> + <p *ngIf="(term$ | async).termCode.isActive()" class="no-courses"> No Courses in cart </p> - <p *ngIf="(term$ | async).era === 'future'" class="no-courses"> + <p *ngIf="(term$ | async).termCode.isFuture()" class="no-courses"> No courses planned </p> - <p *ngIf="(term$ | async).era === 'past'" class="no-courses"> + <p *ngIf="(term$ | async).termCode.isPast()" class="no-courses"> No courses planned </p> </ng-container> @@ -70,7 +70,7 @@ </div> <!-- Add course --> - <div class="add-new-wrapper" *ngIf="(term$ | async).era !== 'past'"> + <div class="add-new-wrapper" *ngIf="(term$ | async).termCode.isPast() == false"> <button mat-raised-button attr.aria-label="Add course to {{ termCode | getTermDescription }}" @@ -163,7 +163,7 @@ </div> <!-- If this term is an active term --> - <ng-container *ngIf="(term$ | async).era === 'active'"> + <ng-container *ngIf="(term$ | async).termCode.isActive()"> <mat-tab-group (selectedTabChange)="changeVisibleCredits($event)" [selectedIndex]="(enrolledCourses.length > 0) ? 0 : 1"> <mat-tab [label]="'In Progress (' + enrolledCourses.length + ')'" aria-label="In progress courses"> <ng-container cdkFocusinitial *ngTemplateOutlet="enrolled"></ng-container> @@ -175,7 +175,7 @@ </ng-container> <!-- If this term is a past term --> - <ng-container *ngIf="(term$ | async).era === 'past'"> + <ng-container *ngIf="(term$ | async).termCode.isPast()"> <mat-tab-group (selectedTabChange)="changeVisibleCredits($event)" [selectedIndex]="0"> <mat-tab [label]="'Completed (' + enrolledCourses.length + ')'" aria-label="Completed courses"> <ng-container cdkFocusinitial *ngTemplateOutlet="enrolled"></ng-container> @@ -187,7 +187,7 @@ </ng-container> <!-- If this term is a future term --> - <ng-container *ngIf="(term$ | async).era === 'future'"> + <ng-container *ngIf="(term$ | async).termCode.isFuture()"> <ng-container cdkFocusinitial *ngTemplateOutlet="planned"></ng-container> </ng-container> </mat-card> 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 84eea72..b21a889 100644 --- a/src/app/degree-planner/term-container/term-container.component.ts +++ b/src/app/degree-planner/term-container/term-container.component.ts @@ -16,7 +16,7 @@ import { NotesDialogData, } from '@app/degree-planner/dialogs/notes-dialog/notes-dialog.component'; import * as utils from '@app/degree-planner/shared/utils'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode, Era } from '@app/degree-planner/shared/term-codes/termcode'; import { ConfirmDialogComponent } from '@app/shared/dialogs/confirm-dialog/confirm-dialog.component'; import { MediaMatcher } from '@angular/cdk/layout'; import { ConstantsService } from '../services/constants.service'; @@ -72,7 +72,6 @@ export class TermContainerComponent implements OnInit, OnDestroy { // List of courses pulled for the Observable public plannedCourses: ReadonlyArray<Course>; public enrolledCourses: ReadonlyArray<Course>; - public era: 'past' | 'active' | 'future'; public hasItemDraggedOver: boolean; public plannedCredits: string; public enrolledCredits: number; @@ -156,19 +155,14 @@ export class TermContainerComponent implements OnInit, OnDestroy { } this.termSubscription = this.term$.subscribe(term => { - // const {plannedCourses, enrolledCourses} = term; this.plannedCourses = term.plannedCourses; this.plannedCredits = this.sumPlannedCredits(term.plannedCourses); this.enrolledCourses = term.enrolledCourses; this.enrolledCredits = this.sumEnrolledCredits(term.enrolledCourses); - this.era = term.era; - const activeTermEnrollmentStatus = this.plannedCourses.forEach( - course => course.studentEnrollmentStatus, - ); - - this.visibleCredits = term.era === 'past' ? 'enrolled' : 'planned'; + this.visibleCredits = + term.termCode.era === Era.Past ? 'enrolled' : 'planned'; }); this.note$ = this.term$.pipe( @@ -247,11 +241,12 @@ export class TermContainerComponent implements OnInit, OnDestroy { if (newContainer === previousContainer) { const newIndex = event.currentIndex; - const { id: recordId, termCode } = event.item.data as Course; + const termCode = event.container.data; + const { id: recordId } = event.item.data as Course; if (recordId !== null) { const action = new actions.MoveCourseInsideTerm({ - termCode: new TermCode(termCode), + termCode, recordId, newIndex, }); diff --git a/src/app/degree-planner/year-container/year-container.component.html b/src/app/degree-planner/year-container/year-container.component.html index ab9ed4a..f685417 100644 --- a/src/app/degree-planner/year-container/year-container.component.html +++ b/src/app/degree-planner/year-container/year-container.component.html @@ -14,11 +14,11 @@ fxLayoutGap="20px" fxLayoutAlign="start stretch" class="term-container-wrapper"> - <cse-term-container fxFlex="33%" [termCode]="yearCode.fall"> + <cse-term-container fxFlex="33%" [termCode]="yearCode.fall()"> </cse-term-container> - <cse-term-container fxFlex="33%" [termCode]="yearCode.spring"> + <cse-term-container fxFlex="33%" [termCode]="yearCode.spring()"> </cse-term-container> - <cse-term-container fxFlex="33%" [termCode]="yearCode.summer"> + <cse-term-container fxFlex="33%" [termCode]="yearCode.summer()"> </cse-term-container> </div> </mat-expansion-panel> diff --git a/src/app/degree-planner/year-container/year-container.component.ts b/src/app/degree-planner/year-container/year-container.component.ts index 3c3064a..be7ab44 100644 --- a/src/app/degree-planner/year-container/year-container.component.ts +++ b/src/app/degree-planner/year-container/year-container.component.ts @@ -7,7 +7,7 @@ import { ExpandAcademicYear, CollapseAcademicYear, } from '@app/degree-planner/store/actions/ui.actions'; -import { YearCode } from '@app/core/models/termcode'; +import { YearCode } from '../shared/term-codes/yearcode'; @Component({ selector: 'cse-year-container', diff --git a/src/app/shared/components/course-details/course-details.component.html b/src/app/shared/components/course-details/course-details.component.html index c5b9bdd..9cff49b 100644 --- a/src/app/shared/components/course-details/course-details.component.html +++ b/src/app/shared/components/course-details/course-details.component.html @@ -34,6 +34,7 @@ <form [formGroup]="termSelector" (ngSubmit)="addCourseToPlan($event)"> <mat-form-field style="margin-right:20px;"> <mat-select + [compareWith]="sameTermCodes" placeholder="Term" aria-label="Term" matInput diff --git a/src/app/shared/components/course-details/course-details.component.ts b/src/app/shared/components/course-details/course-details.component.ts index e6e31cd..ef6d419 100644 --- a/src/app/shared/components/course-details/course-details.component.ts +++ b/src/app/shared/components/course-details/course-details.component.ts @@ -3,14 +3,14 @@ import { Component, OnInit, OnDestroy, Inject, Input } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, map, filter } from 'rxjs/operators'; +import { distinctUntilChanged, filter, take } from 'rxjs/operators'; import { MatDialog } from '@angular/material'; import { Store, select } from '@ngrx/store'; import { CourseDetails } from '@app/core/models/course-details'; import * as selectors from '@app/degree-planner/store/selectors'; import * as utils from '@app/degree-planner/shared/utils'; -import { TermCode } from '@app/core/models/termcode'; +import { TermCode } from '@app/degree-planner/shared/term-codes/termcode'; import { GlobalState } from '@app/core/state'; import { Course } from '@app/core/models/course'; import { PlannedTerm } from '@app/core/models/planned-term'; @@ -34,13 +34,13 @@ export class CourseDetailsComponent implements OnInit, OnDestroy { @Input() termCode: TermCode; public courseDetails: CourseDetails; public type: 'course' | 'search' | 'saved'; - public selectedSearchTerm: string; + public selectedSearchTerm: TermCode | undefined; public term$: Observable<PlannedTerm>; public termSelector: FormGroup; public mobileView: MediaQueryList; - public selectedSearchTerm$: Observable<string>; - public droppableTermCodes$: Observable<string[]>; + public selectedSearchTerm$: Observable<TermCode | undefined>; + public droppableTermCodes$: Observable<TermCode[]>; public searchTermSubscription: Subscription; public termSubscription: Subscription; public plannedCourses: ReadonlyArray<Course>; @@ -58,26 +58,16 @@ export class CourseDetailsComponent implements OnInit, OnDestroy { } ngOnInit() { - this.selectedSearchTerm$ = this.store.pipe( - select(selectors.getSelectedSearchTerm), - map(termCode => (termCode ? termCode.toString() : '0000')), - ); + this.searchTermSubscription = this.store + .select(selectors.getSelectedSearchTerm) + .pipe(take(1)) + .subscribe(term => (this.termSelector = this.fb.group({ term }))); this.droppableTermCodes$ = this.store.pipe( select(selectors.selectAllVisibleYears), utils.yearsToDroppableTermCodes(), - distinctUntilChanged(utils.compareStringArrays), - ); - - this.searchTermSubscription = this.selectedSearchTerm$.subscribe( - termCode => { - this.selectedSearchTerm = termCode; - }, + distinctUntilChanged(utils.compareArrays((a, b) => a.equals(b))), ); - - this.termSelector = this.fb.group({ - term: this.selectedSearchTerm, - }); } ngOnDestroy() { @@ -87,9 +77,14 @@ export class CourseDetailsComponent implements OnInit, OnDestroy { addCourseToPlan($event) { $event.preventDefault(); - const termCode = new TermCode(this.termSelector.value.term); + const termCode: TermCode | undefined = this.termSelector.value.term; const subjectCode = this.courseDetails.subject.subjectCode; const courseId = this.courseDetails.courseId; + + if (termCode === undefined) { + return; + } + const payload = { courseId, termCode, @@ -100,7 +95,7 @@ export class CourseDetailsComponent implements OnInit, OnDestroy { this.term$ = this.store.pipe( select(selectors.selectVisibleTerm, { - termCode: new TermCode(this.termSelector.value.term), + termCode, }), filter(isntUndefined), distinctUntilChanged(), @@ -117,7 +112,7 @@ export class CourseDetailsComponent implements OnInit, OnDestroy { this.dialog .open(ConfirmDialogComponent, { data: { - title: "Can't add course to term", + title: `Can't add course to term`, confirmText: 'OK', dialogClass: 'alertDialog', text: `This course already exists in selected term`, @@ -139,4 +134,14 @@ export class CourseDetailsComponent implements OnInit, OnDestroy { } } } + + public sameTermCodes(a: TermCode | undefined, b: TermCode | undefined) { + if (a === undefined && b === undefined) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.equals(b); + } + } } diff --git a/src/app/shared/pipes/academic-year-state.pipe.ts b/src/app/shared/pipes/academic-year-state.pipe.ts index a4a1328..6e22ae6 100644 --- a/src/app/shared/pipes/academic-year-state.pipe.ts +++ b/src/app/shared/pipes/academic-year-state.pipe.ts @@ -1,25 +1,17 @@ import { Pipe, PipeTransform } from '@angular/core'; -import * as utils from '@app/degree-planner/shared/utils'; -import { YearCode } from '@app/core/models/termcode'; -import { ConstantsService } from '@app/degree-planner/services/constants.service'; +import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode'; @Pipe({ name: 'academicYearState' }) export class AcademicYearStatePipe implements PipeTransform { - constructor(private constants: ConstantsService) {} - - transform(yearCode: string | YearCode): string { - if (typeof yearCode === 'string') { - yearCode = new YearCode(yearCode); + transform(yearCode: YearCode): string { + if (yearCode.summer().isPast()) { + return `Past year: ${yearCode.fromYear}-${yearCode.toYear}`; } - const era = utils.pickYearEra(yearCode, this.constants.activeTermCodes()); - switch (era) { - case 'past': - return `Past year: ${yearCode.fromYear}-${yearCode.toYear}`; - case 'future': - return `Future year: ${yearCode.fromYear}-${yearCode.toYear}`; - default: - return `Active year: ${yearCode.fromYear}-${yearCode.toYear}`; + if (yearCode.fall().isFuture()) { + return `Future year: ${yearCode.fromYear}-${yearCode.toYear}`; } + + return `Active year: ${yearCode.fromYear}-${yearCode.toYear}`; } } diff --git a/src/app/shared/pipes/get-term-description.pipe.ts b/src/app/shared/pipes/get-term-description.pipe.ts index 917f1bb..21d62ba 100644 --- a/src/app/shared/pipes/get-term-description.pipe.ts +++ b/src/app/shared/pipes/get-term-description.pipe.ts @@ -1,13 +1,11 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { TermCode } from '@app/core/models/termcode'; +import { RawTermCode } from '@app/degree-planner/shared/term-codes/without-era'; -@Pipe({ - name: 'getTermDescription', -}) +@Pipe({ name: 'getTermDescription' }) export class GetTermDescriptionPipe implements PipeTransform { - transform(termCode: string | TermCode): string { + transform(termCode: RawTermCode | string): string { if (typeof termCode === 'string') { - termCode = new TermCode(termCode); + termCode = new RawTermCode(termCode); } return termCode.description; -- GitLab