From 0b3101698a7c1f6f015dbee64ad92ba7a83531ca Mon Sep 17 00:00:00 2001 From: Pablo Martinez Date: Wed, 24 Jul 2024 12:05:37 +0200 Subject: [PATCH] AAE-23666 Make Datetime timezone aware (#9942) * AAE-23520 Fix date parse with invalid display format * AAE-23640 Fix dates * Fix sonar * Fix tests --- .../lib/common/utils/date-fns-utils.spec.ts | 33 ++++++++ .../src/lib/common/utils/date-fns-utils.ts | 14 ++++ .../widgets/core/error-message.model.ts | 4 +- .../widgets/core/form-field-validator.ts | 84 +++++++++---------- .../widgets/core/form-field.model.ts | 11 ++- .../widgets/date-time/date-time.widget.ts | 11 +-- 6 files changed, 104 insertions(+), 53 deletions(-) diff --git a/lib/core/src/lib/common/utils/date-fns-utils.spec.ts b/lib/core/src/lib/common/utils/date-fns-utils.spec.ts index 519bc85b9c6..a2afe7bcd93 100644 --- a/lib/core/src/lib/common/utils/date-fns-utils.spec.ts +++ b/lib/core/src/lib/common/utils/date-fns-utils.spec.ts @@ -138,4 +138,37 @@ describe('DateFnsUtils', () => { expect(forceLocalDateJapan.getMonth()).toBe(0); expect(forceLocalDateJapan.getFullYear()).toBe(2020); }); + + it('should detect if a formatted string contains a timezone', () => { + let result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10'); + expect(result).toEqual(false); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00'); + expect(result).toEqual(false); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00Z'); + expect(result).toEqual(true); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00+00:00'); + expect(result).toEqual(true); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00-00:00'); + expect(result).toEqual(true); + }); + + it('should get the date from number', () => { + const spyUtcToLocal = spyOn(DateFnsUtils, 'utcToLocal').and.callThrough(); + + const date = DateFnsUtils.getDate(1623232200000); + expect(date.toISOString()).toBe('2021-06-09T09:50:00.000Z'); + expect(spyUtcToLocal).not.toHaveBeenCalled(); + }); + + it('should get transformed date when string date does not contain the timezone', () => { + const spyUtcToLocal = spyOn(DateFnsUtils, 'utcToLocal').and.callThrough(); + + DateFnsUtils.getDate('2021-06-09T14:10:00'); + + expect(spyUtcToLocal).toHaveBeenCalled(); + }); }); diff --git a/lib/core/src/lib/common/utils/date-fns-utils.ts b/lib/core/src/lib/common/utils/date-fns-utils.ts index 12af9a5e66e..7f8daf4f4cb 100644 --- a/lib/core/src/lib/common/utils/date-fns-utils.ts +++ b/lib/core/src/lib/common/utils/date-fns-utils.ts @@ -225,4 +225,18 @@ export class DateFnsUtils { const utcDate = `${date.getFullYear()}-${panDate(date.getMonth() + 1)}-${panDate(date.getDate())}T00:00:00.000Z`; return new Date(utcDate); } + + static stringDateContainsTimeZone(value: string): boolean { + return /(Z|([+|-]\d\d:?\d\d))$/.test(value); + } + + static getDate(value: string | number | Date): Date { + let date = new Date(value); + + if (typeof value === 'string' && !DateFnsUtils.stringDateContainsTimeZone(value)) { + date = this.utcToLocal(date); + } + + return date; + } } diff --git a/lib/core/src/lib/form/components/widgets/core/error-message.model.ts b/lib/core/src/lib/form/components/widgets/core/error-message.model.ts index e1987c185fb..ecba680bc87 100644 --- a/lib/core/src/lib/form/components/widgets/core/error-message.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/error-message.model.ts @@ -33,11 +33,9 @@ export class ErrorMessageModel { getAttributesAsJsonObj() { let result = {}; if (this.attributes.size > 0) { - const obj = Object.create(null); this.attributes.forEach((value, key) => { - obj[key] = value; + result[key] = typeof value === 'string' ? value : JSON.stringify(value); }); - result = JSON.stringify(obj); } return result; } diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts index ce097b7dd84..79f0f63de75 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts @@ -159,7 +159,7 @@ export class DateTimeFieldValidator implements FormFieldValidator { } static isValidDateTime(input: string): boolean { - const date = new Date(input); + const date = DateFnsUtils.getDate(input); return isDateValid(date); } @@ -245,19 +245,11 @@ export class MaxDateFieldValidator extends BoundaryDateFieldValidator { } } -/** - * Validates the min constraint for the datetime value. - * - * Notes for developers: - * the format of the min/max values is always the ISO datetime: i.e. 2023-10-01T15:21:00.000Z. - * Min/Max values can be parsed with standard `new Date(value)` calls. - * - */ -export class MinDateTimeFieldValidator implements FormFieldValidator { +export abstract class BoundaryDateTimeFieldValidator implements FormFieldValidator { private supportedTypes = [FormFieldTypes.DATETIME]; isSupported(field: FormFieldModel): boolean { - return field && this.supportedTypes.indexOf(field.type) > -1 && !!field.minValue; + return field && this.supportedTypes.indexOf(field.type) > -1 && !!field[this.getSubjectField()]; } validate(field: FormFieldModel): boolean { @@ -275,57 +267,65 @@ export class MinDateTimeFieldValidator implements FormFieldValidator { private checkDateTime(field: FormFieldModel): boolean { let isValid = true; - const fieldValueDate = new Date(field.value); - const min = new Date(field.minValue); + const fieldValueDate = DateFnsUtils.getDate(field.value); + const subjectFieldDate = DateFnsUtils.getDate(field[this.getSubjectField()]); - if (isBefore(fieldValueDate, min)) { - field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`; - field.validationSummary.attributes.set('minValue', DateFnsUtils.formatDate(min, field.dateDisplayFormat).replace(':', '-')); + if (this.compareDates(fieldValueDate, subjectFieldDate)) { + field.validationSummary.message = this.getErrorMessage(); + field.validationSummary.attributes.set(this.getSubjectField(), DateFnsUtils.formatDate(subjectFieldDate, field.dateDisplayFormat)); isValid = false; } return isValid; } + + protected abstract compareDates(fieldValueDate: Date, subjectFieldDate: Date): boolean; + + protected abstract getSubjectField(): string; + + protected abstract getErrorMessage(): string; } /** - * Validates the max constraint for the datetime value. + * Validates the min constraint for the datetime value. * * Notes for developers: * the format of the min/max values is always the ISO datetime: i.e. 2023-10-01T15:21:00.000Z. * Min/Max values can be parsed with standard `new Date(value)` calls. * */ -export class MaxDateTimeFieldValidator implements FormFieldValidator { - private supportedTypes = [FormFieldTypes.DATETIME]; +export class MinDateTimeFieldValidator extends BoundaryDateTimeFieldValidator { + protected compareDates(fieldValueDate: Date, subjectFieldDate: Date): boolean { + return isBefore(fieldValueDate, subjectFieldDate); + } - isSupported(field: FormFieldModel): boolean { - return field && this.supportedTypes.indexOf(field.type) > -1 && !!field.maxValue; + protected getSubjectField(): string { + return 'minValue'; } - validate(field: FormFieldModel): boolean { - let isValid = true; - if (this.isSupported(field) && field.value && field.isVisible) { - if (!DateTimeFieldValidator.isValidDateTime(field.value)) { - field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE'; - isValid = false; - } else { - isValid = this.checkDateTime(field); - } - } - return isValid; + protected getErrorMessage(): string { + return `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`; } +} - private checkDateTime(field: FormFieldModel): boolean { - let isValid = true; - const fieldValueDate = new Date(field.value); - const max = new Date(field.maxValue); +/** + * Validates the max constraint for the datetime value. + * + * Notes for developers: + * the format of the min/max values is always the ISO datetime: i.e. 2023-10-01T15:21:00.000Z. + * Min/Max values can be parsed with standard `new Date(value)` calls. + * + */ +export class MaxDateTimeFieldValidator extends BoundaryDateTimeFieldValidator { + protected compareDates(fieldValueDate: Date, subjectFieldDate: Date): boolean { + return isAfter(fieldValueDate, subjectFieldDate); + } - if (isAfter(fieldValueDate, max)) { - field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`; - field.validationSummary.attributes.set('maxValue', DateFnsUtils.formatDate(max, field.dateDisplayFormat).replace(':', '-')); - isValid = false; - } - return isValid; + protected getSubjectField(): string { + return 'maxValue'; + } + + protected getErrorMessage(): string { + return `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`; } } diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index aae980098a7..b0ad4a1f639 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -299,7 +299,7 @@ export class FormFieldModel extends FormWidgetModel { } parseValue(json: any): any { - let value = Object.prototype.hasOwnProperty.call(json, 'value') && json.value !== undefined ? json.value : null; + const value = Object.prototype.hasOwnProperty.call(json, 'value') && json.value !== undefined ? json.value : null; /* This is needed due to Activiti issue related to reading dropdown values as value string @@ -440,7 +440,12 @@ export class FormFieldModel extends FormWidgetModel { this.value = new Date(); } - const dateValue = DateFnsUtils.parseDate(this.value, this.dateDisplayFormat); + let dateValue; + try { + dateValue = DateFnsUtils.parseDate(this.value, this.dateDisplayFormat); + } catch (e) { + dateValue = new Date('error'); + } if (isValidDate(dateValue)) { const datePart = DateFnsUtils.formatDate(dateValue, 'yyyy-MM-dd'); @@ -456,7 +461,7 @@ export class FormFieldModel extends FormWidgetModel { this.value = new Date(); } - const dateTimeValue = this.value !== null ? new Date(this.value) : null; + const dateTimeValue = this.value !== null ? DateFnsUtils.getDate(this.value) : null; if (isValidDate(dateTimeValue)) { this.form.values[this.id] = dateTimeValue.toISOString(); diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts index cdf9d0ca19f..8954d265b1b 100644 --- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts +++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts @@ -67,15 +67,15 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { if (this.field) { if (this.field.minValue) { - this.minDate = DateFnsUtils.localToUtc(new Date(this.field.minValue)); + this.minDate = DateFnsUtils.getDate(this.field.minValue); } if (this.field.maxValue) { - this.maxDate = DateFnsUtils.localToUtc(new Date(this.field.maxValue)); + this.maxDate = DateFnsUtils.getDate(this.field.maxValue); } if (this.field.value) { - this.value = DateFnsUtils.localToUtc(new Date(this.field.value)); + this.value = DateFnsUtils.getDate(this.field.value); } } } @@ -85,11 +85,12 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { const newValue = this.dateTimeAdapter.parse(input.value, this.field.dateDisplayFormat); if (isValid(newValue)) { - this.field.value = DateFnsUtils.utcToLocal(newValue).toISOString(); + this.field.value = newValue.toISOString(); } else { this.field.value = input.value; } + this.value = DateFnsUtils.getDate(this.field.value); this.onFieldChanged(this.field); } @@ -98,7 +99,7 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { const input = event.targetElement as HTMLInputElement; if (newValue && isValid(newValue)) { - this.field.value = DateFnsUtils.utcToLocal(newValue).toISOString(); + this.field.value = newValue.toISOString(); } else { this.field.value = input.value; }