diff --git a/i18n/en.pot b/i18n/en.pot index 9abb22fa99..11c24b0478 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-11-04T18:45:47.626Z\n" -"PO-Revision-Date: 2024-11-04T18:45:47.626Z\n" +"POT-Creation-Date: 2024-11-07T11:57:59.094Z\n" +"PO-Revision-Date: 2024-11-07T11:57:59.094Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1547,6 +1547,12 @@ msgstr "Change" msgid "Value" msgstr "Value" +msgid "File" +msgstr "File" + +msgid "Image" +msgstr "Image" + msgid "New {{trackedEntityTypeName}} relationship" msgstr "New {{trackedEntityTypeName}} relationship" diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js index 70ee627719..30af8fca67 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js @@ -56,6 +56,7 @@ const getStyles = () => ({ type Props = { showEditEvent: ?boolean, eventId: string, + eventData: Object, onOpenEditEvent: (orgUnit: Object, programCategory: ?ProgramCategory) => void, programStage: ProgramStage, eventAccess: { read: boolean, write: boolean }, @@ -76,6 +77,7 @@ const EventDetailsSectionPlain = (props: Props) => { const { classes, eventId, + eventData, onOpenEditEvent, showEditEvent, programStage, @@ -200,6 +202,7 @@ const EventDetailsSectionPlain = (props: Props) => { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.container.js b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.container.js index e57972d6bc..46c97da75d 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.container.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.container.js @@ -10,6 +10,7 @@ import type { ProgramCategory } from '../../../WidgetEventSchedule/CategoryOptio const mapStateToProps = (state: ReduxState) => ({ showEditEvent: state.viewEventPage.eventDetailsSection && state.viewEventPage.eventDetailsSection.showEditEvent, eventId: state.viewEventPage.eventId, + eventData: state.viewEventPage.loadedValues?.eventContainer?.values || {}, programId: state.currentSelections.programId, }); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.component.js b/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.component.js index d9f20c2e66..e7041bdd74 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.component.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.component.js @@ -5,7 +5,7 @@ import { dataElementTypes } from '../../../metaData'; import type { Props } from './EventChangelogWrapper.types'; import { WidgetEventChangelog } from '../../WidgetsChangelog'; -export const EventChangelogWrapper = ({ formFoundation, eventId, ...passOnProps }: Props) => { +export const EventChangelogWrapper = ({ formFoundation, eventId, eventData, ...passOnProps }: Props) => { const dataItemDefinitions = useMemo(() => { const elements = formFoundation.getElements(); const contextLabels = formFoundation.getLabels(); @@ -52,6 +52,7 @@ export const EventChangelogWrapper = ({ formFoundation, eventId, ...passOnProps ); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.types.js b/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.types.js index 274891017e..58cb1d981f 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.types.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EventChangelogWrapper/EventChangelogWrapper.types.js @@ -10,4 +10,5 @@ type PassOnProps = {| export type Props = { ...PassOnProps, formFoundation: RenderFoundation, + eventData: Object, }; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js index 075855e6cd..2a6cb8be80 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js @@ -236,6 +236,7 @@ export const WidgetEventEditPlain = ({ isOpen setIsOpen={setChangeLogIsOpen} eventId={loadedValues.eventContainer.id} + eventData={loadedValues.eventContainer.values} formFoundation={formFoundation} /> )} 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..faf83dfe91 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 @@ -9,6 +9,7 @@ import { TrackedEntityChangelogWrapper } from './TrackedEntityChangelogWrapper'; export const OverflowMenuComponent = ({ trackedEntity, + trackedEntityData, trackedEntityTypeName, canWriteData, canCascadeDeleteTei, @@ -68,6 +69,7 @@ export const OverflowMenuComponent = ({ programAPI={programAPI} isOpen={changelogIsOpen} setIsOpen={setChangelogIsOpen} + trackedEntityData={trackedEntityData} /> )} diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.js b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.js index 031f7fd462..38d17ad0c0 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.js +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.container.js @@ -8,6 +8,7 @@ export const OverflowMenu = ({ trackedEntityTypeName, canWriteData, trackedEntity, + trackedEntityData, onDeleteSuccess, displayChangelog, teiId, @@ -21,6 +22,7 @@ export const OverflowMenu = ({ canWriteData={canWriteData} canCascadeDeleteTei={hasAuthority} trackedEntity={trackedEntity} + trackedEntityData={trackedEntityData} onDeleteSuccess={onDeleteSuccess} displayChangelog={displayChangelog} teiId={teiId} diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.types.js b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.types.js index ad3cc687d4..84ea57e86a 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.types.js +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.types.js @@ -3,6 +3,7 @@ export type Props = {| trackedEntity: { trackedEntity: string }, trackedEntityTypeName: string, + trackedEntityData: Object, canWriteData: boolean, onDeleteSuccess?: () => void, displayChangelog: boolean, @@ -13,6 +14,7 @@ export type Props = {| export type PlainProps = {| trackedEntity: { trackedEntity: string }, trackedEntityTypeName: string, + trackedEntityData: Object, canWriteData: boolean, canCascadeDeleteTei: boolean, onDeleteSuccess?: () => void, diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.component.js b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.component.js index 1dc3172af6..27db89634d 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.component.js +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.component.js @@ -5,9 +5,14 @@ import { useFormFoundation } from '../../DataEntry/hooks'; import { WidgetTrackedEntityChangelog } from '../../../WidgetsChangelog'; import type { Props } from './TrackedEntityChangelogWrapper.types'; -export const TrackedEntityChangelogWrapper = ({ programAPI, teiId, setIsOpen, ...passOnProps }: Props) => { +export const TrackedEntityChangelogWrapper = ({ programAPI, teiId, setIsOpen, trackedEntityData, ...passOnProps }: Props) => { const formFoundation: RenderFoundation = useFormFoundation(programAPI); + const transformedTrackedEntityData = trackedEntityData.reduce((acc, item) => { + acc[item.attribute] = item.value; + return acc; + }, {}); + const dataItemDefinitions = useMemo(() => { if (!Object.keys(formFoundation)?.length) return {}; const elements = formFoundation.getElements(); @@ -58,6 +63,7 @@ export const TrackedEntityChangelogWrapper = ({ programAPI, teiId, setIsOpen, .. close={() => setIsOpen(false)} programId={programAPI.id} dataItemDefinitions={dataItemDefinitions} + trackedEntityData={transformedTrackedEntityData} /> ); }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.types.js b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.types.js index e0bb62461f..c2c5d726f4 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.types.js +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/TrackedEntityChangelogWrapper/TrackedEntityChangelogWrapper.types.js @@ -5,6 +5,7 @@ type PassOnProps = {| teiId: string, isOpen: boolean, setIsOpen: (boolean | boolean => boolean) => void, + trackedEntityData: Object, |} export type Props = { diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js index 0204ac19a7..f63800dd43 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js @@ -162,6 +162,7 @@ const WidgetProfilePlain = ({ trackedEntity={trackedEntity} onDeleteSuccess={onDeleteSuccess} displayChangelog={displayChangelog} + trackedEntityData={clientAttributesWithSubvalues} teiId={teiId} programAPI={program} /> diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/WidgetEventChangelog/WidgetEventChangelog.js b/src/core_modules/capture-core/components/WidgetsChangelog/WidgetEventChangelog/WidgetEventChangelog.js index 86137d295e..aabb240e4a 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/WidgetEventChangelog/WidgetEventChangelog.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/WidgetEventChangelog/WidgetEventChangelog.js @@ -5,6 +5,7 @@ import { Changelog, CHANGELOG_ENTITY_TYPES } from '../common/Changelog'; type Props = { eventId: string, + eventData: Object, dataItemDefinitions: ItemDefinitions, isOpen: boolean, setIsOpen: (boolean | boolean => boolean) => void, @@ -12,6 +13,7 @@ type Props = { export const WidgetEventChangelog = ({ eventId, + eventData, setIsOpen, ...passOnProps }: Props) => ( @@ -19,6 +21,7 @@ export const WidgetEventChangelog = ({ {...passOnProps} close={() => setIsOpen(false)} entityId={eventId} + entityData={eventData} entityType={CHANGELOG_ENTITY_TYPES.EVENT} /> ); diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/WidgetTrackedEntityChangelog/WidgetTrackedEntityChangelog.js b/src/core_modules/capture-core/components/WidgetsChangelog/WidgetTrackedEntityChangelog/WidgetTrackedEntityChangelog.js index 86e941142c..a28370ea35 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/WidgetTrackedEntityChangelog/WidgetTrackedEntityChangelog.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/WidgetTrackedEntityChangelog/WidgetTrackedEntityChangelog.js @@ -9,17 +9,20 @@ type Props = { dataItemDefinitions: ItemDefinitions, isOpen: boolean, close: () => void, + trackedEntityData: Object, } export const WidgetTrackedEntityChangelog = ({ teiId, programId, close, + trackedEntityData, ...passOnProps }: Props) => ( , isOpen: boolean, close: () => void, dataItemDefinitions: ItemDefinitions, programId?: string, -} +}; export const Changelog = ({ entityId, + entityData, entityType, programId, isOpen, @@ -25,9 +27,11 @@ export const Changelog = ({ dataItemDefinitions, }: Props) => { const { - records, + rawRecords, pager, - isLoading, + isLoading: isChangelogLoading, + page, + pageSize, setPage, setPageSize, sortDirection, @@ -36,10 +40,24 @@ export const Changelog = ({ entityId, entityType, programId, + }); + + const { + processedRecords, + isLoading: isProcessingLoading, + } = useListDataValues({ + rawRecords, dataItemDefinitions, + entityId, + entityData, + entityType, + programId, + sortDirection, + page, + pageSize, }); - if (isLoading) { + if (isChangelogLoading || isProcessingLoading) { return ( @@ -51,7 +69,7 @@ export const Changelog = ({ (
- {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} - + ); diff --git a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/index.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/index.js index 1cc9b74cc8..7d2911cd6d 100644 --- a/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/index.js +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/index.js @@ -1,3 +1,4 @@ // @flow export { useChangelogData } from './useChangelogData'; +export { useListDataValues } from './useListDataValues'; 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..f9562cf4ac 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,48 +1,31 @@ // @flow -import moment from 'moment'; -import { v4 as uuid } from 'uuid'; -import log from 'loglevel'; -import { errorCreator } from 'capture-core-utils'; -import { useMemo, useState } from 'react'; -import { useTimeZoneConversion } from '@dhis2/app-runtime'; +import { useState } from 'react'; 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 { + CHANGELOG_ENTITY_TYPES, + QUERY_KEYS_BY_ENTITY_TYPE, +} from '../Changelog/Changelog.constants'; +import type { + SortDirection, +} from '../Changelog/Changelog.types'; type Props = { entityId: string, programId?: string, entityType: $Values, - 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 [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 { fromServerDate } = useTimeZoneConversion(); const handleChangePageSize = (newPageSize: number) => { setPage(1); @@ -50,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: { @@ -67,62 +50,15 @@ export const useChangelogData = ({ }, ); - 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; - } - - const { firstName, surname, username } = createdBy; - const { options } = metadataElement; - - const previousValue = convert( - convertServerToClient(change.previousValue, metadataElement.type), - metadataElement.type, - options, - ); - - const currentValue = convert( - convertServerToClient(change.currentValue, metadataElement.type), - 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); - }, [data, dataItemDefinitions, fromServerDate]); - 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/useListDataValues.js b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useListDataValues.js new file mode 100644 index 0000000000..e94bf94a88 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/hooks/useListDataValues.js @@ -0,0 +1,177 @@ +// @flow +import { useMemo } from 'react'; +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 } from '../../../../converters'; +import { convert as convertClientToList } from '../../../../converters/clientToList'; +import { RECORD_TYPE, subValueGetterByElementType } from '../utils/getSubValueForChangelogData'; +import { makeQuerySingleResource } from '../../../../utils/api'; +import { attributeOptionsKey } from '../../../DataEntryDhis2Helpers'; + + +type Props = { + rawRecords: Object, + dataItemDefinitions: ItemDefinitions, + entityId: string, + entityData: Object, + entityType: $Values, + programId?: string, + sortDirection: SortDirection, + page: number, + pageSize: number, +}; + +const fetchFormattedValues = async ({ + rawRecords, + dataItemDefinitions, + entityId, + entityData, + entityType, + programId, + absoluteApiPath, + querySingleResource, + fromServerDate, +}) => { + if (!rawRecords) return []; + + const getMetadataItemDefinition = ( + elementKey: string, + change: Change, + ) => { + const fieldId = change.dataElement || change.attribute; + if (!fieldId) { + log.error('Could not find fieldId in change:', change); + return { metadataElement: null, fieldId: null }; + } + const metadataElement = dataItemDefinitions[fieldId]; + 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, + ); + + if (!metadataElement) { + log.error( + errorCreator('Could not find metadata for element')({ ...changelog }), + ); + return null; + } + + const getSubValue = subValueGetterByElementType[RECORD_TYPE[entityType]]?.[metadataElement.type]; + + 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 [previousValueClient, currentValueClient] = await Promise.all([ + change.previousValue ? getValue(change.previousValue, false) : null, + getValue(change.currentValue, entityData?.[change.attribute ?? change.dataElement]?.value === change.currentValue), + ]); + + const { firstName, surname, username } = createdBy; + const { options } = metadataElement; + + const previousValue = convertClientToList(previousValueClient, metadataElement.type, options); + const currentValue = convertClientToList(currentValueClient, metadataElement.type, options); + + return { + reactKey: fieldId ? `${createdAt}-${fieldId}` : attributeOptionsKey, + date: pipe(convertServerToClient, convertClientToList)(fromServerDate(createdAt), dataElementTypes.DATETIME), + user: `${firstName} ${surname} (${username})`, + changeType: type, + dataItemLabel: metadataElement.name, + previousValue, + currentValue, + }; + }), + ); + + return fetchedRecords.filter(Boolean); +}; + +export const useListDataValues = ({ + rawRecords, + dataItemDefinitions, + entityId, + entityData, + 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, + entityData, + entityType, + programId, + absoluteApiPath, + querySingleResource, + fromServerDate, + }), + { + enabled: !!rawRecords && !!dataItemDefinitions && !!entityId && !!entityType, + staleTime: Infinity, + cacheTime: Infinity, + }, + ); + + return { + processedRecords, + isError, + isLoading, + }; +}; 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..85b13724e2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetsChangelog/common/utils/getSubValueForChangelogData.js @@ -0,0 +1,148 @@ +// @flow +import log from 'loglevel'; +import i18n from '@dhis2/d2-i18n'; +import { errorCreator } from 'capture-core-utils'; +import { dataElementTypes } from '../../../../metaData'; +import type { QuerySingleResource } from '../../../../utils/api'; + +type SubValueTEAProps = { + trackedEntity: Object, + attributeId: string, + programId: string, + absoluteApiPath: string, + querySingleResource: QuerySingleResource, + latestValue?: boolean, +}; + +type SubValuesDataElementProps = { + dataElement: Object, + querySingleResource: QuerySingleResource, + eventId: string, + absoluteApiPath: string, + latestValue?: boolean, +}; + +const buildTEAUrlByElementType: {| +[string]: Function, +|} = { + [dataElementTypes.FILE_RESOURCE]: async ({ + trackedEntity, + attributeId, + programId, + absoluteApiPath, + querySingleResource, + latestValue, + }: SubValueTEAProps) => { + const { teiId, value } = trackedEntity; + if (!value) return null; + try { + if (!latestValue) { + return i18n.t('File'); + } + + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + 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; + } + }, + [dataElementTypes.IMAGE]: async ({ + trackedEntity, + attributeId, + programId, + absoluteApiPath, + latestValue, + querySingleResource, + }: SubValueTEAProps) => { + const { teiId, value } = trackedEntity; + if (!value) return null; + try { + if (!latestValue) { + return i18n.t('Image'); + } + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + 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, +|} = { + [dataElementTypes.FILE_RESOURCE]: async ({ dataElement, querySingleResource, eventId, absoluteApiPath, latestValue }: SubValuesDataElementProps) => { + const { id: dataElementId, value } = dataElement; + if (!value) return null; + + try { + if (!latestValue) { + return i18n.t('File'); + } + + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + 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; + + try { + if (!latestValue) { + return i18n.t('Image'); + } + + const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); + + 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; + } + }, +}; + +export const RECORD_TYPE = Object.freeze({ + event: 'event', + trackedEntity: 'trackedEntity', +}); + +export const subValueGetterByElementType = Object.freeze({ + [RECORD_TYPE.trackedEntity]: buildTEAUrlByElementType, + [RECORD_TYPE.event]: buildDataElementUrlByElementType, +});