diff --git a/src/core_modules/capture-core/dataQueries/useOrganisationUnit.js b/src/core_modules/capture-core/dataQueries/useOrganisationUnit.js index 2760a59fd1..5472a1ccfa 100644 --- a/src/core_modules/capture-core/dataQueries/useOrganisationUnit.js +++ b/src/core_modules/capture-core/dataQueries/useOrganisationUnit.js @@ -4,6 +4,7 @@ import { useDataQuery } from '@dhis2/app-runtime'; import log from 'loglevel'; import { errorCreator } from '../../capture-core-utils'; +// Skips fetching if orgUnitId is falsy export const useOrganisationUnit = (orgUnitId: string, fields?: string) => { const [orgUnit, setOrgUnit] = useState(); const { error, loading, data, refetch, called } = useDataQuery( @@ -24,7 +25,7 @@ export const useOrganisationUnit = (orgUnitId: string, fields?: string) => { ); useEffect(() => { - refetch({ variables: { orgUnitId } }); + orgUnitId && refetch({ variables: { orgUnitId } }); }, [refetch, orgUnitId]); useEffect(() => { @@ -35,7 +36,7 @@ export const useOrganisationUnit = (orgUnitId: string, fields?: string) => { useEffect(() => { const organisationUnit = data?.organisationUnits; - setOrgUnit( + orgUnitId && setOrgUnit( (loading || !called || error) ? undefined : { id: orgUnitId, diff --git a/src/core_modules/capture-core/redux/organisationUnits/fetchReduxOrgUnit.js b/src/core_modules/capture-core/redux/organisationUnits/fetchReduxOrgUnit.js new file mode 100644 index 0000000000..51ab968d41 --- /dev/null +++ b/src/core_modules/capture-core/redux/organisationUnits/fetchReduxOrgUnit.js @@ -0,0 +1,26 @@ +// @flow +import { getAssociatedOrgUnitGroups } from 'capture-core/MetaDataStoreUtils/getAssociatedOrgUnitGroups'; +import type { ReduxOrgUnit } from './organisationUnits.types'; +import type { QuerySingleResource } from '../../utils/api/api.types'; + +// Builds new ReduxOrgUnit by fetching data from the api and index db +export async function fetchReduxOrgUnit( + orgUnitId: string, + querySingleResource: QuerySingleResource, +): Promise { + return Promise.all([ + querySingleResource({ + resource: `organisationUnits/${orgUnitId}`, + params: { + fields: 'displayName,code,path', + }, + }), + getAssociatedOrgUnitGroups(orgUnitId), + ]).then(([orgUnit, groups]) => ({ + id: orgUnitId, + name: orgUnit.displayName, + code: orgUnit.code, + path: orgUnit.path, + groups, + })); +} diff --git a/src/core_modules/capture-core/redux/organisationUnits/getReduxOrgUnit.epics.js b/src/core_modules/capture-core/redux/organisationUnits/getReduxOrgUnit.epics.js new file mode 100644 index 0000000000..c37d75e8fb --- /dev/null +++ b/src/core_modules/capture-core/redux/organisationUnits/getReduxOrgUnit.epics.js @@ -0,0 +1,28 @@ +// @flow +import { ofType } from 'redux-observable'; +import { catchError, mergeMap, concatMap } from 'rxjs/operators'; +import { from, of } from 'rxjs'; +import { actionTypes, orgUnitFetched, type FetchOrgUnitPayload } from './organisationUnits.actions'; +import { fetchReduxOrgUnit } from './fetchReduxOrgUnit'; + +export const getReduxOrgUnitEpic = ( + action$: InputObservable, + store: ReduxStore, + { querySingleResource }: ApiUtils, +) => action$.pipe( + ofType(actionTypes.GET_ORGUNIT), + concatMap((action: ReduxAction) => { + const { organisationUnits } = store.value; + const payload = action.payload; + if (organisationUnits[payload.orgUnitId]) { + return of(payload.onSuccess(organisationUnits[payload.orgUnitId])); + } + return from(fetchReduxOrgUnit(payload.orgUnitId, querySingleResource)) + .pipe( + mergeMap(orgUnit => + of(orgUnitFetched(orgUnit), payload.onSuccess(orgUnit))), + catchError(error => + (payload.onError ? of(payload.onError(error)) : of({}))), + ); + }), +); diff --git a/src/core_modules/capture-core/redux/organisationUnits/index.js b/src/core_modules/capture-core/redux/organisationUnits/index.js new file mode 100644 index 0000000000..ed880346dc --- /dev/null +++ b/src/core_modules/capture-core/redux/organisationUnits/index.js @@ -0,0 +1,5 @@ +// @flow +export { useReduxOrgUnit } from './useReduxOrgUnit'; +export { getOrgUnit } from './organisationUnits.actions'; +export { getReduxOrgUnitEpic } from './getReduxOrgUnit.epics'; +export type { ReduxOrgUnit } from './organisationUnits.types'; diff --git a/src/core_modules/capture-core/redux/organisationUnits/organisationUnits.actions.js b/src/core_modules/capture-core/redux/organisationUnits/organisationUnits.actions.js new file mode 100644 index 0000000000..5d7dc72165 --- /dev/null +++ b/src/core_modules/capture-core/redux/organisationUnits/organisationUnits.actions.js @@ -0,0 +1,21 @@ +// @flow +import { actionCreator } from '../../actions/actions.utils'; +import type { ReduxOrgUnit } from './organisationUnits.types'; + +export const actionTypes = { + GET_ORGUNIT: 'organisationUnits.GetOrgUnit', + ORG_UNIT_FETCHED: 'organisationUnits.OrgUnitFetched', +}; + +type ActionCreator = (payload: T) => ReduxAction; + +// Public +export type FetchOrgUnitPayload = { + orgUnitId: string, + onSuccess: ActionCreator, + onError?: ActionCreator, +} +export const getOrgUnit = (payload: FetchOrgUnitPayload) => actionCreator(actionTypes.GET_ORGUNIT)(payload); + +// Private +export const orgUnitFetched = (orgUnit: ReduxOrgUnit) => actionCreator(actionTypes.ORG_UNIT_FETCHED)(orgUnit); diff --git a/src/core_modules/capture-core/redux/organisationUnits/organisationUnits.types.js b/src/core_modules/capture-core/redux/organisationUnits/organisationUnits.types.js new file mode 100644 index 0000000000..f08257f9f6 --- /dev/null +++ b/src/core_modules/capture-core/redux/organisationUnits/organisationUnits.types.js @@ -0,0 +1,11 @@ +// @flow +import type { OrgUnitGroup } from '@dhis2/rules-engine-javascript'; + +// Make sure rules engine OrgUnit is a subset of this! +export type ReduxOrgUnit = {| + id: string, + name: string, // this is the translated name (displayName) + code: string, + path: string, + groups: Array, +|}; diff --git a/src/core_modules/capture-core/redux/organisationUnits/useReduxOrgUnit.js b/src/core_modules/capture-core/redux/organisationUnits/useReduxOrgUnit.js new file mode 100644 index 0000000000..ba0b439d79 --- /dev/null +++ b/src/core_modules/capture-core/redux/organisationUnits/useReduxOrgUnit.js @@ -0,0 +1,46 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { useSelector, useDispatch } from 'react-redux'; +import { useOrgUnitGroups } from 'capture-core/hooks/useOrgUnitGroups'; +import { useOrganisationUnit } from '../../dataQueries'; +import { orgUnitFetched } from './organisationUnits.actions'; +import { type ReduxOrgUnit } from './organisationUnits.types'; + +export function useReduxOrgUnit(orgUnitId: string): { + orgUnit?: ReduxOrgUnit, + error?: any, +} { + const dispatch = useDispatch(); + const reduxOrgUnit = useSelector(({ organisationUnits }) => organisationUnits && organisationUnits[orgUnitId]); + const id = reduxOrgUnit ? undefined : orgUnitId; + // These hooks do no work when id is undefined + const { orgUnit, error } = useOrganisationUnit(id, 'displayName,code,path'); + const { orgUnitGroups, error: groupError } = useOrgUnitGroups(id); + + if (reduxOrgUnit) { + return { orgUnit: reduxOrgUnit }; + } + + if (error) { + return { error: { error, errorComponent } }; + } else if (groupError) { + return { error: { groupError, errorComponent } }; + } + + if (orgUnit && orgUnitGroups) { + orgUnit.name = orgUnit.displayName; + orgUnit.groups = orgUnitGroups; + delete orgUnit.displayName; + dispatch(orgUnitFetched(orgUnit)); + return { orgUnit }; + } + + return {}; +} + +const errorComponent = ( +
+ {i18n.t('organisation unit could not be retrieved. Please try again later.')} +
+); diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index 6cce6ae2eb..19be4f8c00 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -214,6 +214,9 @@ import { import { orgUnitFetcherEpic, } from '../core_modules/capture-core/components/OrgUnitFetcher'; +import { + getReduxOrgUnitEpic, +} from '../core_modules/capture-core/redux/organisationUnits'; export const epics = combineEpics( resetProgramAfterSettingOrgUnitIfApplicableEpic, @@ -339,6 +342,7 @@ export const epics = combineEpics( navigateToEnrollmentOverviewEpic, scheduleEnrollmentEventEpic, orgUnitFetcherEpic, + getReduxOrgUnitEpic, updateTeiEpic, updateTeiSucceededEpic, updateTeiFailedEpic,