Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import { useMemo, useEffect, useState } from 'react';
import { useDataQuery } from '@dhis2/app-runtime';
import { useDataQuery, type FetchError } from '@dhis2/app-runtime';
import log from 'loglevel';
import { errorCreator } from 'capture-core-utils';

export const useOrganisationUnit = (orgUnitId: string | null | undefined, fields?: string): {
orgUnit: any,
error: any,
} => {
const [orgUnit, setOrgUnit] = useState<any>();
type OrganisationUnit<TFields extends Record<string, unknown>> = TFields & {
id: string,
};

type OrganisationUnitQueryResult<TFields extends Record<string, unknown>> = {
organisationUnits: TFields,
};

type UseOrganisationUnitResult<TFields extends Record<string, unknown>> = {
orgUnit?: OrganisationUnit<TFields>,
error?: FetchError,
};

export const useOrganisationUnit = <TFields extends Record<string, unknown> = Record<string, unknown>>(
orgUnitId: string | null | undefined,
fields?: string,
): UseOrganisationUnitResult<TFields> => {
const [orgUnit, setOrgUnit] = useState<OrganisationUnit<TFields>>();
const [requestedOrgUnitId, setRequestedOrgUnitId] = useState<string>();
const [fetchingInProgress, setFetchingInProgress] = useState(false);
const { error, data, loading, refetch } = useDataQuery(
const { error, data, loading, refetch } = useDataQuery<OrganisationUnitQueryResult<TFields>>(
useMemo(
() => ({
organisationUnits: {
Expand Down Expand Up @@ -45,10 +58,10 @@ export const useOrganisationUnit = (orgUnitId: string | null | undefined, fields
useEffect(() => {
if (fetchingInProgress && !loading) {
setFetchingInProgress(false);
if (orgUnitId === requestedOrgUnitId && !error && data?.organisationUnits) {
if (orgUnitId && orgUnitId === requestedOrgUnitId && !error && data?.organisationUnits) {
setOrgUnit({
id: orgUnitId,
...data.organisationUnits as Record<string, any>,
...data.organisationUnits,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { useOrganisationUnit } from '../../dataQueries';
import { orgUnitFetched } from './coreOrgUnit.actions';
import type { CoreOrgUnit } from './coreOrgUnit.types';

type CoreOrgUnitFields = {
displayName: string,
code: string,
path: string,
};

export function useCoreOrgUnit(orgUnitId: string): {
orgUnit?: CoreOrgUnit,
error?: any,
Expand All @@ -14,7 +20,7 @@ export function useCoreOrgUnit(orgUnitId: string): {
const reduxOrgUnit = useSelector(({ organisationUnits }: any) => organisationUnits && organisationUnits[orgUnitId]);
const fetchId = reduxOrgUnit ? undefined : orgUnitId;
// These hooks do no work when id is undefined
const { orgUnit, error } = useOrganisationUnit(fetchId, 'displayName,code,path');
const { orgUnit, error } = useOrganisationUnit<CoreOrgUnitFields>(fetchId, 'displayName,code,path');
const { orgUnitGroups, error: groupError } = useOrgUnitGroups(fetchId);

if (reduxOrgUnit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
getAncestorIds,
getCachedOrgUnitName,
getOrgUnitNames,
} from '../orgUnitName';

describe('organisation unit name retrieval', () => {
it('returns subvalues and caches names and ancestors from the API response', async () => {
const rootId = 'org-unit-name-test-root';
const childId = 'org-unit-name-test-child';
const querySingleResource = jest.fn().mockResolvedValue({
organisationUnits: [
{
id: childId,
displayName: 'Child organisation unit',
ancestors: [
{
id: rootId,
displayName: 'Root organisation unit',
},
],
},
],
});

await expect(getOrgUnitNames([childId], querySingleResource)).resolves.toEqual({
[childId]: {
id: childId,
name: 'Child organisation unit',
},
});
expect(querySingleResource).toHaveBeenCalledWith(
expect.objectContaining({ resource: 'organisationUnits' }),
{ filter: childId },
);
expect(getCachedOrgUnitName(childId)).toBe('Child organisation unit');
expect(getCachedOrgUnitName(rootId)).toBe('Root organisation unit');

await expect(getAncestorIds(childId, querySingleResource)).resolves.toEqual([rootId]);
expect(querySingleResource).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useDataQuery } from '@dhis2/app-runtime';
import { useDataQuery, type FetchError } from '@dhis2/app-runtime';
import { useOrganisationUnit } from '../../dataQueries';
import type { OrgUnitNames } from './orgUnitName.types';
import type { QuerySingleResource } from '../../utils/api';

type OrganisationUnitAncestor = {
id: string,
displayName: string,
};

type OrganisationUnitDetails = {
displayName: string,
ancestors: Array<OrganisationUnitAncestor>,
};

type ApiOrganisationUnit = OrganisationUnitDetails & {
id: string,
};

type CachedOrganisationUnit = {
displayName: string,
ancestor?: string,
};

type OrganisationUnitsResponse = {
organisationUnits: Array<ApiOrganisationUnit>,
};

type DisplayNamesQueryResult = {
organisationUnits: OrganisationUnitsResponse,
};

type OrgUnitSubValue = {
id: string,
name?: string,
};

type OrgUnitSubValues = Record<string, OrgUnitSubValue>;
type AncestorProperty = 'id' | 'displayName';

// Avoid exporting displayNameCache to keep it truly private.
// As a consequence all functions using it must be in this file.
const displayNameCache: any = {};
const displayNameCache: Record<string, CachedOrganisationUnit> = {};
const maxBatchSize = 50;

const fields = 'id,displayName,ancestors[id,displayName]';
Expand All @@ -15,9 +50,9 @@
const displayNamesQuery = {
organisationUnits: {
resource,
params: ({ filter }: any) => ({
params: (variables: Record<string, unknown>) => ({
fields,
filter: `id:in:[${filter}]`,
filter: `id:in:[${typeof variables.filter === 'string' ? variables.filter : ''}]`,
pageSize: maxBatchSize,
}),
},
Expand All @@ -31,15 +66,15 @@
},
});

const updateCacheWithOrgUnits = (organisationUnits: any) => {
organisationUnits.forEach(({ id, displayName, ancestors }: any) => {
const updateCacheWithOrgUnits = (organisationUnits: Array<ApiOrganisationUnit>) => {
organisationUnits.forEach(({ id, displayName, ancestors }) => {
if (ancestors.length > 0) {
displayNameCache[id] = {
displayName,
ancestor: ancestors[ancestors.length - 1].id,

Check warning on line 74 in src/core_modules/capture-core/metadataRetrieval/orgUnitName/orgUnitName.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.at(…)` over `[….length - index]`.

See more on https://sonarcloud.io/project/issues?id=dhis2_capture-app&issues=AaBzITg4aeHH-gkmhENK&open=AaBzITg4aeHH-gkmhENK&pullRequest=4724
};

ancestors.findLast((ancestor: any, index: any) => {
ancestors.findLast((ancestor, index) => {
if (displayNameCache[ancestor.id]) {
// Ancestors already cached
return true;
Expand Down Expand Up @@ -79,42 +114,47 @@
return batches;
};

const getAncestors = (orgUnitId: any, property: any) => {
const orgUnit = orgUnitId && displayNameCache[orgUnitId];
const getAncestors = (orgUnitId: string | undefined, property: AncestorProperty): Array<string> => {
if (!orgUnitId) {
return [];
}

if (!orgUnit) return [];
const orgUnit = displayNameCache[orgUnitId];
if (!orgUnit) {
return [];
}

const ancestors = getAncestors(orgUnit.ancestor, property);
ancestors.push(property === 'id' ? orgUnitId : orgUnit[property]);
ancestors.push(property === 'id' ? orgUnitId : orgUnit.displayName);

return ancestors;
};

// Works best with memoized input arrays.
export const useOrgUnitNames = (orgUnitIds: Array<string>): {
loading: boolean,
orgUnitNames: OrgUnitNames | null,
error: any,
orgUnitNames?: OrgUnitNames,
error?: FetchError,
} => {
const [fetching, setFetching] = useState(false);
const [fetchNextBatch, setFetchNextBatch] = useState(false);
const [requestedArray, setRequestedArray] = useState<any>();
const [requestedArray, setRequestedArray] = useState<Array<string>>();
const [currentBatches, setCurrentBatches] = useState<Array<Array<string>>>([]);
const [completedBatches, setCompletedBatches] = useState(0);
const [error, setError] = useState<any>();
const [error, setError] = useState<FetchError>();

const ready = !fetching && orgUnitIds === requestedArray;

const batches = useMemo(() => createBatches(orgUnitIds), [orgUnitIds]);
const filter = useMemo(() => (
fetching ? currentBatches[completedBatches].join(',') : ''
), [fetching, currentBatches, completedBatches]);
const result = useMemo(() => (ready ? orgUnitIds.reduce((acc: any, id) => {
const result = useMemo(() => (ready ? orgUnitIds.reduce<OrgUnitNames>((acc, id) => {
acc[id] = displayNameCache[id] ? displayNameCache[id].displayName : null;
return acc;
}, {}) : null), [ready, orgUnitIds]);
}, {}) : undefined), [ready, orgUnitIds]);

const onComplete = useCallback(({ organisationUnits }: any) => {
const onComplete = useCallback(({ organisationUnits }: DisplayNamesQueryResult) => {
updateCacheWithOrgUnits(organisationUnits.organisationUnits);

const completeCount = completedBatches + 1;
Expand All @@ -127,12 +167,12 @@
}
}, [completedBatches, setCompletedBatches, currentBatches, setFetching, setFetchNextBatch]);

const onError = useCallback((fetchError: any) => {
const onError = useCallback((fetchError: FetchError) => {
setFetching(false);
setError(fetchError);
}, [setFetching, setError]);

const { refetch } = useDataQuery(displayNamesQuery, {
const { refetch } = useDataQuery<DisplayNamesQueryResult>(displayNamesQuery, {
variables: { filter },
onComplete,
onError,
Expand Down Expand Up @@ -168,21 +208,19 @@
};
};

export async function getOrgUnitNames(orgUnitIds: Array<string>, querySingleResource: QuerySingleResource): Promise<{
[orgUnitId: string]: {
id: string,
displayName: string,
}
}> {
export async function getOrgUnitNames(
orgUnitIds: Array<string>,
querySingleResource: QuerySingleResource,
): Promise<OrgUnitSubValues> {
await Promise.all(createBatches(orgUnitIds)
.map(batch => querySingleResource(displayNamesQuery.organisationUnits, { filter: batch.join(',') })
.then(({ organisationUnits }: any) => {
.then(({ organisationUnits }: OrganisationUnitsResponse) => {
updateCacheWithOrgUnits(organisationUnits);
}),
),
);

return orgUnitIds.reduce((acc: any, orgUnitId) => {
return orgUnitIds.reduce<OrgUnitSubValues>((acc, orgUnitId) => {
acc[orgUnitId] = {
id: orgUnitId,
name: displayNameCache[orgUnitId]?.displayName,
Expand All @@ -194,11 +232,14 @@
export const useOrgUnitNameWithAncestors = (orgUnitId?: string | null): {
displayName?: string,
ancestors?: Array<string>,
error: any,
error?: FetchError,
} => {
const cachedOrgUnit = orgUnitId && displayNameCache[orgUnitId];
const fetchId = cachedOrgUnit ? undefined : orgUnitId;
const { orgUnit: fetchedOrgUnit, error } = useOrganisationUnit(fetchId, 'displayName,ancestors[id,displayName]');
const { orgUnit: fetchedOrgUnit, error } = useOrganisationUnit<OrganisationUnitDetails>(
fetchId,
'displayName,ancestors[id,displayName]',
);

if (orgUnitId && cachedOrgUnit) {
const ancestors = getAncestors(cachedOrgUnit.ancestor, 'displayName');
Expand All @@ -210,7 +251,7 @@
};
} else if (fetchedOrgUnit && fetchId) {
updateCacheWithOrgUnits([fetchedOrgUnit]);
const ancestors = fetchedOrgUnit.ancestors.map((ancestor: any) => ancestor.displayName);
const ancestors = fetchedOrgUnit.ancestors.map(ancestor => ancestor.displayName);

return {
displayName: fetchedOrgUnit.displayName,
Expand All @@ -222,15 +263,19 @@
return { error };
};

export const getAncestorIds = async (orgUnitId: string, querySingleResource: QuerySingleResource) => {
export const getAncestorIds = async (
orgUnitId: string,
querySingleResource: QuerySingleResource,
): Promise<Array<string>> => {
const cachedOrgUnit = displayNameCache[orgUnitId];
if (cachedOrgUnit) {
return getAncestors(cachedOrgUnit.ancestor, 'id');
}

const apiOrgUnit = await querySingleResource(displayNameQuery(orgUnitId));
const apiOrgUnit: ApiOrganisationUnit = await querySingleResource(displayNameQuery(orgUnitId));
updateCacheWithOrgUnits([apiOrgUnit]);
return getAncestors(displayNameCache[orgUnitId].ancestor, 'id');
};

export const getCachedOrgUnitName = (orgUnitId: string): string | null => displayNameCache[orgUnitId]?.displayName;
export const getCachedOrgUnitName = (orgUnitId: string): string | undefined =>
displayNameCache[orgUnitId]?.displayName;
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export type OrgUnitNames = {
[orgUnitId: string]: string,
[orgUnitId: string]: string | null,
};