LP1879517: Surveys shouldn't end before they begin
authorJane Sandberg <js7389@princeton.edu>
Thu, 21 Jul 2022 00:00:24 +0000 (20:00 -0400)
committerJason Boyer <JBoyer@equinoxOLI.org>
Thu, 30 Mar 2023 22:24:47 +0000 (18:24 -0400)
To test:
1. Go to Admin > Local > Surveys.
2. Create a new survey.
3. Attempt to create a survey where the end date
comes before the start date.  Without this patch, you will get
no notice that this is invalid, and you can save the invalid
record.
4. Edit an existing survey.
5. Repeat step 3 while editing the existing survey.
6. Apply the patch.
7. Repeat steps 1-5.  Note that you now get a notice and
cannot save if the end date is before the start date.

This commit generalizes a validator already present in the booking
module, and corrects several small bugs related to the datetime-select
component.

Signed-off-by: Jane Sandberg <sandbergja@gmail.com>
Signed-off-by: Susan Morrison <smorrison@georgialibraries.org>
Signed-off-by: Jason Boyer <JBoyer@equinoxOLI.org>

14 files changed:
Open-ILS/src/eg2/src/app/core/format.service.ts
Open-ILS/src/eg2/src/app/core/format.spec.ts
Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
Open-ILS/src/eg2/src/app/share/validators/dates_in_order_validator.directive.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/validators/dates_in_order_validator.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts
Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/common.module.ts

