From 9ccc5892259fe71d1e6bf4f76f1ffd2c47f476bf Mon Sep 17 00:00:00 2001 From: Jane Sandberg Date: Wed, 8 May 2019 15:06:22 -0700 Subject: [PATCH] LP1834662: Add date-related components to Angular client. * Adds a moment.js-based datetime-select widget to Angular (including a read-only version) * Adds a daterange-select widget to Angular Signed-off-by: Jane Sandberg --- Open-ILS/src/eg2/package.json | 2 + Open-ILS/src/eg2/src/app/common.module.ts | 4 +- Open-ILS/src/eg2/src/app/core/format.service.ts | 117 +++++++++++++++- Open-ILS/src/eg2/src/app/core/format.spec.ts | 26 ++++ .../daterange-select.component.css | 12 ++ .../daterange-select.component.html | 21 +++ .../daterange-select.component.spec.ts | 50 +++++++ .../daterange-select/daterange-select.component.ts | 102 ++++++++++++++ .../datetime-select/datetime-select.component.html | 56 ++++++++ .../datetime-select/datetime-select.component.ts | 147 +++++++++++++++++++++ .../validators/datetime_validator.directive.ts | 41 ++++++ Open-ILS/src/eg2/src/app/staff/common.module.ts | 13 +- .../src/app/staff/sandbox/sandbox.component.html | 20 +++ .../eg2/src/app/staff/sandbox/sandbox.component.ts | 24 ++++ .../eg2/src/app/staff/sandbox/sandbox.module.ts | 3 +- Open-ILS/src/eg2/src/styles.css | 19 ++- 16 files changed, 645 insertions(+), 12 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.css create mode 100644 Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.html create mode 100644 Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.spec.ts create mode 100644 Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.ts create mode 100644 Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html create mode 100644 Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts create mode 100644 Open-ILS/src/eg2/src/app/share/validators/datetime_validator.directive.ts diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json index 98b52939e5..30e1d7a3e2 100644 --- a/Open-ILS/src/eg2/package.json +++ b/Open-ILS/src/eg2/package.json @@ -28,6 +28,8 @@ "bootstrap-css-only": "^4.2.1", "core-js": "^2.6.3", "file-saver": "^2.0.0", + "moment": "2.24.0", + "moment-timezone": "0.5.23", "ngx-cookie": "^4.1.2", "rxjs": "^6.4.0", "zone.js": "^0.8.29" diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts index a4e402680d..8f3a0cc9f3 100644 --- a/Open-ILS/src/eg2/src/app/common.module.ts +++ b/Open-ILS/src/eg2/src/app/common.module.ts @@ -4,7 +4,7 @@ import {CommonModule, DatePipe, CurrencyPipe} from '@angular/common'; import {NgModule, ModuleWithProviders} from '@angular/core'; import {RouterModule} from '@angular/router'; -import {FormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; /* @@ -42,6 +42,7 @@ import {BoolDisplayComponent} from '@eg/share/util/bool.component'; imports: [ CommonModule, FormsModule, + ReactiveFormsModule, RouterModule, NgbModule ], @@ -50,6 +51,7 @@ import {BoolDisplayComponent} from '@eg/share/util/bool.component'; RouterModule, NgbModule, FormsModule, + ReactiveFormsModule, PrintComponent, DialogComponent, AlertDialogComponent, 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 d2b2ce51e2..65f53b9a18 100644 --- a/Open-ILS/src/eg2/src/app/core/format.service.ts +++ b/Open-ILS/src/eg2/src/app/core/format.service.ts @@ -2,6 +2,7 @@ import {Injectable, Pipe, PipeTransform} from '@angular/core'; import {DatePipe, CurrencyPipe} from '@angular/common'; import {IdlService, IdlObject} from '@eg/core/idl.service'; import {OrgService} from '@eg/core/org.service'; +import * as Moment from 'moment-timezone'; /** * Format IDL vield values for display. @@ -16,6 +17,7 @@ export interface FormatParams { datatype?: string; orgField?: string; // 'shortname' || 'name' datePlusTime?: boolean; + timezoneContextOrg?: number; } @Injectable({providedIn: 'root'}) @@ -107,12 +109,15 @@ export class FormatService { return org ? org[orgField]() : ''; case 'timestamp': - const date = new Date(value); - let fmt = this.dateFormat || 'shortDate'; - if (params.datePlusTime) { - fmt = this.dateTimeFormat || 'short'; + const date = Moment(value).tz(this.wsOrgTimezone); + if (date) { + let fmt = this.dateFormat || 'shortDate'; + if (params.datePlusTime) { + fmt = this.dateTimeFormat || 'short'; + } + return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ')); } - return this.datePipe.transform(date, fmt); + return value; case 'money': return this.currencyPipe.transform(value); @@ -130,6 +135,108 @@ export class FormatService { return value + ''; } } + /** + * Create an IDL-friendly display version of a human-readable date + */ + idlFormatDate(date: string, timezone: string): string { return this.momentizeDateString(date, timezone).format('YYYY-MM-DD'); } + + /** + * Create an IDL-friendly display version of a human-readable datetime + */ + idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); } + + /** + * Turn a date string into a Moment using the date format org setting. + */ + momentizeDateString(date: string, timezone: string, strict = false): Moment { + return this.momentize(date, this.makeFormatParseable(this.dateFormat), timezone, strict); + } + + /** + * Turn a datetime string into a Moment using the datetime format org setting. + */ + momentizeDateTimeString(date: string, timezone: string, strict = false): Moment { + return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat), timezone, strict); + } + + /** + * Turn a string into a Moment using the provided format string. + */ + private momentize(date: string, format: string, timezone: string, strict: boolean): Moment { + if (format.length) { + const result = Moment.tz(date, format, true, timezone); + if (isNaN(result) || 'Invalid date' === result) { + if (strict) { + throw new Error('Error parsing date ' + date); + } + return Moment.tz(date, format, false, timezone); + } + // TODO: The following fallback returns the date at midnight UTC, + // rather than midnight in the local TZ + return Moment.tz(date, timezone); + } + } + + /** + * Takes a dateFormat or dateTimeFormat string (which uses Angular syntax) and transforms + * it into a format string that MomentJs can use to parse input human-readable strings + * (https://momentjs.com/docs/#/parsing/string-format/) + * + * Returns a blank string if it can't do this transformation. + */ + private makeFormatParseable(original: string): string { + if (!original) { return ''; } + switch (original) { + case 'short': { + return 'M/D/YY, h:mm a'; + } + case 'medium': { + return 'MMM D, Y, h:mm:ss a'; + } + case 'long': { + return 'MMMM D, Y, h:mm:ss a [GMT]Z'; + } + case 'full': { + return 'dddd, MMMM D, Y, h:mm:ss a [GMT]Z'; + } + case 'shortDate': { + return 'M/D/YY'; + } + case 'mediumDate': { + return 'MMM D, Y'; + } + case 'longDate': { + return 'MMMM D, Y'; + } + case 'fullDate': { + return 'dddd, MMMM D, Y'; + } + case 'shortTime': { + return 'h:mm a'; + } + case 'mediumTime': { + return 'h:mm:ss a'; + } + case 'longTime': { + return 'h:mm:ss a [GMT]Z'; + } + case 'fullTime': { + return 'h:mm:ss a [GMT]Z'; + } + } + return original + .replace(/a+/g, 'a') // MomentJs can handle all sorts of meridian strings + .replace(/d/g, 'D') // MomentJs capitalizes day of month + .replace(/EEEEEE/g, '') // MomentJs does not handle short day of week + .replace(/EEEEE/g, '') // MomentJs does not handle narrow day of week + .replace(/EEEE/g, 'dddd') // MomentJs has different syntax for long day of week + .replace(/E{1,3}/g, 'ddd') // MomentJs has different syntax for abbreviated day of week + .replace(/L/g, 'M') // MomentJs does not differentiate between month and month standalone + .replace(/W/g, '') // MomentJs uses W for something else + .replace(/y/g, 'Y') // MomentJs capitalizes year + .replace(/ZZZZ|z{1,4}/g, '[GMT]Z') // MomentJs doesn't put "UTC" in front of offset + .replace(/Z{2,3}/g, 'Z'); // MomentJs only uses 1 Z + } } diff --git a/Open-ILS/src/eg2/src/app/core/format.spec.ts b/Open-ILS/src/eg2/src/app/core/format.spec.ts index 05991df68f..81b3201897 100644 --- a/Open-ILS/src/eg2/src/app/core/format.spec.ts +++ b/Open-ILS/src/eg2/src/app/core/format.spec.ts @@ -86,5 +86,31 @@ describe('FormatService', () => { expect(str).toBe('$12.10'); }); + it('should transform M/d/yy, h:mm a Angular format string to a valid MomentJS one', () => { + const momentVersion = service['makeFormatParseable']('M/d/yy, h:mm a'); + expect(momentVersion).toBe('M/D/YY, h:mm a'); + }); + it('should transform MMM d, y, h:mm:ss a Angular format string to a valid MomentJS one', () => { + const momentVersion = service['makeFormatParseable']('MMM d, y, h:mm:ss a'); + expect(momentVersion).toBe('MMM D, Y, h:mm:ss a'); + }); + it('should transform MMMM d, y, h:mm:ss a z Angular format strings to a valid MomentJS one', () => { + const momentVersion = service['makeFormatParseable']('MMMM d, y, h:mm:ss a z'); + expect(momentVersion).toBe('MMMM D, Y, h:mm:ss a [GMT]Z'); + }); + it('should transform full Angular format strings to a valid MomentJS one', () => { + const momentVersion = service['makeFormatParseable']('full'); + expect(momentVersion).toBe('dddd, MMMM D, Y, h:mm:ss a [GMT]Z'); + }); + it('can create a valid Momentjs object given a valid datetime string and correct format', () => { + const moment = service['momentize']('7/3/12, 6:06 PM', 'M/D/YY, h:mm a', 'Africa/Addis_Ababa', false); + expect(moment.isValid()).toBe(true); + }); + it('can create a valid Momentjs object given a valid datetime string and a dateTimeFormat from org settings', () => { + service['dateTimeFormat'] = 'M/D/YY, h:mm a'; + const moment = service.momentizeDateTimeString('7/3/12, 6:06 PM', 'Africa/Addis_Ababa', false); + expect(moment.isValid()).toBe(true); + }); + }); diff --git a/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.css b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.css new file mode 100644 index 0000000000..4e24456465 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.css @@ -0,0 +1,12 @@ +.daterange-day { + text-align: center; + padding: 0.185rem 0.25rem; + display: inline-block; + height: 2rem; + width: 2rem; +} + +.today { + border: solid 2px #129a78; + border-radius: 5px; +} diff --git a/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.html b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.html new file mode 100644 index 0000000000..b88d0ef974 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.html @@ -0,0 +1,21 @@ + + + + + + {{ date.day }} + + + diff --git a/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.spec.ts b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.spec.ts new file mode 100644 index 0000000000..52a0c47e1d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.spec.ts @@ -0,0 +1,50 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, DebugElement, Input, TemplateRef} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {DateRange, DateRangeSelectComponent} from './daterange-select.component'; +import {ReactiveFormsModule} from '@angular/forms'; +import {NgbDate} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngb-datepicker', + template: '' +}) +class EgMockDateSelectComponent { + @Input() displayMonths: number; + @Input() dayTemplate: TemplateRef; + @Input() outsideDays: string; + @Input() markDisabled: + (date: NgbDate, current: { year: number; month: number; }) => boolean = + (date: NgbDate, current: { year: number; month: number; }) => false +} + +describe('Component: DateRangeSelect', () => { + let component: DateRangeSelectComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + DateRangeSelectComponent, + EgMockDateSelectComponent, + ]}); + + fixture = TestBed.createComponent(DateRangeSelectComponent); + component = fixture.componentInstance; + component.ngOnInit(); + }); + + + it('creates a range when the user clicks two dates, with the earlier date clicked first', () => { + component.onDateSelection(new NgbDate(2004, 6, 4)); + component.onDateSelection(new NgbDate(2005, 7, 27)); + expect(component.selectedRange.toDate).toBeTruthy(); + }); + + it('creates a range with a null value when the user clicks two dates, with the later date clicked first', () => { + component.onDateSelection(new NgbDate(2011, 1, 27)); + component.onDateSelection(new NgbDate(2006, 11, 16)); + expect(component.selectedRange.toDate).toBeNull(); + }); + +}); diff --git a/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.ts b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.ts new file mode 100644 index 0000000000..74a1e342a2 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.ts @@ -0,0 +1,102 @@ +import {Component, Input, forwardRef, OnInit} from '@angular/core'; +import {NgbDate, NgbCalendar} from '@ng-bootstrap/ng-bootstrap'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; + +export interface DateRange { + fromDate?: NgbDate; + toDate?: NgbDate; +} + +@Component({ + selector: 'eg-daterange-select', + templateUrl: './daterange-select.component.html', + styleUrls: [ './daterange-select.component.css' ], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DateRangeSelectComponent), + multi: true + }] +}) +export class DateRangeSelectComponent implements ControlValueAccessor, OnInit { + + // Number of days in the initial + // date range shown to user + @Input() initialRangeLength = 10; + + // Start date of the initial + // date range shown to user + @Input() initialRangeStart = new Date(); + + hoveredDate: NgbDate; + + selectedRange: DateRange; + + // Function to disable certain dates + @Input() markDisabled: + (date: NgbDate, current: { year: number; month: number; }) => boolean = + (date: NgbDate, current: { year: number; month: number; }) => false + + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor(private calendar: NgbCalendar) { } + + ngOnInit() { + this.selectedRange = { + fromDate: new NgbDate( + this.initialRangeStart.getFullYear(), + this.initialRangeStart.getMonth() + 1, + this.initialRangeStart.getDate()), + toDate: this.calendar.getNext( + this.calendar.getToday(), + 'd', + this.initialRangeLength) + }; + } + + onDateSelection(date: NgbDate) { + if (!this.selectedRange.fromDate && !this.selectedRange.toDate) { + this.selectedRange.fromDate = date; + } else if (this.selectedRange.fromDate && !this.selectedRange.toDate && date.after(this.selectedRange.fromDate)) { + this.selectedRange.toDate = date; + } else { + this.selectedRange.toDate = null; + this.selectedRange.fromDate = date; + } + this.onChange(this.selectedRange); + } + + isHovered(date: NgbDate) { + return this.selectedRange.fromDate && + !this.selectedRange.toDate && + this.hoveredDate && + date.after(this.selectedRange.fromDate) && + date.before(this.hoveredDate); + } + + isInside(date: NgbDate) { + return date.after(this.selectedRange.fromDate) && date.before(this.selectedRange.toDate); + } + + isRange(date: NgbDate) { + return date.equals(this.selectedRange.fromDate) || + date.equals(this.selectedRange.toDate) || + this.isInside(date) || + this.isHovered(date); + } + + writeValue(value: DateRange) { + if (value) { + this.selectedRange = value; + } + } + registerOnChange(fn: (value: DateRange) => any): void { + this.onChange = fn; + } + registerOnTouched(fn: () => any): void { + this.onTouched = fn; + } + today(): NgbDate { + return this.calendar.getToday(); + } +} diff --git a/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html new file mode 100644 index 0000000000..7f13c12444 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html @@ -0,0 +1,56 @@ +error +
+ +
+ +
+
+
+ Cannot edit this date or time. +
+
+ + + +
+
+ + + + + {{ timezone }} + Timezone not set + + + +
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 new file mode 100644 index 0000000000..83dee3da27 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts @@ -0,0 +1,147 @@ +import {Component, Input, forwardRef, ViewChild, OnInit, Optional, Self} from '@angular/core'; +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'; + +@Component({ + selector: 'eg-datetime-select', + templateUrl: './datetime-select.component.html', +}) +export class DateTimeSelectComponent implements OnInit, ControlValueAccessor { + @Input() domId = ''; + @Input() fieldName: string; + @Input() required: boolean; + @Input() minuteStep = 15; + @Input() showTZ = true; + @Input() timezone: string = this.format.wsOrgTimezone; + @Input() readOnly = false; + + dateTimeForm: FormGroup; + + @ViewChild('datePicker') datePicker; + + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor( + private format: FormatService, + private dtv: DatetimeValidator, + @Optional() + @Self() + public controlDir: NgControl, // so that the template can access validation state + ) { + controlDir.valueAccessor = this; + const startValue = Moment.tz([], this.timezone); + this.dateTimeForm = new FormGroup({ + 'stringVersion': new FormControl( + this.format.transform({value: startValue, datatype: 'timestamp', datePlusTime: true}), + this.dtv.validate), + 'date': new FormControl({ + year: startValue.year(), + month: startValue.month() + 1, + day: startValue.date() }), + 'time': new FormControl({ + hour: startValue.hour(), + minute: startValue.minute(), + second: 0 }) + }); + } + + ngOnInit() { + this.dateTimeForm.get('stringVersion').valueChanges.subscribe((value) => { + if ('VALID' === this.dateTimeForm.get('stringVersion').status) { + const model = this.format.momentizeDateTimeString(value, this.timezone, false); + if (model && model.isValid()) { + this.onChange(model); + this.dateTimeForm.patchValue({date: { + year: model.year(), + month: model.month() + 1, + day: model.date()}, time: { + hour: model.hour(), + minute: model.minute(), + second: 0 } + }, {emitEvent: false, onlySelf: true}); + this.datePicker.navigateTo({ + year: model.year(), + month: model.month() + 1 + }); + } + } + }); + this.dateTimeForm.get('date').valueChanges.subscribe((date) => { + 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})}, + {emitEvent: false, onlySelf: true}); + this.onChange(newDate); + }); + + this.dateTimeForm.get('time').valueChanges.subscribe((time) => { + const newDate = Moment.tz([this.date.value.year, + (this.date.value.month - 1), + this.date.value.day, + time.hour, time.minute, 0], + this.timezone); + this.dateTimeForm.patchValue({stringVersion: + this.format.transform({ + value: newDate, datatype: 'timestamp', datePlusTime: true})}, + {emitEvent: false, onlySelf: true}); + this.onChange(newDate); + }); + } + + setDatePicker(current: 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); + this.dateTimeForm.patchValue({time: { + hour: withTZ.hour(), + minute: withTZ.minute(), + second: 0 }}); + } + + + writeValue(value: Moment) { + if (!value) { + value = Moment.tz([], this.timezone); + } + this.dateTimeForm.patchValue({ + stringVersion: this.format.transform({value: value, datatype: 'timestamp', datePlusTime: true})}); + this.setDatePicker(value); + this.setTimePicker(value); + } + + registerOnChange(fn: (value: Moment) => any): void { + this.onChange = fn; + } + registerOnTouched(fn: () => any): void { + this.onTouched = fn; + } + + firstError(errors: Object) { + return Object.values(errors)[0]; + } + + get stringVersion(): AbstractControl { + return this.dateTimeForm.get('stringVersion'); + } + + get date(): AbstractControl { + return this.dateTimeForm.get('date'); + } + + get time(): AbstractControl { + return this.dateTimeForm.get('time'); + } + +} + diff --git a/Open-ILS/src/eg2/src/app/share/validators/datetime_validator.directive.ts b/Open-ILS/src/eg2/src/app/share/validators/datetime_validator.directive.ts new file mode 100644 index 0000000000..bed582e426 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/validators/datetime_validator.directive.ts @@ -0,0 +1,41 @@ +import {Directive, forwardRef} from '@angular/core'; +import {NG_VALIDATORS, AbstractControl, FormControl, ValidationErrors, Validator} from '@angular/forms'; +import {FormatService} from '@eg/core/format.service'; +import {EmptyError, Observable, of} from 'rxjs'; +import {single, switchMap, catchError} from 'rxjs/operators'; +import {Injectable} from '@angular/core'; + +@Injectable({providedIn: 'root'}) +export class DatetimeValidator implements Validator { + constructor( + private format: FormatService) { + } + + validate = (control: FormControl) => { + try { + this.format.momentizeDateTimeString(control.value, 'Africa/Addis_Ababa', true); + } catch (err) { + return {datetimeParseError: err.message}; + } + return null; + } +} + +@Directive({ + selector: '[egValidDatetime]', + providers: [{ + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DatetimeValidator), + multi: true + }] +}) +export class DatetimeValidatorDirective { + constructor( + private dtv: DatetimeValidator + ) { } + + validate = (control: FormControl) => { + this.dtv.validate(control); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts index bbf959c98c..e4f1fadbd9 100644 --- a/Open-ILS/src/eg2/src/app/staff/common.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts @@ -17,13 +17,16 @@ import {StringComponent} from '@eg/share/string/string.component'; import {StringService} from '@eg/share/string/string.service'; import {TitleComponent} from '@eg/share/title/title.component'; import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {DateRangeSelectComponent} from '@eg/share/daterange-select/daterange-select.component'; import {DateSelectComponent} from '@eg/share/date-select/date-select.component'; import {BucketDialogComponent} from '@eg/staff/share/buckets/bucket-dialog.component'; +import {DateTimeSelectComponent} from '@eg/share/datetime-select/datetime-select.component'; import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component'; import {TranslateComponent} from '@eg/staff/share/translate/translate.component'; import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component'; import {EgHelpPopoverComponent} from '@eg/share/eg-help-popover/eg-help-popover.component'; import {ReactiveFormsModule} from '@angular/forms'; +import {DatetimeValidatorDirective} from '@eg/share/validators/datetime_validator.directive'; /** * Imports the EG common modules and adds modules common to all staff UI's. @@ -43,12 +46,15 @@ import {ReactiveFormsModule} from '@angular/forms'; TitleComponent, OpChangeComponent, FmRecordEditorComponent, + DateRangeSelectComponent, DateSelectComponent, BucketDialogComponent, + DateTimeSelectComponent, BibSummaryComponent, TranslateComponent, AdminPageComponent, - EgHelpPopoverComponent + EgHelpPopoverComponent, + DatetimeValidatorDirective, ], imports: [ EgCommonModule, @@ -70,12 +76,15 @@ import {ReactiveFormsModule} from '@angular/forms'; TitleComponent, OpChangeComponent, FmRecordEditorComponent, + DateRangeSelectComponent, DateSelectComponent, BucketDialogComponent, + DateTimeSelectComponent, BibSummaryComponent, TranslateComponent, AdminPageComponent, - EgHelpPopoverComponent + EgHelpPopoverComponent, + DatetimeValidatorDirective, ] }) diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index fa58e905e0..b5503064ed 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -174,6 +174,26 @@

