From d10cc188854f5a440ee83f5ae8cfbc920da44961 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Mon, 30 Oct 2023 14:09:36 +0100 Subject: [PATCH 1/9] feat: include enableUserAssignment flag StagesAndEvents-widget --- .../EnrollmentPageDefault/hooks/useProgramMetadata.js | 2 +- .../Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js | 3 ++- .../components/WidgetStagesAndEvents/types/common.types.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js index d319df06d1..e5daad92a8 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js @@ -8,7 +8,7 @@ const query = { id: ({ id }) => id, params: { fields: - ['programStages[id,repeatable,hideDueDate,programStageDataElements[displayInReports,dataElement[id,valueType,displayName,displayFormName,optionSet[options[code,name]]]'], + ['programStages[id,repeatable,hideDueDate,enableUserAssignment,programStageDataElements[displayInReports,dataElement[id,valueType,displayName,displayFormName,optionSet[options[code,name]]]'], }, }, }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js index 3c8b50285a..ec25eeb48a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js @@ -10,7 +10,7 @@ export const useProgramStages = (program: Program, programStages?: Array { const { id, name, icon, stageForm } = item; - const { hideDueDate, programStageDataElements, repeatable } = programStages.find(p => p.id === id) || {}; + const { hideDueDate, programStageDataElements, repeatable, enableUserAssignment } = programStages.find(p => p.id === id) || {}; if (!programStageDataElements) { log.error(errorCreator(i18n.t('Program stage not found'))(id)); } else { @@ -20,6 +20,7 @@ export const useProgramStages = (program: Program, programStages?: Array { const { displayInReports, dataElement } = currentStageData; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js index a3b0c008d4..718bfdf2c3 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js @@ -21,6 +21,7 @@ export type Stage = { description?: ?string, icon?: Icon, dataElements: Array, + enableUserAssignment: boolean, hideDueDate?: boolean, repeatable?: boolean } From 7487a3ae594b3878b3c351a9a428ae10358fd6d7 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Mon, 30 Oct 2023 16:02:37 +0100 Subject: [PATCH 2/9] feat: display assigned users on the events in enrollment overview --- .../Stages/Stage/Stage.component.js | 3 ++- .../Stages/Stage/StageDetail/StageDetail.component.js | 3 ++- .../Stages/Stage/StageDetail/hooks/useEventList.js | 11 +++++++---- .../Stages/Stage/StageDetail/stageDetail.types.js | 1 + .../capture-core/converters/clientToList.js | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js index f8a8908b20..55daffa89a 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js @@ -30,7 +30,7 @@ const hideProgramStage = (ruleEffects, stageId) => ( export const StagePlain = ({ stage, events, classes, className, onCreateNew, ruleEffects, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); - const { id, name, icon, description, dataElements, hideDueDate, repeatable } = stage; + const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; const hiddenProgramStage = hideProgramStage(ruleEffects, id); return ( @@ -57,6 +57,7 @@ export const StagePlain = ({ stage, events, classes, className, onCreateNew, rul dataElements={dataElements} hideDueDate={hideDueDate} repeatable={repeatable} + enableUserAssignment={enableUserAssignment} onCreateNew={onCreateNew} hiddenProgramStage={hiddenProgramStage} {...passOnProps} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js index 6d5da181fa..633ed75b08 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js @@ -66,6 +66,7 @@ const StageDetailPlain = (props: Props) => { dataElements, hideDueDate = false, repeatable = false, + enableUserAssignment = false, onEventClick, onViewAll, onCreateNew, @@ -76,7 +77,7 @@ const StageDetailPlain = (props: Props) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); - const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, stage?.stageForm); + const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElements, events); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js index ab47615397..a41287ad73 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js @@ -18,10 +18,11 @@ import { import { SORT_DIRECTION, MULIT_TEXT_WITH_NO_OPTIONS_SET } from './constants'; import { isNotValidOptionSet } from '../../../../../../utils/isNotValidOptionSet'; -const baseKeys = [{ id: 'status' }, { id: 'occurredAt' }, { id: 'orgUnitName' }, { id: 'scheduledAt' }, { id: 'comments' }]; +const baseKeys = [{ id: 'status' }, { id: 'occurredAt' }, { id: 'assignedUser' }, { id: 'orgUnitName' }, { id: 'scheduledAt' }, { id: 'comments' }]; const basedFieldTypes = [ { type: dataElementTypes.STATUS, resolveValue: convertStatusForView }, { type: dataElementTypes.DATE }, + { type: 'ASSIGNEE' }, { type: dataElementTypes.TEXT }, { type: dataElementTypes.DATE }, { type: dataElementTypes.UNKNOWN, resolveValue: convertCommentForView }, @@ -29,6 +30,7 @@ const basedFieldTypes = [ const getBaseColumnHeaders = props => [ { header: i18n.t('Status'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: props.formFoundation.getLabel('occurredAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: i18n.t('Assigned to'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: i18n.t('Registering unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: props.formFoundation.getLabel('scheduledAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: '', sortDirection: null, isPredefined: true }, @@ -110,7 +112,7 @@ const useComputeDataFromEvent = (dataElements: Array, events: }; -const useComputeHeaderColumn = (dataElements: Array, hideDueDate: boolean, formFoundation: Object) => { +const useComputeHeaderColumn = (dataElements: Array, hideDueDate: boolean, enableUserAssignment: boolean, formFoundation: Object) => { const headerColumns = useMemo(() => { const dataElementHeaders = dataElements.reduce((acc, currDataElement) => { const { id, name, formName, type, optionSet } = currDataElement; @@ -124,9 +126,10 @@ const useComputeHeaderColumn = (dataElements: Array, hideDueDa return acc; }, []); return [ - ...getBaseColumns({ formFoundation }).filter(col => (hideDueDate ? col.id !== 'scheduledAt' : true)), + ...getBaseColumns({ formFoundation }) + .filter(col => (enableUserAssignment || col.id !== 'assignedUser') && (!hideDueDate || col.id !== 'scheduledAt')), ...dataElementHeaders]; - }, [dataElements, hideDueDate, formFoundation]); + }, [dataElements, hideDueDate, enableUserAssignment, formFoundation]); return headerColumns; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js index a5edeeab75..31d8c79dea 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js @@ -7,6 +7,7 @@ import type { StageDataElement, StageCommonProps } from '../../../types/common.t eventName: string, hideDueDate?: boolean, repeatable?: boolean, + enableUserAssignment?: boolean, stageId: string, hiddenProgramStage?: boolean, ...CssClasses, diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index ff6a7b3825..ff78d99f5c 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -92,7 +92,7 @@ const valueConvertersForType = { [dataElementTypes.FILE_RESOURCE]: convertResourceForDisplay, [dataElementTypes.IMAGE]: convertResourceForDisplay, [dataElementTypes.ORGANISATION_UNIT]: (rawValue: Object) => rawValue.name, - [dataElementTypes.ASSIGNEE]: (rawValue: Object) => `${rawValue.name} (${rawValue.username})`, + [dataElementTypes.ASSIGNEE]: (rawValue: Object) => `${rawValue.name || rawValue.displayName} (${rawValue.username})`, [dataElementTypes.NUMBER_RANGE]: convertNumberRangeForDisplay, [dataElementTypes.STATUS]: convertStatusForDisplay, }; From d1b77bc57cb73a5902efe890c1270f06f30dd6c8 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Thu, 2 Nov 2023 17:56:00 +0100 Subject: [PATCH 3/9] fix: undefined assignee name after adding event --- .../events/mainConverters/mainEventClientToServerConverter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js b/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js index 1e3f62f036..207918b8cb 100644 --- a/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js +++ b/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js @@ -1,5 +1,5 @@ // @flow -import { convertClientToServer } from '../../converters'; +import { convertClientToServer, convertAssigneeToServer } from '../../converters'; import { convertMainEvent } from './mainEventConverter'; import { dataElementTypes } from '../../metaData'; import { convertEventAttributeOptions } from '../convertEventAttributeOptions'; @@ -26,7 +26,7 @@ export function convertMainEventClientToServer(event: Object) { convertedValue = convertClientToServer(value, dataElementTypes.DATE); break; case 'assignee': - convertedValue = value && ({ uid: value.id }); + convertedValue = value && convertAssigneeToServer(value); break; default: convertedValue = value; From 23c694efe6b5e27d18831af1901b1f560bc38f41 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 3 Nov 2023 12:39:35 +0100 Subject: [PATCH 4/9] refactor: remove fallback to displayName --- .../capture-core/converters/clientToList.js | 2 +- .../capture-core/converters/serverToClient.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index ff78d99f5c..ff6a7b3825 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -92,7 +92,7 @@ const valueConvertersForType = { [dataElementTypes.FILE_RESOURCE]: convertResourceForDisplay, [dataElementTypes.IMAGE]: convertResourceForDisplay, [dataElementTypes.ORGANISATION_UNIT]: (rawValue: Object) => rawValue.name, - [dataElementTypes.ASSIGNEE]: (rawValue: Object) => `${rawValue.name || rawValue.displayName} (${rawValue.username})`, + [dataElementTypes.ASSIGNEE]: (rawValue: Object) => `${rawValue.name} (${rawValue.username})`, [dataElementTypes.NUMBER_RANGE]: convertNumberRangeForDisplay, [dataElementTypes.STATUS]: convertStatusForDisplay, }; diff --git a/src/core_modules/capture-core/converters/serverToClient.js b/src/core_modules/capture-core/converters/serverToClient.js index 897a4cc13e..0115b1d7d6 100644 --- a/src/core_modules/capture-core/converters/serverToClient.js +++ b/src/core_modules/capture-core/converters/serverToClient.js @@ -11,6 +11,19 @@ function convertTime(d2Value: string) { return parseData.momentTime; } +const convertAssignedUserToClient = (assignedUser?: ApiAssignedUser) => { + if (!assignedUser) { + return null; + } + return { + id: assignedUser.uid, + name: assignedUser.displayName, + username: assignedUser.username, + firstName: assignedUser.firstName, + surname: assignedUser.surname, + }; +}; + const optionSetConvertersForType = { [dataElementTypes.NUMBER]: parseNumber, [dataElementTypes.INTEGER]: parseNumber, @@ -51,6 +64,7 @@ const valueConvertersForType = { return { latitude: arr[1], longitude: arr[0] }; }, [dataElementTypes.POLYGON]: () => 'Polygon', + [dataElementTypes.ASSIGNEE]: convertAssignedUserToClient, }; export function convertValue(value: any, type: $Keys) { From f64a7d720f3a9bff58342a582fd0365a2e17c5c3 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Mon, 13 Nov 2023 15:00:47 +0100 Subject: [PATCH 5/9] fix: `assignedUser` can be an empty object --- .../capture-core/converters/serverToClient.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/core_modules/capture-core/converters/serverToClient.js b/src/core_modules/capture-core/converters/serverToClient.js index 0115b1d7d6..288948461e 100644 --- a/src/core_modules/capture-core/converters/serverToClient.js +++ b/src/core_modules/capture-core/converters/serverToClient.js @@ -11,18 +11,14 @@ function convertTime(d2Value: string) { return parseData.momentTime; } -const convertAssignedUserToClient = (assignedUser?: ApiAssignedUser) => { - if (!assignedUser) { - return null; - } - return { +const convertAssignedUserToClient = (assignedUser?: ApiAssignedUser) => + ((assignedUser && assignedUser.uid) ? { id: assignedUser.uid, name: assignedUser.displayName, username: assignedUser.username, firstName: assignedUser.firstName, surname: assignedUser.surname, - }; -}; + } : null); const optionSetConvertersForType = { [dataElementTypes.NUMBER]: parseNumber, From 6764efb46d189953327cc70f111d2629b452a52a Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 24 Nov 2023 16:56:01 +0100 Subject: [PATCH 6/9] refactor: fetch program metadata from IndexedDB --- .../hooks/useProgramMetadata.js | 112 ++++++++++++++---- .../utils/api/useQueryStyleEvaluation.js | 33 ++++++ .../useDataElementsFromIndexedDB.js | 28 +++++ .../useOptionSetsFromIndexedDB.js | 28 +++++ 4 files changed, 180 insertions(+), 21 deletions(-) create mode 100644 src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js create mode 100644 src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js create mode 100644 src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js index e5daad92a8..feafd5051a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js @@ -1,28 +1,98 @@ // @flow -import { useEffect } from 'react'; -import { useDataQuery } from '@dhis2/app-runtime'; - -const query = { - programData: { - resource: 'programs', - id: ({ id }) => id, - params: { - fields: - ['programStages[id,repeatable,hideDueDate,enableUserAssignment,programStageDataElements[displayInReports,dataElement[id,valueType,displayName,displayFormName,optionSet[options[code,name]]]'], - }, - }, -}; +import { useMemo } from 'react'; +import { useProgramFromIndexedDB } from '../../../../../utils/cachedDataHooks/useProgramFromIndexedDB'; +import { useDataElementsFromIndexedDB } from '../../../../../utils/cachedDataHooks/useDataElementsFromIndexedDB'; +import { useOptionSetsFromIndexedDB } from '../../../../../utils/cachedDataHooks/useOptionSetsFromIndexedDB'; export const useProgramMetadata = (programId: string) => { - const { data, error, loading, refetch } = useDataQuery(query, { - lazy: true, - }); + const { program, isLoading, isError } = useProgramFromIndexedDB(programId, { enabled: !!programId }); + + const dataElementIds = useMemo(() => + (program ? program.programStages.reduce( + (acc, stage) => stage.programStageDataElements.reduce( + (accIds, dataElement) => { + accIds.add(dataElement.dataElementId); + return accIds; + }, acc), + new Set) : undefined), [program]); + + const { + loading: loadingDataElements, + dataElements, + error: dataElementsError, + } = useDataElementsFromIndexedDB(dataElementIds); + + const derivedDataElementValues = useMemo(() => + (dataElements ? ({ + optionSetIds: dataElements.reduce((acc, dataElement) => { + if (dataElement.optionSetValue) { + acc.add(dataElement.optionSet.id); + } + return acc; + }, new Set), + dataElementDictionary: dataElements.reduce((acc, dataElement) => { + acc[dataElement.id] = dataElement; + return acc; + }, {}), + }) : undefined), [dataElements]); + + const { + loading: loadingOptionSets, + optionSets, + error: optionSetsError, + } = useOptionSetsFromIndexedDB(derivedDataElementValues && derivedDataElementValues.optionSetIds); - useEffect(() => { - if (programId) { - refetch({ id: programId }); + const optionSetDictionary = useMemo( + () => (optionSets ? optionSets.reduce( + (acc, optionSet) => { + acc[optionSet.id] = { + optionSet: { + options: optionSet.options.map(option => ({ + name: option.displayName, + code: option.code, + })), + }, + }; + return acc; + }, {}) : undefined), + [optionSets], + ); + + const programMetadata = useMemo(() => { + if (!program || !derivedDataElementValues || !optionSetDictionary) { + return undefined; } - }, [refetch, programId]); - return { error, programMetadata: !loading && data?.programData ? data.programData : undefined }; + const dataElementDictionary = derivedDataElementValues.dataElementDictionary; + + return { + programStages: program.programStages.map(stage => ({ + id: stage.id, + repeatable: stage.repeatable, + hideDueDate: stage.hideDueDate, + enableUserAssignment: stage.enableUserAssignment, + programStageDataElements: stage.programStageDataElements + .map((programStageDataElement) => { + const dataElement = dataElementDictionary[programStageDataElement.dataElementId]; + const optionSet = dataElement.optionSetValue ? optionSetDictionary[dataElement.optionSet.id] : {}; + return { + displayInReports: programStageDataElement.displayInReports, + dataElement: { + id: dataElement.id, + valueType: dataElement.valueType, + displayName: dataElement.displayName, + displayFormName: dataElement.displayFormName, + ...optionSet, + }, + }; + }), + })), + }; + }, [program, derivedDataElementValues, optionSetDictionary]); + + + return { + error: (isError || dataElementsError || optionSetsError) && { programId }, + programMetadata: (isLoading || loadingDataElements || loadingOptionSets) ? undefined : programMetadata, + }; }; diff --git a/src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js b/src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js new file mode 100644 index 0000000000..2ada5c193b --- /dev/null +++ b/src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js @@ -0,0 +1,33 @@ +// @flow +import { useState, useEffect } from 'react'; + +// Caches result from previous evaluation +// Does not evaluate falsy input values - returns previously cached value instead +export const useQueryStyleEvaluation = (asyncFn: (input: any) => Promise, input: any) => { + const [loading, setLoading] = useState(false); + const [data, setData] = useState(); + const [error, setError] = useState(); + const [previousInput, setPreviousInput] = useState(); + + useEffect(() => { + if (input && input !== previousInput && !loading) { + setLoading(true); + setData(undefined); + setError(undefined); + setPreviousInput(input); + asyncFn(input).then((result) => { + setLoading(false); + setData(result); + }).catch((e) => { + setLoading(false); + setError(e); + }); + } + }, [loading, asyncFn, input, previousInput, setLoading, setData, setError, setPreviousInput]); + + return { + loading: loading || (!!input && input !== previousInput), + data, + error, + }; +}; diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js new file mode 100644 index 0000000000..666a438ec2 --- /dev/null +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js @@ -0,0 +1,28 @@ +// @flow +import { useCallback } from 'react'; +import { userStores, getUserStorageController } from '../../storageControllers'; +import { useQueryStyleEvaluation } from '../api/useQueryStyleEvaluation'; +import type { CachedDataElement } from '../../storageControllers/'; + +export const useDataElementsFromIndexedDB = (dataElementIds: ?Set): { + dataElements: ?Array, + loading: boolean, + error: any, +} => { + const storageController = getUserStorageController(); + + const getDataElements = useCallback(requestedIds => + storageController.getAll( + userStores.DATA_ELEMENTS, { + predicate: dataElement => requestedIds.has(dataElement.id), + }, + ), [storageController]); + + const { loading, data, error } = useQueryStyleEvaluation(getDataElements, dataElementIds); + + return { + dataElements: data, + loading, + error, + }; +}; diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js new file mode 100644 index 0000000000..d7f51efd34 --- /dev/null +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js @@ -0,0 +1,28 @@ +// @flow +import { useCallback } from 'react'; +import { userStores, getUserStorageController } from '../../storageControllers'; +import { useQueryStyleEvaluation } from '../api/useQueryStyleEvaluation'; +import type { CachedOptionSet } from '../../storageControllers/'; + +export const useOptionSetsFromIndexedDB = (optionSetIds: ?Set): { + optionSets: ?Array, + loading: boolean, + error: any, +} => { + const storageController = getUserStorageController(); + + const getOptionSets = useCallback(requestedIds => + storageController.getAll( + userStores.OPTION_SETS, { + predicate: optionSet => requestedIds.has(optionSet.id), + }, + ), [storageController]); + + const { loading, data, error } = useQueryStyleEvaluation(getOptionSets, optionSetIds); + + return { + optionSets: data, + loading, + error, + }; +}; From b5ee3514111f8fe1e92cb28d68cb99e271161ff4 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 24 Nov 2023 16:57:55 +0100 Subject: [PATCH 7/9] style: lower case first letter in variable names --- .../utils/cachedDataHooks/useProgramFromIndexedDB.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js index c8a73f7838..d5a1c8a02e 100644 --- a/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useProgramFromIndexedDB.js @@ -4,9 +4,9 @@ import { userStores, getUserStorageController } from '../../storageControllers'; import { useIndexedDBQuery } from '../reactQueryHelpers'; -export const useProgramFromIndexedDB = (programId: ?string, QueryOptions?: UseQueryOptions<>) => { +export const useProgramFromIndexedDB = (programId: ?string, queryOptions?: UseQueryOptions<>) => { const storageController = getUserStorageController(); - const { enabled = true } = QueryOptions ?? {}; + const { enabled = true } = queryOptions ?? {}; const { data, isLoading, isError } = useIndexedDBQuery( // $FlowFixMe - only gets called when programId is defined because of enabled From 30b53e67216bb491fd43faef8ddf8dc396c9f4e6 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 24 Nov 2023 18:30:31 +0100 Subject: [PATCH 8/9] fix: property `optionSet` was always defined before doing the refactor --- .../EnrollmentPageDefault/hooks/useProgramMetadata.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js index feafd5051a..d83f328f2b 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js @@ -74,7 +74,6 @@ export const useProgramMetadata = (programId: string) => { programStageDataElements: stage.programStageDataElements .map((programStageDataElement) => { const dataElement = dataElementDictionary[programStageDataElement.dataElementId]; - const optionSet = dataElement.optionSetValue ? optionSetDictionary[dataElement.optionSet.id] : {}; return { displayInReports: programStageDataElement.displayInReports, dataElement: { @@ -82,7 +81,7 @@ export const useProgramMetadata = (programId: string) => { valueType: dataElement.valueType, displayName: dataElement.displayName, displayFormName: dataElement.displayFormName, - ...optionSet, + optionSet: dataElement.optionSetValue ? optionSetDictionary[dataElement.optionSet.id] : {}, }, }; }), From 91c56e791e3c2a4b42afcbf2da9c206e5b889843 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 15 Dec 2023 13:27:24 +0100 Subject: [PATCH 9/9] refactor: use react-query hooks --- .../hooks/useProgramMetadata.js | 14 ++++---- .../utils/api/useQueryStyleEvaluation.js | 33 ------------------- .../useDataElementsFromIndexedDB.js | 30 +++++++++-------- .../useOptionSetsFromIndexedDB.js | 30 +++++++++-------- .../query/useMetadataQuery.js | 4 +-- .../reactQueryHelpers.const.js | 2 +- 6 files changed, 45 insertions(+), 68 deletions(-) delete mode 100644 src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js index d83f328f2b..542e07dfe6 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js @@ -4,6 +4,8 @@ import { useProgramFromIndexedDB } from '../../../../../utils/cachedDataHooks/us import { useDataElementsFromIndexedDB } from '../../../../../utils/cachedDataHooks/useDataElementsFromIndexedDB'; import { useOptionSetsFromIndexedDB } from '../../../../../utils/cachedDataHooks/useOptionSetsFromIndexedDB'; +const queryKey = 'useProgramMetadata'; + export const useProgramMetadata = (programId: string) => { const { program, isLoading, isError } = useProgramFromIndexedDB(programId, { enabled: !!programId }); @@ -17,10 +19,10 @@ export const useProgramMetadata = (programId: string) => { new Set) : undefined), [program]); const { - loading: loadingDataElements, + isLoading: loadingDataElements, dataElements, - error: dataElementsError, - } = useDataElementsFromIndexedDB(dataElementIds); + isError: dataElementsError, + } = useDataElementsFromIndexedDB([queryKey, programId], dataElementIds); const derivedDataElementValues = useMemo(() => (dataElements ? ({ @@ -37,10 +39,10 @@ export const useProgramMetadata = (programId: string) => { }) : undefined), [dataElements]); const { - loading: loadingOptionSets, + isLoading: loadingOptionSets, optionSets, - error: optionSetsError, - } = useOptionSetsFromIndexedDB(derivedDataElementValues && derivedDataElementValues.optionSetIds); + isError: optionSetsError, + } = useOptionSetsFromIndexedDB([queryKey, programId], derivedDataElementValues && derivedDataElementValues.optionSetIds); const optionSetDictionary = useMemo( () => (optionSets ? optionSets.reduce( diff --git a/src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js b/src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js deleted file mode 100644 index 2ada5c193b..0000000000 --- a/src/core_modules/capture-core/utils/api/useQueryStyleEvaluation.js +++ /dev/null @@ -1,33 +0,0 @@ -// @flow -import { useState, useEffect } from 'react'; - -// Caches result from previous evaluation -// Does not evaluate falsy input values - returns previously cached value instead -export const useQueryStyleEvaluation = (asyncFn: (input: any) => Promise, input: any) => { - const [loading, setLoading] = useState(false); - const [data, setData] = useState(); - const [error, setError] = useState(); - const [previousInput, setPreviousInput] = useState(); - - useEffect(() => { - if (input && input !== previousInput && !loading) { - setLoading(true); - setData(undefined); - setError(undefined); - setPreviousInput(input); - asyncFn(input).then((result) => { - setLoading(false); - setData(result); - }).catch((e) => { - setLoading(false); - setError(e); - }); - } - }, [loading, asyncFn, input, previousInput, setLoading, setData, setError, setPreviousInput]); - - return { - loading: loading || (!!input && input !== previousInput), - data, - error, - }; -}; diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js index 666a438ec2..dbbf4f83c3 100644 --- a/src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useDataElementsFromIndexedDB.js @@ -1,28 +1,32 @@ // @flow -import { useCallback } from 'react'; +import type { UseQueryOptions } from 'react-query'; import { userStores, getUserStorageController } from '../../storageControllers'; -import { useQueryStyleEvaluation } from '../api/useQueryStyleEvaluation'; +import { useIndexedDBQuery } from '../reactQueryHelpers'; import type { CachedDataElement } from '../../storageControllers/'; -export const useDataElementsFromIndexedDB = (dataElementIds: ?Set): { +export const useDataElementsFromIndexedDB = (queryKey: Array, dataElementIds: ?Set, queryOptions?: UseQueryOptions<>): { dataElements: ?Array, - loading: boolean, - error: any, + isLoading: boolean, + isError: boolean, } => { const storageController = getUserStorageController(); + const { enabled = !!dataElementIds } = queryOptions ?? {}; - const getDataElements = useCallback(requestedIds => - storageController.getAll( + const { data, isLoading, isError } = useIndexedDBQuery( + ['dataElements', ...queryKey], + () => storageController.getAll( userStores.DATA_ELEMENTS, { - predicate: dataElement => requestedIds.has(dataElement.id), + // $FlowIgnore - the enabled prop guarantees that dataElementIds will be defined + predicate: dataElement => dataElementIds.has(dataElement.id), }, - ), [storageController]); - - const { loading, data, error } = useQueryStyleEvaluation(getDataElements, dataElementIds); + ), { + enabled, + }, + ); return { dataElements: data, - loading, - error, + isLoading, + isError, }; }; diff --git a/src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js b/src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js index d7f51efd34..f0480779ee 100644 --- a/src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js +++ b/src/core_modules/capture-core/utils/cachedDataHooks/useOptionSetsFromIndexedDB.js @@ -1,28 +1,32 @@ // @flow -import { useCallback } from 'react'; +import type { UseQueryOptions } from 'react-query'; import { userStores, getUserStorageController } from '../../storageControllers'; -import { useQueryStyleEvaluation } from '../api/useQueryStyleEvaluation'; +import { useIndexedDBQuery } from '../reactQueryHelpers'; import type { CachedOptionSet } from '../../storageControllers/'; -export const useOptionSetsFromIndexedDB = (optionSetIds: ?Set): { +export const useOptionSetsFromIndexedDB = (queryKey: Array, optionSetIds: ?Set, queryOptions?: UseQueryOptions<>): { optionSets: ?Array, - loading: boolean, - error: any, + isLoading: boolean, + isError: boolean, } => { const storageController = getUserStorageController(); + const { enabled = !!optionSetIds } = queryOptions ?? {}; - const getOptionSets = useCallback(requestedIds => - storageController.getAll( + const { data, isLoading, isError } = useIndexedDBQuery( + ['optionSets', ...queryKey], + () => storageController.getAll( userStores.OPTION_SETS, { - predicate: optionSet => requestedIds.has(optionSet.id), + // $FlowIgnore - the enabled prop guarantees that optionSetIds will be defined + predicate: optionSet => optionSetIds.has(optionSet.id), }, - ), [storageController]); - - const { loading, data, error } = useQueryStyleEvaluation(getOptionSets, optionSetIds); + ), { + enabled, + }, + ); return { optionSets: data, - loading, - error, + isLoading, + isError, }; }; diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js index ee31061b8c..2452405547 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useMetadataQuery.js @@ -5,7 +5,7 @@ import { useDataEngine, type ResourceQuery } from '@dhis2/app-runtime'; import type { QueryFunction, UseQueryOptions } from 'react-query'; import { IndexedDBError } from '../../../../capture-core-utils/storage/IndexedDBError/IndexedDBError'; import type { Result } from './useMetadataQuery.types'; -import { ReactQueryAppNamespace } from '../reactQueryHelpers.const'; +import { ReactQueryAppNamespace, IndexedDBNamespace } from '../reactQueryHelpers.const'; const throwErrorForIndexedDB = (error) => { if (error instanceof IndexedDBError) { @@ -43,7 +43,7 @@ export const useIndexedDBQuery = ( queryFn: QueryFunction, queryOptions?: UseQueryOptions, ): Result => - useAsyncMetadata(queryKey, queryFn, { + useAsyncMetadata([IndexedDBNamespace, ...queryKey], queryFn, { cacheTime: 0, ...queryOptions, onError: (error) => { diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js b/src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js index 2659b304ba..5256c91b69 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/reactQueryHelpers.const.js @@ -1,4 +1,4 @@ - // @flow export const ReactQueryAppNamespace = 'capture'; +export const IndexedDBNamespace = 'indexedDB';