Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [DHIS2-15730] OrganizationUnit Contextualizations #3622

Closed
Closed
8 changes: 4 additions & 4 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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}}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';


Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';


Expand Down Expand Up @@ -34,7 +34,7 @@ export const ScopeSelector: ComponentType<OwnProps> = ({
}) => {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip content={orgUnitNameFullPath} openDelay={400}>
<span style={{ textDecoration: 'underline dotted' }}>
{orgUnitName}
</span>
</Tooltip>
);
};

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TooltipOrgUnit } from './TooltipOrgUnit.component';
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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 (
<div data-test="widget-enrollment">
Expand Down Expand Up @@ -129,19 +132,20 @@ export const WidgetEnrollmentPlain = ({
<span className={classes.icon} data-test="widget-enrollment-icon-orgunit">
<IconDimensionOrgUnit16 color={colors.grey600} />
</span>
{i18n.t('Started at {{orgUnitName}}', {
orgUnitName,
interpolation: { escapeValue: false },
})}
<span>
{i18n.t('Started at ')}
<TooltipOrgUnit orgUnitName={orgUnitName} ancestors={ancestors} />
</span>
</div>

<div className={classes.row} data-test="widget-enrollment-owner-orgunit">
<span className={classes.icon} data-test="widget-enrollment-icon-owner-orgunit">
<IconDimensionOrgUnit16 color={colors.grey600} />
</span>
{i18n.t('Owned by {{ownerOrgUnit}}', {
ownerOrgUnit: ownerOrgUnit.displayName,
})}
<span>
{i18n.t('Owned by ')}
<TooltipOrgUnit orgUnitName={ownerOrgUnitName} ancestors={ownerAncestors} />
</span>
</div>

<div className={classes.row} data-test="widget-enrollment-last-update">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -132,7 +135,10 @@ const WidgetProfilePlain = ({

return (
<div className={classes.container}>
<FlatList dataTest="profile-widget-flatlist" list={displayInListAttributes} />
<FlatList
dataTest="profile-widget-flatlist"
list={displayInListAttributes}
/>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
export {
useOrgUnitName,
useOrgUnitNameWithAncestors,
useFormatOrgUnitNameFullPath,
useOrgUnitNames,
getOrgUnitNames,
getCachedOrgUnitName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const useOrgUnitNames = (orgUnitIds: Array<string>): {

const onComplete = useCallback(({ organisationUnits }) => {
for (const { id, displayName } of organisationUnits.organisationUnits) {
displayNameCache[id] = displayName;
displayNameCache[id].displayName = displayName;
}
const completeCount = completedBatches + 1;
setCompletedBatches(completeCount);
Expand Down Expand Up @@ -139,35 +139,60 @@ export async function getOrgUnitNames(orgUnitIds: Array<string>, 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;
31 changes: 22 additions & 9 deletions src/core_modules/capture-ui/FlatList/FlatList.component.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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 (
<div
key={item.reactKey}
className={cx(classes.itemRow, { isLastItem: item.reactKey === lastItemKey })}
>
<div className={classes.itemKey}>{item.key}:</div>
<div className={classes.itemValue}>{item.value}</div>
<div className={classes.itemValue}>
{item.valueType === 'ORGANISATION_UNIT' ? (
<TooltipOrgUnit orgUnitName={orgUnitName} ancestors={ancestors} />
) : (
item.value
)}
</div>
</div>
);
};

const FlatListPlain = ({ list, classes, dataTest }) => {
const lastItemKey = list[list.length - 1]?.reactKey;

return (
<div data-test={dataTest}>
{list.map(item => renderItem(item))}
{list.map(item => (
<FlatListItem key={item.reactKey} item={item} classes={classes} lastItemKey={lastItemKey} />
))}
</div>
);
};

export const FlatList: ComponentType<$Diff<Props, CssClasses>> = withStyles(styles)(FlatListPlain);
export const FlatList = withStyles(styles)(FlatListPlain);
Loading
Loading