[open-ils-commits] [GIT] Evergreen ILS branch master updated. 9f4e3b50d8f61a759a3c55bd6ac40fbecde98972

Evergreen Git git at git.evergreen-ils.org
Mon Aug 26 11:55:34 EDT 2019


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  9f4e3b50d8f61a759a3c55bd6ac40fbecde98972 (commit)
       via  fe0c1ee84ac59a7dc965d388e56b5bb734700366 (commit)
       via  0dc564508af0d7faf4329faf04301aa52ce8f2cf (commit)
       via  5f66a6106c005548e46dc23df70a6d84c4711ae9 (commit)
       via  d95ab870ae2b143799c8d369e0e898c588d36e13 (commit)
      from  5489249b535f2817a5da3e862dd59f1501434e87 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 9f4e3b50d8f61a759a3c55bd6ac40fbecde98972
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Aug 26 11:50:02 2019 -0400

    LP1834662 Minor lingering lint repair
    
    Unrelated to LP1834662, but there's no time like the present.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts
index ecfe67adcc..34d6666c30 100644
--- a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts
+++ b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts
@@ -24,9 +24,9 @@ export class AccessKeyService {
         // Most recent assignment always wins.
         this.assignments.forEach(a => {
             if (a.key === assn.key) {
-                if (a.ctx === assn.ctx) { 
+                if (a.ctx === assn.ctx) {
                     // If key and context match, keep only the most recent.
-                    return; 
+                    return;
                 } else {
                     // An assignment within a different context shadows
                     // an existing assignment.  Keep the assignment

commit fe0c1ee84ac59a7dc965d388e56b5bb734700366
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Thu Jul 11 12:48:22 2019 -0700

    LP1834662: Fix incorrect locale fallback
    
    Resolves a deprecation warning and validation errors in the
    datetime-select component
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

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 b6fba2dea4..043ea920f3 100644
--- a/Open-ILS/src/eg2/src/app/core/format.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/format.service.ts
@@ -159,17 +159,24 @@ export class FormatService {
     idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); }
 
     /**
+     * Create a Moment from an ISO string
+     */
+    momentizeIsoString(isoString: string, timezone: string): 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 = false): Moment {
-        return this.momentize(date, this.makeFormatParseable(this.dateFormat), timezone, strict);
+    momentizeDateString(date: string, timezone: string, strict?, locale?): 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 = false): Moment {
-        return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat), timezone, strict);
+    momentizeDateTimeString(date: string, timezone: string, strict?, locale?): Moment {
+        return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat, locale), timezone, strict);
     }
 
     /**
@@ -184,9 +191,7 @@ export class FormatService {
                 }
                 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);
+        return Moment(new Date(date), timezone);
         }
     }
 
@@ -199,7 +204,7 @@ export class FormatService {
      */
     private makeFormatParseable(original: string, locale?: string): string {
         if (!original) { return ''; }
-        if (!locale) { locale = locale; }
+        if (!locale) { locale = this.locale.currentLocaleCode(); }
         switch (original) {
             case 'short': {
                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Short);
@@ -299,3 +304,4 @@ export class FormatValuePipe implements PipeTransform {
         return this.formatter.transform({value: value, datatype: datatype});
     }
 }
+
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 272ab41b3f..cd5feaa107 100644
--- a/Open-ILS/src/eg2/src/app/core/format.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/format.spec.ts
@@ -97,15 +97,15 @@ describe('FormatService', () => {
     });
 
     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');
+        const momentVersion = service['makeFormatParseable']('M/d/yy, h:mm a', 'en-US');
         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');
+        const momentVersion = service['makeFormatParseable']('MMM d, y, h:mm:ss a', 'ar-JO');
         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');
+        const momentVersion = service['makeFormatParseable']('MMMM d, y, h:mm:ss a z', 'fr-CA');
         expect(momentVersion).toBe('MMMM D, Y, h:mm:ss a [GMT]Z');
     });
     it('should transform full Angular format strings to a valid MomentJS one using Angular locale en-US', () => {
@@ -133,7 +133,7 @@ describe('FormatService', () => {
     });
     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);
+        const moment = service.momentizeDateTimeString('7/3/12, 6:06 PM', 'Africa/Addis_Ababa', false, 'fr-CA');
         expect(moment.isValid()).toBe(true);
     });
 

