diff --git a/.hooks/pre-push b/.hooks/pre-push old mode 100755 new mode 100644 diff --git a/src/components/app-wrapper/metadata-helpers/analytics-data.ts b/src/components/app-wrapper/metadata-helpers/analytics-data.ts index bd62f383..593f5862 100644 --- a/src/components/app-wrapper/metadata-helpers/analytics-data.ts +++ b/src/components/app-wrapper/metadata-helpers/analytics-data.ts @@ -2,7 +2,7 @@ import { isMetadataInputItem } from './type-guards' import type { AnalyticsResponseMetadataItems, MetadataInput } from './types' import type { LineListAnalyticsDataHeader } from '@components/line-list/types' import type { AnalyticsResponseMetadataDimensions } from '@components/plugin-wrapper/hooks/use-line-list-analytics-data' -import { headersMap } from '@modules/visualization' +import { reversedHeadersMap } from '@modules/visualization' const extractItemsMetadata = ( items: AnalyticsResponseMetadataItems, @@ -50,18 +50,6 @@ const extractItemsMetadata = ( return acc }, {}) -/* The headersMap is a lookup for app -> webApi (i.e. eventDate -> eventdate) - * but here the lookup needs to be in the reverse order (i.e. eventdate -> eventDate) - * because we need to map the keys from the header columns in the response data - * for usage in the app */ -const reversedHeadersMap = Object.entries(headersMap).reduce( - (acc, [key, value]) => { - acc[value] = key - return acc - }, - {} -) - const updateNamesFromHeaders = ( headers: Array, metdataFromItems: MetadataInput diff --git a/src/components/app-wrapper/metadata-helpers/visualization.ts b/src/components/app-wrapper/metadata-helpers/visualization.ts index fd1ae90f..c691615a 100644 --- a/src/components/app-wrapper/metadata-helpers/visualization.ts +++ b/src/components/app-wrapper/metadata-helpers/visualization.ts @@ -1,4 +1,3 @@ -import i18n from '@dhis2/d2-i18n' import deepmerge from 'deepmerge' import type { MetadataInput, @@ -16,10 +15,13 @@ import { getTimeDimensions, getUiDimensionType, } from '@modules/dimension' -import { transformVisualization } from '@modules/visualization' +import { getDefaultOrgUnitMetadata } from '@modules/metadata' +import { + dimensionMetadataPropMap, + transformVisualization, +} from '@modules/visualization' import type { DimensionId, - DimensionType, InternalDimensionRecord, SavedVisualization, } from '@types' @@ -29,34 +31,6 @@ const FIXED_DIMENSION_LOOKUP = new Set([ 'eventStatus', 'programStatus', ]) -const DIMENSION_METADATA_PROP_MAP = { - dataElementDimensions: 'dataElement', - attributeDimensions: 'attribute', - programIndicatorDimensions: 'programIndicator', - categoryDimensions: 'category', - categoryOptionGroupSetDimensions: 'categoryOptionGroupSet', - organisationUnitGroupSetDimensions: 'organisationUnitGroupSet', - dataElementGroupSetDimensions: 'dataElementGroupSet', -} -const getDefaultOrgUnitMetadata = ( - outputType: SavedVisualization['outputType'] -) => ({ - ou: { - id: 'ou', - dimensionType: 'ORGANISATION_UNIT' as DimensionType, - name: getDefaultOrgUnitLabel(outputType), - }, -}) - -const getDefaultOrgUnitLabel = ( - outputType: SavedVisualization['outputType'] -) => { - if (outputType === 'TRACKED_ENTITY_INSTANCE') { - return i18n.t('Registration org. unit') - } else { - return i18n.t('Organisation unit') - } -} const getDefaultDynamicTimeDimensionsMetadata = ( program?: SavedVisualization['program'], @@ -165,19 +139,20 @@ export const extractProgramDimensionsMetadata = ( export const extractDimensionMetadata = ( visualization: SavedVisualization ): MetadataInputMap => { - const dimensionMetadata = Object.entries( - DIMENSION_METADATA_PROP_MAP - ).reduce((metaData, [listName, dimensionName]) => { - const dimensionList = visualization[listName] || [] - - dimensionList.forEach((dimensionWrapper: object) => { - const dimension: InternalDimensionRecord = - dimensionWrapper[dimensionName] - metaData[dimension.id] = dimension - }) + const dimensionMetadata = Object.entries(dimensionMetadataPropMap).reduce( + (metaData, [listName, dimensionName]) => { + const dimensionList = visualization[listName] || [] + + dimensionList.forEach((dimensionWrapper: object) => { + const dimension: InternalDimensionRecord = + dimensionWrapper[dimensionName] + metaData[dimension.id] = dimension + }) - return metaData - }, {}) + return metaData + }, + {} + ) return dimensionMetadata } diff --git a/src/components/line-list/use-transformed-line-list-data.ts b/src/components/line-list/use-transformed-line-list-data.ts index bd5e8055..8823a939 100644 --- a/src/components/line-list/use-transformed-line-list-data.ts +++ b/src/components/line-list/use-transformed-line-list-data.ts @@ -13,7 +13,10 @@ import { getMainDimensions, getProgramDimensions, } from '@modules/dimension' -import { headersMap } from '@modules/visualization' +import { + headersMap, + getDimensionIdFromHeaderName, +} from '@modules/visualization' import type { CurrentVisualization, OutputType, @@ -63,12 +66,7 @@ const getHeaderDimensionId = ( id: header.name ?? '', outputType, }) - const idMatch = - Object.keys(headersMap).find( - (key) => headersMap[key] === dimensionId - // TODO: find a better solution - // https://dhis2.atlassian.net/browse/DHIS2-20136 - ) ?? '' + const idMatch = getDimensionIdFromHeaderName(dimensionId) ?? '' const formattedDimensionId = getFullDimensionId({ dimensionId: [ diff --git a/src/components/plugin-wrapper/hooks/use-line-list-analytics-data.ts b/src/components/plugin-wrapper/hooks/use-line-list-analytics-data.ts index cf2f5e04..37194655 100644 --- a/src/components/plugin-wrapper/hooks/use-line-list-analytics-data.ts +++ b/src/components/plugin-wrapper/hooks/use-line-list-analytics-data.ts @@ -24,7 +24,7 @@ import { } from '@modules/dimension' import { isValueTypeNumeric } from '@modules/value-type' import { - headersMap, + getDimensionIdFromHeaderName, isVisualizationWithTimeDimension, } from '@modules/visualization' import type { CurrentUser, CurrentVisualization, OutputType } from '@types' @@ -182,12 +182,7 @@ const extractHeaders = ( id: header.name, outputType, }) - const idMatch = - Object.keys(headersMap).find( - (key) => headersMap[key] === dimensionId - // TODO: find a better solution - // https://dhis2.atlassian.net/browse/DHIS2-20136 - ) ?? '' + const idMatch = getDimensionIdFromHeaderName(dimensionId) ?? '' const formattedDimensionId = getFullDimensionId({ dimensionId: [ @@ -242,12 +237,7 @@ const extractHeaders = ( outputType, }) - const idMatch = - Object.keys(headersMap).find( - (key) => headersMap[key] === dimensionId - // TODO: find a better solution - // https://dhis2.atlassian.net/browse/DHIS2-20136 - ) ?? '' + const idMatch = getDimensionIdFromHeaderName(dimensionId) ?? '' result.column = labels.find( diff --git a/src/constants/options.ts b/src/constants/options.ts index 631c3932..cca51d92 100644 --- a/src/constants/options.ts +++ b/src/constants/options.ts @@ -1,15 +1,11 @@ import type { EventVisualizationOptions, LegendOption } from '@types' -export const OPTIONS_SECTION_KEYS_LINE_LIST = [ - 'data', - 'style', - 'legend', -] as const -export const OPTIONS_SECTION_KEYS_PIVOT_TABLE = [ - 'data', - 'style', - 'legend', -] as const +// Base options section keys shared by all visualization types +const OPTIONS_SECTION_KEYS = ['data', 'style', 'legend'] as const + +// Re-export with specific names for backwards compatibility and type derivation +export const OPTIONS_SECTION_KEYS_LINE_LIST = OPTIONS_SECTION_KEYS +export const OPTIONS_SECTION_KEYS_PIVOT_TABLE = OPTIONS_SECTION_KEYS export const DEFAULT_LEGEND_OPTION: LegendOption = { showKey: false, diff --git a/src/modules/visualization.ts b/src/modules/visualization.ts index 73ef382a..addcfc09 100644 --- a/src/modules/visualization.ts +++ b/src/modules/visualization.ts @@ -54,6 +54,28 @@ export const headersMap: Record = { lastUpdated: 'lastupdated', } +/** + * Pre-computed reverse map from API header names to dimension IDs. + * Use this when you need to look up multiple dimension IDs efficiently. + */ +export const reversedHeadersMap: Record = Object.entries( + headersMap +).reduce((acc, [key, value]) => { + acc[value] = key as DimensionId + return acc +}, {} as Record) + +/** + * Get the dimension ID (app format) from a header name (API format). + * This is a reverse lookup from headersMap (e.g., 'eventdate' -> 'eventDate'). + * Uses the pre-computed reversedHeadersMap for O(1) lookup. + * @param headerName - The header name from the API response + * @returns The dimension ID for the app, or undefined if not found + */ +export const getDimensionIdFromHeaderName = ( + headerName: string +): DimensionId | undefined => reversedHeadersMap[headerName] + export const getHeadersMap = ({ showHierarchy, }: { @@ -170,14 +192,20 @@ const removeDimensionPropertiesBeforeSaving = ( }) } -const getDimensionIdFromHeaderName = ( +const getDimensionIdFromHeaderNameWithContext = ( headerName: string, visualization: CurrentVisualization ) => { - const headersMap = getHeadersMap( - getRequestOptions(visualization) as unknown as CurrentVisualization + // Type cast is needed because getRequestOptions returns ParameterRecord + // which doesn't expose showHierarchy explicitly, but may contain it. + // TODO: Consider updating getRequestOptions return type to be more specific + // See: https://dhis2.atlassian.net/browse/DHIS2-19823 + const contextHeadersMap = getHeadersMap( + getRequestOptions(visualization) as { showHierarchy?: boolean } + ) + return Object.keys(contextHeadersMap).find( + (key) => contextHeadersMap[key] === headerName ) - return Object.keys(headersMap).find((key) => headersMap[key] === headerName) } export const getSaveableVisualization = ( @@ -213,7 +241,7 @@ export const getSaveableVisualization = ( ? [ { dimension: - getDimensionIdFromHeaderName( + getDimensionIdFromHeaderNameWithContext( vis.sorting[0].dimension, vis ) || vis.sorting[0].dimension,