diff --git a/src/data-workspace/custom-form/custom-form-indicator-cell.jsx b/src/data-workspace/custom-form/custom-form-indicator-cell.jsx
index 4d7f7e14f0..b2ad2efae5 100644
--- a/src/data-workspace/custom-form/custom-form-indicator-cell.jsx
+++ b/src/data-workspace/custom-form/custom-form-indicator-cell.jsx
@@ -16,6 +16,7 @@ export const CustomFormIndicatorCell = ({ indicatorId, metadata }) => {
numerator={numerator}
factor={factor}
decimals={decimals}
+ indicatorId={indicatorId}
/>
)
}
diff --git a/src/data-workspace/data-details-sidebar/basic-information.jsx b/src/data-workspace/data-details-sidebar/basic-information.jsx
index b1e58b61e0..7caf3c1719 100644
--- a/src/data-workspace/data-details-sidebar/basic-information.jsx
+++ b/src/data-workspace/data-details-sidebar/basic-information.jsx
@@ -26,58 +26,76 @@ const BasicInformation = ({ item }) => {
{item.categoryOptionComboName &&
` | ${item.categoryOptionComboName}`}
-
-
- {item.description && (
- -
- {i18n.t('Description: {{- description}}', {
- description: item.description,
- nsSeparator: '-:-',
- })}
-
- )}
- {item.code && (
- -
- {i18n.t('Code: {{code}}', {
- code: item.code,
- nsSeparator: '-:-',
- })}
-
- )}
- -
- {i18n.t('Data element ID: {{id}}', {
- id: item.dataElement,
- nsSeparator: '-:-',
- })}
-
- -
- {i18n.t('Category option combo ID: {{id}}', {
- id: item.categoryOptionCombo,
- nsSeparator: '-:-',
- })}
-
- -
- {item.lastUpdated && (
- }>
- {i18n.t(
- 'Last updated {{- timeAgo}} by {{- name}}',
- {
- timeAgo,
- name: item.storedBy,
- }
- )}
-
+ {item?.isIndicator ? (
+
+ {item.description && (
+ -
+ {i18n.t('Description: {{- description}}', {
+ description: item.description,
+ nsSeparator: '-:-',
+ })}
+
)}
-
+
+ ) : (
+ <>
+
+ {item.description && (
+ -
+ {i18n.t('Description: {{- description}}', {
+ description: item.description,
+ nsSeparator: '-:-',
+ })}
+
+ )}
+ {item.code && (
+ -
+ {i18n.t('Code: {{code}}', {
+ code: item.code,
+ nsSeparator: '-:-',
+ })}
+
+ )}
+ -
+ {i18n.t('Data element ID: {{id}}', {
+ id: item.dataElement,
+ nsSeparator: '-:-',
+ })}
+
+ -
+ {i18n.t('Category option combo ID: {{id}}', {
+ id: item.categoryOptionCombo,
+ nsSeparator: '-:-',
+ })}
+
+ -
+ {item.lastUpdated && (
+
+ }
+ >
+ {i18n.t(
+ 'Last updated {{- timeAgo}} by {{- name}}',
+ {
+ timeAgo,
+ name: item.storedBy,
+ }
+ )}
+
+ )}
+
- {item.followUp ? (
- -
-
- {i18n.t('Marked for follow-up')}
-
- ) : null}
-
-
+ {item.followUp ? (
+
+
+ {i18n.t('Marked for follow-up')}
+
+ ) : null}
+
+
+ >
+ )}
)
}
diff --git a/src/data-workspace/data-details-sidebar/data-details-sidebar.jsx b/src/data-workspace/data-details-sidebar/data-details-sidebar.jsx
index eedcc98247..ca397273fa 100644
--- a/src/data-workspace/data-details-sidebar/data-details-sidebar.jsx
+++ b/src/data-workspace/data-details-sidebar/data-details-sidebar.jsx
@@ -24,6 +24,14 @@ export default function DataDetailsSidebar({ hide }) {
if (!dataValue) {
return null
}
+ if (dataValue?.isIndicator) {
+ return (
+
+ {i18n.t('Details')}
+
+
+ )
+ }
return (
{i18n.t('Details')}
diff --git a/src/data-workspace/data-details-sidebar/data-details-sidebar.test.jsx b/src/data-workspace/data-details-sidebar/data-details-sidebar.test.jsx
index 8cc3f576db..057090058e 100644
--- a/src/data-workspace/data-details-sidebar/data-details-sidebar.test.jsx
+++ b/src/data-workspace/data-details-sidebar/data-details-sidebar.test.jsx
@@ -86,4 +86,53 @@ describe('DataDetailsSideBar', () => {
).toBeInTheDocument()
})
})
+
+ describe('Indicator info', () => {
+ const mockIndicatorHighlightedFieldReturn = {
+ isIndicator: true,
+ displayFormName: 'EXP Drugs Expense Ratio',
+ description: 'Ratio of drug expenses to total expenses',
+ }
+
+ beforeAll(() => {
+ useHighlightedField.mockReturnValue({
+ ...mockIndicatorHighlightedFieldReturn,
+ })
+ useUserInfo.mockImplementation(() => ({
+ data: {
+ authorities: ['ALL'],
+ },
+ }))
+ })
+
+ it('should only display the name and description', async () => {
+ const { getByText, queryByText } = renderComponent()
+
+ expect(
+ getByText(mockIndicatorHighlightedFieldReturn.displayFormName, {
+ exact: false,
+ })
+ ).toBeInTheDocument()
+ expect(
+ getByText(
+ `Description: ${mockIndicatorHighlightedFieldReturn.description}`,
+ { exact: false }
+ )
+ ).toBeInTheDocument()
+
+ expect(queryByText(/Code:/)).not.toBeInTheDocument()
+ expect(queryByText(/Data element ID:/)).not.toBeInTheDocument()
+ expect(
+ queryByText(/Category option combo ID:/)
+ ).not.toBeInTheDocument()
+ expect(queryByText(/Last updated/)).not.toBeInTheDocument()
+ expect(queryByText(/Marked for follow-up/)).not.toBeInTheDocument()
+
+ // the other sidebar sections should not be rendered for indicators
+ expect(queryByText('Comment')).not.toBeInTheDocument()
+ expect(queryByText('Min and max limits')).not.toBeInTheDocument()
+ expect(queryByText('History')).not.toBeInTheDocument()
+ expect(queryByText('Audit log')).not.toBeInTheDocument()
+ })
+ })
})
diff --git a/src/data-workspace/indicators-table-body/indicator-table-cell.jsx b/src/data-workspace/indicators-table-body/indicator-table-cell.jsx
index 27cf981f53..f7c8165855 100644
--- a/src/data-workspace/indicators-table-body/indicator-table-cell.jsx
+++ b/src/data-workspace/indicators-table-body/indicator-table-cell.jsx
@@ -1,7 +1,12 @@
import i18n from '@dhis2/d2-i18n'
import { TableCell, Tooltip, IconInfo16, IconWarning16 } from '@dhis2/ui'
+import cx from 'classnames'
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useState } from 'react'
+import {
+ useHighlightedFieldStore,
+ useComponentWillUnmount,
+} from '../../shared/index.js'
import styles from '../table-body.module.css'
import {
NONCALCULABLE_VALUE,
@@ -25,6 +30,7 @@ InvalidIndicatorWrapper.propTypes = {
export const IndicatorTableCell = ({
denominator,
factor,
+ indicatorId,
numerator,
decimals,
}) => {
@@ -39,6 +45,23 @@ export const IndicatorTableCell = ({
decimals,
})
+ const setHighlightedFieldId = useHighlightedFieldStore(
+ (state) => state.setHighlightedField
+ )
+
+ const [active, setActive] = useState(false)
+ const highlighted = useHighlightedFieldStore((state) =>
+ state.isFieldHighlighted({
+ indicatorId: indicatorId,
+ })
+ )
+
+ useComponentWillUnmount(() => {
+ if (highlighted) {
+ setHighlightedFieldId(null)
+ }
+ }, [highlighted, setHighlightedFieldId])
+
if (indicatorValue === NONCALCULABLE_VALUE) {
return (
{indicatorValue}
+
+ {
+ setActive(true)
+ setHighlightedFieldId({ indicatorId: indicatorId })
+ }}
+ onBlur={() => {
+ setActive(false)
+ }}
+ >
+ {indicatorValue}
+
+
)
}
IndicatorTableCell.propTypes = {
denominator: PropTypes.string.isRequired,
factor: PropTypes.number.isRequired,
+ indicatorId: PropTypes.string.isRequired,
numerator: PropTypes.string.isRequired,
decimals: PropTypes.number,
}
diff --git a/src/data-workspace/indicators-table-body/indicator-table-cell.test.js b/src/data-workspace/indicators-table-body/indicator-table-cell.test.js
index 4f20de8dfd..bdcdd12bc6 100644
--- a/src/data-workspace/indicators-table-body/indicator-table-cell.test.js
+++ b/src/data-workspace/indicators-table-body/indicator-table-cell.test.js
@@ -1,6 +1,8 @@
import { IconInfo16, IconWarning16 } from '@dhis2/ui'
import { fireEvent } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
import React from 'react'
+import { useHighlightedFieldStore } from '../../shared/index.js'
import { render } from '../../test-utils/index.js'
import { IndicatorTableCell } from './indicator-table-cell.jsx'
import { useIndicatorValue } from './use-indicator-value.js'
@@ -20,9 +22,16 @@ const MOCK_INDICATOR_INFO = {
factor: 1,
numerator: '#{fakeUID1}',
denominator: '#{fakeUID2}',
+ indicatorId: 'indicator1',
}
describe('IndicatorTableCell', () => {
+ const initialHighlightedFieldState = useHighlightedFieldStore.getState()
+
+ beforeEach(() => {
+ useHighlightedFieldStore.setState(initialHighlightedFieldState)
+ })
+
it('shows the value returned by useIndicatorValue if valid', async () => {
useIndicatorValue.mockReturnValue({ value: '42' })
const { findByText } = render(
@@ -74,4 +83,40 @@ describe('IndicatorTableCell', () => {
// check that the mocked value of info icon is present
expect(await findByText('WARNING_ICON')).toBeInTheDocument()
})
+
+ it('calls setHighlightedFieldId with the indicatorId of the focused cell', async () => {
+ useIndicatorValue.mockReturnValue({ value: '42' })
+ const { container, findAllByText } = render(
+ <>
+
+
+ >
+ )
+
+ await findAllByText('42')
+ const [cellA, cellB] = container.querySelectorAll('[tabindex="0"]')
+
+ expect(
+ useHighlightedFieldStore.getState().highlightedFieldId
+ ).not.toEqual({ indicatorId: 'indicator1' })
+
+ // activate the first cell by clicking it
+ await userEvent.click(cellA)
+ expect(useHighlightedFieldStore.getState().highlightedFieldId).toEqual({
+ indicatorId: 'indicator1',
+ })
+
+ // tab into the second cell
+ await userEvent.tab()
+ expect(document.activeElement).toBe(cellB)
+ expect(useHighlightedFieldStore.getState().highlightedFieldId).toEqual({
+ indicatorId: 'indicator2',
+ })
+ })
})
diff --git a/src/data-workspace/indicators-table-body/indicators-table-body.jsx b/src/data-workspace/indicators-table-body/indicators-table-body.jsx
index 3f159c73e1..d4615c8fdc 100644
--- a/src/data-workspace/indicators-table-body/indicators-table-body.jsx
+++ b/src/data-workspace/indicators-table-body/indicators-table-body.jsx
@@ -62,6 +62,7 @@ export const IndicatorsTableBody = ({
numerator={indicator.numerator}
factor={indicator.indicatorType.factor}
decimals={indicator.decimals}
+ indicatorId={indicator.id}
/>
{padColumns.map((_, i) => (
{
- if (!item || !dataElement) {
+ if (!item || !dataElementOrIndicator) {
return null
}
+ if (item.indicatorId) {
+ return { isIndicator: true, ...dataElementOrIndicator }
+ }
+
return gatherHighlightedFieldData({
metadata,
dataValue,
- dataElement,
+ dataElement: dataElementOrIndicator,
categoryOptionComboId: item.categoryOptionComboId,
})
- }, [item, dataElement, dataValue, metadata])
+ }, [item, dataElementOrIndicator, dataValue, metadata])
}
diff --git a/src/shared/metadata/selectors.js b/src/shared/metadata/selectors.js
index 1cff44a7d6..b893c2a4e2 100644
--- a/src/shared/metadata/selectors.js
+++ b/src/shared/metadata/selectors.js
@@ -40,6 +40,7 @@ export const getOptionSetById = (metadata, id) => getOptionSets(metadata)[id]
export const getDataSetById = (metadata, id) => getDataSets(metadata)?.[id]
export const getDataElementById = (metadata, id) =>
getDataElements(metadata)?.[id]
+export const getIndicatorById = (metadata, id) => getIndicators(metadata)?.[id]
export const getSectionsByDataSetId = (metadata, dataSetId) => {
const dataSetSections = getDataSetById(metadata, dataSetId)?.sections
diff --git a/src/shared/stores/highlighted-field-store.js b/src/shared/stores/highlighted-field-store.js
index 47071a9586..3fca34fe4a 100644
--- a/src/shared/stores/highlighted-field-store.js
+++ b/src/shared/stores/highlighted-field-store.js
@@ -8,17 +8,35 @@ export const useHighlightedFieldStore = create((set, get) => ({
* @param {} field
* @param {} field.dataElementId id of the dataElement
* @param {} field.categoryOptionComboId id of the categoryOptionCombo
+ * @param {} field.indicatorId id of the indicator
* @returns
*/
setHighlightedField: (highlightedFieldId) => set({ highlightedFieldId }),
getHighlightedField: () => get().highlightedFieldId,
- isFieldHighlighted: ({ dataElementId, categoryOptionComboId }) => {
+ isFieldHighlighted: ({
+ dataElementId,
+ categoryOptionComboId,
+ indicatorId,
+ }) => {
const highlightedField = get().highlightedFieldId
- return (
- !!highlightedField &&
+ if (!highlightedField) {
+ return false
+ }
+
+ if (
+ dataElementId &&
highlightedField.dataElementId === dataElementId &&
+ categoryOptionComboId &&
highlightedField.categoryOptionComboId === categoryOptionComboId
- )
+ ) {
+ return true
+ }
+
+ if (indicatorId && highlightedField?.indicatorId === indicatorId) {
+ return true
+ }
+
+ return false
},
}))