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-17770] Org unit contextualization in self contained widgets #3720

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
25a3c5a
feat: org unit context in Stages and Events widget
henrikmv Jul 19, 2024
07f80b0
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-17771…
henrikmv Jul 28, 2024
3dde56b
feat: orgunit in enrollment widget completed
henrikmv Jul 31, 2024
c201f96
fix: lint
henrikmv Jul 31, 2024
0f0f3c8
feat: profile widget org unit tooltip completed
henrikmv Jul 31, 2024
4cd6a58
fix: remove unused component
henrikmv Jul 31, 2024
13eb68d
fix: merge conflict
henrikmv Jul 31, 2024
06ef38f
fix: error in getOrgUnitNames
henrikmv Aug 1, 2024
d453275
fix: cache
henrikmv Aug 1, 2024
efbd81d
fix: disabled value
henrikmv Aug 3, 2024
8be30ca
fix: merge conflict
henrikmv Aug 12, 2024
039c1ac
fix: review changes
henrikmv Aug 12, 2024
763b495
fix: restructure cache
henrikmv Aug 15, 2024
57db7da
fix: set back to right cache
henrikmv Aug 15, 2024
2b43005
fix: cache structure for useorgunitnames
henrikmv Aug 15, 2024
940f45c
feat: follow cache standard for all functions
henrikmv Aug 16, 2024
712d78b
fix: move full path hook
henrikmv Aug 16, 2024
5b671b8
fix: change cache name
henrikmv Aug 16, 2024
dc7ce33
fix: undefined value for ancestor
henrikmv Aug 16, 2024
57eab52
fix: remove level
henrikmv Aug 19, 2024
fbf8410
feat: use recursion
henrikmv Aug 19, 2024
d788114
fix: remove console log
henrikmv Aug 19, 2024
d6c8a91
fix: changes on recursion
henrikmv Aug 20, 2024
707e1a9
fix: remove unnecessary function from recursion
henrikmv Aug 21, 2024
0ff202f
fix: code clean up
henrikmv Aug 21, 2024
b7a18ff
feat: change to clienttolist for widgetenrollment
henrikmv Aug 22, 2024
a66b0a2
fix: missing orgunitname in chip component
henrikmv Aug 22, 2024
b7f1a36
fix: change to orgunitname
henrikmv Aug 22, 2024
b1d1c06
fix: change to clienttolist in widgetprofile
henrikmv Aug 22, 2024
6f132f0
fix: set back to name
henrikmv Aug 23, 2024
deb46b5
feat: change from orgunitname to name
henrikmv Aug 23, 2024
8af08ee
fix: set back to cleint to view
henrikmv Aug 26, 2024
5f9e2a6
fix: review changes for orgunitname file
henrikmv Aug 26, 2024
8cc389a
feat: remove id from ancestors
henrikmv Aug 26, 2024
649107f
feat: change tooltip component
henrikmv Aug 26, 2024
9c3d38e
feat: clean up for tooltip
henrikmv Aug 26, 2024
4a598ee
fix: merge with master
henrikmv Aug 26, 2024
c1e0f57
Merge branch 'master' into hv/feat/DHIS2-17771_OrgUnitContextualizati…
henrikmv Aug 26, 2024
9622a4b
fix: after review changes
henrikmv Sep 2, 2024
1193a08
fix: cy test
henrikmv Sep 2, 2024
efe949d
Merge branch 'master' into hv/feat/DHIS2-17771_OrgUnitContextualizati…
henrikmv Sep 12, 2024
8477c66
Merge branch 'master' of https://github.com/dhis2/capture-app into hv…
henrikmv Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2024-06-28T11:23:02.970Z\n"
"PO-Revision-Date: 2024-06-28T11:23:02.970Z\n"
"POT-Creation-Date: 2024-04-26T17:02:42.850Z\n"
"PO-Revision-Date: 2024-04-26T17:02:42.850Z\n"

msgid "Choose one or more dates..."
msgstr "Choose one or more dates..."
Expand Down Expand Up @@ -1274,11 +1274,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,14 @@
import React from 'react';
henrikmv marked this conversation as resolved.
Show resolved Hide resolved
import { Tooltip } from '@dhis2/ui';
import { useFormatOrgUnitNameFullPath } from '../../../metadataRetrieval/orgUnitName';

export const TooltipOrgUnit = ({ orgUnitName, ancestors }) => {
henrikmv marked this conversation as resolved.
Show resolved Hide resolved
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 ')}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to keep the orgUnitName as part of the translatable string? In other languages, the order of the words can be different than in English. One suggestion is to pass an optional prop to TooltipOrgUnit and to display it instead of {orgUnitName}. Something like this:

<TooltipOrgUnit
    orgUnitName={orgUnitName}
    ancestors={ancestors}
    tooltip={i18n.t('Started at {{orgUnitName}}', {
            orgUnitName,
             interpolation: { escapeValue: false },
     })}
 />

// inside TooltipOrgUnit.component.js

 <Tooltip content={orgUnitNameFullPath} openDelay={400}>
     <span style={{ textDecoration: 'underline dotted' }}>
            {tooltip ? tooltip : orgUnitName}
     </span>
</Tooltip>

This change will apply the dotted line to the whole label. WYDT? Thanks!

Copy link
Member

@JoakimSM JoakimSM Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked to Henrik about this one. I agree it's important to be conscious about this problem, but in this case I suggest we simplify as you could really think of the string as two columns ("started at" and then the org unit name).

EDIT: Let's add a colon to be 100% sure (Started at: Ngelehun CHC)

<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 ')}
henrikmv marked this conversation as resolved.
Show resolved Hide resolved
<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 WidgetEnrollmentNote } 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(() =>
henrikmv marked this conversation as resolved.
Show resolved Hide resolved
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;
Loading
Loading