From 488f8c090680ad47ed693214478a7ef2919b82e7 Mon Sep 17 00:00:00 2001 From: Tony Valle <79843014+superskip@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:33:24 +0100 Subject: [PATCH 01/17] fix: [DHIS2-15814] missing orgunit names (#3449) --- .../StagesAndEventsWidget.feature | 6 +- .../WidgetEnrollment/index.js | 2 +- .../VariableService/variableService.types.js | 1 - .../CardList/CardListItem.component.js | 14 +- .../TeiRegistrationEntry.component.js | 5 +- .../EnrollmentPageDefault.component.js | 2 +- .../hooks/useProgramStages.js | 5 +- .../useCommonEnrollmentDomainData.types.js | 2 - .../useRuleEffects/useRuleEffects.js | 1 - .../ScopeSelector/ScopeSelector.container.js | 31 ++-- .../components/ScopeSelector/hooks/index.js | 2 - .../hooks/useOrganizationUnit.js | 28 --- .../WidgetEnrollment.component.js | 6 +- .../WidgetEnrollment.container.js | 4 +- .../hooks/useOrganizationUnit.js | 32 ---- .../WidgetEnrollmentEventNew/common.types.js | 1 - .../ScheduleDate/scheduleDate.types.js | 2 +- .../WidgetEventSchedule.container.js | 4 +- .../DataEntry/hooks/useEvents.js | 22 ++- .../StageDetail/StageDetail.component.js | 13 +- .../Stages/Stage/StageDetail/hooks/helpers.js | 4 + .../Stage/StageDetail/hooks/useEventList.js | 14 +- .../Stages/Stages.component.js | 35 +++- .../types/common.types.js | 1 - .../helpers/getListDataCommon/getSubvalues.js | 26 +-- .../workingListsBase.types.js | 7 +- .../dataQueries/useOrganisationUnit.js | 41 +++-- .../capture-core/events/eventRequests.js | 1 - .../capture-core/flow/apiTypes.js | 1 - .../capture-core/flow/typeDeclarations.js | 1 - .../metadataRetrieval/orgUnitName/index.js | 7 + .../orgUnitName/orgUnitName.js | 173 ++++++++++++++++++ .../orgUnitName/orgUnitName.types.js | 4 + 33 files changed, 332 insertions(+), 166 deletions(-) delete mode 100644 src/core_modules/capture-core/components/ScopeSelector/hooks/useOrganizationUnit.js delete mode 100644 src/core_modules/capture-core/components/WidgetEnrollment/hooks/useOrganizationUnit.js create mode 100644 src/core_modules/capture-core/metadataRetrieval/orgUnitName/index.js create mode 100644 src/core_modules/capture-core/metadataRetrieval/orgUnitName/orgUnitName.js create mode 100644 src/core_modules/capture-core/metadataRetrieval/orgUnitName/orgUnitName.types.js diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature index 480c827c0a..f50fe17c60 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature @@ -21,15 +21,11 @@ Feature: User interacts with Stages and Events Widget And you see the first 5 events in the table And you see buttons in the footer list - Scenario: User can view more events + Scenario: User can view more events and then view less Given you open the enrollment page which has multiples events and stages When you click show more button in stages&event list Then more events should be displayed And reset button should be displayed - - Scenario: User can reset events - Given you open the enrollment page which has multiples events and stages - When you click show more button in stages&event list And you click reset button Then there should be 5 rows in the table diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js index 676a73b3bb..be1a87296a 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js @@ -15,7 +15,7 @@ Then('the enrollment widget should be closed', () => { Then('the enrollment widget should be opened', () => { cy.get('[data-test="widget-enrollment"]').within(() => { - cy.get('[data-test="widget-contents"]').children().should('exist'); + cy.get('[data-test="widget-enrollment-contents"]').children().should('exist'); }); }); diff --git a/packages/rules-engine/src/services/VariableService/variableService.types.js b/packages/rules-engine/src/services/VariableService/variableService.types.js index 5529c58a43..0a0b7e65d2 100644 --- a/packages/rules-engine/src/services/VariableService/variableService.types.js +++ b/packages/rules-engine/src/services/VariableService/variableService.types.js @@ -20,7 +20,6 @@ type EventMain = { +programStageId?: string, +programStageName?: string, +orgUnitId?: string, - +orgUnitName?: string, +trackedEntityInstanceId?: string, +enrollmentId?: string, +enrollmentStatus?: string, diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.js b/src/core_modules/capture-core/components/CardList/CardListItem.component.js index d2542d3c1b..328926bdb8 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.js +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.js @@ -13,6 +13,7 @@ import { searchScopes } from '../SearchBox'; import { enrollmentTypes } from './CardList.constants'; import { ListEntry } from './ListEntry.component'; import { dataElementTypes, getTrackerProgramThrowIfNotFound } from '../../metaData'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import type { ListItem, RenderCustomCardActions } from './CardList.types'; @@ -96,24 +97,24 @@ const deriveEnrollmentType = return enrollmentTypes.DONT_SHOW_TAG; }; -const deriveEnrollmentOrgUnitAndDate = (enrollments, enrollmentType, currentProgramId): {orgUnitName?: string, enrolledAt?: string} => { +const deriveEnrollmentOrgUnitIdAndDate = (enrollments, enrollmentType, currentProgramId): {orgUnitId?: string, enrolledAt?: string} => { if (!enrollments?.length) { return {}; } if (!currentProgramId && enrollments.length) { - const { orgUnitName, enrolledAt } = enrollments[0]; + const { orgUnit: orgUnitId, enrolledAt } = enrollments[0]; return { - orgUnitName, + orgUnitId, enrolledAt, }; } - const { orgUnitName, enrolledAt } = + const { orgUnit: orgUnitId, enrolledAt } = enrollments .filter(({ program }) => program === currentProgramId) .filter(({ status }) => status === enrollmentType) .sort((a, b) => moment.utc(a.lastUpdated).diff(moment.utc(b.lastUpdated)))[0] || {}; - return { orgUnitName, enrolledAt }; + return { orgUnitId, enrolledAt }; }; const deriveProgramFromEnrollment = (enrollments, currentSearchScopeType) => { @@ -137,7 +138,8 @@ const CardListItemIndex = ({ }: Props) => { const enrollments = item.tei ? item.tei.enrollments : []; const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); - const { orgUnitName, enrolledAt } = deriveEnrollmentOrgUnitAndDate(enrollments, enrollmentType, currentProgramId); + const { orgUnitId, enrolledAt } = deriveEnrollmentOrgUnitIdAndDate(enrollments, enrollmentType, currentProgramId); + const { displayName: orgUnitName } = useOrgUnitName(orgUnitId); const program = enrollments && enrollments.length ? deriveProgramFromEnrollment(enrollments, currentSearchScopeType) : undefined; diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js index 7b61054364..3e2ea0d8a9 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js @@ -9,7 +9,7 @@ import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { scopeTypes } from '../../../metaData'; import { TrackedEntityInstanceDataEntry } from '../TrackedEntityInstance'; import { useCurrentOrgUnitId } from '../../../hooks/useCurrentOrgUnitId'; -import { useCoreOrgUnit } from '../../../metadataRetrieval/coreOrgUnit'; +import { useOrgUnitName } from '../../../metadataRetrieval/orgUnitName'; import type { Props, PlainProps } from './TeiRegistrationEntry.types'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { withSaveHandler } from '../../DataEntry'; @@ -56,8 +56,7 @@ const TeiRegistrationEntryPlain = const { scopeType } = useScopeInfo(selectedScopeId); const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const orgUnitId = useCurrentOrgUnitId(); - const { orgUnit } = useCoreOrgUnit(orgUnitId); // Tony: [DHIS2-15814] Change this to new hook - const orgUnitName = orgUnit ? orgUnit.name : ''; + const { displayName: orgUnitName } = useOrgUnitName(orgUnitId); const handleOnCancel = () => { if (!isUserInteractionInProgress) { diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 05be58a16e..b59ad5a3a5 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -75,7 +75,7 @@ export const EnrollmentPageDefaultPlain = ({ }: PlainProps) => { const [mainContentVisible, setMainContentVisibility] = useState(true); const [addRelationShipContainerElement, setAddRelationshipContainerElement] = - useState(undefined); + useState(undefined); const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js index 3c8b50285a..7d692e9867 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js @@ -1,11 +1,12 @@ // @flow +import { useMemo } from 'react'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import i18n from '@dhis2/d2-i18n'; import type { apiProgramStage } from 'capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types'; import { Program } from '../../../../../metaData'; -export const useProgramStages = (program: Program, programStages?: Array) => { +export const useProgramStages = (program: Program, programStages?: Array) => useMemo(() => { const stages = []; if (program && programStages) { program.stages.forEach((item) => { @@ -48,4 +49,4 @@ export const useProgramStages = (program: Program, programStages?: Array { programId: event.program, programStageId: event.programStage, orgUnitId: event.orgUnit, - orgUnitName: event.orgUnitName, trackedEntityInstanceId: event.trackedEntityInstance, enrollmentId: event.enrollment, enrollmentStatus: event.enrollmentStatus, diff --git a/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js b/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js index 18557db76b..0aba4ccd71 100644 --- a/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js +++ b/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js @@ -3,15 +3,15 @@ import React, { type ComponentType, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { ScopeSelectorComponent } from './ScopeSelector.component'; import type { OwnProps } from './ScopeSelector.types'; -import { useOrganizationUnit } from './hooks'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { resetOrgUnitIdFromScopeSelector } from './ScopeSelector.actions'; -const deriveReadiness = (lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnitName) => { +const deriveReadiness = (lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnitName, displayName) => { // because we want the orgUnit to be fetched and stored // before allowing the user to view the locked selector - if (selectedOrgUnitId && selectedOrgUnitName) { - return true; + if (selectedOrgUnitId && (!selectedOrgUnitName || selectedOrgUnitName !== displayName)) { + return false; } return !lockedSelectorLoads; }; @@ -32,21 +32,20 @@ export const ScopeSelector: ComponentType = ({ children, }) => { const dispatch = useDispatch(); - const { refetch: refetchOrganisationUnit, displayName } = useOrganizationUnit(); - const [selectedOrgUnit, setSelectedOrgUnit] = useState({ name: displayName, id: selectedOrgUnitId }); + const [selectedOrgUnit, setSelectedOrgUnit] = useState({ name: undefined, id: selectedOrgUnitId }); + const { displayName } = useOrgUnitName(selectedOrgUnit.id); useEffect(() => { - const missName = !selectedOrgUnit.name; - const hasDifferentId = selectedOrgUnit.id !== selectedOrgUnitId; - - selectedOrgUnitId && - (hasDifferentId || missName) && - refetchOrganisationUnit({ variables: { selectedOrgUnitId } }); - }, [selectedOrgUnitId]); // eslint-disable-line react-hooks/exhaustive-deps + if (displayName && selectedOrgUnit.name !== displayName) { + setSelectedOrgUnit(prevSelectedOrgUnit => ({ ...prevSelectedOrgUnit, name: displayName })); + } + }, [displayName, selectedOrgUnit, setSelectedOrgUnit]); useEffect(() => { - displayName && setSelectedOrgUnit(prevSelectedOrgUnit => ({ ...prevSelectedOrgUnit, name: displayName })); - }, [displayName, setSelectedOrgUnit]); + if (selectedOrgUnitId && !selectedOrgUnit.id) { + selectedOrgUnitId && setSelectedOrgUnit(prevSelectedOrgUnit => ({ ...prevSelectedOrgUnit, id: selectedOrgUnitId })); + } + }, [selectedOrgUnitId, selectedOrgUnit, setSelectedOrgUnit]); const handleSetOrgUnit = (orgUnitId, orgUnitObject) => { setSelectedOrgUnit(orgUnitObject); @@ -59,7 +58,7 @@ export const ScopeSelector: ComponentType = ({ previousOrgUnitId: app.previousOrgUnitId, } )); - const ready = deriveReadiness(lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnit.name); + const ready = deriveReadiness(lockedSelectorLoads, selectedOrgUnit.id, selectedOrgUnit.name, displayName); return ( { - const { data, refetch } = useDataQuery( - useMemo( - () => ({ - organisationUnits: { - resource: 'organisationUnits', - id: ({ variables: { selectedOrgUnitId: id } }) => id, - params: { - fields: ['displayName'], - }, - }, - }), - [], - ), - { - lazy: true, - }, - ); - - return { - displayName: data?.organisationUnits?.displayName, - refetch, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js index 78c7f1cb08..d9d400d044 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js @@ -19,6 +19,7 @@ import { Status } from './Status'; import { convertValue as convertValueServerToClient } from '../../converters/serverToClient'; import { convertValue as convertValueClientToView } from '../../converters/clientToView'; import { dataElementTypes } from '../../metaData'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; import { Actions } from './Actions'; @@ -68,6 +69,7 @@ export const WidgetEnrollmentPlain = ({ const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const geometryType = getGeometryType(enrollment?.geometry?.type); + const { displayName: orgUnitName } = useOrgUnitName(enrollment.orgUnit); return (
@@ -84,7 +86,7 @@ export const WidgetEnrollmentPlain = ({ )} {loading && } {!initError && !loading && ( -
+
{enrollment.followUp && ( @@ -125,7 +127,7 @@ export const WidgetEnrollmentPlain = ({ {i18n.t('Started at {{orgUnitName}}', { - orgUnitName: enrollment.orgUnitName, + orgUnitName, interpolation: { escapeValue: false }, })}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js index 71a1c43728..c0a388d46a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js @@ -3,7 +3,7 @@ import React from 'react'; import { errorCreator } from 'capture-core-utils'; import log from 'loglevel'; import { WidgetEnrollment as WidgetEnrollmentComponent } from './WidgetEnrollment.component'; -import { useOrganizationUnit } from './hooks/useOrganizationUnit'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useTrackedEntityInstances } from './hooks/useTrackedEntityInstances'; import { useEnrollment } from './hooks/useEnrollment'; import { useProgram } from './hooks/useProgram'; @@ -42,7 +42,7 @@ export const WidgetEnrollment = ({ enrollments, refetch: refetchTEI, } = useTrackedEntityInstances(teiId, programId); - const { error: errorOrgUnit, displayName } = useOrganizationUnit(ownerOrgUnit); + const { error: errorOrgUnit, displayName } = useOrgUnitName(typeof ownerOrgUnit === 'string' ? ownerOrgUnit : undefined); const { error: errorLocale, locale } = useUserLocale(); const canAddNew = enrollments .filter(item => item.program === programId) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useOrganizationUnit.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useOrganizationUnit.js deleted file mode 100644 index 875e86796b..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useOrganizationUnit.js +++ /dev/null @@ -1,32 +0,0 @@ -// @flow -import { useMemo } from 'react'; -import { useDataQuery } from '@dhis2/app-runtime'; - -export const useOrganizationUnit = (ownerOrgUnit: string | boolean) => { - const { error, loading, data, refetch, called } = useDataQuery( - useMemo( - () => ({ - organisationUnits: { - resource: 'organisationUnits', - id: ({ variables: { ownerOrgUnit: id } }) => id, - params: { - fields: ['displayName'], - }, - }, - }), - [], - ), - { - lazy: true, - }, - ); - - if (ownerOrgUnit && !called) { - refetch({ variables: { ownerOrgUnit } }); - } - - return { - error, - displayName: !loading && data?.organisationUnits?.displayName, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js index ccdcea6c45..791ff8fa85 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js @@ -25,7 +25,6 @@ export type EnrollmentEvent = {| programId: string, programStageId: string, orgUnitId: string, - orgUnitName: string, trackedEntityInstanceId: string, enrollmentId: string, enrollmentStatus: string, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js index 84d5f037e3..e181165e6f 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js @@ -12,6 +12,6 @@ export type Props = {| eventCountInOrgUnit: number, suggestedScheduleDate?: ?string, hideDueDate?: boolean, - orgUnit: Object, + orgUnit: { id: string, name: string }, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 367ab395bb..489ce42dbc 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -4,7 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { useDispatch } from 'react-redux'; import moment from 'moment'; import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess } from '../../metaData'; -import { useOrganisationUnit } from '../../dataQueries'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; import { WidgetEventScheduleComponent } from './WidgetEventSchedule.component'; @@ -35,7 +35,7 @@ export const WidgetEventSchedule = ({ }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); const dispatch = useDispatch(); - const { orgUnit } = useOrganisationUnit(orgUnitId, 'displayName'); + const orgUnit = { id: orgUnitId, name: useOrgUnitName(orgUnitId).displayName }; const { programStageScheduleConfig } = useScheduleConfigFromProgramStage(stageId); const { programConfig } = useScheduleConfigFromProgram(programId); const suggestedScheduleDate = useDetermineSuggestedScheduleDate({ diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js index 868e1c5889..0c900af9a4 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { convertValue } from '../../../../converters/serverToClient'; import { dataElementTypes } from '../../../../metaData'; +import { useOrgUnitNames } from '../../../../metadataRetrieval/orgUnitName'; const convertDate = date => convertValue(date, dataElementTypes.DATE); @@ -14,16 +15,26 @@ const getClientFormattedDataValuesAsObject = (dataValues, elementsById) => return acc; }, {}); -export const useEvents = (enrollment: any, elementsById: Array) => - useMemo( +const getOrgUnitIds = (enrollment: any): Array => + (enrollment ? enrollment.events.reduce((acc, event) => { + if (event.orgUnit) { + acc.push(event.orgUnit); + } + return acc; + }, []) : []); + +export const useEvents = (enrollment: any, elementsById: Array) => { + const orgUnitIds = useMemo(() => getOrgUnitIds(enrollment), [enrollment]); + const { orgUnitNames } = useOrgUnitNames(orgUnitIds); + return useMemo( () => - enrollment && + enrollment && orgUnitNames && enrollment.events.map(event => ({ eventId: event.event, programId: event.program, programStageId: event.programStage, orgUnitId: event.orgUnit, - orgUnitName: event.orgUnitName, + orgUnitName: orgUnitNames[event.orgUnit], trackedEntityInstanceId: event.trackedEntityInstance, enrollmentId: event.enrollment, enrollmentStatus: event.enrollmentStatus, @@ -32,5 +43,6 @@ export const useEvents = (enrollment: any, elementsById: Array) => dueDate: convertDate(event.dueDate), ...getClientFormattedDataValuesAsObject(event.dataValues, elementsById), })), - [elementsById, enrollment], + [elementsById, enrollment, orgUnitNames], ); +}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js index 6d5da181fa..0cc28bc98c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js @@ -103,6 +103,11 @@ const StageDetailPlain = (props: Props) => { onCreateNew(stageId); }, [onCreateNew, stageId]); + const handleShowMore = useCallback(() => { + const nextRowIndex = Math.min(events.length, displayedRowNumber + DEFAULT_NUMBER_OF_ROW); + setDisplayedRowNumber(nextRowIndex); + }, [events, displayedRowNumber, setDisplayedRowNumber]); + function renderHeader() { const headerCells = headerColumns .map(column => ( @@ -181,16 +186,14 @@ const StageDetailPlain = (props: Props) => { } function renderFooter() { - const renderShowMoreButton = () => (events.length > DEFAULT_NUMBER_OF_ROW + const renderShowMoreButton = () => (dataSource && !loading + && events.length > DEFAULT_NUMBER_OF_ROW && displayedRowNumber < events.length ? + ) : ( +
+ ); + + const renderActions = () => ( + + + + + ); + + return ( + + {i18n.t('Coordinates')} + + {renderMap()} +
+
+
{renderLatitude()}
+
{renderLongitude()}
+ {renderFieldButton()} +
+ {hasErrors && ( +
{i18n.t('Please provide valid coordinates')}
+ )} +
+
+ {renderActions()} +
+ ); +}; +export const Coordinates = withStyles(styles)(CoordinatesPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.js new file mode 100644 index 0000000000..56e3c5f689 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.js @@ -0,0 +1,10 @@ +// @flow + +export type CoordinatesProps = { + center: ?[number, number], + setOpen: (open: boolean) => void, + onSetCoordinates: (coordinates: ?[number, number] | ?Array<[number, number]>) => void, + defaultValues?: ?[number, number], + ...CssClasses, +} + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/converters.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/converters.js new file mode 100644 index 0000000000..357a034be3 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/converters.js @@ -0,0 +1,11 @@ +// @flow + +export const convertCoordinatesToServer = (coordinates?: Array | null): ?[number, number] => { + if (!coordinates || !coordinates[0]) { + return null; + } + + const lng: number = coordinates[0][1]; + const lat: number = coordinates[0][0]; + return [lng, lat]; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/coordinate.validator.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/coordinate.validator.js new file mode 100644 index 0000000000..1d37df3233 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/coordinate.validator.js @@ -0,0 +1,32 @@ +// @flow + +type Location = { + longitude: number, + latitude: number, +}; + +function isNumValid(num) { + if (typeof num === 'number') { + return true; + } else if (typeof num === 'string') { + return num.match(/[^0-9.,-]+/) === null; + } + + return false; +} + +export const isValidCoordinate = (value: Location) => { + if (!value) { + return false; + } + + const { longitude, latitude } = value; + if (!isNumValid(latitude) || !isNumValid(longitude)) { + return false; + } + + const ld = parseInt(longitude, 10); + const lt = parseInt(latitude, 10); + + return ld >= -180 && ld <= 180 && lt >= -90 && lt <= 90; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/index.js new file mode 100644 index 0000000000..8d4c2efc14 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/index.js @@ -0,0 +1,3 @@ +// @flow +export { Coordinates } from './Coordinates.component'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.js new file mode 100644 index 0000000000..355490f756 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.js @@ -0,0 +1,27 @@ +// @flow +import React from 'react'; +import { dataElementTypes } from '../../../metaData'; +import type { MapModalComponentProps } from './MapModal.types'; +import { Coordinates } from './Coordinates'; +import { Polygon } from './Polygon'; + +export const MapModal = ({ type, center, setOpen, onSetCoordinates, defaultValues }: MapModalComponentProps) => ( + <> + {type === dataElementTypes.COORDINATE && ( + + )} + {type === dataElementTypes.POLYGON && ( + + )} + +); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.container.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.container.js new file mode 100644 index 0000000000..b39d141b1d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.container.js @@ -0,0 +1,32 @@ +// @flow +import React, { useCallback } from 'react'; +import { useGeometry } from '../hooks/useGeometry'; +import type { MapModalProps } from './MapModal.types'; +import { MapModal as MapModalComponent } from './MapModal.component'; + +const DEFAULT_CENTER = [51.505, -0.09]; + +export const MapModal = ({ + enrollment, + onUpdate, + setOpenMap, + defaultValues, + center, +}: MapModalProps) => { + const { geometryType, dataElementType } = useGeometry(enrollment); + + const onSetCoordinates = useCallback((coordinates) => { + const geometry = coordinates ? { type: geometryType, coordinates } : null; + onUpdate({ ...enrollment, geometry }); + }, [enrollment, geometryType, onUpdate]); + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.js new file mode 100644 index 0000000000..80a273420b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.js @@ -0,0 +1,18 @@ +// @flow +import { dataElementTypes } from '../../../metaData'; + +export type MapModalComponentProps = { + center: ?[number, number], + type: typeof dataElementTypes.COORDINATE | typeof dataElementTypes.POLYGON, + defaultValues?: ?Array> | ?[number, number], + setOpen: (open: boolean) => void, + onSetCoordinates: (coordinates: ?[number, number] | ?Array<[number, number]>) => void, +} + +export type MapModalProps = {| + center?: ?[number, number], + enrollment: Object, + onUpdate: (arg: Object) => void, + setOpenMap: (toggle: boolean) => void, + defaultValues?: ?Array> | ?[number, number], +|}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/ConditionalTooltip.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/ConditionalTooltip.component.js new file mode 100644 index 0000000000..1250a792d2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/ConditionalTooltip.component.js @@ -0,0 +1,29 @@ +// @flow +import React from 'react'; +import { Tooltip } from '@dhis2/ui'; + +type Props = { + enabled: boolean, + children: any, +}; + +export const ConditionalTooltip = (props: Props) => { + const { enabled, children, ...passOnProps } = props; + + return enabled ? + ( + { ({ onMouseOver, onMouseOut, ref }) => ( + { + if (btnRef) { + btnRef.onpointerenter = onMouseOver; + btnRef.onpointerleave = onMouseOut; + ref.current = btnRef; + } + }} + > + {children} + + )} + ) : children; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/DeleteControl.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/DeleteControl.component.js new file mode 100644 index 0000000000..3dd18b0e96 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/DeleteControl.component.js @@ -0,0 +1,52 @@ +// @flow +import React, { useEffect, useState, useCallback } from 'react'; +import ReactDOM from 'react-dom'; +import i18n from '@dhis2/d2-i18n'; +import classNames from 'classnames'; +import L, { Control } from 'leaflet'; +import { withLeaflet } from 'react-leaflet'; + +type Props = { + onClick: () => void, + disabled?: ?boolean, + leaflet: typeof Control, +}; + +const DeleteControlPlain = ({ onClick, disabled, leaflet }: Props) => { + const [leafletElement, setLeafletElement] = useState(); + const onHandleClick = useCallback(() => !disabled && onClick(), [disabled, onClick]); + + useEffect(() => { + const deleteControl = L.control({ position: 'topright' }); + const text = i18n.t('Delete polygon'); + const jsx = ( +
+ {/* eslint-disable-next-line */} + +
+ ); + + deleteControl.onAdd = () => { + const div = L.DomUtil.create('div', ''); + ReactDOM.render(jsx, div); + return div; + }; + setLeafletElement(deleteControl); + }, [onHandleClick, disabled]); + + useEffect(() => { + leafletElement && leafletElement.addTo(leaflet.map); + }, [leafletElement, leaflet.map]); + + useEffect(() => () => leafletElement && leafletElement.remove(), [leafletElement]); + + return null; +}; + +export const DeleteControl = withLeaflet(DeleteControlPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.js new file mode 100644 index 0000000000..d1c2eeefe1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.js @@ -0,0 +1,225 @@ +// @flow +import React, { useState, useRef } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Modal, ModalTitle, ModalContent, ModalActions, Button, ButtonStrip } from '@dhis2/ui'; +import { ReactLeafletSearch } from 'react-leaflet-search-unpolyfilled'; +import { Map, TileLayer, FeatureGroup, withLeaflet } from 'react-leaflet'; +import { EditControl } from 'react-leaflet-draw'; +import L from 'leaflet'; +import { withStyles } from '@material-ui/core'; +import type { PolygonProps, FeatureCollection } from './Polygon.types'; +import { convertPolygonToServer } from './converters'; +import { DeleteControl } from './DeleteControl.component'; +import { ConditionalTooltip } from './ConditionalTooltip.component'; + +const styles = () => ({ + modalContent: { + width: '100%', + }, + map: { + width: '100%', + height: 'calc(100vh - 380px)', + }, + setAreaButton: { + marginLeft: '5px', + }, +}); + +const coordsToFeatureCollection = (inputCoordinates: any): ?FeatureCollection => { + if (!inputCoordinates) { + return null; + } + const list = inputCoordinates[0].length > 2 ? inputCoordinates[0] : inputCoordinates.map(c => [c[1], c[0]]); + + return { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [list], + }, + }, + ], + }; +}; + +const drawing = { + STARTED: 'STARTED', + FINISHED: 'FINISHED', +}; + +const WrappedLeafletSearch = withLeaflet(ReactLeafletSearch); + +const PolygonPlain = ({ + classes, + center: initialCenter, + setOpen, + defaultValues, + onSetCoordinates, +}: PolygonProps) => { + const [polygonArea, setPolygonArea] = useState(defaultValues); + const [center, setCenter] = useState(initialCenter); + const [drawingState, setDrawingState] = useState(undefined); + const prevDrawingState = useRef(undefined); + + const resetToDefaultValues = () => { + setCenter(initialCenter); + setPolygonArea(defaultValues); + }; + + const onMapPolygonCreated = (e: any) => { + const polygonCoordinates = e.layer.toGeoJSON().geometry.coordinates[0].map(c => [c[1], c[0]]); + setPolygonArea(polygonCoordinates); + setDrawingState(drawing.FINISHED); + prevDrawingState.current = drawing.FINISHED; + }; + + const onMapPolygonDelete = () => { + setPolygonArea(null); + setDrawingState(drawing.FINISHED); + prevDrawingState.current = drawing.FINISHED; + }; + + const onSearch = (searchPosition: any) => { + setCenter(searchPosition); + }; + + const getFeatureCollection = () => (Array.isArray(polygonArea) ? coordsToFeatureCollection(polygonArea) : null); + + const renderMap = () => ( + { + if (ref?.leafletElement) { + ref.leafletElement.invalidateSize(); + if (ref.contextValue && polygonArea) { + const { map } = ref.contextValue; + map?.fitBounds(polygonArea); + } + } + }} + className={classes.map} + > + + + { + onFeatureGroupReady(reactFGref, getFeatureCollection()); + }} + > + setDrawingState(drawing.STARTED)} + onDrawStop={() => setDrawingState(prevDrawingState.current)} + draw={{ + rectangle: false, + polyline: false, + circle: false, + marker: false, + circlemarker: false, + }} + edit={{ + remove: false, + edit: false, + }} + /> + + + + ); + + const onFeatureGroupReady = (reactFGref: any, featureCollection: ?FeatureCollection) => { + if (!reactFGref) { + return; + } + if (featureCollection) { + const leafletGeoJSON = new L.GeoJSON(featureCollection); + const leafletFG = reactFGref.leafletElement; + leafletFG.clearLayers(); + + leafletGeoJSON.eachLayer((layer) => { + leafletFG.addLayer(layer); + }); + } else { + const leafletFG = reactFGref.leafletElement; + leafletFG.clearLayers(); + } + }; + + const renderActions = () => ( + + {!drawingState && ( + + )} + {drawingState && ( + <> + + + + + + )} + + ); + + return ( + + {i18n.t('Area')} + {renderMap()} + {renderActions()} + + ); +}; + +export const Polygon = withStyles(styles)(PolygonPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.js new file mode 100644 index 0000000000..024be1b432 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.js @@ -0,0 +1,24 @@ +// @flow + +type Feature = { + type: string, + properties: Object, + geometry: { + type: string, + coordinates: Array | number>>, + }, +} + +export type FeatureCollection = { + type: string, + features: Array, +}; + +export type PolygonProps = { + center: ?[number, number], + setOpen: (open: boolean) => void, + onSetCoordinates: (coordinates: ?[number, number] | ?Array<[number, number]>) => void, + defaultValues?: ?Array>, + ...CssClasses, +} + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/converters.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/converters.js new file mode 100644 index 0000000000..15556ee8c2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/converters.js @@ -0,0 +1,8 @@ +// @flow + +export const convertPolygonToServer = (coordinates?: Array> | null): ?Array<[number, number]> => { + if (!coordinates) { + return null; + } + return Array<[number, number]>(coordinates.map(c => (c ? [c[1], c[0]] : null))); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/index.js new file mode 100644 index 0000000000..04659c427b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/index.js @@ -0,0 +1,3 @@ +// @flow +export { Polygon } from './Polygon.component'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/index.js new file mode 100644 index 0000000000..41266d1ca4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/index.js @@ -0,0 +1,3 @@ +// @flow +export { MapModal } from './MapModal.container'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.js new file mode 100644 index 0000000000..a8d3304847 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.js @@ -0,0 +1,80 @@ +// @flow +import React, { useState } from 'react'; +import { Map, TileLayer, Marker, Polygon } from 'react-leaflet'; +import { withStyles } from '@material-ui/core'; +import { dataElementTypes } from '../../../metaData'; +import { MapModal } from '../MapModal'; +import type { MiniMapProps } from './MiniMap.types'; +import { convertToClientCoordinates } from './converters'; +import { useUpdateEnrollment } from '../dataMutation/dataMutation'; + +const styles = () => ({ + mapContainer: { + width: 150, + height: 120, + }, + map: { + width: '100%', + height: '100%', + }, +}); + +const MiniMapPlain = ({ + coordinates, + geometryType, + enrollment, + refetchEnrollment, + refetchTEI, + onError, + classes, +}: MiniMapProps) => { + const [isOpenMap, setOpenMap] = useState(false); + const { updateMutation } = useUpdateEnrollment(refetchEnrollment, refetchTEI, onError); + const clientValues = convertToClientCoordinates(coordinates, geometryType); + const center = geometryType === dataElementTypes.COORDINATE ? clientValues : clientValues[0]; + const onMapReady = (mapRef) => { + if (mapRef?.contextValue && geometryType === dataElementTypes.POLYGON) { + const { map } = mapRef.contextValue; + map?.fitBounds(clientValues); + } + }; + + return ( + <> +
+ { + onMapReady(mapRef); + }} + center={center} + className={classes.map} + zoom={11} + zoomControl={false} + attributionControl={false} + key="minimap" + onClick={() => { + setOpenMap(true); + }} + > + + {geometryType === dataElementTypes.COORDINATE && } + {geometryType === dataElementTypes.POLYGON && } + +
+ {isOpenMap && ( + + )} + + ); +}; + +export const MiniMap = withStyles(styles)(MiniMapPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.js new file mode 100644 index 0000000000..b905c28783 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.js @@ -0,0 +1,13 @@ +// @flow +import type { QueryRefetchFunction } from '@dhis2/app-runtime'; +import { dataElementTypes } from '../../../metaData'; + +export type MiniMapProps = { + coordinates: Array>, + enrollment: any, + refetchEnrollment: QueryRefetchFunction, + refetchTEI: QueryRefetchFunction, + onError?: (message: string) => void, + geometryType: typeof dataElementTypes.COORDINATE | typeof dataElementTypes.POLYGON, + ...CssClasses +} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/converters.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/converters.js new file mode 100644 index 0000000000..498738ef13 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/converters.js @@ -0,0 +1,10 @@ +// @flow +import { dataElementTypes } from '../../../metaData'; + +export const convertToClientCoordinates = (coordinates: any[], type: $Values) => { + if (type === dataElementTypes.COORDINATE) { + return [coordinates[1], coordinates[0]]; + } + + return coordinates[0].map(coord => [coord[1], coord[0]]); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/index.js new file mode 100644 index 0000000000..c1e7fa0e6f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/index.js @@ -0,0 +1,2 @@ +// @flow +export { MiniMap } from './MiniMap.component'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js index d9d400d044..018a6e7798 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js @@ -4,7 +4,6 @@ import moment from 'moment'; import { IconClock16, IconDimensionOrgUnit16, - IconLocation16, colors, Tag, spacersNum, @@ -16,12 +15,11 @@ import { LoadingMaskElementCenter } from '../LoadingMasks'; import { Widget } from '../Widget'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; -import { convertValue as convertValueServerToClient } from '../../converters/serverToClient'; -import { convertValue as convertValueClientToView } from '../../converters/clientToView'; import { dataElementTypes } from '../../metaData'; import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; import { Actions } from './Actions'; +import { MiniMap } from './MiniMap'; const styles = { enrollment: { @@ -152,13 +150,14 @@ export const WidgetEnrollmentPlain = ({ {enrollment.geometry && (
- - - - {convertValueClientToView( - convertValueServerToClient(enrollment.geometry.coordinates, geometryType), - geometryType, - )} +
)} ({ + enrollments: [enrollment], + }), +}; + +const enrollmentDelete = { + resource: 'tracker?async=false&importStrategy=DELETE', + type: 'create', + data: enrollment => ({ + enrollments: [enrollment], + }), +}; + +const processErrorReports = (error) => { + // $FlowFixMe[prop-missing] + const errorReports = error?.details?.validationReport?.errorReports; + return errorReports?.length > 0 + ? errorReports.reduce((acc, errorReport) => `${acc} ${errorReport.message}`, '') + : error.message; +}; + + +export const useUpdateEnrollment = ( + refetchEnrollment: QueryRefetchFunction, + refetchTEI: QueryRefetchFunction, + onError?: ?(message: string) => void, +) => { + const [updateMutation, { loading: updateLoading }] = useDataMutation( + enrollmentUpdate, + { + onComplete: () => { + refetchEnrollment(); + refetchTEI(); + }, + onError: (e) => { + onError && onError(processErrorReports(e)); + }, + }, + ); + return { + updateMutation, updateLoading, + }; +}; + +export const useDeleteEnrollment = ( + onDelete: () => void, + onError?: ?(message: string) => void, +) => { + const [deleteMutation, { loading: deleteLoading }] = useDataMutation( + enrollmentDelete, + { + onComplete: onDelete, + onError: (e) => { + onError && onError(processErrorReports(e)); + }, + }, + ); + return { deleteMutation, deleteLoading }; +}; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useGeometry.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useGeometry.js new file mode 100644 index 0000000000..3569c211db --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useGeometry.js @@ -0,0 +1,39 @@ +// @flow +import i18n from '@dhis2/d2-i18n'; +import { dataElementTypes } from '../../../metaData'; +import { useProgram } from './useProgram'; + +export const useGeometry = (enrollment: { program: string }) => { + const { + program: { featureType }, + } = useProgram(enrollment.program); + + if (featureType === 'POINT') { + return { + geometryType: 'Point', + dataElementType: dataElementTypes.COORDINATE, + }; + } + + return { + geometryType: 'Polygon', + dataElementType: dataElementTypes.POLYGON, + }; +}; + +export const useGeometryLabel = (enrollment: { program: string, geometry: { type: string } }) => { + const { + program: { featureType }, + error, + } = useProgram(enrollment.program); + + if (error || !featureType || !['POINT', 'POLYGON'].includes(featureType) || enrollment.geometry?.type) { + return undefined; + } + + if (featureType === 'POINT') { + return i18n.t('Add coordinates'); + } + + return i18n.t('Add area'); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js index 4d643cb140..de41c3692e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js @@ -10,7 +10,7 @@ export const useProgram = (programId: string) => { resource: `programs/${programId}`, params: { fields: [ - 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName],programStages[autoGenerateEvent],access', + 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName],programStages[autoGenerateEvent],access,featureType', ], }, }, @@ -18,5 +18,5 @@ export const useProgram = (programId: string) => { [programId], ), ); - return { error, program: !loading && data?.program }; + return { error, loading, program: data?.program }; }; From 05d268e7b022f643d1e7ff919d41280f061a3ddc Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Mon, 20 Nov 2023 10:49:34 +0000 Subject: [PATCH 12/17] chore(release): cut 100.45.0 [skip release] # [100.45.0](https://github.com/dhis2/capture-app/compare/v100.44.7...v100.45.0) (2023-11-20) ### Features * [DHIS2-13237] Enrollment coordinates in enrollment widget ([#3141](https://github.com/dhis2/capture-app/issues/3141)) ([2f2e52c](https://github.com/dhis2/capture-app/commit/2f2e52c3103e9cb48e77766701a9a5fc9af6ad48)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ac4613a7..a096db949f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [100.45.0](https://github.com/dhis2/capture-app/compare/v100.44.7...v100.45.0) (2023-11-20) + + +### Features + +* [DHIS2-13237] Enrollment coordinates in enrollment widget ([#3141](https://github.com/dhis2/capture-app/issues/3141)) ([2f2e52c](https://github.com/dhis2/capture-app/commit/2f2e52c3103e9cb48e77766701a9a5fc9af6ad48)) + ## [100.44.7](https://github.com/dhis2/capture-app/compare/v100.44.6...v100.44.7) (2023-11-19) diff --git a/package.json b/package.json index b19003534a..05175c3689 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.44.7", + "version": "100.45.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.44.7", + "@dhis2/rules-engine-javascript": "100.45.0", "@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 d6850c1d71..dc15c3fc58 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.44.7", + "version": "100.45.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 8f28703ae8b56cfee283ec1ce0ec2b13dfd91e30 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Sun, 26 Nov 2023 02:37:35 +0100 Subject: [PATCH 13/17] fix(translations): sync translations from transifex (master) Automatically merged. --- i18n/uz_UZ_Cyrl.po | 77 +++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/i18n/uz_UZ_Cyrl.po b/i18n/uz_UZ_Cyrl.po index 7d62b3a5cd..620b5591f9 100644 --- a/i18n/uz_UZ_Cyrl.po +++ b/i18n/uz_UZ_Cyrl.po @@ -177,6 +177,7 @@ msgstr "" msgid "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." msgstr "" +"Сақланмоқда {{trackedEntityName}} да {{programName}} да {{orgUnitName}}." msgid "Cancel" msgstr "Бекор қилиш" @@ -212,7 +213,7 @@ msgid "Assignee" msgstr "Ваколат берилган шахс" msgid "Saving to {{programName}} in {{orgUnitName}}" -msgstr "" +msgstr "Сақланади {{programName}} да {{orgUnitName}}" msgid "" "This is not an event program or the metadata is corrupt. See log for " @@ -250,7 +251,7 @@ msgid "Finish" msgstr "Тугатиш" msgid "Save without completing" -msgstr "" +msgstr "Тугалланмасдан сақлаш" msgid "Complete" msgstr "Тўлдириш" @@ -673,36 +674,38 @@ msgid "" msgstr "" msgid "Enrollment Dashboard" -msgstr "" +msgstr "Қайд этиш панели" msgid "No indicator output for this enrollment yet" -msgstr "" +msgstr "Ушбу қайд этишда индикатор ахбороти мавжуд эмас" msgid "No feedback for this enrollment yet" -msgstr "" +msgstr "Ушбу қайд этишда қайта алоқа ахбороти мавжуд эмас" msgid "Quick actions" -msgstr "" +msgstr "Тезкор харакатлар" msgid "New Event" -msgstr "" +msgstr "Янги Ҳодиса/Ҳолат" msgid "Schedule an event" -msgstr "" +msgstr "Ҳодиса/Ҳолатни режалаш" msgid "Make referral" msgstr "Йўналиш яратинг" msgid "No available program stages" -msgstr "" +msgstr "Дастур босқичлари мавжуд эмас" msgid "Program stage not found" -msgstr "" +msgstr "Дастур босқичлари топилмади" msgid "" "Choose a program to add new or see existing enrollments for " "{{teiDisplayName}}" msgstr "" +" {{teiDisplayName}} учун янги рўйхатга ёки мавжуд рўйхатга олишларини кўриш " +"учун дастурни танланг." msgid "" "{{programName}} has categories. Choose all categories to view dashboard." @@ -713,7 +716,7 @@ msgid "Invalid enrollment id {{enrollmentId}}." msgstr "" msgid "Choose an enrollment to view the dashboard." -msgstr "" +msgstr "бошқарув панелини кўриш учун рўйхатдан ўтишни танланг." msgid "There are no active enrollments." msgstr "" @@ -751,19 +754,19 @@ msgid "View working list in this program." msgstr "Ушбу дастурдаги ишчи рўйхатни кўриб чиқиш" msgid "Page is missing required values from URL" -msgstr "" +msgstr "URL саҳифасида керакли қийматлар йўқ" msgid "Program is not valid" -msgstr "" +msgstr "Дастур яроқли эмас" msgid "Org unit is not valid with current program" -msgstr "" +msgstr "Жорий дастур учун ташкилий бирлик яроқли эмас" msgid "There was an error opening the Page" -msgstr "" +msgstr "Саҳифани очишда хатолик мавжуд" msgid "Enrollment{{escape}} New Event" -msgstr "" +msgstr "Рўйхатга олишда {{escape}} Янги Ҳодиса/Ҳолат" msgid "There was an error loading the page" msgstr "" @@ -1174,6 +1177,33 @@ msgstr "Кузатув учун белгиланг" msgid "Existing dates for auto-generated events will not be updated." msgstr "" +msgid "Latitude" +msgstr "Кенглик" + +msgid "Longitude" +msgstr "" + +msgid "Edit" +msgstr "Таҳрирлаш" + +msgid "Set coordinates" +msgstr "" + +msgid "Coordinates" +msgstr "Координаталар" + +msgid "Delete polygon" +msgstr "Полигон (кўпбурчак) ни ўчириб ташлаш" + +msgid "Close without saving" +msgstr "" + +msgid "Finish drawing before saving" +msgstr "" + +msgid "Set area" +msgstr "Ҳудудни белгиланг" + msgid "Enrollment date" msgstr "Қайд этилган сана" @@ -1199,6 +1229,12 @@ msgstr " {{date}}да охирги марта янгиланган" msgid "Cancelled" msgstr "Бекор қилинди" +msgid "Add coordinates" +msgstr "" + +msgid "Add area" +msgstr "" + msgid "Comments about this enrollment" msgstr "" @@ -1322,9 +1358,6 @@ msgstr "" msgid "{{TETName}} profile" msgstr "" -msgid "Edit" -msgstr "Таҳрирлаш" - msgid "tracked entity instance" msgstr "кузатилаётган объект намунаси" @@ -1572,12 +1605,6 @@ msgstr "Шу вақтгача" msgid "Page {{currentPage}}" msgstr "{{currentPage}} саҳифаси" -msgid "Delete polygon" -msgstr "Полигон (кўпбурчак) ни ўчириб ташлаш" - -msgid "Set area" -msgstr "Ҳудудни белгиланг" - msgid "Area on map saved" msgstr "" From fad338ddac06ca6d984ef40a5be3a562f1611197 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Sun, 26 Nov 2023 01:44:31 +0000 Subject: [PATCH 14/17] chore(release): cut 100.45.1 [skip release] ## [100.45.1](https://github.com/dhis2/capture-app/compare/v100.45.0...v100.45.1) (2023-11-26) ### Bug Fixes * **translations:** sync translations from transifex (master) ([8f28703](https://github.com/dhis2/capture-app/commit/8f28703ae8b56cfee283ec1ce0ec2b13dfd91e30)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a096db949f..364f1d18d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [100.45.1](https://github.com/dhis2/capture-app/compare/v100.45.0...v100.45.1) (2023-11-26) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([8f28703](https://github.com/dhis2/capture-app/commit/8f28703ae8b56cfee283ec1ce0ec2b13dfd91e30)) + # [100.45.0](https://github.com/dhis2/capture-app/compare/v100.44.7...v100.45.0) (2023-11-20) diff --git a/package.json b/package.json index 05175c3689..70d4742f93 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.45.0", + "version": "100.45.1", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.45.0", + "@dhis2/rules-engine-javascript": "100.45.1", "@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 dc15c3fc58..017039b19c 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.45.0", + "version": "100.45.1", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 2dbca1efe36ed0e166d4aea803505c30c79cb35d Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 29 Nov 2023 13:26:44 +0100 Subject: [PATCH 15/17] fix: [DHIS2-15693] Rules not triggered on program update (#3472) --- .../EnrollmentRegistrationEntry/hooks/useLifecycle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js index 4a84d7ccb7..a9b0eda796 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js @@ -45,7 +45,7 @@ export const useLifecycle = ( }); useEffect(() => { dataEntryReadyRef.current = false; - }, [teiId]); + }, [teiId, selectedScopeId]); useEffect(() => { if ( From 9e60cf78c7205423b2f0aa9ccb30909dd2e049fa Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Wed, 29 Nov 2023 12:33:21 +0000 Subject: [PATCH 16/17] chore(release): cut 100.45.2 [skip release] ## [100.45.2](https://github.com/dhis2/capture-app/compare/v100.45.1...v100.45.2) (2023-11-29) ### Bug Fixes * [DHIS2-15693] Rules not triggered on program update ([#3472](https://github.com/dhis2/capture-app/issues/3472)) ([2dbca1e](https://github.com/dhis2/capture-app/commit/2dbca1efe36ed0e166d4aea803505c30c79cb35d)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 364f1d18d5..a28036713d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [100.45.2](https://github.com/dhis2/capture-app/compare/v100.45.1...v100.45.2) (2023-11-29) + + +### Bug Fixes + +* [DHIS2-15693] Rules not triggered on program update ([#3472](https://github.com/dhis2/capture-app/issues/3472)) ([2dbca1e](https://github.com/dhis2/capture-app/commit/2dbca1efe36ed0e166d4aea803505c30c79cb35d)) + ## [100.45.1](https://github.com/dhis2/capture-app/compare/v100.45.0...v100.45.1) (2023-11-26) diff --git a/package.json b/package.json index 70d4742f93..a3a3e55966 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.45.1", + "version": "100.45.2", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.45.1", + "@dhis2/rules-engine-javascript": "100.45.2", "@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 017039b19c..360a957889 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.45.1", + "version": "100.45.2", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 2404fca8085965e699ae518cfe816f2f3d38dea7 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Thu, 30 Nov 2023 13:20:00 +0100 Subject: [PATCH 17/17] feat: [DHIS2-14275] Support custom icons (#3473) --- cypress/e2e/MainPage.feature | 12 ++++++++++++ cypress/e2e/MainPage/index.js | 12 ++++++++++++ .../capture-core-utils/featuresSupport/support.js | 2 ++ .../NonBundledDhis2Icon.component.js | 11 +++++++++-- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/MainPage.feature b/cypress/e2e/MainPage.feature index 70a5430db6..980d9fd1e2 100644 --- a/cypress/e2e/MainPage.feature +++ b/cypress/e2e/MainPage.feature @@ -42,3 +42,15 @@ Feature: User interacts with Main page Then you see the opt out component for Child Programme When you opt out to use the new enrollment Dashboard for Child Programme Then you see the opt in component for Child Programme + + @v<41 + Scenario: The icon is rendered as an svg + Given you are in the main page with no selections made + When you select Child Programme + Then the icon is rendered as an svg + + @v>=41 + Scenario: The icon is rendered as a custom icon + Given you are in the main page with no selections made + When you select Child Programme + Then the icon is rendered as a custom icon diff --git a/cypress/e2e/MainPage/index.js b/cypress/e2e/MainPage/index.js index f004659122..6090ac6fb5 100644 --- a/cypress/e2e/MainPage/index.js +++ b/cypress/e2e/MainPage/index.js @@ -20,6 +20,18 @@ And('you can load the view with the name Events assigned to me', () => { }); }); +Then('the icon is rendered as a custom icon', () => { + cy.get('[alt="child_program_positive"]') + .invoke('attr', 'src') + .should('match', /\/icons\/child_program_positive\/icon$/); +}); + +Then('the icon is rendered as an svg', () => { + cy.get('[alt="child_program_positive"]') + .invoke('attr', 'src') + .should('match', /\/icons\/child_program_positive\/icon.svg$/); +}); + Then('the TEI working list is displayed', () => { cy.get('[data-test="tei-working-lists"]').within(() => { cy.contains('Rows per page').should('exist'); diff --git a/src/core_modules/capture-core-utils/featuresSupport/support.js b/src/core_modules/capture-core-utils/featuresSupport/support.js index 0b31bf6dd9..207c929c21 100644 --- a/src/core_modules/capture-core-utils/featuresSupport/support.js +++ b/src/core_modules/capture-core-utils/featuresSupport/support.js @@ -2,12 +2,14 @@ export const FEATURES = Object.freeze({ programStageWorkingList: 'programStageWorkingList', storeProgramStageWorkingList: 'storeProgramStageWorkingList', + customIcons: 'customIcons', }); // The first minor version that supports the feature const MINOR_VERSION_SUPPORT = Object.freeze({ [FEATURES.programStageWorkingList]: 39, [FEATURES.storeProgramStageWorkingList]: 40, + [FEATURES.customIcons]: 41, }); export const hasAPISupportForFeature = (minorVersion: string, featureName: string) => diff --git a/src/core_modules/capture-core/components/NonBundledDhis2Icon/NonBundledDhis2Icon.component.js b/src/core_modules/capture-core/components/NonBundledDhis2Icon/NonBundledDhis2Icon.component.js index b16ac14c1e..c6dc0002eb 100644 --- a/src/core_modules/capture-core/components/NonBundledDhis2Icon/NonBundledDhis2Icon.component.js +++ b/src/core_modules/capture-core/components/NonBundledDhis2Icon/NonBundledDhis2Icon.component.js @@ -1,13 +1,20 @@ // @flow import React from 'react'; import { useConfig } from '@dhis2/app-runtime'; -import { buildUrl } from 'capture-core-utils'; +import { buildUrl, FEATURES, useFeature } from 'capture-core-utils'; import { NonBundledIcon } from 'capture-ui'; import type { Props } from './nonBundledDhis2Icon.types'; export const NonBundledDhis2Icon = ({ name, alternativeText = name, ...passOnProps }: Props) => { + const supportCustomIcons = useFeature(FEATURES.customIcons); const { baseUrl, apiVersion } = useConfig(); - const source = name && buildUrl(baseUrl, `api/${apiVersion}/icons/${name}/icon.svg`); + let source; + + if (name) { + source = buildUrl(baseUrl, `api/${apiVersion}/icons/${name}/icon`); + // Append .svg to source if supportCustomIcons is false (feature flag v41) + source = supportCustomIcons ? source : `${source}.svg`; + } return (