diff --git a/cypress/e2e/NewPage/NewPage.js b/cypress/e2e/NewPage/NewPage.js index 74201146ac..caecc7dd8b 100644 --- a/cypress/e2e/NewPage/NewPage.js +++ b/cypress/e2e/NewPage/NewPage.js @@ -634,6 +634,9 @@ And('you see the enrollment minimap', () => { }); And('you delete the recently added tracked entity', () => { + cy.get('[data-test="profile-widget"]') + .contains('Person profile') + .should('exist'); cy.get('[data-test="widget-profile-overflow-menu"]') .click(); cy.contains('Delete Person') @@ -646,6 +649,9 @@ And('you delete the recently added tracked entity', () => { }); And('you delete the recently added malaria entity', () => { + cy.get('[data-test="profile-widget"]') + .contains('Malaria Entity profile') + .should('exist'); cy.get('[data-test="widget-profile-overflow-menu"]') .click(); cy.contains('Delete Malaria Entity') diff --git a/i18n/en.pot b/i18n/en.pot index 6f7cde475a..afccea8bc5 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-11-07T11:57:59.094Z\n" -"PO-Revision-Date: 2024-11-07T11:57:59.094Z\n" +"POT-Creation-Date: 2024-12-03T10:58:18.077Z\n" +"PO-Revision-Date: 2024-12-03T10:58:18.077Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -113,66 +113,6 @@ msgstr "This value is validating" msgid "Async field update failed" msgstr "Async field update failed" -msgid "A value is required" -msgstr "A value is required" - -msgid "Please provide a valid number" -msgstr "Please provide a valid number" - -msgid "Please provide a valid integer" -msgstr "Please provide a valid integer" - -msgid "Please provide a positive integer" -msgstr "Please provide a positive integer" - -msgid "Please provide zero or a positive integer" -msgstr "Please provide zero or a positive integer" - -msgid "Please provide a negative integer" -msgstr "Please provide a negative integer" - -msgid "Please provide a valid date" -msgstr "Please provide a valid date" - -msgid "A date in the future is not allowed" -msgstr "A date in the future is not allowed" - -msgid "Please provide a valid date and time" -msgstr "Please provide a valid date and time" - -msgid "Please provide a valid time" -msgstr "Please provide a valid time" - -msgid "Please provide an integer between 0 and 100" -msgstr "Please provide an integer between 0 and 100" - -msgid "Please provide a valid url" -msgstr "Please provide a valid url" - -msgid "Please provide a valid email address" -msgstr "Please provide a valid email address" - -msgid "Please provide a valid age" -msgstr "Please provide a valid age" - -msgid "Please provide a valid phone number" -msgstr "Please provide a valid phone number" - -msgid "Please provide a valid organisation unit" -msgstr "Please provide a valid organisation unit" - -msgid "Please provide valid coordinates" -msgstr "Please provide valid coordinates" - -msgid "This value already exists" -msgstr "This value already exists" - -msgid "\"From\" cannot be greater than \"To\"" -msgstr "\"From\" cannot be greater than \"To\"" - -msgid "Checking..." -msgstr "Checking..." - msgid "Area" msgstr "Area" @@ -185,6 +125,12 @@ msgstr "Enrollment" msgid "Complete event" msgstr "Complete event" +msgid "A value is required" +msgstr "A value is required" + +msgid "Please provide a valid date" +msgstr "Please provide a valid date" + msgid "{{ stageName }} - Basic info" msgstr "{{ stageName }} - Basic info" @@ -197,6 +143,9 @@ msgstr "{{ stageName }} - Status" msgid "Please select {{categoryName}}" msgstr "Please select {{categoryName}}" +msgid "A date in the future is not allowed" +msgstr "A date in the future is not allowed" + msgid "Saving a new enrollment in {{programName}} in {{orgUnitName}}." msgstr "Saving a new enrollment in {{programName}} in {{orgUnitName}}." @@ -1195,6 +1144,9 @@ msgstr "Set coordinates" msgid "Coordinates" msgstr "Coordinates" +msgid "Please provide valid coordinates" +msgstr "Please provide valid coordinates" + msgid "Delete polygon" msgstr "Delete polygon" @@ -1454,6 +1406,9 @@ msgstr "Report date" msgid "Please enter a date" msgstr "Please enter a date" +msgid "Please provide a valid organisation unit" +msgstr "Please provide a valid organisation unit" + msgid "Please select a valid event" msgstr "Please select a valid event" @@ -1930,6 +1885,51 @@ 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 number" +msgstr "Please provide a valid number" + +msgid "Please provide a valid integer" +msgstr "Please provide a valid integer" + +msgid "Please provide a positive integer" +msgstr "Please provide a positive integer" + +msgid "Please provide zero or a positive integer" +msgstr "Please provide zero or a positive integer" + +msgid "Please provide a negative integer" +msgstr "Please provide a negative integer" + +msgid "Please provide a valid date and time" +msgstr "Please provide a valid date and time" + +msgid "Please provide a valid time" +msgstr "Please provide a valid time" + +msgid "Please provide an integer between 0 and 100" +msgstr "Please provide an integer between 0 and 100" + +msgid "Please provide a valid url" +msgstr "Please provide a valid url" + +msgid "Please provide a valid email address" +msgstr "Please provide a valid email address" + +msgid "Please provide a valid age" +msgstr "Please provide a valid age" + +msgid "Please provide a valid phone number" +msgstr "Please provide a valid phone number" + +msgid "This value already exists" +msgstr "This value already exists" + +msgid "\"From\" cannot be greater than \"To\"" +msgstr "\"From\" cannot be greater than \"To\"" + +msgid "Checking..." +msgstr "Checking..." + msgid "Please provide a valid positive integer" msgstr "Please provide a valid positive integer" diff --git a/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js b/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js index c5aa1266a8..31d6d45b1a 100644 --- a/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js +++ b/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js @@ -11,7 +11,7 @@ import { buildField } from './field/buildField'; import { validationStrategies } from '../../metaData/RenderFoundation/renderFoundation.const'; import type { CustomForm, DataElement } from '../../metaData'; import { messageStateKeys } from '../../reducers/descriptions/rulesEffects.reducerDescription'; -import { validatorTypes } from './field/validators/constants'; +import { validatorTypes } from '../../utils/validation/constants'; import type { QuerySingleResource } from '../../utils/api/api.types'; import { FormFieldPlugin } from './FormFieldPlugin'; import { FormFieldPluginConfig } from '../../metaData/FormFieldPluginConfig'; diff --git a/src/core_modules/capture-core/components/D2Form/D2SectionFields.container.js b/src/core_modules/capture-core/components/D2Form/D2SectionFields.container.js index 0ce87af445..39bfcce74b 100644 --- a/src/core_modules/capture-core/components/D2Form/D2SectionFields.container.js +++ b/src/core_modules/capture-core/components/D2Form/D2SectionFields.container.js @@ -16,7 +16,7 @@ const makeMapStateToProps = () => { const getRulesMessages = makeGetMessages(); const getCompulsory = makeGetCompulsory(); const getDisabled = makeGetDisabled(); - const mapStateToProps = (state: Object, props: { formId: string }) => ({ + const mapStateToProps = (state: Object, props: { formId: string, fieldsMetaData: any }) => ({ values: getSectionValues(state, props), rulesHiddenFields: getHiddenFields(state, props), rulesMessages: getRulesMessages(state, props), 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 6e5b37a2a2..9f07004ebb 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 @@ -11,28 +11,17 @@ import isObject from 'd2-utilizr/lib/isObject'; 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 { getValidators, validateValue, validatorTypes } from '../../../utils/validation'; +import type { ValidatorContainer } from '../../../utils/validation'; import type { DataElement } from '../../../metaData'; import type { QuerySingleResource } from '../../../utils/api'; -export type ValidatorContainer = { - validator: (value: any, validationContext: ?Object, internalError?: ?{ - error?: ?string, - errorCode?: ?string, - }) => boolean | Promise, - message: string, - validatingMessage?: ?string, - type?: ?string, - async?: ?boolean, -}; - export type FieldConfig = { id: string, component: React.ComponentType, plugin?: boolean, props: Object, - validators?: ?Array, + validators?: Array, commitEvent?: ?string, onIsEqual?: ?(newValue: any, oldValue: any) => boolean, }; @@ -51,7 +40,7 @@ type GetContainerPropsFn = (index: number, fieldsCount: number, field: FieldConf type FieldCommitConfig = {| fieldId: string, - validators?: ?Array, + validators?: Array, onIsEqual?: ?(newValue: any, oldValue: any) => boolean, |} @@ -100,7 +89,7 @@ export type FieldCommitOptions = {| errorCode?: string, |}; -type FieldCommitOptionsExtended = {| +export type FieldCommitOptionsExtended = {| ...FieldCommitOptions, plugin?: ?boolean, |}; @@ -109,59 +98,6 @@ type FieldCommitOptionsExtended = {| type FieldsValidatingPromiseContainer = { [fieldId: string]: ?{ cancelableValidatingPromise?: ?CancelablePromise, validatingCompleteUid: string } }; export class FormBuilder extends React.Component { - static async validateField( - { validators }: { validators?: ?Array }, - value: any, - validationContext: ?Object, - onIsValidatingInternal: ?Function, - commitOptions?: ?FieldCommitOptions, - ): Promise<{ valid: boolean, errorMessage?: ?string, errorType?: ?string }> { - if (!validators || validators.length === 0) { - return { - valid: true, - }; - } - - const validatorResult = await validators - .reduce(async (passPromise, currentValidator) => { - const pass = await passPromise; - if (pass === true) { - let result = currentValidator.validator(value, - { error: commitOptions?.error, errorCode: commitOptions?.errorCode }, - validationContext); - if (result instanceof Promise) { - result = onIsValidatingInternal ? - onIsValidatingInternal(currentValidator.validatingMessage, result) : - result; - result = await result; - } - - if (result === true || (result && result.valid)) { - return true; - } - return { - message: (result && result.errorMessage) || currentValidator.message, - type: currentValidator.type, - data: result && result.data, - }; - } - return pass; - }, Promise.resolve(true)); - - if (validatorResult !== true) { - return { - valid: false, - errorMessage: validatorResult.message, - errorType: validatorResult.type, - errorData: validatorResult.data, - }; - } - - return { - valid: true, - }; - } - static getAsyncUIState(fieldsUI: { [id: string]: FieldUI }) { return Object.keys(fieldsUI).reduce((accAsyncUIState, fieldId) => { const fieldUI = fieldsUI[fieldId]; @@ -234,12 +170,12 @@ export class FormBuilder extends React.Component { let validationData; try { - validationData = await FormBuilder.validateField( - field, - values[field.id], + validationData = await validateValue({ + validators: field.validators, + value: values[field.id], validationContext, - handleIsValidatingInternal, - ); + postProcessAsyncValidatonInitiation: handleIsValidatingInternal, + }); } catch (reason) { if (reason && isObject(reason) && reason.isCanceled) { validationData = null; @@ -468,14 +404,13 @@ export class FormBuilder extends React.Component { errorMessage: options.error, errorType: validatorTypes.TYPE_BASE, errorData: undefined }) : - (await FormBuilder.validateField( - { validators }, + (await validateValue({ + validators, value, - onGetValidationContext && onGetValidationContext(), - handleIsValidatingInternal, - // $FlowFixMe - options, - ) + validationContext: onGetValidationContext && onGetValidationContext(), + postProcessAsyncValidatonInitiation: handleIsValidatingInternal, + commitOptions: options, + }) // $FlowFixMe[prop-missing] automated comment .then(({ valid, errorMessage, errorType, errorData }) => { updateField({ valid, errorMessage, errorType, errorData }); diff --git a/src/core_modules/capture-core/components/D2Form/FormBuilder/index.js b/src/core_modules/capture-core/components/D2Form/FormBuilder/index.js index 42434e8133..fab5ff84f6 100644 --- a/src/core_modules/capture-core/components/D2Form/FormBuilder/index.js +++ b/src/core_modules/capture-core/components/D2Form/FormBuilder/index.js @@ -1,4 +1,4 @@ // @flow export { FormBuilder } from './FormBuilder.component'; export type { PostProcessErrorMessage, ErrorData } from './formbuilder.types'; -export type { FieldConfig, ValidatorContainer } from './FormBuilder.component'; +export type { FieldConfig, FieldCommitOptionsExtended } from './FormBuilder.component'; diff --git a/src/core_modules/capture-core/components/D2Form/field/configs/base/configBase.js b/src/core_modules/capture-core/components/D2Form/field/configs/base/configBase.js index 9aeec1d79d..f33d880d64 100644 --- a/src/core_modules/capture-core/components/D2Form/field/configs/base/configBase.js +++ b/src/core_modules/capture-core/components/D2Form/field/configs/base/configBase.js @@ -1,7 +1,7 @@ // @flow import { type ComponentType } from 'react'; -import type { ValidatorContainer } from '../../../FormBuilder'; -import { getValidators } from '../../validators'; +import type { ValidatorContainer } from '../../../../../utils/validation'; +import { getValidators } from '../../../../../utils/validation'; import type { DataElement } from '../../../../../metaData'; import type { QuerySingleResource } from '../../../../../utils/api/api.types'; diff --git a/src/core_modules/capture-core/components/D2Form/field/validators/index.js b/src/core_modules/capture-core/components/D2Form/field/validators/index.js deleted file mode 100644 index bc50f272c3..0000000000 --- a/src/core_modules/capture-core/components/D2Form/field/validators/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { getValidators } from './getValidators'; diff --git a/src/core_modules/capture-core/components/D2Form/index.js b/src/core_modules/capture-core/components/D2Form/index.js index ac6560e0f2..8117978a25 100644 --- a/src/core_modules/capture-core/components/D2Form/index.js +++ b/src/core_modules/capture-core/components/D2Form/index.js @@ -1,3 +1,4 @@ // @flow export { asyncHandlerActionTypes, asyncUpdateFieldEpic } from './asyncHandlerHOC'; export { D2Form } from './D2Form.container'; +export type { FieldCommitOptionsExtended } from './FormBuilder'; 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 00efbf978f..1444a98aea 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 @@ -417,17 +417,38 @@ export class EnrollmentDataEntryComponent extends React.Component) => { const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props; - this.props.onUpdateField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation); - } + this.props.onUpdateField( + ...args, + programId, + orgUnit, + firstStageMetaData?.stage, + formFoundation, + this.getValidationContext, + ); + }; handleUpdateDataEntryField = (...args: Array) => { const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props; - this.props.onUpdateDataEntryField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation); + this.props.onUpdateDataEntryField( + ...args, + programId, + orgUnit, + firstStageMetaData?.stage, + formFoundation, + this.getValidationContext, + ); } handleStartAsyncUpdateField = (...args: Array) => { const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props; - this.props.onStartAsyncUpdateField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation); + this.props.onStartAsyncUpdateField( + ...args, + programId, + orgUnit, + firstStageMetaData?.stage, + formFoundation, + this.getValidationContext, + ); } render() { diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.container.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.container.js index 6df22c0f34..4bf992d94a 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.container.js @@ -14,8 +14,11 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ orgUnit: OrgUnit, stage?: ProgramStage, formFoundation: RenderFoundation, + onGetValidationContext: () => Object, ) => { - dispatch(updateDataEntryFieldBatch(innerAction, programId, orgUnit, stage, formFoundation)); + dispatch( + updateDataEntryFieldBatch(innerAction, programId, orgUnit, stage, formFoundation, onGetValidationContext), + ); }, onUpdateField: ( innerAction: ReduxAction, @@ -23,8 +26,9 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ orgUnit: OrgUnit, stage?: ProgramStage, formFoundation: RenderFoundation, + onGetValidationContext: () => Object, ) => { - dispatch(updateFieldBatch(innerAction, programId, orgUnit, stage, formFoundation)); + dispatch(updateFieldBatch(innerAction, programId, orgUnit, stage, formFoundation, onGetValidationContext)); }, onStartAsyncUpdateField: ( innerAction: ReduxAction, @@ -34,9 +38,10 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ orgUnit: OrgUnit, stage?: ProgramStage, formFoundation: RenderFoundation, + onGetValidationContext: () => Object, ) => { const onAsyncUpdateSuccess = (successInnerAction: ReduxAction) => - asyncUpdateSuccessBatch(successInnerAction, dataEntryId, itemId, programId, orgUnit, stage, formFoundation); + asyncUpdateSuccessBatch(successInnerAction, dataEntryId, itemId, programId, orgUnit, stage, formFoundation, onGetValidationContext); const onAsyncUpdateError = (errorInnerAction: ReduxAction) => errorInnerAction; dispatch(startAsyncUpdateFieldForNewEnrollment(innerAction, onAsyncUpdateSuccess, onAsyncUpdateError)); 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 3d06861d5b..2f9290b07a 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,7 +1,7 @@ // @flow import i18n from '@dhis2/d2-i18n'; import { hasValue } from 'capture-core-utils/validators/form'; -import { isValidDate } from '../../../../../utils/validators/form'; +import { isValidDate } from '../../../../../utils/validation/validators/form'; const preValidateDate = (value?: ?string, internalComponentError: ?{error?: ?string, errorCode?: ?string}) => { if (!value) { diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js index f84452c7fe..da21783486 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js @@ -6,11 +6,16 @@ import type { TEIValues, OrgUnit, } from '@dhis2/rules-engine-javascript'; -import { getApplicableRuleEffectsForTrackerProgram, updateRulesEffects } from '../../../../rules'; +import { + getApplicableRuleEffectsForTrackerProgram, + updateRulesEffects, + validateAssignEffects, +} from '../../../../rules'; import { rulesExecutedPostUpdateField } from '../../../DataEntry/actions/dataEntry.actions'; import { TrackerProgram, RenderFoundation, ProgramStage } from '../../../../metaData'; import { startRunRulesPostUpdateField } from '../../../DataEntry'; import { startRunRulesOnUpdateForNewEnrollment } from './enrollment.actions'; +import type { QuerySingleResource } from '../../../../utils/api'; export const batchActionTypes = { RULES_EXECUTED_POST_UPDATE_FIELD_FOR_ENROLLMENT: 'RulesExecutedPostUpdateFieldForEnrollment', @@ -18,7 +23,7 @@ export const batchActionTypes = { UPDATE_DATA_ENTRY_FIELD_NEW_ENROLLMENT_ACTION_BATCH: 'UpdateDataEntryFieldNewEnrollmentActionBatch', }; -export const runRulesOnUpdateFieldBatch = ({ +export const runRulesOnUpdateFieldBatch = async ({ program, formId, dataEntryId, @@ -31,6 +36,8 @@ export const runRulesOnUpdateFieldBatch = ({ stage, formFoundation, currentEvent, + querySingleResource, + onGetValidationContext, }: { program: TrackerProgram, formId: string, @@ -41,9 +48,11 @@ export const runRulesOnUpdateFieldBatch = ({ attributeValues?: TEIValues, extraActions: Array>, uid: string, - stage?: ProgramStage, + stage: ProgramStage, formFoundation?: RenderFoundation, currentEvent?: {[id: string]: any}, + querySingleResource: QuerySingleResource, + onGetValidationContext: () => Object, }) => { const effects = getApplicableRuleEffectsForTrackerProgram({ program, @@ -54,8 +63,16 @@ export const runRulesOnUpdateFieldBatch = ({ attributeValues, formFoundation, }); + + const effectsWithValidations = await validateAssignEffects({ + dataElements: formFoundation ? formFoundation.getElements() : program.attributes, + effects, + querySingleResource, + onGetValidationContext, + }); + return batchActions([ - updateRulesEffects(effects, formId), + updateRulesEffects(effectsWithValidations, formId), rulesExecutedPostUpdateField(dataEntryId, itemId, uid), ...extraActions, ], batchActionTypes.RULES_EXECUTED_POST_UPDATE_FIELD_FOR_ENROLLMENT); @@ -67,15 +84,27 @@ export const updateDataEntryFieldBatch = ( orgUnit: OrgUnit, stage?: ProgramStage, formFoundation: RenderFoundation, + onGetValidationContext: () => Object, ) => { const { dataEntryId, itemId } = innerAction.payload; const uid = uuid(); - return batchActions([ - innerAction, - startRunRulesPostUpdateField(dataEntryId, itemId, uid), - startRunRulesOnUpdateForNewEnrollment(innerAction.payload, uid, programId, orgUnit, stage, formFoundation), - ], batchActionTypes.UPDATE_DATA_ENTRY_FIELD_NEW_ENROLLMENT_ACTION_BATCH); + return batchActions( + [ + innerAction, + startRunRulesPostUpdateField(dataEntryId, itemId, uid), + startRunRulesOnUpdateForNewEnrollment({ + payload: innerAction.payload, + uid, + programId, + orgUnit, + stage, + formFoundation, + onGetValidationContext, + }), + ], + batchActionTypes.UPDATE_DATA_ENTRY_FIELD_NEW_ENROLLMENT_ACTION_BATCH, + ); }; export const updateFieldBatch = ( @@ -84,15 +113,27 @@ export const updateFieldBatch = ( orgUnit: OrgUnit, stage?: ProgramStage, formFoundation: RenderFoundation, + onGetValidationContext: () => Object, ) => { const { dataEntryId, itemId } = innerAction.payload; const uid = uuid(); - return batchActions([ - innerAction, - startRunRulesPostUpdateField(dataEntryId, itemId, uid), - startRunRulesOnUpdateForNewEnrollment(innerAction.payload, uid, programId, orgUnit, stage, formFoundation), - ], batchActionTypes.UPDATE_FIELD_NEW_ENROLLMENT_ACTION_BATCH); + return batchActions( + [ + innerAction, + startRunRulesPostUpdateField(dataEntryId, itemId, uid), + startRunRulesOnUpdateForNewEnrollment({ + payload: innerAction.payload, + uid, + programId, + orgUnit, + stage, + formFoundation, + onGetValidationContext, + }), + ], + batchActionTypes.UPDATE_FIELD_NEW_ENROLLMENT_ACTION_BATCH, + ); }; export const asyncUpdateSuccessBatch = ( @@ -103,12 +144,24 @@ export const asyncUpdateSuccessBatch = ( orgUnit: OrgUnit, stage?: ProgramStage, formFoundation: RenderFoundation, + onGetValidationContext: () => Object, ) => { const uid = uuid(); - return batchActions([ - innerAction, - startRunRulesPostUpdateField(dataEntryId, itemId, uid), - startRunRulesOnUpdateForNewEnrollment({ ...innerAction.payload, dataEntryId, itemId }, uid, programId, orgUnit, stage, formFoundation), - ], batchActionTypes.UPDATE_FIELD_NEW_ENROLLMENT_ACTION_BATCH); + return batchActions( + [ + innerAction, + startRunRulesPostUpdateField(dataEntryId, itemId, uid), + startRunRulesOnUpdateForNewEnrollment({ + payload: { ...innerAction.payload, dataEntryId, itemId }, + uid, + programId, + orgUnit, + stage, + formFoundation, + onGetValidationContext, + }), + ], + batchActionTypes.UPDATE_FIELD_NEW_ENROLLMENT_ACTION_BATCH, + ); }; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js index 68d8a8cb7e..e0e0bbe343 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js @@ -7,16 +7,32 @@ export const actionTypes = { START_RUN_RULES_ON_UPDATE: 'StartRunRulesOnUpdateForNewEnrollment', }; -export const startRunRulesOnUpdateForNewEnrollment = ( +export const startRunRulesOnUpdateForNewEnrollment = ({ + payload, + uid, + programId, + orgUnit, + stage, + formFoundation, + onGetValidationContext, +}: { payload: Object, uid: string, programId: string, orgUnit: OrgUnit, stage?: ProgramStage, formFoundation: RenderFoundation, -) => - actionCreator(actionTypes.START_RUN_RULES_ON_UPDATE)( - { innerPayload: payload, uid, programId, orgUnit, stage, formFoundation }); + onGetValidationContext: () => Object, +}) => + actionCreator(actionTypes.START_RUN_RULES_ON_UPDATE)({ + innerPayload: payload, + uid, + programId, + orgUnit, + stage, + formFoundation, + onGetValidationContext, + }); export const startAsyncUpdateFieldForNewEnrollment = ( innerAction: ReduxAction, diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js index 9bc4da634a..77ee439a01 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js @@ -1,7 +1,8 @@ // @flow import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import { ofType } from 'redux-observable'; -import { map } from 'rxjs/operators'; +import { from } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { batchActionTypes, runRulesOnUpdateFieldBatch } from '../actions/enrollment.actionBatchs'; import { actionTypes } from '../actions/enrollment.actions'; import { getTrackerProgramThrowIfNotFound, ProgramStage, RenderFoundation, Section } from '../../../../metaData'; @@ -9,6 +10,7 @@ import { getCurrentClientMainData, type FieldData } from '../../../../rules'; import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; import { convertFormToClient } from '../../../../converters'; import { stageMainDataIds, convertToRulesEngineIds } from '../EnrollmentWithFirstStageDataEntry'; +import type { QuerySingleResource } from '../../../../utils/api'; type Context = { dataEntryId: string, @@ -16,7 +18,7 @@ type Context = { uid: string, programId: string, orgUnit: OrgUnit, - stage?: ProgramStage, + stage: ProgramStage, formFoundation: RenderFoundation, } @@ -38,46 +40,62 @@ const splitCurrentClientMainData = (stage, currentClientMainData) => { }, { currentEnrollmentValues: {}, currentEventMainData: {} }); }; -const runRulesOnEnrollmentUpdate = - (store: ReduxStore, context: Context, fieldData?: ?FieldData, searchActions?: any = []) => { - const state = store.value; - const { programId, dataEntryId, itemId, orgUnit, uid, stage, formFoundation } = context; - const formId = getDataEntryKey(dataEntryId, itemId); - const program = getTrackerProgramThrowIfNotFound(programId); - const currentFormData = state.formsValues[formId] || {}; - const convertedValues = formFoundation.convertAndGroupBySection(currentFormData, convertFormToClient); - const attributeValues = convertedValues[Section.groups.ENROLLMENT]; - const currentEventValues = convertedValues[Section.groups.EVENT] || {}; - const currentClientMainData = - getCurrentClientMainData(state, itemId, dataEntryId, formFoundation) || {}; - const { currentEnrollmentValues, currentEventMainData } - = splitCurrentClientMainData(state, currentClientMainData); - const currentEvent = stage - ? { ...currentEventValues, ...currentEventMainData, programStageId: stage.id } : undefined; - - return runRulesOnUpdateFieldBatch({ - program, - formId, - dataEntryId, - itemId, - orgUnit, - enrollmentData: currentEnrollmentValues, - attributeValues, - currentEvent, - extraActions: searchActions, - uid, - stage, - formFoundation: stage ? formFoundation : undefined, - }); - }; +const runRulesOnEnrollmentUpdate = ({ + store, + context, + searchActions = [], + querySingleResource, + onGetValidationContext, +}: { + store: ReduxStore, + context: Context, + searchActions?: any, + querySingleResource: QuerySingleResource, + onGetValidationContext: () => Object, +}) => { + const state = store.value; + const { programId, dataEntryId, itemId, orgUnit, uid, stage, formFoundation } = context; + const formId = getDataEntryKey(dataEntryId, itemId); + const program = getTrackerProgramThrowIfNotFound(programId); + const currentFormData = state.formsValues[formId] || {}; + const convertedValues = formFoundation.convertAndGroupBySection(currentFormData, convertFormToClient); + const attributeValues = convertedValues[Section.groups.ENROLLMENT]; + const currentEventValues = convertedValues[Section.groups.EVENT] || {}; + const currentClientMainData = getCurrentClientMainData(state, itemId, dataEntryId, formFoundation) || {}; + const { currentEnrollmentValues, currentEventMainData } = splitCurrentClientMainData(state, currentClientMainData); + const currentEvent = stage + ? { ...currentEventValues, ...currentEventMainData, programStageId: stage.id } + : undefined; + const runRulesOnUpdateFieldBatchPromise = runRulesOnUpdateFieldBatch({ + program, + formId, + dataEntryId, + itemId, + orgUnit, + enrollmentData: currentEnrollmentValues, + attributeValues, + currentEvent, + extraActions: searchActions, + uid, + stage, + formFoundation: stage ? formFoundation : undefined, + querySingleResource, + onGetValidationContext, + }); + return from(runRulesOnUpdateFieldBatchPromise); +}; -export const runRulesOnEnrollmentDataEntryFieldUpdateEpic = (action$: InputObservable, store: ReduxStore) => +export const runRulesOnEnrollmentDataEntryFieldUpdateEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => action$.pipe( ofType(batchActionTypes.UPDATE_DATA_ENTRY_FIELD_NEW_ENROLLMENT_ACTION_BATCH), map(actionBatch => actionBatch.payload.find(action => action.type === actionTypes.START_RUN_RULES_ON_UPDATE)), - map((action) => { + switchMap((action) => { const { uid, programId, @@ -85,6 +103,7 @@ export const runRulesOnEnrollmentDataEntryFieldUpdateEpic = (action$: InputObser innerPayload, stage, formFoundation, + onGetValidationContext, } = action.payload; const { @@ -92,24 +111,42 @@ export const runRulesOnEnrollmentDataEntryFieldUpdateEpic = (action$: InputObser itemId, } = innerPayload; - return runRulesOnEnrollmentUpdate(store, { - dataEntryId, - itemId, - uid, - programId, - orgUnit, - stage, - formFoundation, + return runRulesOnEnrollmentUpdate({ + store, + context: { + dataEntryId, + itemId, + uid, + programId, + orgUnit, + stage, + formFoundation, + }, + querySingleResource, + onGetValidationContext, }); })); -export const runRulesOnEnrollmentFieldUpdateEpic = (action$: InputObservable, store: ReduxStore) => +export const runRulesOnEnrollmentFieldUpdateEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => action$.pipe( ofType(batchActionTypes.UPDATE_FIELD_NEW_ENROLLMENT_ACTION_BATCH), map(actionBatch => actionBatch.payload.find(action => action.type === actionTypes.START_RUN_RULES_ON_UPDATE)), - map((action) => { - const { innerPayload: payload, searchActions, uid, programId, orgUnit, stage, formFoundation } = action.payload; + switchMap((action) => { + const { + innerPayload: payload, + searchActions, + uid, + programId, + orgUnit, + stage, + formFoundation, + onGetValidationContext, + } = action.payload; const { dataEntryId, itemId, elementId, value, uiState } = payload; const fieldData: FieldData = { @@ -118,14 +155,21 @@ export const runRulesOnEnrollmentFieldUpdateEpic = (action$: InputObservable, st valid: uiState.valid, }; - return runRulesOnEnrollmentUpdate(store, { - programId, - orgUnit, - dataEntryId, - itemId, - uid, - stage, - formFoundation, - }, fieldData, searchActions); + return runRulesOnEnrollmentUpdate({ + store, + context: { + programId, + orgUnit, + dataEntryId, + itemId, + uid, + stage, + formFoundation, + }, + fieldData, + searchActions, + querySingleResource, + onGetValidationContext, + }); }), ); 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 3fd678d77a..32d0534018 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,7 +1,7 @@ // @flow import i18n from '@dhis2/d2-i18n'; import { hasValue } from 'capture-core-utils/validators/form'; -import { isValidDate, isValidNonFutureDate } from '../../../../utils/validators/form'; +import { isValidDate, isValidNonFutureDate } from '../../../../utils/validation/validators/form'; const isValidEnrollmentDate = (value: string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { if (!value) { 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 07aaadf567..897510621f 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,7 +1,7 @@ // @flow import i18n from '@dhis2/d2-i18n'; import { hasValue } from 'capture-core-utils/validators/form'; -import { isValidDate, isValidNonFutureDate } from '../../../../utils/validators/form'; +import { isValidDate, isValidNonFutureDate } from '../../../../utils/validation/validators/form'; const isValidIncidentDate = (value: string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { if (!value) { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.js index 0da25fbb44..006d2a7003 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.js @@ -1,6 +1,7 @@ // @flow import { ofType } from 'redux-observable'; -import { map, filter } from 'rxjs/operators'; +import { from } from 'rxjs'; +import { map, filter, switchMap } from 'rxjs/operators'; import { batchActions } from 'redux-batched-actions'; import { type OrgUnit } from '@dhis2/rules-engine-javascript'; import { rulesExecutedPostUpdateField } from '../../../../../DataEntry/actions/dataEntry.actions'; @@ -37,6 +38,8 @@ import { actionTypes as crossPageActionTypes } from '../../../../../Pages/action import { lockedSelectorActionTypes } from '../../../../../LockedSelector/LockedSelector.actions'; import { newPageActionTypes } from '../../../../../Pages/New/NewPage.actions'; import { programCollection } from '../../../../../../metaDataMemoryStores'; +import { validateAssignEffects } from '../../../../../../rules'; +import type { QuerySingleResource } from '../../../../../../utils/api'; export const resetDataEntryForNewEventEpic = (action$: InputObservable) => action$.pipe( @@ -121,14 +124,23 @@ export const resetRecentlyAddedEventsWhenNewEventInDataEntryEpic = (action$: Inp })); -const runRulesForNewSingleEvent = ( +const runRulesForNewSingleEvent = async ({ + store, + dataEntryId, + itemId, + uid, + orgUnit, + fieldData, + querySingleResource, +}: { store: ReduxStore, dataEntryId: string, itemId: string, uid: string, orgUnit: OrgUnit, fieldData?: ?FieldData, -) => { + querySingleResource: QuerySingleResource, +}) => { const state = store.value; const formId = getDataEntryKey(dataEntryId, itemId); @@ -147,35 +159,66 @@ const runRulesForNewSingleEvent = ( currentEvent, }); + const effectsWithValidations = await validateAssignEffects({ + dataElements: foundation.getElements(), + effects, + querySingleResource, + }); + return batchActions([ - updateRulesEffects(effects, formId), + updateRulesEffects(effectsWithValidations, formId), rulesExecutedPostUpdateField(dataEntryId, itemId, uid), ], batchActionTypes.RULES_EFFECTS_ACTIONS_BATCH, ); }; -export const runRulesOnUpdateDataEntryFieldForSingleEventEpic = (action$: InputObservable, store: ReduxStore) => +export const runRulesOnUpdateDataEntryFieldForSingleEventEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => action$.pipe( ofType(batchActionTypes.UPDATE_DATA_ENTRY_FIELD_NEW_SINGLE_EVENT_ACTION_BATCH), map(actionBatch => actionBatch.payload.find(action => action.type === newEventDataEntryActionTypes.START_RUN_RULES_ON_UPDATE)), - map((action) => { + switchMap((action) => { const { dataEntryId, itemId, uid, orgUnit } = action.payload; - return runRulesForNewSingleEvent(store, dataEntryId, itemId, uid, orgUnit); + const runRulesForNewSingleEventPromise = runRulesForNewSingleEvent({ + store, + dataEntryId, + itemId, + uid, + orgUnit, + querySingleResource, + }); + return from(runRulesForNewSingleEventPromise); })); -export const runRulesOnUpdateFieldForSingleEventEpic = (action$: InputObservable, store: ReduxStore) => +export const runRulesOnUpdateFieldForSingleEventEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => action$.pipe( ofType(batchActionTypes.UPDATE_FIELD_NEW_SINGLE_EVENT_ACTION_BATCH), map(actionBatch => actionBatch.payload.find(action => action.type === newEventDataEntryActionTypes.START_RUN_RULES_ON_UPDATE)), - map((action) => { + switchMap((action) => { const { dataEntryId, itemId, uid, orgUnit, elementId, value, uiState } = action.payload; const fieldData: FieldData = { elementId, value, valid: uiState.valid, }; - return runRulesForNewSingleEvent(store, dataEntryId, itemId, uid, orgUnit, fieldData); + const runRulesForNewSingleEventPromise = runRulesForNewSingleEvent({ + store, + dataEntryId, + itemId, + uid, + orgUnit, + fieldData, + querySingleResource, + }); + return from(runRulesForNewSingleEventPromise); })); 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 c5dcf70ab2..d1ff0940ed 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 @@ -1,7 +1,7 @@ // @flow import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; -import { isValidDate } from '../../../../../../utils/validators/form'; +import { isValidDate } from '../../../../../../utils/validation/validators/form'; const preValidateDate = (value?: ?string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { if (!value) { diff --git a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js index 0971d33264..928d9fd754 100644 --- a/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js +++ b/src/core_modules/capture-core/components/FiltersForTypes/Date/DateFilterManager.component.js @@ -8,7 +8,7 @@ import { mainOptionKeys } from './options'; import { dateFilterTypes } from './constants'; import type { DateFilterData } from './types'; import type { Value } from './DateFilter.component'; -import { areRelativeRangeValuesSupported } from '../../../utils/validators/areRelativeRangeValuesSupported'; +import { areRelativeRangeValuesSupported } from '../../../utils/validation/validators/areRelativeRangeValuesSupported'; type Props = { filter: ?DateFilterData, diff --git a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js index 3ff9d3289b..4dce8ba7a9 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js +++ b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/buttonTextBuilder/converters/dateConverter.js @@ -4,7 +4,8 @@ import { pipe } from 'capture-core-utils'; import moment from 'moment'; import { convertMomentToDateFormatString } from '../../../../../../utils/converters/date'; import type { DateFilterData, AbsoluteDateFilterData } from '../../../../../FiltersForTypes'; -import { areRelativeRangeValuesSupported } from '../../../../../../utils/validators/areRelativeRangeValuesSupported'; +import { areRelativeRangeValuesSupported } + from '../../../../../../utils/validation/validators/areRelativeRangeValuesSupported'; const periods = { TODAY: 'TODAY', diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.js index 73e0160c03..f582f16368 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.js @@ -1,6 +1,7 @@ // @flow import { ofType } from 'redux-observable'; -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; +import { from } from 'rxjs'; import { batchActions } from 'redux-batched-actions'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import i18n from '@dhis2/d2-i18n'; @@ -15,22 +16,34 @@ import { getCurrentClientMainData, getApplicableRuleEffectsForTrackerProgram, updateRulesEffects, + validateAssignEffects, type FieldData, } from '../../../../rules'; import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; import type { RulesExecutionDependenciesClientFormatted } from '../../common.types'; import { getLocationQuery } from '../../../../utils/routing'; +import type { QuerySingleResource } from '../../../../utils/api'; -const runRulesForNewEvent = ( +const runRulesForNewEvent = async ({ + store, + dataEntryId, + itemId, + uid, + orgUnit, + rulesExecutionDependenciesClientFormatted, + fieldData, + querySingleResource, +}: { store: ReduxStore, dataEntryId: string, itemId: string, uid: string, orgUnit: OrgUnit, - history: Object, - { events, attributeValues, enrollmentData }: RulesExecutionDependenciesClientFormatted, + rulesExecutionDependenciesClientFormatted: RulesExecutionDependenciesClientFormatted, fieldData?: ?FieldData, -) => { + querySingleResource: QuerySingleResource, +}) => { + const { events, attributeValues, enrollmentData } = rulesExecutionDependenciesClientFormatted; const state = store.value; const formId = getDataEntryKey(dataEntryId, itemId); const { programId, stageId } = getLocationQuery(); @@ -47,7 +60,7 @@ const runRulesForNewEvent = ( const currentEventMainData = getCurrentClientMainData(state, itemId, dataEntryId, foundation); const currentEvent = { ...currentEventValues, ...currentEventMainData, programStageId }; - const ruleEffects = getApplicableRuleEffectsForTrackerProgram({ + const effects = getApplicableRuleEffectsForTrackerProgram({ program, stage, orgUnit, @@ -57,40 +70,55 @@ const runRulesForNewEvent = ( enrollmentData, }); + const effectsWithValidations = await validateAssignEffects({ + dataElements: foundation.getElements(), + effects, + querySingleResource, + }); + return batchActions([ - updateRulesEffects(ruleEffects, formId), + updateRulesEffects(effectsWithValidations, formId), rulesExecutedPostUpdateField(dataEntryId, itemId, uid), ], newEventWidgetDataEntryBatchActionTypes.RULES_EFFECTS_ACTIONS_BATCH, ); }; -export const runRulesOnUpdateDataEntryFieldForNewEnrollmentEventEpic = (action$: InputObservable, store: ReduxStore, { history }: ApiUtils) => +export const runRulesOnUpdateDataEntryFieldForNewEnrollmentEventEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => action$.pipe( ofType(newEventWidgetDataEntryBatchActionTypes.UPDATE_DATA_ENTRY_FIELD_ADD_EVENT_ACTION_BATCH), map(actionBatch => actionBatch.payload .find(action => action.type === newEventWidgetDataEntryActionTypes.RULES_ON_UPDATE_EXECUTE)), - map((action) => { + switchMap((action) => { const { dataEntryId, itemId, uid, orgUnit, rulesExecutionDependenciesClientFormatted } = action.payload; - return runRulesForNewEvent( + const runRulesForNewEventPromise = runRulesForNewEvent({ store, dataEntryId, itemId, uid, orgUnit, - history, rulesExecutionDependenciesClientFormatted, - ); + querySingleResource, + }); + return from(runRulesForNewEventPromise); })); -export const runRulesOnUpdateFieldForNewEnrollmentEventEpic = (action$: InputObservable, store: ReduxStore, { history }: ApiUtils) => +export const runRulesOnUpdateFieldForNewEnrollmentEventEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => action$.pipe( ofType(newEventWidgetDataEntryBatchActionTypes.FIELD_UPDATE_BATCH), map(actionBatch => actionBatch.payload .find(action => action.type === newEventWidgetDataEntryActionTypes.RULES_ON_UPDATE_EXECUTE)), - map((action) => { + switchMap((action) => { const { dataEntryId, itemId, @@ -107,14 +135,16 @@ export const runRulesOnUpdateFieldForNewEnrollmentEventEpic = (action$: InputObs value, valid: uiState.valid, }; - return runRulesForNewEvent( + + const runRulesForNewEventPromise = runRulesForNewEvent({ store, dataEntryId, itemId, uid, orgUnit, - history, rulesExecutionDependenciesClientFormatted, fieldData, - ); + querySingleResource, + }); + return from(runRulesForNewEventPromise); })); 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 d1dcf08c6d..046217b02a 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 @@ -1,7 +1,7 @@ // @flow import { hasValue } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; -import { isValidDate } from '../../../../utils/validators/form'; +import { isValidDate } from '../../../../utils/validation/validators/form'; const preValidateDate = (value?: ?string, internalComponentError?: ?{error: ?string, errorCode: ?string}) => { if (!value) { diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.js b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.js index 05515b4bcc..6ff85dc625 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.js @@ -1,7 +1,8 @@ // @flow import i18n from '@dhis2/d2-i18n'; +import { from } from 'rxjs'; import { ofType } from 'redux-observable'; -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { batchActions } from 'redux-batched-actions'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import { rulesExecutedPostUpdateField } from '../../../DataEntry/actions/dataEntry.actions'; @@ -16,6 +17,7 @@ import { getApplicableRuleEffectsForEventProgram, getApplicableRuleEffectsForTrackerProgram, updateRulesEffects, + validateAssignEffects, type FieldData, } from '../../../../rules'; import { getStageFromEvent } from '../../../../metaData/helpers/getStageFromEvent'; @@ -23,8 +25,9 @@ import { EventProgram, TrackerProgram } from '../../../../metaData/Program'; import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; import { prepareEnrollmentEventsForRulesEngine } from '../../../../events/prepareEnrollmentEvents'; import { getEnrollmentForRulesEngine, getAttributeValuesForRulesEngine } from '../../helpers'; +import type { QuerySingleResource } from '../../../../utils/api'; -const runRulesForEditSingleEvent = ({ +const runRulesForEditSingleEvent = async ({ store, dataEntryId, itemId, @@ -32,6 +35,7 @@ const runRulesForEditSingleEvent = ({ orgUnit, fieldData, programId, + querySingleResource, }: { store: ReduxStore, dataEntryId: string, @@ -40,6 +44,7 @@ const runRulesForEditSingleEvent = ({ programId: string, orgUnit: OrgUnit, fieldData?: ?FieldData, + querySingleResource: QuerySingleResource }) => { const state = store.value; const formId = getDataEntryKey(dataEntryId, itemId); @@ -91,33 +96,56 @@ const runRulesForEditSingleEvent = ({ }); } + const effectsWithValidations = await validateAssignEffects({ + dataElements: foundation.getElements(), + effects, + querySingleResource, + }); + return batchActions([ - updateRulesEffects(effects, formId), + updateRulesEffects(effectsWithValidations, formId), rulesExecutedPostUpdateField(dataEntryId, itemId, uid), ], editEventDataEntryBatchActionTypes.RULES_EFFECTS_ACTIONS_BATCH); }; -export const runRulesOnUpdateDataEntryFieldForEditSingleEventEpic = (action$: InputObservable, store: ReduxStore) => +export const runRulesOnUpdateDataEntryFieldForEditSingleEventEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => // $FlowSuppress action$.pipe( ofType(editEventDataEntryBatchActionTypes.UPDATE_DATA_ENTRY_FIELD_EDIT_SINGLE_EVENT_ACTION_BATCH), map(actionBatch => actionBatch.payload.find(action => action.type === editEventDataEntryActionTypes.START_RUN_RULES_ON_UPDATE), ), - map((action) => { + switchMap((action) => { const { dataEntryId, itemId, uid, orgUnit, programId } = action.payload; - return runRulesForEditSingleEvent({ store, dataEntryId, itemId, uid, orgUnit, programId }); + const runRulesForEditSingleEventPromise = runRulesForEditSingleEvent({ + store, + dataEntryId, + itemId, + uid, + orgUnit, + programId, + querySingleResource, + }); + return from(runRulesForEditSingleEventPromise); })); -export const runRulesOnUpdateFieldForEditSingleEventEpic = (action$: InputObservable, store: ReduxStore) => +export const runRulesOnUpdateFieldForEditSingleEventEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => // $FlowSuppress action$.pipe( ofType(editEventDataEntryBatchActionTypes.UPDATE_FIELD_EDIT_SINGLE_EVENT_ACTION_BATCH), map(actionBatch => actionBatch.payload.find(action => action.type === editEventDataEntryActionTypes.START_RUN_RULES_ON_UPDATE), ), - map((action) => { + switchMap((action) => { const { elementId, value, @@ -133,7 +161,7 @@ export const runRulesOnUpdateFieldForEditSingleEventEpic = (action$: InputObserv value, valid: uiState.valid, }; - return runRulesForEditSingleEvent({ + const runRulesForEditSingleEventPromise = runRulesForEditSingleEvent({ store, dataEntryId, itemId, @@ -141,6 +169,8 @@ export const runRulesOnUpdateFieldForEditSingleEventEpic = (action$: InputObserv orgUnit, fieldData, programId, + querySingleResource, }); + return from(runRulesForEditSingleEventPromise); })); 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 410292ce17..c5117776e5 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,7 +1,7 @@ // @flow import i18n from '@dhis2/d2-i18n'; import { hasValue } from 'capture-core-utils/validators/form'; -import { isValidDate } from '../../../../utils/validators/form'; +import { isValidDate } from '../../../../utils/validation/validators/form'; const preValidateDate = (value?: ?string, internalComponentError: ?{error?: ?string, errorCode?: ?string}) => { if (!value) { diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js index 34a6eba39f..3fd1630ac2 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js @@ -1,16 +1,21 @@ // @flow +import { v4 as uuid } from 'uuid'; import React, { useState, useCallback } from 'react'; import { useDispatch } from 'react-redux'; +import { useDataEngine } from '@dhis2/app-runtime'; +import { makeQuerySingleResource } from 'capture-core/utils/api'; import type { Props } from './dataEntry.types'; import { DataEntryComponent } from './DataEntry.component'; import { useLifecycle, useFormValidations } from './hooks'; import { getUpdateFieldActions, updateTeiRequest, setTeiModalError } from './dataEntry.actions'; +import { startRunRulesPostUpdateField } from '../../DataEntry'; export const DataEntry = ({ programAPI, orgUnitId, onCancel, onDisable, + onEnable, clientAttributesWithSubvalues, userRoles, modalState, @@ -24,9 +29,21 @@ export const DataEntry = ({ }: Props) => { const dataEntryId = 'trackedEntityProfile'; const itemId = 'edit'; + const dataEngine = useDataEngine(); + const querySingleResource = makeQuerySingleResource(dataEngine.query.bind(dataEngine)); const dispatch = useDispatch(); const [saveAttempted, setSaveAttempted] = useState(false); + const onGetValidationContext = useCallback( + () => ({ + programId: programAPI.id, + orgUnitId, + trackedEntityInstanceId, + trackedEntityTypeId: programAPI.trackedEntityType.id, + }), + [programAPI, orgUnitId, trackedEntityInstanceId], + ); + const context = useLifecycle({ programAPI, orgUnitId, @@ -41,8 +58,19 @@ export const DataEntry = ({ const { formValidated, errorsMessages, warningsMessages } = useFormValidations(dataEntryId, itemId, saveAttempted); const onUpdateFormField = useCallback( - (...args: Array) => dispatch(getUpdateFieldActions(context, ...args)), - [dispatch, context], + (innerAction: ReduxAction) => { + const uid = uuid(); + onDisable(); + dispatch(startRunRulesPostUpdateField(dataEntryId, itemId, uid)); + + getUpdateFieldActions({ context, querySingleResource, onGetValidationContext, innerAction, uid }).then( + (actions) => { + onEnable(); + return dispatch(actions); + }, + ); + }, + [dispatch, querySingleResource, context, onGetValidationContext, onDisable, onEnable], ); const onUpdateFormFieldAsync = useCallback( (innerAction: ReduxAction) => { @@ -50,15 +78,6 @@ export const DataEntry = ({ }, [dispatch], ); - const getValidationContext = useCallback( - () => ({ - programId: programAPI.id, - orgUnitId, - trackedEntityInstanceId, - trackedEntityTypeId: programAPI.trackedEntityType.id, - }), - [programAPI, orgUnitId, trackedEntityInstanceId], - ); const onSave = useCallback(() => { setSaveAttempted(true); @@ -107,7 +126,7 @@ export const DataEntry = ({ onUpdateFormField={onUpdateFormField} onUpdateFormFieldAsync={onUpdateFormFieldAsync} modalState={modalState} - onGetValidationContext={getValidationContext} + onGetValidationContext={onGetValidationContext} errorsMessages={errorsMessages} warningsMessages={warningsMessages} orgUnit={{ id: orgUnitId }} diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/getRulesActionsForTEI.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/getRulesActionsForTEI.js index 74aa721f08..03c2aff309 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/getRulesActionsForTEI.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/getRulesActionsForTEI.js @@ -12,7 +12,13 @@ import type { } from '@dhis2/rules-engine-javascript'; import { rulesEngine } from '../../../../rules/rulesEngine'; import type { RenderFoundation } from '../../../../metaData'; -import { updateRulesEffects, postProcessRulesEffects, buildEffectsHierarchy } from '../../../../rules'; +import { + updateRulesEffects, + postProcessRulesEffects, + buildEffectsHierarchy, + validateAssignEffects, +} from '../../../../rules'; +import type { QuerySingleResource } from '../../../../utils/api'; const getEnrollmentForRulesExecution = enrollment => enrollment && { @@ -36,12 +42,48 @@ const getDataElementsForRulesExecution = (dataElements: ?DataElements) => {}, ); -const getRulesActions = (rulesEffects: OutputEffects, foundation: RenderFoundation, formId: string) => { - const effectsHierarchy = buildEffectsHierarchy(postProcessRulesEffects(rulesEffects, foundation)); - return [updateRulesEffects(effectsHierarchy, formId)]; +export const getRulesActionsForTEI = ({ + foundation, + formId, + orgUnit, + enrollmentData, + teiValues, + trackedEntityAttributes, + optionSets, + rulesContainer, + otherEvents, + dataElements, + userRoles, +}: { + foundation: RenderFoundation, + formId: string, + orgUnit: OrgUnit, + enrollmentData?: ?Enrollment, + teiValues?: ?TEIValues, + trackedEntityAttributes: ?TrackedEntityAttributes, + optionSets: OptionSets, + rulesContainer: ProgramRulesContainer, + otherEvents?: ?EventsData, + dataElements: ?DataElements, + userRoles: Array, +}) => { + const effects: OutputEffects = rulesEngine.getProgramRuleEffects({ + programRulesContainer: rulesContainer, + currentEvent: null, + otherEvents, + dataElements: getDataElementsForRulesExecution(dataElements), + trackedEntityAttributes, + selectedEnrollment: getEnrollmentForRulesExecution(enrollmentData), + selectedEntity: teiValues, + selectedOrgUnit: orgUnit, + selectedUserRoles: userRoles, + optionSets, + }); + const effectsHierarchy = buildEffectsHierarchy(postProcessRulesEffects(effects, foundation)); + return updateRulesEffects(effectsHierarchy, formId); }; -export const getRulesActionsForTEI = ({ +export const getRulesActionsForTEIAsync = async ({ foundation, formId, orgUnit, @@ -53,6 +95,8 @@ export const getRulesActionsForTEI = ({ otherEvents, dataElements, userRoles, + querySingleResource, + onGetValidationContext, }: { foundation: RenderFoundation, formId: string, @@ -65,6 +109,8 @@ export const getRulesActionsForTEI = ({ otherEvents?: ?EventsData, dataElements: ?DataElements, userRoles: Array, + querySingleResource: QuerySingleResource, + onGetValidationContext: () => Object, }) => { const effects: OutputEffects = rulesEngine.getProgramRuleEffects({ programRulesContainer: rulesContainer, @@ -78,5 +124,12 @@ export const getRulesActionsForTEI = ({ selectedUserRoles: userRoles, optionSets, }); - return getRulesActions(effects, foundation, formId); + const effectsHierarchy = buildEffectsHierarchy(postProcessRulesEffects(effects, foundation)); + const effectsWithValidations = await validateAssignEffects({ + dataElements: foundation.getElements(), + effects: effectsHierarchy, + querySingleResource, + onGetValidationContext, + }); + return updateRulesEffects(effectsWithValidations, formId); }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/index.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/index.js index a088da2ddb..494f821921 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/index.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/ProgramRules/index.js @@ -1,3 +1,3 @@ // @flow export { buildRulesContainer } from './rulesContainer'; -export { getRulesActionsForTEI } from './getRulesActionsForTEI'; +export { getRulesActionsForTEI, getRulesActionsForTEIAsync } from './getRulesActionsForTEI'; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.actions.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.actions.js index 331f6e1a4d..f326ab200c 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.actions.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.actions.js @@ -1,5 +1,4 @@ // @flow -import { v4 as uuid } from 'uuid'; import { batchActions } from 'redux-batched-actions'; import type { OrgUnit, @@ -18,10 +17,10 @@ import type { FieldData } from '../../../rules'; import { getCurrentClientValues } from '../../../rules'; import { loadNewDataEntry } from '../../DataEntry/actions/dataEntryLoadNew.actions'; import { rulesExecutedPostUpdateField } from '../../DataEntry/actions/dataEntry.actions'; -import { startRunRulesPostUpdateField } from '../../DataEntry'; -import { getRulesActionsForTEI } from './ProgramRules'; +import { getRulesActionsForTEIAsync } from './ProgramRules'; import { addFormData } from '../../D2Form/actions/form.actions'; import type { Geometry } from './helpers/types'; +import type { QuerySingleResource } from '../../../utils/api'; export const TEI_MODAL_STATE = { OPEN: 'Open', @@ -65,8 +64,19 @@ type Context = { state: ReduxState, }; -export const getUpdateFieldActions = (context: Context, innerAction: ReduxAction) => { - const uid = uuid(); +export const getUpdateFieldActions = async ({ + context, + querySingleResource, + onGetValidationContext, + innerAction, + uid, +}: { + context: Context, + querySingleResource: QuerySingleResource, + onGetValidationContext: () => Object, + innerAction: ReduxAction, + uid: string +}) => { const { orgUnit, trackedEntityAttributes, @@ -87,7 +97,7 @@ export const getUpdateFieldActions = (context: Context, innerAction: ReduxAction }; const formId = `${dataEntryId}-${itemId}`; const currentTEIValues = getCurrentClientValues(state, formFoundation, formId, fieldData); - const rulesActions = getRulesActionsForTEI({ + const rulesActions = await getRulesActionsForTEIAsync({ foundation: formFoundation, formId, orgUnit, @@ -99,14 +109,15 @@ export const getUpdateFieldActions = (context: Context, innerAction: ReduxAction otherEvents, dataElements, userRoles, + querySingleResource, + onGetValidationContext, }); return batchActions( [ innerAction, - ...rulesActions, + rulesActions, rulesExecutedPostUpdateField(dataEntryId, itemId, uid), - startRunRulesPostUpdateField(dataEntryId, itemId, uid), ], dataEntryActionTypes.UPDATE_FIELD_PROFILE_ACTION_BATCH, ); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js index ddb4f533c9..a5a7ef9c4c 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js @@ -13,7 +13,7 @@ export type PlainProps = {| formFoundation: any, onCancel: () => void, onSave: () => void, - onUpdateFormField: () => void, + onUpdateFormField: (innerAction: ReduxAction) => void, onUpdateFormFieldAsync: (innerAction: ReduxAction) => void, onGetValidationContext: () => Object, modalState: string, @@ -29,6 +29,7 @@ export type Props = {| dataEntryFormConfig: ?DataEntryFormConfig, onCancel: () => void, onDisable: () => void, + onEnable: () => void, clientAttributesWithSubvalues: Array, trackedEntityInstanceId: string, onSaveSuccessActionType?: string, diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js index 0847eda23d..16a8ff9cea 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js @@ -89,7 +89,7 @@ export const useLifecycle = ({ Object.entries(rulesContainer).length > 0 ) { dispatch( - ...getRulesActionsForTEI({ + getRulesActionsForTEI({ foundation: formFoundation, formId: `${dataEntryId}-${itemId}`, orgUnit, diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js index f63800dd43..c94ed1cdb6 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js @@ -140,6 +140,8 @@ const WidgetProfilePlain = ({ ); }; + const handleOnDisable = useCallback(() => setTeiModalState(TEI_MODAL_STATE.OPEN_DISABLE), [setTeiModalState]); + const handleOnEnable = useCallback(() => setTeiModalState(TEI_MODAL_STATE.OPEN), [setTeiModalState]); return (
@@ -179,7 +181,8 @@ const WidgetProfilePlain = ({ <> setTeiModalState(TEI_MODAL_STATE.CLOSE)} - onDisable={() => setTeiModalState(TEI_MODAL_STATE.OPEN_DISABLE)} + onDisable={handleOnDisable} + onEnable={handleOnEnable} programAPI={program} dataEntryFormConfig={dataEntryFormConfig} orgUnitId={orgUnitId} 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 8a12223d6b..219e354ded 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.js @@ -1,7 +1,7 @@ // @flow import i18n from '@dhis2/d2-i18n'; import { isValidOrgUnit } from '../../../../capture-core-utils/validators/form'; -import { isValidDate } from '../../../../capture-core/utils/validators/form'; +import { isValidDate } from '../../../utils/validation/validators/form'; import { actions as RelatedStageModes } from '../constants'; type Props = { diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/helpers/eventFilters/apiEventFilterToClientConfigConverter/convertToClientConfig.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/helpers/eventFilters/apiEventFilterToClientConfigConverter/convertToClientConfig.js index 42bdd39aee..d34d81ab1e 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/helpers/eventFilters/apiEventFilterToClientConfigConverter/convertToClientConfig.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/helpers/eventFilters/apiEventFilterToClientConfigConverter/convertToClientConfig.js @@ -26,7 +26,8 @@ import type { ColumnsMetaForDataFetching, ClientConfig, } from '../../../types'; -import { areRelativeRangeValuesSupported } from '../../../../../../utils/validators/areRelativeRangeValuesSupported'; +import { areRelativeRangeValuesSupported } + from '../../../../../../utils/validation/validators/areRelativeRangeValuesSupported'; const getTextFilter = (filter: ApiDataFilterText): TextFilterData => { const value = filter.like; diff --git a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/helpers/TEIFilters/apiTEIFilterToClientConfigConverter/convertToClientFilters.js b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/helpers/TEIFilters/apiTEIFilterToClientConfigConverter/convertToClientFilters.js index 6730b1eba4..33863ea4f5 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/helpers/TEIFilters/apiTEIFilterToClientConfigConverter/convertToClientFilters.js +++ b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/helpers/TEIFilters/apiTEIFilterToClientConfigConverter/convertToClientFilters.js @@ -19,7 +19,8 @@ import type { ApiTrackerQueryCriteria, TeiColumnsMetaForDataFetching, } from '../../../types'; -import { areRelativeRangeValuesSupported } from '../../../../../../utils/validators/areRelativeRangeValuesSupported'; +import { areRelativeRangeValuesSupported } + from '../../../../../../utils/validation/validators/areRelativeRangeValuesSupported'; import { DATE_TYPES, ASSIGNEE_MODES, MAIN_FILTERS } from '../../../constants'; import { ADDITIONAL_FILTERS } from '../../../helpers'; diff --git a/src/core_modules/capture-core/components/WorkingLists/WorkingListsCommon/helpers/buildFilterQueryArgs/filterConverters/dateConverter.js b/src/core_modules/capture-core/components/WorkingLists/WorkingListsCommon/helpers/buildFilterQueryArgs/filterConverters/dateConverter.js index c5c59e7675..4930ba75bb 100644 --- a/src/core_modules/capture-core/components/WorkingLists/WorkingListsCommon/helpers/buildFilterQueryArgs/filterConverters/dateConverter.js +++ b/src/core_modules/capture-core/components/WorkingLists/WorkingListsCommon/helpers/buildFilterQueryArgs/filterConverters/dateConverter.js @@ -8,7 +8,8 @@ import type { RelativeDateFilterData, AbsoluteDateFilterData, } from '../../../../../ListView'; -import { areRelativeRangeValuesSupported } from '../../../../../../utils/validators/areRelativeRangeValuesSupported'; +import { areRelativeRangeValuesSupported } + from '../../../../../../utils/validation/validators/areRelativeRangeValuesSupported'; const periods = { TODAY: 'TODAY', diff --git a/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js index 30dd29a15f..bed2120f6e 100644 --- a/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js @@ -1,6 +1,6 @@ // @flow import { effectActions } from '@dhis2/rules-engine-javascript'; -import type { AssignOutputEffect } from '@dhis2/rules-engine-javascript'; +import type { AssignOutputEffectWithValidations } from '../../rules'; import { createReducerDescription } from '../../trackerRedux'; import { asyncHandlerActionTypes } from '../../components/D2Form'; import { actionTypes as fieldActionTypes } from '../../components/D2Form/D2SectionFields.actions'; @@ -63,7 +63,7 @@ export const formsValuesDesc = createReducerDescription({ return newState; }, [rulesEffectsActionTypes.UPDATE_RULES_EFFECTS]: (state, action) => { - const assignEffects: { [id: string]: Array } = + const assignEffects: { [id: string]: Array } = action.payload.rulesEffects && action.payload.rulesEffects[effectActions.ASSIGN_VALUE]; if (!assignEffects) { return state; @@ -76,7 +76,7 @@ export const formsValuesDesc = createReducerDescription({ ...state[payload.formId], ...Object.keys(assignEffects).reduce((acc, id) => { const effectsForId = assignEffects[id]; - const value = effectsForId[effectsForId.length - 1].value; + const value = effectsForId[0].value; acc[id] = value; return acc; }, {}), @@ -206,24 +206,29 @@ export const formsSectionsFieldsUIDesc = createReducerDescription({ [rulesEffectsActionTypes.UPDATE_RULES_EFFECTS]: (state, action) => { const { formId, rulesEffects } = action.payload; const formSectionFields = state[formId]; - const assignEffects: { [id: string]: Array } = + const assignEffects: { [id: string]: Array } = rulesEffects && rulesEffects[effectActions.ASSIGN_VALUE]; - if (!assignEffects || !formSectionFields) { + if (!assignEffects) { return state; } const updatedFields = Object.keys(assignEffects).reduce((acc, id) => { - if (formSectionFields[id]) { + const effect = assignEffects[id][0]; + const isEffectWithValidations = effect.hasOwnProperty('valid'); + if (formSectionFields?.[id] && isEffectWithValidations) { acc[id] = { - valid: true, - errorData: undefined, - errorMessage: undefined, - errorType: undefined, - modified: true, + valid: effect.valid, + errorData: effect.errorData, + errorMessage: effect.errorMessage, + errorType: effect.errorType, touched: true, validatingMessage: null, }; + } else { + acc[id] = { + touched: true, + }; } return acc; }, {}); diff --git a/src/core_modules/capture-core/rules/index.js b/src/core_modules/capture-core/rules/index.js index 7eedf9ff6d..96467328c2 100644 --- a/src/core_modules/capture-core/rules/index.js +++ b/src/core_modules/capture-core/rules/index.js @@ -10,3 +10,5 @@ export type { FieldData } from './inputHelpers'; export { postProcessRulesEffects } from './postProcessRulesEffects'; export { buildEffectsHierarchy } from './buildEffectsHierarchy'; export { filterApplicableRuleEffects } from './filterApplicableRuleEffects'; +export { validateAssignEffects } from './validateAssignEffects'; +export type { AssignOutputEffectWithValidations } from './validateAssignEffects'; diff --git a/src/core_modules/capture-core/rules/validateAssignEffects.js b/src/core_modules/capture-core/rules/validateAssignEffects.js new file mode 100644 index 0000000000..62f71694d7 --- /dev/null +++ b/src/core_modules/capture-core/rules/validateAssignEffects.js @@ -0,0 +1,63 @@ +// @flow +import { errorCreator } from 'capture-core-utils'; +import { effectActions } from '@dhis2/rules-engine-javascript'; +import log from 'loglevel'; +import type { AssignOutputEffect } from '@dhis2/rules-engine-javascript'; +import { type DataElement } from '../metaData'; +import type { QuerySingleResource } from '../utils/api'; +import { getValidators } from '../utils/validation/getValidators'; +import type { Validations } from '../utils/validation/validateValue'; +import { validateValue } from '../utils/validation/validateValue'; + +export type AssignOutputEffectWithValidations = { + [metaDataId: string]: Array, +}; + +export const validateAssignEffects = async ({ + dataElements, + effects, + querySingleResource, + onGetValidationContext, +}: { + dataElements: Array, + effects: Object, + querySingleResource: QuerySingleResource, + onGetValidationContext?: () => Object, +}): Promise => { + const assignEffects: {| [metaDataId: string]: Array |} = effects[effectActions.ASSIGN_VALUE]; + if (!assignEffects) { + return effects; + } + + const assignEffectsWithValidations = await dataElements.reduce(async (passPromise, metaData: DataElement) => { + const acc = await passPromise; + if (!assignEffects[metaData.id]) { + return acc; + } + + const effectsForId = assignEffects[metaData.id]; + const lastIndex = effectsForId.length - 1; + const value = effectsForId[lastIndex].value; + const validators = getValidators(metaData, querySingleResource); + const validationContext = onGetValidationContext && onGetValidationContext(); + + try { + const validatorResult = await validateValue({ validators, value, validationContext }); + const effectWithValidation = Object.assign({}, effectsForId[lastIndex], validatorResult); + + acc[metaData.id] = [effectWithValidation]; + return acc; + } catch (error) { + log.error( + errorCreator('an error occured while validating the assigned program rule effect')({ + metaData, + lastIndex, + error, + }), + ); + return acc; + } + }, Promise.resolve({})); + + return { ...effects, [effectActions.ASSIGN_VALUE]: assignEffectsWithValidations }; +}; diff --git a/src/core_modules/capture-core/components/D2Form/field/validators/constants/index.js b/src/core_modules/capture-core/utils/validation/constants/index.js similarity index 100% rename from src/core_modules/capture-core/components/D2Form/field/validators/constants/index.js rename to src/core_modules/capture-core/utils/validation/constants/index.js diff --git a/src/core_modules/capture-core/components/D2Form/field/validators/constants/validatorTypes.const.js b/src/core_modules/capture-core/utils/validation/constants/validatorTypes.const.js similarity index 100% rename from src/core_modules/capture-core/components/D2Form/field/validators/constants/validatorTypes.const.js rename to src/core_modules/capture-core/utils/validation/constants/validatorTypes.const.js diff --git a/src/core_modules/capture-core/components/D2Form/field/validators/getValidators.js b/src/core_modules/capture-core/utils/validation/getValidators.js similarity index 94% rename from src/core_modules/capture-core/components/D2Form/field/validators/getValidators.js rename to src/core_modules/capture-core/utils/validation/getValidators.js index f9fb35be3c..7f65ac02c1 100644 --- a/src/core_modules/capture-core/components/D2Form/field/validators/getValidators.js +++ b/src/core_modules/capture-core/utils/validation/getValidators.js @@ -25,12 +25,19 @@ import { getDateRangeValidator, getDateTimeRangeValidator, getTimeRangeValidator, -} from '../../../../utils/validators/form'; -import { dataElementTypes, type DateDataElement, type DataElement } from '../../../../metaData'; +} from './validators/form'; +import { dataElementTypes, type DateDataElement, type DataElement } from '../../metaData'; import { validatorTypes } from './constants'; -import type { QuerySingleResource } from '../../../../utils/api/api.types'; +import type { QuerySingleResource } from '../../utils/api/api.types'; -type Validator = (value: any) => Promise | boolean | { valid: boolean, errorMessage?: any}; +type Validator = ( + value: any, + validationContext: ?Object, + internalError?: ?{ + error?: ?string, + errorCode?: ?string, + } +) => Promise | boolean | { valid: boolean, errorMessage?: any, data?: any }; export type ValidatorContainer = { validator: Validator, @@ -205,7 +212,7 @@ const validatorsForTypes = { }], }; -function buildTypeValidators(metaData: DataElement | DateDataElement): ?Array { +function buildTypeValidators(metaData: DataElement | DateDataElement): Array { // $FlowFixMe dataElementTypes flow error let validatorContainersForType = validatorsForTypes[metaData.type] ? validatorsForTypes[metaData.type] : []; @@ -226,7 +233,7 @@ function buildTypeValidators(metaData: DataElement | DateDataElement): ?Array { +function buildCompulsoryValidator(metaData: DataElement): Array { return metaData.compulsory ? [ @@ -243,7 +250,7 @@ function buildCompulsoryValidator(metaData: DataElement): Array { +): Array { return metaData.unique ? [ @@ -265,7 +272,7 @@ function buildUniqueValidator( } export const getValidators = -(metaData: DataElement, querySingleResource: QuerySingleResource): Array => [ +(metaData: DataElement, querySingleResource: QuerySingleResource): Array => [ buildCompulsoryValidator, buildTypeValidators, buildUniqueValidator, diff --git a/src/core_modules/capture-core/utils/validation/index.js b/src/core_modules/capture-core/utils/validation/index.js new file mode 100644 index 0000000000..aa54c5ad24 --- /dev/null +++ b/src/core_modules/capture-core/utils/validation/index.js @@ -0,0 +1,6 @@ +// @flow +export { getValidators } from './getValidators'; +export { validateValue } from './validateValue'; +export { validatorTypes } from './constants'; + +export type { ValidatorContainer } from './getValidators'; diff --git a/src/core_modules/capture-core/utils/validation/validateValue.js b/src/core_modules/capture-core/utils/validation/validateValue.js new file mode 100644 index 0000000000..c91bf08fda --- /dev/null +++ b/src/core_modules/capture-core/utils/validation/validateValue.js @@ -0,0 +1,70 @@ +// @flow +import type { ValidatorContainer } from './getValidators'; +import type { FieldCommitOptionsExtended } from '../../components/D2Form'; + +export type Validations = { + valid: boolean, + errorMessage?: ?string, + errorType?: ?string, + errorData?: Object, +}; + +export const validateValue = async ({ + validators, + value, + validationContext, + postProcessAsyncValidatonInitiation, + commitOptions, +}: { + validators?: Array, + value: any, + validationContext: ?Object, + postProcessAsyncValidatonInitiation?: Function, + commitOptions?: ?FieldCommitOptionsExtended, +}): Promise => { + if (!validators || validators.length === 0) { + return { + valid: true, + }; + } + + const validatorResult = await validators.reduce(async (passPromise, currentValidator) => { + const pass = await passPromise; + if (pass === true) { + let result = currentValidator.validator( + value, + { error: commitOptions?.error, errorCode: commitOptions?.errorCode }, + validationContext, + ); + if (result instanceof Promise) { + result = postProcessAsyncValidatonInitiation + ? postProcessAsyncValidatonInitiation(currentValidator.validatingMessage, result) + : result; + result = await result; + } + + if (result === true || (result && result.valid)) { + return true; + } + return { + message: (result && result.errorMessage) || currentValidator.message, + type: currentValidator.type, + data: result && result.data, + }; + } + return pass; + }, Promise.resolve(true)); + + if (validatorResult !== true) { + return { + valid: false, + errorMessage: validatorResult.message, + errorType: validatorResult.type, + errorData: validatorResult.data, + }; + } + + return { + valid: true, + }; +}; diff --git a/src/core_modules/capture-core/utils/validators/areRelativeRangeValuesSupported.js b/src/core_modules/capture-core/utils/validation/validators/areRelativeRangeValuesSupported.js similarity index 100% rename from src/core_modules/capture-core/utils/validators/areRelativeRangeValuesSupported.js rename to src/core_modules/capture-core/utils/validation/validators/areRelativeRangeValuesSupported.js diff --git a/src/core_modules/capture-core/utils/validators/form/ageValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/ageValidator.js similarity index 100% rename from src/core_modules/capture-core/utils/validators/form/ageValidator.js rename to src/core_modules/capture-core/utils/validation/validators/form/ageValidator.js diff --git a/src/core_modules/capture-core/utils/validators/form/dateTimeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/dateTimeValidator.js similarity index 98% rename from src/core_modules/capture-core/utils/validators/form/dateTimeValidator.js rename to src/core_modules/capture-core/utils/validation/validators/form/dateTimeValidator.js index 43adf8627c..f35bd67c25 100644 --- a/src/core_modules/capture-core/utils/validators/form/dateTimeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/dateTimeValidator.js @@ -13,7 +13,8 @@ type ValidationResult = { errorMessage?: { timeError?: ?string, dateError?: ?string - } + }, + data?: any, }; const CUSTOM_VALIDATION_MESSAGES = { diff --git a/src/core_modules/capture-core/utils/validators/form/dateValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/dateValidator.js similarity index 100% rename from src/core_modules/capture-core/utils/validators/form/dateValidator.js rename to src/core_modules/capture-core/utils/validation/validators/form/dateValidator.js diff --git a/src/core_modules/capture-core/utils/validators/form/getDateRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js similarity index 94% rename from src/core_modules/capture-core/utils/validators/form/getDateRangeValidator.js rename to src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js index e82e219f27..c76d738184 100644 --- a/src/core_modules/capture-core/utils/validators/form/getDateRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateRangeValidator.js @@ -1,6 +1,6 @@ // @flow import { isValidDate } from './dateValidator'; -import { parseDate } from '../../converters/date'; +import { parseDate } from '../../../converters/date'; /** * * @export diff --git a/src/core_modules/capture-core/utils/validators/form/getDateTimeRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js similarity index 97% rename from src/core_modules/capture-core/utils/validators/form/getDateTimeRangeValidator.js rename to src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js index 0953479053..154749b666 100644 --- a/src/core_modules/capture-core/utils/validators/form/getDateTimeRangeValidator.js +++ b/src/core_modules/capture-core/utils/validation/validators/form/getDateTimeRangeValidator.js @@ -1,6 +1,6 @@ // @flow import { isValidDateTime } from './dateTimeValidator'; -import { parseDate } from '../../converters/date'; +import { parseDate } from '../../../converters/date'; function isValidDateTimeWithEmptyCheck(value: ?Object) { return value && isValidDateTime(value); diff --git a/src/core_modules/capture-core/utils/validators/form/getNumberRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getNumberRangeValidator.js similarity index 100% rename from src/core_modules/capture-core/utils/validators/form/getNumberRangeValidator.js rename to src/core_modules/capture-core/utils/validation/validators/form/getNumberRangeValidator.js diff --git a/src/core_modules/capture-core/utils/validators/form/getTimeRangeValidator.js b/src/core_modules/capture-core/utils/validation/validators/form/getTimeRangeValidator.js similarity index 100% rename from src/core_modules/capture-core/utils/validators/form/getTimeRangeValidator.js rename to src/core_modules/capture-core/utils/validation/validators/form/getTimeRangeValidator.js diff --git a/src/core_modules/capture-core/utils/validators/form/index.js b/src/core_modules/capture-core/utils/validation/validators/form/index.js similarity index 100% rename from src/core_modules/capture-core/utils/validators/form/index.js rename to src/core_modules/capture-core/utils/validation/validators/form/index.js diff --git a/src/core_modules/capture-core/utils/validators/form/isValidNonFutureDate.js b/src/core_modules/capture-core/utils/validation/validators/form/isValidNonFutureDate.js similarity index 100% rename from src/core_modules/capture-core/utils/validators/form/isValidNonFutureDate.js rename to src/core_modules/capture-core/utils/validation/validators/form/isValidNonFutureDate.js