From 0e7c3136f981c6f3d00a9f27132a7a1f27ad528e Mon Sep 17 00:00:00 2001 From: Galen Charlton Date: Tue, 23 Jun 2020 14:39:45 -0400 Subject: [PATCH] LP#1884787: update Angular staff client to work with momement-timezone >= 0.5.29 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 Signed-off-by: Jane Sandberg Signed-off-by: Bill Erickson --- .../src/eg2/src/app/core/format.service.ts | 22 +++++----- .../datetime-select.component.ts | 22 +++++----- .../not_before_moment_validator.directive.ts | 6 +-- .../create-reservation-dialog.component.ts | 10 ++--- .../booking/create-reservation.component.html | 4 +- .../booking/create-reservation.component.ts | 6 +-- .../booking/reservations-grid.component.ts | 12 +++--- .../staff/booking/schedule-grid.service.ts | 43 +++++++++++-------- .../app/staff/booking/schedule-grid.spec.ts | 22 ++++++---- 9 files changed, 78 insertions(+), 69 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/core/format.service.ts b/Open-ILS/src/eg2/src/app/core/format.service.ts index 043ea920f3..3cd755ded1 100644 --- a/Open-ILS/src/eg2/src/app/core/format.service.ts +++ b/Open-ILS/src/eg2/src/app/core/format.service.ts @@ -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); } } diff --git a/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts index 604b23a1ca..ed20e841a4 100644 --- a/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts +++ b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts @@ -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(); - 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 { diff --git a/Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts b/Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts index 046488bfc9..c6ee1f4b38 100644 --- a/Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts +++ b/Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts @@ -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 ? diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts index bf61b190c7..b40a1f3c08 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts @@ -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') }); } diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html index 9addf19c32..7769d99af0 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html +++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html @@ -193,9 +193,9 @@ - +
    -
  • +
  • diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts index e026920807..9f05c27759 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts @@ -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(', ') : ''; }; }); }); diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts index 79caf7ce75..07ee97ee4e 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts @@ -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 => { 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); } } diff --git a/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts index 7c6823f6e1..46929deb06 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts @@ -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 => { + fetchReservations = (range: {startTime: moment.Moment, endTime: moment.Moment}, resourceIds: number[]): Observable => { 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()}); diff --git a/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts index 85b567e73b..a51a725083 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts @@ -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'); }); -- 2.43.2