From e0398d0b952b0202d0a5498c96219fe6cff038ec Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 24 Jan 2024 09:33:27 +0100 Subject: [PATCH] feat: prefix time dimension ids with program id when tracked entity output type (#480) Implements https://dhis2.atlassian.net/browse/DHIS2-16023 Description Time dimensions need to be prefixed with the programId when using TE output type. * adding time dimensions from different programs to layout - they get suffixed with the program name * analytics query works - correct data is displayed * saving LL with time dimensions saves successfully * opening a saved LL with time dimensions - layout is empty but table shows correctly (metadata issue to be solved with backend changes) The failing cypress tests are the same failures as the target branch --- src/actions/ui.js | 9 +- .../PeriodDimension/PeriodDimension.js | 22 ++++- src/modules/__tests__/metadata.spec.js | 85 +++++++++++++++++++ src/modules/__tests__/timeDimensions.spec.js | 34 ++++---- src/modules/metadata.js | 17 +++- src/modules/timeDimensions.js | 12 +-- src/reducers/ui.js | 32 +++++-- 7 files changed, 171 insertions(+), 40 deletions(-) create mode 100644 src/modules/__tests__/metadata.spec.js diff --git a/src/actions/ui.js b/src/actions/ui.js index ec71cb7f2..bbbe808ec 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -164,14 +164,19 @@ export const tSetUiProgram = (dispatch, getState) => { const state = getState() dispatch(acClearUiProgram()) - if (sGetUiInputType(state) !== OUTPUT_TYPE_TRACKED_ENTITY) { + const inputType = sGetUiInputType(state) + if (inputType !== OUTPUT_TYPE_TRACKED_ENTITY) { dispatch(tClearUiProgramRelatedDimensions()) } program && dispatch( acUpdateUiProgramId(program.id, { ...getProgramAsMetadata(program), - ...getDynamicTimeDimensionsMetadata(program, stage), + ...getDynamicTimeDimensionsMetadata( + program, + stage, + inputType + ), }) ) stage && dispatch(acUpdateUiProgramStageId(stage.id)) diff --git a/src/components/Dialogs/PeriodDimension/PeriodDimension.js b/src/components/Dialogs/PeriodDimension/PeriodDimension.js index b4b73498e..f9cfe4626 100644 --- a/src/components/Dialogs/PeriodDimension/PeriodDimension.js +++ b/src/components/Dialogs/PeriodDimension/PeriodDimension.js @@ -25,9 +25,14 @@ import { SYSTEM_SETTINGS_HIDE_BIMONTHLY_PERIODS, } from '../../../modules/systemSettings.js' import { USER_SETTINGS_UI_LOCALE } from '../../../modules/userSettings.js' +import { + extractDimensionIdParts, + formatDimensionId, +} from '../../../modules/utils.js' import { sGetDimensionIdsFromLayout, sGetUiItemsByDimension, + sGetUiInputType, } from '../../../reducers/ui.js' import DimensionModal from '../DimensionModal.js' import styles from './PeriodDimension.module.css' @@ -118,18 +123,27 @@ export const PeriodDimension = ({ dimension, onClose }) => { : OPTION_PRESETS ) + const outputType = useSelector(sGetUiInputType) + + const { programId } = extractDimensionIdParts(dimension.id, outputType) + const updatePeriodDimensionItems = (items) => { const { uiItems, metadata } = items.reduce( (acc, item) => { - acc.uiItems.push(item.id) + const id = formatDimensionId({ + dimensionId: item.id, + programId, + outputType, + }) + acc.uiItems.push(id) if (isStartEndDate(item.id)) { - acc.metadata[item.id] = { - id: item.id, + acc.metadata[id] = { + id, name: formatStartEndDate(item.id), } } else { - acc.metadata[item.id] = item + acc.metadata[id] = { ...item, id } } return acc diff --git a/src/modules/__tests__/metadata.spec.js b/src/modules/__tests__/metadata.spec.js new file mode 100644 index 000000000..2d4db3326 --- /dev/null +++ b/src/modules/__tests__/metadata.spec.js @@ -0,0 +1,85 @@ +import { getDynamicTimeDimensionsMetadata } from '../metadata.js' + +describe('getDynamicTimeDimensionsMetadata', () => { + it('should return correct dynamic time dimensions metadata when inputType is OUTPUT_TYPE_TRACKED_ENTITY', () => { + const program = { + id: 'programId', + displayEnrollmentDateLabel: 'Custom Enrollment Date', + displayIncidentDateLabel: 'Custom Incident Date', + } + const stage = { displayExecutionDateLabel: 'Stage Execution Date' } + const inputType = 'TRACKED_ENTITY_INSTANCE' // OUTPUT_TYPE_TRACKED_ENTITY + + const result = getDynamicTimeDimensionsMetadata( + program, + stage, + inputType + ) + + const expected = { + 'programId.eventDate': { + id: 'programId.eventDate', + dimensionType: 'PERIOD', + name: 'Stage Execution Date', + }, + 'programId.enrollmentDate': { + id: 'programId.enrollmentDate', + dimensionType: 'PERIOD', + name: 'Custom Enrollment Date', + }, + 'programId.incidentDate': { + id: 'programId.incidentDate', + dimensionType: 'PERIOD', + name: 'Custom Incident Date', + }, + 'programId.scheduledDate': { + id: 'programId.scheduledDate', + dimensionType: 'PERIOD', + name: 'Scheduled date', + }, + } + + expect(result).toEqual(expected) + }) + + it('should return correct dynamic time dimensions metadata when inputType is not OUTPUT_TYPE_TRACKED_ENTITY', () => { + const program = { + id: 'programId', + displayEnrollmentDateLabel: 'Program Enrollment Date', + displayIncidentDateLabel: 'Custom Incident Date', + } + const stage = { displayExecutionDateLabel: 'Stage Execution Date' } + const inputType = 'OTHER_TYPE' + + const result = getDynamicTimeDimensionsMetadata( + program, + stage, + inputType + ) + + const expected = { + eventDate: { + id: 'eventDate', + dimensionType: 'PERIOD', + name: 'Stage Execution Date', + }, + enrollmentDate: { + id: 'enrollmentDate', + dimensionType: 'PERIOD', + name: 'Program Enrollment Date', + }, + incidentDate: { + id: 'incidentDate', + dimensionType: 'PERIOD', + name: 'Custom Incident Date', + }, + scheduledDate: { + id: 'scheduledDate', + dimensionType: 'PERIOD', + name: 'Scheduled date', + }, + } + + expect(result).toEqual(expected) + }) +}) diff --git a/src/modules/__tests__/timeDimensions.spec.js b/src/modules/__tests__/timeDimensions.spec.js index 3a718e77c..35a669a59 100644 --- a/src/modules/__tests__/timeDimensions.spec.js +++ b/src/modules/__tests__/timeDimensions.spec.js @@ -36,7 +36,7 @@ describe('ER > Dimensions > getTimeDimensionName', () => { } Object.values(getTimeDimensions()).forEach((dimension) => { expect(getTimeDimensionName(dimension, program, stage)).toEqual( - dimension.name + dimension.defaultName ) }) }) @@ -57,16 +57,16 @@ describe('ER > Dimensions > getTimeDimensionName', () => { expect( getTimeDimensionName(eventDateDimension, program, stage) - ).toEqual(stage.displayExecutionDateLabel) + ).toEqual('le event date') expect( getTimeDimensionName(enrollmentDateDimension, program, stage) - ).toEqual(enrollmentDateDimension.name) + ).toEqual('Enrollment date') expect( getTimeDimensionName(incidentDateDimension, program, stage) - ).toEqual(incidentDateDimension.name) + ).toEqual('Incident date') expect( getTimeDimensionName(scheduledDateDimension, program, stage) - ).toEqual(scheduledDateDimension.name) + ).toEqual('Scheduled date') }) it('uses displayEnrollmentDateLabel from program for enrollment date', () => { const program = { @@ -85,16 +85,16 @@ describe('ER > Dimensions > getTimeDimensionName', () => { expect( getTimeDimensionName(eventDateDimension, program, stage) - ).toEqual(eventDateDimension.name) + ).toEqual('Event date') expect( getTimeDimensionName(enrollmentDateDimension, program, stage) - ).toEqual(program.displayEnrollmentDateLabel) + ).toEqual('le enrollment date') expect( getTimeDimensionName(incidentDateDimension, program, stage) - ).toEqual(incidentDateDimension.name) + ).toEqual('Incident date') expect( getTimeDimensionName(scheduledDateDimension, program, stage) - ).toEqual(scheduledDateDimension.name) + ).toEqual('Scheduled date') }) it('uses displayDueDateLabel from stage for scheduled date', () => { const program = { @@ -113,16 +113,16 @@ describe('ER > Dimensions > getTimeDimensionName', () => { expect( getTimeDimensionName(eventDateDimension, program, stage) - ).toEqual(eventDateDimension.name) + ).toEqual('Event date') expect( getTimeDimensionName(enrollmentDateDimension, program, stage) - ).toEqual(enrollmentDateDimension.name) + ).toEqual('Enrollment date') expect( getTimeDimensionName(incidentDateDimension, program, stage) - ).toEqual(incidentDateDimension.name) + ).toEqual('Incident date') expect( getTimeDimensionName(scheduledDateDimension, program, stage) - ).toEqual(stage.displayDueDateLabel) + ).toEqual('le due date') }) it('uses displayIncidentDateLabel from program for incident date', () => { const program = { @@ -141,16 +141,16 @@ describe('ER > Dimensions > getTimeDimensionName', () => { expect( getTimeDimensionName(eventDateDimension, program, stage) - ).toEqual(eventDateDimension.name) + ).toEqual('Event date') expect( getTimeDimensionName(enrollmentDateDimension, program, stage) - ).toEqual(enrollmentDateDimension.name) + ).toEqual('Enrollment date') expect( getTimeDimensionName(incidentDateDimension, program, stage) - ).toEqual(program.displayIncidentDateLabel) + ).toEqual('le incident date') expect( getTimeDimensionName(scheduledDateDimension, program, stage) - ).toEqual(scheduledDateDimension.name) + ).toEqual('Scheduled date') }) }) diff --git a/src/modules/metadata.js b/src/modules/metadata.js index ff9a3a6a2..8da3b0c1b 100644 --- a/src/modules/metadata.js +++ b/src/modules/metadata.js @@ -9,6 +9,7 @@ import i18n from '@dhis2/d2-i18n' import { getMainDimensions, getCreatedDimension } from './mainDimensions.js' import { getProgramDimensions } from './programDimensions.js' import { getTimeDimensions, getTimeDimensionName } from './timeDimensions.js' +import { formatDimensionId } from './utils.js' import { OUTPUT_TYPE_TRACKED_ENTITY, getStatusNames } from './visualization.js' const formatObject = (object) => @@ -70,10 +71,20 @@ export const getProgramAsMetadata = (program) => ({ [program.id]: program, }) -export const getDynamicTimeDimensionsMetadata = (program, stage) => ({ +export const getDynamicTimeDimensionsMetadata = ( + program, + stage, + inputType +) => ({ ...Object.values(getTimeDimensions()).reduce((acc, dimension) => { - acc[dimension.id] = { - id: dimension.id, + const id = formatDimensionId({ + dimensionId: dimension.id, + programId: program?.id, + outputType: inputType, + }) + + acc[id] = { + id, dimensionType: dimension.dimensionType, name: getTimeDimensionName(dimension, program, stage), } diff --git a/src/modules/timeDimensions.js b/src/modules/timeDimensions.js index 8447b0857..727b7291a 100644 --- a/src/modules/timeDimensions.js +++ b/src/modules/timeDimensions.js @@ -21,28 +21,28 @@ export const getTimeDimensions = () => ({ [DIMENSION_ID_EVENT_DATE]: { id: DIMENSION_ID_EVENT_DATE, dimensionType: DIMENSION_TYPE_PERIOD, - name: i18n.t('Event date'), + defaultName: i18n.t('Event date'), nameParentProperty: NAME_PARENT_PROPERTY_STAGE, nameProperty: 'displayExecutionDateLabel', }, [DIMENSION_ID_ENROLLMENT_DATE]: { id: DIMENSION_ID_ENROLLMENT_DATE, dimensionType: DIMENSION_TYPE_PERIOD, - name: i18n.t('Enrollment date'), + defaultName: i18n.t('Enrollment date'), nameParentProperty: NAME_PARENT_PROPERTY_PROGRAM, nameProperty: 'displayEnrollmentDateLabel', }, [DIMENSION_ID_INCIDENT_DATE]: { id: DIMENSION_ID_INCIDENT_DATE, dimensionType: DIMENSION_TYPE_PERIOD, - name: i18n.t('Incident date'), + defaultName: i18n.t('Incident date'), nameParentProperty: NAME_PARENT_PROPERTY_PROGRAM, nameProperty: 'displayIncidentDateLabel', }, [DIMENSION_ID_SCHEDULED_DATE]: { id: DIMENSION_ID_SCHEDULED_DATE, dimensionType: DIMENSION_TYPE_PERIOD, - name: i18n.t('Scheduled date'), + defaultName: i18n.t('Scheduled date'), nameParentProperty: NAME_PARENT_PROPERTY_STAGE, nameProperty: 'displayDueDateLabel', }, @@ -50,14 +50,14 @@ export const getTimeDimensions = () => ({ export const getTimeDimensionName = (dimension, program, stage) => { if (!dimension.nameParentProperty || !program) { - return dimension.name + return dimension.defaultName } const name = dimension.nameParentProperty === NAME_PARENT_PROPERTY_PROGRAM ? program[dimension.nameProperty] : stage?.[dimension.nameProperty] - return name || dimension.name + return name || dimension.defaultName } export const getHiddenTimeDimensions = (inputType, program, stage) => { diff --git a/src/reducers/ui.js b/src/reducers/ui.js index 784bce92f..c44e24f6e 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -25,6 +25,7 @@ import { } from '../modules/programDimensions.js' import { getHiddenTimeDimensions } from '../modules/timeDimensions.js' import { getAdaptedUiByType, getUiFromVisualization } from '../modules/ui.js' +import { formatDimensionId, extractDimensionIdParts } from '../modules/utils.js' import { OUTPUT_TYPE_EVENT, OUTPUT_TYPE_TRACKED_ENTITY, @@ -489,17 +490,24 @@ export const useProgramDimensions = () => { const programId = useSelector(sGetUiProgramId) const programStageId = useSelector(sGetUiProgramStageId) + const getId = (dimensionId) => + formatDimensionId({ + dimensionId, + programId, + outputType: inputType, + }) + const eventDateDim = useSelector((state) => - sGetMetadataById(state, DIMENSION_ID_EVENT_DATE) + sGetMetadataById(state, getId(DIMENSION_ID_EVENT_DATE)) ) const enrollmentDateDim = useSelector((state) => - sGetMetadataById(state, DIMENSION_ID_ENROLLMENT_DATE) + sGetMetadataById(state, getId(DIMENSION_ID_ENROLLMENT_DATE)) ) const incidentDateDim = useSelector((state) => - sGetMetadataById(state, DIMENSION_ID_INCIDENT_DATE) + sGetMetadataById(state, getId(DIMENSION_ID_INCIDENT_DATE)) ) const scheduledDateDim = useSelector((state) => - sGetMetadataById(state, DIMENSION_ID_SCHEDULED_DATE) + sGetMetadataById(state, getId(DIMENSION_ID_SCHEDULED_DATE)) ) return useMemo(() => { @@ -520,10 +528,18 @@ export const useProgramDimensions = () => { ? [scheduledDateDim] : []), incidentDateDim, - ].filter( - (dimension) => - !!dimension && !hiddenTimeDimensions.includes(dimension.id) - ) + ].filter((dimension) => { + // TODO this filter used to return !!dimension && !hiddenTimeDimensions.includes(dimension.id) + // is there a reason the dimension is checked for existence? + if (!dimension) { + return false + } + const { dimensionId } = extractDimensionIdParts( + dimension.id, + inputType + ) + return !hiddenTimeDimensions.includes(dimensionId) + }) const programDimensions = Object.values( getProgramDimensions(