"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"
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';
imports: [
CommonModule,
FormsModule,
+ ReactiveFormsModule,
RouterModule,
NgbModule,
EgCoreModule
NgbModule,
FormsModule,
EgCoreModule,
+ ReactiveFormsModule,
PrintComponent,
DialogComponent,
AlertDialogComponent,
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.
datatype?: string;
orgField?: string; // 'shortname' || 'name'
datePlusTime?: boolean;
+ timezoneContextOrg?: number;
}
@Injectable({providedIn: 'root'})
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
// 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);
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
+ }
}
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);
+ });
+
});
*/
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
],
ComboboxComponent,
ComboboxEntryComponent,
DateSelectComponent,
- OrgSelectComponent
+ OrgSelectComponent,
+ DateRangeSelectComponent,
+ DateTimeSelectComponent,
],
})
--- /dev/null
+.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;
+}
--- /dev/null
+<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>
+
--- /dev/null
+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<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();
+ });
+
+});
--- /dev/null
+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();
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+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';
+
+@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');
+ }
+
+}
+
--- /dev/null
+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);
+ }
+}
+
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.
BibSummaryComponent,
TranslateComponent,
AdminPageComponent,
- EgHelpPopoverComponent
+ EgHelpPopoverComponent,
+ DatetimeValidatorDirective,
],
imports: [
EgCommonModule,
- ReactiveFormsModule,
CommonWidgetsModule,
GridModule
],
BibSummaryComponent,
TranslateComponent,
AdminPageComponent,
- EgHelpPopoverComponent
+ EgHelpPopoverComponent,
+ DatetimeValidatorDirective,
]
})
</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"
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'
private sbChannel: any;
sbChannelText: string;
+ myTimeForm: FormGroup;
+
constructor(
private idl: IdlService,
private org: OrgService,
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 => {
);
});
}
+
+ 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;
+ }
}
* 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;
}
* 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;
+}