From fea1bf4cdc598738b4b85d94f3cb7df7a6ebeaed Mon Sep 17 00:00:00 2001 From: Bertrand Zuchuat Date: Thu, 10 Oct 2024 11:54:44 +0200 Subject: [PATCH] refactor: remove moment, add luxon * Refactors the date validator. Co-Authored-by: Bertrand Zuchuat --- package-lock.json | 18 +-- package.json | 4 +- .../src/lib/record/editor/extensions.ts | 68 ++---------- .../lib/translate/translate-service.spec.ts | 8 +- .../src/lib/translate/translate-service.ts | 6 +- .../src/lib/validator/time.validator.spec.ts | 104 ++++++++++++++++++ .../src/lib/validator/time.validator.ts | 22 ++-- .../src/lib/validator/validators.spec.ts | 76 +++++++++++++ .../ng-core/src/lib/validator/validators.ts | 46 ++++++++ projects/rero/ng-core/src/public-api.ts | 1 + 10 files changed, 265 insertions(+), 88 deletions(-) create mode 100644 projects/rero/ng-core/src/lib/validator/time.validator.spec.ts create mode 100644 projects/rero/ng-core/src/lib/validator/validators.spec.ts create mode 100644 projects/rero/ng-core/src/lib/validator/validators.ts diff --git a/package-lock.json b/package-lock.json index 3f2b9e07..e7d7f037 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,8 +29,8 @@ "font-awesome": "^4.7.0", "js-generate-password": "^0.1.0", "lodash-es": "^4.17.21", + "luxon": "^3.5.0", "marked": "^10.0.0", - "moment": "^2.30.1", "ngx-spinner": "^16.0.2", "primeflex": "^3.3.1", "primeicons": "^7.0.0", @@ -10858,6 +10858,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", @@ -11304,14 +11312,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/package.json b/package.json index 7fe59051..f005ffa7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@angular/platform-browser": "^17.1.0", "@angular/platform-browser-dynamic": "^17.1.0", "@angular/router": "^17.1.0", - "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@ngx-formly/core": "^6.3.6", "@ngx-formly/primeng": "^6.3.6", "@ngx-translate/core": "^15.0.0", @@ -35,8 +34,8 @@ "font-awesome": "^4.7.0", "js-generate-password": "^0.1.0", "lodash-es": "^4.17.21", + "luxon": "^3.5.0", "marked": "^10.0.0", - "moment": "^2.30.1", "ngx-spinner": "^16.0.2", "primeflex": "^3.3.1", "primeicons": "^7.0.0", @@ -56,6 +55,7 @@ "@angular/cli": "^17.1.0", "@angular/compiler-cli": "^17.1.0", "@angular/language-service": "^17.1.0", + "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@ngx-formly/schematics": "^6.3.6", "@types/jasmine": "^5.1.4", "@typescript-eslint/eslint-plugin": "^6.19.1", diff --git a/projects/rero/ng-core/src/lib/record/editor/extensions.ts b/projects/rero/ng-core/src/lib/record/editor/extensions.ts index 0356a24b..3b913dde 100644 --- a/projects/rero/ng-core/src/lib/record/editor/extensions.ts +++ b/projects/rero/ng-core/src/lib/record/editor/extensions.ts @@ -19,11 +19,11 @@ import { UntypedFormControl } from '@angular/forms'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { FormlyExtension, FormlyFieldConfig } from '@ngx-formly/core'; import { TranslateService } from '@ngx-translate/core'; -import moment from 'moment'; import { isObservable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { RecordService } from '../record.service'; import { isEmpty, removeEmptyValues } from './utils'; +import { Validators } from '../../validator/validators'; export class NgCoreFormlyExtension { @@ -326,68 +326,16 @@ export class NgCoreFormlyExtension { }; } // The start date must be less than the end date. - if (customValidators.dateMustBeLessThan) { - const startDate: string = customValidators.dateMustBeLessThan.startDate; - const endDate: string = customValidators.dateMustBeLessThan.endDate; - const strict: boolean = customValidators.dateMustBeLessThan.strict || false; - const updateOn: 'change' | 'blur' | 'submit' = customValidators.dateMustBeLessThan.strict || 'blur'; + if (customValidators.dateGreaterThan) { + const dateFirst: string = customValidators.dateGreaterThan.dateFirst; + const dateLast: string = customValidators.dateGreaterThan.dateLast; + const strict: boolean = customValidators.dateGreaterThan.strict || false; + const updateOn: 'change' | 'blur' | 'submit' = customValidators.dateGreaterThan.strict || 'blur'; field.validators = { - dateMustBeLessThan: { + dateGreaterThan: { updateOn, expression: (control: UntypedFormControl) => { - const startDateFc = control.parent.get(startDate); - const endDateFc = control.parent.get(endDate); - if (startDateFc.value !== null && endDateFc.value !== null) { - const dateStart = moment(startDateFc.value, 'YYYY-MM-DD'); - const dateEnd = moment(endDateFc.value, 'YYYY-MM-DD'); - const isMustLessThan = strict - ? dateStart >= dateEnd - ? false - : true - : dateStart > dateEnd - ? false - : true; - if (isMustLessThan) { - endDateFc.setErrors(null); - endDateFc.markAsDirty(); - } - return isMustLessThan; - } - return false; - }, - }, - }; - } - - // The end date must be greater than the start date. - if (customValidators.dateMustBeGreaterThan) { - const startDate: string = customValidators.dateMustBeGreaterThan.startDate; - const endDate: string = customValidators.dateMustBeGreaterThan.endDate; - const strict: boolean = customValidators.dateMustBeGreaterThan.strict || false; - const updateOn: 'change' | 'blur' | 'submit' = customValidators.dateMustBeGreaterThan.strict || 'blur'; - field.validators = { - datesMustBeGreaterThan: { - updateOn, - expression: (control: UntypedFormControl) => { - const startDateFc = control.parent.get(startDate); - const endDateFc = control.parent.get(endDate); - if (startDateFc.value !== null && endDateFc.value !== null) { - const dateStart = moment(startDateFc.value, 'YYYY-MM-DD'); - const dateEnd = moment(endDateFc.value, 'YYYY-MM-DD'); - const isMustBeGreaterThan = strict - ? dateStart <= dateEnd - ? true - : false - : dateStart < dateEnd - ? true - : false; - if (isMustBeGreaterThan) { - startDateFc.setErrors(null); - startDateFc.markAsDirty(); - } - return isMustBeGreaterThan; - } - return false; + return Validators.dateGreaterThan(dateFirst, dateLast, strict)(control) }, }, }; diff --git a/projects/rero/ng-core/src/lib/translate/translate-service.spec.ts b/projects/rero/ng-core/src/lib/translate/translate-service.spec.ts index 5eab7f71..f2fdfa77 100644 --- a/projects/rero/ng-core/src/lib/translate/translate-service.spec.ts +++ b/projects/rero/ng-core/src/lib/translate/translate-service.spec.ts @@ -15,10 +15,10 @@ * along with this program. If not, see . */ import { TestBed } from "@angular/core/testing"; -import { NgCoreTranslateService } from "./translate-service"; -import { PrimeNGConfig } from "primeng/api"; import { TranslateModule } from "@ngx-translate/core"; -import moment from "moment"; +import { DateTime } from "luxon"; +import { PrimeNGConfig } from "primeng/api"; +import { NgCoreTranslateService } from "./translate-service"; describe('NgCoreTranslateService', () => { let service: NgCoreTranslateService; @@ -46,6 +46,6 @@ describe('NgCoreTranslateService', () => { it('should have changed the local service', () => { service.use('fr'); expect(primeConfig.translation.today).toEqual("Aujourd'hui"); - expect(moment().locale()).toEqual('fr'); + expect(DateTime.locale).toEqual('fr'); }); }); diff --git a/projects/rero/ng-core/src/lib/translate/translate-service.ts b/projects/rero/ng-core/src/lib/translate/translate-service.ts index 77221202..853c26df 100644 --- a/projects/rero/ng-core/src/lib/translate/translate-service.ts +++ b/projects/rero/ng-core/src/lib/translate/translate-service.ts @@ -14,13 +14,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import localeEn from '@angular/common/locales/en-GB'; import localeFr from '@angular/common/locales/fr'; import localeIt from '@angular/common/locales/it'; import { inject, Injectable } from "@angular/core"; import { TranslateService } from "@ngx-translate/core"; -import moment from "moment"; +import { DateTime } from "luxon"; import de from 'primelocale/de.json'; import en from 'primelocale/en.json'; import fr from 'primelocale/fr.json'; @@ -28,7 +29,6 @@ import it from 'primelocale/it.json'; import { PrimeNGConfig } from "primeng/api"; import { Observable } from "rxjs"; import { CoreConfigService } from "../core-config.service"; -import { registerLocaleData } from '@angular/common'; @Injectable({ providedIn: 'root' @@ -62,7 +62,7 @@ export class NgCoreTranslateService extends TranslateService { } use(lang: string): Observable { - moment.locale(lang); + DateTime.locale = lang; this.primengConfig.setTranslation(this.locales[lang].primeng[lang]); return super.use(lang); diff --git a/projects/rero/ng-core/src/lib/validator/time.validator.spec.ts b/projects/rero/ng-core/src/lib/validator/time.validator.spec.ts new file mode 100644 index 00000000..81f230d2 --- /dev/null +++ b/projects/rero/ng-core/src/lib/validator/time.validator.spec.ts @@ -0,0 +1,104 @@ +/* + * RERO angular core + * Copyright (C) 2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { TestBed } from '@angular/core/testing'; +import { TimeValidator } from './time.validator'; +import { FormBuilder, FormControl, FormsModule } from '@angular/forms'; + +describe('TimeValidator', () => { + let formBuilder: FormBuilder; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule + ], + providers: [ + FormBuilder + ] + }); + formBuilder = TestBed.inject(FormBuilder); + }); + + it('should return the value of the validator greaterThanValidator', () => { + let formGroup = formBuilder.group({ + start_time: new FormControl('07:00'), + end_time: new FormControl('08:00') + }); + let result = TimeValidator.greaterThanValidator('start_time', 'end_time')(formGroup); + expect(result).toBeNull(); + + formGroup = formBuilder.group({ + start_time: new FormControl('12:00'), + end_time: new FormControl('08:00') + }); + result = TimeValidator.greaterThanValidator('start_time', 'end_time')(formGroup); + expect(result).toEqual({ lessThan: { value: true }}); + }); + + it('should return the value of the validator RangePeriodValidator', () => { + const formGroup = formBuilder.group({ + 'times': formBuilder.array([ + formBuilder.group({ + start_time: new FormControl('08:00'), + end_time: new FormControl('11:00') + }), + formBuilder.group({ + start_time: new FormControl('13:00'), + end_time: new FormControl('17:00') + }), + ]), + 'day': ['monday'], + 'is_open': true + }); + let result = TimeValidator.RangePeriodValidator()(formGroup); + expect(result).toBeNull(); + + const formGroup2 = formBuilder.group({ + 'times': formBuilder.array([ + formBuilder.group({ + start_time: new FormControl('08:00'), + end_time: new FormControl('11:00') + }), + formBuilder.group({ + start_time: new FormControl('10:00'), + end_time: new FormControl('12:00') + }), + ]), + 'day': ['monday'], + 'is_open': true + }); + result = TimeValidator.RangePeriodValidator()(formGroup2); + expect(result).toEqual({ rangeLessThan: { value: true }}); + + const formGroup3 = formBuilder.group({ + 'times': formBuilder.array([ + formBuilder.group({ + start_time: new FormControl('14:00'), + end_time: new FormControl('16:00') + }), + formBuilder.group({ + start_time: new FormControl('13:00'), + end_time: new FormControl('15:00') + }), + ]), + 'day': ['monday'], + 'is_open': true + }); + result = TimeValidator.RangePeriodValidator()(formGroup3); + expect(result).toEqual({ rangeLessThan: { value: true }}); + }); +}); diff --git a/projects/rero/ng-core/src/lib/validator/time.validator.ts b/projects/rero/ng-core/src/lib/validator/time.validator.ts index 9510c7a3..ab1f2952 100644 --- a/projects/rero/ng-core/src/lib/validator/time.validator.ts +++ b/projects/rero/ng-core/src/lib/validator/time.validator.ts @@ -1,6 +1,6 @@ /* * RERO angular core - * Copyright (C) 2020 RERO + * Copyright (C) 2020-2024 RERO * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -15,11 +15,13 @@ * along with this program. If not, see . */ import {AbstractControl, UntypedFormArray, ValidationErrors, ValidatorFn} from '@angular/forms'; -import moment from 'moment'; +import { DateTime } from "luxon"; // @dynamic export class TimeValidator { + static readonly FORMAT = 'hh:mm'; + /** * Allow to control if time interval limits are well formed : the end limit should be 'older' the start limit * @param start: the field name where to find the start limit value @@ -31,9 +33,9 @@ export class TimeValidator { let isLessThan = false; const startTime = control.get(start); const endTime = control.get(end); - const startDate = moment(startTime.value, 'HH:mm'); - const endDate = moment(endTime.value, 'HH:mm'); - if (startDate.format('HH:mm') !== '00:00' || endDate.format('HH:mm') !== '00:00') { + const startDate = DateTime.fromFormat(startTime.value, TimeValidator.FORMAT); + const endDate = DateTime.fromFormat(endTime.value, TimeValidator.FORMAT); + if (startDate.toFormat(TimeValidator.FORMAT) !== '00:00' || endDate.toFormat(TimeValidator.FORMAT) !== '00:00') { isLessThan = startDate.diff(endDate) >= 0; } return (isLessThan) @@ -49,10 +51,10 @@ export class TimeValidator { let isRangeLessThan = false; const times = control.get('times') as UntypedFormArray; if (control.get('is_open').value && times.value.length > 1) { - const firstStartDate = moment(times.at(0).get('start_time').value, 'HH:mm'); - const firstEndDate = moment(times.at(0).get('end_time').value, 'HH:mm'); - const lastStartDate = moment(times.at(1).get('start_time').value, 'HH:mm'); - const lastEndDate = moment(times.at(1).get('end_time').value, 'HH:mm'); + const firstStartDate = DateTime.fromFormat(times.at(0).get('start_time').value, TimeValidator.FORMAT); + const firstEndDate = DateTime.fromFormat(times.at(0).get('end_time').value, TimeValidator.FORMAT); + const lastStartDate = DateTime.fromFormat(times.at(1).get('start_time').value, TimeValidator.FORMAT); + const lastEndDate = DateTime.fromFormat(times.at(1).get('end_time').value, TimeValidator.FORMAT); if (firstStartDate > lastStartDate) { isRangeLessThan = firstStartDate.diff(lastStartDate) <= 0 || firstStartDate.diff(lastEndDate) <= 0; @@ -62,7 +64,7 @@ export class TimeValidator { } } return (isRangeLessThan) - ? ({ rangeLessThan: { value: isRangeLessThan}}) + ? ({ rangeLessThan: { value: isRangeLessThan }}) : null; } }; diff --git a/projects/rero/ng-core/src/lib/validator/validators.spec.ts b/projects/rero/ng-core/src/lib/validator/validators.spec.ts new file mode 100644 index 00000000..f54cf339 --- /dev/null +++ b/projects/rero/ng-core/src/lib/validator/validators.spec.ts @@ -0,0 +1,76 @@ +/* + * RERO angular core + * Copyright (C) 2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { TestBed } from '@angular/core/testing'; +import { FormBuilder, FormControl, FormsModule } from '@angular/forms'; +import { Validators } from './validators'; + +describe('Validators', () => { + let formBuilder: FormBuilder; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule + ], + providers: [ + FormBuilder + ] + }); + + formBuilder = TestBed.inject(FormBuilder); + }); + + it('should return the value of the validator dateGreaterThan', () => { + const validatorResponse = { dateGreaterThan: { value: true }}; + let formControl = new FormControl(); + let formGroup = formBuilder.group({ + start_date: new FormControl('2024-10-01'), + end_date: new FormControl('2024-10-02') + }); + formControl.setParent(formGroup); + let result = Validators.dateGreaterThan('start_date', 'end_date')(formControl); + expect(result).toBeNull(); + + formControl = new FormControl(); + formGroup = formBuilder.group({ + start_date: new FormControl('2024-10-11'), + end_date: new FormControl('2024-10-11') + }); + formControl.setParent(formGroup); + result = Validators.dateGreaterThan('start_date', 'end_date')(formControl); + expect(result).toBeNull(); + + formControl = new FormControl(); + formGroup = formBuilder.group({ + start_date: new FormControl('2024-10-11'), + end_date: new FormControl('2024-10-11') + }); + formControl.setParent(formGroup); + // With strict mode + result = Validators.dateGreaterThan('start_date', 'end_date', true)(formControl); + expect(result).toEqual(validatorResponse); + + formControl = new FormControl(); + formGroup = formBuilder.group({ + start_date: new FormControl('2024-10-11'), + end_date: new FormControl('2024-10-01') + }); + formControl.setParent(formGroup); + result = Validators.dateGreaterThan('start_date', 'end_date')(formControl); + expect(result).toEqual(validatorResponse); + }); +}); diff --git a/projects/rero/ng-core/src/lib/validator/validators.ts b/projects/rero/ng-core/src/lib/validator/validators.ts new file mode 100644 index 00000000..4ca44647 --- /dev/null +++ b/projects/rero/ng-core/src/lib/validator/validators.ts @@ -0,0 +1,46 @@ +/* + * RERO angular core + * Copyright (C) 2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms"; +import { DateTime } from "luxon"; + +export class Validators { + static dateGreaterThan(dateFirstName: string, dateLastName: string, strictMode: boolean = false): ValidatorFn { + return dateGreaterThanValidator(dateFirstName, dateLastName, strictMode); + } +} + +export function dateGreaterThanValidator(dateFirstName: string, dateLastName: string, strict: boolean): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const dateFormat = 'yyyy-MM-dd'; + const dateFirst = control.parent.get(dateFirstName); + const dateLast = control.parent.get(dateLastName); + if (dateFirst.value !== null && dateLast.value !== null) { + const dateTimeFirst = DateTime.fromFormat(dateFirst.value, dateFormat); + const dateTimeLast = DateTime.fromFormat(dateLast.value, dateFormat); + const status = strict ? dateTimeFirst >= dateTimeLast : dateTimeFirst > dateTimeLast; + + if (status) { + dateLast.setErrors(null); + dateLast.markAsDirty(); + } + + return status ? { dateGreaterThan: { value: true }} : null; + } + + return null; + } +} diff --git a/projects/rero/ng-core/src/public-api.ts b/projects/rero/ng-core/src/public-api.ts index 8d30a731..6e81a5b6 100644 --- a/projects/rero/ng-core/src/public-api.ts +++ b/projects/rero/ng-core/src/public-api.ts @@ -90,6 +90,7 @@ export * from './lib/utils/sort-by-keys'; export * from './lib/utils/utils'; export * from './lib/validator/time.validator'; export * from './lib/validator/unique.validator'; +export * from './lib/validator/validators'; export * from './lib/widget/menu/menu.component'; export * from './lib/widget/sort-list/sort-list.component';