From 02f253671c3b4a2790366acbaabb202a98debb65 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Wed, 9 Oct 2024 13:04:28 +0200 Subject: [PATCH 01/26] feat: temp --- .../common/helpers/buildUrlByElementType.js | 52 +++++++++++++++++++ .../WidgetsChangelog/common/helpers/index.js | 3 ++ .../common/hooks/useChangelogData.js | 34 ++++++++---- .../capture-core/converters/clientToList.js | 8 +-- 4 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js create mode 100644 src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js new file mode 100644 index 0000000000..9bd1856c59 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js @@ -0,0 +1,52 @@ +// @flow +import { dataElementTypes } from '../../../../metaData'; + +const buildTEAUrlByElementType: {| + [string]: Function, +|} = { + [dataElementTypes.FILE_RESOURCE]: ({ + trackedEntity, + id, + programId, + }: { + trackedEntity: string, + id: string, + programId: string, + }) => ({ + fileUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/file?program=${programId}`, + }), + [dataElementTypes.IMAGE]: ({ + trackedEntity, + id, + programId, + }: { + trackedEntity: string, + id: string, + programId: string, + }) => ({ + imageUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}`, + previewUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}&dimension=small`, + }), +}; + +const buildDataElementUrlByElementType: {| + [string]: Function, +|} = { + [dataElementTypes.FILE_RESOURCE]: ({ event, id }: { event: string, id: string }) => ({ + fileUrl: `/tracker/events/${event}/dataValues/${id}/file`, + }), + [dataElementTypes.IMAGE]: ({ event, id }: { event: string, id: string }) => ({ + imageUrl: `/tracker/events/${event}/dataValues/${id}/image`, + previewUrl: `/tracker/events/${event}/dataValues/${id}/image?dimension=small`, + }), +}; + +export const RECORD_TYPE = Object.freeze({ + event: 'event', + trackedEntity: 'trackedEntity', +}); + +export const buildUrlByElementType = Object.freeze({ + [RECORD_TYPE.trackedEntity]: buildTEAUrlByElementType, + [RECORD_TYPE.event]: buildDataElementUrlByElementType, +}); diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js new file mode 100644 index 0000000000..59fdc6ba6a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js @@ -0,0 +1,3 @@ +// @flow +export { buildUrlByElementType } from './buildUrlByElementType'; +export { RECORD_TYPE } from './buildUrlByElementType'; diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index bcf1c82a54..e00b5631d8 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -10,6 +10,7 @@ import { CHANGELOG_ENTITY_TYPES, QUERY_KEYS_BY_ENTITY_TYPE } from '../Changelog/ import type { Change, ChangelogRecord, ItemDefinitions, SortDirection } from '../Changelog/Changelog.types'; import { convertServerToClient } from '../../../../converters'; import { convert } from '../../../../converters/clientToList'; +import { RECORD_TYPE, buildUrlByElementType } from '../helpers'; type Props = { entityId: string, @@ -87,21 +88,32 @@ export const useChangelogData = ({ })); return null; } + let previousValue; let + currentValue; + const urls = buildUrlByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; + + let params = { id: fieldId }; + + if (entityType === RECORD_TYPE.trackedEntity) { + params = { ...params, trackedEntity: entityId, programId }; + } else if (entityType === RECORD_TYPE.event) { + params = { ...params, event: entityId }; + } + + if (urls) { + previousValue = urls(params); + currentValue = urls(params); + } else { + previousValue = convertServerToClient(change.previousValue, metadataElement.type); + currentValue = convertServerToClient(change.currentValue, metadataElement.type); + } const { firstName, surname, username } = createdBy; const { options } = metadataElement; - const previousValue = convert( - convertServerToClient(change.previousValue, metadataElement.type), - metadataElement.type, - options, - ); + previousValue = convert(previousValue, metadataElement.type, options); + currentValue = convert(currentValue, metadataElement.type, options); - const currentValue = convert( - convertServerToClient(change.currentValue, metadataElement.type), - metadataElement.type, - options, - ); return { reactKey: uuid(), @@ -114,7 +126,7 @@ export const useChangelogData = ({ currentValue, }; }).filter(Boolean); - }, [data, dataItemDefinitions, fromServerDate]); + }, [data, dataItemDefinitions, fromServerDate, entityId, entityType, programId]); return { records, diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index a5f39b4105..3afef8d1d7 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -28,7 +28,7 @@ function convertTimeForListDisplay(rawValue: string): string { type FileClientValue = { name: string, - url: string, + fileUrl: string, value: string, }; @@ -44,12 +44,12 @@ function convertFileForDisplay(clientValue: FileClientValue) { } return ( { event.stopPropagation(); }} > - {clientValue.name} + {clientValue.fileUrl} ); } @@ -59,7 +59,7 @@ function convertImageForDisplay(clientValue: ImageClientValue) { if (typeof clientValue === 'string' || clientValue instanceof String) { return clientValue; } - return ; + return ; } function convertRangeForDisplay(parser: any, clientValue: any) { From 1e14e25b2ed00cd7b99c87d76656b50f0a577a57 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Wed, 9 Oct 2024 13:44:29 +0200 Subject: [PATCH 02/26] fix: revert clienttolist changes --- .../common/helpers/buildUrlByElementType.js | 4 ++-- .../capture-core/converters/clientToList.js | 22 ++++--------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js index 9bd1856c59..87d15666e4 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js @@ -13,7 +13,7 @@ const buildTEAUrlByElementType: {| id: string, programId: string, }) => ({ - fileUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/file?program=${programId}`, + url: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/file?program=${programId}`, }), [dataElementTypes.IMAGE]: ({ trackedEntity, @@ -33,7 +33,7 @@ const buildDataElementUrlByElementType: {| [string]: Function, |} = { [dataElementTypes.FILE_RESOURCE]: ({ event, id }: { event: string, id: string }) => ({ - fileUrl: `/tracker/events/${event}/dataValues/${id}/file`, + url: `/tracker/events/${event}/dataValues/${id}/file`, }), [dataElementTypes.IMAGE]: ({ event, id }: { event: string, id: string }) => ({ imageUrl: `/tracker/events/${event}/dataValues/${id}/image`, diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index 3afef8d1d7..5932bec3f5 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -13,14 +13,12 @@ function convertDateForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); return convertMomentToDateFormatString(momentDate); } - function convertDateTimeForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); const dateString = convertMomentToDateFormatString(momentDate); const timeString = momentDate.format('HH:mm'); return `${dateString} ${timeString}`; } - function convertTimeForListDisplay(rawValue: string): string { const momentDate = moment(rawValue, 'HH:mm', true); return momentDate.format('HH:mm'); @@ -28,7 +26,7 @@ function convertTimeForListDisplay(rawValue: string): string { type FileClientValue = { name: string, - fileUrl: string, + url: string, value: string, }; @@ -36,7 +34,6 @@ type ImageClientValue = { ...FileClientValue, previewUrl: string, }; - function convertFileForDisplay(clientValue: FileClientValue) { // Fallback until https://dhis2.atlassian.net/browse/DHIS2-16994 is implemented if (typeof clientValue === 'string' || clientValue instanceof String) { @@ -44,22 +41,21 @@ function convertFileForDisplay(clientValue: FileClientValue) { } return ( { event.stopPropagation(); }} > - {clientValue.fileUrl} + {clientValue.name} ); } - function convertImageForDisplay(clientValue: ImageClientValue) { // Fallback until https://dhis2.atlassian.net/browse/DHIS2-16994 is implemented if (typeof clientValue === 'string' || clientValue instanceof String) { return clientValue; } - return ; + return ; } function convertRangeForDisplay(parser: any, clientValue: any) { @@ -76,7 +72,6 @@ function convertNumberRangeForDisplay(clientValue) { ); } - function convertStatusForDisplay(clientValue: Object) { const { isNegative, isPositive, text } = clientValue; return ( @@ -85,11 +80,9 @@ function convertStatusForDisplay(clientValue: Object) { ); } - function convertOrgUnitForDisplay(rawValue: string | Object) { return (typeof rawValue === 'string' ? rawValue : rawValue.name); } - const valueConvertersForType = { [dataElementTypes.NUMBER]: stringifyNumber, [dataElementTypes.INTEGER]: stringifyNumber, @@ -118,24 +111,19 @@ const valueConvertersForType = { [dataElementTypes.NUMBER_RANGE]: convertNumberRangeForDisplay, [dataElementTypes.STATUS]: convertStatusForDisplay, }; - export function convertValue(value: any, type: $Keys, dataElement?: ?DataElement) { if (!value && value !== 0 && value !== false) { return value; } - if (dataElement && dataElement.optionSet) { if (dataElement.type === dataElementTypes.MULTI_TEXT) { return dataElement.optionSet.getMultiOptionsText(value); } return dataElement.optionSet.getOptionText(value); } - // $FlowFixMe dataElementTypes flow error return valueConvertersForType[type] ? valueConvertersForType[type](value) : value; } - - // This function will replace the convertValue function in the future (as it should not require a dataElement class to use optionSet) export function convert( value: any, @@ -145,7 +133,6 @@ export function convert( if (!value && value !== 0 && value !== false) { return value; } - if (options) { if (type === dataElementTypes.MULTI_TEXT) { return options @@ -157,7 +144,6 @@ export function convert( .find(option => option.code === value) ?.name ?? value; } - // $FlowFixMe dataElementTypes flow error return valueConvertersForType[type] ? valueConvertersForType[type](value) : value; } From 6cd1a7dbfafb817f72292a6df2952307e622d3c4 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Wed, 9 Oct 2024 14:14:53 +0200 Subject: [PATCH 03/26] fix: code clean up --- .../common/hooks/useChangelogData.js | 4 ++-- .../capture-core/converters/clientToList.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index e00b5631d8..5486f4200c 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -88,8 +88,8 @@ export const useChangelogData = ({ })); return null; } - let previousValue; let - currentValue; + let previousValue; + let currentValue; const urls = buildUrlByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; let params = { id: fieldId }; diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index 5932bec3f5..a5f39b4105 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -13,12 +13,14 @@ function convertDateForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); return convertMomentToDateFormatString(momentDate); } + function convertDateTimeForListDisplay(rawValue: string): string { const momentDate = moment(rawValue); const dateString = convertMomentToDateFormatString(momentDate); const timeString = momentDate.format('HH:mm'); return `${dateString} ${timeString}`; } + function convertTimeForListDisplay(rawValue: string): string { const momentDate = moment(rawValue, 'HH:mm', true); return momentDate.format('HH:mm'); @@ -34,6 +36,7 @@ type ImageClientValue = { ...FileClientValue, previewUrl: string, }; + function convertFileForDisplay(clientValue: FileClientValue) { // Fallback until https://dhis2.atlassian.net/browse/DHIS2-16994 is implemented if (typeof clientValue === 'string' || clientValue instanceof String) { @@ -50,6 +53,7 @@ function convertFileForDisplay(clientValue: FileClientValue) { ); } + function convertImageForDisplay(clientValue: ImageClientValue) { // Fallback until https://dhis2.atlassian.net/browse/DHIS2-16994 is implemented if (typeof clientValue === 'string' || clientValue instanceof String) { @@ -72,6 +76,7 @@ function convertNumberRangeForDisplay(clientValue) { ); } + function convertStatusForDisplay(clientValue: Object) { const { isNegative, isPositive, text } = clientValue; return ( @@ -80,9 +85,11 @@ function convertStatusForDisplay(clientValue: Object) { ); } + function convertOrgUnitForDisplay(rawValue: string | Object) { return (typeof rawValue === 'string' ? rawValue : rawValue.name); } + const valueConvertersForType = { [dataElementTypes.NUMBER]: stringifyNumber, [dataElementTypes.INTEGER]: stringifyNumber, @@ -111,19 +118,24 @@ const valueConvertersForType = { [dataElementTypes.NUMBER_RANGE]: convertNumberRangeForDisplay, [dataElementTypes.STATUS]: convertStatusForDisplay, }; + export function convertValue(value: any, type: $Keys, dataElement?: ?DataElement) { if (!value && value !== 0 && value !== false) { return value; } + if (dataElement && dataElement.optionSet) { if (dataElement.type === dataElementTypes.MULTI_TEXT) { return dataElement.optionSet.getMultiOptionsText(value); } return dataElement.optionSet.getOptionText(value); } + // $FlowFixMe dataElementTypes flow error return valueConvertersForType[type] ? valueConvertersForType[type](value) : value; } + + // This function will replace the convertValue function in the future (as it should not require a dataElement class to use optionSet) export function convert( value: any, @@ -133,6 +145,7 @@ export function convert( if (!value && value !== 0 && value !== false) { return value; } + if (options) { if (type === dataElementTypes.MULTI_TEXT) { return options @@ -144,6 +157,7 @@ export function convert( .find(option => option.code === value) ?.name ?? value; } + // $FlowFixMe dataElementTypes flow error return valueConvertersForType[type] ? valueConvertersForType[type](value) : value; } From 94d61f363d5a05977c5d730424114824b81ad0f1 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Wed, 9 Oct 2024 15:22:25 +0200 Subject: [PATCH 04/26] feat: temp --- .../common/helpers/buildUrlByElementType.js | 26 ++++--- .../common/hooks/useChangelogData.js | 70 ++++++++++++++----- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js index 87d15666e4..28acecf3a4 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js @@ -8,36 +8,46 @@ const buildTEAUrlByElementType: {| trackedEntity, id, programId, + absoluteApiPath, }: { trackedEntity: string, id: string, programId: string, + absoluteApiPath: string, }) => ({ - url: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/file?program=${programId}`, + id, + name: 'test.file', + value: id, + url: `${absoluteApiPath}/tracker/trackedEntities/${trackedEntity}/attributes/${id}/file?program=${programId}`, }), [dataElementTypes.IMAGE]: ({ trackedEntity, id, programId, + absoluteApiPath, }: { trackedEntity: string, id: string, programId: string, + absoluteApiPath: string, }) => ({ - imageUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}`, - previewUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}&dimension=small`, + imageUrl: `${absoluteApiPath}/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}`, + previewUrl: `${absoluteApiPath}/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}&dimension=small`, }), }; const buildDataElementUrlByElementType: {| [string]: Function, |} = { - [dataElementTypes.FILE_RESOURCE]: ({ event, id }: { event: string, id: string }) => ({ - url: `/tracker/events/${event}/dataValues/${id}/file`, + [dataElementTypes.FILE_RESOURCE]: ({ event, id, absoluteApiPath }: { event: string, id: string, absoluteApiPath: string }) => ({ + id, + name: 'test.file', + value: id, + url: `${absoluteApiPath}/tracker/events/${event}/dataValues/${id}/file`, }), - [dataElementTypes.IMAGE]: ({ event, id }: { event: string, id: string }) => ({ - imageUrl: `/tracker/events/${event}/dataValues/${id}/image`, - previewUrl: `/tracker/events/${event}/dataValues/${id}/image?dimension=small`, + [dataElementTypes.IMAGE]: ({ event, id, absoluteApiPath }: { event: string, id: string, absoluteApiPath: string }) => ({ + imageUrl: `${absoluteApiPath}/tracker/events/${event}/dataValues/${id}/image`, + previewUrl: `${absoluteApiPath}/tracker/events/${event}/dataValues/${id}/image?dimension=small`, }), }; diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index 5486f4200c..03986d3d68 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -2,9 +2,9 @@ import moment from 'moment'; import { v4 as uuid } from 'uuid'; import log from 'loglevel'; -import { errorCreator } from 'capture-core-utils'; +import { errorCreator, buildUrl } from 'capture-core-utils'; import { useMemo, useState } from 'react'; -import { useTimeZoneConversion } from '@dhis2/app-runtime'; +import { useTimeZoneConversion, useConfig } from '@dhis2/app-runtime'; import { useApiDataQuery } from '../../../../utils/reactQueryHelpers'; import { CHANGELOG_ENTITY_TYPES, QUERY_KEYS_BY_ENTITY_TYPE } from '../Changelog/Changelog.constants'; import type { Change, ChangelogRecord, ItemDefinitions, SortDirection } from '../Changelog/Changelog.types'; @@ -68,6 +68,8 @@ export const useChangelogData = ({ }, ); + const { baseUrl, apiVersion } = useConfig(); + const absoluteApiPath = buildUrl(baseUrl, `api/${apiVersion}`); const records: ?Array = useMemo(() => { if (!data) return undefined; @@ -88,32 +90,62 @@ export const useChangelogData = ({ })); return null; } - let previousValue; - let currentValue; - const urls = buildUrlByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; - let params = { id: fieldId }; - if (entityType === RECORD_TYPE.trackedEntity) { - params = { ...params, trackedEntity: entityId, programId }; - } else if (entityType === RECORD_TYPE.event) { - params = { ...params, event: entityId }; - } + let previousValueRaw; + let currentValueRaw; + + const urls = buildUrlByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; if (urls) { - previousValue = urls(params); - currentValue = urls(params); + const commonParams = { + id: fieldId, + absoluteApiPath, + }; + + if (entityType === RECORD_TYPE.trackedEntity) { + previousValueRaw = urls({ + trackedEntity: entityId, + programId, + ...commonParams, + }); + currentValueRaw = urls({ + trackedEntity: entityId, + programId, + name: metadataElement.name, + ...commonParams, + }); + } else if (entityType === RECORD_TYPE.event) { + previousValueRaw = urls({ + event: entityId, + ...commonParams, + }); + currentValueRaw = urls({ + event: entityId, + ...commonParams, + }); + } else { + previousValueRaw = urls({ ...commonParams }); + currentValueRaw = urls({ ...commonParams }); + } } else { - previousValue = convertServerToClient(change.previousValue, metadataElement.type); - currentValue = convertServerToClient(change.currentValue, metadataElement.type); + previousValueRaw = convertServerToClient(change.previousValue, metadataElement.type); + currentValueRaw = convertServerToClient(change.currentValue, metadataElement.type); } - const { firstName, surname, username } = createdBy; const { options } = metadataElement; - previousValue = convert(previousValue, metadataElement.type, options); - currentValue = convert(currentValue, metadataElement.type, options); + const previousValue = convert( + previousValueRaw, + metadataElement.type, + options, + ); + const currentValue = convert( + currentValueRaw, + metadataElement.type, + options, + ); return { reactKey: uuid(), @@ -126,7 +158,7 @@ export const useChangelogData = ({ currentValue, }; }).filter(Boolean); - }, [data, dataItemDefinitions, fromServerDate, entityId, entityType, programId]); + }, [data, dataItemDefinitions, fromServerDate, entityId, entityType, programId, absoluteApiPath]); return { records, From 1464867877db641b4da828ccb8e9afd0fa3ba514 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Thu, 10 Oct 2024 13:27:09 +0200 Subject: [PATCH 05/26] fix: wrong else statement --- .../WidgetsChangelog/common/hooks/useChangelogData.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index 03986d3d68..0684ca1fdb 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -115,7 +115,8 @@ export const useChangelogData = ({ name: metadataElement.name, ...commonParams, }); - } else if (entityType === RECORD_TYPE.event) { + } + if (entityType === RECORD_TYPE.event) { previousValueRaw = urls({ event: entityId, ...commonParams, @@ -124,14 +125,12 @@ export const useChangelogData = ({ event: entityId, ...commonParams, }); - } else { - previousValueRaw = urls({ ...commonParams }); - currentValueRaw = urls({ ...commonParams }); } } else { previousValueRaw = convertServerToClient(change.previousValue, metadataElement.type); currentValueRaw = convertServerToClient(change.currentValue, metadataElement.type); } + const { firstName, surname, username } = createdBy; const { options } = metadataElement; From 9a5dbdaea886e4f37f57f887ba93b0d5b6ccf110 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 11 Oct 2024 15:10:31 +0200 Subject: [PATCH 06/26] feat: add link for image and file data element --- .../OverflowMenu/OverflowMenu.component.js | 2 +- .../common/helpers/buildUrlByElementType.js | 62 ----- .../WidgetsChangelog/common/helpers/index.js | 3 - .../common/hooks/useChangelogData.js | 247 +++++++++++------- .../utils/getSubValueForChangelogData.js | 121 +++++++++ 5 files changed, 274 insertions(+), 161 deletions(-) delete mode 100644 src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js delete mode 100644 src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js create mode 100644 src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js index e711f1a1f7..c008500432 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js @@ -29,7 +29,7 @@ export const OverflowMenuComponent = ({ icon={} small secondary - dataTest="widget-profile-overflow-menu" + dataTest="widget-profile-overflow-men" component={ {displayChangelog && ( diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js deleted file mode 100644 index 28acecf3a4..0000000000 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/buildUrlByElementType.js +++ /dev/null @@ -1,62 +0,0 @@ -// @flow -import { dataElementTypes } from '../../../../metaData'; - -const buildTEAUrlByElementType: {| - [string]: Function, -|} = { - [dataElementTypes.FILE_RESOURCE]: ({ - trackedEntity, - id, - programId, - absoluteApiPath, - }: { - trackedEntity: string, - id: string, - programId: string, - absoluteApiPath: string, - }) => ({ - id, - name: 'test.file', - value: id, - url: `${absoluteApiPath}/tracker/trackedEntities/${trackedEntity}/attributes/${id}/file?program=${programId}`, - }), - [dataElementTypes.IMAGE]: ({ - trackedEntity, - id, - programId, - absoluteApiPath, - }: { - trackedEntity: string, - id: string, - programId: string, - absoluteApiPath: string, - }) => ({ - imageUrl: `${absoluteApiPath}/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}`, - previewUrl: `${absoluteApiPath}/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}&dimension=small`, - }), -}; - -const buildDataElementUrlByElementType: {| - [string]: Function, -|} = { - [dataElementTypes.FILE_RESOURCE]: ({ event, id, absoluteApiPath }: { event: string, id: string, absoluteApiPath: string }) => ({ - id, - name: 'test.file', - value: id, - url: `${absoluteApiPath}/tracker/events/${event}/dataValues/${id}/file`, - }), - [dataElementTypes.IMAGE]: ({ event, id, absoluteApiPath }: { event: string, id: string, absoluteApiPath: string }) => ({ - imageUrl: `${absoluteApiPath}/tracker/events/${event}/dataValues/${id}/image`, - previewUrl: `${absoluteApiPath}/tracker/events/${event}/dataValues/${id}/image?dimension=small`, - }), -}; - -export const RECORD_TYPE = Object.freeze({ - event: 'event', - trackedEntity: 'trackedEntity', -}); - -export const buildUrlByElementType = Object.freeze({ - [RECORD_TYPE.trackedEntity]: buildTEAUrlByElementType, - [RECORD_TYPE.event]: buildDataElementUrlByElementType, -}); diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js deleted file mode 100644 index 59fdc6ba6a..0000000000 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/helpers/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -export { buildUrlByElementType } from './buildUrlByElementType'; -export { RECORD_TYPE } from './buildUrlByElementType'; diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index 0684ca1fdb..a065df3428 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -3,21 +3,22 @@ import moment from 'moment'; import { v4 as uuid } from 'uuid'; import log from 'loglevel'; import { errorCreator, buildUrl } from 'capture-core-utils'; -import { useMemo, useState } from 'react'; -import { useTimeZoneConversion, useConfig } from '@dhis2/app-runtime'; +import { useState, useEffect, useMemo } from 'react'; +import { useTimeZoneConversion, useConfig, useDataEngine } from '@dhis2/app-runtime'; import { useApiDataQuery } from '../../../../utils/reactQueryHelpers'; import { CHANGELOG_ENTITY_TYPES, QUERY_KEYS_BY_ENTITY_TYPE } from '../Changelog/Changelog.constants'; import type { Change, ChangelogRecord, ItemDefinitions, SortDirection } from '../Changelog/Changelog.types'; import { convertServerToClient } from '../../../../converters'; import { convert } from '../../../../converters/clientToList'; -import { RECORD_TYPE, buildUrlByElementType } from '../helpers'; +import { RECORD_TYPE, subValueGetterByElementType } from '../utils/getSubValueForChangelogData'; +import { makeQuerySingleResource } from '../../../../utils/api'; type Props = { entityId: string, programId?: string, entityType: $Values, dataItemDefinitions: ItemDefinitions, -} +}; const DEFAULT_PAGE_SIZE = 10; const DEFAULT_SORT_DIRECTION = 'default'; @@ -40,6 +41,16 @@ export const useChangelogData = ({ programId, dataItemDefinitions, }: Props) => { + const dataEngine = useDataEngine(); + const { baseUrl, apiVersion } = useConfig(); + + // Memoize the bound query function + const query = useMemo(() => dataEngine.query.bind(dataEngine), [dataEngine]); + + // Create querySingleResource using the memoized query + const querySingleResource = useMemo(() => makeQuerySingleResource(query), [query]); + + const absoluteApiPath = buildUrl(baseUrl, `api/${apiVersion}`); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [sortDirection, setSortDirection] = useState(DEFAULT_SORT_DIRECTION); @@ -58,9 +69,7 @@ export const useChangelogData = ({ page, pageSize, program: programId, - ...{ - order: sortDirection === DEFAULT_SORT_DIRECTION ? undefined : `createdAt:${sortDirection}`, - }, + order: sortDirection === DEFAULT_SORT_DIRECTION ? undefined : `createdAt:${sortDirection}`, }, }, { @@ -68,96 +77,144 @@ export const useChangelogData = ({ }, ); - const { baseUrl, apiVersion } = useConfig(); - const absoluteApiPath = buildUrl(baseUrl, `api/${apiVersion}`); - const records: ?Array = useMemo(() => { - if (!data) return undefined; - - return data.changeLogs.map((changelog) => { - const { change: apiChange, createdAt, createdBy } = changelog; - const elementKey = Object.keys(apiChange)[0]; - const change = apiChange[elementKey]; - - const { metadataElement, fieldId } = getMetadataItemDefinition( - elementKey, - change, - dataItemDefinitions, - ); - - if (!metadataElement) { - log.error(errorCreator('Could not find metadata for element')({ - ...changelog, - })); - return null; - } - - - let previousValueRaw; - let currentValueRaw; - - const urls = buildUrlByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; - - if (urls) { - const commonParams = { - id: fieldId, - absoluteApiPath, - }; - - if (entityType === RECORD_TYPE.trackedEntity) { - previousValueRaw = urls({ - trackedEntity: entityId, - programId, - ...commonParams, - }); - currentValueRaw = urls({ - trackedEntity: entityId, - programId, - name: metadataElement.name, - ...commonParams, - }); - } - if (entityType === RECORD_TYPE.event) { - previousValueRaw = urls({ - event: entityId, - ...commonParams, - }); - currentValueRaw = urls({ - event: entityId, - ...commonParams, - }); - } - } else { - previousValueRaw = convertServerToClient(change.previousValue, metadataElement.type); - currentValueRaw = convertServerToClient(change.currentValue, metadataElement.type); - } - - const { firstName, surname, username } = createdBy; - const { options } = metadataElement; - - const previousValue = convert( - previousValueRaw, - metadataElement.type, - options, - ); - - const currentValue = convert( - currentValueRaw, - metadataElement.type, - options, + const [records, setRecords] = useState>(undefined); + + useEffect(() => { + const fetchRecords = async () => { + if (!data) return; + + const fetchedRecords = await Promise.all( + data.changeLogs + .map(async (changelog) => { + const { change: apiChange, createdAt, createdBy } = changelog; + const elementKey = Object.keys(apiChange)[0]; + const change = apiChange[elementKey]; + + const { metadataElement, fieldId } = getMetadataItemDefinition( + elementKey, + change, + dataItemDefinitions, + ); + + if (!metadataElement) { + log.error( + errorCreator('Could not find metadata for element')({ + ...changelog, + }), + ); + return null; + } + + let previousValueRaw; + let currentValueRaw; + + const urls = + subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; + + if (urls) { + if (entityType === RECORD_TYPE.trackedEntity) { + previousValueRaw = change.previousValue + ? await urls({ + trackedEntity: { + teiId: entityId, + value: change.previousValue, + }, + programId, + attributeId: fieldId, + absoluteApiPath, + querySingleResource, + isPreviousValue: true, + }) + : null; + currentValueRaw = await urls({ + trackedEntity: { + teiId: entityId, + value: change.currentValue, + }, + programId, + attributeId: fieldId, + absoluteApiPath, + querySingleResource, + }); + } else if (entityType === RECORD_TYPE.event) { + previousValueRaw = change.previousValue + ? await urls({ + dataElement: { + id: fieldId, + value: change.previousValue, + }, + eventId: entityId, + absoluteApiPath, + querySingleResource, + isPreviousValue: true, + }) + : null; + currentValueRaw = await urls({ + dataElement: { + id: fieldId, + value: change.currentValue, + }, + eventId: entityId, + absoluteApiPath, + querySingleResource, + }); + } + } else { + previousValueRaw = convertServerToClient( + change.previousValue, + metadataElement.type, + ); + currentValueRaw = convertServerToClient( + change.currentValue, + metadataElement.type, + ); + } + + const { firstName, surname, username } = createdBy; + const { options } = metadataElement; + + const previousValue = convert( + previousValueRaw, + metadataElement.type, + options, + ); + + const currentValue = convert( + currentValueRaw, + metadataElement.type, + options, + ); + + return { + reactKey: uuid(), + date: moment(fromServerDate(createdAt)).format('YYYY-MM-DD HH:mm'), + user: `${firstName} ${surname} (${username})`, + dataItemId: fieldId, + changeType: changelog.type, + dataItemLabel: metadataElement.name, + previousValue, + currentValue, + }; + }) + .filter(Boolean), ); - return { - reactKey: uuid(), - date: moment(fromServerDate(createdAt)).format('YYYY-MM-DD HH:mm'), - user: `${firstName} ${surname} (${username})`, - dataItemId: fieldId, - changeType: changelog.type, - dataItemLabel: metadataElement.name, - previousValue, - currentValue, - }; - }).filter(Boolean); - }, [data, dataItemDefinitions, fromServerDate, entityId, entityType, programId, absoluteApiPath]); + setRecords(fetchedRecords); + }; + + fetchRecords(); + }, [ + data, + dataItemDefinitions, + fromServerDate, + entityId, + entityType, + programId, + absoluteApiPath, + querySingleResource, + ]); + + console.log('records', records); return { records, diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js new file mode 100644 index 0000000000..c7921ea7a1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js @@ -0,0 +1,121 @@ +// @flow +import { dataElementTypes } from '../../../../metaData'; +import type { QuerySingleResource } from '../../../../utils/api'; + + +type subValueTEAProps = { + trackedEntity: Object, + attributeId: string, + programId: string, + absoluteApiPath: string, + querySingleResource: QuerySingleResource, + isPreviousValue?: boolean, +}; + +type SubValuesDataElementProps = { + dataElement: Object, + querySingleResource: QuerySingleResource, + eventId: string, + absoluteApiPath: string, + isPreviousValue?: boolean, +}; + + +const buildTEAUrlByElementType: {| +[string]: Function, +|} = { + [dataElementTypes.FILE_RESOURCE]: async ({ + trackedEntity, + attributeId, + programId, + absoluteApiPath, + querySingleResource, + isPreviousValue, + }: subValueTEAProps) => { + const { id: teiId, value } = trackedEntity; + if (!value) return null; + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + if (isPreviousValue) { + return name; + } + + return { + id, + name, + url: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/file?program=${programId}`, + }; + }, + [dataElementTypes.IMAGE]: async ({ + trackedEntity, + attributeId, + programId, + absoluteApiPath, + isPreviousValue, + querySingleResource, + }: subValueTEAProps) => { + const { id: teiId, value } = trackedEntity; + if (!value) return null; + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + if (isPreviousValue) { + return name; + } + + return { + id, + name, + imageUrl: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}`, + previewUrl: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}&dimension=small`, + }; + }, +}; + + +const buildDataElementUrlByElementType: {| +[string]: Function, +|} = { + [dataElementTypes.FILE_RESOURCE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, isPreviousValue }: SubValuesDataElementProps) => { + const { id: dataElementId, value } = dataElement; + if (!value) return null; + + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + if (isPreviousValue) { + return name; + } + + return { + id, + name, + url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/file`, + }; + }, + [dataElementTypes.IMAGE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, isPreviousValue }: SubValuesDataElementProps) => { + const { id: dataElementId, value } = dataElement; + if (!value) return null; + + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + if (isPreviousValue) { + return name; + } + + return { + id, + name, + imageUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image`, + previewUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image?dimension=small`, + }; + }, +}; + +export const RECORD_TYPE = Object.freeze({ + event: 'event', + trackedEntity: 'trackedEntity', +}); + +export const subValueGetterByElementType = Object.freeze({ + [RECORD_TYPE.trackedEntity]: buildTEAUrlByElementType, + [RECORD_TYPE.event]: buildDataElementUrlByElementType, +}); From 41b9ed1bdbf12615e10bfaac7690dd4527fdb9af Mon Sep 17 00:00:00 2001 From: henrikmv Date: Sun, 13 Oct 2024 17:57:04 +0200 Subject: [PATCH 07/26] fix: image and file for tea --- .../WidgetProfile/OverflowMenu/OverflowMenu.component.js | 2 +- .../WidgetsChangelog/common/hooks/useChangelogData.js | 2 -- .../common/utils/getSubValueForChangelogData.js | 8 ++++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js index c008500432..e711f1a1f7 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.js @@ -29,7 +29,7 @@ export const OverflowMenuComponent = ({ icon={} small secondary - dataTest="widget-profile-overflow-men" + dataTest="widget-profile-overflow-menu" component={ {displayChangelog && ( diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index a065df3428..5728c9f259 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -44,10 +44,8 @@ export const useChangelogData = ({ const dataEngine = useDataEngine(); const { baseUrl, apiVersion } = useConfig(); - // Memoize the bound query function const query = useMemo(() => dataEngine.query.bind(dataEngine), [dataEngine]); - // Create querySingleResource using the memoized query const querySingleResource = useMemo(() => makeQuerySingleResource(query), [query]); const absoluteApiPath = buildUrl(baseUrl, `api/${apiVersion}`); diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js index c7921ea7a1..24edc2073d 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js @@ -32,7 +32,7 @@ const buildTEAUrlByElementType: {| querySingleResource, isPreviousValue, }: subValueTEAProps) => { - const { id: teiId, value } = trackedEntity; + const { teiId, value } = trackedEntity; if (!value) return null; const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); @@ -54,7 +54,7 @@ const buildTEAUrlByElementType: {| isPreviousValue, querySingleResource, }: subValueTEAProps) => { - const { id: teiId, value } = trackedEntity; + const { teiId, value } = trackedEntity; if (!value) return null; const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); @@ -65,7 +65,7 @@ const buildTEAUrlByElementType: {| return { id, name, - imageUrl: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}`, + url: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}`, previewUrl: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}&dimension=small`, }; }, @@ -104,7 +104,7 @@ const buildDataElementUrlByElementType: {| return { id, name, - imageUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image`, + url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image`, previewUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image?dimension=small`, }; }, From c546e9cceabe70879296104ffef58027a7335832 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Sun, 13 Oct 2024 20:52:05 +0200 Subject: [PATCH 08/26] fix: show only latest image and file --- .../common/hooks/useChangelogData.js | 222 ++++++++---------- .../utils/getSubValueForChangelogData.js | 28 +-- 2 files changed, 110 insertions(+), 140 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index 5728c9f259..7ffa840364 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -1,16 +1,27 @@ // @flow +import { useState, useEffect, useMemo } from 'react'; import moment from 'moment'; import { v4 as uuid } from 'uuid'; import log from 'loglevel'; -import { errorCreator, buildUrl } from 'capture-core-utils'; -import { useState, useEffect, useMemo } from 'react'; import { useTimeZoneConversion, useConfig, useDataEngine } from '@dhis2/app-runtime'; +import { errorCreator, buildUrl } from 'capture-core-utils'; import { useApiDataQuery } from '../../../../utils/reactQueryHelpers'; -import { CHANGELOG_ENTITY_TYPES, QUERY_KEYS_BY_ENTITY_TYPE } from '../Changelog/Changelog.constants'; -import type { Change, ChangelogRecord, ItemDefinitions, SortDirection } from '../Changelog/Changelog.types'; +import { + CHANGELOG_ENTITY_TYPES, + QUERY_KEYS_BY_ENTITY_TYPE, +} from '../Changelog/Changelog.constants'; +import type { + Change, + ChangelogRecord, + ItemDefinitions, + SortDirection, +} from '../Changelog/Changelog.types'; import { convertServerToClient } from '../../../../converters'; import { convert } from '../../../../converters/clientToList'; -import { RECORD_TYPE, subValueGetterByElementType } from '../utils/getSubValueForChangelogData'; +import { + RECORD_TYPE, + subValueGetterByElementType, +} from '../utils/getSubValueForChangelogData'; import { makeQuerySingleResource } from '../../../../utils/api'; type Props = { @@ -43,16 +54,18 @@ export const useChangelogData = ({ }: Props) => { const dataEngine = useDataEngine(); const { baseUrl, apiVersion } = useConfig(); - - const query = useMemo(() => dataEngine.query.bind(dataEngine), [dataEngine]); - - const querySingleResource = useMemo(() => makeQuerySingleResource(query), [query]); - + const { fromServerDate } = useTimeZoneConversion(); const absoluteApiPath = buildUrl(baseUrl, `api/${apiVersion}`); + const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [sortDirection, setSortDirection] = useState(DEFAULT_SORT_DIRECTION); - const { fromServerDate } = useTimeZoneConversion(); + const [records, setRecords] = useState>(undefined); + + const querySingleResource = useMemo( + () => makeQuerySingleResource(dataEngine.query.bind(dataEngine)), + [dataEngine], + ); const handleChangePageSize = (newPageSize: number) => { setPage(1); @@ -75,129 +88,90 @@ export const useChangelogData = ({ }, ); - const [records, setRecords] = useState>(undefined); - useEffect(() => { const fetchRecords = async () => { if (!data) return; + const mostRecentCreatedAt = data.changeLogs.reduce( + (latest, record) => (moment(record.createdAt).isAfter(latest) ? record.createdAt : latest), + data.changeLogs[0]?.createdAt, + ); + const fetchedRecords = await Promise.all( - data.changeLogs - .map(async (changelog) => { - const { change: apiChange, createdAt, createdBy } = changelog; - const elementKey = Object.keys(apiChange)[0]; - const change = apiChange[elementKey]; - - const { metadataElement, fieldId } = getMetadataItemDefinition( - elementKey, - change, - dataItemDefinitions, + data.changeLogs.map(async (changelog) => { + const { change: apiChange, createdAt, createdBy, type } = changelog; + const elementKey = Object.keys(apiChange)[0]; + const change = apiChange[elementKey]; + + const { metadataElement, fieldId } = getMetadataItemDefinition( + elementKey, + change, + dataItemDefinitions, + ); + + if (!metadataElement) { + log.error( + errorCreator('Could not find metadata for element')({ ...changelog }), ); + return null; + } - if (!metadataElement) { - log.error( - errorCreator('Could not find metadata for element')({ - ...changelog, - }), - ); - return null; - } - - let previousValueRaw; - let currentValueRaw; - - const urls = - subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; - - if (urls) { - if (entityType === RECORD_TYPE.trackedEntity) { - previousValueRaw = change.previousValue - ? await urls({ - trackedEntity: { - teiId: entityId, - value: change.previousValue, - }, - programId, - attributeId: fieldId, - absoluteApiPath, - querySingleResource, - isPreviousValue: true, - }) - : null; - currentValueRaw = await urls({ - trackedEntity: { - teiId: entityId, - value: change.currentValue, - }, - programId, - attributeId: fieldId, - absoluteApiPath, - querySingleResource, - }); - } else if (entityType === RECORD_TYPE.event) { - previousValueRaw = change.previousValue - ? await urls({ - dataElement: { - id: fieldId, - value: change.previousValue, - }, - eventId: entityId, - absoluteApiPath, - querySingleResource, - isPreviousValue: true, - }) - : null; - currentValueRaw = await urls({ - dataElement: { - id: fieldId, - value: change.currentValue, - }, - eventId: entityId, - absoluteApiPath, - querySingleResource, - }); - } - } else { - previousValueRaw = convertServerToClient( - change.previousValue, - metadataElement.type, - ); - currentValueRaw = convertServerToClient( - change.currentValue, - metadataElement.type, - ); - } - - const { firstName, surname, username } = createdBy; - const { options } = metadataElement; + const getSubValue = + subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; - const previousValue = convert( - previousValueRaw, - metadataElement.type, - options, - ); - - const currentValue = convert( - currentValueRaw, - metadataElement.type, - options, - ); + const isLatestValue = moment(createdAt).isSameOrAfter(mostRecentCreatedAt); - return { - reactKey: uuid(), - date: moment(fromServerDate(createdAt)).format('YYYY-MM-DD HH:mm'), - user: `${firstName} ${surname} (${username})`, - dataItemId: fieldId, - changeType: changelog.type, - dataItemLabel: metadataElement.name, - previousValue, - currentValue, - }; - }) - .filter(Boolean), + const getValue = async (value, latestValue) => { + if (!getSubValue) { + return convertServerToClient(value, metadataElement.type); + } + if (entityType === RECORD_TYPE.trackedEntity) { + return getSubValue({ + trackedEntity: { teiId: entityId, value }, + programId, + attributeId: fieldId, + absoluteApiPath, + querySingleResource, + latestValue, + }); + } + if (entityType === RECORD_TYPE.event) { + return getSubValue({ + dataElement: { id: fieldId, value }, + eventId: entityId, + absoluteApiPath, + querySingleResource, + latestValue, + }); + } + return null; + }; + + const [previousValueRaw, currentValueRaw] = await Promise.all([ + change.previousValue ? getValue(change.previousValue, false) : null, + getValue(change.currentValue, isLatestValue), + ]); + + const { firstName, surname, username } = createdBy; + const { options } = metadataElement; + + const previousValue = convert(previousValueRaw, metadataElement.type, options); + const currentValue = convert(currentValueRaw, metadataElement.type, options); + + return { + reactKey: uuid(), + date: moment(fromServerDate(createdAt)).format('YYYY-MM-DD HH:mm'), + user: `${firstName} ${surname} (${username})`, + dataItemId: fieldId, + changeType: type, + dataItemLabel: metadataElement.name, + previousValue, + currentValue, + }; + }), ); - setRecords(fetchedRecords); + setRecords(fetchedRecords.filter(Boolean)); }; fetchRecords(); @@ -212,8 +186,6 @@ export const useChangelogData = ({ querySingleResource, ]); - console.log('records', records); - return { records, pager: data?.pager, diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js index 24edc2073d..6590f888a9 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js @@ -2,14 +2,13 @@ import { dataElementTypes } from '../../../../metaData'; import type { QuerySingleResource } from '../../../../utils/api'; - -type subValueTEAProps = { +type SubValueTEAProps = { trackedEntity: Object, attributeId: string, programId: string, absoluteApiPath: string, querySingleResource: QuerySingleResource, - isPreviousValue?: boolean, + latestValue?: boolean, }; type SubValuesDataElementProps = { @@ -17,10 +16,9 @@ type SubValuesDataElementProps = { querySingleResource: QuerySingleResource, eventId: string, absoluteApiPath: string, - isPreviousValue?: boolean, + latestValue?: boolean, }; - const buildTEAUrlByElementType: {| [string]: Function, |} = { @@ -30,13 +28,13 @@ const buildTEAUrlByElementType: {| programId, absoluteApiPath, querySingleResource, - isPreviousValue, - }: subValueTEAProps) => { + latestValue, + }: SubValueTEAProps) => { const { teiId, value } = trackedEntity; if (!value) return null; const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - if (isPreviousValue) { + if (!latestValue) { return name; } @@ -51,14 +49,14 @@ const buildTEAUrlByElementType: {| attributeId, programId, absoluteApiPath, - isPreviousValue, + latestValue, querySingleResource, - }: subValueTEAProps) => { + }: SubValueTEAProps) => { const { teiId, value } = trackedEntity; if (!value) return null; const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - if (isPreviousValue) { + if (!latestValue) { return name; } @@ -75,13 +73,13 @@ const buildTEAUrlByElementType: {| const buildDataElementUrlByElementType: {| [string]: Function, |} = { - [dataElementTypes.FILE_RESOURCE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, isPreviousValue }: SubValuesDataElementProps) => { + [dataElementTypes.FILE_RESOURCE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, latestValue }: SubValuesDataElementProps) => { const { id: dataElementId, value } = dataElement; if (!value) return null; const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - if (isPreviousValue) { + if (!latestValue) { return name; } @@ -91,13 +89,13 @@ const buildDataElementUrlByElementType: {| url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/file`, }; }, - [dataElementTypes.IMAGE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, isPreviousValue }: SubValuesDataElementProps) => { + [dataElementTypes.IMAGE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, latestValue }: SubValuesDataElementProps) => { const { id: dataElementId, value } = dataElement; if (!value) return null; const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - if (isPreviousValue) { + if (!latestValue) { return name; } From 05a60591333d15def95737c5d8633380f2b59266 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Sun, 13 Oct 2024 21:11:13 +0200 Subject: [PATCH 09/26] fix: revert change --- .../WidgetsChangelog/common/hooks/useChangelogData.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index 7ffa840364..2e35791ac3 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -80,7 +80,9 @@ export const useChangelogData = ({ page, pageSize, program: programId, - order: sortDirection === DEFAULT_SORT_DIRECTION ? undefined : `createdAt:${sortDirection}`, + ...{ + order: sortDirection === DEFAULT_SORT_DIRECTION ? undefined : `createdAt:${sortDirection}`, + }, }, }, { From 6cfeb662637c95bc4d6d460af35c307efdc15eb5 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 14 Oct 2024 19:54:02 +0200 Subject: [PATCH 10/26] fix: update islatestvalue to check for fieldid --- .../common/hooks/useChangelogData.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index 2e35791ac3..56027871d4 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -94,10 +94,15 @@ export const useChangelogData = ({ const fetchRecords = async () => { if (!data) return; - const mostRecentCreatedAt = data.changeLogs.reduce( - (latest, record) => (moment(record.createdAt).isAfter(latest) ? record.createdAt : latest), - data.changeLogs[0]?.createdAt, - ); + const mostRecentCreatedAtByFieldId = data.changeLogs.reduce((acc, record) => { + const elementKey = Object.keys(record.change)[0]; + const fieldId = record.change[elementKey]?.dataElement ?? record.change[elementKey]?.attribute; + + if (!acc[fieldId] || moment(record.createdAt).isAfter(acc[fieldId])) { + acc[fieldId] = record.createdAt; + } + return acc; + }, {}); const fetchedRecords = await Promise.all( data.changeLogs.map(async (changelog) => { @@ -119,9 +124,10 @@ export const useChangelogData = ({ } const getSubValue = - subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; + subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; - const isLatestValue = moment(createdAt).isSameOrAfter(mostRecentCreatedAt); + const isLatestValue = + moment(createdAt).isSameOrAfter(mostRecentCreatedAtByFieldId[fieldId]); const getValue = async (value, latestValue) => { if (!getSubValue) { From eceb19e27b0fda2d519b989e17e38ba48aab4f3d Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 18 Oct 2024 09:12:36 +0200 Subject: [PATCH 11/26] feat: temp --- i18n/en.pot | 7 ++--- .../common/hooks/useChangelogData.js | 9 ++++-- .../utils/getSubValueForChangelogData.js | 29 ++++++++++++------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9b56b6db68..2e29c5c4af 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-10-10T14:29:59.249Z\n" -"PO-Revision-Date: 2024-10-10T14:29:59.249Z\n" +"POT-Creation-Date: 2024-10-16T13:43:25.928Z\n" +"PO-Revision-Date: 2024-10-16T13:43:25.928Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1071,9 +1071,6 @@ msgstr "Create new event" msgid "Search for a {{trackedEntityName}} in {{programName}}" msgstr "Search for a {{trackedEntityName}} in {{programName}}" -msgid "Back to list" -msgstr "Back to list" - msgid "No tracked entity types available" msgstr "No tracked entity types available" diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js index 56027871d4..2efae8c6e3 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useChangelogData.js @@ -4,8 +4,9 @@ import moment from 'moment'; import { v4 as uuid } from 'uuid'; import log from 'loglevel'; import { useTimeZoneConversion, useConfig, useDataEngine } from '@dhis2/app-runtime'; -import { errorCreator, buildUrl } from 'capture-core-utils'; +import { errorCreator, buildUrl, pipe } from 'capture-core-utils'; import { useApiDataQuery } from '../../../../utils/reactQueryHelpers'; +import { dataElementTypes } from '../../../../metaData'; import { CHANGELOG_ENTITY_TYPES, QUERY_KEYS_BY_ENTITY_TYPE, @@ -16,7 +17,7 @@ import type { ItemDefinitions, SortDirection, } from '../Changelog/Changelog.types'; -import { convertServerToClient } from '../../../../converters'; +import { convertServerToClient, convertClientToView } from '../../../../converters'; import { convert } from '../../../../converters/clientToList'; import { RECORD_TYPE, @@ -24,6 +25,8 @@ import { } from '../utils/getSubValueForChangelogData'; import { makeQuerySingleResource } from '../../../../utils/api'; +const convertFn = pipe(convertServerToClient, convertClientToView); + type Props = { entityId: string, programId?: string, @@ -168,7 +171,7 @@ export const useChangelogData = ({ return { reactKey: uuid(), - date: moment(fromServerDate(createdAt)).format('YYYY-MM-DD HH:mm'), + date: convertFn(fromServerDate(createdAt), dataElementTypes.DATETIME), user: `${firstName} ${surname} (${username})`, dataItemId: fieldId, changeType: type, diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js index 6590f888a9..57e89864a2 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js @@ -1,4 +1,6 @@ // @flow +import log from 'loglevel'; +import { errorCreator } from 'capture-core-utils'; import { dataElementTypes } from '../../../../metaData'; import type { QuerySingleResource } from '../../../../utils/api'; @@ -32,17 +34,24 @@ const buildTEAUrlByElementType: {| }: SubValueTEAProps) => { const { teiId, value } = trackedEntity; if (!value) return null; - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - - if (!latestValue) { - return name; + try { + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + if (!latestValue) { + return name; + } + + return { + id, + name, + url: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/file?program=${programId}`, + }; + } catch (error) { + log.error( + errorCreator('Error fetching file resource')({ error }), + ); + return null; } - - return { - id, - name, - url: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/file?program=${programId}`, - }; }, [dataElementTypes.IMAGE]: async ({ trackedEntity, From 8389805dbbd62307b6ebc4b8ca72fc33ddade756 Mon Sep 17 00:00:00 2001 From: henrikmv Date: Tue, 22 Oct 2024 15:44:40 +0200 Subject: [PATCH 12/26] fix: caching --- .../common/Changelog/Changelog.container.js | 27 ++- .../WidgetsChangelog/common/hooks/index.js | 1 + .../common/hooks/useChangelogData.js | 157 +-------------- .../common/hooks/useClientDataValues.js | 188 ++++++++++++++++++ 4 files changed, 216 insertions(+), 157 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useClientDataValues.js diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/Changelog/Changelog.container.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/Changelog/Changelog.container.js index 00754d95f1..d5c4a7be01 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/Changelog/Changelog.container.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/Changelog/Changelog.container.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import { Modal } from '@dhis2/ui'; -import { useChangelogData } from '../hooks'; +import { useChangelogData, useClientDataValues } from '../hooks'; import { ChangelogComponent } from './Changelog.component'; import { CHANGELOG_ENTITY_TYPES } from './index'; import { LoadingMaskElementCenter } from '../../../LoadingMasks'; @@ -14,7 +14,7 @@ type Props = { close: () => void, dataItemDefinitions: ItemDefinitions, programId?: string, -} +}; export const Changelog = ({ entityId, @@ -25,9 +25,11 @@ export const Changelog = ({ dataItemDefinitions, }: Props) => { const { - records, + rawRecords, pager, - isLoading, + isLoading: isChangelogLoading, + page, + pageSize, setPage, setPageSize, sortDirection, @@ -36,10 +38,23 @@ export const Changelog = ({ entityId, entityType, programId, + }); + + const { + processedRecords, + isLoading: isProcessingLoading, + } = useClientDataValues({ + rawRecords, dataItemDefinitions, + entityId, + entityType, + programId, + sortDirection, + page, + pageSize, }); - if (isLoading) { + if (isChangelogLoading || isProcessingLoading) { return ( @@ -51,7 +66,7 @@ export const Changelog = ({ , - dataItemDefinitions: ItemDefinitions, }; const DEFAULT_PAGE_SIZE = 10; const DEFAULT_SORT_DIRECTION = 'default'; -const getMetadataItemDefinition = ( - elementKey: string, - change: Change, - dataItemDefinitions: ItemDefinitions, -) => { - const { dataElement, attribute } = change; - const fieldId = dataElement ?? attribute; - const metadataElement = fieldId ? dataItemDefinitions[fieldId] : dataItemDefinitions[elementKey]; - - return { metadataElement, fieldId }; -}; - export const useChangelogData = ({ entityId, entityType, programId, - dataItemDefinitions, }: Props) => { - const dataEngine = useDataEngine(); - const { baseUrl, apiVersion } = useConfig(); - const { fromServerDate } = useTimeZoneConversion(); - const absoluteApiPath = buildUrl(baseUrl, `api/${apiVersion}`); - + const [sortDirection, setSortDirection] = useState(DEFAULT_SORT_DIRECTION); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - const [sortDirection, setSortDirection] = useState(DEFAULT_SORT_DIRECTION); - const [records, setRecords] = useState>(undefined); - - const querySingleResource = useMemo( - () => makeQuerySingleResource(dataEngine.query.bind(dataEngine)), - [dataEngine], - ); const handleChangePageSize = (newPageSize: number) => { setPage(1); @@ -76,7 +33,7 @@ export const useChangelogData = ({ }; const { data, isLoading, isError } = useApiDataQuery( - ['changelog', entityType, entityId, { sortDirection, page, pageSize, programId }], + ['changelog', entityType, entityId, 'rawData', { sortDirection, page, pageSize, programId }], { resource: `tracker/${QUERY_KEYS_BY_ENTITY_TYPE[entityType]}/${entityId}/changeLogs`, params: { @@ -93,117 +50,15 @@ export const useChangelogData = ({ }, ); - useEffect(() => { - const fetchRecords = async () => { - if (!data) return; - - const mostRecentCreatedAtByFieldId = data.changeLogs.reduce((acc, record) => { - const elementKey = Object.keys(record.change)[0]; - const fieldId = record.change[elementKey]?.dataElement ?? record.change[elementKey]?.attribute; - - if (!acc[fieldId] || moment(record.createdAt).isAfter(acc[fieldId])) { - acc[fieldId] = record.createdAt; - } - return acc; - }, {}); - - const fetchedRecords = await Promise.all( - data.changeLogs.map(async (changelog) => { - const { change: apiChange, createdAt, createdBy, type } = changelog; - const elementKey = Object.keys(apiChange)[0]; - const change = apiChange[elementKey]; - - const { metadataElement, fieldId } = getMetadataItemDefinition( - elementKey, - change, - dataItemDefinitions, - ); - - if (!metadataElement) { - log.error( - errorCreator('Could not find metadata for element')({ ...changelog }), - ); - return null; - } - - const getSubValue = - subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; - - const isLatestValue = - moment(createdAt).isSameOrAfter(mostRecentCreatedAtByFieldId[fieldId]); - - const getValue = async (value, latestValue) => { - if (!getSubValue) { - return convertServerToClient(value, metadataElement.type); - } - if (entityType === RECORD_TYPE.trackedEntity) { - return getSubValue({ - trackedEntity: { teiId: entityId, value }, - programId, - attributeId: fieldId, - absoluteApiPath, - querySingleResource, - latestValue, - }); - } - if (entityType === RECORD_TYPE.event) { - return getSubValue({ - dataElement: { id: fieldId, value }, - eventId: entityId, - absoluteApiPath, - querySingleResource, - latestValue, - }); - } - return null; - }; - - const [previousValueRaw, currentValueRaw] = await Promise.all([ - change.previousValue ? getValue(change.previousValue, false) : null, - getValue(change.currentValue, isLatestValue), - ]); - - const { firstName, surname, username } = createdBy; - const { options } = metadataElement; - - const previousValue = convert(previousValueRaw, metadataElement.type, options); - const currentValue = convert(currentValueRaw, metadataElement.type, options); - - return { - reactKey: uuid(), - date: convertFn(fromServerDate(createdAt), dataElementTypes.DATETIME), - user: `${firstName} ${surname} (${username})`, - dataItemId: fieldId, - changeType: type, - dataItemLabel: metadataElement.name, - previousValue, - currentValue, - }; - }), - ); - - setRecords(fetchedRecords.filter(Boolean)); - }; - - fetchRecords(); - }, [ - data, - dataItemDefinitions, - fromServerDate, - entityId, - entityType, - programId, - absoluteApiPath, - querySingleResource, - ]); - return { - records, + rawRecords: data, pager: data?.pager, setPage, setPageSize: handleChangePageSize, sortDirection, setSortDirection, + page, + pageSize, isLoading, isError, }; diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useClientDataValues.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useClientDataValues.js new file mode 100644 index 0000000000..09fce9ea57 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useClientDataValues.js @@ -0,0 +1,188 @@ +// @flow +import { useMemo } from 'react'; +import moment from 'moment'; +import { v4 as uuid } from 'uuid'; +import log from 'loglevel'; +import { useTimeZoneConversion, useConfig, useDataEngine } from '@dhis2/app-runtime'; +import { useQuery } from 'react-query'; +import { errorCreator, buildUrl, pipe } from 'capture-core-utils'; +import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers'; +import { dataElementTypes } from '../../../../metaData'; +import { CHANGELOG_ENTITY_TYPES } from '../Changelog/Changelog.constants'; +import type { Change, ItemDefinitions, SortDirection } from '../Changelog/Changelog.types'; +import { convertServerToClient, convertClientToView } from '../../../../converters'; +import { convert } from '../../../../converters/clientToList'; +import { RECORD_TYPE, subValueGetterByElementType } from '../utils/getSubValueForChangelogData'; +import { makeQuerySingleResource } from '../../../../utils/api'; + +const convertFn = pipe(convertServerToClient, convertClientToView); + +type Props = { + rawRecords: Object, + dataItemDefinitions: ItemDefinitions, + entityId: string, + entityType: $Values, + programId?: string, + sortDirection: SortDirection, + page: number, + pageSize: number, +}; + +const fetchFormattedValues = async ({ + rawRecords, + dataItemDefinitions, + entityId, + entityType, + programId, + absoluteApiPath, + querySingleResource, + fromServerDate, +}) => { + if (!rawRecords) return []; + + const mostRecentCreatedAtByFieldId = rawRecords.changeLogs.reduce((acc, record) => { + const elementKey = Object.keys(record.change)[0]; + const fieldId = record.change[elementKey]?.dataElement ?? record.change[elementKey]?.attribute; + + if (!acc[fieldId] || moment(record.createdAt).isAfter(acc[fieldId])) { + acc[fieldId] = record.createdAt; + } + return acc; + }, {}); + + const getMetadataItemDefinition = ( + elementKey: string, + change: Change, + dataItem: ItemDefinitions, + ) => { + const { dataElement, attribute } = change; + const fieldId = dataElement ?? attribute; + const metadataElement = fieldId ? dataItem[fieldId] : dataItem[elementKey]; + return { metadataElement, fieldId }; + }; + + const fetchedRecords = await Promise.all( + rawRecords.changeLogs.map(async (changelog) => { + const { change: apiChange, createdAt, createdBy, type } = changelog; + const elementKey = Object.keys(apiChange)[0]; + const change = apiChange[elementKey]; + + const { metadataElement, fieldId } = getMetadataItemDefinition( + elementKey, + change, + dataItemDefinitions, + ); + + if (!metadataElement) { + log.error( + errorCreator('Could not find metadata for element')({ ...changelog }), + ); + return null; + } + + const getSubValue = + subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; + + const isLatestValue = + moment(createdAt).isSameOrAfter(mostRecentCreatedAtByFieldId[fieldId]); + + const getValue = async (value, latestValue) => { + if (!getSubValue) { + return convertServerToClient(value, metadataElement.type); + } + if (entityType === RECORD_TYPE.trackedEntity) { + return getSubValue({ + trackedEntity: { teiId: entityId, value }, + programId, + attributeId: fieldId, + absoluteApiPath, + querySingleResource, + latestValue, + }); + } + if (entityType === RECORD_TYPE.event) { + return getSubValue({ + dataElement: { id: fieldId, value }, + eventId: entityId, + absoluteApiPath, + querySingleResource, + latestValue, + }); + } + return null; + }; + + const [previousValueRaw, currentValueRaw] = await Promise.all([ + change.previousValue ? getValue(change.previousValue, false) : null, + getValue(change.currentValue, isLatestValue), + ]); + + const { firstName, surname, username } = createdBy; + const { options } = metadataElement; + + const previousValue = convert(previousValueRaw, metadataElement.type, options); + const currentValue = convert(currentValueRaw, metadataElement.type, options); + + return { + reactKey: uuid(), + date: convertFn(fromServerDate(createdAt), dataElementTypes.DATETIME), + user: `${firstName} ${surname} (${username})`, + dataItemId: fieldId, + changeType: type, + dataItemLabel: metadataElement.name, + previousValue, + currentValue, + }; + }), + ); + + return fetchedRecords.filter(Boolean); +}; + +export const useClientDataValues = ({ + rawRecords, + dataItemDefinitions, + entityId, + entityType, + programId, + sortDirection, + page, + pageSize, +}: Props) => { + const dataEngine = useDataEngine(); + const { baseUrl, apiVersion } = useConfig(); + const { fromServerDate } = useTimeZoneConversion(); + const absoluteApiPath = buildUrl(baseUrl, `api/${apiVersion}`); + + const querySingleResource = useMemo( + () => makeQuerySingleResource(dataEngine.query.bind(dataEngine)), + [dataEngine], + ); + + const queryKey = [ReactQueryAppNamespace, 'changelog', entityType, entityId, 'formattedData', { sortDirection, page, pageSize, programId }]; + + const { data: processedRecords, isError, isLoading } = useQuery( + queryKey, + () => fetchFormattedValues({ + rawRecords, + dataItemDefinitions, + entityId, + entityType, + programId, + absoluteApiPath, + querySingleResource, + fromServerDate, + }), + { + enabled: !!rawRecords && !!dataItemDefinitions && !!entityId && !!entityType, + staleTime: Infinity, + cacheTime: Infinity, + }, + ); + + return { + processedRecords, + isError, + isLoading, + }; +}; From 57810353c33e8ab18a8fd1add8407dec6e14207e Mon Sep 17 00:00:00 2001 From: henrikmv Date: Wed, 23 Oct 2024 08:53:23 +0200 Subject: [PATCH 13/26] fix: ensure text utilizes space without overflow --- .../ChangelogCells/ChangelogValueCell.js | 31 +++++++------------ .../ChangelogTable/ChangelogTableRow.js | 4 +-- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogCells/ChangelogValueCell.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogCells/ChangelogValueCell.js index 246c7c2651..4d6bd41b3e 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogCells/ChangelogValueCell.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogCells/ChangelogValueCell.js @@ -14,43 +14,36 @@ type Props = { container: string, previousValue: string, currentValue: string, - updatePreviousValue: string, - updateCurrentValue: string, - updateArrow: string, + arrow: string, } } const styles = { container: { - alignItems: 'center', display: 'flex', + flexDirection: 'row', + alignItems: 'center', + whiteSpace: 'normal', + height: '100%', }, previousValue: { color: colors.grey700, + wordBreak: 'break-word', }, currentValue: { color: colors.grey900, + wordBreak: 'break-word', }, - updatePreviousValue: { - color: colors.grey700, - maxWidth: '45%', - }, - updateCurrentValue: { - color: colors.grey900, - maxWidth: '45%', - }, - updateArrow: { - display: 'inline-flex', - alignItems: 'center', - margin: `${spacers.dp4}`, + arrow: { + margin: `0 ${spacers.dp4}`, }, }; const Updated = ({ previousValue, currentValue, classes }) => (
- {previousValue} - - {currentValue} +
{previousValue}
+
+
{currentValue}
); diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogTableRow.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogTableRow.js index e93571cbf2..3169194fc1 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogTableRow.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/ChangelogTable/ChangelogTableRow.js @@ -13,13 +13,11 @@ type Props = { }, classes: { dataItemColumn: string, - valueColumn: string, }, }; const styles = { dataItemColumn: { wordWrap: 'break-word', hyphens: 'auto' }, - valueColumn: { wordWrap: 'break-word' }, }; const ChangelogTableRowPlain = ({ record, classes }: Props) => ( @@ -28,7 +26,7 @@ const ChangelogTableRowPlain = ({ record, classes }: Props) => ( {record.user} {record.dataItemLabel} - + ); From 56748b7ed456ad15bccb6ab08ceae26889cd861f Mon Sep 17 00:00:00 2001 From: henrikmv Date: Fri, 25 Oct 2024 16:24:43 +0200 Subject: [PATCH 14/26] fix: add try catch to all query calls --- .../utils/getSubValueForChangelogData.js | 80 ++++++++++++------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js index 57e89864a2..24818484a3 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js @@ -63,22 +63,28 @@ const buildTEAUrlByElementType: {| }: SubValueTEAProps) => { const { teiId, value } = trackedEntity; if (!value) return null; - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + try { + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - if (!latestValue) { - return name; - } + if (!latestValue) { + return name; + } - return { - id, - name, - url: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}`, - previewUrl: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}&dimension=small`, - }; + return { + id, + name, + url: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}`, + previewUrl: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}&dimension=small`, + }; + } catch (error) { + log.error( + errorCreator('Error fetching image resource')({ error }), + ); + return null; + } }, }; - const buildDataElementUrlByElementType: {| [string]: Function, |} = { @@ -86,34 +92,48 @@ const buildDataElementUrlByElementType: {| const { id: dataElementId, value } = dataElement; if (!value) return null; - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + try { + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - if (!latestValue) { - return name; - } + if (!latestValue) { + return name; + } - return { - id, - name, - url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/file`, - }; + return { + id, + name, + url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/file`, + }; + } catch (error) { + log.error( + errorCreator('Error fetching file resource')({ error }), + ); + return null; + } }, [dataElementTypes.IMAGE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, latestValue }: SubValuesDataElementProps) => { const { id: dataElementId, value } = dataElement; if (!value) return null; - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + try { + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - if (!latestValue) { - return name; - } + if (!latestValue) { + return name; + } - return { - id, - name, - url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image`, - previewUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image?dimension=small`, - }; + return { + id, + name, + url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image`, + previewUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${dataElementId}/image?dimension=small`, + }; + } catch (error) { + log.error( + errorCreator('Error fetching image resource')({ error }), + ); + return null; + } }, }; From 877d48933879eadde14f74bf2eb480ffa6b3f42a Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 28 Oct 2024 09:18:30 +0100 Subject: [PATCH 15/26] fix: use storagestatus to find latest value --- .../utils/getSubValueForChangelogData.js | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js index 24818484a3..bd81237767 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js @@ -10,7 +10,6 @@ type SubValueTEAProps = { programId: string, absoluteApiPath: string, querySingleResource: QuerySingleResource, - latestValue?: boolean, }; type SubValuesDataElementProps = { @@ -18,7 +17,6 @@ type SubValuesDataElementProps = { querySingleResource: QuerySingleResource, eventId: string, absoluteApiPath: string, - latestValue?: boolean, }; const buildTEAUrlByElementType: {| @@ -30,16 +28,13 @@ const buildTEAUrlByElementType: {| programId, absoluteApiPath, querySingleResource, - latestValue, }: SubValueTEAProps) => { const { teiId, value } = trackedEntity; if (!value) return null; try { - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + const { id, displayName: name, storageStatus } = await querySingleResource({ resource: `fileResources/${value}?fields=id,name,storageStatus` }); - if (!latestValue) { - return name; - } + if (storageStatus === 'NONE') { return 'File'; } return { id, @@ -58,17 +53,14 @@ const buildTEAUrlByElementType: {| attributeId, programId, absoluteApiPath, - latestValue, querySingleResource, }: SubValueTEAProps) => { const { teiId, value } = trackedEntity; if (!value) return null; try { - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + const { id, displayName: name, storageStatus } = await querySingleResource({ resource: `fileResources/${value}?fields=id,name,storageStatus` }); - if (!latestValue) { - return name; - } + if (storageStatus === 'NONE') { return 'Image'; } return { id, @@ -88,16 +80,14 @@ const buildTEAUrlByElementType: {| const buildDataElementUrlByElementType: {| [string]: Function, |} = { - [dataElementTypes.FILE_RESOURCE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, latestValue }: SubValuesDataElementProps) => { + [dataElementTypes.FILE_RESOURCE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath }: SubValuesDataElementProps) => { const { id: dataElementId, value } = dataElement; if (!value) return null; try { - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + const { id, displayName: name, storageStatus } = await querySingleResource({ resource: `fileResources/${value}?fields=id,name,storageStatus` }); - if (!latestValue) { - return name; - } + if (storageStatus === 'NONE') { return 'File'; } return { id, @@ -111,16 +101,13 @@ const buildDataElementUrlByElementType: {| return null; } }, - [dataElementTypes.IMAGE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, latestValue }: SubValuesDataElementProps) => { + [dataElementTypes.IMAGE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath }: SubValuesDataElementProps) => { const { id: dataElementId, value } = dataElement; if (!value) return null; try { - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); - - if (!latestValue) { - return name; - } + const { id, displayName: name, storageStatus } = await querySingleResource({ resource: `fileResources/${value}?fields=id,name,storageStatus` }); + if (storageStatus === 'NONE') { return 'Image'; } return { id, From d8a92bf7498f3e7e6a7ff7b6068a5f50f314f9ed Mon Sep 17 00:00:00 2001 From: henrikmv Date: Mon, 28 Oct 2024 09:24:21 +0100 Subject: [PATCH 16/26] fix: string improvement --- i18n/en.pot | 8 ++++---- .../components/WidgetAssignee/DisplayMode.component.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 94597518b3..ad17e6bb5a 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-10-16T13:43:25.928Z\n" -"PO-Revision-Date: 2024-10-16T13:43:25.928Z\n" +"POT-Creation-Date: 2024-10-28T08:23:06.336Z\n" +"PO-Revision-Date: 2024-10-28T08:23:06.336Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1074,8 +1074,8 @@ msgstr "No tracked entity types available" msgid "Assigned to" msgstr "Assigned to" -msgid "You don't have access to edit this assignee" -msgstr "You don't have access to edit this assignee" +msgid "You don't have access to edit the assigned user" +msgstr "You don't have access to edit the assigned user" msgid "Edit" msgstr "Edit" diff --git a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js index e0b2ebb2dd..4cb6ed894e 100644 --- a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js +++ b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js @@ -44,7 +44,7 @@ const DisplayModePlain = ({ assignee, onEdit, writeAccess, avatarId, classes }: {assignee.name}