commit 0dc564508af0d7faf4329faf04301aa52ce8f2cf
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Sun Jul 7 13:40:27 2019 -0700

    LP1834662: Various fixes to the datetime select
    
    * Adds the caret specifier to package.json and bumps up the version of
    moment-timezone
    * Adds an example of the egValidDatetime directive
    * Makes the makeFormatParseable function localizable
    * Gets rid of an error when this component is used without ngModel or
    reactive forms
    * Fixes various linting errors
    * Closes the datetimepicker if the user opens another datetimepicker
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json
index 1bafd4d4c2..4b6b8bcf5d 100644
--- a/Open-ILS/src/eg2/package.json
+++ b/Open-ILS/src/eg2/package.json
@@ -30,7 +30,7 @@
     "file-saver": "^2.0.2",
     "material-design-icons": "^3.0.1",
     "moment": "2.24.0",
-    "moment-timezone": "0.5.23",
+    "moment-timezone": "^0.5.26",
     "ngx-cookie": "^4.1.2",
     "rxjs": "^6.5.2",
     "zone.js": "^0.8.29"
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 20f0fdff18..b6fba2dea4 100644
--- a/Open-ILS/src/eg2/src/app/core/format.service.ts
+++ b/Open-ILS/src/eg2/src/app/core/format.service.ts
@@ -1,7 +1,8 @@
 import {Injectable, Pipe, PipeTransform} from '@angular/core';
-import {DatePipe, CurrencyPipe} from '@angular/common';
+import {DatePipe, CurrencyPipe, getLocaleDateFormat, getLocaleTimeFormat, getLocaleDateTimeFormat, FormatWidth} from '@angular/common';
 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';
 
 /**
@@ -31,7 +32,8 @@ export class FormatService {
         private datePipe: DatePipe,
         private currencyPipe: CurrencyPipe,
         private idl: IdlService,
-        private org: OrgService
+        private org: OrgService,
+        private locale: LocaleService
     ) {
 
         // Create an inilne polyfill for Number.isNaN, which is
@@ -195,44 +197,81 @@ export class FormatService {
      *
      * Returns a blank string if it can't do this transformation.
      */
