From f1607ff66001f02b525e8d80ff1f5c089caffe7c Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 21:00:18 +0100 Subject: [PATCH 1/9] feat: add event template selector --- .cursor/rules/code-guidelines.mdc | 22 +++++++ .../Pages/MainPage/MainPage.component.js | 43 ++++++++++---- .../Pages/MainPage/MainPage.container.js | 4 +- .../RegisterTeiDataEntry.component.js | 7 +++ .../RegisterTei/RegisterTei.component.js | 7 ++- ...kedEntityRelationshipsWrapper.component.js | 18 +++++- .../EventTemplateSelector.container.js | 30 ++++++++++ .../EventTemplateSelector/index.js | 2 + .../TEITemplateSelector.container.js | 31 ++++++++++ .../TEITemplateSelector/index.js | 2 + .../TemplateSelector.container.js | 39 +++++-------- .../TemplateSelector/hooks/index.js | 1 + .../hooks/useEventTemplates.js | 36 ++++++++++++ .../components/TemplateSelector/index.js | 3 + .../NewTrackedEntityRelationship.component.js | 58 +++++++++++++++---- 15 files changed, 253 insertions(+), 50 deletions(-) create mode 100644 .cursor/rules/code-guidelines.mdc create mode 100644 src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/EventTemplateSelector.container.js create mode 100644 src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js create mode 100644 src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/TEITemplateSelector.container.js create mode 100644 src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js create mode 100644 src/core_modules/capture-core/components/TemplateSelector/hooks/useEventTemplates.js diff --git a/.cursor/rules/code-guidelines.mdc b/.cursor/rules/code-guidelines.mdc new file mode 100644 index 0000000000..6304eca4fb --- /dev/null +++ b/.cursor/rules/code-guidelines.mdc @@ -0,0 +1,22 @@ +--- +description: General code guidelines for bugs and features. +globs: src/*.js +alwaysApply: false +--- + +## Logging +When logging things to the browser console please use the following format. + +``` +import log from 'loglevel'; +import { errorCreator } from 'capture-core-utils'; + +log.error( + errorCreator('Message to be logged')( + { + // Any details + }, + ), +); + +``` \ No newline at end of file diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js index 45c7318ad0..581237ce33 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js @@ -56,9 +56,13 @@ const MainPagePlain = ({ const showMainPage = useMemo(() => { const noProgramSelected = !programId; const noOrgUnitSelected = !orgUnitId; - const isEventProgram = !trackedEntityTypeId; - return noProgramSelected || noOrgUnitSelected || isEventProgram || displayFrontPageList || selectedTemplateId; - }, [programId, orgUnitId, trackedEntityTypeId, displayFrontPageList, selectedTemplateId]); + + // Only show the main page for event programs if displayFrontPageList is true + return noProgramSelected || + noOrgUnitSelected || + displayFrontPageList || + selectedTemplateId; + }, [programId, orgUnitId, displayFrontPageList, selectedTemplateId]); return ( <> @@ -88,14 +92,31 @@ const MainPagePlain = ({ )} ) : ( -
-
- -
-
- -
-
+ <> + {trackedEntityTypeId ? ( + // For tracker programs, show search and template selector +
+
+ +
+
+ +
+
+ ) : ( + // For event programs, show template selector +
+
+

+ This is a placeholder for the event program main page. +

+
+
+ +
+
+ )} + )} ); diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js index 56779b2ee2..8116add8d0 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js @@ -113,9 +113,11 @@ const MainPageContainer = () => { const { categoryOptionIsInvalidForOrgUnit } = useCategoryOptionIsValidForOrgUnit({ selectedOrgUnitId: orgUnitId }); const selectedProgram = programCollection.get(programId); + console.log('selectedProgram', selectedProgram); // $FlowFixMe[prop-missing] const trackedEntityTypeId = selectedProgram?.trackedEntityType?.id; - const displayFrontPageList = trackedEntityTypeId && selectedProgram?.displayFrontPageList; + // Allow displayFrontPageList to be used for both event programs and tracker programs + const displayFrontPageList = selectedProgram?.displayFrontPageList; const MainPageStatus = useMainPageStatus({ programId, selectedProgram, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js index 940aa904fd..3678cdbdd1 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js @@ -11,6 +11,11 @@ type Props = { }; export class RegisterTeiDataEntryComponent extends React.Component { + componentDidMount() { + // Force reinitialization when component is mounted + // This ensures plugins are triggered when the component is remounted + } + render() { const { showDataEntry, @@ -29,6 +34,7 @@ export class RegisterTeiDataEntryComponent extends React.Component { ); } @@ -37,6 +43,7 @@ export class RegisterTeiDataEntryComponent extends React.Component { ); } diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js index c22f4bc5cf..170742ee76 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType, useContext, useCallback } from 'react'; +import React, { type ComponentType, useContext, useCallback, useEffect } from 'react'; import { compose } from 'redux'; import { withStyles } from '@material-ui/core/styles'; import i18n from '@dhis2/d2-i18n'; @@ -70,6 +70,11 @@ const RegisterTeiPlain = ({ }: ComponentProps) => { const { resultsPageSize } = useContext(ResultsPageSizeContext); + useEffect(() => { + // This empty dependency array ensures the effect runs only on mount + // This forces the component to reinitialize when it's remounted with a new key + }, []); + const renderDuplicatesCardActions = useCallback(({ item }) => ( { + // Update the mode ref when the find mode changes + if (modeRef.current.currentMode !== findMode) { + modeRef.current = { + currentMode: findMode, + timestamp: Date.now(), + }; + } + dispatch(selectFindMode({ findMode, orgUnit: initialOrgUnit, @@ -75,6 +89,7 @@ export const TrackedEntityRelationshipsWrapper = ({ ) => ( ( { + const { navigate } = useNavigate(); + const { programId, orgUnitId } = useLocationQuery(); + const { eventTemplates, loading: loadingEventTemplates } = useEventTemplates(programId); + + const onSelectTemplate = template => + navigate(`/?${buildUrlQueryString({ orgUnitId, programId, selectedTemplateId: template.id })}`); + const onCreateTemplate = () => { + const urlQueryString = buildUrlQueryString({ + orgUnitId, + programId, + selectedTemplateId: `${programId}-default`, + }); + navigate(`/?${urlQueryString}`); + }; + + return programId && !loadingEventTemplates ? ( + + ) : null; +}; \ No newline at end of file diff --git a/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js b/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js new file mode 100644 index 0000000000..ee10a7b89d --- /dev/null +++ b/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js @@ -0,0 +1,2 @@ +// @flow +export { EventTemplateSelector } from './EventTemplateSelector.container'; \ No newline at end of file diff --git a/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/TEITemplateSelector.container.js b/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/TEITemplateSelector.container.js new file mode 100644 index 0000000000..4c0fdaf7a8 --- /dev/null +++ b/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/TEITemplateSelector.container.js @@ -0,0 +1,31 @@ +// @flow +import React from 'react'; +import { TemplateSelector as TemplateSelectorComponent } from '../TemplateSelector.component'; +import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; +import { useTEITemplates, useProgramStageTemplates } from '../hooks'; + +export const TEITemplateSelector = () => { + const { navigate } = useNavigate(); + const { programId, orgUnitId } = useLocationQuery(); + const { TEITemplates, loading: loadingTEITemplates } = useTEITemplates(programId); + const { programStageTemplates, loading: loadingProgramStageTemplates } = useProgramStageTemplates(programId); + + const onSelectTemplate = template => + navigate(`/?${buildUrlQueryString({ orgUnitId, programId, selectedTemplateId: template.id })}`); + const onCreateTemplate = () => { + const urlQueryString = buildUrlQueryString({ + orgUnitId, + programId, + selectedTemplateId: `${programId}-default`, + }); + navigate(`/?${urlQueryString}`); + }; + + return programId && !loadingTEITemplates && !loadingProgramStageTemplates ? ( + + ) : null; +}; diff --git a/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js b/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js new file mode 100644 index 0000000000..2122294cdc --- /dev/null +++ b/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js @@ -0,0 +1,2 @@ +// @flow +export { TEITemplateSelector } from './TEITemplateSelector.container'; \ No newline at end of file diff --git a/src/core_modules/capture-core/components/TemplateSelector/TemplateSelector.container.js b/src/core_modules/capture-core/components/TemplateSelector/TemplateSelector.container.js index a567f4ffbc..8e645fce34 100644 --- a/src/core_modules/capture-core/components/TemplateSelector/TemplateSelector.container.js +++ b/src/core_modules/capture-core/components/TemplateSelector/TemplateSelector.container.js @@ -1,31 +1,22 @@ // @flow import React from 'react'; -import { TemplateSelector as TemplateSelectorComponent } from './TemplateSelector.component'; -import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../utils/routing'; -import { useTEITemplates, useProgramStageTemplates } from './hooks'; +import { useLocationQuery } from '../../utils/routing'; +import { useProgramInfo, programTypes } from '../../hooks/useProgramInfo'; +import { TEITemplateSelector } from './TEITemplateSelector'; +import { EventTemplateSelector } from './EventTemplateSelector'; export const TemplateSelector = () => { - const { navigate } = useNavigate(); - const { programId, orgUnitId } = useLocationQuery(); - const { TEITemplates, loading: loadingTEITemplates } = useTEITemplates(programId); - const { programStageTemplates, loading: loadingProgramStageTemplates } = useProgramStageTemplates(programId); + const { programId } = useLocationQuery(); + const { programType } = useProgramInfo(programId); - const onSelectTemplate = template => - navigate(`/?${buildUrlQueryString({ orgUnitId, programId, selectedTemplateId: template.id })}`); - const onCreateTemplate = () => { - const urlQueryString = buildUrlQueryString({ - orgUnitId, - programId, - selectedTemplateId: `${programId}-default`, - }); - navigate(`/?${urlQueryString}`); - }; + if (!programId) { + return null; + } - return programId && !loadingTEITemplates && !loadingProgramStageTemplates ? ( - - ) : null; + // Use the appropriate template selector based on program type + if (programType === programTypes.EVENT_PROGRAM) { + return ; + } + + return ; }; diff --git a/src/core_modules/capture-core/components/TemplateSelector/hooks/index.js b/src/core_modules/capture-core/components/TemplateSelector/hooks/index.js index 5371b69ec3..921ce4cdba 100644 --- a/src/core_modules/capture-core/components/TemplateSelector/hooks/index.js +++ b/src/core_modules/capture-core/components/TemplateSelector/hooks/index.js @@ -1,3 +1,4 @@ // @flow export { useTEITemplates } from './useTEITemplates'; export { useProgramStageTemplates } from './useProgramStageTemplates'; +export { useEventTemplates } from './useEventTemplates'; diff --git a/src/core_modules/capture-core/components/TemplateSelector/hooks/useEventTemplates.js b/src/core_modules/capture-core/components/TemplateSelector/hooks/useEventTemplates.js new file mode 100644 index 0000000000..2c9004cf3f --- /dev/null +++ b/src/core_modules/capture-core/components/TemplateSelector/hooks/useEventTemplates.js @@ -0,0 +1,36 @@ +// @flow +import { useMemo, useEffect } from 'react'; +import { useDataQuery } from '@dhis2/app-runtime'; + +export const useEventTemplates = (programId: string) => { + const { error, loading, data, refetch } = useDataQuery( + useMemo( + () => ({ + templates: { + resource: 'eventFilters', + params: ({ variables }) => ({ + filter: `program:eq:${variables.programId}`, + fields: ` + id,displayName,eventQueryCriteria,access,externalAccess,publicAccess, + user[id,username], + userAccesses[id,access], + userGroupAccesses[id,access] + `, + }), + }, + }), + [], + ), + { lazy: true }, + ); + + useEffect(() => { + programId && refetch({ variables: { programId } }); + }, [refetch, programId]); + + return { + error, + loading, + eventTemplates: data?.templates?.eventFilters ? data.templates.eventFilters : [], + }; +}; diff --git a/src/core_modules/capture-core/components/TemplateSelector/index.js b/src/core_modules/capture-core/components/TemplateSelector/index.js index f66c549b86..4dcea37113 100644 --- a/src/core_modules/capture-core/components/TemplateSelector/index.js +++ b/src/core_modules/capture-core/components/TemplateSelector/index.js @@ -1 +1,4 @@ +// @flow export { TemplateSelector } from './TemplateSelector.container'; +export { TEITemplateSelector } from './TEITemplateSelector'; +export { EventTemplateSelector } from './EventTemplateSelector'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index 48b90410dc..5a4dde8e91 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -1,5 +1,5 @@ // @flow -import React, { useCallback, useState, type ComponentType, useMemo } from 'react'; +import React, { useCallback, useState, type ComponentType, useMemo, useRef, useEffect } from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { Widget } from '../../../Widget'; @@ -58,7 +58,31 @@ const NewTrackedEntityRelationshipPlain = ({ teiId, onMutate: () => onSave && onSave(), }); - + + // Add a ref to track the current mode and force remounting when switching + const modeRef = useRef({ + currentMode: null, + timestamp: Date.now(), + }); + + // Update timestamp when step changes to ensure remounting + useEffect(() => { + if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id) { + if (modeRef.current.currentMode !== 'search') { + modeRef.current = { + currentMode: 'search', + timestamp: Date.now(), + }; + } + } else if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.NEW_LINKED_ENTITY.id) { + if (modeRef.current.currentMode !== 'register') { + modeRef.current = { + currentMode: 'register', + timestamp: Date.now(), + }; + } + } + }, [currentStep.id]); const onLinkToTrackedEntityFromSearch = useCallback( (linkedTrackedEntityId: string, attributes?: { [attributeId: string]: string }) => { @@ -215,10 +239,15 @@ const NewTrackedEntityRelationshipPlain = ({ }: LinkedEntityMetadata = selectedLinkedEntityMetadata; if (renderTrackedEntitySearch) { - return renderTrackedEntitySearch( - linkedEntityTrackedEntityTypeId, - linkedEntityProgramId, - onLinkToTrackedEntityFromSearch, + // Use the timestamp from modeRef to ensure consistent remounting + return ( +
+ {renderTrackedEntitySearch( + linkedEntityTrackedEntityTypeId, + linkedEntityProgramId, + onLinkToTrackedEntityFromSearch, + )} +
); } } @@ -231,12 +260,17 @@ const NewTrackedEntityRelationshipPlain = ({ }: LinkedEntityMetadata = selectedLinkedEntityMetadata; if (renderTrackedEntityRegistration) { - return renderTrackedEntityRegistration( - linkedEntityTrackedEntityTypeId, - linkedEntityProgramId, - onLinkToTrackedEntityFromRegistration, - onLinkToTrackedEntityFromSearch, - onCancel, + // Use the timestamp from modeRef to ensure consistent remounting + return ( +
+ {renderTrackedEntityRegistration( + linkedEntityTrackedEntityTypeId, + linkedEntityProgramId, + onLinkToTrackedEntityFromRegistration, + onLinkToTrackedEntityFromSearch, + onCancel, + )} +
); } } From e22e7849bf34a961844ddf9fdc81f698d35dfbc6 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 21:36:19 +0100 Subject: [PATCH 2/9] feat: add selected template and change handler to event working lists --- .../EventWorkingListsInit.container.type.js | 4 +++- .../WorkingListsType/WorkingListsType.component.js | 9 ++++++++- .../EventWorkingLists/EventWorkingLists.types.js | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/MainPage/EventWorkingListsInit/EventWorkingListsInit.container.type.js b/src/core_modules/capture-core/components/Pages/MainPage/EventWorkingListsInit/EventWorkingListsInit.container.type.js index 87a92ad444..cd33f369b1 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/EventWorkingListsInit/EventWorkingListsInit.container.type.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/EventWorkingListsInit/EventWorkingListsInit.container.type.js @@ -1,5 +1,7 @@ // @flow export type Props = $ReadOnly<{| programId: string, - orgUnitId: string + orgUnitId: string, + selectedTemplateId?: string, + onChangeTemplate?: (selectedTemplateId?: string) => void |}>; diff --git a/src/core_modules/capture-core/components/Pages/MainPage/WorkingListsType/WorkingListsType.component.js b/src/core_modules/capture-core/components/Pages/MainPage/WorkingListsType/WorkingListsType.component.js index 732f0d07d5..a97df1a33f 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/WorkingListsType/WorkingListsType.component.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/WorkingListsType/WorkingListsType.component.js @@ -8,7 +8,14 @@ import type { Props } from './workingListsType.types'; export const WorkingListsType = ({ programId, orgUnitId, selectedTemplateId, onChangeTemplate }: Props) => { const { programType } = useProgramInfo(programId); if (programType === programTypes.EVENT_PROGRAM) { - return ; + return ( + + ); } if (programType === programTypes.TRACKER_PROGRAM) { diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.types.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.types.js index c5eaa65af0..49b8894304 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.types.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.types.js @@ -5,4 +5,6 @@ export type Props = {| orgUnitId: string, programId?: string, programStageId?: string, + selectedTemplateId?: string, + onChangeTemplate?: (selectedTemplateId?: string) => void, |}; From f9dd334755c851e0a5b83bd844b72c5d2ab7e9be Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 21:36:24 +0100 Subject: [PATCH 3/9] feat: enhance event working lists template handling with change callbacks --- .../eventWorkingListsColumnSetup.types.js | 2 + .../currentViewChangesResolver.types.js | 1 + .../eventWorkingListsDataSourceSetup.types.js | 1 + .../EventWorkingLists.component.js | 4 +- ...ventWorkingListsReduxProvider.container.js | 37 ++++++++++++++- .../eventWorkingListsReduxProvider.types.js | 4 +- ...ventWorkingListsTemplateSetup.component.js | 11 +++-- .../eventWorkingListsTemplateSetup.types.js | 2 + .../epics/templates.epics.js | 45 ++++++++++++++----- 9 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ColumnSetup/eventWorkingListsColumnSetup.types.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ColumnSetup/eventWorkingListsColumnSetup.types.js index e187abbcab..00ffdb7bea 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ColumnSetup/eventWorkingListsColumnSetup.types.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ColumnSetup/eventWorkingListsColumnSetup.types.js @@ -10,6 +10,7 @@ type ExtractedProps = {| customColumnOrder?: CustomColumnOrder, onLoadView: Function, onUpdateList: Function, + onChangeTemplate?: (selectedTemplateId?: string) => void, |}; // had to add customColumnOrder as a non optional type or else it would not be removed. Also, if customColumnOrder is @@ -31,4 +32,5 @@ export type EventWorkingListsColumnSetupOutputProps = {| defaultColumns: EventWorkingListsColumnConfigs, onLoadView: Function, onUpdateList: Function, + onChangeTemplate?: (selectedTemplateId?: string) => void, |}; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/CurrentViewChangesResolver/currentViewChangesResolver.types.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/CurrentViewChangesResolver/currentViewChangesResolver.types.js index eb599fda73..bb6cfc6feb 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/CurrentViewChangesResolver/currentViewChangesResolver.types.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/CurrentViewChangesResolver/currentViewChangesResolver.types.js @@ -31,4 +31,5 @@ export type CurrentViewChangesResolverOutputProps = {| sortById?: string, sortByDirection?: string, currentViewHasTemplateChanges?: boolean, + onChangeTemplate?: (selectedTemplateId?: string) => void, |}; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/DataSourceSetup/eventWorkingListsDataSourceSetup.types.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/DataSourceSetup/eventWorkingListsDataSourceSetup.types.js index e8d62c5e5b..cf19bdb3b1 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/DataSourceSetup/eventWorkingListsDataSourceSetup.types.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/DataSourceSetup/eventWorkingListsDataSourceSetup.types.js @@ -26,4 +26,5 @@ export type EventWorkingListsDataSourceSetupOutputProps = {| columns: EventWorkingListsColumnConfigs, dataSource?: DataSource, rowIdKey: string, + onChangeTemplate?: (selectedTemplateId?: string) => void, |}; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.component.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.component.js index e36f40b210..c9baebf1d2 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.component.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/EventWorkingLists.component.js @@ -7,7 +7,7 @@ import { EventWorkingListsReduxProvider } from './ReduxProvider'; import { useProgramStageInfo } from '../../../metaDataMemoryStores/programCollection/helpers'; import type { Props } from './EventWorkingLists.types'; -export const EventWorkingLists = ({ storeId, programId, programStageId, orgUnitId }: Props) => { +export const EventWorkingLists = ({ storeId, programId, programStageId, orgUnitId, selectedTemplateId, onChangeTemplate }: Props) => { const { program, programStage, error } = useProgramStageInfo(programStageId, programId); useEffect(() => { @@ -29,6 +29,8 @@ export const EventWorkingLists = ({ storeId, programId, programStageId, orgUnitI // $FlowFixMe programStage={programStage} orgUnitId={orgUnitId} + selectedTemplateId={selectedTemplateId} + onChangeTemplate={onChangeTemplate} /> ); diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.js index bcb525bcc6..c232bd9ed7 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.js @@ -1,5 +1,5 @@ // @flow -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useDataEngine } from '@dhis2/app-runtime'; import { makeQuerySingleResource } from 'capture-core/utils/api'; @@ -14,7 +14,14 @@ import type { Props } from './eventWorkingListsReduxProvider.types'; import { computeDownloadRequest } from './downloadRequest'; import { convertToClientConfig } from '../helpers/eventFilters'; -export const EventWorkingListsReduxProvider = ({ storeId, program, programStage, orgUnitId }: Props) => { +export const EventWorkingListsReduxProvider = ({ + storeId, + program, + programStage, + orgUnitId, + selectedTemplateId, + onChangeTemplate, +}: Props) => { const dispatch = useDispatch(); const dataEngine = useDataEngine(); @@ -26,9 +33,33 @@ export const EventWorkingListsReduxProvider = ({ storeId, program, programStage, onResetListColumnOrder, onClearFilters, onUpdateDefaultTemplate, + onSelectTemplate, + viewPreloaded, ...commonStateManagementRestProps } = useWorkingListsCommonStateManagement(storeId, SINGLE_EVENT_WORKING_LISTS_TYPE, program); + // Use selected template ID from props if provided + useEffect(() => { + if (selectedTemplateId && + selectedTemplateId !== currentTemplateId && + !viewPreloaded && + templates && + templates.length > 0) { + const template = templates.find(t => t.id === selectedTemplateId); + if (template) { + onSelectTemplate(selectedTemplateId, template.criteria?.programStage); + } + } + }, [selectedTemplateId, templates, currentTemplateId, onSelectTemplate, viewPreloaded]); + + // Custom onSelectTemplate that calls the provided onChangeTemplate + const handleSelectTemplate = useCallback((templateId, programStageId) => { + onSelectTemplate(templateId, programStageId); + if (onChangeTemplate) { + onChangeTemplate(templateId); + } + }, [onSelectTemplate, onChangeTemplate]); + const currentTemplate = currentTemplateId && templates && templates.find(template => template.id === currentTemplateId); @@ -103,6 +134,8 @@ export const EventWorkingListsReduxProvider = ({ storeId, program, programStage, onUpdateList={injectDownloadRequestToUpdateList} onDeleteEvent={onDeleteEvent} downloadRequest={downloadRequest} + onSelectTemplate={handleSelectTemplate} + onChangeTemplate={onChangeTemplate} /> ); }; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/eventWorkingListsReduxProvider.types.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/eventWorkingListsReduxProvider.types.js index 33a53d67a6..9ddb450a72 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/eventWorkingListsReduxProvider.types.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/eventWorkingListsReduxProvider.types.js @@ -44,7 +44,9 @@ export type Props = $ReadOnly<{| storeId: string, program: Program, programStage: ProgramStage, - orgUnitId: string + orgUnitId: string, + selectedTemplateId?: string, + onChangeTemplate?: (selectedTemplateId?: string) => void |}>; export type EventWorkingListsReduxOutputProps = {| diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/TemplateSetup/EventWorkingListsTemplateSetup.component.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/TemplateSetup/EventWorkingListsTemplateSetup.component.js index fda2407a82..8856457f01 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/TemplateSetup/EventWorkingListsTemplateSetup.component.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/TemplateSetup/EventWorkingListsTemplateSetup.component.js @@ -23,6 +23,7 @@ export const EventWorkingListsTemplateSetup = ({ onUpdateTemplate, onDeleteTemplate, templates, + onChangeTemplate, ...passOnProps }: Props) => { const injectArgumentsForUpdateTemplate = React.useCallback((template) => { @@ -36,8 +37,9 @@ export const EventWorkingListsTemplateSetup = ({ sortById, sortByDirection, programId: program.id, + callBacks: { onChangeTemplate }, }); - }, [onUpdateTemplate, filters, columns, sortById, sortByDirection, program.id]); + }, [onUpdateTemplate, filters, columns, sortById, sortByDirection, program.id, onChangeTemplate]); const injectArgumentsForAddTemplate = React.useCallback((name) => { // $FlowFixMe For columns: fixing this will create really ugly code. SortById, sortByDirection: Rather complex logic results in sortById and sortByDirection always having a value here. @@ -51,11 +53,12 @@ export const EventWorkingListsTemplateSetup = ({ sortByDirection, clientId: uuid(), programId: program.id, - }); - }, [onAddTemplate, filters, columns, sortById, sortByDirection, program.id]); + }, { onChangeTemplate }); + }, [onAddTemplate, filters, columns, sortById, sortByDirection, program.id, onChangeTemplate]); const injectArgumentsForDeleteTemplate = React.useCallback(template => - onDeleteTemplate(template, program.id), [onDeleteTemplate, program.id]); + onDeleteTemplate(template, program.id, undefined, { onChangeTemplate }), + [onDeleteTemplate, program.id, onChangeTemplate]); return ( void, |}>; type RestProps = $Rest; @@ -42,4 +43,5 @@ export type EventWorkingListsTemplateSetupOutputProps = {| onDeleteTemplate: DeleteTemplate, templates?: WorkingListTemplates, templateSharingType: string, + onChangeTemplate?: (selectedTemplateId?: string) => void, |}; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js index f99d504f20..c8f4d6fe2f 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js @@ -69,6 +69,7 @@ export const updateTemplateEpic = ( criteria: eventQueryCriteria, programId, storeId, + callBacks, } }) => { const eventFilterData = { name, @@ -90,6 +91,11 @@ export const updateTemplateEpic = ( const isActiveTemplate = store.value.workingListsTemplates[storeId].selectedTemplateId === id; + // Call the onChangeTemplate callback if provided + if (callBacks && callBacks.onChangeTemplate) { + callBacks.onChangeTemplate(id); + } + return updateTemplateSuccess( id, eventQueryCriteria, { @@ -145,6 +151,7 @@ export const addTemplateEpic = ( clientId, programId, storeId, + callBacks, } = action.payload; const eventFilterData = { @@ -160,7 +167,14 @@ export const addTemplateEpic = ( }).then((result) => { const isActiveTemplate = store.value.workingListsTemplates[storeId].selectedTemplateId === clientId; - return addTemplateSuccess(result.response.uid, clientId, { storeId, isActiveTemplate }); + const templateId = result.response.uid; + + // Call the onChangeTemplate callback if provided + if (callBacks && callBacks.onChangeTemplate) { + callBacks.onChangeTemplate(templateId); + } + + return addTemplateSuccess(templateId, clientId, { storeId, isActiveTemplate }); }).catch((error) => { log.error( errorCreator('could not add template')({ @@ -191,21 +205,28 @@ export const deleteTemplateEpic = ( action$.pipe( ofType(workingListsCommonActionTypes.TEMPLATE_DELETE), filter(({ payload: { workingListsType } }) => workingListsType === SINGLE_EVENT_WORKING_LISTS_TYPE), - concatMap(({ payload: { template, storeId } }) => { + concatMap(({ payload: { template, storeId, callBacks } }) => { const requestPromise = mutate({ resource: 'eventFilters', id: template.id, type: 'delete', - }).then(() => deleteTemplateSuccess(template, storeId)) - .catch((error) => { - log.error( - errorCreator('could not delete template')({ - error, - template, - }), - ); - return deleteTemplateError(template, storeId); - }); + }).then(() => { + // Call onChangeTemplate when a template is deleted, passing the default template ID + if (callBacks && callBacks.onChangeTemplate) { + const programId = template.id.split('-')[0]; + callBacks.onChangeTemplate(`${programId}-default`); + } + + return deleteTemplateSuccess(template, storeId); + }).catch((error) => { + log.error( + errorCreator('could not delete template')({ + error, + template, + }), + ); + return deleteTemplateError(template, storeId); + }); return from(requestPromise).pipe( takeUntil( From cf987102cbca84fe41e8a4b4060b122f9b6c36c2 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 21:53:15 +0100 Subject: [PATCH 4/9] fix: improve template deletion callback with dynamic program ID retrieval --- .../WorkingLists/EventWorkingLists/epics/templates.epics.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js index c8f4d6fe2f..b16c87e667 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js @@ -18,6 +18,7 @@ import { } from '../../WorkingListsCommon'; import { getTemplates } from './getTemplates'; import { SINGLE_EVENT_WORKING_LISTS_TYPE } from '../constants'; +import { getLocationQuery } from '../../../../utils/routing'; export const retrieveTemplatesEpic = ( action$: InputObservable, @@ -211,9 +212,9 @@ export const deleteTemplateEpic = ( id: template.id, type: 'delete', }).then(() => { - // Call onChangeTemplate when a template is deleted, passing the default template ID + const { programId } = getLocationQuery(); + if (callBacks && callBacks.onChangeTemplate) { - const programId = template.id.split('-')[0]; callBacks.onChangeTemplate(`${programId}-default`); } From 740dd121f7cecd5958e61cac630f1f46e57951c2 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 23:01:35 +0100 Subject: [PATCH 5/9] feat: replace event program main page placeholder with event registration entry --- ...EventRegistrationEntryWrapper.component.js | 61 +++++++++++++++++++ ...EventRegistrationEntryWrapper.container.js | 56 +++++++++++++++++ .../EventRegistrationEntryWrapper.types.js | 30 +++++++++ .../constants.js | 5 ++ .../EventRegistrationEntryWrapper/index.js | 2 + .../selectors.js | 17 ++++++ .../Pages/MainPage/MainPage.component.js | 11 ++-- .../Pages/MainPage/MainPage.container.js | 2 - 8 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.component.js create mode 100644 src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.container.js create mode 100644 src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.types.js create mode 100644 src/core_modules/capture-core/components/EventRegistrationEntryWrapper/constants.js create mode 100644 src/core_modules/capture-core/components/EventRegistrationEntryWrapper/index.js create mode 100644 src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js diff --git a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.component.js b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.component.js new file mode 100644 index 0000000000..2c31e8f7ed --- /dev/null +++ b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.component.js @@ -0,0 +1,61 @@ +// @flow +import React from 'react'; +import { withStyles } from '@material-ui/core'; +import { colors, spacers } from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { SingleEventRegistrationEntry } from '../DataEntries'; +import type { Props } from './EventRegistrationEntryWrapper.types'; + +const getStyles = () => ({ + container: { + marginBottom: spacers.dp12, + padding: spacers.dp16, + background: colors.white, + border: '1px solid', + borderColor: colors.grey400, + borderRadius: 3, + }, + title: { + padding: '8px 0 0px 8px', + fontWeight: 500, + marginBottom: spacers.dp16, + }, + flexContainer: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + flexWrap: 'wrap', + }, + flexItem: { + flex: 1, + minWidth: '500px', + }, + noAccessContainer: { + padding: spacers.dp16, + }, +}); + +const EventRegistrationEntryWrapperPlain = ({ + classes, + selectedScopeId, + dataEntryId, + eventAccess, +}: Props) => { + if (!eventAccess?.write) { + return ( +
+ {i18n.t('You don\'t have access to create an event in the current selections')} +
+ ); + } + + return ( + + ); +}; + +export const EventRegistrationEntryWrapperComponent = withStyles(getStyles)(EventRegistrationEntryWrapperPlain); diff --git a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.container.js b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.container.js new file mode 100644 index 0000000000..6ecb28ad7d --- /dev/null +++ b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.container.js @@ -0,0 +1,56 @@ +// @flow +import { connect, useDispatch, useSelector } from 'react-redux'; +import * as React from 'react'; +import { useEffect, useRef } from 'react'; +import { compose } from 'redux'; +import { batchActions } from 'redux-batched-actions'; +import { EventRegistrationEntryWrapperComponent } from './EventRegistrationEntryWrapper.component'; +import { withLoadingIndicator } from '../../HOC/withLoadingIndicator'; +import type { ContainerProps, StateProps, ReduxState } from './EventRegistrationEntryWrapper.types'; +import { getOpenDataEntryActions } from '../DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry'; +import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; +import { makeEventAccessSelector } from './selectors'; +import { itemId } from './constants'; + +const makeMapStateToProps = () => { + const eventAccessSelector = makeEventAccessSelector(); + return (state: ReduxState, ownProps: ContainerProps): StateProps => ({ + selectedScopeId: ownProps.selectedScopeId, + dataEntryId: ownProps.dataEntryId, + orgUnitId: state.currentSelections.orgUnitId, + ready: state.dataEntries[ownProps.dataEntryId]?.itemId === itemId, + eventAccess: eventAccessSelector(state), + }); +}; + +const openSingleEventDataEntry = (InnerComponent: React.ComponentType) => ( + (props: ContainerProps) => { + const hasRun = useRef(false); + const { selectedScopeId } = props; + const dispatch = useDispatch(); + const selectedCategories = useSelector((state: ReduxState) => state.currentSelections.categories); + const { isLoading, programCategory } = useCategoryCombinations(selectedScopeId); + + useEffect(() => { + if (!isLoading && !hasRun.current) { + dispatch( + batchActions([ + ...getOpenDataEntryActions(programCategory, selectedCategories), + ]), + ); + hasRun.current = true; + } + }, [selectedCategories, dispatch, isLoading, programCategory]); + + return ( + + ); + }); + +export const EventRegistrationEntryWrapper = compose( + openSingleEventDataEntry, + connect(makeMapStateToProps(), () => ({})), + withLoadingIndicator(), +)(EventRegistrationEntryWrapperComponent); diff --git a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.types.js b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.types.js new file mode 100644 index 0000000000..8804fb77d3 --- /dev/null +++ b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/EventRegistrationEntryWrapper.types.js @@ -0,0 +1,30 @@ +// @flow +export type Props = {| + classes: Object, + selectedScopeId: string, + dataEntryId: string, + orgUnitId: string, + eventAccess?: {| + read: boolean, + write: boolean, + |}, +|}; + +export type ContainerProps = {| + selectedScopeId: string, + dataEntryId: string, +|}; + +export type StateProps = {| + selectedScopeId: string, + orgUnitId: string, + dataEntryId: string, + eventAccess: {| + read: boolean, + write: boolean, + |}, + ready: boolean, +|}; + +export type ReduxState = Object; +export type MapStateToProps = (ReduxState, ContainerProps) => StateProps; diff --git a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/constants.js b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/constants.js new file mode 100644 index 0000000000..5ee70343d8 --- /dev/null +++ b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/constants.js @@ -0,0 +1,5 @@ +// @flow + +// Constants duplicated from SingleEventRegistrationEntry +export const dataEntryId = 'singleEvent'; +export const itemId = 'newEvent'; diff --git a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/index.js b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/index.js new file mode 100644 index 0000000000..c5a906b67a --- /dev/null +++ b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/index.js @@ -0,0 +1,2 @@ +// @flow +export { EventRegistrationEntryWrapper } from './EventRegistrationEntryWrapper.container'; diff --git a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js new file mode 100644 index 0000000000..66b5cab541 --- /dev/null +++ b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js @@ -0,0 +1,17 @@ +// @flow + +import { createSelector } from 'reselect'; +import { getProgramEventAccess } from '../../metaData'; + +const programIdSelector = state => state.currentSelections.programId; +const categoriesMetaSelector = state => state.currentSelections.categoriesMeta; +const programStageIdSelector = state => state.currentSelections.stageId; + +// $FlowFixMe[missing-annot] automated comment +export const makeEventAccessSelector = () => createSelector( + programIdSelector, + categoriesMetaSelector, + programStageIdSelector, + (programId: string, categoriesMeta: ?Object, programStageId: ?string) => + programId && getProgramEventAccess(programId, programStageId, categoriesMeta) +); \ No newline at end of file diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js index 581237ce33..75d8c91dbf 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js @@ -12,10 +12,12 @@ import { WithoutCategorySelectedMessage } from './WithoutCategorySelectedMessage import { withErrorMessageHandler, withLoadingIndicator } from '../../../HOC'; import { SearchBox } from '../../SearchBox'; import { TemplateSelector } from '../../TemplateSelector'; +import { NoSelectionsInfoBox } from './NoSelectionsInfoBox'; +import { EventRegistrationEntryWrapper } from '../../EventRegistrationEntryWrapper'; import { InvalidCategoryCombinationForOrgUnitMessage, } from './InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage'; -import { NoSelectionsInfoBox } from './NoSelectionsInfoBox'; +import { DataEntryWidgetOutput } from '../../DataEntryWidgetOutput/DataEntryWidgetOutput.container'; const getStyles = () => ({ listContainer: { @@ -107,9 +109,10 @@ const MainPagePlain = ({ // For event programs, show template selector
-

- This is a placeholder for the event program main page. -

+
diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js index 8116add8d0..94eaafeca0 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js @@ -113,10 +113,8 @@ const MainPageContainer = () => { const { categoryOptionIsInvalidForOrgUnit } = useCategoryOptionIsValidForOrgUnit({ selectedOrgUnitId: orgUnitId }); const selectedProgram = programCollection.get(programId); - console.log('selectedProgram', selectedProgram); // $FlowFixMe[prop-missing] const trackedEntityTypeId = selectedProgram?.trackedEntityType?.id; - // Allow displayFrontPageList to be used for both event programs and tracker programs const displayFrontPageList = selectedProgram?.displayFrontPageList; const MainPageStatus = useMainPageStatus({ programId, From 85187e0e21296156d3b03be10be694114bcbbba8 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 23:09:18 +0100 Subject: [PATCH 6/9] chore: add onChangeTemplate prop to working lists components --- .../capture-core/components/ListView/types/listView.types.js | 1 + .../components/Pages/MainPage/MainPage.component.js | 1 - .../UpdateTrigger/EventWorkingListsUpdateTrigger.component.js | 2 ++ .../UpdateTrigger/eventWorkingListsUpdateTrigger.types.js | 2 ++ .../WorkingLists/WorkingListsBase/workingListsBase.types.js | 1 + 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/ListView/types/listView.types.js b/src/core_modules/capture-core/components/ListView/types/listView.types.js index c367075b4e..2c5e13e059 100644 --- a/src/core_modules/capture-core/components/ListView/types/listView.types.js +++ b/src/core_modules/capture-core/components/ListView/types/listView.types.js @@ -104,6 +104,7 @@ export type InterfaceProps = $ReadOnly<{| onSetColumnOrder: SetColumnOrder, onSort: Sort, onUpdateFilter: UpdateFilter, + onChangeTemplate?: (selectedTemplateId?: string) => void, rowIdKey: string, rowsPerPage: number, sortById: string, diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js index 75d8c91dbf..406ae87638 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js @@ -17,7 +17,6 @@ import { EventRegistrationEntryWrapper } from '../../EventRegistrationEntryWrapp import { InvalidCategoryCombinationForOrgUnitMessage, } from './InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage'; -import { DataEntryWidgetOutput } from '../../DataEntryWidgetOutput/DataEntryWidgetOutput.container'; const getStyles = () => ({ listContainer: { diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/EventWorkingListsUpdateTrigger.component.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/EventWorkingListsUpdateTrigger.component.js index 0b1954c4eb..9f35e293a9 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/EventWorkingListsUpdateTrigger.component.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/EventWorkingListsUpdateTrigger.component.js @@ -12,6 +12,7 @@ export const EventWorkingListsUpdateTrigger = ({ lastTransactionOnListDataRefresh, onLoadView, onUpdateList, + onChangeTemplate, ...passOnProps }: Props) => { const forceUpdateOnMount = moment().diff(moment(listDataRefreshTimestamp || 0), 'minutes') > 5 || @@ -32,6 +33,7 @@ export const EventWorkingListsUpdateTrigger = ({ forceUpdateOnMount={forceUpdateOnMount} onLoadView={injectCustomUpdateContextToLoadList} onUpdateList={injectCustomUpdateContextToUpdateList} + onChangeTemplate={onChangeTemplate} /> ); }; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/eventWorkingListsUpdateTrigger.types.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/eventWorkingListsUpdateTrigger.types.js index 5402dd4880..d98e4032ba 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/eventWorkingListsUpdateTrigger.types.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/UpdateTrigger/eventWorkingListsUpdateTrigger.types.js @@ -9,6 +9,7 @@ type ExtractedProps = $ReadOnly<{| lastTransactionOnListDataRefresh?: number, onLoadView: Function, onUpdateList: Function, + onChangeTemplate?: (selectedTemplateId?: string) => void, |}>; type RestProps = $Rest void, |}>; diff --git a/src/core_modules/capture-core/components/WorkingLists/WorkingListsBase/workingListsBase.types.js b/src/core_modules/capture-core/components/WorkingLists/WorkingListsBase/workingListsBase.types.js index 046c59d246..df0560372b 100644 --- a/src/core_modules/capture-core/components/WorkingLists/WorkingListsBase/workingListsBase.types.js +++ b/src/core_modules/capture-core/components/WorkingLists/WorkingListsBase/workingListsBase.types.js @@ -216,6 +216,7 @@ export type InterfaceProps = $ReadOnly<{| onUpdateFilter: UpdateFilter, onUpdateList: UpdateList, onUpdateTemplate?: UpdateTemplate, + onChangeTemplate?: (selectedTemplateId?: string) => void, orgUnitId: string, programId: string, rowIdKey: string, From 1eeae242d8fb8dae4fa69d1e143f4c0b6538ef81 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 23:14:03 +0100 Subject: [PATCH 7/9] chore: clean up trailing whitespaces and formatting in various components --- .../components/EventRegistrationEntryWrapper/selectors.js | 4 ++-- .../EventTemplateSelector/EventTemplateSelector.container.js | 2 +- .../TemplateSelector/EventTemplateSelector/index.js | 2 +- .../components/TemplateSelector/TEITemplateSelector/index.js | 2 +- .../NewTrackedEntityRelationship.component.js | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js index 66b5cab541..4ac674b951 100644 --- a/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js +++ b/src/core_modules/capture-core/components/EventRegistrationEntryWrapper/selectors.js @@ -13,5 +13,5 @@ export const makeEventAccessSelector = () => createSelector( categoriesMetaSelector, programStageIdSelector, (programId: string, categoriesMeta: ?Object, programStageId: ?string) => - programId && getProgramEventAccess(programId, programStageId, categoriesMeta) -); \ No newline at end of file + programId && getProgramEventAccess(programId, programStageId, categoriesMeta), +); diff --git a/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/EventTemplateSelector.container.js b/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/EventTemplateSelector.container.js index e9b9793d6a..2faf1cb042 100644 --- a/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/EventTemplateSelector.container.js +++ b/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/EventTemplateSelector.container.js @@ -27,4 +27,4 @@ export const EventTemplateSelector = () => { onCreateTemplate={onCreateTemplate} /> ) : null; -}; \ No newline at end of file +}; diff --git a/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js b/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js index ee10a7b89d..d5de8470d5 100644 --- a/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js +++ b/src/core_modules/capture-core/components/TemplateSelector/EventTemplateSelector/index.js @@ -1,2 +1,2 @@ // @flow -export { EventTemplateSelector } from './EventTemplateSelector.container'; \ No newline at end of file +export { EventTemplateSelector } from './EventTemplateSelector.container'; diff --git a/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js b/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js index 2122294cdc..2833eca8bd 100644 --- a/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js +++ b/src/core_modules/capture-core/components/TemplateSelector/TEITemplateSelector/index.js @@ -1,2 +1,2 @@ // @flow -export { TEITemplateSelector } from './TEITemplateSelector.container'; \ No newline at end of file +export { TEITemplateSelector } from './TEITemplateSelector.container'; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index 5a4dde8e91..53212f71b8 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -58,13 +58,13 @@ const NewTrackedEntityRelationshipPlain = ({ teiId, onMutate: () => onSave && onSave(), }); - + // Add a ref to track the current mode and force remounting when switching const modeRef = useRef({ currentMode: null, timestamp: Date.now(), }); - + // Update timestamp when step changes to ensure remounting useEffect(() => { if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id) { From 00d8e6a5f6a73f9608e6743e0685e7b61c259954 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 23:30:01 +0100 Subject: [PATCH 8/9] feat: cleanup --- .../Pages/MainPage/MainPage.component.js | 3 - .../RegisterTeiDataEntry.component.js | 7 --- .../RegisterTei/RegisterTei.component.js | 7 +-- ...kedEntityRelationshipsWrapper.component.js | 18 +----- .../hooks/useEventTemplates.js | 51 ++++++++--------- .../NewTrackedEntityRelationship.component.js | 56 ++++--------------- 6 files changed, 37 insertions(+), 105 deletions(-) diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js index 406ae87638..ceaf9552ab 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.component.js @@ -58,7 +58,6 @@ const MainPagePlain = ({ const noProgramSelected = !programId; const noOrgUnitSelected = !orgUnitId; - // Only show the main page for event programs if displayFrontPageList is true return noProgramSelected || noOrgUnitSelected || displayFrontPageList || @@ -95,7 +94,6 @@ const MainPagePlain = ({ ) : ( <> {trackedEntityTypeId ? ( - // For tracker programs, show search and template selector
@@ -105,7 +103,6 @@ const MainPagePlain = ({
) : ( - // For event programs, show template selector
{ - componentDidMount() { - // Force reinitialization when component is mounted - // This ensures plugins are triggered when the component is remounted - } - render() { const { showDataEntry, @@ -34,7 +29,6 @@ export class RegisterTeiDataEntryComponent extends React.Component { ); } @@ -43,7 +37,6 @@ export class RegisterTeiDataEntryComponent extends React.Component { ); } diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js index 170742ee76..c22f4bc5cf 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType, useContext, useCallback, useEffect } from 'react'; +import React, { type ComponentType, useContext, useCallback } from 'react'; import { compose } from 'redux'; import { withStyles } from '@material-ui/core/styles'; import i18n from '@dhis2/d2-i18n'; @@ -70,11 +70,6 @@ const RegisterTeiPlain = ({ }: ComponentProps) => { const { resultsPageSize } = useContext(ResultsPageSizeContext); - useEffect(() => { - // This empty dependency array ensures the effect runs only on mount - // This forces the component to reinitialize when it's remounted with a new key - }, []); - const renderDuplicatesCardActions = useCallback(({ item }) => ( { - // Update the mode ref when the find mode changes - if (modeRef.current.currentMode !== findMode) { - modeRef.current = { - currentMode: findMode, - timestamp: Date.now(), - }; - } - dispatch(selectFindMode({ findMode, orgUnit: initialOrgUnit, @@ -89,7 +75,6 @@ export const TrackedEntityRelationshipsWrapper = ({ ) => ( ( { - const { error, loading, data, refetch } = useDataQuery( - useMemo( - () => ({ - templates: { - resource: 'eventFilters', - params: ({ variables }) => ({ - filter: `program:eq:${variables.programId}`, - fields: ` - id,displayName,eventQueryCriteria,access,externalAccess,publicAccess, - user[id,username], - userAccesses[id,access], - userGroupAccesses[id,access] - `, - }), - }, - }), - [], - ), - { lazy: true }, - ); + const query = useMemo(() => ({ + resource: 'eventFilters', + params: { + filter: `program:eq:${programId}`, + fields: ` + id,displayName,eventQueryCriteria,access,externalAccess,publicAccess, + user[id,username], + userAccesses[id,access], + userGroupAccesses[id,access] + `, + }, + }), [programId]); - useEffect(() => { - programId && refetch({ variables: { programId } }); - }, [refetch, programId]); + const { data, isLoading, error } = useApiDataQuery( + ['eventTemplates', programId], + query, + { + enabled: !!programId, + select: (response: any) => response?.eventFilters || [], + }, + ); return { error, - loading, - eventTemplates: data?.templates?.eventFilters ? data.templates.eventFilters : [], + loading: isLoading, + eventTemplates: data || [], }; }; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index 53212f71b8..48b90410dc 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -1,5 +1,5 @@ // @flow -import React, { useCallback, useState, type ComponentType, useMemo, useRef, useEffect } from 'react'; +import React, { useCallback, useState, type ComponentType, useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { Widget } from '../../../Widget'; @@ -59,30 +59,6 @@ const NewTrackedEntityRelationshipPlain = ({ onMutate: () => onSave && onSave(), }); - // Add a ref to track the current mode and force remounting when switching - const modeRef = useRef({ - currentMode: null, - timestamp: Date.now(), - }); - - // Update timestamp when step changes to ensure remounting - useEffect(() => { - if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY.id) { - if (modeRef.current.currentMode !== 'search') { - modeRef.current = { - currentMode: 'search', - timestamp: Date.now(), - }; - } - } else if (currentStep.id === NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.NEW_LINKED_ENTITY.id) { - if (modeRef.current.currentMode !== 'register') { - modeRef.current = { - currentMode: 'register', - timestamp: Date.now(), - }; - } - } - }, [currentStep.id]); const onLinkToTrackedEntityFromSearch = useCallback( (linkedTrackedEntityId: string, attributes?: { [attributeId: string]: string }) => { @@ -239,15 +215,10 @@ const NewTrackedEntityRelationshipPlain = ({ }: LinkedEntityMetadata = selectedLinkedEntityMetadata; if (renderTrackedEntitySearch) { - // Use the timestamp from modeRef to ensure consistent remounting - return ( -
- {renderTrackedEntitySearch( - linkedEntityTrackedEntityTypeId, - linkedEntityProgramId, - onLinkToTrackedEntityFromSearch, - )} -
+ return renderTrackedEntitySearch( + linkedEntityTrackedEntityTypeId, + linkedEntityProgramId, + onLinkToTrackedEntityFromSearch, ); } } @@ -260,17 +231,12 @@ const NewTrackedEntityRelationshipPlain = ({ }: LinkedEntityMetadata = selectedLinkedEntityMetadata; if (renderTrackedEntityRegistration) { - // Use the timestamp from modeRef to ensure consistent remounting - return ( -
- {renderTrackedEntityRegistration( - linkedEntityTrackedEntityTypeId, - linkedEntityProgramId, - onLinkToTrackedEntityFromRegistration, - onLinkToTrackedEntityFromSearch, - onCancel, - )} -
+ return renderTrackedEntityRegistration( + linkedEntityTrackedEntityTypeId, + linkedEntityProgramId, + onLinkToTrackedEntityFromRegistration, + onLinkToTrackedEntityFromSearch, + onCancel, ); } } From c2030fff942e46b76c1296fe61fe558e67964fd5 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Wed, 26 Feb 2025 23:39:18 +0100 Subject: [PATCH 9/9] refactor: simplify template callback checks using optional chaining --- .../EventWorkingLists/epics/templates.epics.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js index b16c87e667..d164a9ff24 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/templates.epics.js @@ -92,8 +92,7 @@ export const updateTemplateEpic = ( const isActiveTemplate = store.value.workingListsTemplates[storeId].selectedTemplateId === id; - // Call the onChangeTemplate callback if provided - if (callBacks && callBacks.onChangeTemplate) { + if (callBacks?.onChangeTemplate) { callBacks.onChangeTemplate(id); } @@ -170,8 +169,7 @@ export const addTemplateEpic = ( store.value.workingListsTemplates[storeId].selectedTemplateId === clientId; const templateId = result.response.uid; - // Call the onChangeTemplate callback if provided - if (callBacks && callBacks.onChangeTemplate) { + if (callBacks?.onChangeTemplate) { callBacks.onChangeTemplate(templateId); } @@ -214,7 +212,7 @@ export const deleteTemplateEpic = ( }).then(() => { const { programId } = getLocationQuery(); - if (callBacks && callBacks.onChangeTemplate) { + if (callBacks?.onChangeTemplate) { callBacks.onChangeTemplate(`${programId}-default`); }