index b58f551..5386a8d 100644 (file)
@@ -235,7 +235,7 @@ export class FormatService {
      * Create a Moment from an ISO string
      */
     momentizeIsoString(isoString: string, timezone: string): moment.Moment {
-        return (isoString.length) ? moment(isoString, timezone) : moment();
+        return (isoString?.length) ? moment(isoString).tz(timezone) : moment();
     }
 
     /**
index 4da4830..40cbb29 100644 (file)
@@ -143,6 +143,11 @@ describe('FormatService', () => {
         const moment = service.momentizeDateTimeString('7/3/12, 6:06 PM', 'Africa/Addis_Ababa', false, 'fr-CA');
         expect(moment.isValid()).toBe(true);
     });
+    it('can momentize ISO strings', () => {
+        const moment = service.momentizeIsoString('2022-07-29T17:56:00.000Z', 'America/New_York');
+        expect(moment.isValid()).toBe(true);
+        expect(moment.format('YYYY')).toBe('2022');
+    });
 
 });
 
diff --git a/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.spec.ts b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.spec.ts
new file mode 100644 (file)
index 0000000..b049dbd
--- /dev/null
@@ -0,0 +1,20 @@
+import * as moment from "moment";
+import { DateTimeSelectComponent } from "./datetime-select.component";
+
+
+describe('DateTimeSelectComponent', () => {
+    const mockFormatService = jasmine.createSpyObj('FormatService', ['transform', 'momentizeIsoString']);
+    mockFormatService.momentizeIsoString.and.returnValue(moment('2020-12-11T01:30:05.606Z').tz('America/Vancouver'));
+    const mockDateTimeValidator = jasmine.createSpyObj('DateTimeValidator', ['']);
+    const mockNgControl = jasmine.createSpyObj('ngControl', ['']);
+    const component = new DateTimeSelectComponent(mockFormatService, mockDateTimeValidator, mockNgControl);
+
+    it('accepts an initialIso input and converts it to the correct timezone', () => {
+        component.initialIso = '2020-12-11T01:30:05.606Z';
+        component.timezone = 'America/Vancouver';
+        component.ngOnInit();
+        expect(component.date.value).toEqual({year: 2020, month: 12, day: 10});
+        expect(component.time.value).toEqual({hour: 17, minute: 30, second: 0});
+    });
+
+});
index d9564a3..29cc2ae 100644 (file)
@@ -1,7 +1,6 @@
-import {Component, EventEmitter, Input, Output, forwardRef, ViewChild, OnInit, Optional, Self} from '@angular/core';
+import {Component, EventEmitter, Input, Output, 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';
 import {DateUtil} from '@eg/share/util/date';
@@ -133,7 +132,14 @@ export class DateTimeSelectComponent implements OnInit, ControlValueAccessor {
     }
 
 
-    writeValue(value: moment.Moment) {
+    writeValue(value: moment.Moment|string) {
+        if (typeof value === 'string') {
+            if (value.length === 0) {
+               return;
+            };
+            value = this.format.momentizeIsoString(value, this.timezone);
+        }
+
         if (value !== undefined && value !== null) {
             this.dateTimeForm.patchValue({
                 stringVersion: this.format.transform({value: value, datatype: 'timestamp', datePlusTime: true})});
index 518125c..8a734fb 100644 (file)
   <div class="modal-header bg-info" *ngIf="!hideBanner">
     <h4 class="modal-title" i18n>Record Editor: {{recordLabel}}</h4>
     <ng-container *ngIf="isDialog()">
-      <button type="button" class="close" 
+      <button type="button" class="close"
         i18n-aria-label aria-label="Close" (click)="closeEditor()">
         <span aria-hidden="true">&times;</span>
       </button>
     </ng-container>
   </div>
   <div class="modal-body">
-    <form #fmEditForm="ngForm" role="form" class="form-validated common-form striped-odd">
+    <form #fmEditForm="ngForm" role="form"
+          class="form-validated common-form striped-odd"
+          [egDateFieldOrderList]="dateFieldOrderList">
       <ng-container *ngIf="!record">
-        <!-- display a progress dialog while the editor 
+        <!-- display a progress dialog while the editor
             fetches the needed data -->
-        <eg-progress-inline></eg-progress-inline> 
+        <eg-progress-inline></eg-progress-inline>
       </ng-container>
       <ng-container *ngIf="record">
+      <div role="alert" class="alert alert-danger" *ngIf="fmEditForm.errors?.['datesOutOfOrder'] && (fmEditForm.touched || fmEditForm.dirty)">
+        <span class="material-icons" aria-hidden="true">error</span>
+        <span i18n>Dates must be in the correct order</span>
+      </div>
+
       <div class="form-group row" *ngFor="let field of fields">
         <div class="col-lg-3">
           <label for="{{idPrefix}}-{{field.name}}">{{field.label}}</label>
@@ -39,8 +46,8 @@
             <ng-container *ngSwitchCase="'template'">
               <ng-container
                 *ngTemplateOutlet="field.template; context:customTemplateFieldContext(field)">
-              </ng-container> 
-            </ng-container> 
+              </ng-container>
+            </ng-container>
 
             <ng-container *ngSwitchCase="'readonly'">
               <span>{{record[field.name]()}}</span>
@@ -60,6 +67,8 @@
               <eg-date-select
                 domId="{{idPrefix}}-{{field.name}}"
                 [readOnly]="field.readOnly"
+                [ngModel]="record[field.name]()"
+                name="{{field.name}}"
                 (onChangeAsIso)="record[field.name]($event)"
                 initialIso="{{record[field.name]()}}">
               </eg-date-select>
@@ -73,6 +82,8 @@
                 (onChangeAsIso)="record[field.name]($event)"
                 i18n-validatorError
                 [readOnly]="field.readOnly"
+                [ngModel]="record[field.name]()"
+                name="{{field.name}}"
                 initialIso="{{record[field.name]()}}">
               </eg-datetime-select>
             </ng-container>
                 (onChange)="record[field.name]($event)">
               </eg-org-select>
             </ng-container>
-          
+
             <ng-container *ngSwitchCase="'money'">
               <input
                 class="form-control"
                 [ngModel]="record[field.name]()"
                 (ngModelChange)="record[field.name]($event)"/>
             </ng-container>
-  
+
            <ng-container *ngSwitchCase="'readonly-au'">
               <ng-container *ngIf="field.linkedValues">
                 <a href="/eg/staff/circ/patron/{{field.linkedValues[0].id}}/checkout" target="_blank">{{field.linkedValues[0].label}}
             <ng-container *ngSwitchCase="'link'">
               <eg-combobox
                 id="{{idPrefix}}-{{field.name}}" name="{{field.name}}"
-                placeholder="{{field.label}}..." i18n-placeholder 
+                placeholder="{{field.label}}..." i18n-placeholder
                 [required]="field.isRequired()"
                 [idlClass]="field.class" [asyncSupportsEmptyTermClick]="true"
                 [idlBaseQuery]="field.idlBaseQuery"
             <ng-container *ngSwitchCase="'list'">
               <eg-combobox
                 domId="{{idPrefix}}-{{field.name}}" name="{{field.name}}"
-                placeholder="{{field.label}}..." i18n-placeholder 
+                placeholder="{{field.label}}..." i18n-placeholder
                 [required]="field.isRequired()"
                 [entries]="field.linkedValues"
                 [asyncDataSource]="field.linkedValuesSource"
         [disabled]="record && record.isnew()" i18n>Delete</button>
     </ng-container>
 
-    <button type="button" class="btn btn-info" 
+    <button type="button" class="btn btn-info"
       [disabled]="fmEditForm.invalid" *ngIf="mode !== 'view'"
       (click)="save()" i18n>Save</button>
   </div>
index 8c1f5ba..7328f08 100644 (file)
@@ -157,6 +157,11 @@ export class FmRecordEditorComponent
     // do not close dialog on error saving record
     @Input() remainOpenOnError: false;
 
+    // if date fields need to be in a specific order (e.g.
+    // start date before end date), specify them in a comma-
+    // separated list here.
+    @Input() dateFieldOrderList: '';
+
     // Emit the modified object when the save action completes.
     @Output() recordSaved = new EventEmitter<IdlObject>();
 
diff --git a/Open-ILS/src/eg2/src/app/share/validators/dates_in_order_validator.directive.ts b/Open-ILS/src/eg2/src/app/share/validators/dates_in_order_validator.directive.ts
new file mode 100644 (file)
index 0000000..2b54d76
--- /dev/null
@@ -0,0 +1,44 @@
+import { Directive, Input } from "@angular/core";
+import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn } from "@angular/forms";
+import * as moment from "moment";
+
+export function datesInOrderValidator(fieldNames: string[]): ValidatorFn {
+    return (control: AbstractControl): {[key: string]: any} | null => {
+        if (fieldsAreInOrder(fieldNames, control)) return null;
+        return {datesOutOfOrder: 'Dates should be in order'};
+    };
+}
+
+function fieldsAreInOrder(fieldNames: string[], control: AbstractControl): boolean {
+    if (fieldNames.length === 0) return true;
+    return fieldNames.every((field, index) => {
+        // No need to compare field[0] to the field before it
+        if (index === 0) return true;
+
+        const previousValue = moment(control.get(fieldNames[index - 1])?.value);
+        const currentValue = moment(control.get(field)?.value);
+
+        // If either field is invalid, return true -- there should be other
+        // validation that can catch that
+        if (!previousValue.isValid() || !currentValue.isValid()) return true;
+
+        // Check each field against its predecessor
+        return previousValue.isSameOrBefore(currentValue);
+    });
+}
+
+@Directive({
+    selector: '[egDateFieldOrderList]',
+    providers: [{ provide: NG_VALIDATORS, useExisting: DatesInOrderValidatorDirective, multi: true }]
+  })
+  export class DatesInOrderValidatorDirective implements Validator {
+    @Input('egDateFieldOrderList') dateFieldOrderList = '';
+    validate(control: AbstractControl): ValidationErrors | null {
+      if (this.dateFieldOrderList?.length > 0) {
+        return datesInOrderValidator(this.dateFieldOrderList.split(','))(control);
+      } else {
+        // Don't run validations if we have no fields to examine
+        return () => {null}
+      }
+    }
+  }
diff --git a/Open-ILS/src/eg2/src/app/share/validators/dates_in_order_validator.spec.ts b/Open-ILS/src/eg2/src/app/share/validators/dates_in_order_validator.spec.ts
new file mode 100644 (file)
index 0000000..8e6d52e
--- /dev/null
@@ -0,0 +1,16 @@
+import { AbstractControl } from "@angular/forms";
+import { datesInOrderValidator } from "./dates_in_order_validator.directive";
+
+describe('datesInOrderValidator', () => {
+    const mockForm = jasmine.createSpyObj<AbstractControl>(['get']);
+    const mockEarlierDateInput = jasmine.createSpyObj<AbstractControl>('AbstractControl', [], {value: '2020-10-12'});
+    const mockLaterDateInput = jasmine.createSpyObj<AbstractControl>('AbstractControl', [], {value: '2030-01-01'});
+    it('returns null if two fields are in order', () => {
+        mockForm.get.and.returnValues(mockEarlierDateInput, mockLaterDateInput);
+        expect(datesInOrderValidator(['startDate', 'endDate'])(mockForm)).toEqual(null);
+    });
+    it('returns an object if fields are out of order', () => {
+        mockForm.get.and.returnValues(mockLaterDateInput, mockEarlierDateInput);
+        expect(datesInOrderValidator(['startDate', 'endDate'])(mockForm)).toEqual({ datesOutOfOrder: 'Dates should be in order' });
+    });
+});
index b3d5895..d817c87 100644 (file)
@@ -6,14 +6,15 @@
         <ng-template ngbNavContent>
             <div class="col-lg-6 offset-lg-3 mt-3">
                 <div style="text-align: center;">
-                    <button class="p-2 mb-3 btn btn-danger btn-lg" 
+                    <button class="p-2 mb-3 btn btn-danger btn-lg"
                     (click)="endSurvey()" i18n>
                         End Survey Now
                     </button>
                 </div>
-                <eg-fm-record-editor displayMode="inline" 
+                <eg-fm-record-editor displayMode="inline"
                     hiddenFieldsList="id"
                     datetimeFieldsList="start_date,end_date"
+                    dateFieldOrderList="start_date,end_date"
                     fieldOrder="name,description,owner,start_date,end_date,opac,poll,required,usr_summary"
                     idlClass="asv"
                     mode="update"
@@ -36,7 +37,7 @@
                         <input type="text" [(ngModel)]="question.words" class="form-control"
                             name="question-{{questionIndex}}">
                         <span class="input-group-append">
-                            <button class="ml-2 btn btn-info" 
+                            <button class="ml-2 btn btn-info"
                                 (click)="updateQuestion(question)" i18n>
                                 Save
                             </button>
                             </button>
                         </span>
                     </div>
-                    <div *ngFor="let answer of question.answers; let answerIndex = index;" 
+                    <div *ngFor="let answer of question.answers; let answerIndex = index;"
                         class="mb-2 input-group">
-                        <input class="form-control" type="text" 
+                        <input class="form-control" type="text"
                             [(ngModel)]="answer.words"
                             name="answer-{{questionIndex}}-{{answerIndex}}">
                         <span class="input-group-append">
-                            <button class="ml-2 btn btn-info" 
+                            <button class="ml-2 btn btn-info"
                                 (click)="updateAnswer(answer, question, questionIndex, answerIndex)"
                                 i18n>
                                 Save
                         </span>
                     </div>
                     <div class="mb-2 input-group">
-                        <input class="form-control" type="text" 
+                        <input class="form-control" type="text"
                             [(ngModel)]="newAnswerArray[questionIndex].inputText"
                                 value="">
                         <span class="input-group-append">
-                            <button class="ml-2 btn btn-info" 
+                            <button class="ml-2 btn btn-info"
                                 (click)="createAnswer(newAnswerArray[questionIndex].inputText, question)"
                                 i18n>
                                 Add Answer
@@ -80,9 +81,9 @@
                     <label class="input-group-text">
                         <b>New Question</b>
                     </label>
-                    <input #newQuestionInput 
-                        class="form-control" 
-                        type="text" 
+                    <input #newQuestionInput
+                        class="form-control"
+                        type="text"
                         [(ngModel)]="newQuestionText"
                         name="question-new" value="">
                     <span class="input-group-append">
     </eg-string>
 <eg-string #updateQuestionFailStr i18n-text text="Survey Question update failed">
     </eg-string>
-<eg-string #endSurveyFailedString i18n-text 
+<eg-string #endSurveyFailedString i18n-text
     text="Ending Survey failed or was not allowed"></eg-string>
 <eg-string #endSurveySuccessString i18n-text text="Survey ended"></eg-string>
-<eg-string #questionAlreadyStartedErrString i18n-text 
+<eg-string #questionAlreadyStartedErrString i18n-text
     text="The survey Start Date must be set for the future to add new questions or modify existing questions.">
-    </eg-string>
\ No newline at end of file
+    </eg-string>
index 0ac33cb..2dad71d 100644 (file)
@@ -1,34 +1,36 @@
 <eg-staff-banner bannerText="Survey Configuration" i18n-bannerText>
 </eg-staff-banner>
 
-<eg-grid #grid idlClass="asv" [dataSource]="gridDataSource" 
+<eg-grid #grid idlClass="asv" [dataSource]="gridDataSource"
 [sortable]="true">
     <eg-grid-toolbar-button label="New Survey" i18n-label [action]="createNew">
     </eg-grid-toolbar-button>
     <eg-grid-toolbar-action label="Edit Selected" i18n-label [action]="editSelected">
     </eg-grid-toolbar-action>
-    <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+    <eg-grid-toolbar-action label="Delete Selected" i18n-label
     (onClick)="deleteSelected($event)"></eg-grid-toolbar-action>
-    <eg-grid-toolbar-action label="End Survey Now" i18n-label 
+    <eg-grid-toolbar-action label="End Survey Now" i18n-label
     (onClick)="endSurvey($event)"></eg-grid-toolbar-action>
 </eg-grid>
 
-<eg-fm-record-editor 
+<eg-fm-record-editor
     datetimeFieldsList="start_date,end_date"
+    dateFieldOrderList="start_date,end_date"
     fieldOrder="name,description,owner,start_date,end_date,opac,poll,required,usr_summary"
     hiddenFieldsList="id"
-    #editDialog 
+    #editDialog
+    [defaultNewRecord]="defaultNewRecord"
     idlClass="asv">
 </eg-fm-record-editor>
 
 <eg-string #createString i18n-text text="New Survey Added"></eg-string>
 <eg-string #createErrString i18n-text text="Failed to Create New Survey">
     </eg-string>
-<eg-string #endSurveyFailedString i18n-text 
+<eg-string #endSurveyFailedString i18n-text
     text="Ending Survey failed or was not allowed"></eg-string>
 <eg-string #endSurveySuccessString i18n-text text="Survey ended">
     </eg-string>
-<eg-string #deleteFailedString i18n-text 
+<eg-string #deleteFailedString i18n-text
     text="Delete of Survey failed or was not allowed"></eg-string>
 <eg-string #deleteSuccessString i18n-text text="Delete of Survey succeeded">
     </eg-string>
index c51af57..9a47724 100644 (file)
@@ -3,7 +3,7 @@ import {Component, OnInit, Input, ViewChild} from '@angular/core';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {GridDataSource} from '@eg/share/grid/grid';
 import {Router} from '@angular/router';
-import {IdlObject} from '@eg/core/idl.service';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {StringComponent} from '@eg/share/string/string.component';
@@ -17,6 +17,7 @@ import {AuthService} from '@eg/core/auth.service';
 
 export class SurveyComponent implements OnInit {
 
+    defaultNewRecord: IdlObject;
     gridDataSource: GridDataSource;
 
     @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
@@ -36,6 +37,7 @@ export class SurveyComponent implements OnInit {
 
     constructor(
         private auth: AuthService,
+        private idl: IdlService,
         private net: NetService,
         private pcrud: PcrudService,
         private toast: ToastService,
@@ -69,6 +71,11 @@ export class SurveyComponent implements OnInit {
                 this.navigateToEditPage(idToEdit);
             }
         );
+
+        this.defaultNewRecord = this.idl.create('asv');
+        const nextWeek = new Date();
+        nextWeek.setDate(nextWeek.getDate() + 7);
+        this.defaultNewRecord.end_date(nextWeek.toISOString());
     }
 
     showEditDialog(idlThing: IdlObject): Promise<any> {
index d4a7fa8..e6d89d2 100644 (file)
@@ -43,7 +43,7 @@
         formControlName="endTime"
         [timezone]="timezone">
       </eg-datetime-select>
-      <div role="alert" class="alert alert-danger offset-lg-4" *ngIf="create.errors && create.errors.startTimeNotBeforeEndTime">
+      <div role="alert" class="alert alert-danger offset-lg-4" *ngIf="create.errors?.datesOutOfOrder">
         <span class="material-icons" aria-hidden="true">error</span>
         <span i18n>Start time must be before end time</span>
       </div>
index 98828e7..bea63d9 100644 (file)
@@ -18,15 +18,7 @@ import {ToastService} from '@eg/share/toast/toast.service';
 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import * as moment from 'moment-timezone';
-
-const startTimeIsBeforeEndTimeValidator: ValidatorFn = (fg: FormGroup): ValidationErrors | null => {
-    const start = fg.get('startTime').value;
-    const end = fg.get('endTime').value;
-    return start !== null && end !== null &&
-        start.isBefore(end)
-        ? null
-        : { startTimeNotBeforeEndTime: true };
-};
+import { datesInOrderValidator } from '@eg/share/validators/dates_in_order_validator.directive';
 
 @Component({
   selector: 'eg-create-reservation-dialog',
@@ -84,7 +76,7 @@ export class CreateReservationDialogComponent
             'endTime': new FormControl(),
             'resourceList': new FormControl(),
             'note': new FormControl(),
-        }, [startTimeIsBeforeEndTimeValidator]
+        }, [datesInOrderValidator(['startTime', 'endTime'])]
         );
         if (this.patronId) {
             this.pcrud.search('au', {id: this.patronId}, {
index d463853..7802455 100644 (file)
@@ -18,6 +18,7 @@ import {DatetimeValidatorDirective} from '@eg/share/validators/datetime_validato
 import {MultiSelectComponent} from '@eg/share/multi-select/multi-select.component';
 import {TextMultiSelectComponent} from '@eg/share/text-multi-select/text-multi-select.component';
 import {NotBeforeMomentValidatorDirective} from '@eg/share/validators/not_before_moment_validator.directive';
+import {DatesInOrderValidatorDirective} from '@eg/share/validators/dates_in_order_validator.directive';
 import {PatronBarcodeValidatorDirective} from '@eg/share/validators/patron_barcode_validator.directive';
 import {BroadcastService} from '@eg/share/util/broadcast.service';
 import {CourseService} from './share/course.service';
@@ -43,6 +44,7 @@ import {OfflineService} from '@eg/staff/share/offline.service';
     MultiSelectComponent,
     TextMultiSelectComponent,
     NotBeforeMomentValidatorDirective,
+    DatesInOrderValidatorDirective,
     PatronBarcodeValidatorDirective,
   ],
   imports: [
@@ -69,6 +71,7 @@ import {OfflineService} from '@eg/staff/share/offline.service';
     MultiSelectComponent,
     TextMultiSelectComponent,
     NotBeforeMomentValidatorDirective,
+    DatesInOrderValidatorDirective,
     PatronBarcodeValidatorDirective
   ]
 })