Skip to content

Commit

Permalink
AAE-23666 Make Datetime timezone aware (#9942)
Browse files Browse the repository at this point in the history
* AAE-23520 Fix date parse with invalid display format

* AAE-23640 Fix dates

* Fix sonar

* Fix tests
  • Loading branch information
pmartinezga committed Jul 24, 2024
1 parent c070a5e commit 0b31016
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 53 deletions.
33 changes: 33 additions & 0 deletions lib/core/src/lib/common/utils/date-fns-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
14 changes: 14 additions & 0 deletions lib/core/src/lib/common/utils/date-fns-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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`;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand All @@ -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);
}

Expand All @@ -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;
}
Expand Down

0 comments on commit 0b31016

Please sign in to comment.