[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

@@ -5,11 +5,11 @@
id="dateInput"
type="text"
[matDatepicker]="datePicker"
[value]="value"
[(ngModel)]="value"
[id]="column.id"
[required]="column.required"
[disabled]="!column.editable"
(focusout)="onDateChanged($any($event).srcElement)"
(focusout)="onDateChanged($any($event).target.value)"
(dateChange)="onDateChanged($event)">
<mat-datepicker-toggle *ngIf="column.editable" matSuffix [for]="datePicker" class="adf-date-editor-button" ></mat-datepicker-toggle>
</mat-form-field>

View File

@@ -22,7 +22,6 @@ import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { DateEditorComponent } from './date.editor';
import { By } from '@angular/platform-browser';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import { TranslateModule } from '@ngx-translate/core';
describe('DateEditorComponent', () => {
@@ -55,7 +54,7 @@ describe('DateEditorComponent', () => {
describe('using Date Piker', () => {
it('should update row value on change', () => {
const input = {value: '14-03-2016'} as MatDatepickerInputEvent<any>;
const input = {value: '2016-03-14'} as any;
component.ngOnInit();
component.onDateChanged(input);
@@ -66,7 +65,7 @@ describe('DateEditorComponent', () => {
it('should flush value on user input', () => {
spyOn(table, 'flushValue').and.callThrough();
const input = {value: '14-03-2016'} as MatDatepickerInputEvent<any>;
const input = { value: '2016-03-14' } as any;
component.ngOnInit();
component.onDateChanged(input);

View File

@@ -15,32 +15,29 @@
* limitations under the License.
*/
/* eslint-disable @angular-eslint/component-selector */
import { UserPreferencesService, UserPreferenceValues, MomentDateAdapter, MOMENT_DATE_FORMATS } from '@alfresco/adf-core';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { ADF_DATE_FORMATS, AdfDateFnsAdapter, DateFnsUtils } from '@alfresco/adf-core';
import { Component, Input, OnInit } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import moment, { Moment } from 'moment';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { isValid } from 'date-fns';
@Component({
selector: 'adf-date-editor',
templateUrl: './date.editor.html',
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }
{ provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: DateAdapter, useClass: AdfDateFnsAdapter }
],
styleUrls: ['./date.editor.scss']
})
export class DateEditorComponent implements OnInit, OnDestroy {
export class DateEditorComponent implements OnInit {
DATE_FORMAT: string = 'DD-MM-YYYY';
value: any;
@Input()
value: Date;
@Input()
table: DynamicTableModel;
@@ -51,43 +48,32 @@ export class DateEditorComponent implements OnInit, OnDestroy {
@Input()
column: DynamicTableColumn;
minDate: Moment;
maxDate: Moment;
minDate: Date;
maxDate: Date;
private onDestroy$ = new Subject<boolean>();
constructor(private dateAdapter: DateAdapter<Moment>, private userPreferencesService: UserPreferencesService) {}
constructor(private dateAdapter: DateAdapter<Date>) {}
ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.subscribe((locale) => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as AdfDateFnsAdapter;
momentDateAdapter.displayFormat = this.DATE_FORMAT;
const momentDateAdapter = this.dateAdapter as MomentDateAdapter;
momentDateAdapter.overrideDisplayFormat = this.DATE_FORMAT;
this.value = moment(this.table.getCellValue(this.row, this.column), this.DATE_FORMAT);
this.value = this.table.getCellValue(this.row, this.column) as Date;
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
onDateChanged(newDateValue: MatDatepickerInputEvent<Date> | string) {
if (typeof newDateValue === 'string') {
const newValue = DateFnsUtils.parseDate(newDateValue, this.DATE_FORMAT);
onDateChanged(newDateValue: MatDatepickerInputEvent<any> | HTMLInputElement) {
if (newDateValue?.value) {
/* validates the user inputs */
const momentDate = moment(newDateValue.value, this.DATE_FORMAT, true);
if (!momentDate.isValid()) {
this.row.value[this.column.id] = newDateValue.value;
} else {
this.row.value[this.column.id] = `${momentDate.format('YYYY-MM-DD')}T00:00:00.000Z`;
if (isValid(newValue)) {
this.row.value[this.column.id] = `${DateFnsUtils.formatDate(newValue, 'yyyy-MM-dd')}T00:00:00.000Z`;
this.table.flushValue();
} else {
this.row.value[this.column.id] = newDateValue;
}
} else {
/* removes the date */
} else if (newDateValue?.value) {
this.row.value[this.column.id] = `${DateFnsUtils.formatDate(newDateValue?.value, 'yyyy-MM-dd')}T00:00:00.000Z`;
this.table.flushValue();
} else {
this.row.value[this.column.id] = '';
}
}

View File

@@ -7,7 +7,7 @@
[id]="column.id"
[required]="column.required"
[disabled]="!column.editable"
(focusout)="onDateChanged($any($event).srcElement.value)"
(focusout)="onDateChanged($any($event).target.value)"
(dateChange)="onDateChanged($event)">
<mat-datetimepicker-toggle
matSuffix

View File

@@ -16,7 +16,6 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import moment from 'moment';
import { FormFieldModel, FormModel, CoreTestingModule } from '@alfresco/adf-core';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
@@ -52,26 +51,27 @@ describe('DateTimeEditorComponent', () => {
component.column = column;
});
it('should update fow value on change', () => {
it('should update row value on change', () => {
component.ngOnInit();
const newDate = moment('22-6-2018 04:20 AM', 'D-M-YYYY hh:mm A');
component.onDateChanged(newDate);
expect(moment(row.value[column.id]).isSame(newDate)).toBeTruthy();
const newDate = new Date('2018-6-22 04:20 AM');
component.onDateChanged({ value: newDate } as any);
expect(row.value[column.id]).toBe('22/06/2018 04:20');
});
it('should update row value upon user input', () => {
const input = '22-6-2018 04:20 AM';
const input = '22/6/2018 04:20';
component.ngOnInit();
component.onDateChanged(input);
const actual = row.value[column.id];
expect(actual).toBe('22-6-2018 04:20 AM');
expect(actual).toBe('2018-06-22T04:20:00.000Z');
});
it('should flush value on user input', () => {
spyOn(table, 'flushValue').and.callThrough();
const input = '22-6-2018 04:20 AM';
const input = '22/6/2018 04:20';
component.ngOnInit();
component.onDateChanged(input);

View File

@@ -15,35 +15,31 @@
* limitations under the License.
*/
/* eslint-disable @angular-eslint/component-selector */
import { MOMENT_DATE_FORMATS, MomentDateAdapter, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { ADF_DATETIME_FORMATS, ADF_DATE_FORMATS, AdfDateFnsAdapter, AdfDateTimeFnsAdapter, /*MOMENT_DATE_FORMATS, MomentDateAdapter*/
DateFnsUtils} from '@alfresco/adf-core';
import { Component, Input, OnInit } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import moment, { Moment } from 'moment';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { DatetimeAdapter, MAT_DATETIME_FORMATS } from '@mat-datetimepicker/core';
import { MomentDatetimeAdapter, MAT_MOMENT_DATETIME_FORMATS } from '@mat-datetimepicker/moment';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
@Component({
selector: 'adf-datetime-editor',
templateUrl: './datetime.editor.html',
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS },
{ provide: DatetimeAdapter, useClass: MomentDatetimeAdapter },
{ provide: MAT_DATETIME_FORMATS, useValue: MAT_MOMENT_DATETIME_FORMATS }
{ provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: MAT_DATETIME_FORMATS, useValue: ADF_DATETIME_FORMATS },
{ provide: DateAdapter, useClass: AdfDateFnsAdapter },
{ provide: DatetimeAdapter, useClass: AdfDateTimeFnsAdapter }
],
styleUrls: ['./datetime.editor.scss']
})
export class DateTimeEditorComponent implements OnInit, OnDestroy {
export class DateTimeEditorComponent implements OnInit {
DATE_TIME_FORMAT: string = 'DD/MM/YYYY HH:mm';
value: any;
@Input()
value: Date;
@Input()
table: DynamicTableModel;
@@ -54,40 +50,28 @@ export class DateTimeEditorComponent implements OnInit, OnDestroy {
@Input()
column: DynamicTableColumn;
minDate: Moment;
maxDate: Moment;
minDate: Date;
maxDate: Date;
private onDestroy$ = new Subject<boolean>();
constructor(private dateAdapter: DateAdapter<Moment>, private userPreferencesService: UserPreferencesService) {}
constructor(private dateAdapter: DateAdapter<Date>) {}
ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.subscribe((locale) => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as AdfDateFnsAdapter;
momentDateAdapter.displayFormat = this.DATE_TIME_FORMAT;
const momentDateAdapter = this.dateAdapter as MomentDateAdapter;
momentDateAdapter.overrideDisplayFormat = this.DATE_TIME_FORMAT;
this.value = moment(this.table.getCellValue(this.row, this.column), this.DATE_TIME_FORMAT);
this.value = this.table.getCellValue(this.row, this.column) as Date;
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
onDateChanged(newDateValue) {
if (newDateValue?.value) {
const newValue = moment(newDateValue.value, this.DATE_TIME_FORMAT);
this.row.value[this.column.id] = newDateValue.value.format(this.DATE_TIME_FORMAT);
onDateChanged(newDateValue: MatDatetimepickerInputEvent<Date> | string) {
if (typeof newDateValue === 'string') {
const newValue = DateFnsUtils.parseDate(newDateValue, this.DATE_TIME_FORMAT);
this.value = newValue;
this.row.value[this.column.id] = newValue.toISOString();
this.table.flushValue();
} else if (newDateValue) {
const newValue = moment(newDateValue, this.DATE_TIME_FORMAT);
this.value = newValue;
this.row.value[this.column.id] = newDateValue;
} else if (newDateValue.value) {
const newValue = DateFnsUtils.formatDate(newDateValue.value, this.DATE_TIME_FORMAT);
this.row.value[this.column.id] = newValue;
this.value = newDateValue.value;
this.table.flushValue();
} else {
this.row.value[this.column.id] = '';

View File

@@ -0,0 +1,94 @@
/*!
* @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 { DateCellValidator } from './date-cell-validator-model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
describe('DateCellValidator', () => {
let validator: DateCellValidator;
beforeEach(() => {
validator = new DateCellValidator();
});
it('should require column to validate', () => {
expect(validator.isSupported(null)).toBeFalse();
});
it('should support only editable columns', () => {
const readonly = { editable: false, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
expect(validator.isSupported(readonly)).toBeFalse();
const editable = { editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
expect(validator.isSupported(editable)).toBeTrue();
});
it('should support only date column type', () => {
const date = { editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
expect(validator.isSupported(date)).toBeTrue();
const unsupported = { editable: true, type: 'unknown' } as DynamicTableColumn;
expect(validator.isSupported(unsupported)).toBeFalse();
});
it('should skip validating unsupported columns', () => {
const column = { editable: true, type: 'unknown' } as DynamicTableColumn;
const row = {} as DynamicTableRow;
expect(validator.validate(row, column)).toBeTrue();
});
it('should reject when required column has no value', () => {
const column = { id: 'col1', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: null } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeFalse();
});
it('should approve when optional column has no value', () => {
const column = { id: 'col1', required: false, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: null } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeTrue();
});
it('should approve the valid datetime value', () => {
const column = { id: 'col1', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: '2023-10-12T10:59:24.773Z' } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeTrue();
});
it('should reject invalid datetime value', () => {
const column = { id: 'col1', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: '!2023-10-12T10:59:24.773Z' } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeFalse();
});
it('should update validation summary of rejection', () => {
const column = { id: 'col1', name: 'created_on', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: '!2023-10-12T10:59:24.773Z' } } as DynamicTableRow;
const summary = new DynamicRowValidationSummary();
expect(validator.validate(row, column, summary)).toBeFalse();
expect(summary.isValid).toBeFalse();
expect(summary.message).toBe(`Invalid 'created_on' format.`);
});
});

View File

@@ -17,34 +17,40 @@
/* eslint-disable @angular-eslint/component-selector */
import moment from 'moment';
import { CellValidator } from './cell-validator.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { isValid } from 'date-fns';
export class DateCellValidator implements CellValidator {
private supportedTypes: string[] = ['Date'];
static DATE_TYPE = 'Date';
private supportedTypes: string[] = [DateCellValidator.DATE_TYPE];
isSupported(column: DynamicTableColumn): boolean {
return column?.editable && this.supportedTypes.indexOf(column.type) > -1;
return !!(column?.editable && this.supportedTypes.indexOf(column?.type) > -1);
}
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) {
const value = row.value[column.id];
const value = row?.value[column.id];
if (!value && !column.required) {
return true;
}
if (value) {
const dateValue = new Date(value);
if (isValid(dateValue)) {
return true;
}
const dateValue = moment(value, 'YYYY-MM-DDTHH:mm:ss.SSSSZ', true);
if (!dateValue.isValid()) {
if (summary) {
summary.isValid = false;
summary.message = `Invalid '${column.name}' format.`;
}
return false;
} else {
return !column.required;
}
}

View File

@@ -17,14 +17,12 @@
import { ErrorMessageModel } from '@alfresco/adf-core';
/* eslint-disable @angular-eslint/component-selector */
export class DynamicRowValidationSummary extends ErrorMessageModel {
isValid: boolean;
constructor(json?: any) {
super(json);
this.isValid = json.isValid;
this.isValid = json?.isValid;
}
}

View File

@@ -15,9 +15,6 @@
* limitations under the License.
*/
/* eslint-disable @angular-eslint/component-selector */
import moment from 'moment';
import { ValidateDynamicTableRowEvent } from '../../../../events/validate-dynamic-table-row.event';
import { FormService, FormFieldModel, FormWidgetModel } from '@alfresco/adf-core';
import { CellValidator } from './cell-validator.model';
@@ -178,7 +175,7 @@ export class DynamicTableModel extends FormWidgetModel {
if (column.type === 'Date') {
if (rowValue) {
return moment(rowValue.split('T')[0], 'YYYY-MM-DD').format('DD-MM-YYYY');
return new Date(rowValue.split('T')[0]);
}
}