Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7d6c5a1
chore: refactor EventDialog to functional component [DHIS2-18209]
BRaimbault Jun 18, 2026
ccd1c78
fix: prevent default relative period from re-applying after switching…
BRaimbault Jun 19, 2026
12a365d
Merge branch 'master' into chore/DHIS2-18209-EventDialog-v2
BRaimbault Jun 19, 2026
d3a0485
fix: forward both arguments to setEventCoordinateField to prevent inf…
BRaimbault Jun 19, 2026
34ed240
Merge branch 'master' into chore/DHIS2-18209-EventDialog-v2
BRaimbault Jun 22, 2026
f7ed9f7
Merge origin/master into feat/DHIS2-8165-v2
BRaimbault Jun 26, 2026
befc3af
fix: prevent duplicate datastore requests on load (#3696)
karolinelien Jun 26, 2026
357cc53
fix: remove north arrow rotate listener on cleanup (#3698)
karolinelien Jun 26, 2026
36f8f31
fix: prevent data table from crashing when cursor leaves browser wind…
karolinelien Jun 26, 2026
f57e515
fix: correct time period filtering in Earth Engine layers (#3697)
karolinelien Jun 26, 2026
1a3f267
fix: prevent crash when loading map with external layer (#3700)
karolinelien Jun 26, 2026
7ec0beb
chore(release): cut 101.13.4 [skip release]
dhis2-bot Jun 29, 2026
a65ae36
chore(deps): bump the dependencies group across 1 directory with 2 up…
dependabot[bot] Jun 29, 2026
4d52924
chore(deps): bump react-sortable-hoc from 1.11.0 to 2.0.0 (#3704)
dependabot[bot] Jun 29, 2026
eaa71c2
chore(deps): bump @dhis2/analytics from 29.4.1 to 29.5.4 (#3706)
dependabot[bot] Jun 29, 2026
dc0e02a
feat: add style by geometry source for event layers [DHIS2-8165]
BRaimbault Jun 29, 2026
70f2525
Merge branch 'master' into feat/DHIS2-8165-v2
BRaimbault Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/support/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const requests = {
},
getCachedDataProvider_SystemSettings: {
method: 'GET',
url: '**/systemSettings?key=keyAnalysisRelativePeriod,keyHideDailyPeriods,keyHideWeeklyPeriods,keyHideBiWeeklyPeriods,keyHideMonthlyPeriods,keyHideBiMonthlyPeriods,keyDefaultBaseMap,keyBingMapsApiKey,keyAzureMapsApiKey',
url: '**/systemSettings?key=keyAnalysisRelativePeriod,keyHideDailyPeriods,keyHideWeeklyPeriods,keyHideBiWeeklyPeriods,keyHideMonthlyPeriods,keyHideBiMonthlyPeriods,keyDefaultBaseMap,orgUnitCentroidsInEventsAnalytics,keyBingMapsApiKey,keyAzureMapsApiKey',
},
getCachedDataProvider_ExternalMapLayers: {
method: 'GET',
Expand Down
37 changes: 24 additions & 13 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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: 2026-06-25T13:36:15.573Z\n"
"PO-Revision-Date: 2026-06-25T13:36:15.573Z\n"
"POT-Creation-Date: 2026-06-29T14:10:13.913Z\n"
"PO-Revision-Date: 2026-06-29T14:10:13.913Z\n"

msgid "2020"
msgstr "2020"
Expand Down Expand Up @@ -101,15 +101,15 @@ msgstr "Enrollment location"
msgid "Tracked entity location"
msgstr "Tracked entity location"

msgid "Select a program stage to see additional coordinate options"
msgstr "Select a program stage to see additional coordinate options"

msgid "Enrollment > event > tracked entity > org unit coordinate"
msgstr "Enrollment > event > tracked entity > org unit coordinate"

msgid "Event > org unit coordinate"
msgstr "Event > org unit coordinate"

msgid "Select a program stage to see additional coordinate options"
msgstr "Select a program stage to see additional coordinate options"

msgid "Select a program to see additional coordinate options"
msgstr "Select a program to see additional coordinate options"

Expand All @@ -119,11 +119,14 @@ msgstr "Fallback coordinate field"
msgid "Coordinate field"
msgstr "Coordinate field"

msgid "Include events with no data"
msgstr "Include events with no data"

msgid "Include unclassified events"
msgstr "Include unclassified events"

msgid "Include events with no data"
msgstr "Include events with no data"
msgid "Geometry source"
msgstr "Geometry source"

msgid "Previously selected value not available in list: {{id}}"
msgstr "Previously selected value not available in list: {{id}}"
Expand Down Expand Up @@ -549,21 +552,24 @@ msgstr "the date a tracked entity was registered or enrolled in a program"
msgid "Program status"
msgstr "Program status"

msgid "Tracked Entity Type is required"
msgstr "Tracked Entity Type is required"

msgid "Relationships"
msgstr "Relationships"

msgid "Follow up"
msgstr "Follow up"

msgid "Please select a Tracked Entity Type before selecting a Relationship Type"
msgstr "Please select a Tracked Entity Type before selecting a Relationship Type"

msgid "Displaying tracked entity relationships in Maps is an experimental feature"
msgstr "Displaying tracked entity relationships in Maps is an experimental feature"

msgid "Display Tracked Entity relationships"
msgstr "Display Tracked Entity relationships"

msgid "Please select a Tracked Entity Type before selecting a Relationship Type"
msgstr "Please select a Tracked Entity Type before selecting a Relationship Type"

msgid "Tracked entity style"
msgstr "Tracked entity style"

Expand All @@ -576,9 +582,6 @@ msgstr "Related entity style"
msgid "Line Color"
msgstr "Line Color"

msgid "Tracked Entity Type is required"
msgstr "Tracked Entity Type is required"

msgid "No relationship types were found for tracked entity type {{type}}"
msgstr "No relationship types were found for tracked entity type {{type}}"

Expand Down Expand Up @@ -1899,3 +1902,11 @@ msgstr "End date is invalid"

msgid "End date cannot be earlier than start date"
msgstr "End date cannot be earlier than start date"

msgctxt "Application title"
msgid "__MANIFEST_APP_TITLE"
msgstr "Maps"

msgctxt "Application description"
msgid "__MANIFEST_APP_DESCRIPTION"
msgstr "DHIS2 Maps"
6 changes: 6 additions & 0 deletions src/actions/layerEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export const setBooleanStyle = (value, color) => ({
color,
})

export const setGeometrySourceStyle = (value, color) => ({
type: types.LAYER_EDIT_STYLE_DATA_ITEM_GEOMETRY_SOURCE_SET,
value,
color,
})

// Set thematic map type (choropleth, bubble map)
export const setThematicMapType = (type) => ({
type: types.LAYER_EDIT_THEMATIC_MAP_TYPE_SET,
Expand Down
32 changes: 16 additions & 16 deletions src/components/dataItem/CoordinateField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
eventCoordinateField,
onChange,
className,
dataTest = 'coordinatefield',
}) => {
const { serverVersion } = useConfig()

Expand Down Expand Up @@ -48,7 +49,10 @@
includeTypes,
})

const defaultValue = eventCoordinateField ? NONE : EVENT_COORDINATE_DEFAULT
const defaultValue = useMemo(
() => (eventCoordinateField ? NONE : EVENT_COORDINATE_DEFAULT),
[eventCoordinateField]
)

const fields = useMemo(() => {
const isFallback = !!eventCoordinateField
Expand Down Expand Up @@ -93,30 +97,23 @@
fields.push(...eventDataItems)
}

if (isFallback) {
fields.push({
id: EVENT_COORDINATE_ORG_UNIT,
name: i18n.t('Organisation unit location'),
})
}

return eventCoordinateField
return isFallback
? fields.filter((f) => f.id !== eventCoordinateField)
: fields
}, [trackedEntityType, eventDataItems, eventCoordinateField])

let helpText = null
if (program) {
if (!programStage && trackedEntityType) {
helpText = i18n.t(
'Select a program stage to see additional coordinate options'
)
} else if (value === EVENT_COORDINATE_CASCADING) {
if (value === EVENT_COORDINATE_CASCADING) {
helpText = trackedEntityType
? i18n.t(
'Enrollment > event > tracked entity > org unit coordinate'
)
: i18n.t('Event > org unit coordinate')
} else if (!programStage && trackedEntityType) {
helpText = i18n.t(
'Select a program stage to see additional coordinate options'
)
}
} else {
helpText = i18n.t(
Expand All @@ -139,7 +136,9 @@
if (
trackedEntityType &&
eventDataItems &&
!fields.find((f) => f.id === value)
!fields.find((f) => f.id === value) &&

Check warning on line 139 in src/components/dataItem/CoordinateField.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.some(…)` over `.find(…)`.

See more on https://sonarcloud.io/project/issues?id=dhis2_maps-app&issues=AZ8TuqUjj1L7c67w9CLE&open=AZ8TuqUjj1L7c67w9CLE&pullRequest=3707
value !== defaultValue &&
fields.length > 0
) {
onChange(defaultValue, defaultValue)
}
Expand Down Expand Up @@ -169,14 +168,15 @@
onChange(field.id, field.valueType || field.id)
}
className={className}
dataTest="coordinatefield"
dataTest={dataTest}
/>
)
}

CoordinateField.propTypes = {
onChange: PropTypes.func.isRequired,
className: PropTypes.string,
dataTest: PropTypes.string,
eventCoordinateField: PropTypes.string,
program: PropTypes.object,
programStage: PropTypes.object,
Expand Down
19 changes: 19 additions & 0 deletions src/components/dataItem/DataItemStyle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
setNoDataLegend,
setUnclassifiedLegend,
} from '../../actions/layerEdit.js'
import { GEOMETRY_SOURCE_DATA_ITEM_ID } from '../../constants/layers.js'
import {
numberValueTypes,
booleanValueTypes,
Expand All @@ -15,6 +16,7 @@ import NoDataLegend from '../edit/shared/NoDataLegend.jsx'
import UnclassifiedLegend from '../edit/shared/UnclassifiedLegend.jsx'
import OptionSetStyle from '../optionSet/OptionSetStyle.jsx'
import BooleanStyle from './BooleanStyle.jsx'
import GeometrySourceStyle from './GeometrySourceStyle.jsx'
import styles from './styles/DataItemStyle.module.css'

const DataItemStyle = ({ dataItem, style }) => {
Expand All @@ -28,6 +30,23 @@ const DataItemStyle = ({ dataItem, style }) => {
return null
}

if (dataItem.id === GEOMETRY_SOURCE_DATA_ITEM_ID) {
return (
<div style={style}>
<GeometrySourceStyle />
<div className={styles.flexColumnFlow}>
<div className={styles.flexColumn}>
<NoDataLegend
label={i18n.t('Include events with no data')}
value={noDataLegend}
onChange={(v) => dispatch(setNoDataLegend(v))}
/>
</div>
</div>
</div>
)
}

const { valueType, optionSet } = dataItem
const hasClassification =
numberValueTypes.includes(valueType) ||
Expand Down
132 changes: 132 additions & 0 deletions src/components/dataItem/GeometrySourceStyle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { setGeometrySourceStyle } from '../../actions/layerEdit.js'
import { qualitativeColors } from '../../constants/colors.js'
import {
COORDINATE_FIELD_NAMES,
EVENT_COORDINATE_CASCADING,
EVENT_COORDINATE_DEFAULT,
EVENT_COORDINATE_ENROLLMENT,
EVENT_COORDINATE_ORG_UNIT,
EVENT_COORDINATE_TRACKED_ENTITY,
NONE,
} from '../../constants/layers.js'
import { useEventDataItems } from '../../hooks/useEventDataItems.js'
import OptionStyle from '../optionSet/OptionStyle.jsx'

const style = {
marginTop: 20,
}

// Expands a coordinate field id to the set of geometrySource values the backend can return.
// For 'cascading', this is the full cascade chain based on whether the program has a TEI type.
const expandField = (fieldId, hasTei) => {
if (fieldId === EVENT_COORDINATE_CASCADING) {
return hasTei
? [
EVENT_COORDINATE_ENROLLMENT,
EVENT_COORDINATE_DEFAULT,
EVENT_COORDINATE_TRACKED_ENTITY,
EVENT_COORDINATE_ORG_UNIT,
]
: [EVENT_COORDINATE_DEFAULT, EVENT_COORDINATE_ORG_UNIT]
}
return [fieldId]
}

const getPossibleSources = (
eventCoordinateField,
fallbackCoordinateField,
hasTei
) => {
const main = expandField(
eventCoordinateField ?? EVENT_COORDINATE_DEFAULT,
hasTei
)
const fallback =
fallbackCoordinateField && fallbackCoordinateField !== NONE
? expandField(fallbackCoordinateField, hasTei)
: []
return [...new Set([...main, ...fallback])]
}

const GeometrySourceStyle = () => {
const dispatch = useDispatch()
const styleDataItem = useSelector((state) => state.layerEdit.styleDataItem)
const eventCoordinateField = useSelector(
(state) => state.layerEdit.eventCoordinateField
)
const fallbackCoordinateField = useSelector(
(state) => state.layerEdit.fallbackCoordinateField
)
const program = useSelector((state) => state.layerEdit.program)
const programStage = useSelector((state) => state.layerEdit.programStage)

const { eventDataItems, trackedEntityType } = useEventDataItems({
programId: program?.id,
programStageId: programStage?.id,
includeTypes: ['COORDINATE', 'ORGANISATION_UNIT'],
})

const hasTei = !!trackedEntityType?.id

const sources = getPossibleSources(
eventCoordinateField,
fallbackCoordinateField,
hasTei
)

const values = styleDataItem?.values

useEffect(() => {
if (eventDataItems === null) {
return
}
const usedColors = new Set(Object.values(values || {}))
const availableColors = qualitativeColors.filter(
(c) => !usedColors.has(c)
)
let nextColorIndex = 0
sources.forEach((sourceId) => {
if (!values?.[sourceId]) {
const color =
availableColors[nextColorIndex] ??
qualitativeColors[nextColorIndex % qualitativeColors.length]
nextColorIndex++
dispatch(setGeometrySourceStyle(sourceId, color))
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sources.join(','), eventDataItems === null, dispatch])

// Wait for event data items to load before rendering, so DE/TEA UIDs
// are never shown raw (trackedEntityType also drives sources via hasTei)
if (eventDataItems === null || !values) {
return null
}

const resolveLabel = (sourceId) => {
if (COORDINATE_FIELD_NAMES[sourceId]) {
return COORDINATE_FIELD_NAMES[sourceId]
}
const item = eventDataItems?.find((i) => i.id === sourceId)
return item?.name ?? sourceId
}

return (
<div style={style}>
{sources.map((sourceId) => (
<OptionStyle
key={sourceId}
name={resolveLabel(sourceId)}
color={values[sourceId]}
onChange={(color) =>
dispatch(setGeometrySourceStyle(sourceId, color))
}
/>
))}
</div>
)
}

export default GeometrySourceStyle
Loading
Loading