From fa00e73c5c9ed70eded36206f27e99ae247a3de7 Mon Sep 17 00:00:00 2001 From: Bruno Raimbault Date: Thu, 20 Nov 2025 18:17:09 +0100 Subject: [PATCH 1/2] chore: refactor TrackedEntityDialog to functional component --- .../trackedEntity/TrackedEntityDialog.jsx | 715 ++++++++---------- 1 file changed, 334 insertions(+), 381 deletions(-) diff --git a/src/components/edit/trackedEntity/TrackedEntityDialog.jsx b/src/components/edit/trackedEntity/TrackedEntityDialog.jsx index 754b3644b..e184ef911 100644 --- a/src/components/edit/trackedEntity/TrackedEntityDialog.jsx +++ b/src/components/edit/trackedEntity/TrackedEntityDialog.jsx @@ -1,16 +1,14 @@ import i18n from '@dhis2/d2-i18n' import { NoticeBox, IconErrorFilled24 } from '@dhis2/ui' import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { Component, Fragment } from 'react' -import { connect } from 'react-redux' +import React, { useState, useEffect, useCallback, Fragment } from 'react' +import { useSelector, useDispatch } from 'react-redux' import { setTrackedEntityType, setProgram, setProgramStatus, setFollowUpStatus, setTrackedEntityRelationshipType, - setTrackedEntityRelationshipOutsideProgram, setPeriodType, setStartDate, setEndDate, @@ -20,7 +18,6 @@ import { setRelatedPointColor, setRelatedPointRadius, setRelationshipLineColor, - setStyleDataItem, } from '../../../actions/layerEdit.js' import { TEI_COLOR, @@ -53,377 +50,128 @@ import PeriodTypeSelect from './PeriodTypeSelect.jsx' import ProgramStatusSelect from './ProgramStatusSelect.jsx' import TrackedEntityRelationshipTypeSelect from './TrackedEntityRelationshipTypeSelect.jsx' -class TrackedEntityDialog extends Component { - static propTypes = { - setEndDate: PropTypes.func.isRequired, - setEventPointColor: PropTypes.func.isRequired, - setEventPointRadius: PropTypes.func.isRequired, - setFollowUpStatus: PropTypes.func.isRequired, - setOrgUnits: PropTypes.func.isRequired, - setPeriodType: PropTypes.func.isRequired, - setProgram: PropTypes.func.isRequired, - setProgramStatus: PropTypes.func.isRequired, - setRelatedPointColor: PropTypes.func.isRequired, - setRelatedPointRadius: PropTypes.func.isRequired, - setRelationshipLineColor: PropTypes.func.isRequired, - setStartDate: PropTypes.func.isRequired, - setTrackedEntityRelationshipType: PropTypes.func.isRequired, - setTrackedEntityType: PropTypes.func.isRequired, - validateLayer: PropTypes.bool.isRequired, - onLayerValidation: PropTypes.func.isRequired, - endDate: PropTypes.string, - eventPointColor: PropTypes.string, - eventPointRadius: PropTypes.number, - followUp: PropTypes.bool, - orgUnits: PropTypes.object, - periodType: PropTypes.string, - periodsSettings: PropTypes.object, - program: PropTypes.object, - programStatus: PropTypes.string, - relatedPointColor: PropTypes.string, - relatedPointRadius: PropTypes.number, - relationshipLineColor: PropTypes.string, - relationshipType: PropTypes.string, - rows: PropTypes.array, - startDate: PropTypes.string, - trackedEntityType: PropTypes.object, - } +const TrackedEntityDialog = () => { + const dispatch = useDispatch() - constructor(props, context) { - super(props, context) - this.state = { - tab: 'data', - showRelationshipsChecked: false, - } - } + // -------------------------- + // Redux state + // -------------------------- + const { + startDate, + endDate, + trackedEntityType, + program, + programStatus, + followUp, + relationshipType, + periodType, + rows, + orgUnits, + eventPointColor, + eventPointRadius, + relatedPointColor, + relatedPointRadius, + relationshipLineColor, + periodsSettings, + validateLayer, + onLayerValidation, + } = useSelector((state) => ({ + startDate: state.layerEdit.startDate, + endDate: state.layerEdit.endDate, + trackedEntityType: state.layerEdit.trackedEntityType, + program: state.layerEdit.program, + programStatus: state.layerEdit.programStatus, + followUp: state.layerEdit.followUp, + relationshipType: state.layerEdit.relationshipType, + periodType: state.layerEdit.periodType, + rows: state.layerEdit.rows, + orgUnits: state.layerEdit.orgUnits, + eventPointColor: state.layerEdit.eventPointColor, + eventPointRadius: state.layerEdit.eventPointRadius, + relatedPointColor: state.layerEdit.relatedPointColor, + relatedPointRadius: state.layerEdit.relatedPointRadius, + relationshipLineColor: state.layerEdit.relationshipLineColor, + periodsSettings: state.layerEdit.periodsSettings, + validateLayer: state.layerEdit.validateLayer, + onLayerValidation: state.layerEdit.onLayerValidation, + })) - componentDidMount() { - const { - rows, - startDate, - endDate, - relationshipType, - orgUnits, - setStartDate, - setEndDate, - setOrgUnits, - } = this.props + // -------------------------- + // Local state + // -------------------------- + const [tab, setTab] = useState('data') + const [showRelationshipsChecked, setShowRelationshipsChecked] = + useState(false) + const [trackedEntityTypeError, setTrackedEntityTypeError] = useState(null) + const [orgUnitsError, setOrgUnitsError] = useState(null) + const [periodError, setPeriodError] = useState(null) + // -------------------------- + // Lifecycle: componentDidMount + // -------------------------- + useEffect(() => { const hasDate = startDate !== undefined && endDate !== undefined - // Set default period (last year) + // Set default period if missing if (!hasDate) { const defaultDates = getDefaultDatesInCalendar() - setStartDate(defaultDates.startDate) - setEndDate(defaultDates.endDate) + dispatch(setStartDate(defaultDates.startDate)) + dispatch(setEndDate(defaultDates.endDate)) } if (relationshipType) { - this.setState({ - showRelationshipsChecked: true, - }) + setShowRelationshipsChecked(true) } - // Set org unit tree roots as default - if (!rows && orgUnits.roots) { - setOrgUnits({ - dimension: 'ou', - items: orgUnits.roots, - }) + // Set default org units if missing + if (!rows && orgUnits?.roots) { + dispatch(setOrgUnits({ dimension: 'ou', items: orgUnits.roots })) } - } - - componentDidUpdate(prev) { - const { - validateLayer, - onLayerValidation, - startDate, - endDate, - program, - setPeriodType, - } = this.props - const { periodError } = this.state + }, [dispatch, endDate, orgUnits.roots, relationshipType, rows, startDate]) - if (validateLayer && validateLayer !== prev.validateLayer) { - onLayerValidation(this.validate()) + // -------------------------- + // Lifecycle: componentDidUpdate + // -------------------------- + useEffect(() => { + if (validateLayer) { + onLayerValidation(validate()) } + }, [validateLayer, validate, onLayerValidation]) - if ( - periodError && - (startDate !== prev.startDate || endDate !== prev.endDate) - ) { - this.setErrorState('periodError', null, 'period') + useEffect(() => { + if (periodError && startDate && endDate) { + setPeriodError(null) } + }, [periodError, startDate, endDate]) - if (!program && prev.program !== program) { - setPeriodType(LAST_UPDATED_DATES) + useEffect(() => { + if (!program) { + dispatch(setPeriodType(LAST_UPDATED_DATES)) } - } - - render() { - const { - eventPointColor, - eventPointRadius, - followUp, - periodType, - program, - programStatus, - trackedEntityType, - relationshipType, - relatedPointColor, - relatedPointRadius, - relationshipLineColor, - periodsSettings, - } = this.props - - const { - setTrackedEntityType, - setPeriodType, - setProgram, - setProgramStatus, - setFollowUpStatus, - setTrackedEntityRelationshipType, - setEventPointColor, - setEventPointRadius, - setRelatedPointColor, - setRelatedPointRadius, - setRelationshipLineColor, - } = this.props - - const { tab, trackedEntityTypeError, orgUnitsError, periodError } = - this.state - - return ( -
- this.setState({ tab })}> - {i18n.t('Data')} - {i18n.t('Relationships')} - {i18n.t('Period')} - {i18n.t('Org Units')} - {i18n.t('Style')} - -
- {tab === 'data' && ( -
- - {trackedEntityType && ( - - )} - {program && ( - - )} - {program && ( - - )} -
- )} - {tab === 'relationships' && - (!this.props.trackedEntityType ? ( -
- {i18n.t( - 'Please select a Tracked Entity Type before selecting a Relationship Type' - )} -
- ) : ( -
-
- - {i18n.t( - 'Displaying tracked entity relationships in Maps is an experimental feature' - )} - -
- { - if (!checked) { - setTrackedEntityRelationshipType( - null - ) - } - this.setState({ - showRelationshipsChecked: checked, - }) - }} - style={{ - marginBottom: 0, - }} - /> - {this.state.showRelationshipsChecked && ( - - - - )} -
- ))} - {tab === 'period' && ( -
- - - {periodError && ( -
- - {periodError} -
- )} -
- )} - {tab === 'orgunits' && ( - - )} - {tab === 'style' && ( -
-
-
- {i18n.t('Tracked entity style')}: -
-
- - -
- - - {relationshipType ? ( - -
- {i18n.t('Related entity style')}: -
-
- - - -
-
- ) : null} -
-
-
- )} -
-
- ) - } - - // TODO: Add to parent class? - setErrorState(key, message, tab) { - this.setState({ - [key]: message, - tab, - }) + }, [program, dispatch]) + // -------------------------- + // Validation + // -------------------------- + const setErrorState = useCallback((key, message, tabName) => { + switch (key) { + case 'trackedEntityTypeError': + setTrackedEntityTypeError(message) + break + case 'orgUnitsError': + setOrgUnitsError(message) + break + case 'periodError': + setPeriodError(message) + break + } + setTab(tabName) return false - } - - validate() { - const { trackedEntityType, rows, startDate, endDate } = this.props + }, []) + const validate = useCallback(() => { if (!trackedEntityType) { - return this.setErrorState( + return setErrorState( 'trackedEntityTypeError', i18n.t('Tracked Entity Type is required'), 'data' @@ -432,11 +180,11 @@ class TrackedEntityDialog extends Component { const error = getStartEndDateError(startDate, endDate) if (error) { - return this.setErrorState('periodError', error, 'period') + return setErrorState('periodError', error, 'period') } if (!getOrgUnitsFromRows(rows).length) { - return this.setErrorState( + return setErrorState( 'orgUnitsError', i18n.t('No organisation units are selected.'), 'orgunits' @@ -444,31 +192,236 @@ class TrackedEntityDialog extends Component { } return true - } + }, [trackedEntityType, startDate, endDate, rows, setErrorState]) + + // -------------------------- + // Render + // -------------------------- + return ( +
+ + {i18n.t('Data')} + {i18n.t('Relationships')} + {i18n.t('Period')} + {i18n.t('Org Units')} + {i18n.t('Style')} + + +
+ {tab === 'data' && ( +
+ + dispatch(setTrackedEntityType(val)) + } + className={styles.select} + errorText={trackedEntityTypeError} + /> + {trackedEntityType && ( + dispatch(setProgram(val))} + className={styles.select} + /> + )} + {program && ( + + dispatch(setProgramStatus(val)) + } + className={styles.select} + /> + )} + {program && ( + + dispatch(setFollowUpStatus(val)) + } + /> + )} +
+ )} + + {tab === 'relationships' && + (!trackedEntityType ? ( +
+ {i18n.t( + 'Please select a Tracked Entity Type before selecting a Relationship Type' + )} +
+ ) : ( +
+
+ + {i18n.t( + 'Displaying tracked entity relationships in Maps is an experimental feature' + )} + +
+ { + if (!checked) { + dispatch( + setTrackedEntityRelationshipType( + null + ) + ) + } + setShowRelationshipsChecked(checked) + }} + style={{ marginBottom: 0 }} + /> + {showRelationshipsChecked && ( + + dispatch( + setTrackedEntityRelationshipType( + val + ) + ) + } + className={cx(styles.select, styles.indent)} + /> + )} +
+ ))} + + {tab === 'period' && ( +
+ dispatch(setPeriodType(val))} + /> + + dispatch(setStartDate(val)) + } + onSelectEndDate={(val) => dispatch(setEndDate(val))} + periodsSettings={periodsSettings} + /> + {periodError && ( +
+ + {periodError} +
+ )} +
+ )} + + {tab === 'orgunits' && ( + + )} + + {tab === 'style' && ( +
+
+
+ {i18n.t('Tracked entity style')}: +
+
+ + dispatch(setEventPointColor(val)) + } + className={styles.flexInnerColumn} + /> + + dispatch(setEventPointRadius(val)) + } + className={styles.flexInnerColumn} + /> +
+ + + {relationshipType && ( + +
+ {i18n.t('Related entity style')}: +
+
+ + dispatch( + setRelatedPointColor(val) + ) + } + className={styles.flexInnerColumn} + /> + + dispatch( + setRelatedPointRadius(val) + ) + } + className={styles.flexInnerColumn} + /> + + dispatch( + setRelationshipLineColor( + val + ) + ) + } + className={styles.flexInnerColumn} + /> +
+
+ )} +
+
+ )} +
+
+ ) } -export default connect( - null, - { - setTrackedEntityType, - setProgram, - setProgramStatus, - setFollowUpStatus, - setTrackedEntityRelationshipType, - setTrackedEntityRelationshipOutsideProgram, - setPeriodType, - setStartDate, - setEndDate, - setOrgUnits, - setEventPointColor, - setEventPointRadius, - setRelatedPointColor, - setRelatedPointRadius, - setRelationshipLineColor, - setStyleDataItem, - }, - null, - { - forwardRef: true, - } -)(TrackedEntityDialog) +export default TrackedEntityDialog From 7479520acf7f8d0d63f33ade225b67219de49e7d Mon Sep 17 00:00:00 2001 From: Bruno Raimbault Date: Thu, 20 Nov 2025 18:41:40 +0100 Subject: [PATCH 2/2] fix: update TrackedEntityDialog --- .../edit/trackedEntity/TrackedEntityDialog.jsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/edit/trackedEntity/TrackedEntityDialog.jsx b/src/components/edit/trackedEntity/TrackedEntityDialog.jsx index e184ef911..056996639 100644 --- a/src/components/edit/trackedEntity/TrackedEntityDialog.jsx +++ b/src/components/edit/trackedEntity/TrackedEntityDialog.jsx @@ -127,17 +127,11 @@ const TrackedEntityDialog = () => { if (!rows && orgUnits?.roots) { dispatch(setOrgUnits({ dimension: 'ou', items: orgUnits.roots })) } - }, [dispatch, endDate, orgUnits.roots, relationshipType, rows, startDate]) + }, [dispatch, endDate, orgUnits?.roots, relationshipType, rows, startDate]) // -------------------------- // Lifecycle: componentDidUpdate // -------------------------- - useEffect(() => { - if (validateLayer) { - onLayerValidation(validate()) - } - }, [validateLayer, validate, onLayerValidation]) - useEffect(() => { if (periodError && startDate && endDate) { setPeriodError(null) @@ -194,6 +188,15 @@ const TrackedEntityDialog = () => { return true }, [trackedEntityType, startDate, endDate, rows, setErrorState]) + // -------------------------- + // Lifecycle: componentDidUpdate + // -------------------------- + useEffect(() => { + if (validateLayer) { + onLayerValidation(validate()) + } + }, [validateLayer, validate, onLayerValidation]) + // -------------------------- // Render // --------------------------