LP#1884787: update Angular staff client to work with momement-timezone >= 0.5.29
authorGalen Charlton <gmc@equinoxinitiative.org>
Tue, 23 Jun 2020 18:39:45 +0000 (14:39 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Thu, 25 Jun 2020 14:01:27 +0000 (10:01 -0400)
Now that moment-timezone ships with an index.d.ts (as of 0.5.29), this
patch updates how moment-timezone is imported and used since we
now have to care more about type-checking.

Among other things, this updates the ScheduleRow interface to account
for the fact that with the stricter type checking coming from the recent
moment-timezone change, ScheduleRow.time as a moment.Moment object
cannot be in the same interface as a string index type.

To test
-------
[1] Make sure that results of  'npm run test' are clean.
[2] Create a reservation or two in the booking interface and
    verify that scheduled reservations show up on the grid
    in the create reservations page.
[3] Verify that the icons indicating whether resources are
    available or not at a given time are correct on the
    create reservations grid.

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/core/format.service.ts
Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts
Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts
Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts
Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts

index 043ea92..3cd755d 100644 (file)
@@ -3,7 +3,7 @@ import {DatePipe, CurrencyPipe, getLocaleDateFormat, getLocaleTimeFormat, getLoc
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {LocaleService} from '@eg/core/locale.service';
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
 /**
  * Format IDL vield values for display.
@@ -121,7 +121,7 @@ export class FormatService {
                 } else {
                     tz = this.wsOrgTimezone;
                 }
-                const date = Moment(value).tz(tz);
+                const date = moment(value).tz(tz);
                 if (!date.isValid()) {
                     console.error('Invalid date in format service', value);
                     return '';
@@ -161,37 +161,37 @@ export class FormatService {
     /**
      * Create a Moment from an ISO string
      */
-    momentizeIsoString(isoString: string, timezone: string): Moment {
-        return (isoString.length) ? Moment(isoString, timezone) : Moment();
+    momentizeIsoString(isoString: string, timezone: string): moment.Moment {
+        return (isoString.length) ? moment(isoString, timezone) : moment();
     }
 
     /**
      * Turn a date string into a Moment using the date format org setting.
      */
-    momentizeDateString(date: string, timezone: string, strict?, locale?): Moment {
+    momentizeDateString(date: string, timezone: string, strict?, locale?): moment.Moment {
         return this.momentize(date, this.makeFormatParseable(this.dateFormat, locale), timezone, strict);
     }
 
     /**
      * Turn a datetime string into a Moment using the datetime format org setting.
      */
-    momentizeDateTimeString(date: string, timezone: string, strict?, locale?): Moment {
+    momentizeDateTimeString(date: string, timezone: string, strict?, locale?): moment.Moment {
         return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat, locale), timezone, strict);
     }
 
     /**
      * Turn a string into a Moment using the provided format string.
      */
-    private momentize(date: string, format: string, timezone: string, strict: boolean): Moment {
+    private momentize(date: string, format: string, timezone: string, strict: boolean): moment.Moment {
         if (format.length) {
-            const result = Moment.tz(date, format, true, timezone);
-            if (isNaN(result) || 'Invalid date' === result) {
+            const result = moment.tz(date, format, true, timezone);
+            if (!result.isValid()) {
                 if (strict) {
                     throw new Error('Error parsing date ' + date);
                 }
-                return Moment.tz(date, format, false, timezone);
+                return moment.tz(date, format, false, timezone);
             }
-        return Moment(new Date(date), timezone);
+        return moment(new Date(date), timezone);
         }
     }
 
index 604b23a..ed20e84 100644 (file)
@@ -3,7 +3,7 @@ import {FormatService} from '@eg/core/format.service';
 import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, NgControl} from '@angular/forms';
 import {NgbDatepicker, NgbTimeStruct, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap';
 import {DatetimeValidator} from '@eg/share/validators/datetime_validator.directive';
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
 @Component({
     selector: 'eg-datetime-select',
@@ -36,7 +36,7 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
     ) {
         if (controlDir) { controlDir.valueAccessor = this; }
         this.onChangeAsIso = new EventEmitter<string>();
-        const startValue = Moment.tz([], this.timezone);
+        const startValue = moment.tz([], this.timezone);
         this.dateTimeForm = new FormGroup({
             'stringVersion': new FormControl(
                 this.format.transform({value: startValue, datatype: 'timestamp', datePlusTime: true}),
@@ -57,7 +57,7 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
             this.timezone = this.format.wsOrgTimezone;
         }
         if (this.initialIso) {
-            this.writeValue(Moment(this.initialIso).tz(this.timezone));
+            this.writeValue(moment(this.initialIso).tz(this.timezone));
         }
         this.dateTimeForm.get('stringVersion').valueChanges.subscribe((value) => {
             if ('VALID' === this.dateTimeForm.get('stringVersion').status) {
@@ -81,7 +81,7 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
             }
         });
         this.dateTimeForm.get('date').valueChanges.subscribe((date) => {
-            const newDate = Moment.tz([date.year, (date.month - 1), date.day,
+            const newDate = moment.tz([date.year, (date.month - 1), date.day,
                 this.time.value.hour, this.time.value.minute, 0], this.timezone);
             this.dateTimeForm.patchValue({stringVersion:
                 this.format.transform({value: newDate, datatype: 'timestamp', datePlusTime: true})},
@@ -91,7 +91,7 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
         });
 
         this.dateTimeForm.get('time').valueChanges.subscribe((time) => {
-            const newDate = Moment.tz([this.date.value.year,
+            const newDate = moment.tz([this.date.value.year,
                 (this.date.value.month - 1),
                 this.date.value.day,
                 time.hour, time.minute, 0],
@@ -105,16 +105,16 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
         });
     }
 
-    setDatePicker(current: Moment) {
-        const withTZ = current ? current.tz(this.timezone) : Moment.tz([], this.timezone);
+    setDatePicker(current: moment.Moment) {
+        const withTZ = current ? current.tz(this.timezone) : moment.tz([], this.timezone);
         this.dateTimeForm.patchValue({date: {
             year: withTZ.year(),
             month: withTZ.month() + 1,
             day: withTZ.date() }});
     }
 
-    setTimePicker(current: Moment) {
-        const withTZ = current ? current.tz(this.timezone) : Moment.tz([], this.timezone);
+    setTimePicker(current: moment.Moment) {
+        const withTZ = current ? current.tz(this.timezone) : moment.tz([], this.timezone);
         this.dateTimeForm.patchValue({time: {
             hour: withTZ.hour(),
             minute: withTZ.minute(),
@@ -122,7 +122,7 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
     }
 
 
-    writeValue(value: Moment) {
+    writeValue(value: moment.Moment) {
         if (value !== undefined && value !== null) {
             this.dateTimeForm.patchValue({
                 stringVersion: this.format.transform({value: value, datatype: 'timestamp', datePlusTime: true})});
@@ -131,7 +131,7 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
         }
     }
 
-    registerOnChange(fn: (value: Moment) => any): void {
+    registerOnChange(fn: (value: moment.Moment) => any): void {
         this.onChange = fn;
     }
     registerOnTouched(fn: () => any): void {
index 046488b..c6ee1f4 100644 (file)
@@ -2,9 +2,9 @@ import {Directive, Input} from '@angular/core';
 import {NG_VALIDATORS, AbstractControl, FormControl, ValidationErrors, ValidatorFn} from '@angular/forms';
 import {Injectable} from '@angular/core';
 
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
-export function notBeforeMomentValidator(notBeforeMe: Moment): ValidatorFn {
+export function notBeforeMomentValidator(notBeforeMe: moment.Moment): ValidatorFn {
     return (control: AbstractControl): {[key: string]: any} | null => {
         return (control.value && control.value.isBefore(notBeforeMe)) ?
             {tooEarly: 'This cannot be before ' + notBeforeMe.format('LLL')} : null;
@@ -20,7 +20,7 @@ export function notBeforeMomentValidator(notBeforeMe: Moment): ValidatorFn {
     }]
 })
 export class NotBeforeMomentValidatorDirective {
-    @Input('egNotBeforeMoment') notBeforeMoment: Moment;
+    @Input('egNotBeforeMoment') notBeforeMoment: moment.Moment;
 
     validate(control: AbstractControl): {[key: string]: any} | null {
         return this.notBeforeMoment ?
index bf61b19..b40a1f3 100644 (file)
@@ -16,7 +16,7 @@ import {PatronBarcodeValidator} from '@eg/share/validators/patron_barcode_valida
 import {ToastService} from '@eg/share/toast/toast.service';
 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
 const startTimeIsBeforeEndTimeValidator: ValidatorFn = (fg: FormGroup): ValidationErrors | null => {
     const start = fg.get('startTime').value;
@@ -80,7 +80,7 @@ export class CreateReservationDialogComponent
                 [this.pbv.validate]
             ),
             'emailNotify': new FormControl(true),
-            'startTime': new FormControl(null, notBeforeMomentValidator(Moment().add('15', 'minutes'))),
+            'startTime': new FormControl(null, notBeforeMomentValidator(moment().add('15', 'minutes'))),
             'endTime': new FormControl(),
             'resourceList': new FormControl(),
             'note': new FormControl(),
@@ -174,9 +174,9 @@ export class CreateReservationDialogComponent
         );
     }
 
-    setDefaultTimes(times: Moment[], granularity: number) {
-        this.create.patchValue({startTime: Moment.min(times),
-        endTime: Moment.max(times).clone().add(granularity, 'minutes')
+    setDefaultTimes(times: moment.Moment[], granularity: number) {
+        this.create.patchValue({startTime: moment.min(times),
+        endTime: moment.max(times).clone().add(granularity, 'minutes')
         });
     }
 
index 9addf19..7769d99 100644 (file)
 </eg-create-reservation-dialog>
 
 <ng-template #reservationsTemplate let-row="row" let-col="col">
-  <ng-container *ngIf="row[col.name]">
+  <ng-container *ngIf="row.patrons && row.patrons[col.name]">
     <ul class="list-unstyled">
-      <li *ngFor="let reservation of row[col.name]">
+      <li *ngFor="let reservation of row.patrons[col.name]">
         <button class="btn btn-info" (click)="openReservationViewer(reservation['reservationId'])">
           {{reservation['patronLabel']}}
         </button>
index e026920..9f05c27 100644 (file)
@@ -20,7 +20,7 @@ import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {ScheduleGridService, ScheduleRow} from './schedule-grid.service';
 import {NoTimezoneSetComponent} from './no-timezone-set.component';
 
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
 const startOfDayIsBeforeEndOfDayValidator: ValidatorFn = (fg: FormGroup): ValidationErrors | null => {
     const start = fg.get('startOfDay').value;
@@ -280,7 +280,7 @@ export class CreateReservationComponent implements OnInit, AfterViewInit, OnDest
             }),
             takeLast(1),
             switchMap(() => {
-                let range = {startTime: Moment(), endTime: Moment()};
+                let range = {startTime: moment(), endTime: moment()};
 
                 if (this.multiday) {
                     range = this.scheduleService.momentizeDateRange(
@@ -316,7 +316,7 @@ export class CreateReservationComponent implements OnInit, AfterViewInit, OnDest
             };
             this.resources.forEach(resource => {
                 this.cellTextGenerator[resource.barcode()] = row =>  {
-                    return row[resource.barcode()] ? row[resource.barcode()].map(reservation => reservation['patronLabel']).join(', ') : '';
+                    return row.patrons[resource.barcode()] ? row.patrons[resource.barcode()].map(reservation => reservation['patronLabel']).join(', ') : '';
                 };
             });
         });
index 79caf7c..07ee97e 100644 (file)
@@ -17,7 +17,7 @@ import {NoTimezoneSetComponent} from './no-timezone-set.component';
 import {ReservationActionsService} from './reservation-actions.service';
 import {CancelReservationDialogComponent} from './cancel-reservation-dialog.component';
 
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
 // A filterable grid of reservations used in various booking interfaces
 
@@ -121,10 +121,10 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
                     where['pickup_time'] = {'!=': null};
                     where['return_time'] = null;
                 } else if ('returnedToday' === this.status) {
-                    where['return_time'] = {'>': Moment().startOf('day').toISOString()};
+                    where['return_time'] = {'>': moment().startOf('day').toISOString()};
                 } else if ('capturedToday' === this.status) {
-                    where['capture_time'] = {'between': [Moment().startOf('day').toISOString(),
-                        Moment().add(1, 'day').startOf('day').toISOString()]};
+                    where['capture_time'] = {'between': [moment().startOf('day').toISOString(),
+                        moment().add(1, 'day').startOf('day').toISOString()]};
                 }
             } else {
                 where['return_time'] = null;
@@ -299,7 +299,7 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
     enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
         return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
             switchMap((tz) => {
-                row['length'] = Moment(row['end_time']()).from(Moment(row['start_time']()), true);
+                row['length'] = moment(row['end_time']()).from(moment(row['start_time']()), true);
                 row['timezone'] = tz['lib.timezone'];
                 return of(row);
             })
@@ -325,7 +325,7 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
         this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
     }
 
-    momentizeIsoString(isoString: string, timezone: string): Moment {
+    momentizeIsoString(isoString: string, timezone: string): moment.Moment {
         return this.format.momentizeIsoString(isoString, timezone);
     }
 }
index 7c6823f..46929de 100644 (file)
@@ -8,7 +8,7 @@ import {PcrudService} from '@eg/core/pcrud.service';
 import {GridRowFlairEntry} from '@eg/share/grid/grid';
 import {DateRange} from '@eg/share/daterange-select/daterange-select.component';
 
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
 export interface ReservationPatron {
   patronId: number;
@@ -16,11 +16,15 @@ export interface ReservationPatron {
   reservationId: number;
 }
 
-export interface ScheduleRow {
-    time: Moment;
+interface ScheduleRowPatrons {
     [key: string]: ReservationPatron[];
 }
 
+export interface ScheduleRow {
+    time: moment.Moment;
+    patrons: ScheduleRowPatrons;
+}
+
 // Various methods that fetch data for and process the schedule of reservations
 
 @Injectable({providedIn: 'root'})
@@ -54,8 +58,8 @@ export class ScheduleGridService {
     resourceAvailabilityIcon = (row: ScheduleRow, numResources: number): GridRowFlairEntry => {
         let icon = {icon: 'event_busy', title: 'All resources are reserved at this time'};
         let busyColumns = 0;
-        for (const key in row) {
-            if (row[key] instanceof Array && row[key].length) {
+        for (const key in row.patrons) {
+            if (row.patrons[key] instanceof Array && row.patrons[key].length) {
                 busyColumns += 1;
             }
         }
@@ -83,30 +87,30 @@ export class ScheduleGridService {
         });
     }
 
-    momentizeDateRange = (range: DateRange, timezone: string): {startTime: Moment, endTime: Moment} => {
+    momentizeDateRange = (range: DateRange, timezone: string): {startTime: moment.Moment, endTime: moment.Moment} => {
         return {
-            startTime: Moment.tz([
+            startTime: moment.tz([
                 range.fromDate.year,
                 range.fromDate.month - 1,
                 range.fromDate.day],
                 timezone),
-            endTime: Moment.tz([
+            endTime: moment.tz([
                 range.toDate.year,
                 range.toDate.month - 1,
                 range.toDate.day + 1],
                 timezone)
         };
     }
-    momentizeDay = (date: Date, start: NgbTimeStruct, end: NgbTimeStruct, timezone: string): {startTime: Moment, endTime: Moment} => {
+    momentizeDay = (date: Date, start: NgbTimeStruct, end: NgbTimeStruct, timezone: string): {startTime: moment.Moment, endTime: moment.Moment} => {
         return {
-            startTime: Moment.tz([
+            startTime: moment.tz([
                 date.getFullYear(),
                 date.getMonth(),
                 date.getDate(),
                 start.hour,
                 start.minute],
                 timezone),
-            endTime: Moment.tz([
+            endTime: moment.tz([
                 date.getFullYear(),
                 date.getMonth(),
                 date.getDate(),
@@ -116,7 +120,7 @@ export class ScheduleGridService {
         };
     }
 
-    createBasicSchedule = (range: {startTime: Moment, endTime: Moment}, granularity: number): ScheduleRow[] => {
+    createBasicSchedule = (range: {startTime: moment.Moment, endTime: moment.Moment}, granularity: number): ScheduleRow[] => {
         const currentTime = range.startTime.clone();
         const schedule = [];
         while (currentTime < range.endTime) {
@@ -126,7 +130,7 @@ export class ScheduleGridService {
         return schedule;
     }
 
-    fetchReservations = (range: {startTime: Moment, endTime: Moment}, resourceIds: number[]): Observable<IdlObject> => {
+    fetchReservations = (range: {startTime: moment.Moment, endTime: moment.Moment}, resourceIds: number[]): Observable<IdlObject> => {
         return this.pcrud.search('bresv', {
             '-or': {'target_resource': resourceIds, 'current_resource': resourceIds},
             'end_time': {'>': range.startTime.toISOString()},
@@ -142,14 +146,15 @@ export class ScheduleGridService {
             const end = (index + 1 < schedule.length) ?
                 schedule[index + 1].time :
                 schedule[index].time.clone().add(granularity, 'minutes');
-            if ((Moment.tz(reservation.start_time(), timezone).isBefore(end)) &&
-                (Moment.tz(reservation.end_time(), timezone).isAfter(start))) {
-                if (!schedule[index][reservation.current_resource().barcode()]) {
-                    schedule[index][reservation.current_resource().barcode()] = [];
+            if ((moment.tz(reservation.start_time(), timezone).isBefore(end)) &&
+                (moment.tz(reservation.end_time(), timezone).isAfter(start))) {
+                if (!schedule[index]['patrons']) schedule[index].patrons = {};
+                if (!schedule[index].patrons[reservation.current_resource().barcode()]) {
+                    schedule[index].patrons[reservation.current_resource().barcode()] = [];
                 }
-                if (schedule[index][reservation.current_resource().barcode()]
+                if (schedule[index].patrons[reservation.current_resource().barcode()]
                     .findIndex(patron => patron.patronId === reservation.usr().id()) === -1) {
-                    schedule[index][reservation.current_resource().barcode()].push(
+                    schedule[index].patrons[reservation.current_resource().barcode()].push(
                         {'patronLabel': reservation.usr().usrname(),
                         'patronId': reservation.usr().id(),
                         'reservationId': reservation.id()});
index 85b567e..a51a725 100644 (file)
@@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing';
 import { AuthService } from '@eg/core/auth.service';
 import { PcrudService } from '@eg/core/pcrud.service';
 import { ScheduleGridService, ScheduleRow } from './schedule-grid.service';
-import * as Moment from 'moment-timezone';
+import * as moment from 'moment-timezone';
 
 describe('ScheduleGridService', () => {
     let service: ScheduleGridService;
@@ -21,20 +21,24 @@ describe('ScheduleGridService', () => {
 
     it('should recognize when a row is completely busy', () => {
         const busyRow: ScheduleRow = {
-            'time': Moment(),
-            'barcode1': [{patronLabel: 'Joe', patronId: 1, reservationId: 3}],
-            'barcode2': [{patronLabel: 'Jill', patronId: 2, reservationId: 5}],
-            'barcode3': [{patronLabel: 'James', patronId: 3, reservationId: 12},
-                {patronLabel: 'Juanes', patronId: 4, reservationId: 18}]
+            'time': moment(),
+            'patrons': {
+                'barcode1': [{patronLabel: 'Joe', patronId: 1, reservationId: 3}],
+                'barcode2': [{patronLabel: 'Jill', patronId: 2, reservationId: 5}],
+                'barcode3': [{patronLabel: 'James', patronId: 3, reservationId: 12},
+                             {patronLabel: 'Juanes', patronId: 4, reservationId: 18}]
+             }
         };
         expect(service.resourceAvailabilityIcon(busyRow, 3).icon).toBe('event_busy');
     });
 
     it('should recognize when a row has some availability', () => {
         const rowWithAvailability: ScheduleRow = {
-            'time': Moment(),
-            'barcode3': [{patronLabel: 'James', patronId: 3, reservationId: 11},
-                {patronLabel: 'Juanes', patronId: 4, reservationId: 17}]
+            'time': moment(),
+            'patrons': {
+                'barcode3': [{patronLabel: 'James', patronId: 3, reservationId: 11},
+                             {patronLabel: 'Juanes', patronId: 4, reservationId: 17}]
+            }
         };
         expect(service.resourceAvailabilityIcon(rowWithAvailability, 3).icon).toBe('event_available');
     });