[APPS-2108] date-fns adapter for datetime pickers, many datetime parsing and validation fixes (#8992)

* migrate cloud date widget to date-fns, fix test bugs

* [ci:force] update docs

* [ci:force] remove commented out code

* [APPS-2232] date cell validator, unit tests

* improved moment adapter, code cleanup

* datetime adapter, many code fixes

* code review fixes

* code cleanup

* cleanup

* fix max datetime validation, update tests

* remove e2e already covered by unit tests

* fix search date range

* remove fake demo shell e2e for search

* remove fake demo shell e2e for search page

* cleanup e2e

* migrate dynamic table to date-fns

* fix e2e formatting

* migrate protractor to unit tests

* cleanup e2e
This commit is contained in:
Denys Vuika
2023-10-15 15:58:22 +01:00
committed by GitHub
parent c637f3eb2a
commit 2f36da5765
37 changed files with 881 additions and 1059 deletions

View File

@@ -7,29 +7,26 @@
<mat-form-field class="adf-date-widget" [class.adf-left-label-input-datepicker]="field.leftLabels" [hideRequiredMarker]="true">
<label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<input matInput
[id]="field.id"
[value]="field.value"
[required]="isRequired()"
[disabled]="field.readOnly"
(change)="onDateChanged($any($event).srcElement.value)"
[placeholder]="field.placeholder"
[matTooltip]="field.tooltip"
(blur)="markAsTouched()"
matTooltipPosition="above"
matTooltipShowDelay="1000">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly" ></mat-datepicker-toggle>
<input matInput [matDatepicker]="datePicker"
[id]="field.id"
[(ngModel)]="value"
[required]="field.required"
[placeholder]="field.placeholder"
[min]="minDate"
[max]="maxDate"
[disabled]="field.readOnly"
[matTooltip]="field.tooltip"
matTooltipPosition="above"
matTooltipShowDelay="1000"
(dateChange)="onDateChanged($event)"
(blur)="markAsTouched()">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly"></mat-datepicker-toggle>
<mat-datepicker #datePicker
[startAt]="startAt"
[disabled]="field.readOnly">
</mat-datepicker>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datepicker #datePicker [touchUi]="true" [startAt]="field.value | adfMomentDate: field.dateDisplayFormat" [disabled]="field.readOnly"></mat-datepicker>
<input
type="hidden"
[matDatepicker]="datePicker"
[value]="field.value | adfMomentDate: field.dateDisplayFormat"
[min]="minDate"
[max]="maxDate"
[disabled]="field.readOnly"
(dateInput)="onDateChanged($any($event).targetElement.value)">
</div>
</div>
</div>

View File

@@ -17,18 +17,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DateCloudWidgetComponent } from './date-cloud.widget';
import { FormFieldModel, FormModel, FormFieldTypes } from '@alfresco/adf-core';
import moment from 'moment';
import { FormFieldModel, FormModel, FormFieldTypes, DateFieldValidator, MinDateFieldValidator, MaxDateFieldValidator } from '@alfresco/adf-core';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { DATE_FORMAT_CLOUD } from '../../../../models/date-format-cloud.model';
import { By } from '@angular/platform-browser';
import { DateAdapter } from '@angular/material/core';
import { isEqual, subDays, addDays } from 'date-fns';
describe('DateWidgetComponent', () => {
let widget: DateCloudWidgetComponent;
let fixture: ComponentFixture<DateCloudWidgetComponent>;
let element: HTMLElement;
let adapter: DateAdapter<Date>;
let form: FormModel;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -37,7 +39,13 @@ describe('DateWidgetComponent', () => {
ProcessServiceCloudTestingModule
]
});
form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new MinDateFieldValidator(), new MaxDateFieldValidator()];
fixture = TestBed.createComponent(DateCloudWidgetComponent);
adapter = fixture.debugElement.injector.get(DateAdapter);
widget = fixture.componentInstance;
element = fixture.nativeElement;
});
@@ -52,13 +60,14 @@ describe('DateWidgetComponent', () => {
widget.ngOnInit();
const expected = moment(minValue, DATE_FORMAT_CLOUD);
expect(widget.minDate.isSame(expected)).toBeTruthy();
const expected = adapter.parse(minValue, widget.DATE_FORMAT);
expect(isEqual(widget.minDate, expected)).toBeTrue();
});
it('should date field be present', () => {
const minValue = '1982-03-13';
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
minValue
});
@@ -70,29 +79,29 @@ describe('DateWidgetComponent', () => {
it('should setup max value for date picker', () => {
const maxValue = '1982-03-13';
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
maxValue
});
widget.ngOnInit();
const expected = moment(maxValue, DATE_FORMAT_CLOUD);
expect(widget.maxDate.isSame(expected)).toBeTruthy();
const expected = adapter.parse(maxValue, widget.DATE_FORMAT);
expect(isEqual(widget.maxDate, expected)).toBeTrue();
});
it('should eval visibility on date changed', () => {
spyOn(widget, 'onFieldChanged').and.callThrough();
const field = new FormFieldModel(new FormModel(), {
const field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
id: 'date-field-id',
name: 'date-name',
value: '9999-9-9',
type: 'date',
readOnly: 'false'
});
widget.field = field;
const todayDate = moment().format(DATE_FORMAT_CLOUD);
widget.onDateChanged({ value: todayDate });
widget.onDateChanged({ value: adapter.today() } as any);
expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
});
@@ -105,48 +114,46 @@ describe('DateWidgetComponent', () => {
});
it('should show visible date widget', async () => {
widget.field = new FormFieldModel(new FormModel(), {
widget.field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
// always stored as dd-MM-yyyy
value: '9999-9-9',
type: 'date',
readOnly: 'false'
type: FormFieldTypes.DATE
});
widget.field.isVisible = true;
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement.value).toContain('9-9-9999');
expect(dateElement).not.toBeNull();
expect(dateElement?.value).toContain('9-9-9999');
});
it('should show the correct format type', async () => {
widget.field = new FormFieldModel(new FormModel(), {
widget.field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-30-12',
type: 'date',
readOnly: 'false'
// always stored as dd-MM-yyyy
value: '30-12-9999',
type: FormFieldTypes.DATE,
dateDisplayFormat: 'YYYY-DD-MM'
});
widget.field.isVisible = true;
widget.field.dateDisplayFormat = 'YYYY-DD-MM';
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement.value).toContain('9999-30-12');
});
it('should disable date button when is readonly', async () => {
widget.field = new FormFieldModel(new FormModel(), {
widget.field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-9-9',
type: 'date',
type: FormFieldTypes.DATE,
readOnly: 'false'
});
widget.field.isVisible = true;
@@ -170,7 +177,7 @@ describe('DateWidgetComponent', () => {
id: 'date-field-id',
name: 'date-name',
value: 'aa',
type: 'date',
type: FormFieldTypes.DATE,
readOnly: 'false'
});
widget.field.isVisible = true;
@@ -184,31 +191,31 @@ describe('DateWidgetComponent', () => {
});
it('should display always the json value', async () => {
const field = new FormFieldModel(new FormModel(), {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '12-30-9999',
type: 'date',
readOnly: 'false'
// always stored as dd-MM-yyyy
value: '30-12-9999',
type: FormFieldTypes.DATE,
readOnly: 'false',
dateDisplayFormat: 'MM-DD-YYYY'
});
field.isVisible = true;
field.dateDisplayFormat = 'MM-DD-YYYY';
widget.field = field;
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
const dateElement: any = element.querySelector('#date-field-id');
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).toBeDefined();
expect(dateElement.value).toContain('12-30-9999');
widget.field.value = '03-02-2020';
fixture.componentInstance.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect(dateElement.value).toContain('03-02-2020');
expect(dateElement.value).toContain('02-03-2020');
});
describe('when form model has left labels', () => {
@@ -285,7 +292,8 @@ describe('DateWidgetComponent', () => {
describe('Set dynamic dates', () => {
it('should min date equal to the today date minus minimum date range value', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
minDateRangeValue: 4
});
@@ -293,13 +301,13 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const todayDate = moment().format(DATE_FORMAT_CLOUD);
const expected = moment(todayDate).subtract(widget.field.minDateRangeValue, 'days');
expect(widget.minDate).toEqual(expected);
const expected = subDays(adapter.today(), widget.field.minDateRangeValue);
expect(widget.minDate.toDateString()).toBe(expected.toDateString());
});
it('should min date and max date be undefined if dynamic min and max date are not set', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true
});
@@ -311,7 +319,8 @@ describe('DateWidgetComponent', () => {
});
it('should max date be undefined if only minimum date range value is set', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
minDateRangeValue: 4
});
@@ -323,7 +332,8 @@ describe('DateWidgetComponent', () => {
});
it('should min date be undefined if only maximum date range value is set', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: 4
});
@@ -335,7 +345,8 @@ describe('DateWidgetComponent', () => {
});
it('should max date equal to the today date plus maximum date range value', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: 5
});
@@ -343,13 +354,13 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const todayDate = moment().format(DATE_FORMAT_CLOUD);
const expected = moment(todayDate).add(widget.field.maxDateRangeValue, 'days');
expect(widget.maxDate).toEqual(expected);
const expected = addDays(adapter.today(), widget.field.maxDateRangeValue);
expect(widget.maxDate.toDateString()).toBe(expected.toDateString());
});
it('should maxDate and minDate be undefined if minDateRangeValue and maxDateRangeValue are null', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: null,
minDateRangeValue: null
@@ -363,7 +374,8 @@ describe('DateWidgetComponent', () => {
});
it('should minDate be undefined if minDateRangeValue is null and maxDateRangeValue is greater than 0', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: 15,
minDateRangeValue: null
@@ -377,7 +389,8 @@ describe('DateWidgetComponent', () => {
});
it('should maxDate be undefined if maxDateRangeValue is null and minDateRangeValue is greater than 0', async () => {
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: null,
minDateRangeValue: 10
@@ -392,8 +405,10 @@ describe('DateWidgetComponent', () => {
describe('check date validation by dynamic date ranges', () => {
it('should minValue be equal to today date minus minDateRangeValue', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22');
widget.field = new FormFieldModel(null, {
spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: null,
minDateRangeValue: 1,
@@ -404,16 +419,16 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const expectedMinValueString = '2022-07-21';
expect(widget.field.minValue).toEqual(expectedMinValueString);
expect(widget.field.minValue).toEqual('21-07-2022');
expect(widget.maxDate).toBeUndefined();
expect(widget.field.maxValue).toBeNull();
});
it('should maxValue be equal to today date plus maxDateRangeValue', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22');
widget.field = new FormFieldModel(null, {
spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: 8,
minDateRangeValue: null,
@@ -424,16 +439,16 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const expectedMaxValueString = '2022-07-30';
expect(widget.field.maxValue).toEqual(expectedMaxValueString);
expect(widget.field.maxValue).toEqual('30-07-2022');
expect(widget.minDate).toBeUndefined();
expect(widget.field.minValue).toBeNull();
});
it('should maxValue and minValue be null if maxDateRangeValue and minDateRangeValue are null', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22');
widget.field = new FormFieldModel(null, {
spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: null,
minDateRangeValue: null,
@@ -451,8 +466,10 @@ describe('DateWidgetComponent', () => {
});
it('should maxValue and minValue not be null if maxDateRangeVale and minDateRangeValue are not null', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22');
widget.field = new FormFieldModel(null, {
spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
maxDateRangeValue: 8,
minDateRangeValue: 10,
@@ -463,11 +480,8 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const expectedMaxValueString = '2022-07-30';
const expectedMinValueString = '2022-07-12';
expect(widget.field.maxValue).toEqual(expectedMaxValueString);
expect(widget.field.minValue).toEqual(expectedMinValueString);
expect(widget.field.minValue).toEqual('12-07-2022');
expect(widget.field.maxValue).toEqual('30-07-2022');
});
});
});

View File

@@ -17,22 +17,20 @@
/* eslint-disable @angular-eslint/component-selector */
import { Component, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core';
import { Component, OnInit, ViewEncapsulation, OnDestroy, Input } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import moment, { Moment } from 'moment';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
MOMENT_DATE_FORMATS, MomentDateAdapter, WidgetComponent,
UserPreferencesService, UserPreferenceValues, FormService
} from '@alfresco/adf-core';
import { DATE_FORMAT_CLOUD } from '../../../../models/date-format-cloud.model';
import { WidgetComponent, FormService, AdfDateFnsAdapter, DateFnsUtils } from '@alfresco/adf-core';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import { CLOUD_FORM_DATE_FORMATS } from '../../../date-formats';
import { addDays, subDays } from 'date-fns';
@Component({
selector: 'date-widget',
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }],
{ provide: MAT_DATE_FORMATS, useValue: CLOUD_FORM_DATE_FORMATS },
{ provide: DateAdapter, useClass: AdfDateFnsAdapter }
],
templateUrl: './date-cloud.widget.html',
styleUrls: ['./date-cloud.widget.scss'],
host: {
@@ -50,52 +48,58 @@ import { DATE_FORMAT_CLOUD } from '../../../../models/date-format-cloud.model';
})
export class DateCloudWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
typeId = 'DateCloudWidgetComponent';
readonly DATE_FORMAT = 'dd-MM-yyyy';
minDate: Moment;
maxDate: Moment;
minDate: Date;
maxDate: Date;
startAt: Date;
/**
* Current date value.
* The value is always stored in the format `dd-MM-yyyy`,
* but displayed in the UI component using `dateDisplayFormat`
*/
@Input()
value: any = null;
private onDestroy$ = new Subject<boolean>();
constructor(public formService: FormService,
private dateAdapter: DateAdapter<Moment>,
private userPreferencesService: UserPreferencesService) {
private dateAdapter: DateAdapter<Date>) {
super(formService);
}
ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.subscribe(locale => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as MomentDateAdapter;
momentDateAdapter.overrideDisplayFormat = this.field.dateDisplayFormat;
if (this.field.dateDisplayFormat) {
const adapter = this.dateAdapter as AdfDateFnsAdapter;
adapter.displayFormat = this.field.dateDisplayFormat;
}
if (this.field) {
if (this.field.dynamicDateRangeSelection) {
const today = this.getTodaysFormattedDate();
if (Number.isInteger(this.field.minDateRangeValue)) {
this.minDate = moment(today).subtract(this.field.minDateRangeValue, 'days');
this.field.minValue = this.minDate.format(DATE_FORMAT_CLOUD);
this.minDate = subDays(this.dateAdapter.today(), this.field.minDateRangeValue);
this.field.minValue = DateFnsUtils.formatDate(this.minDate, this.DATE_FORMAT);
}
if (Number.isInteger(this.field.maxDateRangeValue)) {
this.maxDate = moment(today).add(this.field.maxDateRangeValue, 'days');
this.field.maxValue = this.maxDate.format(DATE_FORMAT_CLOUD);
this.maxDate = addDays(this.dateAdapter.today(), this.field.maxDateRangeValue);
this.field.maxValue = DateFnsUtils.formatDate(this.maxDate, this.DATE_FORMAT);
}
} else {
if (this.field.minValue) {
this.minDate = moment(this.field.minValue, DATE_FORMAT_CLOUD);
this.minDate = this.dateAdapter.parse(this.field.minValue, this.DATE_FORMAT);
}
if (this.field.maxValue) {
this.maxDate = moment(this.field.maxValue, DATE_FORMAT_CLOUD);
this.maxDate = this.dateAdapter.parse(this.field.maxValue, this.DATE_FORMAT);
}
}
}
}
getTodaysFormattedDate() {
return moment().format(DATE_FORMAT_CLOUD);
if (this.field.value) {
this.startAt = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
this.value = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
}
}
}
ngOnDestroy() {
@@ -103,13 +107,16 @@ export class DateCloudWidgetComponent extends WidgetComponent implements OnInit,
this.onDestroy$.complete();
}
onDateChanged(newDateValue) {
const date = moment(newDateValue, this.field.dateDisplayFormat, true);
if (date.isValid()) {
this.field.value = date.format(this.field.dateDisplayFormat);
onDateChanged(event: MatDatepickerInputEvent<Date>) {
const value = event.value;
const input = event.targetElement as HTMLInputElement;
if (value) {
this.field.value = this.dateAdapter.format(value, this.DATE_FORMAT);
} else {
this.field.value = newDateValue;
this.field.value = input.value;
}
this.onFieldChanged(this.field);
}
}

View File

@@ -0,0 +1,38 @@
/*!
* @license
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { MatDateFormats } from '@angular/material/core';
/**
* Provides date/time display formatting for the cloud components.
*
* Notes for developers: display formats are different from the storage formats.
* Components have a fixed format for saving dates and datetime values,
* while dynamic format for UI display.
*/
export const CLOUD_FORM_DATE_FORMATS: MatDateFormats = {
parse: {
dateInput: 'yyyy-MM-dd'
},
display: {
dateInput: 'yyyy-MM-dd',
monthLabel: 'LLL',
monthYearLabel: 'LLL uuuu',
dateA11yLabel: 'PP',
monthYearA11yLabel: 'LLLL uuuu'
}
};

View File

@@ -40,3 +40,4 @@ export * from './services/content-cloud-node-selector.service';
export * from './services/process-cloud-content.service';
export * from './form-cloud.module';
export * from './date-formats';