+
+
+ + + Your range is: {{myRange.value | json}} +
+
+
+ + + Your datetime is: {{myTimeForm.get('datetime').value | json}} +
+
+
+

PCRUD auto flesh and FormatService detection

Fingerprint: {{aMetarecord}}
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index 17c6e6deee..0c26e8f3f3 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -15,7 +15,9 @@ import {PrintService} from '@eg/share/print/print.service'; import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; import {FormatService} from '@eg/core/format.service'; import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {NgbDate} from '@ng-bootstrap/ng-bootstrap'; import {FormGroup, FormControl} from '@angular/forms'; +import * as Moment from 'moment-timezone'; @Component({ templateUrl: 'sandbox.component.html' @@ -73,6 +75,8 @@ export class SandboxComponent implements OnInit { private sbChannel: any; sbChannelText: string; + myTimeForm: FormGroup; + constructor( private idl: IdlService, private org: OrgService, @@ -160,6 +164,16 @@ export class SandboxComponent implements OnInit { idlField: 'metarecord' }); }); + this.myTimeForm = new FormGroup({ + 'datetime': new FormControl(Moment([]), (c: FormControl) => { + // An Angular custom validator + if (c.value.year() < 2019) { + return { tooLongAgo: 'That\'s before 2019' }; + } else { + return null; + } + } ) + }); } sbChannelHandler = msg => { @@ -265,6 +279,16 @@ export class SandboxComponent implements OnInit { .then(txt => this.toast.success(txt)); }, 4000); } + allFutureDates(date: NgbDate, current: { year: number; month: number; }) { + const currentTime = new Date(); + const today = new NgbDate(currentTime.getFullYear(), currentTime.getMonth() + 1, currentTime.getDate()); + return date.after(today); + } + sevenDaysAgo() { + const d = new Date(); + d.setDate(d.getDate() - 7); + return d; + } } diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts index ec817d0d51..0937ab0ee3 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts @@ -2,7 +2,7 @@ import {NgModule} from '@angular/core'; import {StaffCommonModule} from '@eg/staff/common.module'; import {SandboxRoutingModule} from './routing.module'; import {SandboxComponent} from './sandbox.component'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {ReactiveFormsModule} from '@angular/forms'; @NgModule({ declarations: [ @@ -11,7 +11,6 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms'; imports: [ StaffCommonModule, SandboxRoutingModule, - FormsModule, ReactiveFormsModule ], providers: [ diff --git a/Open-ILS/src/eg2/src/styles.css b/Open-ILS/src/eg2/src/styles.css index 10424f2722..3fa7281404 100644 --- a/Open-ILS/src/eg2/src/styles.css +++ b/Open-ILS/src/eg2/src/styles.css @@ -138,10 +138,10 @@ h5 {font-size: .95rem} * Required valid fields are left-border styled in green-ish. * Invalid fields are left-border styled in red-ish. */ -.form-validated .ng-valid[required], .form-validated .ng-valid.required { +.form-validated .ng-valid[required], .form-validated .ng-valid.required, input[formcontrolname].ng-valid { border-left: 5px solid #78FA89; } -.form-validated .ng-invalid:not(form) { +.form-validated .ng-invalid:not(form), input[formcontrolname].ng-invalid { border-left: 5px solid #FA787E; } @@ -191,3 +191,18 @@ h5 {font-size: .95rem} #eg-print-container pre {border: none} } +/* Styles for eg-daterange-select that don't work + * in the component's CSS file. + */ +.ngb-dp-day:not(.disabled) .daterange-day.focused { + background-color: #e6e6e6; +} +.ngb-dp-day:not(.disabled) .daterange-day.range, .ngb-dp-day:not(.disabled) .daterange-day:hover { + background-color: #129a78; + color: white; + font-size: 1.4em; +} +.ngb-dp-day:not(.disabled) .daterange-day.faded { + background-color: #c9efe4; + color: black; +} -- 2.11.0