From f39fd471cb2f176a417667481b17a72b2d4651dd Mon Sep 17 00:00:00 2001 From: Alaa Yahia <6881345+alaa-yahia@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:01:15 +0200 Subject: [PATCH] feat: [DHIS2-15462] Use dhis2 UI calendarinput component in forms (#3658) * feat: use calendarInput component in forms * fix: add disabled to calendar input * fix: remove ref * fix: flow errors * chore: update tests * feat: add error messages * fix: date and dateTime errors to display only once * feat: add errors to main field * feat: add validation to dateTime & age fields * feat: scheduleDate reset when there is invalid date * fix: width & calendarWidth to be string * fix: schedule date in related stages to display calendarInput internal errors * fix: remove current context from function * fix: enrollment date input fields * fix: refactor incident date validation to use form validations * feat: eventDate validation to use internal error from calendarInput * fix: pass validationContext for unique validator * fix: always pass current context * chore: update calendarInput ui version * fix: update rules engine version * fix: runtime error when date is null * fix: flow types * fix: failing tests --- .../EnrollmentAddEventPageForm.js | 4 +- .../EnrollmentEditEventPageForm.js | 6 +- .../BreakingTheGlass/BreakingTheGlass.js | 10 +- .../HiddenProgramStage/HiddenProgramStage.js | 2 +- .../StagesAndEventsWidget.js | 2 +- cypress/e2e/NewPage/NewPage.js | 58 ++--- cypress/e2e/ScopeSelector/ScopeSelector.js | 6 +- ...archForDuplicatesThroughAddRelationship.js | 10 +- cypress/e2e/SearchPage/SearchPage.js | 64 ++--- .../SearchThroughAddRelationship.js | 26 +- cypress/e2e/TopBarActions/TopBarActions.js | 2 +- .../WidgetAssignee/index.js | 4 +- .../WidgetEnrollment/index.js | 2 +- .../WidgetProfile/index.js | 2 +- .../WidgetTab/index.js | 2 +- .../WidgetsForEnrollmentAddEventPage.js | 4 +- .../WidgetsForEnrollmentDashboard.js | 4 +- .../WidgetsForEventSchedule.js | 4 +- .../TeiWorkingListsUser.js | 2 +- .../step_definitions/common/baseSteps.js | 6 +- i18n/en.pot | 15 +- package.json | 4 +- .../validators/form/age.validator.js | 76 ------ .../validators/form/date.validator.js | 13 - .../validators/form/dateTime.validator.js | 20 -- .../validators/form/index.js | 3 - .../FormBuilder/FormBuilder.component.js | 131 +++++++---- .../D2Form/FormBuilder/formbuilder.types.js | 4 +- .../dateTimeField/getDateTimeFieldConfig.js | 6 +- .../getDateTimeFieldConfigForCustomForm.js | 4 +- .../D2Form/field/validators/getValidators.js | 10 +- .../EnrollmentDataEntry.component.js | 6 +- .../eventDate.validatorContainersGetter.js | 6 +- ...nrollmentDate.validatorContainersGetter.js | 30 +-- .../incidentDate.validatorContainerGetter.js | 30 +-- .../eventDate.validatorContainersGetter.js | 4 +- .../internal/DataEntryField.component.js | 4 +- .../internal/dataEntryField.utils.js | 10 +- .../New/Fields/AgeField/AgeField.component.js | 15 +- .../DateField/DateField.component.js | 3 +- .../DateRangeField.component.js | 3 +- .../DateTimeField/DateTimeField.component.js | 3 +- .../DateTimeRangeField.component.js | 3 +- .../DateAndTimeFields/getCalendarTheme.js | 14 -- .../New/HOC/messages/withDisplayMessages.js | 1 - .../FormFields/New/HOC/withCalendarProps.js | 69 ------ .../eventDate.validatorContainersGetter.js | 4 +- .../eventDate.validatorContainersGetter.js | 6 +- .../ScheduleDate/ScheduleDate.component.js | 11 +- .../DateFieldForRelatedStages.js | 9 +- .../ScheduleInOrgUnit.component.js | 6 +- .../WidgetRelatedStages.component.js | 4 +- .../WidgetRelatedStages.types.js | 1 + .../ValidationFunctions.js | 23 +- .../relatedStageEventIsValid.js | 2 + .../relatedStageEventIsValid.types.js | 1 + .../capture-core/converters/formToClient.js | 4 +- .../utils/validators/form/ageValidator.js | 76 +++++- .../validators/form/dateTimeValidator.js | 64 ++++- .../utils/validators/form/dateValidator.js | 22 +- .../validators/form/isValidNonFutureDate.js | 25 +- .../capture-ui/AgeField/AgeField.component.js | 23 +- .../DateField/Date.component.js | 222 +++++------------- .../DateField/DateCalendar.component.js | 98 -------- .../DateField/DatePopup.component.js | 87 ------- .../DateField/datePopup.const.js | 17 -- .../DateField/datePopup.module.css | 8 - .../DateTimeField/DateTime.component.js | 96 +++++--- .../AgeInput/AgeDateInput.component.js | 6 +- .../DateTimeInput/DateTimeDate.component.js | 9 +- .../DateTimeInput/DateTimeTime.component.js | 19 +- .../internal/TextInput/TextInput.component.js | 2 +- .../internal/TextInput/withFocusHandler.js | 6 +- yarn.lock | 2 +- 74 files changed, 613 insertions(+), 947 deletions(-) delete mode 100644 src/core_modules/capture-core-utils/validators/form/age.validator.js delete mode 100644 src/core_modules/capture-core-utils/validators/form/date.validator.js delete mode 100644 src/core_modules/capture-core-utils/validators/form/dateTime.validator.js delete mode 100644 src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/getCalendarTheme.js delete mode 100644 src/core_modules/capture-core/components/FormFields/New/HOC/withCalendarProps.js delete mode 100644 src/core_modules/capture-ui/DateAndTimeFields/DateField/DateCalendar.component.js delete mode 100644 src/core_modules/capture-ui/DateAndTimeFields/DateField/DatePopup.component.js delete mode 100644 src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.const.js delete mode 100644 src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.module.css diff --git a/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/EnrollmentAddEventPageForm.js b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/EnrollmentAddEventPageForm.js index ca8d21ed03..7a8523cfa6 100644 --- a/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/EnrollmentAddEventPageForm.js +++ b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/EnrollmentAddEventPageForm.js @@ -89,7 +89,7 @@ When(/^you click the create new button number (.*)$/, (eq) => { }); When(/^you type (.*) in the input number (.*)$/, (value, eq) => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(eq) .type(value) .blur(); @@ -155,7 +155,7 @@ When(/^the user selects (.*)$/, (value) => { }); When(/^you focus and blur a required field/, () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .focus() .blur(); diff --git a/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.js b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.js index 93ab1beea1..3f42786176 100644 --- a/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.js +++ b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.js @@ -84,7 +84,7 @@ When(/^the user clicks on the cancel button/, () => When(/^the user set the apgar score to (.*)/, score => cy .get('[data-test="widget-enrollment-event"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(2) .clear() .type(score) @@ -118,8 +118,8 @@ When('the user clicks switch tab to Schedule', () => { Then('the user selects another schedule date', () => { cy.get('[data-test="schedule-section"]').within(() => { - cy.get("[data-test='capture-ui-input']").eq(0).should('have.value', `${getCurrentYear() - 15}-01-07`); - cy.get("[data-test='capture-ui-input']").eq(0) + cy.get('input[type="text"]').eq(0).should('have.value', `${getCurrentYear() - 15}-01-07`); + cy.get('input[type="text"]').eq(0) .clear() .type(`${getCurrentYear()}-08-01`) .blur(); diff --git a/cypress/e2e/EnrollmentPage/BreakingTheGlass/BreakingTheGlass.js b/cypress/e2e/EnrollmentPage/BreakingTheGlass/BreakingTheGlass.js index 7e33f96bc7..a9ca4a6e31 100644 --- a/cypress/e2e/EnrollmentPage/BreakingTheGlass/BreakingTheGlass.js +++ b/cypress/e2e/EnrollmentPage/BreakingTheGlass/BreakingTheGlass.js @@ -16,21 +16,21 @@ Given('the tei created by this test is cleared from the database', () => { And('you create a new tei in Child programme from Ngelehun CHC', () => { cy.visit('/#/new?orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW'); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .type('1999-09-01') .blur(); cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Breaking') .blur(); cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('TheGlass') .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(7) .type('2023-09-01') .blur(); @@ -68,7 +68,7 @@ And('you enroll the tei from Njandama MCHP', () => { .click(); cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(8) .type('1999-09-01') .blur(); diff --git a/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js b/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js index 43152029ff..151c272842 100644 --- a/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js +++ b/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js @@ -28,7 +28,7 @@ Given('you add an enrollment event that will result in a rule effect to hide a p '/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&stageId=PFDfvmGpsR3&teiId=uW8Y7AIcRKA', ); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(0) .type(moment().format('YYYY-MM-DD')) .blur(); diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js index d4771a8876..35d8996ee7 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.js @@ -11,7 +11,7 @@ After({ tags: '@with-restore-deleted-event' }, () => { .find('[data-test="create-new-button"]') .click(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .first() .type('2023-01-26') .blur(); diff --git a/cypress/e2e/NewPage/NewPage.js b/cypress/e2e/NewPage/NewPage.js index a4a7579656..74201146ac 100644 --- a/cypress/e2e/NewPage/NewPage.js +++ b/cypress/e2e/NewPage/NewPage.js @@ -328,7 +328,7 @@ Then('you see validation error on visit date', () => { }); And('you fill in 200 in the hemoglobin', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .type('200'); }); @@ -340,13 +340,13 @@ And('you see validation error on hemoglobin', () => { }); And('you fill in the visit date', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(0) - .type(`${getCurrentYear()}-01-01`); + .type(`${getCurrentYear()}-01-01`).blur(); }); And('you fill in the hemoglobin', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .type('50'); }); @@ -389,14 +389,14 @@ And('you are in the Person registration page', () => { }); And('you fill in the first name with value that has duplicates', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .type('Sarah') .blur(); }); And('you fill in a unique first name', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .type(`Sarah-${Math.round((new Date()).getTime() / 1000)}`) .blur(); @@ -427,7 +427,7 @@ Then('you submit the form again from the duplicates modal', () => { // New person in WHO RMNCH Tracker And('you are in the WHO RMNCH program registration page', () => { cy.visit('/#/new?programId=WSGAb5XwJ3Y&orgUnitId=DiszpKrYNg8'); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .invoke('val').should('not.be.empty'); }); @@ -441,7 +441,7 @@ And('you are in Child programme and Buma MCHP organization unit registration pag }); And('you fill the form with age 0', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(9) .type(moment().format('YYYY-MM-DD')) .blur(); @@ -454,37 +454,37 @@ And('you see validation warning on birth date', () => { }); And('you fill the WHO RMNCH program registration form with its required unique values', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(2) .type(`Sarah-${Math.round((new Date()).getTime() / 1000)}`); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(3) .type('Gonzales'); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(9) .type('1992-01-01') .blur(); }); And('you fill the WHO RMNCH program registration form with its required values', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(3) .type('Didriksson'); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(2) .type('Ava'); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(9) .type('1985-10-01') .blur(); }); And('you fill in child programme first name with value that has duplicates', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(4) .type('Sarah') .blur(); @@ -492,18 +492,18 @@ And('you fill in child programme first name with value that has duplicates', () And('you fill the Child programme registration form with a first name with value that has duplicates', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .type('2021-01-01') .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(4) .type('Sarah') .blur(); }); And('you fill in the birth report date', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(7) .type('2023-01-01') .blur(); @@ -530,9 +530,9 @@ And('you see the form prefield with existing TEI attributes values', () => { cy.get('[data-test="registration-page-content"]').within(() => { cy.contains('New Enrollment in program: Child Programme').should('exist'); cy.contains('First name').should('exist'); - cy.get('[data-test="capture-ui-input"]').eq(4).should('have.value', 'Anna'); + cy.get('input[type="text"]').eq(4).should('have.value', 'Anna'); cy.contains('Last name').should('exist'); - cy.get('[data-test="capture-ui-input"]').eq(5).should('have.value', 'Jones'); + cy.get('input[type="text"]').eq(5).should('have.value', 'Jones'); cy.contains('Gender').should('exist'); cy.contains('Female').should('exist'); }); @@ -554,15 +554,15 @@ Given('you open the main page with Ngelehun and Malaria case diagnosis, treatmen }); And('you fill the Malaria case diagnosis registration form with values', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(3) .type(`Ana-${Math.round((new Date()).getTime() / 1000)}`) .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(4) .type(`Maria-${Math.round((new Date()).getTime() / 1000)}`) .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(5) .type(moment().add(-1, 'day').format('YYYY-MM-DD')) .blur(); @@ -602,26 +602,26 @@ Then('the first stage appears on registration page', () => { }); And('you fill the Child Program program registration form with unique values', () => { - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(1) .type('2021-01-01') .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(2) .type(20); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(3) .type(30) .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(4) .type(`Sarah-${Math.round((new Date()).getTime() / 1000)}`) .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(5) .type(`Beth-${Math.round((new Date()).getTime() / 1000)}`) .blur(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(7) .type('2021-01-01') .blur(); diff --git a/cypress/e2e/ScopeSelector/ScopeSelector.js b/cypress/e2e/ScopeSelector/ScopeSelector.js index c870644462..65bd9930f3 100644 --- a/cypress/e2e/ScopeSelector/ScopeSelector.js +++ b/cypress/e2e/ScopeSelector/ScopeSelector.js @@ -49,7 +49,7 @@ Given('you are in the main page with program preselected', () => { Given('you select both org unit and program Malaria case registration', () => { cy.get('[data-test="org-unit-selector-container"]') .click(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .type('Ngelehun C'); cy.contains('Ngelehun CHC') .click(); @@ -67,7 +67,7 @@ Given('you select both org unit and program Malaria case registration', () => { Given('you select both org unit and program Child Programme', () => { cy.get('[data-test="org-unit-selector-container"]') .click(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .type('Ngelehun C'); cy.contains('Ngelehun CHC') .click(); @@ -359,7 +359,7 @@ And('you see message explaining this is an Event program', () => { When('you select org unit that is incompatible with the already selected program', () => { cy.get('[data-test="org-unit-selector-container"]') .click(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .type('Biriw'); cy.contains('Biriwa') .click(); diff --git a/cypress/e2e/SearchForDuplicatesThroughAddRelationship/SearchForDuplicatesThroughAddRelationship.js b/cypress/e2e/SearchForDuplicatesThroughAddRelationship/SearchForDuplicatesThroughAddRelationship.js index 6ddaef31b1..8cbb6729d7 100644 --- a/cypress/e2e/SearchForDuplicatesThroughAddRelationship/SearchForDuplicatesThroughAddRelationship.js +++ b/cypress/e2e/SearchForDuplicatesThroughAddRelationship/SearchForDuplicatesThroughAddRelationship.js @@ -2,7 +2,7 @@ import { When, defineStep as And } from '@badeball/cypress-cucumber-preprocessor And('you fill in the first name with values that have duplicates', () => { cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .wait(500) .type('Tesmi') @@ -11,12 +11,12 @@ And('you fill in the first name with values that have duplicates', () => { And('you fill in the first name with values that have less than 5 duplicates', () => { cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Sarah') .blur(); cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(2) .type('Fis') .blur(); @@ -24,13 +24,13 @@ And('you fill in the first name with values that have less than 5 duplicates', ( And('you fill in the first name with values that have exactly 5 duplicates', () => { cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .wait(500) .type('Tesmi') .blur(); cy.get('[data-test="d2-section"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(2) .type('Abel') .blur(); diff --git a/cypress/e2e/SearchPage/SearchPage.js b/cypress/e2e/SearchPage/SearchPage.js index 19ff59af2d..829e87a571 100644 --- a/cypress/e2e/SearchPage/SearchPage.js +++ b/cypress/e2e/SearchPage/SearchPage.js @@ -21,7 +21,7 @@ When('you select the search domain Person', () => { Then('there should be Person domain forms available to search with', () => { cy.get('[data-test="search-page-content"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .should('have.length', 1); }); @@ -53,7 +53,7 @@ When('you select the search domain WHO RMNCH Tracker', () => { When('you fill in the unique identifier field with values that will not return a tracked entity instance', () => { cy.get('[data-test="form-unique"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .type('123') .blur(); @@ -85,7 +85,7 @@ When('you can close the modal', () => { When('you fill in the unique identifier field with values that will return a tracked entity instance', () => { cy.get('[data-test="form-unique"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .clear() .type('3131112445555') @@ -95,7 +95,7 @@ When('you fill in the unique identifier field with values that will return a tra When('you fill in the first name with values that will return no results', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .type('user non existing') .blur(); @@ -110,7 +110,7 @@ And('you expand the attributes search area', () => { When('you fill in the last name with values that will return results', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Smith') .blur(); @@ -118,7 +118,7 @@ When('you fill in the last name with values that will return results', () => { When('for Malaria case you fill in values that will return less than 5 results', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(2) .type('Sara') .blur(); @@ -126,12 +126,12 @@ When('for Malaria case you fill in values that will return less than 5 results', When('for Person you fill in values that will return less than 5 results', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Sara') .blur(); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Fis') .blur(); @@ -139,37 +139,37 @@ When('for Person you fill in values that will return less than 5 results', () => When('you dont fill in any of the values', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .clear(); }); When('you fill the values with nothing but spaces', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .type(' '); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type(' '); }); When('you fill in the the form with values', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Smith'); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Smith'); }); And(/^you fill in the the form with first name value: (.*)$/, (firstName) => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type(firstName) .blur(); @@ -177,14 +177,14 @@ And(/^you fill in the the form with first name value: (.*)$/, (firstName) => { When('you clear the values', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .clear(); cy.get('[data-test="form-attributes"]').click(); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .clear(); cy.get('[data-test="form-attributes"]').click(); @@ -235,7 +235,7 @@ Then('there should be visible a title with Malaria case diagnosis', () => { And('there should be Malaria case diagnosis forms visible to search with', () => { cy.get('[data-test="search-page-content"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .should('have.length', 1); }); @@ -245,12 +245,12 @@ Given('you are in the search page with the Adult Woman being preselected from th When('you fill in the date of birth', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(2) .type('1999-09-01') .blur(); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(3) .type('2020-01-01') .blur(); @@ -275,7 +275,7 @@ When('you fill in the zip code range numbers', () => { When('you fill in the first name', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Lid') .blur(); @@ -288,13 +288,13 @@ When('you click the fallback search button', () => { When('you fill in the first and last name with values that will return results', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Go') .blur(); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Sarah') .blur(); @@ -302,13 +302,13 @@ When('you fill in the first and last name with values that will return results', When('you press enter after filling in the first and last name with values that will return results', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Go') .blur(); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Sarah') .wait(500) @@ -317,7 +317,7 @@ When('you press enter after filling in the first and last name with values that When('you press enter after filling in the unique identifier field with values that will return a tracked entity instance', () => { cy.get('[data-test="form-unique"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .clear() .type('3131112445555{enter}'); @@ -325,13 +325,13 @@ When('you press enter after filling in the unique identifier field with values t When('you fill in the first name with value and last name with empty space', () => { cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Thomas') .blur(); cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type(' ') .blur(); @@ -369,7 +369,7 @@ When('you see the attributes search area being expanded', () => { When('and you can see the unique identifier input', () => { cy.get('[data-test="form-unique"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .should('exist'); }); @@ -404,14 +404,14 @@ Then('you should be taken to the registration page with program with prefilled v .contains('New person in program: Child Programme') .should('exist'); cy.get('[data-test="registration-page-content"]').contains('First name').should('exist'); - cy.get('[data-test="capture-ui-input"]').eq(4).should('have.value', 'Sarah'); + cy.get('input[type="text"]').eq(4).should('have.value', 'Sarah'); cy.get('[data-test="registration-page-content"]').contains('Last name').should('exist'); - cy.get('[data-test="capture-ui-input"]').eq(5).should('have.value', 'Go'); + cy.get('input[type="text"]').eq(5).should('have.value', 'Go'); }); Then('you should be taken to the registration page without program with prefilled values', () => { cy.get('[data-test="registration-page-content"]').contains('First name').should('exist'); - cy.get('[data-test="capture-ui-input"]').eq(1).should('have.value', 'Sara'); + cy.get('input[type="text"]').eq(1).should('have.value', 'Sara'); cy.get('[data-test="registration-page-content"]').contains('Last name').should('exist'); - cy.get('[data-test="capture-ui-input"]').eq(2).should('have.value', 'Fis'); + cy.get('input[type="text"]').eq(2).should('have.value', 'Fis'); }); diff --git a/cypress/e2e/SearchThroughAddRelationship/SearchThroughAddRelationship.js b/cypress/e2e/SearchThroughAddRelationship/SearchThroughAddRelationship.js index 6ca75b0e34..89bc760ec7 100644 --- a/cypress/e2e/SearchThroughAddRelationship/SearchThroughAddRelationship.js +++ b/cypress/e2e/SearchThroughAddRelationship/SearchThroughAddRelationship.js @@ -21,7 +21,7 @@ When('you expand the third search area', () => { And('you fill in the first name with values that will return no results', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Name doesnt exist') .blur(); @@ -29,7 +29,7 @@ And('you fill in the first name with values that will return no results', () => And('you fill in the first name with values that will return results', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Tesmi') .blur(); @@ -49,23 +49,23 @@ And('there should be a validation error message', () => { And('you fill the values with nothing but spaces', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type(' '); cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type(' '); }); And('you fill in the the form with values', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Tesmi') .blur(); cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Tesmi') .blur(); @@ -73,18 +73,18 @@ And('you fill in the the form with values', () => { And('you clear the values', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .clear(); cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .clear(); }); And('you fill in the first name with values that will return an error', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .first() .type(',,,,') .blur(); @@ -104,12 +104,12 @@ And('the next page button is disabled', () => { And('you fill in the the form with values that will return less than 5 results', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Sara') .blur(); cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Gonzalez') .blur(); @@ -117,12 +117,12 @@ And('you fill in the the form with values that will return less than 5 results', And('you fill in the the form with values that will return exactly 5 results', () => { cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(0) .type('Tesmi') .blur(); cy.get('[data-test="d2-form-area"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .eq(1) .type('Abel') .blur(); diff --git a/cypress/e2e/TopBarActions/TopBarActions.js b/cypress/e2e/TopBarActions/TopBarActions.js index 624ca1ac82..521e5f4415 100644 --- a/cypress/e2e/TopBarActions/TopBarActions.js +++ b/cypress/e2e/TopBarActions/TopBarActions.js @@ -20,7 +20,7 @@ Then('the user sees the warning popup', () => { }); When(/^the user set the WHOMCH Diastolic blood pressure to (.*)/, score => - cy.get('[data-test="new-enrollment-event-form"]').find('[data-test="capture-ui-input"]').eq(6).clear() + cy.get('[data-test="new-enrollment-event-form"]').find('input[type="text"]').eq(6).clear() .type(score) .blur(), ); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js index 07403f7566..37b328ec17 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js @@ -3,7 +3,7 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; When('you assign the user Geetha in the view mode', () => { cy.get('[data-test="widget-assignee"]').within(() => { cy.get('[data-test="widget-assignee-assign"]').click(); - cy.get('[data-test="capture-ui-input"]').type('Geetha'); + cy.get('input[type="text"]').type('Geetha'); cy.contains('Geetha Alwan').click(); cy.get('[data-test="widget-assignee-save"]').click(); }); @@ -18,7 +18,7 @@ When('you assign the user Tracker demo User in the edit mode', () => { cy.get('[data-test="widget-assignee"]').within(() => { cy.get('[data-test="widget-assignee-edit"]').click(); cy.get('[data-test="dhis2-uicore-chip-remove"]').click(); - cy.get('[data-test="capture-ui-input"]').type('Tracker demo'); + cy.get('input[type="text"]').type('Tracker demo'); cy.contains('Tracker demo User').click(); cy.get('[data-test="widget-assignee-save"]').click(); }); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js index 5303500a8d..9f4148abf9 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js @@ -250,7 +250,7 @@ Then(/^the user successfully transfers the enrollment/, () => { Then(/^the user types in (.*)/, (orgunit) => { cy.get('[data-test="widget-enrollment-transfer-modal"]').within(() => { - cy.get('[data-test="capture-ui-input"]').type(orgunit); + cy.get('input[type="text"]').type(orgunit); }); }); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetProfile/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetProfile/index.js index 37328551ba..7dde4dcc06 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetProfile/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetProfile/index.js @@ -33,7 +33,7 @@ Then(/^the user sees the edit profile modal/, () => Given('you add a new tracked entity in the Malaria focus investigation program', () => { cy.visit('/#/new?programId=M3xtLkYBlKI&orgUnitId=DiszpKrYNg8'); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .eq(2) .type(`Local id-${Math.round((new Date()).getTime() / 1000)}`) .blur(); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetTab/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetTab/index.js index 036671a649..07d8f01448 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetTab/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetTab/index.js @@ -24,6 +24,6 @@ Then('you should see Schedule tab', () => { And(/you should see suggested date: (.*)/, (date) => { cy.get('[data-test="schedule-section"]').within(() => { - cy.get('[data-test="capture-ui-input"]').should('have.value', `${getCurrentYear()}-${date}`); + cy.get('input[type="text"]').should('have.value', `${getCurrentYear()}-${date}`); }); }); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.js index 24f07d7593..5aa738ee44 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.js @@ -6,8 +6,8 @@ import '../WidgetTab'; Then('you can assign a user when scheduling the event', () => { cy.get('[data-test="assignee-section"]').within(() => { - cy.get('[data-test="capture-ui-input"]').click(); - cy.get('[data-test="capture-ui-input"]').type('Tracker demo'); + cy.get('input[type="text"]').click(); + cy.get('input[type="text"]').type('Tracker demo'); cy.contains('Tracker demo User').click(); }); cy.get('[data-test="assignee-section"]').within(() => { diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/WidgetsForEnrollmentDashboard.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/WidgetsForEnrollmentDashboard.js index 96a583bd38..b6efc6f7df 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/WidgetsForEnrollmentDashboard.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/WidgetsForEnrollmentDashboard.js @@ -6,14 +6,14 @@ import '../WidgetProfile'; import '../WidgetEnrollmentNote'; When('the user sets the birthday date to the current date', () => { - cy.get('[data-test="modal-edit-profile"]').find('[data-test="capture-ui-input"]').eq(8).clear() + cy.get('[data-test="modal-edit-profile"]').find('input[type="text"]').eq(8).clear() .blur() .type(moment().format('YYYY-MM-DD')) .blur(); }); When(/^the user sets the first name to (.*)$/, (name) => { - cy.get('[data-test="modal-edit-profile"]').find('[data-test="capture-ui-input"]').eq(1).clear() + cy.get('[data-test="modal-edit-profile"]').find('input[type="text"]').eq(1).clear() .blur() .type(name) .blur(); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule/WidgetsForEventSchedule.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule/WidgetsForEventSchedule.js index 3896685c41..1e0d6a4f4e 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule/WidgetsForEventSchedule.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule/WidgetsForEventSchedule.js @@ -5,8 +5,8 @@ import '../WidgetTab'; Then('you choose a schedule date', () => { cy.get('[data-test="schedule-section"]').within(() => { - cy.get("[data-test='capture-ui-input']").eq(0).should('have.value', `${getCurrentYear()}-08-01`); - cy.get("[data-test='capture-ui-input']").eq(0) + cy.get('input[type="text"]').eq(0).should('have.value', `${getCurrentYear()}-08-01`); + cy.get('input[type="text"]').eq(0) .clear() .type(`${getCurrentYear() + 1}-08-01`) .blur(); diff --git a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js index 0ea736c26a..04f0af84b7 100644 --- a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js +++ b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js @@ -723,7 +723,7 @@ Then('the TEI working list initial configuration was kept', () => { And('you change the org unit', () => { cy.get('[data-test="org-unit-selector-container"]') .click(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .type('Njandama MCHP'); cy.contains('Njandama MCHP') .click(); diff --git a/cypress/support/step_definitions/common/baseSteps.js b/cypress/support/step_definitions/common/baseSteps.js index a4db6e4a30..c4122b51cc 100644 --- a/cypress/support/step_definitions/common/baseSteps.js +++ b/cypress/support/step_definitions/common/baseSteps.js @@ -22,7 +22,7 @@ And('you see the dropdown menu for selecting tracked entity type', () => { And('you select org unit', () => { cy.get('[data-test="org-unit-selector-container"]') .click(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .type('Ngelehun C'); cy.contains('Ngelehun CHC') .click(); @@ -36,7 +36,7 @@ Then('there should be visible a title with Child Program', () => { And('there should be Child Programme domain forms visible to search with', () => { cy.get('[data-test="search-page-content"]') - .find('[data-test="capture-ui-input"]') + .find('input[type="text"]') .should('have.length', 1); }); @@ -176,7 +176,7 @@ When(/^the user selects the program (.*)$/, (program) => { When(/^the user selects the org unit (.*)$/, (orgUnit) => { cy.get('[data-test="org-unit-selector-container"]') .click(); - cy.get('[data-test="capture-ui-input"]') + cy.get('input[type="text"]') .type(orgUnit.slice(0, -1)); cy.contains(orgUnit) .click(); diff --git a/i18n/en.pot b/i18n/en.pot index 8ba577cb6c..6f7cde475a 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -197,9 +197,6 @@ msgstr "{{ stageName }} - Status" msgid "Please select {{categoryName}}" msgstr "Please select {{categoryName}}" -msgid "A future date is not allowed" -msgstr "A future date is not allowed" - msgid "Saving a new enrollment in {{programName}} in {{orgUnitName}}." msgstr "Saving a new enrollment in {{programName}} in {{orgUnitName}}." @@ -1454,6 +1451,9 @@ msgstr "Scheduled date" msgid "Report date" msgstr "Report date" +msgid "Please enter a date" +msgstr "Please enter a date" + msgid "Please select a valid event" msgstr "Please select a valid event" @@ -1930,6 +1930,15 @@ msgstr "Error editing the event, the changes made were not saved" msgid "Error updating the Assignee" msgstr "Error updating the Assignee" +msgid "Please provide a valid positive integer" +msgstr "Please provide a valid positive integer" + +msgid "Please enter a valid time" +msgstr "Please enter a valid time" + +msgid "Please enter a time" +msgstr "Please enter a time" + msgid "Set coordinate" msgstr "Set coordinate" diff --git a/package.json b/package.json index 4053ecf698..24a754ca1e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@dhis2/d2-ui-rich-text": "^7.4.0", "@dhis2/d2-ui-sharing-dialog": "^7.3.3", "@dhis2/ui": "^9.10.1", - "@dhis2-ui/calendar": "10.0.3", + "@dhis2-ui/calendar": "^10.0.3", "@joakim_sm/react-infinite-calendar": "^2.4.2", "@material-ui/core": "3.9.4", "@material-ui/icons": "3", @@ -137,7 +137,7 @@ "@dhis2/app-runtime": "^3.10.2", "@babel/preset-react": "7.16.7", "@dhis2/ui": "^9.10.1", - "@dhis2-ui/calendar": "10.0.3", + "@dhis2-ui/calendar": "^10.0.3", "@js-temporal/polyfill": "0.4.3", "core-js": "2.5.7", "i18next": "^20.5.0" diff --git a/src/core_modules/capture-core-utils/validators/form/age.validator.js b/src/core_modules/capture-core-utils/validators/form/age.validator.js deleted file mode 100644 index 8006279562..0000000000 --- a/src/core_modules/capture-core-utils/validators/form/age.validator.js +++ /dev/null @@ -1,76 +0,0 @@ -// @flow -import { isValidZeroOrPositiveInteger } from './integerZeroOrPositive.validator'; -import { isValidDate } from './date.validator'; -/** - * - * @export - * @param {string} value - * @returns {boolean} - */ - -type AgeValues = { - date?: ?string, - years?: ?string, - months?: ?string, - days?: ?string, -} - -const errorMessages = { - date: 'Please provide a valid date', - years: 'Please provide a valid positive integer', - months: 'Please provide a valid positive integer', - days: 'Please provide a valid positive integer', - -}; - -function isValidNumberPart(value: ?string) { - return !value || isValidZeroOrPositiveInteger(value); -} - -function validateNumbers(years: ?string, months: ?string, days: ?string) { - const errorResult = []; - - if (!isValidNumberPart(years)) { - errorResult.push({ years: errorMessages.years }); - } - if (!isValidNumberPart(months)) { - errorResult.push({ months: errorMessages.months }); - } - if (!isValidNumberPart(days)) { - errorResult.push({ days: errorMessages.days }); - } - - if (errorResult.length > 0) { - return { - valid: false, - // $FlowFixMe[exponential-spread] automated comment - errorMessage: errorResult.reduce((map, error) => ({ ...map, ...error }), {}), - }; - } - return { valid: true }; -} - -function validateDate(date: ?string, dateFormat: string) { - return (!date || isValidDate(date, dateFormat)) ? - { valid: true } : - { valid: false, errorMessage: { date: errorMessages.date } }; -} - -function isAllEmpty(value: AgeValues) { - return (!value.date && !value.years && !value.months && !value.days); -} - -export function isValidAge(value: AgeValues, dateFormat: string) { - if (isAllEmpty(value)) { - return false; - } - - const numberResult = validateNumbers( - value.years, - value.months, - value.days, - ); - - if (!numberResult.valid) return numberResult; - return validateDate(value.date, dateFormat); -} diff --git a/src/core_modules/capture-core-utils/validators/form/date.validator.js b/src/core_modules/capture-core-utils/validators/form/date.validator.js deleted file mode 100644 index fe14203f11..0000000000 --- a/src/core_modules/capture-core-utils/validators/form/date.validator.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import { parseDate } from '../../parsers'; -/** - * - * @export - * @param {string} value - * @param {string} format - * @returns {boolean} - */ -export function isValidDate(value: string, format: string) { - const parseData = parseDate(value, format); - return parseData.isValid; -} diff --git a/src/core_modules/capture-core-utils/validators/form/dateTime.validator.js b/src/core_modules/capture-core-utils/validators/form/dateTime.validator.js deleted file mode 100644 index a6de799acf..0000000000 --- a/src/core_modules/capture-core-utils/validators/form/dateTime.validator.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import { isValidDate } from './date.validator'; -import { isValidTime } from './time.validator'; - -type DateTimeValue = { - date?: ?string, - time?: ?string, -}; - -export function isValidDateTime(value: DateTimeValue, dateFormat: string) { - if (!value) return false; - const date = value.date; - const time = value.time; - - if (!date || !time) { - return false; - } - - return (isValidDate(date, dateFormat) && isValidTime(time)); -} diff --git a/src/core_modules/capture-core-utils/validators/form/index.js b/src/core_modules/capture-core-utils/validators/form/index.js index 65af548dde..3355778be3 100644 --- a/src/core_modules/capture-core-utils/validators/form/index.js +++ b/src/core_modules/capture-core-utils/validators/form/index.js @@ -1,7 +1,5 @@ // @flow export { hasValue } from './compulsory.validator'; -export { isValidDate } from './date.validator'; -export { isValidDateTime } from './dateTime.validator'; export { isValidEmail } from './email.validator'; export { isValidInteger } from './integer.validator'; export { isValidPositiveInteger } from './integerPositive.validator'; @@ -11,7 +9,6 @@ export { isValidNumber } from './number.validator'; export { isValidPercentage } from './percentage.validator'; export { isValidTime } from './time.validator'; export { isValidUrl } from './url.validator'; -export { isValidAge } from './age.validator'; export { isValidPhoneNumber } from './phone.validator'; export { isValidOrgUnit } from './orgUnit.validator'; export { isValidCoordinate } from './coordinate.validator'; diff --git a/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js b/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js index f62a5c32f0..6e5b37a2a2 100644 --- a/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js +++ b/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js @@ -12,11 +12,15 @@ import defaultClasses from './formBuilder.module.css'; import type { ErrorData, PostProcessErrorMessage } from './formbuilder.types'; import type { PluginContext } from '../FormFieldPlugin/FormFieldPlugin.types'; import { getValidators } from '../field/validators'; +import { validatorTypes } from '../field/validators/constants'; import type { DataElement } from '../../../metaData'; import type { QuerySingleResource } from '../../../utils/api'; export type ValidatorContainer = { - validator: (value: any, validationContext: ?Object) => boolean | Promise, + validator: (value: any, validationContext: ?Object, internalError?: ?{ + error?: ?string, + errorCode?: ?string, + }) => boolean | Promise, message: string, validatingMessage?: ?string, type?: ?string, @@ -36,7 +40,7 @@ export type FieldConfig = { type FieldUI = { touched?: ?boolean, valid?: ?boolean, - errorMessage?: ?string | Array, + errorMessage?: ?string | Array | Array<{[key: string]: string}>, errorType?: ?string, errorData?: ErrorData, validatingMessage?: ?string, @@ -89,11 +93,17 @@ type Props = { onPostProcessErrorMessage?: PostProcessErrorMessage, }; -type FieldCommitOptions = { +export type FieldCommitOptions = {| touched?: boolean, valid?: boolean, - error?: string | Array, -}; + error?: string | Array | Array<{[key: string]: string}>, + errorCode?: string, +|}; + +type FieldCommitOptionsExtended = {| + ...FieldCommitOptions, + plugin?: ?boolean, +|}; // container for handling async validations type FieldsValidatingPromiseContainer = { [fieldId: string]: ?{ cancelableValidatingPromise?: ?CancelablePromise, validatingCompleteUid: string } }; @@ -104,6 +114,7 @@ export class FormBuilder extends React.Component { value: any, validationContext: ?Object, onIsValidatingInternal: ?Function, + commitOptions?: ?FieldCommitOptions, ): Promise<{ valid: boolean, errorMessage?: ?string, errorType?: ?string }> { if (!validators || validators.length === 0) { return { @@ -115,9 +126,13 @@ export class FormBuilder extends React.Component { .reduce(async (passPromise, currentValidator) => { const pass = await passPromise; if (pass === true) { - let result = currentValidator.validator(value, validationContext); + let result = currentValidator.validator(value, + { error: commitOptions?.error, errorCode: commitOptions?.errorCode }, + validationContext); if (result instanceof Promise) { - result = onIsValidatingInternal ? onIsValidatingInternal(currentValidator.validatingMessage, result) : result; + result = onIsValidatingInternal ? + onIsValidatingInternal(currentValidator.validatingMessage, result) : + result; result = await result; } @@ -377,7 +392,7 @@ export class FormBuilder extends React.Component { commitFieldUpdateFromDataElement(fieldId: string, value: any, options?: ?FieldCommitOptions) { const { validators, onIsEqual } = this.getFieldProp(fieldId); - + // $FlowFixMe this.commitFieldUpdate({ fieldId, validators, onIsEqual }, value, options); } @@ -387,10 +402,10 @@ export class FormBuilder extends React.Component { const validators = getValidators(fieldMetadata, querySingleResource); // $FlowFixMe - Async handled in business logic - this.commitFieldUpdate({ fieldId, validators }, value, options); + this.commitFieldUpdate({ fieldId, validators }, value, { ...options, plugin: true }); } - async commitFieldUpdate({ fieldId, validators, onIsEqual }: FieldCommitConfig, value: any, options?: ?FieldCommitOptions) { + async commitFieldUpdate({ fieldId, validators, onIsEqual }: FieldCommitConfig, value: any, options?: ?FieldCommitOptionsExtended) { const { onUpdateFieldUIOnly, onUpdateField, @@ -428,49 +443,61 @@ export class FormBuilder extends React.Component { return fieldValidatingPromiseContainer.cancelableValidatingPromise.promise; }; + const updateField = ({ valid, errorMessage, errorType, errorData }) => { + onUpdateField( + value, + { + valid, + touched, + errorMessage, + errorType, + errorData, + }, + fieldId, + id, + fieldValidatingPromiseContainer.validatingCompleteUid, + ); + this.fieldsValidatingPromiseContainer[fieldId] = null; + }; + this.commitUpdateTriggeredForFields[fieldId] = true; - const updatePromise = FormBuilder.validateField( - { validators }, - value, - onGetValidationContext && onGetValidationContext(), - handleIsValidatingInternal, - ) - // $FlowFixMe[prop-missing] automated comment - .then(({ valid, errorMessage, errorType, errorData }) => { - onUpdateField( - value, - { - valid: options?.valid ?? valid, - touched, - errorMessage: options?.error ?? errorMessage, - errorType, - errorData, - }, - fieldId, - id, - fieldValidatingPromiseContainer.validatingCompleteUid, - ); - this.fieldsValidatingPromiseContainer[fieldId] = null; - }) - .catch((reason) => { - if (!reason || !isObject(reason) || !reason.isCanceled) { - log.error({ reason, fieldId, value }); - onUpdateField( - value, - { - valid: false, - touched: true, - errorMessage: i18n.t('error encountered during field validation'), - errorType: i18n.t('error'), - }, - fieldId, - id, - fieldValidatingPromiseContainer.validatingCompleteUid, - ); - this.fieldsValidatingPromiseContainer[fieldId] = null; - } - }); - await updatePromise; + + options?.plugin && (options.error || options.valid === false) ? + updateField({ + valid: false, + errorMessage: options.error, + errorType: validatorTypes.TYPE_BASE, + errorData: undefined }) : + (await FormBuilder.validateField( + { validators }, + value, + onGetValidationContext && onGetValidationContext(), + handleIsValidatingInternal, + // $FlowFixMe + options, + ) + // $FlowFixMe[prop-missing] automated comment + .then(({ valid, errorMessage, errorType, errorData }) => { + updateField({ valid, errorMessage, errorType, errorData }); + }) + .catch((reason) => { + if (!reason || !isObject(reason) || !reason.isCanceled) { + log.error({ reason, fieldId, value }); + onUpdateField( + value, + { + valid: false, + touched: true, + errorMessage: i18n.t('error encountered during field validation'), + errorType: i18n.t('error'), + }, + fieldId, + id, + fieldValidatingPromiseContainer.validatingCompleteUid, + ); + this.fieldsValidatingPromiseContainer[fieldId] = null; + } + })); } handleUpdateAsyncState = (fieldId: string, asyncStateToAdd: Object) => { diff --git a/src/core_modules/capture-core/components/D2Form/FormBuilder/formbuilder.types.js b/src/core_modules/capture-core/components/D2Form/FormBuilder/formbuilder.types.js index e5e5a78793..99b480e90d 100644 --- a/src/core_modules/capture-core/components/D2Form/FormBuilder/formbuilder.types.js +++ b/src/core_modules/capture-core/components/D2Form/FormBuilder/formbuilder.types.js @@ -7,10 +7,10 @@ export type ErrorData = { attributeValueExistsUnsaved?: ?boolean, }; export type PostProcessErrorMessage = ({ - errorMessage: string | Array, + errorMessage: string | Array | Array<{[key: string]: string}>, errorType: ?string, errorData?: ErrorData, id: string, fieldId: string, fieldLabel: string, -}) => Node; +}) => string | Array | Array<{[key: string]: string}> | Node; diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js index 9322ab0816..d7a24cb7f3 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfig.js @@ -11,11 +11,11 @@ export const getDateTimeFieldConfig = (metaData: MetaDataElement, options: Objec const props = createProps({ formHorizontal: options.formHorizontal, fieldLabelMediaBasedClass: options.fieldLabelMediaBasedClass, - dateWidth: options.formHorizontal ? 150 : '100%', - dateMaxWidth: options.formHorizontal ? 150 : 350, + dateWidth: options.formHorizontal ? '150px' : '100%', + dateMaxWidth: options.formHorizontal ? '150px' : '350px', orientation: options.formHorizontal ? orientations.VERTICAL : orientations.HORIZONTAL, shrinkDisabled: options.formHorizontal, - calendarWidth: options.formHorizontal ? 250 : 350, + calendarWidth: options.formHorizontal ? '250px' : '350px', popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal), }, options, metaData); diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js index cd8e1d1d99..86024056fc 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/dateTimeField/getDateTimeFieldConfigForCustomForm.js @@ -8,8 +8,8 @@ import type { QuerySingleResource } from '../../../../../utils/api/api.types'; export const getDateTimeFieldConfigForCustomForm = (metaData: MetaDataElement, options: Object, querySingleResource: QuerySingleResource) => { const props = createProps({ dateWidth: '100%', - dateMaxWidth: 350, - calendarWidth: 350, + dateMaxWidth: '350px', + calendarWidth: '350px', orientation: orientations.HORIZONTAL, shrinkDisabled: false, }, metaData); diff --git a/src/core_modules/capture-core/components/D2Form/field/validators/getValidators.js b/src/core_modules/capture-core/components/D2Form/field/validators/getValidators.js index be3c6b8e16..f9fb35be3c 100644 --- a/src/core_modules/capture-core/components/D2Form/field/validators/getValidators.js +++ b/src/core_modules/capture-core/components/D2Form/field/validators/getValidators.js @@ -34,7 +34,7 @@ type Validator = (value: any) => Promise | boolean | { valid: boolean, export type ValidatorContainer = { validator: Validator, - message: string, + message: string | Object, type?: string, validatingMessage?: string, } @@ -118,7 +118,7 @@ const validatorsForTypes = { type: validatorTypes.TYPE_BASE, }, { - validator: (value: string, allowFutureDate) => (allowFutureDate ? true : isValidNonFutureDate(value)), + validator: isValidNonFutureDate, type: validatorTypes.TYPE_EXTENDED, message: errorMessages.DATE_FUTURE_NOT_ALLOWED, }], @@ -212,14 +212,14 @@ function buildTypeValidators(metaData: DataElement | DateDataElement): ?Array ({ ...validatorContainer, - validator: (value: any) => { + validator: (value: any, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { if (!value && value !== 0 && value !== false) { return true; } const toValidateValue = isString(value) ? value.trim() : value; // $FlowFixMe dataElementTypes flow error - return validatorContainer.validator(toValidateValue, metaData.allowFutureDate); + return validatorContainer.validator(toValidateValue, internalComponentError); }, })); @@ -248,7 +248,7 @@ function buildUniqueValidator( ? [ { - validator: (value: any, contextProps: ?Object) => { + validator: (value: any, internalComponentError?: ?{error: ?string, errorCode: ?string}, contextProps: ?Object) => { if (!value && value !== 0 && value !== false) { return true; } diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js index 9a902bab07..00efbf978f 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js @@ -114,8 +114,7 @@ const getEnrollmentDateSettings = () => { calendarMaxMoment: !props.enrollmentMetadata.allowFutureEnrollmentDate ? moment() : undefined, }), getPropName: () => 'enrolledAt', - getValidatorContainers: (props: Object) => - getEnrollmentDateValidatorContainer(props.enrollmentMetadata.allowFutureEnrollmentDate), + getValidatorContainers: getEnrollmentDateValidatorContainer, getPassOnFieldData: () => true, getMeta: () => ({ placement: placements.TOP, @@ -164,8 +163,7 @@ const getIncidentDateSettings = () => { }), getPropName: () => 'occurredAt', getPassOnFieldData: () => true, - getValidatorContainers: (props: Object) => - getIncidentDateValidatorContainer(props.enrollmentMetadata.allowFutureIncidentDate), + getValidatorContainers: getIncidentDateValidatorContainer, getMeta: () => ({ placement: placements.TOP, section: sectionKeysForEnrollmentDataEntry.ENROLLMENT, diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/eventDate.validatorContainersGetter.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/eventDate.validatorContainersGetter.js index 3cb0a56c19..3d06861d5b 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/eventDate.validatorContainersGetter.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/eventDate.validatorContainersGetter.js @@ -1,14 +1,14 @@ // @flow -import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; +import { hasValue } from 'capture-core-utils/validators/form'; import { isValidDate } from '../../../../../utils/validators/form'; -const preValidateDate = (value?: ?string) => { +const preValidateDate = (value?: ?string, internalComponentError: ?{error?: ?string, errorCode?: ?string}) => { if (!value) { return true; } - return isValidDate(value); + return isValidDate(value, internalComponentError); }; export const getEventDateValidatorContainers = () => { diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/enrollmentDate.validatorContainersGetter.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/enrollmentDate.validatorContainersGetter.js index 731200e7b8..3fd678d77a 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/enrollmentDate.validatorContainersGetter.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/enrollmentDate.validatorContainersGetter.js @@ -1,30 +1,17 @@ // @flow -import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; -import moment from 'moment'; -import { parseDate } from '../../../../utils/converters/date'; - -const isValidEnrollmentDate = (value: string, isFutureDateAllowed: boolean) => { - const dateContainer = parseDate(value); - if (!dateContainer.isValid) { - return false; - } +import { hasValue } from 'capture-core-utils/validators/form'; +import { isValidDate, isValidNonFutureDate } from '../../../../utils/validators/form'; - if (isFutureDateAllowed) { +const isValidEnrollmentDate = (value: string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { + if (!value) { return true; } - const momentDate = dateContainer.momentDate; - const momentToday = moment(); - // $FlowFixMe -> if parseDate returns isValid true, there should always be a momentDate - const isNotFutureDate = momentDate.isSameOrBefore(momentToday); - return { - valid: isNotFutureDate, - message: i18n.t('A future date is not allowed'), - }; + return isValidDate(value, internalComponentError); }; -export const getEnrollmentDateValidatorContainer = (isFutureEnrollmentDateAllowed: boolean) => { +export const getEnrollmentDateValidatorContainer = () => { const validatorContainers = [ { validator: hasValue, @@ -32,9 +19,12 @@ export const getEnrollmentDateValidatorContainer = (isFutureEnrollmentDateAllowe i18n.t('A value is required'), }, { - validator: (value: string) => isValidEnrollmentDate(value, isFutureEnrollmentDateAllowed), + validator: isValidEnrollmentDate, message: i18n.t('Please provide a valid date'), }, + { validator: isValidNonFutureDate, + message: i18n.t('A date in the future is not allowed'), + }, ]; return validatorContainers; }; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/incidentDate.validatorContainerGetter.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/incidentDate.validatorContainerGetter.js index db3331542c..07aaadf567 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/incidentDate.validatorContainerGetter.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/fieldValidators/incidentDate.validatorContainerGetter.js @@ -1,31 +1,18 @@ // @flow -import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; -import moment from 'moment'; -import { parseDate } from '../../../../utils/converters/date'; - -const isValidIncidentDate = (value: string, isFutureDateAllowed: boolean) => { - const dateContainer = parseDate(value); - if (!dateContainer.isValid) { - return false; - } +import { hasValue } from 'capture-core-utils/validators/form'; +import { isValidDate, isValidNonFutureDate } from '../../../../utils/validators/form'; - if (isFutureDateAllowed) { +const isValidIncidentDate = (value: string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { + if (!value) { return true; } - const momentDate = dateContainer.momentDate; - const momentToday = moment(); - // $FlowFixMe -> if parseDate returns isValid true, there should always be a momentDate - const isNotFutureDate = momentDate.isSameOrBefore(momentToday); - return { - valid: isNotFutureDate, - message: i18n.t('A future date is not allowed'), - }; + return isValidDate(value, internalComponentError); }; -export const getIncidentDateValidatorContainer = (isFutureIncidentDateAllowed: boolean) => { +export const getIncidentDateValidatorContainer = () => { const validatorContainers = [ { validator: hasValue, @@ -33,9 +20,12 @@ export const getIncidentDateValidatorContainer = (isFutureIncidentDateAllowed: b i18n.t('A value is required'), }, { - validator: (value: string) => isValidIncidentDate(value, isFutureIncidentDateAllowed), + validator: isValidIncidentDate, message: i18n.t('Please provide a valid date'), }, + { validator: isValidNonFutureDate, + message: i18n.t('A date in the future is not allowed'), + }, ]; return validatorContainers; }; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js index 31afe2c4da..c5dcf70ab2 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js @@ -3,12 +3,12 @@ import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; import { isValidDate } from '../../../../../../utils/validators/form'; -const preValidateDate = (value?: ?string) => { +const preValidateDate = (value?: ?string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { if (!value) { return true; } - return isValidDate(value); + return isValidDate(value, internalComponentError); }; export const getEventDateValidatorContainers = () => { diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/DataEntryField.component.js b/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/DataEntryField.component.js index e20b69cca2..6d25a0c5ed 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/DataEntryField.component.js +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/DataEntryField.component.js @@ -36,6 +36,8 @@ type Props = { type Options = { touched?: ?boolean, + error?: ?string, + errorCode?: ?string, }; type ContainerProps = { @@ -72,7 +74,7 @@ class DataEntryFieldPlain extends React.Component { handleSet = (value: any, options?: ?Options) => { const { validatorContainers, onUpdateFieldInner, onUpdateField } = this.props; const validationError = - getValidationError(value, validatorContainers); + getValidationError(value, validatorContainers, { error: options?.error, errorCode: options?.errorCode }); onUpdateFieldInner(value, { isValid: !validationError, validationError, diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/dataEntryField.utils.js b/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/dataEntryField.utils.js index 322dfdd783..f42d00f85e 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/dataEntryField.utils.js +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryField/internal/dataEntryField.utils.js @@ -1,14 +1,16 @@ // @flow import i18n from '@dhis2/d2-i18n'; -type Validator = (value: any) => boolean | { valid: boolean, message: ?string }; +type Validator = (value: any, + internalComponentError?: ?{error: ?string, errorCode: ?string}) => + boolean | { valid: boolean, errorMessage?: ?string } | { valid: boolean, message?: ?string }; export type ValidatorContainer = { validator: Validator, message: string, }; -export function getValidationError(value: any, validatorContainers: ?Array) { +export function getValidationError(value: any, validatorContainers: ?Array, internalComponentError?: ?{error: ?string, errorCode: ?string}) { if (!validatorContainers) { return null; } @@ -16,13 +18,13 @@ export function getValidationError(value: any, validatorContainers: ?Array { const validator = validatorContainer.validator; - const result = validator(value); + const result = validator(value, internalComponentError); if (result === true || (result && result.valid)) { return false; } - message = (result && result.message) || validatorContainer.message; + message = (result && result.errorMessage) || (result && result.message) || validatorContainer.message; return true; }); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js index c92edaa872..2306b0854d 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/AgeField/AgeField.component.js @@ -3,7 +3,6 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { AgeField as UIAgeField } from 'capture-ui'; import moment from 'moment'; -import { withCalendarProps } from '../../HOC/withCalendarProps'; import { parseDate, convertMomentToDateFormatString } from '../../../../../utils/converters/date'; import { systemSettingsStore } from '../../../../../metaDataMemoryStores'; @@ -41,18 +40,10 @@ type Props = { innerInputInfo: string, innerInputValidating: string, }, - calendarTheme: Object, - calendarLocale: Object, - calendarOnConvertValueIn: Function, - calendarOnConvertValueOut: Function, } const AgeFieldPlain = (props: Props) => { const { - calendarTheme, - calendarLocale, - calendarOnConvertValueIn, - calendarOnConvertValueOut, ...passOnProps } = props; @@ -62,14 +53,10 @@ const AgeFieldPlain = (props: Props) => { onParseDate={parseDate} onGetFormattedDateStringFromMoment={convertMomentToDateFormatString} moment={moment} - dateCalendarTheme={calendarTheme} - dateCalendarLocale={calendarLocale} - dateCalendarOnConvertValueIn={calendarOnConvertValueIn} - dateCalendarOnConvertValueOut={calendarOnConvertValueOut} datePlaceholder={systemSettingsStore.get().dateFormat.toLowerCase()} {...passOnProps} /> ); }; -export const AgeField = withTheme()(withCalendarProps()(withStyles(getStyles)(AgeFieldPlain))); +export const AgeField = withTheme()(withStyles(getStyles)(AgeFieldPlain)); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js index f5e1f1ed62..8ed4bf9b8c 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateField/DateField.component.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { DateField as UIDateField } from 'capture-ui'; -import { withCalendarProps } from '../../../HOC/withCalendarProps'; import { systemSettingsStore } from '../../../../../../metaDataMemoryStores'; const getStyles = (theme: Theme) => ({ @@ -44,4 +43,4 @@ class DateFieldPlain extends React.Component { } } -export const DateField = withTheme()(withCalendarProps()(withStyles(getStyles)(DateFieldPlain))); +export const DateField = withTheme()(withStyles(getStyles)(DateFieldPlain)); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js index ac892e4aae..496d330d03 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateRangeField/DateRangeField.component.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { DateRangeField as UIDateRangeField } from 'capture-ui'; -import { withCalendarProps } from '../../../HOC/withCalendarProps'; const getStyles = (theme: Theme) => ({ innerInputError: { @@ -49,4 +48,4 @@ const DateRangeFieldPlain = (props: Props) => { ); }; -export const DateRangeField = withTheme()(withCalendarProps()(withStyles(getStyles)(DateRangeFieldPlain))); +export const DateRangeField = withTheme()(withStyles(getStyles)(DateRangeFieldPlain)); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js index 64590c2413..a8d7a72d82 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeField/DateTimeField.component.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { DateTimeField as UIDateTimeField } from 'capture-ui'; -import { withCalendarProps } from '../../../HOC/withCalendarProps'; const getStyles = (theme: Theme) => ({ innerInputError: { @@ -42,4 +41,4 @@ class DateTimeFieldPlain extends React.Component { } } -export const DateTimeField = withTheme()(withCalendarProps()(withStyles(getStyles)(DateTimeFieldPlain))); +export const DateTimeField = withTheme()(withStyles(getStyles)(DateTimeFieldPlain)); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js index c65b6fffd0..852b33525f 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/DateTimeRangeField/DateTimeRangeField.component.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { withStyles, withTheme } from '@material-ui/core/styles'; import { DateTimeRangeField as UIDateTimeRangeField } from 'capture-ui'; -import { withCalendarProps } from '../../../HOC/withCalendarProps'; const getStyles = (theme: Theme) => ({ innerInputError: { @@ -42,4 +41,4 @@ class DateTimeRangeFieldPlain extends React.Component { } } -export const DateTimeRangeField = withTheme()(withCalendarProps()(withStyles(getStyles)(DateTimeRangeFieldPlain))); +export const DateTimeRangeField = withTheme()(withStyles(getStyles)(DateTimeRangeFieldPlain)); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/getCalendarTheme.js b/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/getCalendarTheme.js deleted file mode 100644 index ba5ee04cbb..0000000000 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/DateAndTimeFields/getCalendarTheme.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow - -export const getCalendarTheme = (theme: Theme) => ({ - accentColor: theme.palette.secondary.main, - floatingNav: { - background: 'rgba(0, 30, 64, 0.8)', - chevron: 'rgb(145, 203, 193)', - color: 'white', - }, - headerColor: theme.palette.primary.main, - todayColor: theme.palette.secondary.main, - selectionColor: theme.palette.primary.main, - weekdayColor: theme.palette.primary.main, -}); diff --git a/src/core_modules/capture-core/components/FormFields/New/HOC/messages/withDisplayMessages.js b/src/core_modules/capture-core/components/FormFields/New/HOC/messages/withDisplayMessages.js index 3aedaa7379..2760ecf654 100644 --- a/src/core_modules/capture-core/components/FormFields/New/HOC/messages/withDisplayMessages.js +++ b/src/core_modules/capture-core/components/FormFields/New/HOC/messages/withDisplayMessages.js @@ -160,7 +160,6 @@ const getDisplayMessagesHOC = (InnerComponent: React.ComponentType) => validatingMessage, ...passOnProps } = this.props; - const messages = this.getMessage(errorMessage, warningMessage, infoMessage, validatingMessage); diff --git a/src/core_modules/capture-core/components/FormFields/New/HOC/withCalendarProps.js b/src/core_modules/capture-core/components/FormFields/New/HOC/withCalendarProps.js deleted file mode 100644 index 54480957dd..0000000000 --- a/src/core_modules/capture-core/components/FormFields/New/HOC/withCalendarProps.js +++ /dev/null @@ -1,69 +0,0 @@ -// @flow -import * as React from 'react'; -import { capitalizeFirstLetter } from 'capture-core-utils/string'; -import { parseDate, convertDateObjectToDateFormatString } from '../../../../utils/converters/date'; -import { getCalendarTheme } from '../Fields/DateAndTimeFields/getCalendarTheme'; -import { CurrentLocaleData } from '../../../../utils/localeData/CurrentLocaleData'; - -type Props = { - theme: Object, - calendarWidth?: ?number, - width: number, -} - -export const withCalendarProps = () => (InnerComponent: React.ComponentType) => - class CalendarPropsHOC extends React.Component { - static convertValueIntoCalendar(inputValue: ?string) { - if (!inputValue) { - return new Date(); - } - - const parseData = parseDate(inputValue); - if (!parseData.isValid) { - return new Date(); - } - - // $FlowFixMe[incompatible-use] automated comment - return parseData.momentDate.toDate(); - } - - static convertValueOutFromCalendar(changeDate: Date) { - return convertDateObjectToDateFormatString(changeDate); - } - - calendarTheme: Object; - calendarLocaleData: Object; - constructor(props: Props) { - super(props); - this.calendarTheme = getCalendarTheme(this.props.theme); - const projectLocaleData = CurrentLocaleData.get(); - const calculatedCalendarWidth = this.props.calendarWidth || this.props.width; - this.calendarLocaleData = { - locale: projectLocaleData.dateFnsLocale, - headerFormat: calculatedCalendarWidth >= 400 ? - projectLocaleData.calendarFormatHeaderLong : - projectLocaleData.calendarFormatHeaderShort, - weekdays: projectLocaleData.weekDaysShort.map(day => capitalizeFirstLetter(day)), - blank: projectLocaleData.selectDatesText, - todayLabel: { - long: projectLocaleData.todayLabelLong, - short: projectLocaleData.todayLabelShort, - }, - weekStartsOn: projectLocaleData.weekStartsOn, - }; - } - - render() { - const { theme, ...passOnProps } = this.props; - return ( - // $FlowFixMe - - ); - } - }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js index 8bda3e79ef..d1dcf08c6d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js @@ -3,12 +3,12 @@ import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; import { isValidDate } from '../../../../utils/validators/form'; -const preValidateDate = (value?: ?string) => { +const preValidateDate = (value?: ?string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { if (!value) { return true; } - return isValidDate(value); + return isValidDate(value, internalComponentError); }; export const getEventDateValidatorContainers = () => { diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js index 821d0c2752..410292ce17 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/eventDate.validatorContainersGetter.js @@ -1,14 +1,14 @@ // @flow -import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; +import { hasValue } from 'capture-core-utils/validators/form'; import { isValidDate } from '../../../../utils/validators/form'; -const preValidateDate = (value?: ?string) => { +const preValidateDate = (value?: ?string, internalComponentError: ?{error?: ?string, errorCode?: ?string}) => { if (!value) { return true; } - return isValidDate(value); + return isValidDate(value, internalComponentError); }; export const getEventDateValidatorContainers = () => { diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js index 8b62978b24..ca3f806f2d 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.js @@ -5,8 +5,6 @@ import withStyles from '@material-ui/core/styles/withStyles'; import { DateField } from 'capture-core/components/FormFields/New'; import { InfoBox } from '../InfoBox'; import type { Props } from './scheduleDate.types'; -import { convertStringToDateFormat } from '../../../utils/converters/date'; - const styles = { container: { @@ -36,7 +34,14 @@ const ScheduleDatePlain = ({ onSetFocus={() => {}} onFocus={() => { }} onRemoveFocus={() => { }} - onBlur={(e) => { setScheduleDate(convertStringToDateFormat(e)); }} + onBlur={(e, internalComponentError) => { + const { error } = internalComponentError; + if (error) { + setScheduleDate(''); + return; + } + setScheduleDate(e); + }} /> } void, + onBlurDateField: (value: string, internalComponentError?: {error: ?string, errorCode: ?string}) => void, saveAttempted: boolean, errorMessages: ErrorMessagesForRelatedStages, |} @@ -43,16 +42,16 @@ export const DateFieldForRelatedStages = ({ }: Props) => { const [touched, setTouched] = useState(false); - const onBlur = (event) => { + const onBlur = (event, internalComponentError) => { setTouched(true); - onBlurDateField(event); + onBlurDateField(event, internalComponentError); }; const shouldShowError = (touched || saveAttempted); return ( {}} onFocus={() => {}} diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/ScheduleInOrgUnit/ScheduleInOrgUnit.component.js b/src/core_modules/capture-core/components/WidgetRelatedStages/ScheduleInOrgUnit/ScheduleInOrgUnit.component.js index 33c556f0ed..310e5f7100 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/ScheduleInOrgUnit/ScheduleInOrgUnit.component.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/ScheduleInOrgUnit/ScheduleInOrgUnit.component.js @@ -3,7 +3,6 @@ import React from 'react'; import type { ComponentType } from 'react'; import { withStyles } from '@material-ui/core'; import { colors, spacers, spacersNum } from '@dhis2/ui'; -import { convertStringToDateFormat } from '../../../utils/converters/date'; import { DateFieldForRelatedStages, OrgUnitSelectorForRelatedStages } from '../FormComponents'; import type { ErrorMessagesForRelatedStages } from '../RelatedStagesActions'; import type { RelatedStageDataValueStates } from '../WidgetRelatedStages.types'; @@ -52,10 +51,11 @@ export const ScheduleInOrgUnitPlain = ({ scheduledLabel, classes, }: Props) => { - const onBlurDateField = (e) => { + const onBlurDateField = (e, internalComponentError) => { setRelatedStagesDataValues(prevValues => ({ ...prevValues, - scheduledAt: convertStringToDateFormat(e), + scheduledAt: e, + scheduledAtFormatError: internalComponentError, })); }; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.component.js b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.component.js index 722515d992..4c3013ca3c 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.component.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.component.js @@ -34,6 +34,7 @@ const WidgetRelatedStagesPlain = ({ const [relatedStageDataValues, setRelatedStageDataValues] = useState({ linkMode: undefined, scheduledAt: '', + scheduledAtFormatError: undefined, orgUnit: undefined, linkedEventId: undefined, }); @@ -62,10 +63,11 @@ const WidgetRelatedStagesPlain = ({ }; const formIsValid = useCallback(() => { - const { scheduledAt, orgUnit, linkedEventId, linkMode } = relatedStageDataValues; + const { scheduledAt, scheduledAtFormatError, orgUnit, linkedEventId, linkMode } = relatedStageDataValues; return relatedStageWidgetIsValid({ linkMode, scheduledAt, + scheduledAtFormatError, orgUnit, linkedEventId, setErrorMessages: addErrorMessage, diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js index 6f9288bb30..0274e06456 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js @@ -24,6 +24,7 @@ export type Props = {| export type RelatedStageDataValueStates = {| linkMode: ?$Keys, scheduledAt: string, + scheduledAtFormatError: ?{error: ?string, errorCode: ?string}, orgUnit: ?{ path: string, id: string, diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.js b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.js index b02e1f5ae5..8a12223d6b 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.js @@ -1,29 +1,36 @@ // @flow import i18n from '@dhis2/d2-i18n'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; -import { isValidDate, isValidOrgUnit } from '../../../../capture-core-utils/validators/form'; +import { isValidOrgUnit } from '../../../../capture-core-utils/validators/form'; +import { isValidDate } from '../../../../capture-core/utils/validators/form'; import { actions as RelatedStageModes } from '../constants'; type Props = { scheduledAt: ?string, + scheduledAtFormatError: ?{error: ?string, errorCode: ?string}, orgUnit: ?Object, linkedEventId: ?string, setErrorMessages: (messages: Object) => void, }; -export const isScheduledDateValid = (scheduledDate: string) => { - const dateFormat = systemSettingsStore.get().dateFormat; - return isValidDate(scheduledDate, dateFormat); +export const isScheduledDateValid = (scheduledDate: ?string, scheduledAtFormatError: ?{error: ?string, errorCode: ?string}) => { + if (!scheduledDate) { + return { valid: false, errorMessage: i18n.t('Please enter a date') }; + } + const { valid, errorMessage } = isValidDate(scheduledDate, scheduledAtFormatError); + return { + valid, + errorMessage, + }; }; const scheduleInOrgUnit = (props) => { - const { scheduledAt, orgUnit, setErrorMessages } = props ?? {}; - const scheduledAtIsValid = !!scheduledAt && isScheduledDateValid(scheduledAt); + const { scheduledAt, scheduledAtFormatError, orgUnit, setErrorMessages } = props ?? {}; + const { valid: scheduledAtIsValid, errorMessage } = isScheduledDateValid(scheduledAt, scheduledAtFormatError); const orgUnitIsValid = isValidOrgUnit(orgUnit); if (!scheduledAtIsValid) { setErrorMessages({ - scheduledAt: i18n.t('Please provide a valid date'), + scheduledAt: errorMessage, }); } else { setErrorMessages({ diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.js b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.js index 21e7693f2f..f9f63ac48c 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.js @@ -8,6 +8,7 @@ import { ValidationFunctionsByLinkMode } from './ValidationFunctions'; export const relatedStageWidgetIsValid = ({ linkMode, scheduledAt, + scheduledAtFormatError, orgUnit, linkedEventId, setErrorMessages, @@ -25,6 +26,7 @@ export const relatedStageWidgetIsValid = ({ return validationFunction({ scheduledAt, + scheduledAtFormatError, orgUnit, linkedEventId, setErrorMessages, diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.js b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.js index c97648e958..1c5e56a676 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.js @@ -5,6 +5,7 @@ import { actions as LinkModes } from '../constants'; export type RelatedStageIsValidProps = {| linkMode: ?$Keys, scheduledAt: ?string, + scheduledAtFormatError: ?{error: ?string, errorCode: ?string}, orgUnit: ?{ id: string, name: string, diff --git a/src/core_modules/capture-core/converters/formToClient.js b/src/core_modules/capture-core/converters/formToClient.js index adca6b5b72..61c286d4d2 100644 --- a/src/core_modules/capture-core/converters/formToClient.js +++ b/src/core_modules/capture-core/converters/formToClient.js @@ -24,8 +24,8 @@ function convertDateTime(formValue: DateTimeValue): ?string { const hours = momentTime.hour(); const minutes = momentTime.minute(); - const parsedDate = parseDate(editedDate); - if (!parsedDate.isValid) return null; + const parsedDate = editedDate ? parseDate(editedDate) : null; + if (!(parsedDate && parsedDate.isValid)) return null; // $FlowFixMe[incompatible-type] automated comment const momentDateTime: moment$Moment = parsedDate.momentDate; momentDateTime.hour(hours); diff --git a/src/core_modules/capture-core/utils/validators/form/ageValidator.js b/src/core_modules/capture-core/utils/validators/form/ageValidator.js index f2653017fc..3986f22ebb 100644 --- a/src/core_modules/capture-core/utils/validators/form/ageValidator.js +++ b/src/core_modules/capture-core/utils/validators/form/ageValidator.js @@ -1,8 +1,74 @@ // @flow -import { isValidAge as isValidAgeCore } from 'capture-core-utils/validators/form'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; +import i18n from '@dhis2/d2-i18n'; +import { isValidZeroOrPositiveInteger } from 'capture-core-utils/validators/form'; +import { isValidDate } from './dateValidator'; -export function isValidAge(value: Object) { - const format = systemSettingsStore.get().dateFormat; - return isValidAgeCore(value, format); +type AgeValues = { + date?: ?string, + years?: ?string, + months?: ?string, + days?: ?string, +} + +const errorMessages = { + date: i18n.t('Please provide a valid date'), + years: i18n.t('Please provide a valid positive integer'), + months: i18n.t('Please provide a valid positive integer'), + days: i18n.t('Please provide a valid positive integer'), + +}; + +function isValidNumberPart(value: ?string) { + return !value || isValidZeroOrPositiveInteger(value); +} + +function validateNumbers(years: ?string, months: ?string, days: ?string) { + const errorResult = []; + + if (!isValidNumberPart(years)) { + errorResult.push({ years: errorMessages.years }); + } + if (!isValidNumberPart(months)) { + errorResult.push({ months: errorMessages.months }); + } + if (!isValidNumberPart(days)) { + errorResult.push({ days: errorMessages.days }); + } + + if (errorResult.length > 0) { + return { + valid: false, + // $FlowFixMe[exponential-spread] automated comment + errorMessage: errorResult.reduce((map, error) => ({ ...map, ...error }), {}), + }; + } + return { valid: true }; +} + +function validateDate(date: ?string, internalComponentError?: ?{error: ?string, errorCode: ?string}) { + const { valid } = isValidDate(date, internalComponentError); + return valid ? + { valid: true } : + { valid: false, errorMessage: { date: errorMessages.date } }; +} + +function isAllEmpty(value: AgeValues) { + return (!value.date && !value.years && !value.months && !value.days); +} + + +export function isValidAge(value: Object, internalComponentError?: ?{error: ?string, errorCode: ?string}) { + if (isAllEmpty(value)) { + return false; + } + + const numberResult = validateNumbers( + value.years, + value.months, + value.days, + ); + + if (!numberResult.valid) return numberResult; + + return validateDate(value.date, internalComponentError); } diff --git a/src/core_modules/capture-core/utils/validators/form/dateTimeValidator.js b/src/core_modules/capture-core/utils/validators/form/dateTimeValidator.js index 5db8270a2b..43adf8627c 100644 --- a/src/core_modules/capture-core/utils/validators/form/dateTimeValidator.js +++ b/src/core_modules/capture-core/utils/validators/form/dateTimeValidator.js @@ -1,8 +1,62 @@ // @flow -import { isValidDateTime as isValidDateTimeCore } from 'capture-core-utils/validators/form'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; +import i18n from '@dhis2/d2-i18n'; +import { isValidTime } from 'capture-core-utils/validators/form'; +import { isValidDate } from './dateValidator'; -export function isValidDateTime(value: Object) { - const dateFormat = systemSettingsStore.get().dateFormat; - return isValidDateTimeCore(value, dateFormat); +type DateTimeValue = { + date?: ?string, + time?: ?string, +}; + +type ValidationResult = { + valid: boolean, + errorMessage?: { + timeError?: ?string, + dateError?: ?string + } +}; + +const CUSTOM_VALIDATION_MESSAGES = { + INVALID_TIME: i18n.t('Please enter a valid time'), + MISSING_TIME: i18n.t('Please enter a time'), + MISSING_DATE: i18n.t('Please enter a date'), +}; + +export function isValidDateTime(value: DateTimeValue, + internalComponentError?: ?{error: ?string, errorCode: ?string}): ValidationResult { + if (!value) { + return { valid: true }; + } + + const { date, time } = value; + let dateError = ''; + let timeError = ''; + let isValid = true; + + if (!date) { + dateError = CUSTOM_VALIDATION_MESSAGES.MISSING_DATE; + isValid = false; + } else { + const dateValidation = isValidDate(date, internalComponentError); + if (!dateValidation.valid) { + dateError = dateValidation?.errorMessage; + isValid = false; + } + } + + if (!time) { + timeError = CUSTOM_VALIDATION_MESSAGES.MISSING_TIME; + isValid = false; + } else if (!isValidTime(time)) { + timeError = CUSTOM_VALIDATION_MESSAGES.INVALID_TIME; + isValid = false; + } + + return { + valid: isValid, + errorMessage: { + timeError, + dateError, + }, + }; } diff --git a/src/core_modules/capture-core/utils/validators/form/dateValidator.js b/src/core_modules/capture-core/utils/validators/form/dateValidator.js index 65416c8532..ef12190f5b 100644 --- a/src/core_modules/capture-core/utils/validators/form/dateValidator.js +++ b/src/core_modules/capture-core/utils/validators/form/dateValidator.js @@ -1,8 +1,20 @@ // @flow -import { isValidDate as isValidDateCore } from 'capture-core-utils/validators/form'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; -export function isValidDate(value: string) { - const format = systemSettingsStore.get().dateFormat; - return isValidDateCore(value, format); +export function isValidDate(value: ?string, internalComponentError?: ?{error: ?string, errorCode: ?string}) { + if (!value) { + return { valid: false, errorMessage: null }; + } + + if (internalComponentError && internalComponentError?.errorCode === 'INVALID_DATE_MORE_THAN_MAX') { + return { valid: true, errorMessage: null }; + } + + if (internalComponentError?.error) { + return { + valid: false, + errorMessage: internalComponentError?.error, + }; + } + + return { valid: true, errorMessage: null }; } diff --git a/src/core_modules/capture-core/utils/validators/form/isValidNonFutureDate.js b/src/core_modules/capture-core/utils/validators/form/isValidNonFutureDate.js index 3827f2ee12..a2ad362fb6 100644 --- a/src/core_modules/capture-core/utils/validators/form/isValidNonFutureDate.js +++ b/src/core_modules/capture-core/utils/validators/form/isValidNonFutureDate.js @@ -1,18 +1,21 @@ // @flow import i18n from '@dhis2/d2-i18n'; -import moment from 'moment'; -import { parseDate } from '../../converters/date'; -export const isValidNonFutureDate = (value: string) => { - const { isValid, momentDate } = parseDate(value); +const CUSTOM_VALIDATION_MESSAGES = { + INVALID_DATE_MORE_THAN_MAX: i18n.t('A date in the future is not allowed'), +}; + +export const isValidNonFutureDate = (value: string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { + if (!value) { + return true; + } - if (!isValid) { - return isValid; + if (internalComponentError && internalComponentError?.errorCode === 'INVALID_DATE_MORE_THAN_MAX') { + return { + valid: false, + errorMessage: CUSTOM_VALIDATION_MESSAGES.INVALID_DATE_MORE_THAN_MAX, + }; } - return { - // $FlowFixMe -> if parseDate returns isValid true, there should always be a momentDate - valid: momentDate.isSameOrBefore(moment()), - message: i18n.t('A future date is not allowed'), - }; + return true; }; diff --git a/src/core_modules/capture-ui/AgeField/AgeField.component.js b/src/core_modules/capture-ui/AgeField/AgeField.component.js index ccf1bbaad9..a125ea8900 100644 --- a/src/core_modules/capture-ui/AgeField/AgeField.component.js +++ b/src/core_modules/capture-ui/AgeField/AgeField.component.js @@ -26,11 +26,17 @@ type InputMessageClasses = { } type DateParser = (value: string) => { isValid: boolean, momentDate: any }; + type DateStringFromMomentFormatter = (momentValue: Object) => string; +type ValidationOptions = { + error?: ?string, + errorCode?: ?string, +}; + type Props = { value: ?AgeValues, - onBlur: (value: ?AgeValues) => void, + onBlur: (value: ?AgeValues, options: ?ValidationOptions) => void, onChange: (value: ?AgeValues) => void, onRemoveFocus: () => void, orientation: $Values, @@ -141,7 +147,7 @@ class D2AgeFieldPlain extends Component { this.props.onBlur(calculatedValues); } - handleDateBlur = (date: ?string) => { + handleDateBlur = (date: ?string, options: ?ValidationOptions) => { const { onParseDate, onGetFormattedDateStringFromMoment, onRemoveFocus, moment } = this.props; onRemoveFocus && onRemoveFocus(); const calculatedValues = date ? getCalculatedValues( @@ -149,7 +155,7 @@ class D2AgeFieldPlain extends Component { onParseDate, onGetFormattedDateStringFromMoment, moment) : null; - this.props.onBlur(calculatedValues); + this.props.onBlur(calculatedValues, options); } renderMessage = (key: string) => { @@ -200,17 +206,13 @@ class D2AgeFieldPlain extends Component { value, onBlur, shrinkDisabled, - dateCalendarOnConvertValueIn, - dateCalendarOnConvertValueOut, dateCalendarWidth, - datePopupAnchorPosition, - dateCalendarTheme, - dateCalendarLocale, datePlaceholder, moment, onParseDate, ...passOnProps } = this.props; + const dateInputContainerClass = classNames( { [defaultClasses.ageDateInputContainerHorizontal]: !isVertical }, ); @@ -222,11 +224,6 @@ class D2AgeFieldPlain extends Component { value={currentValues.date} onChange={date => onChange({ ...currentValues, date })} calendarWidth={dateCalendarWidth} - popupAnchorPosition={datePopupAnchorPosition} - calendarTheme={dateCalendarTheme} - calendarLocale={dateCalendarLocale} - calendarOnConvertValueIn={dateCalendarOnConvertValueIn} - calendarOnConvertValueOut={dateCalendarOnConvertValueOut} placeholder={datePlaceholder} {...passOnProps} /> diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js index c3d3517db0..b48ee77ae5 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateField/Date.component.js @@ -1,144 +1,62 @@ // @flow -import React, { createRef } from 'react'; -import { DatePopup } from './DatePopup.component'; -import { DateCalendar } from './DateCalendar.component'; -import { lowerCaseFirstLetter } from '../../internal/utils/string/lowerCaseFirstLetter'; -import { DateInput } from '../../internal/DateInput/DateInput.component'; +import React from 'react'; +import { CalendarInput } from '@dhis2/ui'; +import { systemSettingsStore } from '../../../capture-core/metaDataMemoryStores'; + +type ValidationOptions = { + error?: ?string, + errorCode?: ?string, +}; type Props = { - value: ?string, + value: ?Object, width: number, - maxWidth?: ?number, - calendarWidth?: ?number, - calendarHeight?: ?number, - inputWidth?: ?number, + maxWidth?: ?string, + calendarWidth?: ?string, + inputWidth?: ?string, disabled?: ?boolean, - onBlur: (value: string) => void, + onBlur: (value: Object, options: ValidationOptions) => void, onFocus?: ?() => void, onDateSelectedFromCalendar?: () => void, + calendar?: string, + placeholder?: string, + label?: string, + calendarMaxMoment?: any, + innerMessage?: any }; +type Validation = {| + validationCode: ?string, + validationText: ?string, + error?: boolean, + valid: boolean, +|}; + type State = { - popoverOpen: boolean, + calendarError: ?Validation, }; -export class DateField extends React.Component { - static splitPassOnProps(passOnProps: ?Object) { - const splittedProps = { - input: {}, - popup: {}, - calendar: {}, - }; - - if (!passOnProps) { - return splittedProps; - } - - return Object - .keys(passOnProps) - .reduce((accSplittedProps, propKey) => { - let propContainer; - if (propKey.startsWith(DateField.propContainers.CALENDAR)) { - propContainer = DateField.propContainers.CALENDAR; - } else if (propKey.startsWith(DateField.propContainers.POPUP)) { - propContainer = DateField.propContainers.POPUP; - } else { - propContainer = DateField.propContainers.INPUT; - } - - const outputKey = lowerCaseFirstLetter(propKey.replace(propContainer, '')); - accSplittedProps[propContainer][outputKey] = passOnProps[propKey]; - return accSplittedProps; - }, splittedProps); - } +const formatDate = (date: any, dateFormat: string): ?string => + (dateFormat === 'dd-MM-yyyy' ? date?.format('DD-MM-YYYY') : date?.format('YYYY-MM-DD')); - containerInstance: ?HTMLElement; - handleTextFieldFocus: () => void; - handleDateSelected: (value: string) => void; - handleTextFieldBlur: (event: SyntheticEvent) => void; - hidePopover: () => void; - handleDocumentClick: (event: MouseEvent) => void; - calendarWrapperDOMElementRef: { current: ?HTMLDivElement }; +export class DateField extends React.Component { + handleDateSelected: (value: {calendarDateString: string}) => void; constructor(props: Props) { super(props); - this.state = { - popoverOpen: false, - }; - - this.handleTextFieldFocus = this.handleTextFieldFocus.bind(this); this.handleDateSelected = this.handleDateSelected.bind(this); - this.handleTextFieldBlur = this.handleTextFieldBlur.bind(this); - this.hidePopover = this.hidePopover.bind(this); - this.handleDocumentClick = this.handleDocumentClick.bind(this); - - this.calendarWrapperDOMElementRef = createRef(); } - componentWillUnmount() { - document.removeEventListener('click', this.handleDocumentClick); - } - - static propContainers = { - CALENDAR: 'calendar', - POPUP: 'popup', - INPUT: 'input', - }; - - handleTextFieldFocus() { - document.removeEventListener('click', this.handleDocumentClick); - - this.setState({ - popoverOpen: true, - }); + handleDateSelected(value: { calendarDateString: string, validation: Validation}) { + const { calendarDateString: date, validation } = value || {}; + this.props.onBlur( + date, { + error: validation?.validationText, + errorCode: validation?.validationCode, + }); - this.props.onFocus && this.props.onFocus(); - } - - handleDateSelected(value: string) { - this.props.onBlur(value); - this.hidePopover(); this.props.onDateSelectedFromCalendar && this.props.onDateSelectedFromCalendar(); - document.removeEventListener('click', this.handleDocumentClick); - } - - handleDocumentClick({ target }: MouseEvent) { - const calendarWrapperDOMElement = this.calendarWrapperDOMElementRef.current; - - if (!calendarWrapperDOMElement) { - throw Error('calendar wrapper DOM element not found'); - } - - if (target === calendarWrapperDOMElement || - (target instanceof Node && calendarWrapperDOMElement.contains(target))) { - return; - } - - this.hidePopover(); - document.removeEventListener('click', this.handleDocumentClick); - } - - handleTextFieldBlur({ relatedTarget, currentTarget }: SyntheticFocusEvent) { - const calendarWrapperDOMElement = this.calendarWrapperDOMElementRef.current; - - if (!calendarWrapperDOMElement) { - throw Error('calendar wrapper DOM element not found'); - } - - if (relatedTarget === calendarWrapperDOMElement || - (relatedTarget instanceof Node && calendarWrapperDOMElement.contains(relatedTarget))) { - document.addEventListener('click', this.handleDocumentClick); - } else { - this.props.onBlur(currentTarget.value); - this.hidePopover(); - } - } - - hidePopover() { - this.setState({ - popoverOpen: false, - }); } render() { @@ -146,61 +64,43 @@ export class DateField extends React.Component { width, maxWidth, calendarWidth, - calendarHeight, inputWidth, - onBlur, - onFocus, - onDateSelectedFromCalendar, - ...passOnProps + calendar, + calendarMaxMoment, + value, + innerMessage, } = this.props; - const { popoverOpen } = this.state; + const calculatedInputWidth = inputWidth || width; const calculatedCalendarWidth = calendarWidth || width; - const splittedPassOnProps = DateField.splitPassOnProps(passOnProps); - const calculatedCalendarHeight = calendarHeight || 350; + const calendarType = calendar || 'gregory'; + const format = systemSettingsStore.get().dateFormat; + const errorProps = innerMessage && innerMessage.messageType === 'error' + ? { error: !!innerMessage.message?.dateInnerErrorMessage, + validationText: innerMessage.message?.dateInnerErrorMessage } + : {}; return (
{ this.containerInstance = containerInstance; }} style={{ width, maxWidth, }} > - { /* // $FlowFixMe */} - {/* $FlowFixMe[prop-missing] automated comment */} - -
- { /* // $FlowFixMe */} - {/* $FlowFixMe[prop-missing] automated comment */} - - { /* // $FlowFixMe */} - {/* $FlowFixMe[prop-missing] automated comment */} - - -
); } diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/DateCalendar.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/DateCalendar.component.js deleted file mode 100644 index 73fb27a7c6..0000000000 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/DateCalendar.component.js +++ /dev/null @@ -1,98 +0,0 @@ -// @flow -/* eslint-disable class-methods-use-this */ -import React, { Component } from 'react'; -import moment from 'moment'; -import InfiniteCalendar from '@joakim_sm/react-infinite-calendar'; -import '@joakim_sm/react-infinite-calendar/styles.css'; -import './customStyles.css'; - -type Props = { - onDateSelected: (value: any) => void, - value?: ?string, - minMoment?: Object, - maxMoment?: Object, - currentWidth: number, - height?: ?number, - classes: Object, - displayOptions?: ?Object, - calendarTheme: Object, - onConvertValueIn: (inputValue: ?string) => Date, - onConvertValueOut: (date: Date) => string, -}; - -export class DateCalendar extends Component { - handleChange: (e: any, dates: ?Array) => void; - displayOptions: Object; - - constructor(props: Props) { - super(props); - this.handleChange = this.handleChange.bind(this); - - this.displayOptions = { - ...DateCalendar.displayOptions, - ...this.props.displayOptions, - }; - } - - shouldComponentUpdate() { - return false; - } - - static displayOptions = { - showHeader: true, - showMonthsForYears: false, - }; - - handleChange(changeDate: Date) { - const changeDateInLocalFormat = this.props.onConvertValueOut(changeDate); - this.props.onDateSelected(changeDateInLocalFormat); - } - - getValue(inputValue: ?string) { - return this.props.onConvertValueIn(inputValue); - } - - getMinMaxProps() { - const { minMoment = moment('1900-01-01'), maxMoment = moment('2099-12-31') } = this.props; - - const minDate = minMoment.toDate(); - const maxDate = maxMoment.toDate(); - - return { - min: minDate, - minDate, - max: maxDate, - maxDate, - }; - } - - render() { - const { - value, - classes, - currentWidth, - height, - minMoment, - maxMoment, - onDateSelected, - displayOptions, - ...passOnProps - } = this.props; - - return ( -
- { /* $FlowFixMe */} - -
- ); - } -} diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/DatePopup.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/DatePopup.component.js deleted file mode 100644 index db9e3aef4a..0000000000 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/DatePopup.component.js +++ /dev/null @@ -1,87 +0,0 @@ -// @flow -import * as React from 'react'; -import { anchorPositions, modes, absoluteDirections } from './datePopup.const'; -import defaultClasses from './datePopup.module.css'; - -type Props = { - open: boolean, - children: React.Node, - anchorPosition?: $Values, - mode?: $Values, - absoluteDirection: $Values, - inputWidth: number, - calendarWidth: number, - inputUsesFloatingLabel: boolean, -}; - -export class DatePopup extends React.Component { - getAbsoluteBottom() { - const inputUsesFloatingLabel = this.props.inputUsesFloatingLabel; - return inputUsesFloatingLabel ? 60 : 40; - } - getAbsoluteVerticalPosition() { - const absoluteDirection = this.props.absoluteDirection; - return absoluteDirection === absoluteDirections.UP ? { bottom: this.getAbsoluteBottom() } : { top: 0 }; - } - calculateMarginLeftInline() { - const { inputWidth, calendarWidth } = this.props; - return calendarWidth - inputWidth; - } - getPopupStyle() { - const { anchorPosition, mode } = this.props; - - let calendarStyle; - if (anchorPosition === anchorPositions.RIGHT) { - calendarStyle = this.getRightCalendarStyle(mode); - } else if (anchorPosition === anchorPositions.CENTER) { - calendarStyle = this.getCenterCalendarStyle(mode); - } else { - calendarStyle = this.getLeftCalendarStyle(mode); - } - return calendarStyle; - } - - getRightCalendarStyle = (mode: ?$Values) => - (mode === modes.INLINE ? - { marginLeft: `-${this.calculateMarginLeftInline()}px` } : - { ...this.getAbsoluteVerticalPosition(), right: 0 } - ); - - getLeftCalendarStyle = (mode: ?$Values) => - (mode === modes.INLINE ? { } : { ...this.getAbsoluteVerticalPosition(), left: 0 }); - - getCenterCalendarStyle = (mode: ?$Values) => - (mode === modes.INLINE ? - { marginLeft: `-${(this.calculateMarginLeftInline() / 2)}px` } : - { ...this.getAbsoluteVerticalPosition(), left: '50%', transform: 'translate(-50%, 0)' } - ); - - render() { - const { - open, - mode, - children, - } = this.props; - - if (!open) { - return null; - } - - const containerClasses = mode === modes.INLINE ? defaultClasses.containerInline : defaultClasses.containerAbsolute; - const calendarClasses = mode === modes.INLINE ? defaultClasses.calendarInline : defaultClasses.calendarAbsolute; - const calendarStyle = this.getPopupStyle(); - - return ( -
-
- {children} -
-
- ); - } -} diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.const.js b/src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.const.js deleted file mode 100644 index 592588264e..0000000000 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.const.js +++ /dev/null @@ -1,17 +0,0 @@ -// @flow - -export const anchorPositions = { - LEFT: 'left', - RIGHT: 'right', - CENTER: 'center', -}; - -export const absoluteDirections = { - UP: 'up', - DOWN: 'down', -}; - -export const modes = { - ABSOLUTE: 'absolute', - INLINE: 'inline', -}; diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.module.css b/src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.module.css deleted file mode 100644 index 23ae546712..0000000000 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateField/datePopup.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.containerAbsolute { - position: relative; -} - -.calendarAbsolute { - position: absolute; - z-index: 201; -} \ No newline at end of file diff --git a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js index 7ee422ba37..a97077e1ff 100644 --- a/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js +++ b/src/core_modules/capture-ui/DateAndTimeFields/DateTimeField/DateTime.component.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import i18n from '@dhis2/d2-i18n'; import classNames from 'classnames'; import defaultClasses from './dateTime.module.css'; - import { orientations } from '../../constants/orientations.const'; import { DateTimeDate } from '../../internal/DateTimeInput/DateTimeDate.component'; import { DateTimeTime } from '../../internal/DateTimeInput/DateTimeTime.component'; @@ -14,24 +13,28 @@ type Value = { }; type Props = { - onBlur: (value: ?Value, options: Object) => void, + onBlur: (value: ?Value, options: Object, internalError: Object) => void, onChange: (value: ?Value) => void, value: Value, - dateMaxWidth: any, - dateWidth: any, + dateMaxWidth: string, + dateWidth: string, calendarWidth?: ?number, orientation: $Values, - calendarTheme: Object, - calendarLocale: Object, - calendarOnConvertValueIn: Function, - calendarOnConvertValueOut: Function, - popupAnchorPosition?: ?any, classes: Object, dateLabel: string, timeLabel: string, + innerMessage: Object +}; + +type State = { + dateError: ?{ + error?: ?string, + errorCode?: ?string + }, }; -export class DateTimeField extends Component { + +export class DateTimeField extends Component { handleTimeChange: (timeValue: string) => void; handleDateChange: (dateValue: string) => void; handleTimeBlur: (timeValue: string) => void; @@ -45,6 +48,9 @@ export class DateTimeField extends Component { constructor(props: Props) { super(props); + this.state = { + dateError: { error: null, errorCode: null }, + }; this.handleTimeChange = this.handleTimeChange.bind(this); this.handleDateChange = this.handleDateChange.bind(this); this.handleTimeBlur = this.handleTimeBlur.bind(this); @@ -71,20 +77,32 @@ export class DateTimeField extends Component { const currentValue = this.getValue(); this.handleBlur({ time: timeValue, - date: currentValue.date, - }, !!currentValue.date); + date: this.props.value?.date, + }, { + touched: !!currentValue.date, + error: this.state.dateError?.error, + errorCode: this.state.dateError?.errorCode, + }); } - handleDateBlur(dateValue: string) { + handleDateBlur(dateValue: string, options: ?Object) { this.touchedFields.add('dateTouched'); - const currentValue = this.getValue(); - this.handleBlur({ - time: currentValue.time, - date: dateValue, - }, !!currentValue.time); + this.setState(() => ({ + dateError: { error: options?.error, errorCode: options?.errorCode }, + }), () => { + const currentValue = this.getValue(); + this.handleBlur({ + time: currentValue.time, + date: dateValue, + }, { + touched: !!currentValue.date, + error: this.state.dateError?.error, + errorCode: this.state.dateError?.errorCode, + }); + }); } - handleBlur(value: Value, otherFieldHasValue: boolean) { + handleBlur(value: Value, otherFieldHasValue: Object) { const onBlur = this.props.onBlur; const touched = this.touchedFields.size === 2; if (!value.date && !value.time) { @@ -94,7 +112,9 @@ export class DateTimeField extends Component { return; } onBlur(value, { - touched: touched || otherFieldHasValue, + touched: touched || otherFieldHasValue.touched, + error: otherFieldHasValue?.error, + errorCode: otherFieldHasValue?.errorCode, }); } @@ -106,18 +126,15 @@ export class DateTimeField extends Component { dateMaxWidth, dateWidth, calendarWidth, - popupAnchorPosition, - calendarTheme, - calendarLocale, - calendarOnConvertValueIn, - calendarOnConvertValueOut, classes, orientation, onBlur, dateLabel, timeLabel, onChange, + innerMessage, ...passOnProps } = this.props; + const isVertical = orientation === orientations.VERTICAL; const currentValue = this.getValue(); const dateValue = currentValue.date; @@ -144,25 +161,26 @@ export class DateTimeField extends Component { onChange={this.handleDateChange} onBlur={this.handleDateBlur} label={dateLabel} - calendarTheme={calendarTheme} - popupAnchorPosition={popupAnchorPosition} - calendarLocale={calendarLocale} - calendarOnConvertValueIn={calendarOnConvertValueIn} - calendarOnConvertValueOut={calendarOnConvertValueOut} classes={classes} + innerMessage={innerMessage} {...passOnProps} /> +
{innerMessage?.message?.dateError}
- {/* $FlowFixMe[cannot-spread-inexact] automated comment */} - +
+ {/* $FlowFixMe[cannot-spread-inexact] automated comment */} + +
{innerMessage?.message?.timeError}
+
); diff --git a/src/core_modules/capture-ui/internal/AgeInput/AgeDateInput.component.js b/src/core_modules/capture-ui/internal/AgeInput/AgeDateInput.component.js index 69b6018ca7..4940f4d3fc 100644 --- a/src/core_modules/capture-ui/internal/AgeInput/AgeDateInput.component.js +++ b/src/core_modules/capture-ui/internal/AgeInput/AgeDateInput.component.js @@ -1,6 +1,5 @@ // @flow import React, { Component } from 'react'; -import moment from 'moment'; import { DateField } from '../../DateAndTimeFields/DateField/Date.component'; import typeof { orientations } from '../../constants/orientations.const'; import { withFocusSaver } from '../../HOC/withFocusSaver'; @@ -19,9 +18,8 @@ class AgeDateInputPlain extends Component { // $FlowFixMe[cannot-spread-inexact] automated comment diff --git a/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeDate.component.js b/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeDate.component.js index b1202d9335..0200268e59 100644 --- a/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeDate.component.js +++ b/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeDate.component.js @@ -1,18 +1,19 @@ // @flow import React from 'react'; import { withFocusSaver } from '../../HOC/withFocusSaver'; -import { DateField } from '../../DateAndTimeFields/DateField/Date.component'; +import { withTextFieldFocusHandler } from '../TextInput/withFocusHandler'; import { withShrinkLabel } from '../../HOC/withShrinkLabel'; - +import { DateField } from '../../DateAndTimeFields/DateField/Date.component'; function DateTimeDatePlain(props) { const { value, ...passOnProps } = props; + return ( ); } -export const DateTimeDate = withFocusSaver()(withShrinkLabel()(DateTimeDatePlain)); +export const DateTimeDate = withFocusSaver()(withShrinkLabel()(withTextFieldFocusHandler()(DateTimeDatePlain))); diff --git a/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeTime.component.js b/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeTime.component.js index d3e13858b3..4d6f7784b5 100644 --- a/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeTime.component.js +++ b/src/core_modules/capture-ui/internal/DateTimeInput/DateTimeTime.component.js @@ -1,8 +1,8 @@ // @flow import React from 'react'; +import { InputField } from '@dhis2-ui/input'; import { withFocusSaver } from '../../HOC/withFocusSaver'; import { withTextFieldFocusHandler } from '../TextInput/withFocusHandler'; -import { TextInput } from '../TextInput/TextInput.component'; import { withShrinkLabel } from '../../HOC/withShrinkLabel'; type Props = { @@ -12,23 +12,28 @@ type Props = { class DateTimeTimePlain extends React.Component { handleBlur = (event) => { - this.props.onBlur(event.currentTarget.value); + this.props.onBlur(event.value); } handleChange = (event) => { - this.props.onChange && this.props.onChange(event.currentTarget.value); + this.props.onChange && this.props.onChange(event.value); } render() { // $FlowFixMe[prop-missing] automated comment - const { onBlur, onChange, value, ...passOnProps } = this.props; + const { onBlur, onChange, value, innerMessage, ...passOnProps } = this.props; + const errorProps = innerMessage + ? { error: !!innerMessage.message?.errorMessage?.timeError, validationText: innerMessage.message?.errorMessage?.timeError } + : {}; + return ( - // $FlowFixMe[cannot-spread-inexact] automated comment - ); } diff --git a/src/core_modules/capture-ui/internal/TextInput/TextInput.component.js b/src/core_modules/capture-ui/internal/TextInput/TextInput.component.js index 0dd99c6715..8f56586745 100644 --- a/src/core_modules/capture-ui/internal/TextInput/TextInput.component.js +++ b/src/core_modules/capture-ui/internal/TextInput/TextInput.component.js @@ -31,8 +31,8 @@ export const TextInput = (props: Props) => { /> : // $FlowFixMe[cannot-spread-inexact] automated comment diff --git a/src/core_modules/capture-ui/internal/TextInput/withFocusHandler.js b/src/core_modules/capture-ui/internal/TextInput/withFocusHandler.js index e66c7ab510..fac5f24e69 100644 --- a/src/core_modules/capture-ui/internal/TextInput/withFocusHandler.js +++ b/src/core_modules/capture-ui/internal/TextInput/withFocusHandler.js @@ -7,7 +7,7 @@ type Props = { onSetFocus: () => void, onRemoveFocus: () => void, inFocus: boolean, - onBlur?: ?(event: SyntheticEvent) => void, + onBlur?: ?(event: SyntheticEvent, rest?: ?Object) => void, onFocus: () => void, classes: { inputWrapperFocused: string, @@ -17,9 +17,9 @@ type Props = { export const withTextFieldFocusHandler = () => (InnerCompnent: React.ComponentType) => class FocusHandlerHOC extends React.Component { - handleBlur = (event: SyntheticEvent) => { + handleBlur = (event: SyntheticEvent, rest?: ?Object) => { this.props.onRemoveFocus(); - this.props.onBlur && this.props.onBlur(event); + this.props.onBlur && this.props.onBlur(event, rest); } handleFocus = () => { diff --git a/yarn.lock b/yarn.lock index 82c88bc729..4c2c7b3910 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1763,7 +1763,7 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/calendar@10.0.3", "@dhis2-ui/calendar@9.11.0": +"@dhis2-ui/calendar@9.11.0", "@dhis2-ui/calendar@^10.0.3": version "10.0.3" resolved "https://registry.yarnpkg.com/@dhis2-ui/calendar/-/calendar-10.0.3.tgz#726560825eb0919db8018097608075a46bd9b638" integrity sha512-yxLESkgO+PlCdkREqzCKGq5KXmKtUjMkRWb6LY3hkpYZ0DmHCrqUpIHGqm/cxA3xo812km2SwpoKgVbmP+T6YA==