diff --git a/packages/rules-engine/src/d2Functions/getD2Functions.types.js b/packages/rules-engine/src/d2Functions/getD2Functions.types.js index cb2fbcd0f5..4bba6395a1 100644 --- a/packages/rules-engine/src/d2Functions/getD2Functions.types.js +++ b/packages/rules-engine/src/d2Functions/getD2Functions.types.js @@ -8,7 +8,7 @@ import type { export type D2FunctionsInput = $ReadOnly<{| dateUtils: IDateUtils, variablesHash: RuleVariables, - selectedOrgUnit: OrgUnit, + selectedOrgUnit: ?OrgUnit, selectedUserRoles: Array, |}>; diff --git a/packages/rules-engine/src/rulesEngine.types.js b/packages/rules-engine/src/rulesEngine.types.js index 5d3d5bbf47..fd25b9e251 100644 --- a/packages/rules-engine/src/rulesEngine.types.js +++ b/packages/rules-engine/src/rulesEngine.types.js @@ -156,7 +156,7 @@ export type RulesEngineInput = {| selectedEntity?: ?TEIValues, trackedEntityAttributes?: ?TrackedEntityAttributes, selectedEnrollment?: ?Enrollment, - selectedOrgUnit: OrgUnit, + selectedOrgUnit: ?OrgUnit, selectedUserRoles?: ?Array, optionSets: OptionSets, |} diff --git a/packages/rules-engine/src/services/VariableService/VariableService.js b/packages/rules-engine/src/services/VariableService/VariableService.js index 9b596a8190..4e2ef49c8c 100644 --- a/packages/rules-engine/src/services/VariableService/VariableService.js +++ b/packages/rules-engine/src/services/VariableService/VariableService.js @@ -37,7 +37,7 @@ type SourceData = { selectedEntity: ?TEIValues, selectedEnrollment: ?Enrollment, optionSets: OptionSets, - selectedOrgUnit: OrgUnit, + selectedOrgUnit: ?OrgUnit, }; const variableSourceTypesDataElementSpecific = { @@ -516,7 +516,7 @@ export class VariableService { return variables; } - getOrganisationContextVariables(orgUnit: OrgUnit) { + getOrganisationContextVariables(orgUnit: ?OrgUnit) { const variables = {}; variables.orgunit_code = this.buildContextVariable(orgUnit?.code, typeKeys.TEXT); return variables; diff --git a/packages/rules-engine/src/services/VariableService/variableService.types.js b/packages/rules-engine/src/services/VariableService/variableService.types.js index 2977933c29..01e1810ff0 100644 --- a/packages/rules-engine/src/services/VariableService/variableService.types.js +++ b/packages/rules-engine/src/services/VariableService/variableService.types.js @@ -85,7 +85,7 @@ export type VariableServiceInput = {| selectedEntity: ?TEIValues, trackedEntityAttributes: ?TrackedEntityAttributes, selectedEnrollment: ?Enrollment, - selectedOrgUnit: OrgUnit, + selectedOrgUnit: ?OrgUnit, optionSets: OptionSets, constants: ?Constants, |}; 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 1444a98aea..b2f34f533c 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 @@ -237,7 +237,7 @@ const getGeometrySettings = () => ({ dialogLabel: i18n.t('Area'), required: false, orientation: getOrientation(props.formHorizontal), - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); } @@ -248,7 +248,7 @@ const getGeometrySettings = () => ({ required: false, orientation: getOrientation(props.formHorizontal), shrinkDisabled: props.formHorizontal, - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); }, getPropName: () => 'geometry', @@ -329,6 +329,7 @@ type FinalTeiDataEntryProps = { programId: string, id: string, orgUnitId: string, + orgUnit: OrgUnit, onUpdateDataEntryField: Function, onUpdateFormFieldAsync: Function, onUpdateFormField: Function, @@ -457,6 +458,7 @@ export class EnrollmentDataEntryComponent extends React.Component ); diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js index cff88329d0..c085023578 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js @@ -128,7 +128,7 @@ const getStageGeometrySettings = () => ({ dialogLabel: i18n.t('Area'), required: false, orientation: getOrientation(props.formHorizontal), - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); } @@ -139,7 +139,7 @@ const getStageGeometrySettings = () => ({ required: false, orientation: getOrientation(props.formHorizontal), shrinkDisabled: props.formHorizontal, - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); }, getPropName: () => stageMainDataIds.GEOMETRY, diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js index b831b9f197..7160ac4787 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js @@ -9,7 +9,7 @@ const getSectionId = sectionId => (sectionId === Section.MAIN_SECTION_ID ? `${Section.MAIN_SECTION_ID}-stage` : sectionId); export const EnrollmentWithFirstStageDataEntry = (props: Props) => { - const { firstStageMetaData, ...passOnProps } = props; + const { firstStageMetaData, orgUnit, ...passOnProps } = props; const { stage: { stageForm: firstStageFormFoundation, name: stageName }, } = firstStageMetaData; @@ -21,6 +21,8 @@ export const EnrollmentWithFirstStageDataEntry = (props: Props) => { return ( diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.types.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.types.js index 6c2a054867..99ae963893 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.types.js @@ -1,4 +1,5 @@ // @flow +import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import type { ProgramStage, RenderFoundation } from '../../../../metaData'; export type Props = { @@ -6,4 +7,5 @@ export type Props = { stage: ProgramStage, }, formFoundation: RenderFoundation, + orgUnit: ?OrgUnit, }; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js index 3743fa5dc4..546aca4224 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js @@ -236,7 +236,7 @@ const buildGeometrySettingsFn = () => ({ dialogLabel: i18n.t('Area'), required: false, orientation: getOrientation(props.formHorizontal), - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); } @@ -247,7 +247,7 @@ const buildGeometrySettingsFn = () => ({ required: false, orientation: getOrientation(props.formHorizontal), shrinkDisabled: props.formHorizontal, - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); }, getPropName: () => 'geometry', @@ -637,6 +637,7 @@ class NewEventDataEntry extends Component { dataEntrySections={this.dataEntrySections} relationshipsRef={this.setRelationshipsInstance} orgUnit={orgUnit} + orgUnitId={orgUnit?.id} {...passOnProps} /> diff --git a/src/core_modules/capture-core/components/DataEntry/index.js b/src/core_modules/capture-core/components/DataEntry/index.js index 97ce464d1a..b27e7eb6c8 100644 --- a/src/core_modules/capture-core/components/DataEntry/index.js +++ b/src/core_modules/capture-core/components/DataEntry/index.js @@ -30,4 +30,5 @@ export { } from './actions/dataEntry.actions'; export { actionTypes as loadNewActionTypes } from './actions/dataEntryLoadNew.actions'; export { actionTypes as loadEditActionTypes, cleanUpDataEntry } from './actions/dataEntry.actions'; +export { getDataEntryKey } from './common/getDataEntryKey'; diff --git a/src/core_modules/capture-core/components/FormFields/New/HOC/withCenterPoint.js b/src/core_modules/capture-core/components/FormFields/New/HOC/withCenterPoint.js index 04c542c051..a702ea22b5 100644 --- a/src/core_modules/capture-core/components/FormFields/New/HOC/withCenterPoint.js +++ b/src/core_modules/capture-core/components/FormFields/New/HOC/withCenterPoint.js @@ -1,59 +1,115 @@ // @flow -import React, { type ComponentType, useMemo, useState } from 'react'; +import React, { type ComponentType, useMemo, useState, useEffect } from 'react'; +import { ceil } from 'lodash'; import { useApiMetadataQuery } from 'capture-core/utils/reactQueryHelpers'; +type Props = { + orgUnitId: ?string, +}; + const DEFAULT_CENTER = [51.505, -0.09]; const convertToClientCoordinates = ({ coordinates, type }: { coordinates: any[], type: string }) => { switch (type) { case 'Point': return [coordinates[1], coordinates[0]]; - case 'Polygon': - return coordinates[0][0]; + case 'Polygon': { + // Calculate a center point by finding the min and max values for longitude and latitude + // and getting the mean of those values + const { minLatitude, maxLatitude, minLongitude, maxLongitude } = coordinates[0] + .reduce((accExtremes, [iLongitude, iLatitude]) => { + if (iLatitude > accExtremes.maxLatitude) { + accExtremes.maxLatitude = iLatitude; + } else if (iLatitude < accExtremes.minLatitude) { + accExtremes.minLatitude = iLatitude; + } + + if (iLongitude > accExtremes.maxLongitude) { + accExtremes.maxLongitude = iLongitude; + } else if (iLongitude < accExtremes.minLongitude) { + accExtremes.minLongitude = iLongitude; + } + + return accExtremes; + }, { + minLatitude: coordinates[0][0][1], + maxLatitude: coordinates[0][0][1], + minLongitude: coordinates[0][0][0], + maxLongitude: coordinates[0][0][0], + }); + + const latitude = ceil((maxLatitude + minLatitude) / 2, 6); + const longitude = ceil((maxLongitude + minLongitude) / 2, 6); + + return [latitude, longitude]; + } default: return DEFAULT_CENTER; } }; -const getCenterPoint = (InnerComponent: ComponentType) => (props: Object) => { - const { orgUnit, ...passOnProps } = props; - if (!orgUnit || !orgUnit.id) { - return {}} />; - } - const [orgUnitKey, setOrgUnitKey] = useState(orgUnit.id); - const [shouldFetch, setShouldFetch] = useState(false); - const queryKey = ['organisationUnit', 'geometry', orgUnitKey]; +const getCenterPoint = (InnerComponent: ComponentType) => ({ orgUnitId, ...passOnProps }: Props) => { + const [orgUnitFetchId, setOrgUnitFetchId] = useState(orgUnitId); + const [fetchEnabled, setFetchEnabled] = useState(false); + + useEffect(() => { + setOrgUnitFetchId(orgUnitId); + }, [orgUnitId]); + + const queryKey = ['organisationUnit', 'geometry', orgUnitFetchId]; const queryFn = { resource: 'organisationUnits', - id: () => orgUnitKey, + id: () => orgUnitFetchId, params: { fields: 'geometry,parent', }, }; const queryOptions = useMemo( - () => ({ enabled: Boolean(orgUnit.id) && shouldFetch }), - [shouldFetch, orgUnit.id], + () => ({ enabled: Boolean(orgUnitFetchId) && fetchEnabled }), + [fetchEnabled, orgUnitFetchId], ); + + // $FlowFixMe When the query is disabled, the prerequisites for the queryKey and the queryFn are not met. const { data } = useApiMetadataQuery(queryKey, queryFn, queryOptions); + useEffect(() => { + if (data?.parent && !data?.geometry) { + setOrgUnitFetchId(data.parent.id); + } + }, [data]); + const center = useMemo(() => { + if (!orgUnitFetchId) { + return DEFAULT_CENTER; + } if (data) { - const { geometry, parent } = data; - if (geometry) { - return convertToClientCoordinates(geometry); - } else if (parent?.id) { - setOrgUnitKey(parent.id); + if (data.geometry) { + return convertToClientCoordinates(data.geometry); + } + if (data.parent) { + return null; } return DEFAULT_CENTER; } - return undefined; - }, [data]); + return null; + }, [data, orgUnitFetchId]); const onOpenMap = (hasValue) => { - setShouldFetch(!hasValue); + setFetchEnabled(!hasValue); }; - return ; + const onCloseMap = () => { + setFetchEnabled(false); + }; + + return ( + + ); }; export const withCenterPoint = () => (InnerComponent: ComponentType) => getCenterPoint(InnerComponent); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js index f07359bc2b..d8ca9d25cd 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.js @@ -35,6 +35,7 @@ import { } from '../../FormFields/New'; import { Assignee } from './Assignee'; import { inMemoryFileStore } from '../../DataEntry/file/inMemoryFileStore'; +import { SavingText } from '../SavingText'; import { addEventSaveTypes } from './addEventSaveTypes'; import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { withDataEntryFieldIfApplicable } from '../../DataEntry/dataEntryField/withDataEntryFieldIfApplicable'; @@ -266,7 +267,7 @@ const buildGeometrySettingsFn = () => ({ dialogLabel: i18n.t('Area'), required: false, orientation: getOrientation(props.formHorizontal), - orgUnit: props.orgUnit, + orgUnitId: props.orgUnitIdFieldValue, }); } @@ -277,7 +278,7 @@ const buildGeometrySettingsFn = () => ({ required: false, orientation: getOrientation(props.formHorizontal), shrinkDisabled: props.formHorizontal, - orgUnit: props.orgUnit, + orgUnitId: props.orgUnitIdFieldValue, }); }, getPropName: () => 'geometry', @@ -393,7 +394,14 @@ const getCategoryOptionsSettingsFn = () => { const dataEntryFilterProps = (props: Object) => { - const { stage, onScrollToRelationships, recentlyAddedRelationshipId, relationshipsRef, ...passOnProps } = props; + const { + stage, + onScrollToRelationships, + recentlyAddedRelationshipId, + relationshipsRef, + orgUnitIdFieldValue, + ...passOnProps + } = props; return passOnProps; }; @@ -409,6 +417,12 @@ const WrappedDataEntry = compose( withFilterProps(dataEntryFilterProps), )(DataEntryContainer); +type OrgUnit = {| + id: string, + name: string, + path: string, +|}; + type Props = { id: string, orgUnitId: string, @@ -432,6 +446,9 @@ type Props = { theme: Theme, formHorizontal: ?boolean, recentlyAddedRelationshipId?: ?string, + placementDomNodeForSavingText?: HTMLElement, + programName: string, + orgUnitFieldValue: ?OrgUnit, }; type DataEntrySection = { placement: $Values, @@ -507,10 +524,16 @@ class DataEntryPlain extends Component { onSetSaveTypes, theme, id, + placementDomNodeForSavingText, + programName, + stage, + orgUnitFieldValue, ...passOnProps } = this.props; + return (
+ {/* the props orgUnit, orgUnitId and selectedOrgUnitId should all be removed from here. See DHIS2-18869 */} {/* $FlowFixMe[cannot-spread-inexact] automated comment */} { fieldOptions={this.fieldOptions} dataEntrySections={this.dataEntrySections} relationshipsRef={this.setRelationshipsInstance} + stage={stage} + orgUnitIdFieldValue={orgUnitFieldValue?.id} + orgUnit={orgUnitFieldValue} + orgUnitId={orgUnitFieldValue?.id} + selectedOrgUnitId={orgUnitFieldValue?.id} {...passOnProps} /> +
); } diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.js index aa16f0c93b..1e9811e132 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.js @@ -4,7 +4,7 @@ import { v4 as uuid } from 'uuid'; import { useDispatch, useSelector } from 'react-redux'; import { batchActions } from 'redux-batched-actions'; import { DataEntryComponent } from './DataEntry.component'; -import { startRunRulesPostUpdateField } from '../../DataEntry'; +import { startRunRulesPostUpdateField, getDataEntryKey } from '../../DataEntry'; import { startAsyncUpdateFieldForNewEvent, executeRulesOnUpdateForNewEvent, @@ -15,9 +15,13 @@ import { import typeof { addEventSaveTypes } from './addEventSaveTypes'; import type { ContainerProps } from './dataEntry.types'; -export const DataEntry = ({ orgUnit, rulesExecutionDependenciesClientFormatted, ...passOnProps }: ContainerProps) => { +export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...passOnProps }: ContainerProps) => { const dispatch = useDispatch(); - const { orgUnitId, programId } = useSelector(({ currentSelections }) => currentSelections); + const { programId } = useSelector(({ currentSelections }) => currentSelections); + const dataEntryItemId = useSelector(({ dataEntries }) => dataEntries[id] && dataEntries[id].itemId); + const dataEntryKey = getDataEntryKey(id, dataEntryItemId); + const orgUnitFieldValue = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey].orgUnit); + const onUpdateDataEntryField = useCallback((innerAction: ReduxAction) => { const { dataEntryId, itemId } = innerAction.payload; const uid = uuid(); @@ -25,9 +29,9 @@ export const DataEntry = ({ orgUnit, rulesExecutionDependenciesClientFormatted, dispatch(batchActions([ innerAction, startRunRulesPostUpdateField(dataEntryId, itemId, uid), - executeRulesOnUpdateForNewEvent({ ...innerAction.payload, uid, orgUnit, rulesExecutionDependenciesClientFormatted }), + executeRulesOnUpdateForNewEvent({ ...innerAction.payload, uid, rulesExecutionDependenciesClientFormatted }), ], newEventWidgetDataEntryBatchActionTypes.UPDATE_DATA_ENTRY_FIELD_ADD_EVENT_ACTION_BATCH)); - }, [dispatch, orgUnit, rulesExecutionDependenciesClientFormatted]); + }, [dispatch, rulesExecutionDependenciesClientFormatted]); const onUpdateField = useCallback((innerAction: ReduxAction) => { const { dataEntryId, itemId } = innerAction.payload; @@ -36,9 +40,9 @@ export const DataEntry = ({ orgUnit, rulesExecutionDependenciesClientFormatted, dispatch(batchActions([ innerAction, startRunRulesPostUpdateField(dataEntryId, itemId, uid), - executeRulesOnUpdateForNewEvent({ ...innerAction.payload, uid, orgUnit, rulesExecutionDependenciesClientFormatted }), + executeRulesOnUpdateForNewEvent({ ...innerAction.payload, uid, rulesExecutionDependenciesClientFormatted }), ], newEventWidgetDataEntryBatchActionTypes.FIELD_UPDATE_BATCH)); - }, [dispatch, orgUnit, rulesExecutionDependenciesClientFormatted]); + }, [dispatch, rulesExecutionDependenciesClientFormatted]); const onStartAsyncUpdateField = useCallback(( innerAction: ReduxAction, @@ -50,13 +54,13 @@ export const DataEntry = ({ orgUnit, rulesExecutionDependenciesClientFormatted, return batchActions([ successInnerAction, startRunRulesPostUpdateField(dataEntryId, itemId, uid), - executeRulesOnUpdateForNewEvent({ ...successInnerAction.payload, dataEntryId, itemId, uid, orgUnit, rulesExecutionDependenciesClientFormatted }), + executeRulesOnUpdateForNewEvent({ ...successInnerAction.payload, dataEntryId, itemId, uid, rulesExecutionDependenciesClientFormatted }), ], newEventWidgetDataEntryBatchActionTypes.FIELD_UPDATE_BATCH); }; const onAsyncUpdateError = (errorInnerAction: ReduxAction) => errorInnerAction; dispatch(startAsyncUpdateFieldForNewEvent(innerAction, onAsyncUpdateSuccess, onAsyncUpdateError)); - }, [dispatch, orgUnit, rulesExecutionDependenciesClientFormatted]); + }, [dispatch, rulesExecutionDependenciesClientFormatted]); const onAddNote = useCallback((itemId: string, dataEntryId: string, note: string) => { dispatch(addNewEventNote(itemId, dataEntryId, note)); @@ -65,12 +69,11 @@ export const DataEntry = ({ orgUnit, rulesExecutionDependenciesClientFormatted, const onSetSaveTypes = useCallback((newSaveTypes: ?Array<$Values>) => { dispatch(setNewEventSaveTypes(newSaveTypes)); }, [dispatch]); - return ( void, rulesExecutionDependenciesClientFormatted: RulesExecutionDependenciesClientFormatted, onSaveAndCompleteEnrollment: (enrollment: Object) => void, + placementDomNodeForSavingText?: HTMLElement, + programName: string, |}; export type Props = $Diff; 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 f582f16368..50b1b20275 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 @@ -3,7 +3,6 @@ import { ofType } from 'redux-observable'; 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'; import { getTrackerProgramThrowIfNotFound } from '../../../../metaData/helpers'; import { rulesExecutedPostUpdateField } from '../../../DataEntry/actions/dataEntry.actions'; @@ -22,6 +21,7 @@ import { import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; import type { RulesExecutionDependenciesClientFormatted } from '../../common.types'; import { getLocationQuery } from '../../../../utils/routing'; +import { getCoreOrgUnitFn, orgUnitFetched } from '../../../../metadataRetrieval/coreOrgUnit'; import type { QuerySingleResource } from '../../../../utils/api'; const runRulesForNewEvent = async ({ @@ -29,7 +29,6 @@ const runRulesForNewEvent = async ({ dataEntryId, itemId, uid, - orgUnit, rulesExecutionDependenciesClientFormatted, fieldData, querySingleResource, @@ -38,7 +37,6 @@ const runRulesForNewEvent = async ({ dataEntryId: string, itemId: string, uid: string, - orgUnit: OrgUnit, rulesExecutionDependenciesClientFormatted: RulesExecutionDependenciesClientFormatted, fieldData?: ?FieldData, querySingleResource: QuerySingleResource, @@ -59,11 +57,14 @@ const runRulesForNewEvent = async ({ const currentEventValues = getCurrentClientValues(state, foundation, formId, fieldData); const currentEventMainData = getCurrentClientMainData(state, itemId, dataEntryId, foundation); const currentEvent = { ...currentEventValues, ...currentEventMainData, programStageId }; + const { coreOrgUnit, cached } = + // $FlowFixMe + await getCoreOrgUnitFn(querySingleResource)(currentEvent.orgUnit?.id, store.value.organisationUnits); const effects = getApplicableRuleEffectsForTrackerProgram({ program, stage, - orgUnit, + orgUnit: coreOrgUnit, currentEvent, otherEvents: events, attributeValues, @@ -79,6 +80,7 @@ const runRulesForNewEvent = async ({ return batchActions([ updateRulesEffects(effectsWithValidations, formId), rulesExecutedPostUpdateField(dataEntryId, itemId, uid), + ...(coreOrgUnit && !cached ? [orgUnitFetched(coreOrgUnit)] : []), ], newEventWidgetDataEntryBatchActionTypes.RULES_EFFECTS_ACTIONS_BATCH, ); @@ -95,13 +97,12 @@ export const runRulesOnUpdateDataEntryFieldForNewEnrollmentEventEpic = ( actionBatch.payload .find(action => action.type === newEventWidgetDataEntryActionTypes.RULES_ON_UPDATE_EXECUTE)), switchMap((action) => { - const { dataEntryId, itemId, uid, orgUnit, rulesExecutionDependenciesClientFormatted } = action.payload; + const { dataEntryId, itemId, uid, rulesExecutionDependenciesClientFormatted } = action.payload; const runRulesForNewEventPromise = runRulesForNewEvent({ store, dataEntryId, itemId, uid, - orgUnit, rulesExecutionDependenciesClientFormatted, querySingleResource, }); @@ -126,7 +127,6 @@ export const runRulesOnUpdateFieldForNewEnrollmentEventEpic = ( elementId, value, uiState, - orgUnit, rulesExecutionDependenciesClientFormatted, } = action.payload; @@ -141,7 +141,6 @@ export const runRulesOnUpdateFieldForNewEnrollmentEventEpic = ( dataEntryId, itemId, uid, - orgUnit, rulesExecutionDependenciesClientFormatted, fieldData, querySingleResource, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getRulesActions.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getRulesActions.js index d8aaa50247..ea6eb4c7c6 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getRulesActions.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getRulesActions.js @@ -28,7 +28,7 @@ export const getRulesActions = ({ formFoundation: RenderFoundation, dataEntryId: string, itemId: string, - orgUnit: OrgUnit, + orgUnit: ?OrgUnit, eventsRulesDependency: EnrollmentEvents, attributesValuesRulesDependency: AttributeValuesClientFormatted, enrollmentDataRulesDependency: EnrollmentData, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.js index e6ab9c974c..5a89482dbc 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.js @@ -22,7 +22,7 @@ export const OrgUnitFetcher = ({ return ( ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/SavingText.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/SavingText.component.js index 4c0db5aa17..90ca327618 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/SavingText.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/SavingText.component.js @@ -1,17 +1,20 @@ // @flow import React from 'react'; +import ReactDOM from 'react-dom'; import i18n from '@dhis2/d2-i18n'; import { InfoIconText } from '../../InfoIconText'; import type { Props } from './savingText.types'; -export const SavingText = ({ orgUnitName, stageName, programName }: Props) => ( - - - {orgUnitName - ? i18n.t('Saving to {{stageName}} for {{programName}} in {{orgUnitName}}', - { orgUnitName, stageName, programName, interpolation: { escapeValue: false } }) - : i18n.t('Saving to {{stageName}} for {{programName}}', - { stageName, programName, interpolation: { escapeValue: false } })} - - -); +export const SavingText = ({ orgUnitName, stageName, programName, placementDomNode }: Props) => + (placementDomNode ? + ReactDOM.createPortal(( + + + {orgUnitName + ? i18n.t('Saving to {{stageName}} for {{programName}} in {{orgUnitName}}', + { orgUnitName, stageName, programName, interpolation: { escapeValue: false } }) + : i18n.t('Saving to {{stageName}} for {{programName}}', + { stageName, programName, interpolation: { escapeValue: false } })} + + + ), placementDomNode) : null); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/savingText.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/savingText.types.js index ad3ca4d9b3..45cd2d74eb 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/savingText.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/SavingText/savingText.types.js @@ -4,4 +4,5 @@ export type Props = {| orgUnitName?: string, stageName: string, programName: string, + placementDomNode?: HTMLElement, |}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js index 724df8ab7e..b1861cc402 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js @@ -5,8 +5,8 @@ import withStyles from '@material-ui/core/styles/withStyles'; import { Widget } from '../../Widget'; import { DataEntry } from '../DataEntry'; import { FinishButtons } from '../FinishButtons'; -import { SavingText } from '../SavingText'; import { WidgetRelatedStages } from '../../WidgetRelatedStages'; +import { usePlacementDomNode } from '../../../utils/portal/usePlacementDomNode'; import type { Props } from './validated.types'; const styles = () => ({ @@ -27,45 +27,51 @@ const ValidatedPlain = ({ relatedStageRef, onSave, onCancel, - orgUnit, id, ...passOnProps -}: Props) => ( - - } - > - {ready && ( +}: Props) => { + const { domRef: savingTextRef, domNode: savingTextDomNode } = usePlacementDomNode(); + + return ( + + } + >
- - - - + {ready && ( + <> + + + + + )} +
- )} - -); + + ); +}; export const ValidatedComponent: ComponentType< $Diff, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js index b5d03ddaf4..82bf84900b 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js @@ -22,7 +22,7 @@ import { createServerData, useBuildNewEventPayload } from './useBuildNewEventPay const SaveHandlerHOC = withSaveHandler()(ValidatedComponent); const AskToCreateNewHandlerHOC = withAskToCreateNew()(SaveHandlerHOC); -const DataEntry = withAskToCompleteEnrollment()(AskToCreateNewHandlerHOC); +const ValidatedComponentWrapper = withAskToCompleteEnrollment()(AskToCreateNewHandlerHOC); export const Validated = ({ program, @@ -31,7 +31,7 @@ export const Validated = ({ onSaveExternal, onSaveSuccessActionType, onSaveErrorActionType, - orgUnit, + orgUnitContext, teiId, enrollmentId, rulesExecutionDependencies, @@ -61,7 +61,7 @@ export const Validated = ({ program, stage, formFoundation, - orgUnit, + orgUnitContext, dataEntryId, itemId, // $FlowFixMe Investigate @@ -135,7 +135,7 @@ export const Validated = ({ dispatch(startCreateNewAfterCompleting({ enrollmentId, isCreateNew, - orgUnitId: orgUnit?.id, + orgUnitId: orgUnitContext?.id, programId: program.id, teiId, availableProgramStages, @@ -143,7 +143,7 @@ export const Validated = ({ } catch (error) { // Related stages has displayed an error message. No need to do anything here. } - }, [handleSave, formFoundation, dispatch, enrollmentId, orgUnit?.id, program.id, teiId, availableProgramStages]); + }, [handleSave, formFoundation, dispatch, enrollmentId, orgUnitContext?.id, program.id, teiId, availableProgramStages]); const handleSaveAndCompleteEnrollment = useCallback( @@ -170,12 +170,11 @@ export const Validated = ({ }, [dispatch]); return ( - ); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.js index c460bcd970..92a68bf095 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.js @@ -12,7 +12,7 @@ export const useLifecycle = ({ program, stage, formFoundation, - orgUnit, + orgUnitContext, dataEntryId, itemId, rulesExecutionDependenciesClientFormatted: { @@ -24,7 +24,7 @@ export const useLifecycle = ({ program: TrackerProgram, stage: ProgramStage, formFoundation: RenderFoundation, - orgUnit?: OrgUnit, + orgUnitContext?: OrgUnit, dataEntryId: string, itemId: string, rulesExecutionDependenciesClientFormatted: RulesExecutionDependenciesClientFormatted, @@ -39,12 +39,12 @@ export const useLifecycle = ({ useEffect(() => { if (!isLoading) { dispatch(batchActions([ - ...getOpenDataEntryActions(dataEntryId, itemId, programCategory, orgUnit), + ...getOpenDataEntryActions(dataEntryId, itemId, programCategory, orgUnitContext), ])); dataEntryReadyRef.current = true; delayRulesExecutionRef.current = true; } - }, [dispatch, dataEntryId, itemId, program, formFoundation, isLoading, programCategory, orgUnit]); + }, [dispatch, dataEntryId, itemId, program, formFoundation, isLoading, programCategory, orgUnitContext]); const eventsRef = useRef(); const attributesRef = useRef(); @@ -55,7 +55,7 @@ export const useLifecycle = ({ // Refactor the helper methods (getCurrentClientValues, getCurrentClientMainData in rules/actionsCreator) to be more explicit with the arguments. const state = useSelector(stateArg => stateArg); useEffect(() => { - if (isLoading || !orgUnit) { return; } + if (isLoading) { return; } if (delayRulesExecutionRef.current) { // getRulesActions depends on settings in the redux store that are being managed through getOpenDataEntryActions. // The purpose of the following lines of code is to make sure the redux store is ready before calling getRulesActions. @@ -70,7 +70,7 @@ export const useLifecycle = ({ formFoundation, dataEntryId, itemId, - orgUnit, + orgUnit: orgUnitContext, eventsRulesDependency, attributesValuesRulesDependency, enrollmentDataRulesDependency, @@ -84,7 +84,7 @@ export const useLifecycle = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [ dispatch, - orgUnit, + orgUnitContext, eventsRulesDependency, attributesValuesRulesDependency, program, @@ -96,12 +96,9 @@ export const useLifecycle = ({ ]); const rulesReady = - (!orgUnit) || - ( eventsRef.current === eventsRulesDependency && attributesRef.current === attributesValuesRulesDependency && - enrollmentDataRef.current === enrollmentDataRulesDependency - ); + enrollmentDataRef.current === enrollmentDataRulesDependency; return dataEntryReadyRef.current && rulesReady; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js index 0aa4c1292b..4bd5741708 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js @@ -83,8 +83,8 @@ export const saveEvents = ({ serverData, onSaveErrorActionType, onSaveSuccessAct }, }); -export const startCreateNewAfterCompleting = ({ enrollmentId, isCreateNew, orgUnitId, programId, teiId, availableProgramStages }: Object) => - actionCreator(newEventWidgetActionTypes.START_CREATE_NEW_AFTER_COMPLETING)({ enrollmentId, isCreateNew, orgUnitId, programId, teiId, availableProgramStages }); +export const startCreateNewAfterCompleting = ({ enrollmentId, isCreateNew, orgUnitIdContext, programId, teiId, availableProgramStages }: Object) => + actionCreator(newEventWidgetActionTypes.START_CREATE_NEW_AFTER_COMPLETING)({ enrollmentId, isCreateNew, orgUnitIdContext, programId, teiId, availableProgramStages }); export const cleanUpEventSaveInProgress = () => actionCreator(newEventWidgetActionTypes.CLEAN_UP_EVENT_SAVE_IN_PROGRESS)(); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js index 2dda46426b..2800623428 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js @@ -40,7 +40,7 @@ export type RelatedStageRefPayload = {| export type ContainerProps = {| ...CommonValidatedProps, - orgUnit?: OrgUnit, + orgUnitContext?: OrgUnit, |}; export type Props = {| diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js index e958a69644..7c2103b71e 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js @@ -252,7 +252,7 @@ const buildGeometrySettingsFn = () => ({ label: i18n.t('Area'), dialogLabel: i18n.t('Area'), required: false, - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); } return createComponentProps(props, { @@ -260,7 +260,7 @@ const buildGeometrySettingsFn = () => ({ label: i18n.t('Coordinate'), dialogLabel: i18n.t('Coordinate'), required: false, - orgUnit: props.orgUnit, + orgUnitId: props.orgUnit?.id, }); }, getPropName: () => 'geometry', @@ -513,6 +513,7 @@ class EditEventDataEntryPlain extends Component { fieldOptions={this.fieldOptions} dataEntrySections={this.dataEntrySections} orgUnit={orgUnit} + orgUnitId={orgUnit?.id} programId={programId} selectedOrgUnitId={orgUnit?.id} {...passOnProps} diff --git a/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/getCoreOrgUnitFn.js b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/getCoreOrgUnitFn.js new file mode 100644 index 0000000000..317d2f2220 --- /dev/null +++ b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/getCoreOrgUnitFn.js @@ -0,0 +1,17 @@ +// @flow +import { fetchCoreOrgUnit } from './fetchCoreOrgUnit'; +import type { CoreOrgUnit } from './coreOrgUnit.types'; +import type { QuerySingleResource } from '../../utils/api'; + +export const getCoreOrgUnitFn = (querySingleResource: QuerySingleResource) => + async (orgUnitId?: string, cachedOrgUnitsCore: { [orgUnitId: string]: CoreOrgUnit }) => { + if (!orgUnitId) { + return { coreOrgUnit: null, cached: false }; + } + const cachedOrgUnit = cachedOrgUnitsCore[orgUnitId]; + if (cachedOrgUnit) { + return { coreOrgUnit: cachedOrgUnit, cached: true }; + } + const fetchedOrgUnit = await fetchCoreOrgUnit(orgUnitId, querySingleResource); + return { coreOrgUnit: fetchedOrgUnit, cached: false }; + }; diff --git a/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/index.js b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/index.js index ee09984e67..7aa2fd6104 100644 --- a/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/index.js +++ b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/index.js @@ -1,5 +1,6 @@ // @flow export { useCoreOrgUnit } from './useCoreOrgUnit'; -export { getCoreOrgUnit } from './coreOrgUnit.actions'; +export { getCoreOrgUnit, orgUnitFetched } from './coreOrgUnit.actions'; export { getCoreOrgUnitEpic } from './getCoreOrgUnit.epics'; +export { getCoreOrgUnitFn } from './getCoreOrgUnitFn'; export type { CoreOrgUnit } from './coreOrgUnit.types'; diff --git a/src/core_modules/capture-core/rules/rules.types.js b/src/core_modules/capture-core/rules/rules.types.js index 0249ee664c..07f870ae4b 100644 --- a/src/core_modules/capture-core/rules/rules.types.js +++ b/src/core_modules/capture-core/rules/rules.types.js @@ -14,7 +14,7 @@ import type { ProgramStage, TrackerProgram, EventProgram, RenderFoundation } fro export type GetApplicableRuleEffectsForTrackerProgramInput = {| program: TrackerProgram, stage?: ProgramStage, - orgUnit: OrgUnit, + orgUnit: ?OrgUnit, currentEvent?: EventData, otherEvents?: EventsData, attributeValues?: TEIValues, @@ -29,7 +29,7 @@ export type GetApplicableRuleEffectsForEventProgramInput = {| |}; export type GetApplicableRuleEffectsInput = {| - orgUnit: OrgUnit, + orgUnit: ?OrgUnit, currentEvent?: EventData, otherEvents?: EventsData, attributeValues?: TEIValues, diff --git a/src/core_modules/capture-core/utils/portal/index.js b/src/core_modules/capture-core/utils/portal/index.js new file mode 100644 index 0000000000..14fd5ee2e9 --- /dev/null +++ b/src/core_modules/capture-core/utils/portal/index.js @@ -0,0 +1,2 @@ +// @flow +export { usePlacementDomNode } from './usePlacementDomNode'; diff --git a/src/core_modules/capture-core/utils/portal/usePlacementDomNode.js b/src/core_modules/capture-core/utils/portal/usePlacementDomNode.js new file mode 100644 index 0000000000..0ed2b70b16 --- /dev/null +++ b/src/core_modules/capture-core/utils/portal/usePlacementDomNode.js @@ -0,0 +1,19 @@ +// @flow +import { useState, useEffect, useRef } from 'react'; + +// Main use case is to get the placement DOM Node when using portals. +// The hook ensures a rerender after a reference to the DOMNode has been acquired. +export const usePlacementDomNode = () => { + const domRef = useRef(); + const [domNode, setDomNode] = useState(); + useEffect(() => { + if (domRef.current) { + setDomNode(domRef.current); + } + }, [setDomNode]); + + return { + domRef, + domNode, + }; +}; diff --git a/src/core_modules/capture-ui/CoordinateField/CoordinateField.component.js b/src/core_modules/capture-ui/CoordinateField/CoordinateField.component.js index 782b38b821..064b03c35d 100644 --- a/src/core_modules/capture-ui/CoordinateField/CoordinateField.component.js +++ b/src/core_modules/capture-ui/CoordinateField/CoordinateField.component.js @@ -20,7 +20,8 @@ type Coordinate = { type Props = { onBlur: (value: any) => void, - onOpenMap: (hasValue: boolean) => void, + onOpenMap?: (hasValue: boolean) => void, + onCloseMap?: () => void, orientation: $Values, center?: ?Array, onChange?: ?(value: any) => void, @@ -89,11 +90,12 @@ export class CoordinateField extends React.Component { } closeMap = () => { + this.props.onCloseMap?.(); this.setState({ showMap: false }); } openMap = () => { - this.props.onOpenMap(Boolean(this.props.value)); + this.props.onOpenMap?.(Boolean(this.props.value)); this.setState({ showMap: true, position: this.getPosition() }); } @@ -169,7 +171,10 @@ export class CoordinateField extends React.Component { } renderMap = () => { - const { position, zoom } = this.state; + const { position, zoom, showMap } = this.state; + if (!showMap || (!position && !this.props.center)) { + return null; + } const center = position || this.props.center; return (