diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f826365ee..9ba4827f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [100.70.9](https://github.com/dhis2/capture-app/compare/v100.70.8...v100.70.9) (2024-07-10) + + +### Bug Fixes + +* [DHIS2-8814] Table in custom form overflows container with no scrollbar ([#3655](https://github.com/dhis2/capture-app/issues/3655)) ([c39c397](https://github.com/dhis2/capture-app/commit/c39c397ec078b7a8e765a3847d48881ebd720005)) + +## [100.70.8](https://github.com/dhis2/capture-app/compare/v100.70.7...v100.70.8) (2024-07-09) + + +### Bug Fixes + +* [DHIS2-17398] improve unique ID search ([#3688](https://github.com/dhis2/capture-app/issues/3688)) ([183e419](https://github.com/dhis2/capture-app/commit/183e419000245173a5020a965e4d681ab0cfdf7f)) + +## [100.70.7](https://github.com/dhis2/capture-app/compare/v100.70.6...v100.70.7) (2024-07-09) + + +### Bug Fixes + +* [BETA-116][DHIS2-15896] validate values assigned from the rules engine ([#3612](https://github.com/dhis2/capture-app/issues/3612)) ([459ab9a](https://github.com/dhis2/capture-app/commit/459ab9a7fc0b0f3c3ce98e19a7acf213b35eb6e8)) + ## [100.70.6](https://github.com/dhis2/capture-app/compare/v100.70.5...v100.70.6) (2024-07-05) diff --git a/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.feature b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.feature index 740965833b..e1b4555a76 100644 --- a/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.feature +++ b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/EnrollmentEditEventPageForm.feature @@ -51,6 +51,8 @@ Then the user see the following text: Low-dose acetylsalicylic acid given When the user sets Plurality assessed to Singleton Then the user don't see the following text: Low-dose acetylsalicylic acid given +# DHIS2-17730 +@skip Scenario: User can modify and save the data in the form Given you land on the enrollment event page with selected Person by having typed /#/enrollmentEventEdit?orgUnitId=DiszpKrYNg8&eventId=V1CerIi3sdL Then the user see the following text: Enrollment: View Event diff --git a/i18n/en.pot b/i18n/en.pot index 090046525f..18b3dadbab 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -850,9 +850,6 @@ msgstr "Choose the {{missingCategories}} to start reporting" msgid "Save as new" msgstr "Save as new" -msgid "View enrollment" -msgstr "View enrollment" - msgid "New" msgstr "New" diff --git a/package.json b/package.json index 076630ace9..e6a60af020 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.70.6", + "version": "100.70.9", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.70.6", + "@dhis2/rules-engine-javascript": "100.70.9", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 464ab65099..a4386887ed 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.70.6", + "version": "100.70.9", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/src/core_modules/capture-core/components/D2Form/D2Section.component.js b/src/core_modules/capture-core/components/D2Form/D2Section.component.js index 0eb7207524..b20d307de0 100644 --- a/src/core_modules/capture-core/components/D2Form/D2Section.component.js +++ b/src/core_modules/capture-core/components/D2Form/D2Section.component.js @@ -112,6 +112,7 @@ class D2SectionPlain extends React.PureComponent { return (
{ 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 df14dbb598..6707d84ca7 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 @@ -1,6 +1,7 @@ // @flow /* eslint-disable complexity */ import i18n from '@dhis2/d2-i18n'; +import { isEqual } from 'lodash'; import log from 'loglevel'; import { v4 as uuid } from 'uuid'; import * as React from 'react'; @@ -40,6 +41,7 @@ type FieldUI = { errorType?: ?string, errorData?: ErrorData, validatingMessage?: ?string, + pendingValidation?: ?boolean, }; type RenderDividerFn = (index: number, fieldsCount: number, field: FieldConfig) => React.Node; @@ -108,6 +110,7 @@ export class FormBuilder extends React.Component { if (!validators || validators.length === 0) { return { valid: true, + pendingValidation: false, }; } @@ -139,11 +142,16 @@ export class FormBuilder extends React.Component { errorMessage: validatorResult.message, errorType: validatorResult.type, errorData: validatorResult.data, + pendingValidation: false, }; } return { valid: true, + errorMessage: null, + errorType: null, + errorData: null, + pendingValidation: false, }; } @@ -180,17 +188,19 @@ export class FormBuilder extends React.Component { }, asyncUIState); } - static executeValidateAllFields( + static executeValidateFields( props: Props, fieldsValidatingPromiseContainer: FieldsValidatingPromiseContainer, + customFields?: Array, ) { const { id, - fields, + fields: allFields, values, onGetValidationContext, onIsValidating, } = props; + const fields = customFields || allFields || []; const validationContext = onGetValidationContext && onGetValidationContext(); const validationPromises = fields .map(async (field) => { @@ -233,6 +243,7 @@ export class FormBuilder extends React.Component { valid: false, errorMessage: [i18n.t('error encountered during field validation')], errorType: i18n.t('error'), + pendingValidation: false, }; log.error({ reason, field }); } @@ -268,7 +279,7 @@ export class FormBuilder extends React.Component { this.commitUpdateTriggeredForFields = {}; if (this.props.validateIfNoUIData) { - this.validateAllFields(this.props); + this.validateFields(this.props); } } @@ -277,7 +288,7 @@ export class FormBuilder extends React.Component { this.asyncUIState = FormBuilder.getAsyncUIState(this.props.fieldsUI); this.commitUpdateTriggeredForFields = {}; if (this.props.validateIfNoUIData) { - this.validateAllFields(newProps); + this.validateFields(newProps); } } else { this.asyncUIState = @@ -285,6 +296,19 @@ export class FormBuilder extends React.Component { } } + componentDidUpdate(prevProps: Props) { + const { fieldsUI, fields } = this.props; + + if (!isEqual(prevProps.fieldsUI, fieldsUI)) { + const pendingValidationFields = Object.keys(fieldsUI).filter(key => fieldsUI[key].pendingValidation); + + if (pendingValidationFields.length !== 0 && !this.validateAllCancelablePromise) { + const fieldsToValidate = fields.filter(field => pendingValidationFields.includes(field.id)); + this.validateFields(this.props, fieldsToValidate); + } + } + } + getCleanUpData() { const remainingCompleteUids: Array = Object .keys(this.fieldsValidatingPromiseContainer) @@ -300,12 +324,13 @@ export class FormBuilder extends React.Component { return remainingCompleteUids; } - validateAllFields( + validateFields( props: Props, + customFields?: Array, ) { this.validateAllCancelablePromise && this.validateAllCancelablePromise.cancel(); this.validateAllCancelablePromise = makeCancelablePromise( - FormBuilder.executeValidateAllFields(props, this.fieldsValidatingPromiseContainer), + FormBuilder.executeValidateFields(props, this.fieldsValidatingPromiseContainer, customFields), ); this.validateAllCancelablePromise 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 7fc5b5c587..9a902bab07 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 @@ -392,6 +392,7 @@ type PreEnrollmentDataEntryProps = { teiId?: ?string, firstStageMetaData?: ?{ stage: ProgramStage }, formFoundation: RenderFoundation, + enrollmentMetadata: Enrollment, }; class PreEnrollmentDataEntryPure extends React.PureComponent { @@ -411,6 +412,7 @@ export class EnrollmentDataEntryComponent extends React.Component - {programId ? i18n.t('View enrollment') : i18n.t('View dashboard')} + {i18n.t('View dashboard')} ); }, []); diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.component.js index 6269d5ec2a..2c159e7aba 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.component.js @@ -63,6 +63,7 @@ const RegisterTeiPlain = ({ onCancel, onGetUnsavedAttributeValues, trackedEntityName, + trackedEntityTypeId, newRelationshipProgramId, classes, }: Props) => { @@ -111,6 +112,7 @@ const RegisterTeiPlain = ({ renderDuplicatesDialogActions={renderDuplicatesDialogActions} renderDuplicatesCardActions={renderDuplicatesCardActions} ExistingUniqueValueDialogActions={ExistingUniqueValueDialogActions} + trackedEntityTypeId={trackedEntityTypeId} /> dataEntries[dataEntryId]?.itemId); const error = useSelector(({ newRelationshipRegisterTei }) => (newRelationshipRegisterTei.error)); const newRelationshipProgramId = useNewRelationshipScopeId(); - const { trackedEntityName } = useScopeInfo(newRelationshipProgramId); + const { trackedEntityName, tetId: trackedEntityTypeId } = useScopeInfo(newRelationshipProgramId); return ( diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.types.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.types.js index d1dc483194..759abe2e6f 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.types.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.types.js @@ -3,6 +3,7 @@ type PropsFromRedux = {| dataEntryId: string, itemId: string, trackedEntityName: ?string, + trackedEntityTypeId: ?string, newRelationshipProgramId: string, error: string, |}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js index f778f527ef..fdc5b07a8a 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/actions/teiSearch.actions.js @@ -18,6 +18,7 @@ export const actionTypes = { TEI_EDIT_SEARCH: 'RelationshipsWidget.TeiEditSearch', TEI_SEARCH_RESULTS_CHANGE_PAGE: 'RelationshipsWidget.TeiSearchResultsChangePage', TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION: 'RelationshipsWidget.TeiSearchSetOpenSearchGroupSection', + SEARCH_TE_IN_TET_SCOPE: 'RelationshipsWidget.SearchTrackedEntityInTETScope', }; @@ -32,6 +33,27 @@ export const requestSearchTei = ( ) => actionCreator(actionTypes.REQUEST_SEARCH_TEI)({ formId, searchGroupId, searchId, resultsPageSize }); +export const searchViaUniqueIdOnScopeTrackedEntityType = ({ + formId, + searchGroupId, + searchId, + selectedProgramId, + programQueryArgs, +}: { + formId: string, + searchGroupId: string, + searchId: string, + selectedProgramId: string, + programQueryArgs: any, +}) => + actionCreator(actionTypes.SEARCH_TE_IN_TET_SCOPE)({ + formId, + searchGroupId, + searchId, + selectedProgramId, + programQueryArgs, + }); + export const searchTeiFailed = ( formId: string, searchGroupId: string, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js index 325ac72aa6..068d9d2349 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics.js @@ -16,6 +16,7 @@ import { searchTeiResultRetrieved, searchTeiFailed, setProgramAndTrackedEntityType, + searchViaUniqueIdOnScopeTrackedEntityType, } from '../actions/teiSearch.actions'; import { actionTypes as programSelectorActionTypes, @@ -43,6 +44,32 @@ const getContextQueryArgs = (programId: ?string, trackedEntityTypeId: string) => const getPagingQueryArgs = (pageNumber: ?number = 1) => ({ page: pageNumber, pageSize: 5 }); +export const searchTeiByTETIdEpic = ( + action$: InputObservable, + store: ReduxStore, + { absoluteApiPath, querySingleResource }: ApiUtils, +) => + action$.pipe( + ofType(actionTypes.SEARCH_TE_IN_TET_SCOPE), + switchMap((action) => { + const { selectedProgramId, searchId, formId, searchGroupId, programQueryArgs } = action.payload; + const { attributes, trackedEntityType } = getTrackerProgram(selectedProgramId); + const { program, ...restQueryArgs } = programQueryArgs; + const queryArgs = { ...restQueryArgs, trackedEntityType: trackedEntityType.id }; + return from(getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource)).pipe( + map( + ({ trackedEntityInstanceContainers, pagingData }) => + searchTeiResultRetrieved( + { trackedEntityInstanceContainers, currentPage: pagingData.currentPage }, + formId, + searchGroupId, + searchId, + ), + catchError(() => of(searchTeiFailed(formId, searchGroupId, searchId))), + ), + ); + }), + ); const searchTei = ({ state, @@ -100,16 +127,25 @@ const searchTei = ({ getTrackerProgram(selectedProgramId).attributes : getTrackedEntityType(selectedTrackedEntityTypeId).attributes; - return from( - getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource, selectedProgramId), - ).pipe( - map(({ trackedEntityInstanceContainers, pagingData }) => - searchTeiResultRetrieved( + return from(getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource, selectedProgramId)).pipe( + map(({ trackedEntityInstanceContainers, pagingData }) => { + if (searchGroup.unique && trackedEntityInstanceContainers.length === 0 && queryArgs.program) { + return searchViaUniqueIdOnScopeTrackedEntityType({ + selectedProgramId, + searchId, + formId, + searchGroupId, + programQueryArgs: queryArgs, + }); + } + + return searchTeiResultRetrieved( { trackedEntityInstanceContainers, currentPage: pagingData.currentPage }, formId, searchGroupId, searchId, - )), + ); + }), catchError(() => of(searchTeiFailed(formId, searchGroupId, searchId))), ); }; diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js index de084d12d8..3b72d98f5b 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js @@ -12,6 +12,7 @@ import { addSuccessResultsViewOnSearchBox, showTooManyResultsViewOnSearchBox, showFallbackNotEnoughAttributesOnSearchBox, + searchViaUniqueIdOnScopeTrackedEntityType, } from '../SearchBox.actions'; import { getTrackedEntityInstances, @@ -42,23 +43,33 @@ const searchViaUniqueIdStream = ({ programId, absoluteApiPath, querySingleResource, + formId, + programTETId, }: { queryArgs: any, attributes: any, programId?: string, absoluteApiPath: string, querySingleResource: QuerySingleResource, + formId?: string, + programTETId?: string, }) => from(getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource, programId)).pipe( flatMap(({ trackedEntityInstanceContainers }) => { const searchResults = trackedEntityInstanceContainers; + if (searchResults.length === 0 && queryArgs.program) { + return of(searchViaUniqueIdOnScopeTrackedEntityType({ trackedEntityTypeId: programTETId, formId })); + } if (searchResults.length > 0) { - const { id, tei: { orgUnit: orgUnitId } } = searchResults[0]; + const { id, tei: { orgUnit: orgUnitId, enrollments } } = searchResults[0]; + const programToNavigateTo = enrollments?.length === 1 && !programId + ? enrollments[0].program + : programId; return of(navigateToEnrollmentOverview({ teiId: id, orgUnitId, - programId, + programId: programToNavigateTo, })); } return of(showEmptyResultsViewOnSearchBox()); @@ -150,7 +161,7 @@ export const searchViaUniqueIdOnScopeProgramEpic = ( ouMode: 'ACCESSIBLE', }; - const attributes = getTrackerProgramThrowIfNotFound(programId).attributes; + const { attributes, trackedEntityType } = getTrackerProgramThrowIfNotFound(programId); return searchViaUniqueIdStream({ queryArgs, @@ -158,6 +169,8 @@ export const searchViaUniqueIdOnScopeProgramEpic = ( programId, absoluteApiPath, querySingleResource, + formId, + programTETId: trackedEntityType.id, }); }), ); @@ -179,6 +192,7 @@ export const searchViaUniqueIdOnScopeTrackedEntityTypeEpic = ( trackedEntityType: trackedEntityTypeId, pageNumber: 1, ouMode: 'ACCESSIBLE', + fields: 'trackedEntity,trackedEntityType,orgUnit,attributes,enrollments', }; const attributes = getTrackedEntityTypeThrowIfNotFound(trackedEntityTypeId).attributes; diff --git a/src/core_modules/capture-core/components/TeiSearch/actions/teiSearch.actions.js b/src/core_modules/capture-core/components/TeiSearch/actions/teiSearch.actions.js index 8644b09fcf..fe6e9cd90c 100644 --- a/src/core_modules/capture-core/components/TeiSearch/actions/teiSearch.actions.js +++ b/src/core_modules/capture-core/components/TeiSearch/actions/teiSearch.actions.js @@ -18,6 +18,7 @@ export const actionTypes = { TEI_EDIT_SEARCH: 'TeiEditSearch', TEI_SEARCH_RESULTS_CHANGE_PAGE: 'TeiSearchResultsChangePage', TEI_SEARCH_SET_OPEN_SEARCH_GROUP_SECTION: 'TeiSearchSetOpenSearchGroupSection', + SEARCH_TE_IN_TET_SCOPE: 'SearchTrackedEntityInTETScope', }; @@ -32,6 +33,27 @@ export const requestSearchTei = ( ) => actionCreator(actionTypes.REQUEST_SEARCH_TEI)({ formId, searchGroupId, searchId, resultsPageSize }); +export const searchViaUniqueIdOnScopeTrackedEntityType = ({ + formId, + searchGroupId, + searchId, + selectedProgramId, + programQueryArgs, +}: { + formId: string, + searchGroupId: string, + searchId: string, + selectedProgramId: string, + programQueryArgs: any, +}) => + actionCreator(actionTypes.SEARCH_TE_IN_TET_SCOPE)({ + formId, + searchGroupId, + searchId, + selectedProgramId, + programQueryArgs, + }); + export const searchTeiFailed = ( formId: string, searchGroupId: string, diff --git a/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js b/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js index 876c62bd5d..fd6c10bc74 100644 --- a/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js +++ b/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js @@ -16,6 +16,7 @@ import { searchTeiResultRetrieved, searchTeiFailed, setProgramAndTrackedEntityType, + searchViaUniqueIdOnScopeTrackedEntityType, } from '../actions/teiSearch.actions'; import { actionTypes as programSelectorActionTypes, @@ -43,6 +44,32 @@ const getContextQueryArgs = (programId: ?string, trackedEntityTypeId: string) => const getPagingQueryArgs = (pageNumber: ?number = 1) => ({ page: pageNumber, pageSize: 5 }); +export const searchTeiByTETIdEpic = ( + action$: InputObservable, + store: ReduxStore, + { absoluteApiPath, querySingleResource }: ApiUtils, +) => + action$.pipe( + ofType(actionTypes.SEARCH_TE_IN_TET_SCOPE), + switchMap((action) => { + const { selectedProgramId, searchId, formId, searchGroupId, programQueryArgs } = action.payload; + const { attributes, trackedEntityType } = getTrackerProgram(selectedProgramId); + const { program, ...restQueryArgs } = programQueryArgs; + const queryArgs = { ...restQueryArgs, trackedEntityType: trackedEntityType.id }; + return from(getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource)).pipe( + map( + ({ trackedEntityInstanceContainers, pagingData }) => + searchTeiResultRetrieved( + { trackedEntityInstanceContainers, currentPage: pagingData.currentPage }, + formId, + searchGroupId, + searchId, + ), + catchError(() => of(searchTeiFailed(formId, searchGroupId, searchId))), + ), + ); + }), + ); const searchTei = ({ state, @@ -100,16 +127,25 @@ const searchTei = ({ getTrackerProgram(selectedProgramId).attributes : getTrackedEntityType(selectedTrackedEntityTypeId).attributes; - return from( - getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource, selectedProgramId), - ).pipe( - map(({ trackedEntityInstanceContainers, pagingData }) => - searchTeiResultRetrieved( + return from(getTrackedEntityInstances(queryArgs, attributes, absoluteApiPath, querySingleResource, selectedProgramId)).pipe( + map(({ trackedEntityInstanceContainers, pagingData }) => { + if (searchGroup.unique && trackedEntityInstanceContainers.length === 0 && queryArgs.program) { + return searchViaUniqueIdOnScopeTrackedEntityType({ + selectedProgramId, + searchId, + formId, + searchGroupId, + programQueryArgs: queryArgs, + }); + } + + return searchTeiResultRetrieved( { trackedEntityInstanceContainers, currentPage: pagingData.currentPage }, formId, searchGroupId, searchId, - )), + ); + }), catchError(() => of(searchTeiFailed(formId, searchGroupId, searchId))), ); }; 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 26c533631e..7edf4492f8 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 @@ -53,6 +53,7 @@ export const DataEntry = ({ programId: programAPI.id, orgUnitId, trackedEntityInstanceId, + trackedEntityTypeId: programAPI.trackedEntityType.id, }), [programAPI, orgUnitId, trackedEntityInstanceId], ); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/DataElement.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/DataElement.js index b422bcd0be..914ebc9507 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/DataElement.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/DataElement.js @@ -36,6 +36,51 @@ const OPTION_SET_NOT_FOUND = 'Optionset not found'; const TRACKED_ENTITY_ATTRIBUTE_NOT_FOUND = 'TrackedEntityAttributeId missing from programTrackedEntityAttribute or trackedEntityAttribute not found'; +const onValidateOnScopeTrackedEntityType = ( + dataElementUnique: DataElementUnique, + dataElement: DataElement, + serverValue: any, + contextProps: Object = {}, + querySingleResource: QuerySingleResource, +) => { + let requestPromise; + if (dataElementUnique.scope === dataElementUniqueScope.ORGANISATION_UNIT) { + const orgUnitId = contextProps.orgUnitId; + requestPromise = querySingleResource({ + resource: 'tracker/trackedEntities', + params: { + trackedEntityType: contextProps.trackedEntityTypeId, + orgUnit: orgUnitId, + filter: `${dataElement.id}:EQ:${escapeString(serverValue)}`, + }, + }); + } else { + requestPromise = querySingleResource({ + resource: 'tracker/trackedEntities', + params: { + trackedEntityType: contextProps.trackedEntityTypeId, + ouMode: 'ACCESSIBLE', + filter: `${dataElement.id}:EQ:${escapeString(serverValue)}`, + }, + }); + } + return requestPromise + .then((result) => { + const apiTrackedEntities = handleAPIResponse(REQUESTED_ENTITIES.trackedEntities, result); + const otherTrackedEntityInstances = apiTrackedEntities.filter(item => item.trackedEntity !== contextProps.trackedEntityInstanceId); + const trackedEntityInstance = (otherTrackedEntityInstances && otherTrackedEntityInstances[0]) || {}; + const data = { + id: trackedEntityInstance.trackedEntity, + tetId: trackedEntityInstance.trackedEntityType, + }; + + return { + valid: otherTrackedEntityInstances.length === 0, + data, + }; + }); +}; + const buildDataElementUnique = ( dataElement: DataElement, trackedEntityAttribute: TrackedEntityAttribute, @@ -91,6 +136,15 @@ const buildDataElementUnique = ( return requestPromise.then((result) => { const apiTrackedEntities = handleAPIResponse(REQUESTED_ENTITIES.trackedEntities, result); const otherTrackedEntityInstances = apiTrackedEntities.filter(item => item.trackedEntity !== contextProps.trackedEntityInstanceId); + if (otherTrackedEntityInstances.length === 0) { + return onValidateOnScopeTrackedEntityType( + dataEntry, + dataElement, + serverValue, + contextProps, + querySingleResource, + ); + } const trackedEntityInstance = (otherTrackedEntityInstances && otherTrackedEntityInstances[0]) || {}; const data = { diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js index b54a431c19..9a02bb1872 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js @@ -41,6 +41,51 @@ export class DataElementFactory { 'could not create the metadata because a MULIT_TEXT without associated option sets was found', }; + static onValidateOnScopeTrackedEntityType( + dataElementUnique: DataElementUnique, + dataElement: DataElement, + serverValue: any, + contextProps: Object = {}, + querySingleResource: QuerySingleResource, + ) { + let requestPromise; + if (dataElementUnique.scope === dataElementUniqueScope.ORGANISATION_UNIT) { + const orgUnitId = contextProps.orgUnitId; + requestPromise = querySingleResource({ + resource: 'tracker/trackedEntities', + params: { + trackedEntityType: contextProps.trackedEntityTypeId, + orgUnit: orgUnitId, + filter: `${dataElement.id}:EQ:${escapeString(serverValue)}`, + }, + }); + } else { + requestPromise = querySingleResource({ + resource: 'tracker/trackedEntities', + params: { + trackedEntityType: contextProps.trackedEntityTypeId, + ouMode: 'ACCESSIBLE', + filter: `${dataElement.id}:EQ:${escapeString(serverValue)}`, + }, + }); + } + return requestPromise + .then((result) => { + const apiTrackedEntities = handleAPIResponse(REQUESTED_ENTITIES.trackedEntities, result); + const otherTrackedEntityInstances = apiTrackedEntities.filter(item => item.trackedEntity !== contextProps.trackedEntityInstanceId); + const trackedEntityInstance = (otherTrackedEntityInstances && otherTrackedEntityInstances[0]) || {}; + const data = { + id: trackedEntityInstance.trackedEntity, + tetId: trackedEntityInstance.trackedEntityType, + }; + + return { + valid: otherTrackedEntityInstances.length === 0, + data, + }; + }); + } + static buildtetFeatureType(featureType: 'POINT' | 'POLYGON', section: Section) { const dataElement = new DataElement((o) => { o.section = section; @@ -111,6 +156,15 @@ export class DataElementFactory { .then((result) => { const apiTrackedEntities = handleAPIResponse(REQUESTED_ENTITIES.trackedEntities, result); const otherTrackedEntityInstances = apiTrackedEntities.filter(item => item.trackedEntity !== contextProps.trackedEntityInstanceId); + if (otherTrackedEntityInstances.length === 0) { + return this.onValidateOnScopeTrackedEntityType( + o, + dataElement, + serverValue, + contextProps, + querySingleResource, + ); + } const trackedEntityInstance = (otherTrackedEntityInstances && otherTrackedEntityInstances[0]) || {}; const data = { 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..b0d3bd5231 100644 --- a/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js @@ -216,13 +216,11 @@ export const formsSectionsFieldsUIDesc = createReducerDescription({ const updatedFields = Object.keys(assignEffects).reduce((acc, id) => { if (formSectionFields[id]) { acc[id] = { - valid: true, - errorData: undefined, - errorMessage: undefined, - errorType: undefined, + ...state[formId][id], modified: true, touched: true, validatingMessage: null, + pendingValidation: true, }; } return acc; diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index 9e8b011299..497d54d936 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -98,12 +98,14 @@ import { teiSearchSetProgramEpic, teiNewSearchEpic, teiSearchChangePageEpic, + searchTeiByTETIdEpic, } from 'capture-core/components/TeiSearch/epics/teiSearch.epics'; import { teiSearchEpic as teiSearchEpicRelationshipsWidget, teiSearchSetProgramEpic as teiSearchSetProgramEpicRelationshipsWidget, teiNewSearchEpic as teiNewSearchEpicRelationshipsWidget, teiSearchChangePageEpic as teiSearchChangePageEpicRelationshipsWidget, + searchTeiByTETIdEpic as searchTeiByTETIdEpicRelationshipsWidget, } from 'capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/epics/teiSearch.epics'; import { asyncUpdateFieldEpic, @@ -291,10 +293,12 @@ export const epics = combineEpics( TeiRelationshipNewOrEditSearchEpic, teiSearchEpic, teiSearchChangePageEpic, + searchTeiByTETIdEpic, teiSearchEpicRelationshipsWidget, teiSearchSetProgramEpicRelationshipsWidget, teiNewSearchEpicRelationshipsWidget, teiSearchChangePageEpicRelationshipsWidget, + searchTeiByTETIdEpicRelationshipsWidget, teiSearchSetProgramEpic, teiNewSearchEpic, openRelationshipForNewSingleEventEpic,