diff --git a/i18n/en.pot b/i18n/en.pot index 18b3dadbab..0355565ab9 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -1277,11 +1277,11 @@ msgstr "Enrollment widget could not be loaded. Please try again later" msgid "Follow-up" msgstr "Follow-up" -msgid "Started at {{orgUnitName}}" -msgstr "Started at {{orgUnitName}}" +msgid "Started at " +msgstr "Started at " -msgid "Owned by {{ownerOrgUnit}}" -msgstr "Owned by {{ownerOrgUnit}}" +msgid "Owned by " +msgstr "Owned by " msgid "Last updated {{date}}" msgstr "Last updated {{date}}" 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 328926bdb8..06717e2a5e 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.js +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.js @@ -13,7 +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 { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import type { ListItem, RenderCustomCardActions } from './CardList.types'; @@ -139,7 +139,7 @@ const CardListItemIndex = ({ const enrollments = item.tei ? item.tei.enrollments : []; const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); const { orgUnitId, enrolledAt } = deriveEnrollmentOrgUnitIdAndDate(enrollments, enrollmentType, currentProgramId); - const { displayName: orgUnitName } = useOrgUnitName(orgUnitId); + const { displayName: orgUnitName } = useOrgUnitNameWithAncestors(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 457fe3a112..14d9395f29 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 @@ -8,7 +8,7 @@ import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { scopeTypes } from '../../../metaData'; import { TrackedEntityInstanceDataEntry } from '../TrackedEntityInstance'; import { useCurrentOrgUnitId } from '../../../hooks/useCurrentOrgUnitId'; -import { useOrgUnitName } from '../../../metadataRetrieval/orgUnitName'; +import { useOrgUnitNameWithAncestors } from '../../../metadataRetrieval/orgUnitName'; import type { Props, PlainProps } from './TeiRegistrationEntry.types'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { withSaveHandler } from '../../DataEntry'; @@ -54,7 +54,7 @@ const TeiRegistrationEntryPlain = const { scopeType } = useScopeInfo(selectedScopeId); const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const orgUnitId = useCurrentOrgUnitId(); - const { displayName: orgUnitName } = useOrgUnitName(orgUnitId); + const { displayName: orgUnitName } = useOrgUnitNameWithAncestors(orgUnitId); const handleOnCancel = () => { if (!isUserInteractionInProgress) { 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 0fdb374e16..6199295503 100644 --- a/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js +++ b/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js @@ -3,7 +3,7 @@ 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 { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; +import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { resetOrgUnitIdFromScopeSelector } from './ScopeSelector.actions'; @@ -34,7 +34,7 @@ export const ScopeSelector: ComponentType = ({ }) => { const dispatch = useDispatch(); const [selectedOrgUnit, setSelectedOrgUnit] = useState({ name: undefined, id: selectedOrgUnitId }); - const { displayName, error: ouNameError } = useOrgUnitName(selectedOrgUnit.id); + const { displayName, error: ouNameError } = useOrgUnitNameWithAncestors(selectedOrgUnit.id); useEffect(() => { if (displayName && selectedOrgUnit.name !== displayName) { diff --git a/src/core_modules/capture-core/components/Tooltips/TooltipOrgUnit/TooltipOrgUnit.component.js b/src/core_modules/capture-core/components/Tooltips/TooltipOrgUnit/TooltipOrgUnit.component.js new file mode 100644 index 0000000000..f7dd2ceee8 --- /dev/null +++ b/src/core_modules/capture-core/components/Tooltips/TooltipOrgUnit/TooltipOrgUnit.component.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Tooltip } from '@dhis2/ui'; +import { useFormatOrgUnitNameFullPath } from '../../../metadataRetrieval/orgUnitName'; + +export const TooltipOrgUnit = ({ orgUnitName, ancestors }) => { + const orgUnitNameFullPath = useFormatOrgUnitNameFullPath(orgUnitName, ancestors); + return ( + + + {orgUnitName} + + + ); +}; + diff --git a/src/core_modules/capture-core/components/Tooltips/TooltipOrgUnit/index.js b/src/core_modules/capture-core/components/Tooltips/TooltipOrgUnit/index.js new file mode 100644 index 0000000000..046747ddff --- /dev/null +++ b/src/core_modules/capture-core/components/Tooltips/TooltipOrgUnit/index.js @@ -0,0 +1 @@ +export { TooltipOrgUnit } from './TooltipOrgUnit.component'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.js index 3c91794004..2cb21e6f98 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.js @@ -4,7 +4,7 @@ import cx from 'classnames'; import { withStyles } from '@material-ui/core/styles'; import { colors, IconInfo16, IconWarning16 } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; -import { useOrgUnitName } from '../../../../metadataRetrieval/orgUnitName'; +import { useOrgUnitNameWithAncestors } from '../../../../metadataRetrieval/orgUnitName'; import { OrgUnitScopes } from '../hooks/useTransferValidation'; import { ProgramAccessLevels } from '../hooks/useProgramAccessLevel'; @@ -48,8 +48,8 @@ const InfoBoxesPlain = ({ orgUnitScopes, classes, }: Props) => { - const { displayName: ownerOrgUnitName } = useOrgUnitName(ownerOrgUnitId); - const { displayName: newOrgUnitName } = useOrgUnitName(validOrgUnitId); + const { displayName: ownerOrgUnitName } = useOrgUnitNameWithAncestors(ownerOrgUnitId); + const { displayName: newOrgUnitName } = useOrgUnitNameWithAncestors(validOrgUnitId); const showWarning = [ProgramAccessLevels.PROTECTED, ProgramAccessLevels.CLOSED].includes(programAccessLevel) && orgUnitScopes.destination === OrgUnitScopes.SEARCH; 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 edb8d05145..18c597306d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js @@ -16,10 +16,11 @@ import { Widget } from '../Widget'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes } from '../../metaData'; -import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; +import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; import { Actions } from './Actions'; import { MiniMap } from './MiniMap'; +import { TooltipOrgUnit } from '../Tooltips/TooltipOrgUnit'; const styles = { enrollment: { @@ -72,7 +73,9 @@ export const WidgetEnrollmentPlain = ({ const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const geometryType = getGeometryType(enrollment?.geometry?.type); - const { displayName: orgUnitName } = useOrgUnitName(enrollment?.orgUnit); + const { displayName: orgUnitName, ancestors } = useOrgUnitNameWithAncestors(enrollment?.orgUnit); + const { displayName: ownerOrgUnitName, ancestors: ownerAncestors } = useOrgUnitNameWithAncestors(ownerOrgUnit?.id); + return (
@@ -129,19 +132,20 @@ export const WidgetEnrollmentPlain = ({ - {i18n.t('Started at {{orgUnitName}}', { - orgUnitName, - interpolation: { escapeValue: false }, - })} + + {i18n.t('Started at ')} + +
- {i18n.t('Owned by {{ownerOrgUnit}}', { - ownerOrgUnit: ownerOrgUnit.displayName, - })} + + {i18n.t('Owned by ')} + +
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 f6faa2669d..cfe5ad23a3 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, { useMemo } from 'react'; import { errorCreator } from 'capture-core-utils'; import log from 'loglevel'; import { WidgetEnrollment as WidgetEnrollmentComponent } from './WidgetEnrollment.component'; -import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; +import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { useTrackedEntityInstances } from './hooks/useTrackedEntityInstances'; import { useEnrollment } from './hooks/useEnrollment'; import { useProgram } from './hooks/useProgram'; @@ -68,7 +68,7 @@ export const WidgetEnrollment = ({ enrollments, refetch: refetchTEI, } = useTrackedEntityInstances(teiId, programId); - const { error: errorOrgUnit, displayName } = useOrgUnitName( + const { error: errorOrgUnit, displayName } = useOrgUnitNameWithAncestors( typeof ownerOrgUnit === 'string' ? ownerOrgUnit : undefined, ); const { error: errorLocale, locale } = useUserLocale(); 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 27cf5bc8b2..51ed379e4b 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, dataElementTypes } from '../../metaData'; -import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; +import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; import { WidgetEventScheduleComponent } from './WidgetEventSchedule.component'; @@ -37,7 +37,7 @@ export const WidgetEventSchedule = ({ }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); const dispatch = useDispatch(); - const orgUnit = { id: orgUnitId, name: useOrgUnitName(orgUnitId).displayName }; + const orgUnit = { id: orgUnitId, name: useOrgUnitNameWithAncestors(orgUnitId).displayName }; const { programStageScheduleConfig } = useScheduleConfigFromProgramStage(stageId); const { programConfig } = useScheduleConfigFromProgram(programId); const suggestedScheduleDate = useDetermineSuggestedScheduleDate({ diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js index 3e98d7db1f..8553c4439c 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js @@ -21,7 +21,7 @@ import { useUserRoles, useTeiDisplayName, } from './hooks'; -import { DataEntry, dataEntryActionTypes, TEI_MODAL_STATE, convertClientToView } from './DataEntry'; +import { DataEntry, dataEntryActionTypes, TEI_MODAL_STATE } from './DataEntry'; import { ReactQueryAppNamespace } from '../../utils/reactQueryHelpers'; import { CHANGELOG_ENTITY_TYPES } from '../WidgetsChangelog'; import { OverflowMenu } from './OverflowMenu'; @@ -86,19 +86,22 @@ const WidgetProfilePlain = ({ const loading = programsLoading || trackedEntityInstancesLoading || userRolesLoading; const error = programsError || trackedEntityInstancesError || userRolesError; - const clientAttributesWithSubvalues = useClientAttributesWithSubvalues(teiId, program, trackedEntityInstanceAttributes); + const clientAttributesWithSubvalues = useClientAttributesWithSubvalues( + teiId, + program, + trackedEntityInstanceAttributes, + ); const teiDisplayName = useTeiDisplayName(program, storedAttributeValues, clientAttributesWithSubvalues, teiId); const displayChangelog = supportsChangelog && program && program.trackedEntityType?.changelogEnabled; - const displayInListAttributes = useMemo(() => clientAttributesWithSubvalues - .filter(item => item.displayInList) - .map((clientAttribute) => { - const { attribute, key } = clientAttribute; - const value = convertClientToView(clientAttribute); - return { - attribute, key, value, reactKey: attribute, - }; - }), [clientAttributesWithSubvalues]); + const displayInListAttributes = useMemo(() => + clientAttributesWithSubvalues + .filter(({ displayInList }) => displayInList) + .map(({ attribute, key, value, valueType }) => ({ + attribute, key, value, valueType, reactKey: attribute, + })), + [clientAttributesWithSubvalues], + ); const onSaveExternal = useCallback(() => { queryClient.removeQueries([ReactQueryAppNamespace, 'changelog', CHANGELOG_ENTITY_TYPES.TRACKED_ENTITY, teiId]); @@ -132,7 +135,10 @@ const WidgetProfilePlain = ({ return (
- +
); }; diff --git a/src/core_modules/capture-core/metadataRetrieval/orgUnitName/index.js b/src/core_modules/capture-core/metadataRetrieval/orgUnitName/index.js index f254f83a12..e452918b15 100644 --- a/src/core_modules/capture-core/metadataRetrieval/orgUnitName/index.js +++ b/src/core_modules/capture-core/metadataRetrieval/orgUnitName/index.js @@ -1,6 +1,7 @@ // @flow export { - useOrgUnitName, + useOrgUnitNameWithAncestors, + useFormatOrgUnitNameFullPath, useOrgUnitNames, getOrgUnitNames, getCachedOrgUnitName, diff --git a/src/core_modules/capture-core/metadataRetrieval/orgUnitName/orgUnitName.js b/src/core_modules/capture-core/metadataRetrieval/orgUnitName/orgUnitName.js index ca45d9de40..91dbdf442d 100644 --- a/src/core_modules/capture-core/metadataRetrieval/orgUnitName/orgUnitName.js +++ b/src/core_modules/capture-core/metadataRetrieval/orgUnitName/orgUnitName.js @@ -72,7 +72,7 @@ export const useOrgUnitNames = (orgUnitIds: Array): { const onComplete = useCallback(({ organisationUnits }) => { for (const { id, displayName } of organisationUnits.organisationUnits) { - displayNameCache[id] = displayName; + displayNameCache[id].displayName = displayName; } const completeCount = completedBatches + 1; setCompletedBatches(completeCount); @@ -139,35 +139,60 @@ export async function getOrgUnitNames(orgUnitIds: Array, querySingleReso .map(batch => querySingleResource(displayNamesQuery.organisationUnits, { filter: batch.join(',') }) .then(({ organisationUnits }) => { for (const { id, displayName } of organisationUnits) { - displayNameCache[id] = displayName; + displayNameCache[id].displayName = displayName; } }))); return orgUnitIds.reduce((acc, orgUnitId) => { acc[orgUnitId] = { id: orgUnitId, - name: displayNameCache[orgUnitId], + name: displayNameCache[orgUnitId].displayName, }; return acc; }, {}); } -export const useOrgUnitName = (orgUnitId: ?string): { +export const useOrgUnitNameWithAncestors = (orgUnitId: ?string): { displayName?: string, - error?: any, + ancestors?: Array<{| displayName: string, level: number |}>, + error ?: any, } => { - const cachedOrgUnitName = orgUnitId && displayNameCache[orgUnitId]; - const fetchId = cachedOrgUnitName ? undefined : orgUnitId; - const { orgUnit, error } = useOrganisationUnit(fetchId, 'displayName'); - if (cachedOrgUnitName) { - return { displayName: cachedOrgUnitName }; + const cachedOrgUnitNameAndAncestor = orgUnitId && displayNameCache[orgUnitId]; + const fetchId = cachedOrgUnitNameAndAncestor ? undefined : orgUnitId; + const { orgUnit, error } = useOrganisationUnit(fetchId, 'displayName,ancestors[displayName,level]'); + + if (cachedOrgUnitNameAndAncestor) { + return { + displayName: cachedOrgUnitNameAndAncestor.displayName, + ancestors: cachedOrgUnitNameAndAncestor.ancestors, + error, + }; } else if (orgUnit && fetchId) { - displayNameCache[orgUnit.id] = orgUnit.displayName; + displayNameCache[orgUnit.id] = { + displayName: orgUnit.displayName, + ancestors: orgUnit.ancestors, + }; if (orgUnit.id === fetchId) { - return { displayName: orgUnit.displayName, error }; + return { displayName: orgUnit.displayName, ancestors: orgUnit.ancestors, error }; } + return { error }; } + return { error }; }; -export const getCachedOrgUnitName = (orgUnitId: string): ?string => displayNameCache[orgUnitId]; +export const useFormatOrgUnitNameFullPath = (orgUnitName: ?string, ancestors?: Array<{| displayName: string, level: number |}>, +): ?string => { + const [path, setPath] = useState(null); + useEffect(() => { + if (orgUnitName && ancestors) { + const ancestorNames = ancestors.map(ancestor => ancestor.displayName); + ancestorNames.push(orgUnitName); + const computedPath = ancestorNames.join(' / '); + setPath(computedPath); + } + }, [orgUnitName, ancestors]); + return path; +}; + +export const getCachedOrgUnitName = (orgUnitId: string): ?string => displayNameCache[orgUnitId].displayName; diff --git a/src/core_modules/capture-ui/FlatList/FlatList.component.js b/src/core_modules/capture-ui/FlatList/FlatList.component.js index 28a5753e60..8a6024df65 100644 --- a/src/core_modules/capture-ui/FlatList/FlatList.component.js +++ b/src/core_modules/capture-ui/FlatList/FlatList.component.js @@ -1,9 +1,9 @@ -// @flow -import React, { type ComponentType } from 'react'; +import React from 'react'; import cx from 'classnames'; import { colors, spacersNum } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; -import type { Props } from './flatList.types'; +import { TooltipOrgUnit } from '../../capture-core/components/Tooltips/TooltipOrgUnit/TooltipOrgUnit.component'; +import { useOrgUnitNameWithAncestors } from '../../capture-core/metadataRetrieval/orgUnitName'; const itemStyles = { overflow: 'hidden', @@ -36,23 +36,36 @@ const styles = { }, }; -const FlatListPlain = ({ list, classes, dataTest }: Props) => { - const lastItemKey = list[list.length - 1]?.reactKey; - const renderItem = item => ( +const FlatListItem = ({ item, classes, lastItemKey }) => { + const { displayName: orgUnitName, ancestors } = useOrgUnitNameWithAncestors(item.value?.id); + + return (
{item.key}:
-
{item.value}
+
+ {item.valueType === 'ORGANISATION_UNIT' ? ( + + ) : ( + item.value + )} +
); +}; + +const FlatListPlain = ({ list, classes, dataTest }) => { + const lastItemKey = list[list.length - 1]?.reactKey; return (
- {list.map(item => renderItem(item))} + {list.map(item => ( + + ))}
); }; -export const FlatList: ComponentType<$Diff> = withStyles(styles)(FlatListPlain); +export const FlatList = withStyles(styles)(FlatListPlain); diff --git a/src/core_modules/capture-ui/FlatList/flatList.types.js b/src/core_modules/capture-ui/FlatList/flatList.types.js index 94dc5a5b47..e0a2dba0db 100644 --- a/src/core_modules/capture-ui/FlatList/flatList.types.js +++ b/src/core_modules/capture-ui/FlatList/flatList.types.js @@ -1,8 +1,7 @@ // @flow -import { type Node } from 'react'; - export type Props = {| - list: { reactKey: string, key: string, value: Node }[], + // value er et object som har en id og en name + list: { reactKey: string, key: string, value: { id: string, name: string }, valueType?: string }[], dataTest?: string, ...CssClasses, |};