-    private makeFormatParseable(original: string): string {
+    private makeFormatParseable(original: string, locale?: string): string {
         if (!original) { return ''; }
+        if (!locale) { locale = locale; }
         switch (original) {
             case 'short': {
-                return 'M/D/YY, h:mm a';
+                const template = getLocaleDateTimeFormat(locale, FormatWidth.Short);
+                const date = getLocaleDateFormat(locale, FormatWidth.Short);
+                const time = getLocaleTimeFormat(locale, FormatWidth.Short);
+                original = template
+                    .replace('{1}', date)
+                    .replace('{0}', time)
+                    .replace(/\'(\w+)\'/, '[$1]');
+                break;
             }
             case 'medium': {
-                return 'MMM D, Y, h:mm:ss a';
+                const template = getLocaleDateTimeFormat(locale, FormatWidth.Medium);
+                const date = getLocaleDateFormat(locale, FormatWidth.Medium);
+                const time = getLocaleTimeFormat(locale, FormatWidth.Medium);
+                original = template
+                    .replace('{1}', date)
+                    .replace('{0}', time)
+                    .replace(/\'(\w+)\'/, '[$1]');
+                break;
             }
             case 'long': {
-                return 'MMMM D, Y, h:mm:ss a [GMT]Z';
+                const template = getLocaleDateTimeFormat(locale, FormatWidth.Long);
+                const date = getLocaleDateFormat(locale, FormatWidth.Long);
+                const time = getLocaleTimeFormat(locale, FormatWidth.Long);
+                original = template
+                    .replace('{1}', date)
+                    .replace('{0}', time)
+                    .replace(/\'(\w+)\'/, '[$1]');
+                break;
             }
             case 'full': {
-                return 'dddd, MMMM D, Y, h:mm:ss a [GMT]Z';
+                const template = getLocaleDateTimeFormat(locale, FormatWidth.Full);
+                const date = getLocaleDateFormat(locale, FormatWidth.Full);
+                const time = getLocaleTimeFormat(locale, FormatWidth.Full);
+                original = template
+                    .replace('{1}', date)
+                    .replace('{0}', time)
+                    .replace(/\'(\w+)\'/, '[$1]');
+                break;
             }
             case 'shortDate': {
-                return 'M/D/YY';
+                original = getLocaleDateFormat(locale, FormatWidth.Short);
+                break;
             }
             case 'mediumDate': {
-                return 'MMM D, Y';
+                original = getLocaleDateFormat(locale, FormatWidth.Medium);
+                break;
             }
             case 'longDate': {
-                return 'MMMM D, Y';
+                original = getLocaleDateFormat(locale, FormatWidth.Long);
+                break;
             }
             case 'fullDate': {
-                return 'dddd, MMMM D, Y';
+                original = getLocaleDateFormat(locale, FormatWidth.Full);
+                break;
             }
             case 'shortTime': {
-                return 'h:mm a';
+                original = getLocaleTimeFormat(locale, FormatWidth.Short);
+                break;
             }
             case 'mediumTime': {
-                return 'h:mm:ss a';
+                original = getLocaleTimeFormat(locale, FormatWidth.Medium);
+                break;
             }
             case 'longTime': {
-                return 'h:mm:ss a [GMT]Z';
+                original = getLocaleTimeFormat(locale, FormatWidth.Long);
+                break;
             }
             case 'fullTime': {
-                return 'h:mm:ss a [GMT]Z';
+                original = getLocaleTimeFormat(locale, FormatWidth.Full);
+                break;
             }
         }
         return original
@@ -260,4 +299,3 @@ export class FormatValuePipe implements PipeTransform {
         return this.formatter.transform({value: value, datatype: datatype});
     }
 }
-
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 81b3201897..272ab41b3f 100644
--- a/Open-ILS/src/eg2/src/app/core/format.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/format.spec.ts
@@ -1,4 +1,4 @@
-import {DatePipe, CurrencyPipe} from '@angular/common';
+import {DatePipe, CurrencyPipe, registerLocaleData} from '@angular/common';
 import {IdlService} from './idl.service';
 import {EventService} from './event.service';
 import {NetService} from './net.service';
@@ -6,8 +6,13 @@ import {AuthService} from './auth.service';
 import {PcrudService} from './pcrud.service';
 import {StoreService} from './store.service';
 import {OrgService} from './org.service';
+import {LocaleService} from './locale.service';
+import {Location} from '@angular/common';
 import {FormatService} from './format.service';
-
+import {SpyLocation} from '@angular/common/testing';
+import localeArJO from '@angular/common/locales/ar-JO';
+import localeCs from '@angular/common/locales/cs';
+import localeFrCA from '@angular/common/locales/fr-CA';
 
 describe('FormatService', () => {
 
@@ -20,6 +25,9 @@ describe('FormatService', () => {
     let orgService: OrgService;
     let evtService: EventService;
     let storeService: StoreService;
+    let localeService: LocaleService;
+    // tslint:disable-next-line:prefer-const
+    let location: SpyLocation;
     let service: FormatService;
 
     beforeEach(() => {
@@ -32,11 +40,13 @@ describe('FormatService', () => {
         authService = new AuthService(evtService, netService, storeService);
         pcrudService = new PcrudService(idlService, netService, authService);
         orgService = new OrgService(netService, authService, pcrudService);
+        localeService = new LocaleService(location, null, pcrudService);
         service = new FormatService(
             datePipe,
             currencyPipe,
             idlService,
-            orgService
+            orgService,
+            localeService
         );
     });
 
@@ -98,9 +108,24 @@ describe('FormatService', () => {
         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('should transform full Angular format strings to a valid MomentJS one using Angular locale en-US', () => {
+        const momentVersion = service['makeFormatParseable']('full', 'en-US');
+        expect(momentVersion).toBe('dddd, MMMM D, Y [at] h:mm:ss a [GMT]Z');
+    });
+    it('should transform shortDate Angular format strings to a valid MomentJS one using Angular locale cs-CZ', () => {
+        registerLocaleData(localeCs);
+        const momentVersion = service['makeFormatParseable']('shortDate', 'cs-CZ');
+        expect(momentVersion).toBe('DD.MM.YY');
+    });
+    it('should transform mediumDate Angular format strings to a valid MomentJS one using Angular locale fr-CA', () => {
+        registerLocaleData(localeFrCA);
+        const momentVersion = service['makeFormatParseable']('mediumDate', 'fr-CA');
+        expect(momentVersion).toBe('D MMM Y');
+    });
+    it('should transform long Angular format strings to a valid MomentJS one using Angular locale ar-JO', () => {
+        registerLocaleData(localeArJO);
+        const momentVersion = service['makeFormatParseable']('long', 'ar-JO');
+        expect(momentVersion).toBe('D MMMM 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);
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
index 52a0c47e1d..5a23c01da6 100644
--- 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
@@ -6,6 +6,7 @@ import {ReactiveFormsModule} from '@angular/forms';
 import {NgbDate} from '@ng-bootstrap/ng-bootstrap';
 
 @Component({
+    // tslint:disable-next-line:component-selector
     selector: 'ngb-datepicker',
     template: ''
 })
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
index 7f13c12444..3b931e904c 100644
--- 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
@@ -3,7 +3,7 @@
   [formGroup]="dateTimeForm"
   class="input-group"
   ngbDropdown
-  [autoClose]="false"
+  [autoClose]="'outside'"
   #dt="ngbDropdown">
   <input type="datetime"
     [attr.id]="domId.length ? domId : null" 
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 e1874137d3..6f7cf8b4b5 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
@@ -34,7 +34,7 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
         @Self()
         public controlDir: NgControl, // so that the template can access validation state
     ) {
-        controlDir.valueAccessor = this;
+        if (controlDir) { controlDir.valueAccessor = this; }
         this.onChangeAsIso = new EventEmitter<string>();
         const startValue = Moment.tz([], this.timezone);
         this.dateTimeForm = new FormGroup({
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
index bed582e426..15a9ae9acc 100644
--- 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
@@ -25,7 +25,7 @@ export class DatetimeValidator implements Validator {
     selector: '[egValidDatetime]',
     providers: [{
         provide: NG_VALIDATORS,
-        useExisting: forwardRef(() => DatetimeValidator),
+        useExisting: DatetimeValidatorDirective,
         multi: true
     }]
 })
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 b46f85d502..b2d14c1b95 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
@@ -238,6 +238,10 @@
     </form>
   </div>
 </div>
+<label for="date-time-input">
+  Set the datetime and timezone library settings, and enter a valid datetime string for an exciting animation surprise:
+</label>
+<input id="date-time-input" type="text" class="date-time-input" ngModel egValidDatetime required>
 <br/><br/>
 
 <h4>Grid with filtering</h4>
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 8ab704a352..c6ea7c3f17 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
@@ -23,7 +23,10 @@ import {GridComponent} from '@eg/share/grid/grid.component';
 import * as Moment from 'moment-timezone';
 
 @Component({
-  templateUrl: 'sandbox.component.html'
+  templateUrl: 'sandbox.component.html',
+  styles: ['.date-time-input.ng-invalid {border: 5px purple solid;}',
+    '.date-time-input.ng-valid {border: 5px green solid; animation: slide 5s linear 1s infinite alternate;}',
+    '@keyframes slide {0% {margin-left:0px;} 50% {margin-left:200px;}']
 })
 export class SandboxComponent implements OnInit {
 

commit 5f66a6106c005548e46dc23df70a6d84c4711ae9
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jul 5 14:21:49 2019 -0400

    LP1834662 Allow date format defaults to persist.
    
    Avoid clobbering the default date / time format strings set by the
    FormatService.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>

diff --git a/Open-ILS/src/eg2/src/app/staff/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
index 2c94ec76d3..94b469ee67 100644
--- a/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
+++ b/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
@@ -133,9 +133,17 @@ export class StaffResolver implements Resolve<Observable<any>> {
             'ui.staff.max_recent_patrons',
             'ui.staff.angular_catalog.enabled' // navbar
         ]).then(settings => {
-            this.format.wsOrgTimezone = settings['lib.timezone'];
-            this.format.dateFormat = settings['webstaff.format.dates'];
-            this.format.dateTimeFormat = settings['webstaff.format.date_and_time'];
+            // Avoid clobbering defaults
+            if (settings['lib.timezone']) {
+                this.format.wsOrgTimezone = settings['lib.timezone'];
+            }
+            if (settings['webstaff.format.dates']) {
+                this.format.dateFormat = settings['webstaff.format.dates'];
+            }
+            if (settings['webstaff.format.date_and_time']) {
+                this.format.dateTimeFormat =
+                    settings['webstaff.format.date_and_time'];
+            }
         });
     }
 }

commit d95ab870ae2b143799c8d369e0e898c588d36e13
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Wed May 8 15:06:22 2019 -0700

    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 <sandbej at linnbenton.edu>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json
index cbc8f27db7..1bafd4d4c2 100644
--- a/Open-ILS/src/eg2/package.json
+++ b/Open-ILS/src/eg2/package.json
@@ -29,6 +29,8 @@
     "core-js": "^2.6.9",
     "file-saver": "^2.0.2",
     "material-design-icons": "^3.0.1",
+    "moment": "2.24.0",
+    "moment-timezone": "0.5.23",
     "ngx-cookie": "^4.1.2",
     "rxjs": "^6.5.2",
     "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 76394c3924..ead50f1e54 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} 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';
 import {EgCoreModule} from '@eg/core/core.module';
 
@@ -41,6 +41,7 @@ import {BoolDisplayComponent} from '@eg/share/util/bool.component';
   imports: [
     CommonModule,
     FormsModule,
+    ReactiveFormsModule,
     RouterModule,
     NgbModule,
     EgCoreModule
@@ -51,6 +52,7 @@ import {BoolDisplayComponent} from '@eg/share/util/bool.component';
     NgbModule,
     FormsModule,
     EgCoreModule,
+    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 63aeec66f0..20f0fdff18 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,15 +109,6 @@ export class FormatService {
                 return org ? org[orgField]() : '';
 
             case 'timestamp':
-                const date = new Date(value);
-                if (Number.isNaN(date.getTime())) {
-                    console.error('Invalid date in format service', value);
-                    return '';
-                }
-                let fmt = this.dateFormat || 'shortDate';
-                if (params.datePlusTime) {
-                    fmt = this.dateTimeFormat || 'short';
-                }
                 let tz;
                 if (params.idlField === 'dob') {
                     // special case: since dob is the only date column that the
@@ -123,8 +116,19 @@ export class FormatService {
                     // as a UTC value; apply the correct timezone rather than the
                     // local one
                     tz = 'UTC';
+                } else {
+                    tz = this.wsOrgTimezone;
+                }
+                const date = Moment(value).tz(tz);
+                if (!date.isValid()) {
+                    console.error('Invalid date in format service', value);
+                    return '';
+                }
+                let fmt = this.dateFormat || 'shortDate';
+                if (params.datePlusTime) {
+                    fmt = this.dateTimeFormat || 'short';
                 }
-                return this.datePipe.transform(date, fmt, tz);
+                return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
 
             case 'money':
                 return this.currencyPipe.transform(value);
@@ -142,6 +146,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/common-widgets.module.ts b/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
index 71c5d3023c..01b16bd4da 100644
--- a/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
+++ b/Open-ILS/src/eg2/src/app/share/common-widgets.module.ts
@@ -5,24 +5,30 @@
 */
 import {NgModule, ModuleWithProviders} from '@angular/core';
 import {CommonModule} from '@angular/common';
-import {FormsModule} from '@angular/forms';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
 import {EgCoreModule} from '@eg/core/core.module';
 import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
 import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
+import {DateRangeSelectComponent} from '@eg/share/daterange-select/daterange-select.component';
+import {DateTimeSelectComponent} from '@eg/share/datetime-select/datetime-select.component';
+
 
 @NgModule({
   declarations: [
     ComboboxComponent,
     ComboboxEntryComponent,
     DateSelectComponent,
-    OrgSelectComponent
+    OrgSelectComponent,
+    DateRangeSelectComponent,
+    DateTimeSelectComponent,
   ],
   imports: [
     CommonModule,
     FormsModule,
+    ReactiveFormsModule,
     NgbModule,
     EgCoreModule
   ],
@@ -34,7 +40,9 @@ import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
     ComboboxComponent,
     ComboboxEntryComponent,
     DateSelectComponent,
-    OrgSelectComponent
+    OrgSelectComponent,
+    DateRangeSelectComponent,
+    DateTimeSelectComponent,
   ],
 })
 
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 @@
+<ngb-datepicker #dp
+  (select)="onDateSelection($event)"
+  [displayMonths]="2"
+  [dayTemplate]="t"
+  [outsideDays]="'hidden'"
+  [markDisabled]="markDisabled">
+</ngb-datepicker>
+
+<ng-template #t let-date let-focused="focused">
+  <span class="daterange-day"
+    [class.focused]="focused"
+    [class.range]="isRange(date)"
+    [class.faded]="isHovered(date) || isInside(date)"
+    [class.today]="date.equals(today())"
+    (touch)="onTouched()"
+    (mouseenter)="hoveredDate = date"
+    (mouseleave)="hoveredDate = null">
+    {{ date.day }}
+  </span>
+</ng-template>
+
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';
+
+ at Component({
+    selector: 'ngb-datepicker',
+    template: ''
+})
+class EgMockDateSelectComponent {
+    @Input() displayMonths: number;
+    @Input() dayTemplate: TemplateRef<any>;
+    @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<DateRangeSelectComponent>;
+
+    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;
+}
+
+ at 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 @@
+<span class="material-icons" *ngIf="controlDir && !controlDir.control.valid">error</span>
+<form
+  [formGroup]="dateTimeForm"
+  class="input-group"
+  ngbDropdown
+  [autoClose]="false"
+  #dt="ngbDropdown">
+  <input type="datetime"
+    [attr.id]="domId.length ? domId : null" 
+    name="{{fieldName}}"
+    class="form-control datetime-input"
+    formControlName="stringVersion"
+    (focus)="dt.open()"
+    [attr.disabled]="readOnly ? true : null"
+    [required]="required"
+    (touch)="onTouched()">
+  <div class="input-group-btn">
+    <button class="btn btn-primary" ngbDropdownToggle
+      aria-label="Select date and time" i18n-aria-label>
+      <span class="material-icons mat-icon-in-button">calendar_today</span>
+    </button>
+  </div>
+  <div ngbDropdownMenu>
+    <div i18n *ngIf="readOnly">
+      Cannot edit this date or time.
+    </div>
+    <div *ngIf="!readOnly">
+      <div *ngIf="controlDir && controlDir.control.errors"
+        role="alert"
+        class="alert alert-danger">
+        <span class="material-icons">error</span>
+	{{firstError(controlDir.control.errors)}}
+      </div>
+      <ngb-datepicker #datePicker
+        formControlName="date"
+        [footerTemplate]="time"
+        (touch)="onTouched()">
+      </ngb-datepicker>
+    </div>
+  </div>
+
+  <ng-template #time>
+    <ngb-timepicker name="time"
+      [meridian]="true"
+      formControlName="time"
+      [spinners]="true"
+      [hourStep]="1"
+      [minuteStep]="minuteStep || 30"
+      (touch)="onTouched()">
+    </ngb-timepicker>
+    <span *ngIf="showTZ && timezone" class="badge badge-info">{{ timezone }}</span>
+    <span *ngIf="showTZ && !timezone" class="badge badge-warning" i18n>Timezone not set</span>
+    <button i18n class="btn btn-success" (click)="dt.close()">Choose time</button>
+  </ng-template>
+
+</form>
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..e1874137d3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts
@@ -0,0 +1,158 @@
+import {Component, EventEmitter, Input, Output, 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';
+
+ at Component({
+    selector: 'eg-datetime-select',
+    templateUrl: './datetime-select.component.html',
+})
+export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
+    @Input() domId = '';
+    @Input() fieldName: string;
+    @Input() initialIso: string;
+    @Input() required: boolean;
+    @Input() minuteStep = 15;
+    @Input() showTZ = true;
+    @Input() timezone: string = this.format.wsOrgTimezone;
+    @Input() readOnly = false;
+    @Output() onChangeAsIso: EventEmitter<string>;
+
+    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;
+        this.onChangeAsIso = new EventEmitter<string>();
+        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() {
+        if (!this.timezone) {
+            this.timezone = this.format.wsOrgTimezone;
+        }
+        if (this.initialIso) {
+            this.writeValue(Moment(this.initialIso).tz(this.timezone));
+        }
+        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.onChangeAsIso.emit(model.toISOString());
+                    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.onChangeAsIso.emit(newDate.toISOString());
+        });
+
+        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);
+            this.onChangeAsIso.emit(newDate.toISOString());
+        });
+    }
+
+    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 !== undefined && value !== null) {
+            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';
+
+ at 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;
+    }
+}
+
+ at 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 66c62c32eb..969ca379aa 100644
--- a/Open-ILS/src/eg2/src/app/staff/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts
@@ -20,7 +20,7 @@ import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.compo
 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.
@@ -41,11 +41,11 @@ import {ReactiveFormsModule} from '@angular/forms';
     BibSummaryComponent,
     TranslateComponent,
     AdminPageComponent,
-    EgHelpPopoverComponent
+    EgHelpPopoverComponent,
+    DatetimeValidatorDirective,
   ],
   imports: [
     EgCommonModule,
-    ReactiveFormsModule,
     CommonWidgetsModule,
     GridModule
   ],
@@ -66,7 +66,8 @@ import {ReactiveFormsModule} from '@angular/forms';
     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 2febd8eb3b..b46f85d502 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
@@ -219,6 +219,26 @@
 </eg-grid>
 
 <br/><br/>
+<div class="row">
+  <div class="col">
+    <eg-daterange-select
+      ngModel #myRange="ngModel"
+      [initialRangeStart]="sevenDaysAgo()"
+      [initialRangeLength]="5"
+      [markDisabled]="allFutureDates">
+    </eg-daterange-select>
+    Your range is: {{myRange.value | json}}
+  </div>
+  <div class="col">
+    <form [formGroup]="myTimeForm">
+      <eg-datetime-select
+        formControlName="datetime">
+      </eg-datetime-select>
+      Your datetime is: {{myTimeForm.get('datetime').value | json}}
+    </form>
+  </div>
+</div>
+<br/><br/>
 
 <h4>Grid with filtering</h4>
 <eg-grid #acpGrid idlClass="acp"
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 740d4d14b8..8ab704a352 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
@@ -14,11 +14,13 @@ import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {PrintService} from '@eg/share/print/print.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {NgbDate} from '@ng-bootstrap/ng-bootstrap';
 import {FormGroup, FormControl} from '@angular/forms';
 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 import {FormatService} from '@eg/core/format.service';
 import {StringComponent} from '@eg/share/string/string.component';
 import {GridComponent} from '@eg/share/grid/grid.component';
+import * as Moment from 'moment-timezone';
 
 @Component({
   templateUrl: 'sandbox.component.html'
@@ -98,6 +100,8 @@ export class SandboxComponent implements OnInit {
     private sbChannel: any;
     sbChannelText: string;
 
+    myTimeForm: FormGroup;
+
     constructor(
         private idl: IdlService,
         private org: OrgService,
@@ -251,6 +255,17 @@ export class SandboxComponent implements OnInit {
         b.cancel_time('2019-03-25T11:07:59-0400');
         this.bresvEditor.mode = 'create';
         this.bresvEditor.record = b;
+
+        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 => {
@@ -382,6 +397,18 @@ export class SandboxComponent implements OnInit {
             );
         });
     }
+
+    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/styles.css b/Open-ILS/src/eg2/src/styles.css
index 4d28583fde..9573b52e4a 100644
--- a/Open-ILS/src/eg2/src/styles.css
+++ b/Open-ILS/src/eg2/src/styles.css
@@ -134,10 +134,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;
 }
 
@@ -195,3 +195,19 @@ h5 {font-size: .95rem}
  * for the upstream issue that necessitates this.
  */
 body>.dropdown-menu {z-index: 2100;}
+
+/* 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;
+}

-----------------------------------------------------------------------

Summary of changes:
 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    | 174 +++++++++++++++++++--
 Open-ILS/src/eg2/src/app/core/format.spec.ts       |  57 ++++++-
 .../src/app/share/accesskey/accesskey.service.ts   |   4 +-
 .../src/eg2/src/app/share/common-widgets.module.ts |  14 +-
 .../daterange-select.component.css                 |  12 ++
 .../daterange-select.component.html                |  21 +++
 .../daterange-select.component.spec.ts             |  51 ++++++
 .../daterange-select/daterange-select.component.ts | 102 ++++++++++++
 .../datetime-select/datetime-select.component.html |  56 +++++++
 .../datetime-select/datetime-select.component.ts   | 158 +++++++++++++++++++
 .../validators/datetime_validator.directive.ts     |  41 +++++
 Open-ILS/src/eg2/src/app/staff/common.module.ts    |   9 +-
 Open-ILS/src/eg2/src/app/staff/resolver.service.ts |  14 +-
 .../src/app/staff/sandbox/sandbox.component.html   |  24 +++
 .../eg2/src/app/staff/sandbox/sandbox.component.ts |  32 +++-
 Open-ILS/src/eg2/src/styles.css                    |  20 ++-
 18 files changed, 764 insertions(+), 31 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


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list