From 75fef63dd377a6bc90769f00df849fcca74651e3 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:05:22 +0200 Subject: [PATCH 01/33] refactor: [DHIS2-17826] Replace Material UI Chip (#3741) --- i18n/en.pot | 4 ++-- .../Section/ViewEventSectionHeader.component.js | 15 +++++++++------ .../WidgetEventEdit/WidgetEventEdit.container.js | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 696d7b5c7b..b2d551ac19 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2024-06-28T11:23:02.970Z\n" -"PO-Revision-Date: 2024-06-28T11:23:02.970Z\n" +"POT-Creation-Date: 2024-08-02T09:44:11.640Z\n" +"PO-Revision-Date: 2024-08-02T09:44:11.640Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Section/ViewEventSectionHeader.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/Section/ViewEventSectionHeader.component.js index 41ae4d6750..03a730d82e 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Section/ViewEventSectionHeader.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Section/ViewEventSectionHeader.component.js @@ -2,8 +2,8 @@ // @flow import * as React from 'react'; import classNames from 'classnames'; - -import { Chip, withStyles } from '@material-ui/core'; +import { Chip, colors } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; const getStyles = (theme: Theme) => ({ @@ -17,9 +17,10 @@ const getStyles = (theme: Theme) => ({ paddingLeft: theme.typography.pxToRem(5), }, badge: { - height: theme.typography.pxToRem(20), - width: theme.typography.pxToRem(20), - fontWeight: 700, + display: 'flex', + alignItems: 'center', + cursor: 'default !important', + backgroundColor: `${colors.grey300} !important`, }, }); @@ -45,7 +46,9 @@ class ViewEventSectionHeaderPlain extends React.Component { {shouldRenderBadge &&
- + + {badgeCount} +
} diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js index 4ca7809ca9..abb1107741 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js @@ -88,6 +88,7 @@ export const WidgetEventEditPlain = ({ }: PlainProps) => { useEffect(() => inMemoryFileStore.clear, []); const dispatch = useDispatch(); + const supportsChangelog = useFeature(FEATURES.changelogs); const { currentPageMode } = useEnrollmentEditEventPageMode(eventStatus); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); From c6b6dc0330d8c92d8334fb1f0b34a5524ebb2967 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:15:06 +0200 Subject: [PATCH 02/33] feat: [DHIS2-16125] hide program stage under certain circumstances (#3735) --- .../HiddenProgramStage.feature | 2 +- .../HiddenProgramStage/HiddenProgramStage.js | 5 +- .../Stages/Stage/Stage.component.js | 48 +++++++++---------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.feature b/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.feature index e13095474a..1eade0c8dd 100644 --- a/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.feature +++ b/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.feature @@ -2,5 +2,5 @@ Feature: Hidden program stage Scenario: The user cannot add an event in a hidden program stage Given you add an enrollment event that will result in a rule effect to hide a program stage - Then the New Postpartum care visit event button is disabled in the stages and events widget + Then the Postpartum care visit stage should not be displayed in the Stages and Events widget And the Postpartum care visit button is disabled in the enrollmentEventNew page diff --git a/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js b/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js index bfd806d758..43152029ff 100644 --- a/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js +++ b/cypress/e2e/EnrollmentPage/HiddenProgramStage/HiddenProgramStage.js @@ -43,9 +43,8 @@ Given('you add an enrollment event that will result in a rule effect to hide a p cy.contains('[data-test="dhis2-uicore-button"]', 'Save without completing').click(); }); -Then('the New Postpartum care visit event button is disabled in the stages and events widget', () => { - cy.contains('[data-test="create-new-button"]', 'New Postpartum care visit event') - .should('be.disabled'); +Then('the Postpartum care visit stage should not be displayed in the Stages and Events widget', () => { + cy.get('[data-test="stages-and-events-widget"]').should('not.contain', 'Postpartum care visit'); }); Then('the Postpartum care visit button is disabled in the enrollmentEventNew page', () => { diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js index 87815c565c..d76690c7eb 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js @@ -4,7 +4,6 @@ import cx from 'classnames'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { spacersNum, colors, IconAdd16, Button } from '@dhis2/ui'; -import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; import { StageOverview } from './StageOverview'; import type { Props } from './stage.types'; import { Widget } from '../../../Widget'; @@ -24,14 +23,20 @@ const styles = { alignItems: 'center', }, }; -const hideProgramStage = (ruleEffects, stageId) => ( +const rulesEffectHideProgramStage = (ruleEffects, stageId) => ( Boolean(ruleEffects?.find(ruleEffect => ruleEffect.type === 'HIDEPROGRAMSTAGE' && ruleEffect.id === stageId)) ); export const StagePlain = ({ stage, events, classes, className, onCreateNew, ruleEffects, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage; - const hiddenProgramStage = hideProgramStage(ruleEffects, id); + const preventAddingNewEvents = rulesEffectHideProgramStage(ruleEffects, id); + const hideProgramStage = preventAddingNewEvents && events.length === 0; + + const handleOpen = useCallback(() => setOpenStatus(true), [setOpenStatus]); + const handleClose = useCallback(() => setOpenStatus(false), [setOpenStatus]); + + if (hideProgramStage) return null; return (
} borderless - onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} - onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} + onOpen={handleOpen} + onClose={handleClose} open={open} > {events.length > 0 ? : ( - } + className={classes.button} + dataTest="create-new-button" + onClick={() => onCreateNew(id)} + > + {i18n.t('New {{ eventName }} event', { + eventName: name, interpolation: { escapeValue: false }, })} - enabled={hiddenProgramStage} - > - - + )}
From 24be3880f5439d6798679786ab6a57159a44da28 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Wed, 7 Aug 2024 08:26:54 +0000 Subject: [PATCH 03/33] chore(release): cut 100.73.0 [skip release] # [100.73.0](https://github.com/dhis2/capture-app/compare/v100.72.0...v100.73.0) (2024-08-07) ### Features * [DHIS2-16125] hide program stage under certain circumstances ([#3735](https://github.com/dhis2/capture-app/issues/3735)) ([c6b6dc0](https://github.com/dhis2/capture-app/commit/c6b6dc0330d8c92d8334fb1f0b34a5524ebb2967)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 797bc7f412..d8d081059b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [100.73.0](https://github.com/dhis2/capture-app/compare/v100.72.0...v100.73.0) (2024-08-07) + + +### Features + +* [DHIS2-16125] hide program stage under certain circumstances ([#3735](https://github.com/dhis2/capture-app/issues/3735)) ([c6b6dc0](https://github.com/dhis2/capture-app/commit/c6b6dc0330d8c92d8334fb1f0b34a5524ebb2967)) + # [100.72.0](https://github.com/dhis2/capture-app/compare/v100.71.3...v100.72.0) (2024-08-06) diff --git a/package.json b/package.json index d0eb4544d0..d0c4f8e1f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.72.0", + "version": "100.73.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.72.0", + "@dhis2/rules-engine-javascript": "100.73.0", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index f7b86d55a3..bb0e1d396f 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.72.0", + "version": "100.73.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 205b9f5746f3d9b3d0d28035fd99e40b5649003c Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Thu, 8 Aug 2024 00:05:39 +0200 Subject: [PATCH 04/33] feat: [DHIS2-17591][DHIS2-17607] Plugins in event forms (#3684) --- i18n/en.pot | 7 +- .../components/D2Form/D2Form.component.js | 4 +- .../D2Form/D2SectionFields.component.js | 1 + .../FormBuilder/FormBuilder.component.js | 2 + .../FormFieldPlugin/FormFieldPlugin.const.js | 1 + .../FormFieldPlugin.container.js | 2 + .../FormFieldPlugin/FormFieldPlugin.types.js | 1 + .../hooks/usePluginCallbacks.js | 11 +- .../DataEntry/DataEntry.container.js | 16 +- .../NewEventDataEntryWrapper.component.js | 4 +- .../NewEventDataEntryWrapper.container.js | 21 +-- .../NewEventDataEntryWrapper.types.js | 6 +- .../newEventDataEntryWrapper.selectors.js | 54 ------- .../DataEntryWrapper/useRulesEngine.js | 5 +- .../ProgramStage/buildProgramStageMetadata.js | 45 ++++++ .../DataEntries/common/ProgramStage/index.js | 4 + .../ProgramStage/useDataElementsForStage.js | 41 +++++ .../useMetadataForProgramStage.js | 96 ++++++++++++ .../common/TEIAndEnrollment/index.js | 12 +- .../useMetadataForRegistrationForm/index.js | 2 + .../useMetadataForRegistrationForm.js | 1 + .../components/DataEntries/index.js | 1 - .../EnrollmentEditEventPage.component.js | 2 + .../EnrollmentEditEventPage.container.js | 1 + .../EnrollmentEditEventPage.types.js | 1 + .../EventDetailsSection.component.js | 64 ++++---- .../LayoutComponentConfig.js | 4 +- .../WidgetEventEditWrapper.js | 22 ++- .../WidgetEnrollmentEventNew.container.js | 21 ++- .../WidgetEventEdit.container.js | 30 ++-- .../WidgetEventEdit/widgetEventEdit.types.js | 13 +- .../factory/enrollment/EnrollmentFactory.js | 2 +- .../programStage/DataElementFactory.js | 3 +- .../programStage/ProgramStageFactory.js | 145 ++++++++++++++++-- .../programStage/programStageFactory.types.js | 6 + .../TeiRegistrationFactory.js | 2 +- .../teiRegistrationFactory.types.js | 2 +- .../trackedEntityTypeFactory.types.js | 2 +- 38 files changed, 492 insertions(+), 165 deletions(-) delete mode 100644 src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/newEventDataEntryWrapper.selectors.js create mode 100644 src/core_modules/capture-core/components/DataEntries/common/ProgramStage/buildProgramStageMetadata.js create mode 100644 src/core_modules/capture-core/components/DataEntries/common/ProgramStage/index.js create mode 100644 src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useDataElementsForStage.js create mode 100644 src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useMetadataForProgramStage.js diff --git a/i18n/en.pot b/i18n/en.pot index b2d551ac19..b28f5b5e2b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2024-08-02T09:44:11.640Z\n" -"PO-Revision-Date: 2024-08-02T09:44:11.640Z\n" +"POT-Creation-Date: 2024-06-18T22:47:46.585Z\n" +"PO-Revision-Date: 2024-06-18T22:47:46.585Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -982,6 +982,9 @@ msgstr "Could not retrieve metadata. Please try again later." msgid "The enrollment event data could not be found" msgstr "The enrollment event data could not be found" +msgid "Loading" +msgstr "Loading" + msgid "Possible duplicates found" msgstr "Possible duplicates found" diff --git a/src/core_modules/capture-core/components/D2Form/D2Form.component.js b/src/core_modules/capture-core/components/D2Form/D2Form.component.js index 4f17677b07..cc5177b453 100644 --- a/src/core_modules/capture-core/components/D2Form/D2Form.component.js +++ b/src/core_modules/capture-core/components/D2Form/D2Form.component.js @@ -85,7 +85,9 @@ class D2Form extends React.PureComponent { renderHorizontal = (section: Section, passOnProps: any) => ( { this.setSectionInstance(sectionInstance, section.id); }} + innerRef={(sectionInstance) => { + this.setSectionInstance(sectionInstance, section.id); + }} sectionMetaData={section} validationStrategy={this.props.formFoundation.validationStrategy} formId={this.getFormId()} diff --git a/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js b/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js index c9b12c15c0..c5aa1266a8 100644 --- a/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js +++ b/src/core_modules/capture-core/components/D2Form/D2SectionFields.component.js @@ -88,6 +88,7 @@ export class D2SectionFieldsComponent extends Component { fieldsMetadata: metaDataElement.fields, customAttributes: metaDataElement.customAttributes, formId: props.formId, + viewMode: props.viewMode, }, }); } diff --git a/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js b/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js index 6707d84ca7..532c6ea560 100644 --- a/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js +++ b/src/core_modules/capture-core/components/D2Form/FormBuilder/FormBuilder.component.js @@ -568,6 +568,7 @@ export class FormBuilder extends React.Component { customAttributes, name, formId, + viewMode, } = field.props; return ( @@ -578,6 +579,7 @@ export class FormBuilder extends React.Component { pluginSource={pluginSource} fieldsMetadata={fieldsMetadata} formId={formId} + viewMode={viewMode} onUpdateField={this.commitFieldUpdateFromPlugin.bind(this)} pluginContext={pluginContext} /> diff --git a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js index 50e1c168bc..23ae3ca48b 100644 --- a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js +++ b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js @@ -3,6 +3,7 @@ export const PluginErrorMessages = Object.freeze({ SET_FIELD_VALUE_MISSING_ID: 'setFieldValue: missing required fieldId', SET_FIELD_VALUE_ID_NOT_ALLOWED: 'setFieldValue: fieldId must be one of the configured plugin ids', + SET_CONTEXT_FIELD_VALUE_MISSING_ID: 'setContextFieldValue: tried to set value for a field that does not exist in the plugin context', }); export const FormFieldTypes = Object.freeze({ diff --git a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.container.js b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.container.js index 657888bdc4..e69e251230 100644 --- a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.container.js +++ b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.container.js @@ -16,6 +16,7 @@ export const FormFieldPlugin = (props: ContainerProps) => { onUpdateField, customAttributes, pluginContext, + viewMode = false, } = props; const metadataByPluginId = useMemo(() => Object.fromEntries(fieldsMetadata), [fieldsMetadata]); const configuredPluginIds = useMemo(() => Object.keys(metadataByPluginId), [metadataByPluginId]); @@ -55,6 +56,7 @@ export const FormFieldPlugin = (props: ContainerProps) => { setContextFieldValue={setContextFieldValue} errors={errors} warnings={warnings} + viewMode={viewMode} /> ); }; diff --git a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.types.js b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.types.js index 93e927fbbb..a271d16566 100644 --- a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.types.js +++ b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.types.js @@ -72,4 +72,5 @@ export type ComponentProps = {| errors: { [id: string]: Array }, warnings: { [id: string]: Array }, setContextFieldValue: (SetFieldValueProps) => void, + viewMode: boolean, |} diff --git a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/hooks/usePluginCallbacks.js b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/hooks/usePluginCallbacks.js index 5e608e0e15..e5091efb24 100644 --- a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/hooks/usePluginCallbacks.js +++ b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/hooks/usePluginCallbacks.js @@ -28,7 +28,16 @@ export const usePluginCallbacks = ({ }, [configuredPluginIds, metadataByPluginId, onUpdateField]); const setContextFieldValue = useCallback(({ fieldId, value }: SetFieldValueProps) => { - pluginContext[fieldId]?.setDataEntryFieldValue(value); + const contextField = pluginContext[fieldId]; + + if (!contextField) { + log.error(errorCreator( + PluginErrorMessages.SET_CONTEXT_FIELD_VALUE_MISSING_ID, + )({ fieldId, value })); + return; + } + + contextField?.setDataEntryFieldValue(value); }, [pluginContext]); return { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.js index abd0998feb..5d5507905f 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.js @@ -29,15 +29,15 @@ import typeof { newEventSaveTypes } from './newEventSaveTypes'; const makeMapStateToProps = () => { const programNameSelector = makeProgramNameSelector(); - const mapStateToProps = (state: ReduxState, props: Object) => - ({ recentlyAddedRelationshipId: state.newEventPage.recentlyAddedRelationshipId, - ready: !state.activePage.isDataEntryLoading, - error: !props.formFoundation ? - i18n.t('This is not an event program or the metadata is corrupt. See log for details.') : null, - programName: programNameSelector(state), - orgUnitName: state.organisationUnits[state.currentSelections.orgUnitId] && + const mapStateToProps = (state: ReduxState, props: Object) => ({ + recentlyAddedRelationshipId: state.newEventPage.recentlyAddedRelationshipId, + ready: !state.activePage.isDataEntryLoading, + error: !props.formFoundation ? + i18n.t('This is not an event program or the metadata is corrupt. See log for details.') : null, + programName: programNameSelector(state), + orgUnitName: state.organisationUnits[state.currentSelections.orgUnitId] && state.organisationUnits[state.currentSelections.orgUnitId].name, - }); + }); // $FlowFixMe[not-an-object] automated comment diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.component.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.component.js index f39c31416a..28f3b35296 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.component.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.component.js @@ -11,6 +11,7 @@ import { useCoreOrgUnit } from '../../../../metadataRetrieval/coreOrgUnit'; import { useLocationQuery } from '../../../../utils/routing'; import { useRulesEngine } from './useRulesEngine'; import type { PlainProps } from './NewEventDataEntryWrapper.types'; +import { useMetadataForProgramStage } from '../../common/ProgramStage/useMetadataForProgramStage'; const getStyles = () => ({ flexContainer: { @@ -41,13 +42,12 @@ const getStyles = () => ({ const NewEventDataEntryWrapperPlain = ({ classes, - formFoundation, formHorizontal, - stage, onFormLayoutDirectionChange, }: PlainProps) => { const { id: programId } = useCurrentProgramInfo(); const orgUnitId = useLocationQuery().orgUnitId; + const { formFoundation, stage } = useMetadataForProgramStage({ programId }); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); const rulesReady = useRulesEngine({ programId, orgUnit, formFoundation }); const titleText = useScopeTitleText(programId); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.container.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.container.js index 1099cf31e5..451ab8fca8 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.container.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.container.js @@ -5,26 +5,13 @@ import { NewEventDataEntryWrapperComponent } from './NewEventDataEntryWrapper.co import { setNewEventFormLayoutDirection, } from './newEventDataEntryWrapper.actions'; -import { - makeStageSelector, -} from './newEventDataEntryWrapper.selectors'; import { getDataEntryHasChanges } from '../getNewEventDataEntryHasChanges'; import type { Props, ContainerProps, StateProps, MapStateToProps } from './NewEventDataEntryWrapper.types'; -const makeMapStateToProps = (): MapStateToProps => { - const stageSelector = makeStageSelector(); - - return (state: ReduxState): StateProps => { - const stage = stageSelector(state); - const formFoundation = stage && stage.stageForm ? stage.stageForm : null; - return ({ - stage, - formFoundation, - dataEntryHasChanges: getDataEntryHasChanges(state), - formHorizontal: (formFoundation && formFoundation.customForm ? false : !!state.newEventPage.formHorizontal), - }); - }; -}; +const makeMapStateToProps = (): MapStateToProps => (state: ReduxState): StateProps => ({ + dataEntryHasChanges: getDataEntryHasChanges(state), + formHorizontal: !!state.newEventPage.formHorizontal, +}); const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ onFormLayoutDirectionChange: (formHorizontal: boolean) => { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.types.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.types.js index b115e61e59..f2b83e3992 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.types.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/NewEventDataEntryWrapper.types.js @@ -1,5 +1,4 @@ // @flow -import type { ProgramStage, RenderFoundation } from '../../../../metaData'; export type PlainProps = {| ...CssClasses, @@ -10,18 +9,15 @@ export type Props = {| dataEntryHasChanges: boolean, formHorizontal: ?boolean, onFormLayoutDirectionChange: (formHorizontal: boolean) => void, - formFoundation: ?RenderFoundation, - stage: ?ProgramStage, |} export type StateProps = {| dataEntryHasChanges: boolean, formHorizontal: ?boolean, - formFoundation: ?RenderFoundation, - stage: ?ProgramStage, |} export type ContainerProps = {| + |}; export type MapStateToProps = (ReduxState, ContainerProps) => StateProps; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/newEventDataEntryWrapper.selectors.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/newEventDataEntryWrapper.selectors.js deleted file mode 100644 index 438d98822e..0000000000 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/newEventDataEntryWrapper.selectors.js +++ /dev/null @@ -1,54 +0,0 @@ -// @flow -import { createSelector } from 'reselect'; -import log from 'loglevel'; -import { errorCreator } from 'capture-core-utils'; -import { programCollection } from '../../../../metaDataMemoryStores/programCollection/programCollection'; - -const programIdSelector = state => state.currentSelections.programId; -const programStageIdSelector = state => state.currentSelections.stageId; - -// $FlowFixMe[missing-annot] automated comment -export const makeFormFoundationSelector = () => createSelector( - programIdSelector, - programStageIdSelector, - (programId: string, programStageId?: string) => { - const program = programCollection.get(programId); - if (!program) { - log.error(errorCreator('programId not found')({ method: 'getFormFoundation' })); - return null; - } - - - // $FlowFixMe[prop-missing] automated comment - const stage = programStageId ? program.getStage(programStageId) : program.stage; - if (!stage) { - log.error(errorCreator('stage not found for program')({ method: 'getFormFoundation' })); - return null; - } - - return stage.stageForm; - }, -); - -// $FlowFixMe[missing-annot] automated comment -export const makeStageSelector = () => createSelector( - programIdSelector, - programStageIdSelector, - (programId: string, programStageId?: string) => { - const program = programCollection.get(programId); - if (!program) { - log.error(errorCreator('programId not found')({ method: 'getFormFoundation' })); - return null; - } - - - // $FlowFixMe[prop-missing] automated comment - const stage = programStageId ? program.getStage(programStageId) : program.stage; - if (!stage) { - log.error(errorCreator('stage not found for program')({ method: 'getFormFoundation' })); - return null; - } - - return stage; - }, -); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/useRulesEngine.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/useRulesEngine.js index b391b885f9..61e646ec29 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/useRulesEngine.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/useRulesEngine.js @@ -14,7 +14,7 @@ export const useRulesEngine = ({ }: { programId: string, orgUnit: ?OrgUnit, - formFoundation: RenderFoundation, + formFoundation: ?RenderFoundation, }) => { const dispatch = useDispatch(); const program = useMemo(() => programId && getEventProgramThrowIfNotFound(programId), [programId]); @@ -25,7 +25,7 @@ export const useRulesEngine = ({ // Refactor the helper methods (getCurrentClientValues, getCurrentClientMainData in rules/actionsCreator) to be more explicit with the arguments. const state = useSelector(stateArg => stateArg); useEffect(() => { - if (orgUnit && program) { + if (orgUnit && program && !!formFoundation) { dispatch(batchActions([ getRulesActions({ state, @@ -42,6 +42,7 @@ export const useRulesEngine = ({ dispatch, program, orgUnit, + formFoundation, ]); return !!orgUnit && orgUnitRef.current === orgUnit; diff --git a/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/buildProgramStageMetadata.js b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/buildProgramStageMetadata.js new file mode 100644 index 0000000000..8c3977382e --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/buildProgramStageMetadata.js @@ -0,0 +1,45 @@ +// @flow +import type { + CachedDataElement, + CachedOptionSet, + CachedProgramStage, +} from '../../../../storageControllers'; +import type { DataEntryFormConfig } from '../TEIAndEnrollment'; +import { getUserStorageController, userStores } from '../../../../storageControllers'; +import { ProgramStageFactory } from '../../../../metaDataMemoryStoreBuilders/programs/factory/programStage'; + +export const buildProgramStageMetadata = async ({ + cachedProgramStage, + programId, + cachedDataElements, + cachedOptionSets, + locale, + minorServerVersion, + dataEntryFormConfig, +}: { + cachedProgramStage: CachedProgramStage, + programId: string, + cachedOptionSets: Array, + cachedDataElements: Array, + dataEntryFormConfig: ?DataEntryFormConfig, + locale: string, + minorServerVersion: number, +}) => { + const storageController = getUserStorageController(); + + const cachedRelationshipTypes = await storageController.getAll(userStores.RELATIONSHIP_TYPES); + + const programStageFactory = new ProgramStageFactory({ + cachedOptionSets: new Map(cachedOptionSets.map(optionSet => [optionSet.id, optionSet])), + cachedRelationshipTypes, + cachedDataElements: new Map(cachedDataElements.map(dataElement => [dataElement.id, dataElement])), + locale, + minorServerVersion, + dataEntryFormConfig, + }); + + return programStageFactory.build( + cachedProgramStage, + programId, + ); +}; diff --git a/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/index.js b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/index.js new file mode 100644 index 0000000000..82843ab868 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/index.js @@ -0,0 +1,4 @@ +// @flow + +export { useDataElementsForStage } from './useDataElementsForStage'; +export { buildProgramStageMetadata } from './buildProgramStageMetadata'; diff --git a/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useDataElementsForStage.js b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useDataElementsForStage.js new file mode 100644 index 0000000000..0136104bff --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useDataElementsForStage.js @@ -0,0 +1,41 @@ +// @flow +import { useIndexedDBQuery } from '../../../../utils/reactQueryHelpers'; +import { getUserStorageController, userStores } from '../../../../storageControllers'; + +type Props = {| + programId: string, + dataElementIds: Array, + stageId?: string, +|} + +const getDataElementsForStage = async ({ + dataElementIds, +}) => { + const storageController = getUserStorageController(); + + return storageController.getAll(userStores.DATA_ELEMENTS, { + predicate: dataElement => dataElementIds.includes(dataElement.id), + }); +}; + +export const useDataElementsForStage = ({ + dataElementIds, + programId, + stageId, +}: Props) => { + const { data, isLoading } = useIndexedDBQuery( + // $FlowFixMe + [programId, 'dataElements', stageId, { dataElementIds }], + () => getDataElementsForStage({ + dataElementIds, + }), + { + enabled: !!dataElementIds, + }, + ); + + return { + dataElements: data, + isLoading, + }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useMetadataForProgramStage.js b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useMetadataForProgramStage.js new file mode 100644 index 0000000000..204d7330d6 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/common/ProgramStage/useMetadataForProgramStage.js @@ -0,0 +1,96 @@ +// @flow +import { useMemo } from 'react'; +import { useConfig } from '@dhis2/app-runtime'; +import type { ProgramStage, RenderFoundation } from '../../../../metaData'; +import { useProgramFromIndexedDB } from '../../../../utils/cachedDataHooks/useProgramFromIndexedDB'; +import { useUserLocale } from '../../../../utils/localeData/useUserLocale'; +import { useDataEntryFormConfig, useOptionSetsForAttributes } from '../TEIAndEnrollment'; +import { useDataElementsForStage } from './useDataElementsForStage'; +import { useIndexedDBQuery } from '../../../../utils/reactQueryHelpers'; +import { buildProgramStageMetadata } from './buildProgramStageMetadata'; + +type Props = {| + programId: string, + stageId?: string, +|} + +type ReturnType = {| + formFoundation: ?RenderFoundation, + stage: ?ProgramStage, + isLoading: boolean, + isError: boolean, +|} + +export const useMetadataForProgramStage = ({ + programId, + stageId, +}: Props): ReturnType => { + const scopeId = stageId || programId; + const { program } = useProgramFromIndexedDB(programId, { enabled: !!programId }); + const { locale } = useUserLocale(); + const { serverVersion: { minor } } = useConfig(); + const { dataEntryFormConfig, configIsFetched } = useDataEntryFormConfig({ selectedScopeId: scopeId }); + + const programStage = useMemo(() => { + if (!stageId) { + return program?.programStages[0]; + } + + return program?.programStages.find(ps => ps.id === stageId); + }, [program?.programStages, stageId]); + + const dataElementIds = useMemo(() => { + if (!programStage) return []; + + return programStage + .programStageDataElements + .map(dataElement => dataElement.dataElementId); + }, [programStage]); + + const { dataElements } = useDataElementsForStage({ + programId, + stageId, + dataElementIds, + }); + + const { optionSets } = useOptionSetsForAttributes({ + attributes: dataElements, + selectedScopeId: scopeId, + }); + + const { data: programStageMetadata, isIdle, isLoading, isError } = useIndexedDBQuery( + // $FlowFixMe + ['programStageMetadata', programId, stageId], + // $FlowFixMe + () => buildProgramStageMetadata({ + // $FlowFixMe + cachedProgramStage: programStage, + // $FlowFixMe + cachedDataElements: dataElements, + programId, + // $FlowFixMe + cachedOptionSets: optionSets, + locale, + minorServerVersion: minor, + dataEntryFormConfig, + }), + { + cacheTime: Infinity, + staleTime: Infinity, + enabled: !!program + && !!programId + && !!dataElements + && !!optionSets + && !!locale + && !!minor + && configIsFetched, + }, + ); + + return { + formFoundation: programStageMetadata?.stageForm, + stage: programStageMetadata, + isLoading: isLoading || isIdle, + isError, + }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js index f82d21b393..34bf6e716b 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js @@ -1,3 +1,13 @@ // @flow -export { getGeneratedUniqueValuesAsync, getUniqueValuesForAttributesWithoutValue } from './getGeneratedUniqueValuesAsync'; +export { + getGeneratedUniqueValuesAsync, + getUniqueValuesForAttributesWithoutValue, +} from './getGeneratedUniqueValuesAsync'; export { geometryType, getPossibleTetFeatureTypeKey, buildGeometryProp } from './geometry'; +export type { DataEntryFormConfig } from './useMetadataForRegistrationForm/types'; +export { + useDataEntryFormConfig, +} from './useMetadataForRegistrationForm/hooks/useDataEntryFormConfig'; +export { + useOptionSetsForAttributes, +} from './useMetadataForRegistrationForm'; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js index 8c7eae8daf..1a3acacd6e 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js @@ -1,2 +1,4 @@ // @flow export { useMetadataForRegistrationForm, FieldElementObjectTypes } from './useMetadataForRegistrationForm'; +export type { DataEntryFormConfig } from './types'; +export { useOptionSetsForAttributes } from './hooks/useOptionSetsForAttributes'; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js index 97488cf7fd..baeb325a66 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js @@ -16,6 +16,7 @@ type Props = {| |} export const FieldElementObjectTypes = Object.freeze({ + // TODO [DHIS2-17605] - Unify TEA and DataElement to a common key TRACKED_ENTITY_ATTRIBUTE: 'TrackedEntityAttribute', ATTRIBUTE: 'Attribute', }); diff --git a/src/core_modules/capture-core/components/DataEntries/index.js b/src/core_modules/capture-core/components/DataEntries/index.js index 486f2c02ab..6d73c67fbd 100644 --- a/src/core_modules/capture-core/components/DataEntries/index.js +++ b/src/core_modules/capture-core/components/DataEntries/index.js @@ -26,4 +26,3 @@ export { SingleEventRegistrationEntry } from './SingleEventRegistrationEntry/Sin export type { SaveForDuplicateCheck as SaveForEnrollmentAndTeiRegistration } from './common/TEIAndEnrollment/DuplicateCheckOnSave'; export type { ExistingUniqueValueDialogActionsComponent } from './withErrorMessagePostProcessor'; export { withAskToCompleteEnrollment } from './common/trackerEvent'; - diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 5bb044b6a6..03b56579de 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -20,6 +20,7 @@ export const EnrollmentEditEventPageComponent = ({ teiId, enrollmentId, eventId, + stageId, trackedEntityTypeId, program, enrollmentsAsOptions, @@ -81,6 +82,7 @@ export const EnrollmentEditEventPageComponent = ({ teiId={teiId} enrollmentId={enrollmentId} eventId={eventId} + stageId={stageId} eventStatus={eventStatus} initialScheduleDate={scheduleDate} onCancelEditEvent={onCancelEditEvent} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index dccb301c52..9ba1bfcdd8 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -247,6 +247,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ teiId={teiId} enrollmentId={enrollmentId} eventId={eventId} + stageId={stageId} trackedEntityTypeId={trackedEntityTypeId} enrollmentsAsOptions={enrollmentsAsOptions} teiDisplayName={teiDisplayName} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index ced8721f8f..e164d9fc02 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -16,6 +16,7 @@ export type PlainProps = {| teiId: string, enrollmentId: string, eventId: string, + stageId: string, program: Program, trackedEntityTypeId: string, mode: string, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js index 40caaa8bb2..70ee627719 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js @@ -21,6 +21,7 @@ import { ReactQueryAppNamespace } from '../../../../utils/reactQueryHelpers'; import { CHANGELOG_ENTITY_TYPES } from '../../../WidgetsChangelog'; import { useCategoryCombinations } from '../../../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; import type { ProgramCategory } from '../../../WidgetEventSchedule/CategoryOptions/CategoryOptions.types'; +import { useMetadataForProgramStage } from '../../../DataEntries/common/ProgramStage/useMetadataForProgramStage'; const getStyles = () => ({ container: { @@ -79,11 +80,12 @@ const EventDetailsSectionPlain = (props: Props) => { showEditEvent, programStage, eventAccess, - programId, onBackToAllEvents, + programId, ...passOnProps } = props; const orgUnitId = useSelector(({ viewEventPage }) => viewEventPage.loadedValues?.orgUnit?.id); + const { formFoundation } = useMetadataForProgramStage({ programId }); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); const { programCategory, isLoading } = useCategoryCombinations(programId); const queryClient = useQueryClient(); @@ -101,31 +103,28 @@ const EventDetailsSectionPlain = (props: Props) => { return error.errorComponent; } - const renderDataEntryContainer = () => { - const formFoundation = programStage.stageForm; - return ( -
- {showEditEvent ? - // $FlowFixMe[cannot-spread-inexact] automated comment - : - // $FlowFixMe[cannot-spread-inexact] automated comment - - } -
- ); - }; + const renderDataEntryContainer = () => ( +
+ {showEditEvent ? + // $FlowFixMe[cannot-spread-inexact] automated comment + : + // $FlowFixMe[cannot-spread-inexact] automated comment + + } +
+ ); const renderActionsContainer = () => { const canEdit = eventAccess.write; @@ -174,8 +173,11 @@ const EventDetailsSectionPlain = (props: Props) => { ); }; + if (!orgUnit || !formFoundation || isLoading) { + return null; + } - return orgUnit && !isLoading ? ( + return (
{
{renderDataEntryContainer()}
- {showEditEvent && } + {showEditEvent && ( + + )}
{supportsChangelog && changeLogIsOpen && ( { /> )}
- ) : null; + ); }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js index 3180e42d27..4be7f82fd6 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js @@ -206,13 +206,13 @@ export const EnrollmentWidget: WidgetConfig = { export const EditEventWorkspace: WidgetConfig = { Component: WidgetEventEditWrapper, getProps: ({ - programStage, onGoBack, program, orgUnitId, teiId, enrollmentId, eventId, + stageId, eventStatus, onCancelEditEvent, onHandleScheduleSave, @@ -223,9 +223,9 @@ export const EditEventWorkspace: WidgetConfig = { onSaveAndCompleteEnrollmentErrorActionType, onSaveAndCompleteEnrollmentSuccessActionType, }): WidgetEventEditProps => ({ - programStage, onGoBack, programId: program.id, + stageId, orgUnitId, teiId, enrollmentId, diff --git a/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.js b/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.js index 27f333189e..815957bf98 100644 --- a/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.js +++ b/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.js @@ -5,13 +5,21 @@ import { pageStatuses } from '../../EnrollmentEditEvent/EnrollmentEditEventPage. import { IncompleteSelectionsMessage } from '../../../IncompleteSelectionsMessage'; import { WidgetEventEdit } from '../../../WidgetEventEdit'; import type { Props } from '../../../WidgetEventEdit/widgetEventEdit.types'; +import { useMetadataForProgramStage } from '../../../DataEntries/common/ProgramStage/useMetadataForProgramStage'; type WidgetProps = {| pageStatus: string, ...Props, |} -export const WidgetEventEditWrapper = ({ pageStatus, ...passOnProps }: WidgetProps) => { +export const WidgetEventEditWrapper = ({ pageStatus, programId, stageId, ...passOnProps }: WidgetProps) => { + const { + formFoundation, + stage, + isLoading, + isError, + } = useMetadataForProgramStage({ programId, stageId }); + if (pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED) { return ( @@ -26,9 +34,21 @@ export const WidgetEventEditWrapper = ({ pageStatus, ...passOnProps }: WidgetPro ); } + if (isLoading || !formFoundation || !stage || isError) { + return ( +
+ {i18n.t('Loading')} +
+ ); + } + return ( ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.js index 359c66d9e4..55e58b2a60 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.js @@ -4,6 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { getProgramAndStageForProgram, TrackerProgram } from '../../metaData'; import { AccessVerification } from './AccessVerification'; import type { WidgetProps } from './WidgetEnrollmentEventNew.types'; +import { useMetadataForProgramStage } from '../DataEntries/common/ProgramStage/useMetadataForProgramStage'; export const WidgetEnrollmentEventNew = ({ programId, @@ -11,17 +12,29 @@ export const WidgetEnrollmentEventNew = ({ onSave, ...passOnProps }: WidgetProps) => { - const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const { program } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const { + stage, + formFoundation, + isLoading, + isError, + } = useMetadataForProgramStage({ programId, stageId }); - if (!program || !stage || !(program instanceof TrackerProgram)) { + if (isLoading) { return (
- {i18n.t('program or stage is invalid')}; + {i18n.t('Loading')}
); } - const formFoundation = stage.stageForm; + if (!program || !stage || !(program instanceof TrackerProgram) || isError || !formFoundation) { + return ( +
+ {i18n.t('program or stage is invalid')}; +
+ ); + } return ( viewEventPage.loadedValues); - const eventAccess = getProgramEventAccess(programId, programStage.id); - const availableProgramStages = useAvailableProgramStages(programStage, teiId, enrollmentId, programId); + const eventAccess = getProgramEventAccess(programId, stageId); + const availableProgramStages = useAvailableProgramStages(stage, teiId, enrollmentId, programId); const { programCategory } = useCategoryCombinations(programId); if (error) { return error.errorComponent; } + const { icon, name } = stage; return orgUnit && loadedValues ? (
@@ -178,17 +180,17 @@ export const WidgetEventEditPlain = ({ {currentPageMode === dataEntryKeys.VIEW ? ( ) : ( )}
) : ; }; -export const WidgetEventEdit: ComponentType = withStyles(styles)(WidgetEventEditPlain); +export const WidgetEventEdit: ComponentType = withStyles(styles)(WidgetEventEditPlain); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js index 766c3e42d6..2018cc24bb 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js @@ -1,10 +1,8 @@ // @flow - -import type { ProgramStage } from '../../metaData'; import type { UserFormField } from '../FormFields/UserField'; +import { ProgramStage, RenderFoundation } from '../../metaData'; export type Props = {| - programStage: ProgramStage, eventStatus?: string, onGoBack: () => void, onCancelEditEvent: (isScheduled: boolean) => void, @@ -14,6 +12,7 @@ export type Props = {| programId: string, enrollmentId: string, eventId: string, + stageId: string, teiId: string, initialScheduleDate?: string, assignee?: UserFormField | null, @@ -22,7 +21,13 @@ export type Props = {| onSaveAndCompleteEnrollmentErrorActionType?: string, |}; -export type PlainProps = {| +export type ComponentProps = {| ...Props, + formFoundation: RenderFoundation, + stage: ProgramStage, +|}; + +export type PlainProps = {| + ...ComponentProps, ...CssClasses, |} diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js index 3618c4ad27..d865498f0a 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js @@ -18,7 +18,7 @@ import { DataElementFactory } from './DataElementFactory'; import type { ConstructorInput } from './enrollmentFactory.types'; import { transformTrackerNode } from '../transformNodeFuntions/transformNodeFunctions'; import { FormFieldPluginConfig } from '../../../../metaData/FormFieldPluginConfig'; -import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/types'; +import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment'; import { FormFieldTypes } from '../../../../components/D2Form/FormFieldPlugin/FormFieldPlugin.const'; import { FieldElementObjectTypes, diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js index 88426041a7..0d3dafaa13 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js @@ -128,8 +128,9 @@ export class DataElementFactory { async build( cachedProgramStageDataElement: CachedProgramStageDataElement, section: ?Section, + cachedDataElementDefinition?: CachedDataElement, ): Promise { - const cachedDataElement = + const cachedDataElement = cachedDataElementDefinition || await getUserStorageController().get(userStores.DATA_ELEMENTS, cachedProgramStageDataElement.dataElementId); if (!cachedDataElement) { diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js index 3d61b8364b..532b40086a 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js @@ -7,11 +7,11 @@ import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirst import { camelCaseUppercaseString } from 'capture-core-utils/string/getCamelCaseFromUppercase'; import type { CachedProgramStageDataElement, - CachedSectionDataElements, CachedProgramStageSection, CachedProgramStage, CachedProgramStageDataElementsAsObject, CachedOptionSet, + CachedDataElement, } from '../../../../storageControllers/cache.types'; import { Section, ProgramStage, RenderFoundation, CustomForm } from '../../../../metaData'; import { buildIcon } from '../../../common/helpers'; @@ -20,6 +20,12 @@ import { DataElementFactory } from './DataElementFactory'; import { RelationshipTypesFactory } from './RelationshipTypesFactory'; import type { ConstructorInput, SectionSpecs } from './programStageFactory.types'; import { transformEventNode } from '../transformNodeFuntions/transformNodeFunctions'; +import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment'; +import { FormFieldTypes } from '../../../../components/D2Form/FormFieldPlugin/FormFieldPlugin.const'; +import { FormFieldPluginConfig } from '../../../../metaData/FormFieldPluginConfig'; +import { + FieldElementObjectTypes, +} from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm'; export class ProgramStageFactory { static CUSTOM_FORM_TEMPLATE_ERROR = 'Error in custom form template'; @@ -27,24 +33,30 @@ export class ProgramStageFactory { cachedOptionSets: Map; locale: ?string; dataElementFactory: DataElementFactory; + cachedDataElements: ?Map; relationshipTypesFactory: RelationshipTypesFactory; + dataEntryFormConfig: ?DataEntryFormConfig; constructor({ cachedOptionSets, cachedRelationshipTypes, + cachedDataElements, locale, minorServerVersion, + dataEntryFormConfig, }: ConstructorInput) { this.cachedOptionSets = cachedOptionSets; this.locale = locale; this.relationshipTypesFactory = new RelationshipTypesFactory( cachedRelationshipTypes, ); + this.cachedDataElements = cachedDataElements; this.dataElementFactory = new DataElementFactory( cachedOptionSets, locale, minorServerVersion, ); + this.dataEntryFormConfig = dataEntryFormConfig; } async _buildSection( @@ -58,17 +70,61 @@ export class ProgramStageFactory { if (sectionSpecs.dataElements) { // $FlowFixMe - await sectionSpecs.dataElements.asyncForEach(async (sectionDataElement: CachedSectionDataElements) => { - const id = sectionDataElement.id; - const cachedProgramStageDataElement = cachedProgramStageDataElements[id]; - if (!cachedProgramStageDataElement) { - log.error( - errorCreator('could not find programStageDataElement')( - { sectionDataElement })); - return; + await sectionSpecs.dataElements.asyncForEach(async (sectionDataElement) => { + if (sectionDataElement.type === FormFieldTypes.PLUGIN) { + const attributes = sectionDataElement.fieldMap + .filter(attributeField => attributeField.objectType === FieldElementObjectTypes.ATTRIBUTE) + .reduce((acc, attribute) => { + acc[attribute.IdFromApp] = attribute; + return acc; + }, {}); + + const element = new FormFieldPluginConfig((o) => { + o.id = sectionDataElement.id; + o.name = sectionDataElement.name; + o.pluginSource = sectionDataElement.pluginSource; + o.fields = new Map(); + o.customAttributes = attributes; + }); + + await sectionDataElement.fieldMap.asyncForEach(async (field) => { + if (field.objectType && field.objectType === FieldElementObjectTypes.TRACKED_ENTITY_ATTRIBUTE) { + const id = field.dataElementId; + const cachedProgramStageDataElement = cachedProgramStageDataElements[id]; + if (!cachedProgramStageDataElement) { + log.error( + errorCreator('could not find programStageDataElement')( + { sectionDataElement })); + return; + } + const currentField = await this.dataElementFactory + .build(cachedProgramStageDataElement, section); + currentField && element.addField(field.IdFromPlugin, currentField); + } + }); + + element && section.addElement(element); + } else { + const id = sectionDataElement.id; + const cachedProgramStageDataElement = cachedProgramStageDataElements[id]; + const cachedDataElementDefinition = this + .cachedDataElements + ?.get(cachedProgramStageDataElement.dataElementId); + + if (!cachedProgramStageDataElement) { + log.error( + errorCreator('could not find programStageDataElement')( + { sectionDataElement })); + return; + } + + const element = await this.dataElementFactory.build( + cachedProgramStageDataElement, + section, + cachedDataElementDefinition, + ); + element && section.addElement(element); } - const element = await this.dataElementFactory.build(cachedProgramStageDataElement, section); - element && section.addElement(element); }); } @@ -83,7 +139,15 @@ export class ProgramStageFactory { if (cachedProgramStageDataElements) { // $FlowFixMe await cachedProgramStageDataElements.asyncForEach((async (cachedProgramStageDataElement) => { - const element = await this.dataElementFactory.build(cachedProgramStageDataElement, section); + const cachedDataElementDefinition = this + .cachedDataElements + ?.get(cachedProgramStageDataElement.dataElementId); + + const element = await this.dataElementFactory.build( + cachedProgramStageDataElement, + section, + cachedDataElementDefinition, + ); element && section.addElement(element); })); } @@ -167,6 +231,63 @@ export class ProgramStageFactory { // $FlowFixMe { template: dataEntryForm.htmlCode, error })); } + } else if (this.dataEntryFormConfig) { + const dataElementDictionary = cachedProgramStage.programStageDataElements.reduce((acc, dataElement) => { + acc[dataElement.dataElementId] = dataElement; + return acc; + }, {}); + + // $FlowFixMe + await this.dataEntryFormConfig.asyncForEach(async (formConfigSection) => { + const formElements = formConfigSection.elements.reduce((acc, element) => { + if (element.type === FormFieldTypes.PLUGIN) { + const fieldMap = element + .fieldMap + ?.map(field => ({ + ...field, + ...dataElementDictionary[field.IdFromApp], + })); + + acc.push({ + ...element, + fieldMap, + }); + return acc; + } + + const dataElement = dataElementDictionary[element.id]; + if (dataElement) { + acc.push({ + ...dataElement, + id: element.id, + }); + } + return acc; + }, []); + + if (isNonEmptyArray(formElements)) { + const cachedProgramStageDataElementsAsObject = + ProgramStageFactory._convertProgramStageDataElementsToObject( + cachedProgramStage.programStageDataElements, + ); + + const metadataSection = cachedProgramStage.programStageSections?.find( + section => section.id === formConfigSection.id, + ); + + const section = await this._buildSection( + cachedProgramStageDataElementsAsObject, + { + id: formConfigSection.id, + displayName: metadataSection?.displayName || formConfigSection.name, + displayDescription: metadataSection?.displayDescription || '', + dataElements: formElements, + }, + ); + + stageForm.addSection(section); + } + }); } else if (isNonEmptyArray(cachedProgramStage.programStageSections)) { const cachedProgramStageDataElementsAsObject = ProgramStageFactory._convertProgramStageDataElementsToObject( diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/programStageFactory.types.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/programStageFactory.types.js index 675f517d96..5e676cc968 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/programStageFactory.types.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/programStageFactory.types.js @@ -1,13 +1,19 @@ // @flow import type { + CachedDataElement, CachedOptionSet, CachedRelationshipType, CachedSectionDataElements, } from '../../../../storageControllers/cache.types'; +import type { + DataEntryFormConfig, +} from '../../../../components/DataEntries/common/TEIAndEnrollment'; export type ConstructorInput = {| cachedOptionSets: Map, + cachedDataElements?: Map, cachedRelationshipTypes: Array, + dataEntryFormConfig?: ?DataEntryFormConfig, locale: ?string, minorServerVersion: number, |}; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TeiRegistrationFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TeiRegistrationFactory.js index ca76b17d4a..19366cbc51 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TeiRegistrationFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TeiRegistrationFactory.js @@ -18,7 +18,7 @@ import type { import { DataElementFactory } from './DataElementFactory'; import type { ConstructorInput } from './teiRegistrationFactory.types'; import { FormFieldPluginConfig } from '../../../../metaData/FormFieldPluginConfig'; -import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/types'; +import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment'; import { FormFieldTypes } from '../../../../components/D2Form/FormFieldPlugin/FormFieldPlugin.const'; import { FieldElementObjectTypes, diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/teiRegistrationFactory.types.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/teiRegistrationFactory.types.js index 972a70293b..0637beff26 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/teiRegistrationFactory.types.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/teiRegistrationFactory.types.js @@ -3,7 +3,7 @@ import type { CachedTrackedEntityAttribute, CachedOptionSet, } from '../../../../storageControllers/cache.types'; -import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/types'; +import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment'; export type ConstructorInput = {| cachedTrackedEntityAttributes: Map, diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/trackedEntityTypeFactory.types.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/trackedEntityTypeFactory.types.js index 90bb283571..9606106b78 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/trackedEntityTypeFactory.types.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/trackedEntityTypeFactory.types.js @@ -6,7 +6,7 @@ import type } from '../../../../storageControllers/cache.types'; import type { DataEntryFormConfig, -} from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/types'; +} from '../../../../components/DataEntries/common/TEIAndEnrollment'; export type ConstructorInput = {| cachedTrackedEntityAttributes: Map, From 32fadca2f29d4cd112f87c1eb76ea1f71b219b16 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Wed, 7 Aug 2024 22:11:08 +0000 Subject: [PATCH 05/33] chore(release): cut 100.74.0 [skip release] # [100.74.0](https://github.com/dhis2/capture-app/compare/v100.73.0...v100.74.0) (2024-08-07) ### Features * [DHIS2-17591][DHIS2-17607] Plugins in event forms ([#3684](https://github.com/dhis2/capture-app/issues/3684)) ([205b9f5](https://github.com/dhis2/capture-app/commit/205b9f5746f3d9b3d0d28035fd99e40b5649003c)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d081059b..1e1f87390b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [100.74.0](https://github.com/dhis2/capture-app/compare/v100.73.0...v100.74.0) (2024-08-07) + + +### Features + +* [DHIS2-17591][DHIS2-17607] Plugins in event forms ([#3684](https://github.com/dhis2/capture-app/issues/3684)) ([205b9f5](https://github.com/dhis2/capture-app/commit/205b9f5746f3d9b3d0d28035fd99e40b5649003c)) + # [100.73.0](https://github.com/dhis2/capture-app/compare/v100.72.0...v100.73.0) (2024-08-07) diff --git a/package.json b/package.json index d0c4f8e1f4..86975c92d7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.73.0", + "version": "100.74.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.73.0", + "@dhis2/rules-engine-javascript": "100.74.0", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index bb0e1d396f..e494b1a693 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.73.0", + "version": "100.74.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From d783ade253e27badf03d267d8a3935186b170fd8 Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Thu, 8 Aug 2024 00:40:58 +0200 Subject: [PATCH 06/33] feat: [DHIS2-17726] Plugins in Profile Widget (#3709) --- .../FormFieldPlugin/FormFieldPlugin.const.js | 1 + .../common/TEIAndEnrollment/index.js | 1 + .../useMetadataForRegistrationForm/index.js | 1 + .../DataEntry/DataEntry.container.js | 2 + .../FormFoundation/RenderFoundation.js | 160 +++++++++++++++--- .../DataEntry/FormFoundation/types.js | 8 + .../DataEntry/dataEntry.types.js | 4 + .../DataEntry/hooks/useFormFoundation.js | 13 +- .../DataEntry/hooks/useLifecycle.js | 5 +- .../WidgetProfile/WidgetProfile.component.js | 7 +- .../FormFieldPluginConfig.js | 6 +- 11 files changed, 172 insertions(+), 36 deletions(-) diff --git a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js index 23ae3ca48b..d9efc18ff3 100644 --- a/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js +++ b/src/core_modules/capture-core/components/D2Form/FormFieldPlugin/FormFieldPlugin.const.js @@ -7,6 +7,7 @@ export const PluginErrorMessages = Object.freeze({ }); export const FormFieldTypes = Object.freeze({ + // TODO [DHIS2-17605] - Unified field types DATA_ELEMENT: 'dataElement', PLUGIN: 'plugin', }); diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js index 34bf6e716b..9551945fd2 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/index.js @@ -10,4 +10,5 @@ export { } from './useMetadataForRegistrationForm/hooks/useDataEntryFormConfig'; export { useOptionSetsForAttributes, + FieldElementObjectTypes, } from './useMetadataForRegistrationForm'; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js index 1a3acacd6e..7f77c53ea0 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/index.js @@ -1,4 +1,5 @@ // @flow export { useMetadataForRegistrationForm, FieldElementObjectTypes } from './useMetadataForRegistrationForm'; +export { useDataEntryFormConfig } from './hooks/useDataEntryFormConfig'; export type { DataEntryFormConfig } from './types'; export { useOptionSetsForAttributes } from './hooks/useOptionSetsForAttributes'; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js index 7edf4492f8..34a6eba39f 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.js @@ -20,6 +20,7 @@ export const DataEntry = ({ onSaveExternal, geometry, trackedEntityName, + dataEntryFormConfig, }: Props) => { const dataEntryId = 'trackedEntityProfile'; const itemId = 'edit'; @@ -34,6 +35,7 @@ export const DataEntry = ({ dataEntryId, itemId, geometry, + dataEntryFormConfig, }); const { formFoundation } = context; const { formValidated, errorsMessages, warningsMessages } = useFormValidations(dataEntryId, itemId, saveAttempted); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js index 4bd3fbed41..4e2d0b26e3 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js @@ -1,5 +1,7 @@ // @flow /* eslint-disable no-underscore-dangle */ +import log from 'loglevel'; +import { errorCreator } from 'capture-core-utils'; import i18n from '@dhis2/d2-i18n'; import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import type { @@ -7,15 +9,26 @@ import type { TrackedEntityAttribute, TrackedEntityType, OptionSet, + PluginElement, } from './types'; import { RenderFoundation, Section } from '../../../../metaData'; import { buildDataElement, buildTetFeatureType } from './DataElement'; import { getProgramTrackedEntityAttributes, getTrackedEntityTypeId } from '../helpers'; import type { QuerySingleResource } from '../../../../utils/api/api.types'; +import { FieldElementObjectTypes, type DataEntryFormConfig } from '../../../DataEntries/common/TEIAndEnrollment'; +import { FormFieldTypes } from '../../../D2Form/FormFieldPlugin/FormFieldPlugin.const'; +import { FormFieldPluginConfig } from '../../../../metaData/FormFieldPluginConfig'; const getFeatureType = (featureType: ?string) => (featureType ? capitalizeFirstLetter(featureType.toLowerCase()) : 'None'); +const isPluginElement = + (attribute: ProgramTrackedEntityAttribute | PluginElement): boolean %checks => attribute + .type === FormFieldTypes.PLUGIN; + +const isProgramTrackedEntityAttribute = + (attribute: ProgramTrackedEntityAttribute | PluginElement): boolean %checks => !isPluginElement(attribute); + const buildProgramSection = programSection => programSection.trackedEntityAttributes.map(({ id }) => id); const buildTetFeatureTypeField = (trackedEntityType: TrackedEntityType) => { @@ -62,7 +75,7 @@ const buildMainSection = async ({ trackedEntityType: TrackedEntityType, trackedEntityAttributes: Array, optionSets: Array, - programTrackedEntityAttributes?: ?Array, + programTrackedEntityAttributes?: ?Array, querySingleResource: QuerySingleResource, minorServerVersion: number, }) => { @@ -97,7 +110,7 @@ const buildElementsForSection = async ({ querySingleResource, minorServerVersion, }: { - programTrackedEntityAttributes: Array, + programTrackedEntityAttributes: Array, trackedEntityAttributes: Array, optionSets: Array, section: Section, @@ -105,15 +118,55 @@ const buildElementsForSection = async ({ minorServerVersion: number, }) => { for (const trackedEntityAttribute of programTrackedEntityAttributes) { - // eslint-disable-next-line no-await-in-loop - const element = await buildDataElement( - trackedEntityAttribute, - trackedEntityAttributes, - optionSets, - querySingleResource, - minorServerVersion, - ); - element && section.addElement(element); + if (isPluginElement(trackedEntityAttribute)) { + const pluginElement = ((trackedEntityAttribute: any): PluginElement); + + const attributes = pluginElement.fieldMap + .filter(attributeField => attributeField.objectType === FieldElementObjectTypes.ATTRIBUTE) + .reduce((acc, attribute) => { + acc[attribute.IdFromApp] = attribute; + return acc; + }, {}); + + const element = new FormFieldPluginConfig((o) => { + o.id = pluginElement.id; + o.name = pluginElement.name; + o.pluginSource = pluginElement.pluginSource; + o.fields = new Map(); + o.customAttributes = attributes; + }); + + /* eslint-disable no-await-in-loop */ + // $FlowFixMe + await pluginElement.fieldMap.asyncForEach(async (field) => { + if (field.objectType && field.objectType === FieldElementObjectTypes.TRACKED_ENTITY_ATTRIBUTE) { + const fieldElement = await buildDataElement( + field, + trackedEntityAttributes, + optionSets, + querySingleResource, + minorServerVersion, + ); + if (!fieldElement) return; + + element.addField(field.IdFromPlugin, fieldElement); + } + }); + /* eslint-enable no-await-in-loop */ + + element && section.addElement(element); + } else if (isProgramTrackedEntityAttribute(trackedEntityAttribute)) { + const programTrackedEntityAttribute = ((trackedEntityAttribute: any): ProgramTrackedEntityAttribute); + // eslint-disable-next-line no-await-in-loop + const element = await buildDataElement( + programTrackedEntityAttribute, + trackedEntityAttributes, + optionSets, + querySingleResource, + minorServerVersion, + ); + element && section.addElement(element); + } } return section; }; @@ -127,7 +180,7 @@ const buildSection = async ({ querySingleResource, minorServerVersion, }: { - programTrackedEntityAttributes?: Array, + programTrackedEntityAttributes?: Array, trackedEntityAttributes: Array, optionSets: Array, sectionCustomLabel: string, @@ -155,7 +208,7 @@ const buildSection = async ({ return section; }; -export const buildFormFoundation = async (program: any, querySingleResource: QuerySingleResource, minorServerVersion: number) => { +export const buildFormFoundation = async (program: any, querySingleResource: QuerySingleResource, minorServerVersion: number, dataEntryFormConfig: ?DataEntryFormConfig) => { const { programSections, trackedEntityType } = program; const programTrackedEntityAttributes = getProgramTrackedEntityAttributes(program.programTrackedEntityAttributes); const trackedEntityTypeId: string = getTrackedEntityTypeId(program); @@ -173,7 +226,7 @@ export const buildFormFoundation = async (program: any, querySingleResource: Que }); let section; - if (programSections?.length) { + if (programSections?.length || dataEntryFormConfig) { if (trackedEntityTypeId) { section = await buildTetFeatureTypeSection(trackedEntityTypeId, trackedEntityType); section && renderFoundation.addSection(section); @@ -187,20 +240,70 @@ export const buildFormFoundation = async (program: any, querySingleResource: Que return acc; }, {}); - for (const programSection of programSections) { - const builtProgramSection = buildProgramSection(programSection); - - // eslint-disable-next-line no-await-in-loop - section = await buildSection({ - programTrackedEntityAttributes: builtProgramSection.map(id => trackedEntityAttributeDictionary[id]), - trackedEntityAttributes, - optionSets, - sectionCustomLabel: programSection.displayFormName, - sectionCustomId: programSection.id, - querySingleResource, - minorServerVersion, + + if (dataEntryFormConfig) { + // $FlowFixMe + await dataEntryFormConfig.asyncForEach(async (formConfigSection) => { + const attributes = formConfigSection.elements.reduce((acc, element) => { + if (element.type === FormFieldTypes.PLUGIN) { + const fieldMap = element + .fieldMap + ?.map(field => ({ + ...field, + ...trackedEntityAttributeDictionary[field.IdFromApp], + })); + + acc.push({ + ...element, + fieldMap, + }); + return acc; + } + const attribute = trackedEntityAttributeDictionary[element.id]; + if (attribute) { + acc.push(attribute); + } + return acc; + }, []); + + const sectionMetadata = programSections + ?.find(cachedSection => cachedSection.id === formConfigSection.id); + + if (!sectionMetadata && programSections && programSections.length > 0) { + log.warn( + errorCreator('Could not find metadata for section. This could indicate that your form configuration may be out of sync with your metadata.')( + { sectionId: formConfigSection.id }, + ), + ); + } + + section = await buildSection({ + programTrackedEntityAttributes: attributes, + sectionCustomLabel: formConfigSection.name ?? sectionMetadata?.displayFormName ?? i18n.t('Profile'), + sectionCustomId: formConfigSection.id, + minorServerVersion, + trackedEntityAttributes, + optionSets, + querySingleResource, + }); + section && renderFoundation.addSection(section); }); - section && renderFoundation.addSection(section); + } else { + for (const programSection of programSections) { + const builtProgramSection = buildProgramSection(programSection); + + // eslint-disable-next-line no-await-in-loop + section = await buildSection({ + programTrackedEntityAttributes: builtProgramSection.map(id => trackedEntityAttributeDictionary[id]), + trackedEntityAttributes, + optionSets, + sectionCustomLabel: programSection.displayFormName, + sectionCustomId: programSection.id, + querySingleResource, + minorServerVersion, + }); + section && renderFoundation.addSection(section); + } } } } else { @@ -222,7 +325,8 @@ export const build = async ( setFormFoundation?: (formFoundation: RenderFoundation) => void, querySingleResource: QuerySingleResource, minorServerVersion: number, + dataEntryFormConfig: ?DataEntryFormConfig, ) => { - const formFoundation = (await buildFormFoundation(program, querySingleResource, minorServerVersion)) || {}; + const formFoundation = (await buildFormFoundation(program, querySingleResource, minorServerVersion, dataEntryFormConfig)) || {}; setFormFoundation && setFormFoundation(formFoundation); }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/types.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/types.js index b699c1b83c..c899a5efc8 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/types.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/types.js @@ -66,6 +66,14 @@ export type ProgramTrackedEntityAttribute = { allowFutureDate?: ?boolean, }; +export type PluginElement = { + id: string, + name: string, + type: string, + pluginSource: string, + fieldMap: Array<{ objectType: string, IdFromApp: string, IdFromPlugin: string }>, +}; + export type TrackedEntityType = { id: string, displayName: string, diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js index b067cac252..ddb4f533c9 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.js @@ -1,6 +1,9 @@ // @flow import type { Geometry } from './helpers/types'; +import type { + DataEntryFormConfig, +} from '../../DataEntries/common/TEIAndEnrollment'; export type PlainProps = {| dataEntryId: string, @@ -23,6 +26,7 @@ export type PlainProps = {| export type Props = {| programAPI: any, orgUnitId: string, + dataEntryFormConfig: ?DataEntryFormConfig, onCancel: () => void, onDisable: () => void, clientAttributesWithSubvalues: Array, diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useFormFoundation.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useFormFoundation.js index 80b1b5219a..9637d5295f 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useFormFoundation.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useFormFoundation.js @@ -3,16 +3,23 @@ import { useState, useEffect } from 'react'; import { useDataEngine, useConfig } from '@dhis2/app-runtime'; import { makeQuerySingleResource } from 'capture-core/utils/api'; import { buildFormFoundation } from '../FormFoundation'; +import type { DataEntryFormConfig } from '../../../DataEntries/common/TEIAndEnrollment'; -export const useFormFoundation = (programAPI: any) => { +export const useFormFoundation = (programAPI: any, dataEntryFormConfig: ?DataEntryFormConfig) => { const [formFoundation, setFormFoundation] = useState({}); const dataEngine = useDataEngine(); const { serverVersion: { minor: minorServerVersion } } = useConfig(); useEffect(() => { const querySingleResource = makeQuerySingleResource(dataEngine.query.bind(dataEngine)); - buildFormFoundation(programAPI, setFormFoundation, querySingleResource, minorServerVersion); - }, [programAPI, dataEngine, minorServerVersion]); + buildFormFoundation( + programAPI, + setFormFoundation, + querySingleResource, + minorServerVersion, + dataEntryFormConfig, + ); + }, [programAPI, dataEngine, minorServerVersion, dataEntryFormConfig]); return formFoundation; }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js index c60464a921..0847eda23d 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useLifecycle.js @@ -24,6 +24,7 @@ import { } from './index'; import type { Geometry } from '../helpers/types'; import { getRulesActionsForTEI } from '../ProgramRules'; +import type { DataEntryFormConfig } from '../../../DataEntries/common/TEIAndEnrollment'; export const useLifecycle = ({ programAPI, @@ -33,6 +34,7 @@ export const useLifecycle = ({ dataEntryId, itemId, geometry, + dataEntryFormConfig, }: { programAPI: any, orgUnitId: string, @@ -41,6 +43,7 @@ export const useLifecycle = ({ dataEntryId: string, itemId: string, geometry: ?Geometry, + dataEntryFormConfig: ?DataEntryFormConfig, }) => { const dispatch = useDispatch(); // TODO: Getting the entire state object is bad and this needs to be refactored. @@ -52,7 +55,7 @@ export const useLifecycle = ({ const otherEvents = useEvents(enrollment, dataElements); const orgUnit: ?OrgUnit = useOrganisationUnit(orgUnitId).orgUnit; const rulesContainer: ProgramRulesContainer = useRulesContainer(programAPI); - const formFoundation: RenderFoundation = useFormFoundation(programAPI); + const formFoundation: RenderFoundation = useFormFoundation(programAPI, dataEntryFormConfig); const { formValues, clientValues } = useFormValues({ formFoundation, clientAttributesWithSubvalues, orgUnit }); const { formGeometryValues, clientGeometryValues } = useGeometryValues({ geometry, diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js index 3e98d7db1f..f14f5fb3ec 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.js @@ -25,6 +25,9 @@ import { DataEntry, dataEntryActionTypes, TEI_MODAL_STATE, convertClientToView } import { ReactQueryAppNamespace } from '../../utils/reactQueryHelpers'; import { CHANGELOG_ENTITY_TYPES } from '../WidgetsChangelog'; import { OverflowMenu } from './OverflowMenu'; +import { + useDataEntryFormConfig, +} from '../DataEntries/common/TEIAndEnrollment'; const styles = { header: { @@ -65,6 +68,7 @@ const WidgetProfilePlain = ({ storedGeometry: trackedEntityInstance?.geometry, hasError: trackedEntityInstance?.hasError, })); + const { configIsFetched, dataEntryFormConfig } = useDataEntryFormConfig({ selectedScopeId: programId }); const { loading: trackedEntityInstancesLoading, error: trackedEntityInstancesError, @@ -84,7 +88,7 @@ const WidgetProfilePlain = ({ trackedEntityInstanceAttributes.length > 0 && trackedEntityTypeAccess?.data?.write && !readOnlyMode, [trackedEntityInstanceAttributes, readOnlyMode, trackedEntityTypeAccess]); - const loading = programsLoading || trackedEntityInstancesLoading || userRolesLoading; + const loading = programsLoading || trackedEntityInstancesLoading || userRolesLoading || !configIsFetched; const error = programsError || trackedEntityInstancesError || userRolesError; const clientAttributesWithSubvalues = useClientAttributesWithSubvalues(teiId, program, trackedEntityInstanceAttributes); const teiDisplayName = useTeiDisplayName(program, storedAttributeValues, clientAttributesWithSubvalues, teiId); @@ -176,6 +180,7 @@ const WidgetProfilePlain = ({ onCancel={() => setTeiModalState(TEI_MODAL_STATE.CLOSE)} onDisable={() => setTeiModalState(TEI_MODAL_STATE.OPEN_DISABLE)} programAPI={program} + dataEntryFormConfig={dataEntryFormConfig} orgUnitId={orgUnitId} clientAttributesWithSubvalues={clientAttributesWithSubvalues} userRoles={userRoles} diff --git a/src/core_modules/capture-core/metaData/FormFieldPluginConfig/FormFieldPluginConfig.js b/src/core_modules/capture-core/metaData/FormFieldPluginConfig/FormFieldPluginConfig.js index d2b4ee2417..655f0fa67a 100644 --- a/src/core_modules/capture-core/metaData/FormFieldPluginConfig/FormFieldPluginConfig.js +++ b/src/core_modules/capture-core/metaData/FormFieldPluginConfig/FormFieldPluginConfig.js @@ -11,7 +11,7 @@ export class FormFieldPluginConfig { _name: string; _pluginSource: string; _fields: Map; - _customAttributes: Map; + _customAttributes: { [string]: { IdFromPlugin: string, IdFromApp: string } }; constructor(initFn: ?(_this: FormFieldPluginConfig) => void) { initFn && isFunction(initFn) && initFn(this); @@ -45,11 +45,11 @@ export class FormFieldPluginConfig { return this._pluginSource; } - get customAttributes(): Map { + get customAttributes(): { [string]: { IdFromPlugin: string, IdFromApp: string } } { return this._customAttributes; } - set customAttributes(value: Map) { + set customAttributes(value: { [string]: { IdFromPlugin: string, IdFromApp: string } }) { this._customAttributes = value; } From e3acab8cf69596b44a19a1a6044680a01eeb78d0 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Wed, 7 Aug 2024 22:47:18 +0000 Subject: [PATCH 07/33] chore(release): cut 100.75.0 [skip release] # [100.75.0](https://github.com/dhis2/capture-app/compare/v100.74.0...v100.75.0) (2024-08-07) ### Features * [DHIS2-17726] Plugins in Profile Widget ([#3709](https://github.com/dhis2/capture-app/issues/3709)) ([d783ade](https://github.com/dhis2/capture-app/commit/d783ade253e27badf03d267d8a3935186b170fd8)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1f87390b..0b934620d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [100.75.0](https://github.com/dhis2/capture-app/compare/v100.74.0...v100.75.0) (2024-08-07) + + +### Features + +* [DHIS2-17726] Plugins in Profile Widget ([#3709](https://github.com/dhis2/capture-app/issues/3709)) ([d783ade](https://github.com/dhis2/capture-app/commit/d783ade253e27badf03d267d8a3935186b170fd8)) + # [100.74.0](https://github.com/dhis2/capture-app/compare/v100.73.0...v100.74.0) (2024-08-07) diff --git a/package.json b/package.json index 86975c92d7..a05dad6df6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.74.0", + "version": "100.75.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.74.0", + "@dhis2/rules-engine-javascript": "100.75.0", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index e494b1a693..85d127be5a 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.74.0", + "version": "100.75.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 19c77ec50c2fff0deef948abd516726b51f95418 Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Thu, 8 Aug 2024 13:14:31 +0200 Subject: [PATCH 08/33] fix: [DHIS2-17859] Add missing ids to Enrollment plugin (#3748) --- .../renderPageComponents/renderPageComponents.js | 4 +++- .../Pages/common/EnrollmentPlugin/EnrollmentPlugin.js | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.js index 7d803df660..182b54368d 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/renderPageComponents/renderPageComponents.js @@ -61,11 +61,13 @@ const renderComponent = ( ); }; -const getPropsForPlugin = ({ program, enrollmentId, teiId, orgUnitId }) => ({ +const getPropsForPlugin = ({ program, enrollmentId, teiId, orgUnitId, programStage, eventId, stageId }) => ({ programId: program.id, enrollmentId, teiId, orgUnitId, + programStageId: stageId ?? programStage?.id, + eventId, }); const renderPlugin = ( diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentPlugin/EnrollmentPlugin.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentPlugin/EnrollmentPlugin.js index 1664de957e..df1911d8a8 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentPlugin/EnrollmentPlugin.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentPlugin/EnrollmentPlugin.js @@ -9,6 +9,8 @@ type EnrollmentPluginProps = {| teiId: string, orgUnitId: string, pluginSource: string, + programStageId?: string, + eventId?: string, |}; export const EnrollmentPlugin = ({ pluginSource, ...passOnProps }: EnrollmentPluginProps) => { From a99fd43678246327eadca4286d8388f5d5f420aa Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Thu, 8 Aug 2024 11:19:14 +0000 Subject: [PATCH 09/33] chore(release): cut 100.75.1 [skip release] ## [100.75.1](https://github.com/dhis2/capture-app/compare/v100.75.0...v100.75.1) (2024-08-08) ### Bug Fixes * [DHIS2-17859] Add missing ids to Enrollment plugin ([#3748](https://github.com/dhis2/capture-app/issues/3748)) ([19c77ec](https://github.com/dhis2/capture-app/commit/19c77ec50c2fff0deef948abd516726b51f95418)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b934620d4..a19cc0d31a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [100.75.1](https://github.com/dhis2/capture-app/compare/v100.75.0...v100.75.1) (2024-08-08) + + +### Bug Fixes + +* [DHIS2-17859] Add missing ids to Enrollment plugin ([#3748](https://github.com/dhis2/capture-app/issues/3748)) ([19c77ec](https://github.com/dhis2/capture-app/commit/19c77ec50c2fff0deef948abd516726b51f95418)) + # [100.75.0](https://github.com/dhis2/capture-app/compare/v100.74.0...v100.75.0) (2024-08-07) diff --git a/package.json b/package.json index a05dad6df6..1561abac2b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.75.0", + "version": "100.75.1", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.75.0", + "@dhis2/rules-engine-javascript": "100.75.1", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 85d127be5a..4781aa9628 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.75.0", + "version": "100.75.1", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 2f51805b469d74b57bff7c927a9aa76420913ea1 Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Thu, 8 Aug 2024 13:56:55 +0200 Subject: [PATCH 10/33] feat: [DHIS2-17171] preview images in versions prior to 41 (#3694) --- .../WidgetProfile/hooks/getSubValueForTei.js | 9 ++---- .../Stages/Stage/getEventDataWithSubValue.js | 26 ++++++--------- .../getEventListData/convertToClientEvents.js | 6 ++-- .../helpers/getListDataCommon/getSubvalues.js | 14 +++++--- .../getTeiListData/convertToClientTeis.js | 4 +-- .../capture-core/converters/clientToList.js | 8 +---- .../capture-core/converters/clientToView.js | 10 +----- .../capture-core/events/getSubValues.js | 32 ++++++------------- .../trackedEntityInstances/getSubValues.js | 14 +++----- 9 files changed, 42 insertions(+), 81 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/getSubValueForTei.js b/src/core_modules/capture-core/components/WidgetProfile/hooks/getSubValueForTei.js index 5d50f9aff8..241316d4eb 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/getSubValueForTei.js +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/getSubValueForTei.js @@ -28,22 +28,19 @@ const getFileResourceSubvalue = async ({ attribute, querySingleResource }: SubVa }; }; -const getImageResourceSubvalue = async ({ attribute, querySingleResource, minorServerVersion }: SubValueFunctionParams) => { +const getImageResourceSubvalue = async ({ attribute, minorServerVersion }: SubValueFunctionParams) => { const { id, value, teiId, programId, absoluteApiPath } = attribute; if (!value) return null; - const { displayName } = await querySingleResource({ resource: 'fileResources', id: value }); - const urls = hasAPISupportForFeature(minorServerVersion, FEATURES.trackerImageEndpoint) ? { url: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${id}/image?program=${programId}`, previewUrl: `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${id}/image?program=${programId}&dimension=small`, } : { - url: `${absoluteApiPath}/trackedEntityInstances/${teiId}/${id}/image`, - previewUrl: `${absoluteApiPath}/trackedEntityInstances/${teiId}/${id}/image`, + url: `${absoluteApiPath}/trackedEntityInstances/${teiId}/${id}/image?program=${programId}`, + previewUrl: `${absoluteApiPath}/trackedEntityInstances/${teiId}/${id}/image?program=${programId}&dimension=SMALL`, }; return { - name: displayName, value, ...urls, }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/getEventDataWithSubValue.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/getEventDataWithSubValue.js index 5042919cbc..29919f0fe8 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/getEventDataWithSubValue.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/getEventDataWithSubValue.js @@ -27,38 +27,32 @@ const getFileResourceSubvalue = async (keys: Object, querySingleResource: QueryS }, {}); }; -const getImageSubvalue = async (keys: Object, querySingleResource: QuerySingleResource, eventId: string, absoluteApiPath: string) => { - const promises = Object.keys(keys) - .map(async (key) => { +const getImageSubvalue = (keys: Object, querySingleResource: QuerySingleResource, eventId: string, absoluteApiPath: string) => ( + Object.keys(keys) + .map((key) => { const value = keys[key]; if (value) { - const { id, displayName: name } = await querySingleResource({ resource: `fileResources/${value}` }); return { - id, - name, + value, ...(featureAvailable(FEATURES.trackerImageEndpoint) ? { url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${key}/image`, previewUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${key}/image?dimension=small`, } : { url: `${absoluteApiPath}/events/files?dataElementUid=${key}&eventUid=${eventId}`, - previewUrl: `${absoluteApiPath}/events/files?dataElementUid=${key}&eventUid=${eventId}`, + previewUrl: `${absoluteApiPath}/events/files?dataElementUid=${key}&eventUid=${eventId}&dimension=SMALL`, } ), }; } return {}; - }); - - return (await Promise.all(promises)) - .reduce((acc, { id, name, url, previewUrl }) => { - if (id) { - acc[id] = { value: id, name, url, previewUrl }; + }).reduce((acc, { value, url, previewUrl }) => { + if (value) { + acc[value] = { value, url, previewUrl }; } return acc; - }, {}); -}; - + }, {}) +); const getOrganisationUnitSubvalue = async (keys: Object, querySingleResource: QuerySingleResource) => { const ids = Object.values(keys) diff --git a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getEventListData/convertToClientEvents.js b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getEventListData/convertToClientEvents.js index 250970edf1..4e0e339c90 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getEventListData/convertToClientEvents.js +++ b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getEventListData/convertToClientEvents.js @@ -53,8 +53,8 @@ const buildTEIRecord = ({ imageUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}`, previewUrl: `/tracker/trackedEntities/${trackedEntity}/attributes/${id}/image?program=${programId}&dimension=small`, } : { - imageUrl: `/trackedEntityInstances/${trackedEntity}/${id}/image`, - previewUrl: `/trackedEntityInstances/${trackedEntity}/${id}/image`, + imageUrl: `/trackedEntityInstances/${trackedEntity}/${id}/image?program=${programId}`, + previewUrl: `/trackedEntityInstances/${trackedEntity}/${id}/image?program=${programId}&dimension=SMALL`, } ))() : {}; @@ -94,7 +94,7 @@ const buildEventRecord = ({ previewUrl: `/tracker/events/${apiEvent.event}/dataValues/${id}/image?dimension=small`, } : { imageUrl: `/events/files?dataElementUid=${id}&eventUid=${apiEvent.event}`, - previewUrl: `/events/files?dataElementUid=${id}&eventUid=${apiEvent.event}`, + previewUrl: `/events/files?dataElementUid=${id}&eventUid=${apiEvent.event}&dimension=SMALL`, } ))() : {}; diff --git a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getListDataCommon/getSubvalues.js b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getListDataCommon/getSubvalues.js index 3087bfd68a..06fee42794 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getListDataCommon/getSubvalues.js +++ b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getListDataCommon/getSubvalues.js @@ -14,7 +14,7 @@ import type { TeiColumnMetaForDataFetching } from '../../../../types'; import type { QuerySingleResource } from '../../../../../../../utils/api'; const getSubvaluesPlain = (querySingleResource: QuerySingleResource, absoluteApiPath: string) => { - const getImageOrFileResourceSubvalue = async (keys: Array) => { + const getFileResourceSubvalue = async (keys: Array) => { const promises = keys .map(async (key) => { const { id, displayName: name } = await querySingleResource({ resource: 'fileResources', id: key }); @@ -31,6 +31,12 @@ const getSubvaluesPlain = (querySingleResource: QuerySingleResource, absoluteApi }, {}); }; + const getImageResourceSubvalue = (keys: Array) => + keys.reduce((acc, key) => { + acc[key] = key; + return acc; + }, {}); + const getOrganisationUnitSubvalue = async (keys: Array) => getOrgUnitNames(keys, querySingleResource); @@ -38,8 +44,8 @@ const getSubvaluesPlain = (querySingleResource: QuerySingleResource, absoluteApi [string]: any, |} = { [dataElementTypes.ORGANISATION_UNIT]: getOrganisationUnitSubvalue, - [dataElementTypes.IMAGE]: getImageOrFileResourceSubvalue, - [dataElementTypes.FILE_RESOURCE]: getImageOrFileResourceSubvalue, + [dataElementTypes.IMAGE]: getImageResourceSubvalue, + [dataElementTypes.FILE_RESOURCE]: getFileResourceSubvalue, }; const subvaluePostProcessorByType: {| @@ -47,11 +53,9 @@ const getSubvaluesPlain = (querySingleResource: QuerySingleResource, absoluteApi |} = { [dataElementTypes.IMAGE]: ({ subvalueKey: value, - subvalue: name, imageUrl, previewUrl, }) => ({ - name, value, url: `${absoluteApiPath}${imageUrl}`, previewUrl: `${absoluteApiPath}${previewUrl}`, diff --git a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getTeiListData/convertToClientTeis.js b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getTeiListData/convertToClientTeis.js index 101a796fd1..f726604bba 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getTeiListData/convertToClientTeis.js +++ b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/epics/teiViewEpics/helpers/getTeiListData/convertToClientTeis.js @@ -34,8 +34,8 @@ export const convertToClientTeis = ( imageUrl: `/tracker/trackedEntities/${tei.trackedEntity}/attributes/${id}/image?program=${programId}`, previewUrl: `/tracker/trackedEntities/${tei.trackedEntity}/attributes/${id}/image?program=${programId}&dimension=small`, } : { - imageUrl: `/trackedEntityInstances/${tei.trackedEntity}/${id}/image`, - previewUrl: `/trackedEntityInstances/${tei.trackedEntity}/${id}/image`, + imageUrl: `/trackedEntityInstances/${tei.trackedEntity}/${id}/image?program=${programId}`, + previewUrl: `/trackedEntityInstances/${tei.trackedEntity}/${id}/image?program=${programId}&dimension=SMALL`, } ))() : {}; diff --git a/src/core_modules/capture-core/converters/clientToList.js b/src/core_modules/capture-core/converters/clientToList.js index ea052fda36..a5f39b4105 100644 --- a/src/core_modules/capture-core/converters/clientToList.js +++ b/src/core_modules/capture-core/converters/clientToList.js @@ -4,7 +4,6 @@ import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { Tag } from '@dhis2/ui'; import { PreviewImage } from 'capture-ui'; -import { featureAvailable, FEATURES } from 'capture-core-utils'; import { dataElementTypes, type DataElement } from '../metaData'; import { convertMomentToDateFormatString } from '../utils/converters/date'; import { stringifyNumber } from './common/stringifyNumber'; @@ -60,12 +59,7 @@ function convertImageForDisplay(clientValue: ImageClientValue) { if (typeof clientValue === 'string' || clientValue instanceof String) { return clientValue; } - return featureAvailable(FEATURES.trackerImageEndpoint) ? ( - - ) : convertFileForDisplay(clientValue); + return ; } function convertRangeForDisplay(parser: any, clientValue: any) { diff --git a/src/core_modules/capture-core/converters/clientToView.js b/src/core_modules/capture-core/converters/clientToView.js index 51b91f07e5..a1301e6836 100644 --- a/src/core_modules/capture-core/converters/clientToView.js +++ b/src/core_modules/capture-core/converters/clientToView.js @@ -3,7 +3,6 @@ import React from 'react'; import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { PreviewImage } from 'capture-ui'; -import { featureAvailable, FEATURES } from 'capture-core-utils'; import { dataElementTypes, type DataElement } from '../metaData'; import { convertMomentToDateFormatString } from '../utils/converters/date'; import { stringifyNumber } from './common/stringifyNumber'; @@ -52,16 +51,9 @@ function convertFileForDisplay(clientValue: FileClientValue) { } function convertImageForDisplay(clientValue: ImageClientValue) { - return featureAvailable(FEATURES.trackerImageEndpoint) ? ( - - ) : convertFileForDisplay(clientValue); + return ; } - const valueConvertersForType = { [dataElementTypes.NUMBER]: stringifyNumber, [dataElementTypes.INTEGER]: stringifyNumber, diff --git a/src/core_modules/capture-core/events/getSubValues.js b/src/core_modules/capture-core/events/getSubValues.js index 2f35c15cd8..005dbc4d9d 100644 --- a/src/core_modules/capture-core/events/getSubValues.js +++ b/src/core_modules/capture-core/events/getSubValues.js @@ -35,37 +35,23 @@ const subValueGetterByElementType = { return null; }), [dataElementTypes.IMAGE]: ({ - value, eventId, metaElementId, absoluteApiPath, - querySingleResource, }: { - value: any, eventId: string, metaElementId: string, absoluteApiPath: string, - querySingleResource: QuerySingleResource, }) => - querySingleResource({ resource: `fileResources/${value}` }) - .then(res => - ({ - name: res.name, - value: res.id, - ...(featureAvailable(FEATURES.trackerImageEndpoint) ? - { - url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${metaElementId}/image`, - previewUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${metaElementId}/image?dimension=small`, - } : { - url: `${absoluteApiPath}/events/files?dataElementUid=${metaElementId}&eventUid=${eventId}`, - previewUrl: `${absoluteApiPath}/events/files?dataElementUid=${metaElementId}&eventUid=${eventId}`, - } - ), - })) - .catch((error) => { - log.warn(errorCreator(GET_SUBVALUE_ERROR)({ value, eventId, metaElementId, error })); - return null; - }), + (featureAvailable(FEATURES.trackerImageEndpoint) ? + { + url: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${metaElementId}/image`, + previewUrl: `${absoluteApiPath}/tracker/events/${eventId}/dataValues/${metaElementId}/image?dimension=small`, + } : { + url: `${absoluteApiPath}/events/files?dataElementUid=${metaElementId}&eventUid=${eventId}`, + previewUrl: `${absoluteApiPath}/events/files?dataElementUid=${metaElementId}&eventUid=${eventId}&dimension=SMALL`, + } + ), [dataElementTypes.ORGANISATION_UNIT]: ({ value, eventId, diff --git a/src/core_modules/capture-core/trackedEntityInstances/getSubValues.js b/src/core_modules/capture-core/trackedEntityInstances/getSubValues.js index 3b6f08c12c..abd6eda46a 100644 --- a/src/core_modules/capture-core/trackedEntityInstances/getSubValues.js +++ b/src/core_modules/capture-core/trackedEntityInstances/getSubValues.js @@ -16,16 +16,10 @@ const subValueGetterByElementType = { absoluteApiPath: string, programId: ?string, }) => { - const buildUrl = () => { - if (featureAvailable(FEATURES.trackerImageEndpoint)) { - if (programId) { - return `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?program=${programId}&dimension=small`; - } - return `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?dimension=small`; - } - return `${absoluteApiPath}/trackedEntityInstances/${teiId}/${attributeId}/image`; - }; - const previewUrl = buildUrl(); + const url = featureAvailable(FEATURES.trackerImageEndpoint) + ? `${absoluteApiPath}/tracker/trackedEntities/${teiId}/attributes/${attributeId}/image?dimension=small` + : `${absoluteApiPath}/trackedEntityInstances/${teiId}/${attributeId}/image?dimension=SMALL`; + const previewUrl = programId ? `${url}&program=${programId}` : url; return { previewUrl, From 55ff94ed781012e0e27580058bb7856313fb7856 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Thu, 8 Aug 2024 12:05:29 +0000 Subject: [PATCH 11/33] chore(release): cut 100.76.0 [skip release] # [100.76.0](https://github.com/dhis2/capture-app/compare/v100.75.1...v100.76.0) (2024-08-08) ### Features * [DHIS2-17171] preview images in versions prior to 41 ([#3694](https://github.com/dhis2/capture-app/issues/3694)) ([2f51805](https://github.com/dhis2/capture-app/commit/2f51805b469d74b57bff7c927a9aa76420913ea1)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a19cc0d31a..b0486b8282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [100.76.0](https://github.com/dhis2/capture-app/compare/v100.75.1...v100.76.0) (2024-08-08) + + +### Features + +* [DHIS2-17171] preview images in versions prior to 41 ([#3694](https://github.com/dhis2/capture-app/issues/3694)) ([2f51805](https://github.com/dhis2/capture-app/commit/2f51805b469d74b57bff7c927a9aa76420913ea1)) + ## [100.75.1](https://github.com/dhis2/capture-app/compare/v100.75.0...v100.75.1) (2024-08-08) diff --git a/package.json b/package.json index 1561abac2b..6f0e8140c6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.75.1", + "version": "100.76.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.75.1", + "@dhis2/rules-engine-javascript": "100.76.0", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 4781aa9628..5d5236d2af 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.75.1", + "version": "100.76.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 36a3921c2f0aeaba16e5d44bf87604f3eb5a706f Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Thu, 8 Aug 2024 14:33:46 +0200 Subject: [PATCH 12/33] refactor: [DHIS2-17839] replace material ui Paper for Card (#3747) --- .../RecentlyAddedEventsList.component.js | 6 +++--- .../NewEventNewRelationshipWrapper.component.js | 6 +++--- .../Filters/FilterRestMenu/FilterRestMenu.component.js | 7 +++---- .../components/ListView/Menu/ListViewMenu.component.js | 8 ++++---- .../ListView/withEndColumnMenu/RowMenu.component.js | 7 +++---- .../ViewEventNewRelationshipWrapper.component.js | 6 +++--- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.js index 907d7c6548..c0a8f2a6a2 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import { withStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; +import { Card } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { OfflineEventsList } from '../../../../EventsList/OfflineEventsList/OfflineEventsList.component'; import { listId } from './RecentlyAddedEventsList.const'; @@ -30,7 +30,7 @@ const NewEventsListPlain = (props: Props) => { return null; } return ( - +
@@ -43,7 +43,7 @@ const NewEventsListPlain = (props: Props) => { emptyListText={i18n.t('No events added')} {...passOnProps} /> - + ); }; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.js index d5d7ad6b4a..88cf67efe7 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; -import Paper from '@material-ui/core/Paper'; +import { Card } from '@dhis2/ui'; import withStyles from '@material-ui/core/styles/withStyles'; import { NewRelationship } from '../../../Pages/NewRelationship/NewRelationship.container'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; @@ -110,7 +110,7 @@ class NewEventNewRelationshipWrapper extends React.Component { {i18n.t('Go back to event without saving relationship')}
- + {/* $FlowFixMe[cannot-spread-inexact] automated comment */} { onCancel={onCancel} {...passOnProps} /> - +
{ style={{ transformOrigin: '0 0 0' }} timeout={{ exit: 0, enter: 200 }} > - + {this.renderMenuItems()} - + diff --git a/src/core_modules/capture-core/components/ListView/Menu/ListViewMenu.component.js b/src/core_modules/capture-core/components/ListView/Menu/ListViewMenu.component.js index dd8d628a4d..07611adc28 100644 --- a/src/core_modules/capture-core/components/ListView/Menu/ListViewMenu.component.js +++ b/src/core_modules/capture-core/components/ListView/Menu/ListViewMenu.component.js @@ -2,8 +2,8 @@ import React, { useCallback, memo, type ComponentType } from 'react'; import { IconButton } from 'capture-ui'; import { withStyles } from '@material-ui/core/styles'; -import { Divider, IconMore24 } from '@dhis2/ui'; -import { Paper, MenuList, MenuItem } from '@material-ui/core'; +import { Divider, IconMore24, Card } from '@dhis2/ui'; +import { MenuList, MenuItem } from '@material-ui/core'; import { MenuPopper } from '../../Popper/Popper.component'; import type { Props } from './listViewMenu.types'; @@ -86,11 +86,11 @@ const ListViewMenuPlain = ({ customMenuContents = [], classes }: Props) => { .flat(1), [customMenuContents, classes]); const renderPopperContent = useCallback((togglePopper: Function) => ( - + {renderMenuItems(togglePopper)} - + ), [renderMenuItems]); if (!customMenuContents.length) { diff --git a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js index 49362c29af..9e51e50a36 100644 --- a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js +++ b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js @@ -2,9 +2,8 @@ import * as React from 'react'; import { Manager, Popper, Reference } from 'react-popper'; import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import { spacers, IconMore24, colors } from '@dhis2/ui'; +import { Card, spacers, IconMore24, colors } from '@dhis2/ui'; import Grow from '@material-ui/core/Grow'; -import Paper from '@material-ui/core/Paper'; import MenuList from '@material-ui/core/MenuList'; import MenuItem from '@material-ui/core/MenuItem'; import withStyles from '@material-ui/core/styles/withStyles'; @@ -144,9 +143,9 @@ class Index extends React.Component { style={{ transformOrigin: '0 0 0' }} timeout={{ exit: 0, enter: 200 }} > - + {this.renderMenuItems()} - + diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.js index d3cf907c06..a667eac5e1 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; -import Paper from '@material-ui/core/Paper'; +import { Card } from '@dhis2/ui'; import withStyles from '@material-ui/core/styles/withStyles'; import { NewRelationship } from '../../NewRelationship/NewRelationship.container'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; @@ -98,14 +98,14 @@ class ViewEventNewRelationshipWrapperPlain extends React.Component {i18n.t('Go back to event without saving relationship')} - + {/* $FlowFixMe[cannot-spread-inexact] automated comment */} - + Date: Thu, 8 Aug 2024 15:34:52 +0200 Subject: [PATCH 13/33] refactor: [DHIS2-17825] replace ClickAwayListener with Layer onBackdropClick (#3745) --- .../EventWorkingListsUser.js | 6 +-- .../TeiWorkingListsUser.js | 29 ++++++++------ .../FilterRestMenu.component.js | 36 ++++++++--------- .../withEndColumnMenu/RowMenu.component.js | 40 +++++++++---------- .../components/Popper/Popper.component.js | 36 ++++++++--------- 5 files changed, 76 insertions(+), 71 deletions(-) diff --git a/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js index f931718570..51cb81153b 100644 --- a/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js +++ b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js @@ -388,11 +388,11 @@ When('you set the date of admission filter', () => { .within(() => { cy.contains('More filters') .click(); - - cy.contains('Date of admission') - .click(); }); + cy.get('[data-test="more-filters-menu"]') + .within(() => cy.contains('Date of admission').click()); + cy.get('[data-test="list-view-filter-contents"]') .within(() => { cy.get('input[type="text"]') diff --git a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js index ccbda13971..5361178c6d 100644 --- a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js +++ b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js @@ -42,10 +42,11 @@ Given('you open the main page with Ngelehun, WHO RMNCH Tracker and First antenat .within(() => { cy.contains('More filters') .click(); - cy.contains('Program stage') - .click(); }); + cy.get('[data-test="more-filters-menu"]') + .within(() => cy.contains('Program stage').click()); + cy.get('[data-test="list-view-filter-contents"]') .contains('First antenatal care visit') .click(); @@ -66,10 +67,11 @@ Given('you open the main page with Ngelehun and Malaria case diagnosis and House .within(() => { cy.contains('More filters') .click(); - cy.contains('Program stage') - .click(); }); + cy.get('[data-test="more-filters-menu"]') + .within(() => cy.contains('Program stage').click()); + cy.get('[data-test="list-view-filter-contents"]') .contains('Household investigation') .click(); @@ -186,10 +188,11 @@ When('you set the WHOMCH Smoking filter to No', () => { .within(() => { cy.get('[data-test="more-filters"]').eq(1) .click(); - cy.contains('WHOMCH Smoking') - .click(); }); + cy.get('[data-test="more-filters-menu"]') + .within(() => cy.contains('WHOMCH Smoking').click()); + cy.get('[data-test="list-view-filter-contents"]') .contains('No') .click(); @@ -580,9 +583,9 @@ When('you open the program stage filters from the more filters dropdown menu', ( .within(() => { cy.contains('More filters') .click(); - cy.contains('Program stage') - .click(); }); + cy.get('[data-test="more-filters-menu"]') + .within(() => cy.contains('Program stage').click()); }); Then('you see the program stages and the default events filters', () => { @@ -741,10 +744,11 @@ Given('you open the main page with Ngelehun and WHO RMNCH Tracker context and co .within(() => { cy.contains('More filters') .click(); - cy.contains('Program stage') - .click(); }); + cy.get('[data-test="more-filters-menu"]') + .within(() => cy.contains('Program stage').click()); + cy.get('[data-test="list-view-filter-contents"]') .contains('Postpartum care visit') .click(); @@ -760,10 +764,11 @@ Given('you open the main page with all accesible records in the WHO RMNCH Tracke .within(() => { cy.contains('More filters') .click(); - cy.contains('Program stage') - .click(); }); + cy.get('[data-test="more-filters-menu"]') + .within(() => cy.contains('Program stage').click()); + cy.get('[data-test="list-view-filter-contents"]') .contains('Postpartum care visit') .click(); diff --git a/src/core_modules/capture-core/components/ListView/Filters/FilterRestMenu/FilterRestMenu.component.js b/src/core_modules/capture-core/components/ListView/Filters/FilterRestMenu/FilterRestMenu.component.js index 3cd1e5535a..97ca1b48e8 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/FilterRestMenu/FilterRestMenu.component.js +++ b/src/core_modules/capture-core/components/ListView/Filters/FilterRestMenu/FilterRestMenu.component.js @@ -1,10 +1,9 @@ // @flow import React from 'react'; import { withStyles } from '@material-ui/core/styles'; -import { Card, IconChevronDown16, IconChevronUp16, Button } from '@dhis2/ui'; +import { Card, IconChevronDown16, IconChevronUp16, Button, Layer } from '@dhis2/ui'; import { Manager, Popper, Reference } from 'react-popper'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; import Grow from '@material-ui/core/Grow'; import MenuList from '@material-ui/core/MenuList'; import MenuItem from '@material-ui/core/MenuItem'; @@ -167,17 +166,17 @@ class FilterRestMenuPlain extends React.Component { } {this.state.filterSelectorOpen && - - { - ({ ref, style, placement }) => ( -
- + + + { + ({ ref, style, placement }) => ( +
{ - -
- ) - } -
} +
+ ) + } +
+ + } ); } diff --git a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js index 9e51e50a36..1ba6d043c5 100644 --- a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js +++ b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js @@ -1,8 +1,7 @@ // @flow import * as React from 'react'; import { Manager, Popper, Reference } from 'react-popper'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import { Card, spacers, IconMore24, colors } from '@dhis2/ui'; +import { Card, spacers, IconMore24, colors, Layer } from '@dhis2/ui'; import Grow from '@material-ui/core/Grow'; import MenuList from '@material-ui/core/MenuList'; import MenuItem from '@material-ui/core/MenuItem'; @@ -124,19 +123,19 @@ class Index extends React.Component { } {this.state.menuOpen && - - { - ({ ref, style, placement }) => ( -
- + + + { + ({ ref, style, placement }) => ( +
{ {this.renderMenuItems()} - -
- ) - } -
} +
+ ) + } +
+ + } ); } diff --git a/src/core_modules/capture-core/components/Popper/Popper.component.js b/src/core_modules/capture-core/components/Popper/Popper.component.js index 36a8f9226a..832de14135 100644 --- a/src/core_modules/capture-core/components/Popper/Popper.component.js +++ b/src/core_modules/capture-core/components/Popper/Popper.component.js @@ -2,7 +2,7 @@ import * as React from 'react'; import { Manager, Popper, Reference } from 'react-popper'; import type { Placement } from '@popperjs/core/lib'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import { Layer } from '@dhis2/ui'; import Grow from '@material-ui/core/Grow'; type Props = { @@ -74,18 +74,18 @@ export class MenuPopper extends React.Component { } {this.state.popperOpen && - - { - ({ ref, style, placement }) => ( -
- + + + { + ({ ref, style, placement }) => ( +
{ {getPopperContent(this.toggleMenu)} - -
- ) - } -
} +
+ ) + } +
+ } ); } From 793da87ca651d1761fb7705fda5b906d5274b807 Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Thu, 8 Aug 2024 15:49:25 +0200 Subject: [PATCH 14/33] refactor: [DHIS2-17750] replace material ui Card for Widget (#3718) --- i18n/en.pot | 32 +++---- .../DataEntry/DataEntry.component.js | 7 +- .../dataEntryOutput/withDataEntryOutput.js | 2 +- .../dataEntryOutput/withErrorOutput.js | 73 +-------------- .../dataEntryOutput/withFeedbackOutput.js | 88 +++---------------- .../dataEntryOutput/withIndicatorOutput.js | 87 +++--------------- .../dataEntryOutput/withWarningOutput.js | 71 +-------------- .../WidgetFeedback.component.js | 4 +- .../WidgetFeedback/WidgetFeedback.types.js | 5 -- .../components/WidgetFeedback/index.js | 3 + 10 files changed, 62 insertions(+), 310 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index b28f5b5e2b..3917b85952 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -366,17 +366,11 @@ msgstr "Registered person" msgid "validation failed" msgstr "validation failed" -msgid "Errors" -msgstr "Errors" - -msgid "Feedback" -msgstr "Feedback" - -msgid "Indicators" -msgstr "Indicators" +msgid "No feedback for this event yet" +msgstr "No feedback for this event yet" -msgid "Warnings" -msgstr "Warnings" +msgid "No indicator output for this event yet" +msgstr "No indicator output for this event yet" msgid "Generate new event" msgstr "Generate new event" @@ -789,12 +783,6 @@ msgstr "There was an error loading the page" msgid "Choose an organisation unit to start reporting" msgstr "Choose an organisation unit to start reporting" -msgid "No feedback for this event yet" -msgstr "No feedback for this event yet" - -msgid "No indicator output for this event yet" -msgstr "No indicator output for this event yet" - msgid "Program stage is invalid" msgstr "Program stage is invalid" @@ -941,6 +929,18 @@ msgstr "" "Leaving this page will discard any selections you made for a new " "relationship" +msgid "Errors" +msgstr "Errors" + +msgid "Feedback" +msgstr "Feedback" + +msgid "Indicators" +msgstr "Indicators" + +msgid "Warnings" +msgstr "Warnings" + msgid "Show all events" msgstr "Show all events" diff --git a/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js b/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js index 9c21fbd1bc..655020b09f 100644 --- a/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js @@ -62,10 +62,15 @@ const styles = theme => ({ position: 'relative', flexGrow: 1, width: theme.typography.pxToRem(300), - margin: theme.typography.pxToRem(10), + '& > div > div > *:not(:first-child)': { + marginTop: '10px', + }, marginRight: 0, }, verticalOutputsContainer: { + '& > *': { + marginTop: '10px', + }, marginBottom: theme.typography.pxToRem(10), }, dataEntrySectionContainer: { diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withDataEntryOutput.js b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withDataEntryOutput.js index 81631e3831..2d13c64fb0 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withDataEntryOutput.js +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withDataEntryOutput.js @@ -21,7 +21,7 @@ const getDataEntryOutput = (InnerComponent: React.ComponentType, Output: Re return dataEntryOutputs ? [...dataEntryOutputs, output] : [output]; }; getOutput = (key: any) => ( -
+
{/* $FlowFixMe[cannot-spread-inexact] automated comment */} , errorOnCompleteItems: ?Array, saveAttempted: boolean, - classes: { - list: string, - listItem: string, - card: string, - header: string, - headerText: string, - }, }; -const styles = (theme: Theme) => ({ - card: { - padding: theme.typography.pxToRem(10), - backgroundColor: theme.palette.error.red200, - borderRadius: theme.typography.pxToRem(5), - }, - list: { - margin: 0, - }, - listItem: { - paddingLeft: theme.typography.pxToRem(10), - marginTop: theme.typography.pxToRem(8), - }, - header: { - color: '#902c02', - display: 'flex', - alignItems: 'center', - }, - headerText: { - marginLeft: theme.typography.pxToRem(10), - }, -}); - const getErrorOutput = () => class ErrorOutputBuilder extends React.Component { - static renderErrorItems = (errorItems: any, classes: any) => - (
- {errorItems - .map(item => ( -
  • -

    {item.message}

    -
  • - )) - } -
    ) - name: string; constructor(props) { super(props); @@ -82,27 +35,8 @@ const getErrorOutput = () => } render = () => { - const { classes } = this.props; const visibleItems = this.getVisibleErrorItems(); - return ( -
    - {visibleItems && visibleItems.length > 0 && - -
    - -
    - {i18n.t('Errors')} -
    -
    -
      - {ErrorOutputBuilder.renderErrorItems(visibleItems, classes)} -
    - -
    - } -
    - - ); + return ; } }; @@ -124,5 +58,4 @@ export const withErrorOutput = () => (InnerComponent: React.ComponentType) => withDataEntryOutput()( InnerComponent, - withStyles(styles)( - connect(mapStateToProps, mapDispatchToProps)(getErrorOutput()))); + connect(mapStateToProps, mapDispatchToProps)(getErrorOutput())); diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.js b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.js index b93b032d5d..83fa479707 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.js +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.js @@ -1,95 +1,35 @@ // @flow import * as React from 'react'; import { connect } from 'react-redux'; -import Card from '@material-ui/core/Card'; -import { Menu, MenuItem } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core/styles'; import i18n from '@dhis2/d2-i18n'; import { getDataEntryKey } from '../common/getDataEntryKey'; import { withDataEntryOutput } from './withDataEntryOutput'; - +import { WidgetFeedback } from '../../WidgetFeedback'; +import type { FilteredText, FilteredKeyValue } from '../../WidgetFeedback'; type Props = { feedbackItems: { - displayTexts: [{ key: string, value: string}], - displayKeyValuePairs: [{ key: string, value: string}], - }, - classes: { - listItem: string, - card: string, - keyValuePairKey: string, + displayTexts: Array, + displayKeyValuePairs: Array, }, }; -const styles = (theme: Theme) => ({ - listItem: { - display: 'flex', - backgroundColor: '#f5f5f5 !important', - paddingLeft: theme.typography.pxToRem(10), - marginTop: theme.typography.pxToRem(8), - }, - keyValuePairKey: { - flexGrow: 1, - margin: 0, - }, - keyValue: { - margin: 0, - fontSize: '0.875rem', - }, - card: { - padding: theme.typography.pxToRem(10), - borderRadius: theme.typography.pxToRem(5), - }, - labelContainer: { - display: 'flex', - }, -}); - const getFeedbackOutput = () => class FeedbackOutputBuilder extends React.Component { - renderFeedbackItems = (feedbackItems: any, classes: any) => - (
    - {feedbackItems.displayTexts && - feedbackItems.displayTexts.map(item => ( - {item.message}

    } - /> - ), - )} - {feedbackItems.displayKeyValuePairs && - feedbackItems.displayKeyValuePairs.map(item => ( - -

    {item.key}

    -

    {item.value}

    -
    - } - /> - ), - )} -
    ) + getItems = () => { + const { feedbackItems } = this.props; + const displayTexts = feedbackItems?.displayTexts || []; + const displayKeyValuePairs = feedbackItems?.displayKeyValuePairs || []; + return [...displayTexts, ...displayKeyValuePairs]; + } render = () => { - const { feedbackItems, classes } = this.props; - const hasItems = feedbackItems && (feedbackItems.displayTexts || feedbackItems.displayKeyValuePairs); + const feedback = this.getItems(); + const hasItems = feedback.length > 0; return (
    {hasItems && - - {i18n.t('Feedback')} - - {feedbackItems && this.renderFeedbackItems(feedbackItems, classes)} - - + }
    ); @@ -113,4 +53,4 @@ export const withFeedbackOutput = () => withDataEntryOutput()( InnerComponent, - withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(getFeedbackOutput()))); + connect(mapStateToProps, mapDispatchToProps)(getFeedbackOutput())); diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.js b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.js index 2c35ae9bd9..dd423a0281 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.js +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.js @@ -1,94 +1,35 @@ // @flow import * as React from 'react'; import { connect } from 'react-redux'; -import Card from '@material-ui/core/Card'; -import { Menu, MenuItem } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core/styles'; import i18n from '@dhis2/d2-i18n'; import { getDataEntryKey } from '../common/getDataEntryKey'; import { withDataEntryOutput } from './withDataEntryOutput'; - +import { WidgetIndicator } from '../../WidgetIndicator'; +import type { FilteredText, FilteredKeyValue } from '../../WidgetFeedback'; type Props = { indicatorItems: { - displayTexts: [{ key: string, value: string}], - displayKeyValuePairs: [{ key: string, value: string}], - }, - classes: { - listItem: string, - card: string, + displayTexts: Array, + displayKeyValuePairs: Array, }, }; -const styles = (theme: Theme) => ({ - listItem: { - display: 'flex', - backgroundColor: '#f5f5f5 !important', - paddingLeft: theme.typography.pxToRem(10), - marginTop: theme.typography.pxToRem(8), - }, - keyValuePairKey: { - flexGrow: 1, - margin: 0, - }, - keyValue: { - margin: 0, - fontSize: '0.875rem', - }, - card: { - padding: theme.typography.pxToRem(10), - borderRadius: theme.typography.pxToRem(5), - }, - labelContainer: { - display: 'flex', - }, -}); - const getIndicatorOutput = () => class IndicatorkOutputBuilder extends React.Component { - renderIndicatorItems = (indicatorItems: any, classes: any) => - (
    - {indicatorItems.displayTexts && - indicatorItems.displayTexts.map(item => ( - {item.message}

    } - /> - ), - )} - {indicatorItems.displayKeyValuePairs && - indicatorItems.displayKeyValuePairs.map(item => ( - -

    {item.key}

    -

    {item.value}

    -
    - } - /> - ), - )} -
    ) + getItems = () => { + const { indicatorItems } = this.props; + const displayTexts = indicatorItems?.displayTexts || []; + const displayKeyValuePairs = indicatorItems?.displayKeyValuePairs || []; + return [...displayTexts, ...displayKeyValuePairs]; + } render = () => { - const { indicatorItems, classes } = this.props; - const hasItems = indicatorItems && (indicatorItems.displayTexts || indicatorItems.displayKeyValuePairs); + const indicators = this.getItems(); + const hasItems = indicators.length > 0; return (
    {hasItems && - - {i18n.t('Indicators')} - - {indicatorItems && this.renderIndicatorItems(indicatorItems, classes)} - - + }
    @@ -113,4 +54,4 @@ export const withIndicatorOutput = () => withDataEntryOutput()( InnerComponent, - withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(getIndicatorOutput()))); + connect(mapStateToProps, mapDispatchToProps)(getIndicatorOutput())); diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withWarningOutput.js b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withWarningOutput.js index 9e03e69a36..5736836b1d 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withWarningOutput.js +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withWarningOutput.js @@ -1,66 +1,19 @@ // @flow import * as React from 'react'; import { connect } from 'react-redux'; -import Card from '@material-ui/core/Card'; -import { IconWarningFilled16 } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core/styles'; -import i18n from '@dhis2/d2-i18n'; import { getDataEntryKey } from '../common/getDataEntryKey'; import { withDataEntryOutput } from './withDataEntryOutput'; +import { WidgetWarning } from '../../WidgetErrorAndWarning/WidgetWarning'; type Props = { warningItems: ?Array, warningOnCompleteItems: ?Array, saveAttempted: boolean, - classes: { - list: string, - listItem: string, - card: string, - header: string, - headerText: string, - }, }; -const styles = theme => ({ - list: { - margin: 0, - }, - listItem: { - paddingLeft: theme.typography.pxToRem(10), - marginTop: theme.typography.pxToRem(8), - }, - header: { - display: 'flex', - alignItems: 'center', - }, - headerText: { - marginLeft: theme.typography.pxToRem(10), - }, - card: { - borderRadius: theme.typography.pxToRem(5), - padding: theme.typography.pxToRem(10), - backgroundColor: theme.palette.warning.lighter, - }, - -}); - const getWarningOutput = () => class WarningOutputBuilder extends React.Component { - static renderWarningItems = (warningItems: any, classes: any) => - (
    - {warningItems && - warningItems.map(item => ( -
  • -

    {item.message}

    -
  • - ), - )} -
    ) - getVisibleWarningItems() { const { warningItems, warningOnCompleteItems, saveAttempted } = this.props; if (saveAttempted) { @@ -76,26 +29,8 @@ const getWarningOutput = () => } render = () => { - const { classes } = this.props; const visibleItems = this.getVisibleWarningItems(); - return ( -
    - {visibleItems && visibleItems.length > 0 && - -
    - -
    - {i18n.t('Warnings')} -
    -
    -
      - {WarningOutputBuilder.renderWarningItems(visibleItems, classes)} -
    -
    - } -
    - - ); + return ; } }; @@ -117,4 +52,4 @@ export const withWarningOutput = () => (InnerComponent: React.ComponentType) => withDataEntryOutput()( InnerComponent, - withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(getWarningOutput()))); + connect(mapStateToProps, mapDispatchToProps)(getWarningOutput())); diff --git a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.js b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.js index 9ed41d6db6..4e0790a5cb 100644 --- a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.js +++ b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.component.js @@ -2,10 +2,10 @@ import React, { useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { Widget } from '../Widget'; -import type { PlainProps } from './WidgetFeedback.types'; +import type { Props } from './WidgetFeedback.types'; import { WidgetFeedbackContent } from './WidgetFeedbackContent/WidgetFeedbackContent'; -export const WidgetFeedback = ({ feedback, emptyText }: PlainProps) => { +export const WidgetFeedback = ({ feedback, emptyText }: Props) => { const [openStatus, setOpenStatus] = useState(true); return ( diff --git a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.js b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.js index 2a868bb36b..c56c9e33e5 100644 --- a/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.js +++ b/src/core_modules/capture-core/components/WidgetFeedback/WidgetFeedback.types.js @@ -31,11 +31,6 @@ export type Props = {| emptyText: string, |} -export type PlainProps = {| - ...PlainProps, - ...CssClasses -|} - export type IndicatorProps = {| indicators?: ?Array, emptyText: string, diff --git a/src/core_modules/capture-core/components/WidgetFeedback/index.js b/src/core_modules/capture-core/components/WidgetFeedback/index.js index b5d3bf95d8..8f608c3a41 100644 --- a/src/core_modules/capture-core/components/WidgetFeedback/index.js +++ b/src/core_modules/capture-core/components/WidgetFeedback/index.js @@ -1 +1,4 @@ +// @flow + export { WidgetFeedback } from './WidgetFeedback.component'; +export type { FilteredText, FilteredKeyValue } from './WidgetFeedback.types'; From bfffe069e3c16387dc0da6c08ed25b91f3f55bf1 Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Fri, 9 Aug 2024 15:02:39 +0200 Subject: [PATCH 15/33] fix: [DHIS2-17632][DHIS2-17633] restrict invalid category combo for orgUnit (#3738) --- i18n/en.pot | 11 +++- .../DataEntry/DataEntry.component.js | 1 + ...lidCategoryCombinationForOrgUnitMessage.js | 25 +++++++++ .../Pages/MainPage/MainPage.component.js | 6 +++ .../Pages/MainPage/MainPage.constants.js | 1 + .../Pages/MainPage/MainPage.container.js | 25 +++++++-- .../components/Pages/New/NewPage.actions.js | 4 ++ .../components/Pages/New/NewPage.component.js | 26 ++++++++-- .../components/Pages/New/NewPage.constants.js | 1 + .../components/Pages/New/NewPage.container.js | 12 ++++- .../components/Pages/New/NewPage.types.js | 2 + .../Validated/Validated.container.js | 1 + .../useCategoryComboIsValidForOrgUnit.js | 51 +++++++++++++++++++ .../newPage.reducerDescription.js | 4 ++ 14 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/MainPage/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.js create mode 100644 src/core_modules/capture-core/hooks/useCategoryComboIsValidForOrgUnit.js diff --git a/i18n/en.pot b/i18n/en.pot index 3917b85952..53ce989a6f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2024-06-18T22:47:46.585Z\n" -"PO-Revision-Date: 2024-06-18T22:47:46.585Z\n" +"POT-Creation-Date: 2024-08-08T11:49:13.423Z\n" +"PO-Revision-Date: 2024-08-08T11:49:13.423Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -813,6 +813,13 @@ msgstr "Stage" msgid "Registered events" msgstr "Registered events" +msgid "" +"The category option is not valid for the selected organisation unit. Please " +"select a valid combination." +msgstr "" +"The category option is not valid for the selected organisation unit. Please " +"select a valid combination." + msgid "Please select {{category}}." msgstr "Please select {{category}}." diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js index a9dd0b8f54..3743fa5dc4 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.js @@ -631,6 +631,7 @@ class NewEventDataEntry extends Component { onUpdateDataEntryField={onUpdateDataEntryField(orgUnit)} onUpdateFormField={onUpdateField(orgUnit)} onUpdateFormFieldAsync={onStartAsyncUpdateField(orgUnit)} + selectedOrgUnitId={orgUnit.id} onSave={this.handleSave} fieldOptions={this.fieldOptions} dataEntrySections={this.dataEntrySections} diff --git a/src/core_modules/capture-core/components/Pages/MainPage/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.js b/src/core_modules/capture-core/components/Pages/MainPage/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.js new file mode 100644 index 0000000000..1c20f8f51d --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/MainPage/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.js @@ -0,0 +1,25 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core/styles'; +import { IncompleteSelectionsMessage } from '../../../IncompleteSelectionsMessage'; + +const styles = { + incompleteMessageContainer: { + marginTop: '10px', + }, +}; + +export const InvalidCategoryCombinationForOrgUnitMessagePlain = ({ classes }: {| ...CssClasses |}) => ( +
    + + {i18n.t( + 'The category option is not valid for the selected organisation unit. Please select a valid combination.', + )} + +
    +); + +export const InvalidCategoryCombinationForOrgUnitMessage = withStyles(styles)( + InvalidCategoryCombinationForOrgUnitMessagePlain, +); 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 a8a231281e..2d29de6e9b 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,6 +12,9 @@ import { withErrorMessageHandler, withLoadingIndicator } from '../../../HOC'; import { TopBar } from './TopBar.container'; import { SearchBox } from '../../SearchBox'; import { TemplateSelector } from '../../TemplateSelector'; +import { + InvalidCategoryCombinationForOrgUnitMessage, +} from './InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage'; const getStyles = () => ({ listContainer: { @@ -57,6 +60,9 @@ const MainPageBody = compose( {MainPageStatus === MainPageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( )} + {MainPageStatus === MainPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT && ( + + )} {MainPageStatus === MainPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED && ( )} diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.constants.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.constants.js index bf38aeadab..3000f74fc0 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.constants.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.constants.js @@ -2,5 +2,6 @@ export const MainPageStatuses = Object.freeze({ DEFAULT: 'DEFAULT', WITHOUT_ORG_UNIT_SELECTED: 'WITHOUT_ORG_UNIT_SELECTED', WITHOUT_PROGRAM_CATEGORY_SELECTED: 'WITHOUT_PROGRAM_CATEGORY_SELECTED', + CATEGORY_OPTION_INVALID_FOR_ORG_UNIT: 'CATEGORY_OPTION_INVALID_FOR_ORG_UNIT', SHOW_WORKING_LIST: 'SHOW_WORKING_LIST', }); 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 976ce35e57..52a10ab4e4 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 @@ -10,6 +10,7 @@ import { updateShowAccessibleStatus } from '../actions/crossPage.actions'; import { buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { MainPageStatuses } from './MainPage.constants'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; +import { useCategoryOptionIsValidForOrgUnit } from '../../../hooks/useCategoryComboIsValidForOrgUnit'; const mapStateToProps = (state: ReduxState) => ({ error: state.activePage.selectionsError && state.activePage.selectionsError.error, // TODO: Should probably remove this @@ -29,7 +30,14 @@ const handleChangeTemplateUrl = ({ programId, orgUnitId, selectedTemplateId, sho } }; -const useMainPageStatus = ({ programId, selectedProgram, categories, orgUnitId, showAllAccessible }) => { +const useMainPageStatus = ({ + programId, + selectedProgram, + categories, + orgUnitId, + showAllAccessible, + categoryOptionIsInvalidForOrgUnit, +}) => { const withoutOrgUnit = useMemo(() => !orgUnitId && !showAllAccessible, [orgUnitId, showAllAccessible]); return useMemo(() => { @@ -44,6 +52,9 @@ const useMainPageStatus = ({ programId, selectedProgram, categories, orgUnitId, if (withoutOrgUnit) { return MainPageStatuses.WITHOUT_ORG_UNIT_SELECTED; } + if (programCategories && categoryOptionIsInvalidForOrgUnit) { + return MainPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT; + } return MainPageStatuses.SHOW_WORKING_LIST; } @@ -52,7 +63,7 @@ const useMainPageStatus = ({ programId, selectedProgram, categories, orgUnitId, } return MainPageStatuses.SHOW_WORKING_LIST; - }, [categories, programId, withoutOrgUnit, selectedProgram]); + }, [programId, selectedProgram, withoutOrgUnit, categories, categoryOptionIsInvalidForOrgUnit]); }; const useSelectorMainPage = () => @@ -95,12 +106,20 @@ const MainPageContainer = () => { error, ready, } = useSelectorMainPage(); + const { categoryOptionIsInvalidForOrgUnit } = useCategoryOptionIsValidForOrgUnit({ selectedOrgUnitId: orgUnitId }); const selectedProgram = programCollection.get(programId); // $FlowFixMe[prop-missing] const trackedEntityTypeId = selectedProgram?.trackedEntityType?.id; const displayFrontPageList = trackedEntityTypeId && selectedProgram?.displayFrontPageList; - const MainPageStatus = useMainPageStatus({ programId, selectedProgram, categories, orgUnitId, showAllAccessible }); + const MainPageStatus = useMainPageStatus({ + programId, + selectedProgram, + categories, + orgUnitId, + showAllAccessible, + categoryOptionIsInvalidForOrgUnit, + }); const { onChangeTemplate, diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.actions.js b/src/core_modules/capture-core/components/Pages/New/NewPage.actions.js index ec6592cc72..69654ab15e 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.actions.js +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.actions.js @@ -5,6 +5,7 @@ export const newPageActionTypes = { NEW_PAGE_OPEN: 'NewPage.NewPageOpen', NEW_PAGE_WITHOUT_ORG_UNIT_SELECTED_VIEW: 'NewPage.WithoutOrgUnitSelectedView', NEW_PAGE_WITHOUT_PROGRAM_CATEGORY_SELECTED_VIEW: 'NewPage.WithoutProgramComboSelectedView', + NEW_PAGE_CATEGORY_OPTION_INVALID_FOR_ORG_UNIT_VIEW: 'NewPage.InvalidCategoryOptionSelectedView', NEW_PAGE_DEFAULT_VIEW: 'NewPage.DefaultView', CLEAN_UP_DATA_ENTRY: 'NewPage.DataEntryCleanUp', CATEGORY_OPTION_SET: 'NewPage.CategoryOptionSet', @@ -19,6 +20,9 @@ export const showMessageToSelectOrgUnitOnNewPage = () => export const showMessageToSelectProgramCategoryOnNewPage = () => actionCreator(newPageActionTypes.NEW_PAGE_WITHOUT_PROGRAM_CATEGORY_SELECTED_VIEW)(); +export const showMessageThatCategoryOptionIsInvalidForOrgUnit = () => + actionCreator(newPageActionTypes.NEW_PAGE_CATEGORY_OPTION_INVALID_FOR_ORG_UNIT_VIEW)(); + export const showDefaultViewOnNewPage = () => actionCreator(newPageActionTypes.NEW_PAGE_DEFAULT_VIEW)(); export const cleanUpDataEntry = (dataEntryId: string) => diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.component.js b/src/core_modules/capture-core/components/Pages/New/NewPage.component.js index c4209e5a6a..a7b070193b 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.component.js +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.component.js @@ -25,6 +25,7 @@ const getStyles = () => ({ const NewPagePlain = ({ showMessageToSelectOrgUnitOnNewPage, showMessageToSelectProgramCategoryOnNewPage, + showMessageThatCategoryOptionIsInvalidForOrgUnit, showDefaultViewOnNewPage, handleMainPageNavigation, classes, @@ -32,6 +33,7 @@ const NewPagePlain = ({ newPageStatus, writeAccess, programCategorySelectionIncomplete, + categoryOptionIsInvalidForOrgUnit, missingCategoriesInProgramSelection, orgUnitSelectionIncomplete, isUserInteractionInProgress, @@ -53,6 +55,8 @@ const NewPagePlain = ({ showMessageToSelectOrgUnitOnNewPage(); } else if (programCategorySelectionIncomplete) { showMessageToSelectProgramCategoryOnNewPage(); + } else if (categoryOptionIsInvalidForOrgUnit) { + showMessageThatCategoryOptionIsInvalidForOrgUnit(); } else { showDefaultViewOnNewPage(); } @@ -63,6 +67,8 @@ const NewPagePlain = ({ showMessageToSelectOrgUnitOnNewPage, showMessageToSelectProgramCategoryOnNewPage, showDefaultViewOnNewPage, + categoryOptionIsInvalidForOrgUnit, + showMessageThatCategoryOptionIsInvalidForOrgUnit, ]); const orgUnitId = useSelector(({ currentSelections }) => currentSelections.orgUnitId); @@ -135,6 +141,16 @@ const NewPagePlain = ({ })() } + { + newPageStatus === newPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT && ( + + {i18n.t( + 'The category option is not valid for the selected organisation unit. Please select a valid combination.', + )} + + ) + } + } @@ -142,8 +158,8 @@ const NewPagePlain = ({ }; export const NewPageComponent: ComponentType = - compose( - withLoadingIndicator(), - withErrorMessageHandler(), - withStyles(getStyles), - )(NewPagePlain); + compose( + withLoadingIndicator(), + withErrorMessageHandler(), + withStyles(getStyles), + )(NewPagePlain); diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.constants.js b/src/core_modules/capture-core/components/Pages/New/NewPage.constants.js index 3d02cfe561..33287d6995 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.constants.js +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.constants.js @@ -5,6 +5,7 @@ export const newPageStatuses = { ERROR: 'ERROR', WITHOUT_ORG_UNIT_SELECTED: 'WITHOUT_ORG_UNIT_SELECTED', WITHOUT_PROGRAM_CATEGORY_SELECTED: 'WITHOUT_PROGRAM_CATEGORY_SELECTED', + CATEGORY_OPTION_INVALID_FOR_ORG_UNIT: 'CATEGORY_OPTION_INVALID_FOR_ORG_UNIT', }; export const NEW_TEI_DATA_ENTRY_ID = 'newPageDataEntryId'; diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.container.js b/src/core_modules/capture-core/components/Pages/New/NewPage.container.js index f40396f498..8ea87c19e2 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.container.js +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.container.js @@ -7,7 +7,7 @@ import { NewPageComponent } from './NewPage.component'; import { showMessageToSelectOrgUnitOnNewPage, showDefaultViewOnNewPage, - showMessageToSelectProgramCategoryOnNewPage, + showMessageToSelectProgramCategoryOnNewPage, showMessageThatCategoryOptionIsInvalidForOrgUnit, } from './NewPage.actions'; import { typeof newPageStatuses } from './NewPage.constants'; import { buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; @@ -17,6 +17,7 @@ import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges' import { useTrackedEntityInstances } from './hooks'; import { deriveTeiName } from '../common/EnrollmentOverviewDomain/useTeiDisplayName'; import { programCollection } from '../../../metaDataMemoryStores/programCollection/programCollection'; +import { useCategoryOptionIsValidForOrgUnit } from '../../../hooks/useCategoryComboIsValidForOrgUnit'; const useUserWriteAccess = (scopeId) => { const scope = getScopeFromScopeId(scopeId); @@ -45,6 +46,9 @@ export const NewPage: ComponentType<{||}> = () => { const history = useHistory(); const { orgUnitId, programId, teiId } = useLocationQuery(); const program = programId && programCollection.get(programId); + const { categoryOptionIsInvalidForOrgUnit } = useCategoryOptionIsValidForOrgUnit({ + selectedOrgUnitId: orgUnitId, + }); const { trackedEntityInstanceAttributes } = useTrackedEntityInstances(teiId, programId); // $FlowFixMe const trackedEntityType = program?.trackedEntityType; @@ -56,6 +60,10 @@ export const NewPage: ComponentType<{||}> = () => { () => { dispatch(showMessageToSelectOrgUnitOnNewPage()); }, [dispatch]); + const dispatchShowMessageThatCategoryOptionIsInvalidForOrgUnit = useCallback( + () => { dispatch(showMessageThatCategoryOptionIsInvalidForOrgUnit()); }, + [dispatch]); + const dispatchShowMessageToSelectProgramCategoryOnNewPage = useCallback( () => { dispatch(showMessageToSelectProgramCategoryOnNewPage()); }, [dispatch]); @@ -104,11 +112,13 @@ export const NewPage: ComponentType<{||}> = () => { showMessageToSelectOrgUnitOnNewPage={dispatchShowMessageToSelectOrgUnitOnNewPage} showMessageToSelectProgramCategoryOnNewPage={dispatchShowMessageToSelectProgramCategoryOnNewPage} showDefaultViewOnNewPage={dispatchShowDefaultViewOnNewPage} + showMessageThatCategoryOptionIsInvalidForOrgUnit={dispatchShowMessageThatCategoryOptionIsInvalidForOrgUnit} handleMainPageNavigation={handleMainPageNavigation} currentScopeId={currentScopeId} orgUnitSelectionIncomplete={orgUnitSelectionIncomplete} programCategorySelectionIncomplete={programSelectionIsIncomplete} missingCategoriesInProgramSelection={missingCategories} + categoryOptionIsInvalidForOrgUnit={categoryOptionIsInvalidForOrgUnit} writeAccess={writeAccess} newPageStatus={newPageStatus} error={error} diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.types.js b/src/core_modules/capture-core/components/Pages/New/NewPage.types.js index f9357422d7..438b61a88f 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.types.js +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.types.js @@ -17,6 +17,8 @@ type InputAttribute = { export type ContainerProps = $ReadOnly<{| showMessageToSelectOrgUnitOnNewPage: ()=>void, showMessageToSelectProgramCategoryOnNewPage: ()=>void, + showMessageThatCategoryOptionIsInvalidForOrgUnit: ()=>void, + categoryOptionIsInvalidForOrgUnit: boolean, showDefaultViewOnNewPage: ()=>void, handleMainPageNavigation: ()=>void, currentScopeId: string, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js index a04a234fb2..61c961a257 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js @@ -177,6 +177,7 @@ export const Validated = ({ stage={stage} allowGenerateNextVisit={stage.allowGenerateNextVisit} askCompleteEnrollmentOnEventComplete={stage.askCompleteEnrollmentOnEventComplete} + selectedOrgUnitId={orgUnit.id} availableProgramStages={availableProgramStages} eventSaveInProgress={eventSaveInProgress} ready={ready} diff --git a/src/core_modules/capture-core/hooks/useCategoryComboIsValidForOrgUnit.js b/src/core_modules/capture-core/hooks/useCategoryComboIsValidForOrgUnit.js new file mode 100644 index 0000000000..694b536a2a --- /dev/null +++ b/src/core_modules/capture-core/hooks/useCategoryComboIsValidForOrgUnit.js @@ -0,0 +1,51 @@ +// @flow +import { useMemo } from 'react'; +// $FlowFixMe +import { shallowEqual, useSelector } from 'react-redux'; +import { useIndexedDBQuery } from '../utils/reactQueryHelpers'; +import { getUserStorageController, userStores } from '../storageControllers'; + +type Props = { + selectedOrgUnitId: string, +} + +const getSelectedCategoryOption = (selectedCategories: Array) => { + const storageController = getUserStorageController(); + return storageController.getAll(userStores.CATEGORY_OPTIONS, { + predicate: ({ id, organisationUnits }) => selectedCategories.includes(id) && organisationUnits, + }); +}; + +export const useCategoryOptionIsValidForOrgUnit = ({ + selectedOrgUnitId, +}: Props) => { + const { categories, complete } = useSelector(({ currentSelections }) => ({ + categories: currentSelections.categories, + complete: currentSelections.complete, + }), shallowEqual); + + const categoryOptionIds = categories && Object.values(categories); + + const { data, isLoading, isError } = useIndexedDBQuery( + ['categoryOptions', categoryOptionIds], + () => getSelectedCategoryOption(categoryOptionIds), + { + enabled: complete && selectedOrgUnitId && !!categoryOptionIds && categoryOptionIds.length > 0, + }, + ); + + const categoryOptionIsInvalidForOrgUnit = useMemo(() => { + if (!data || !data.length) { + return false; + } + + return data.every(({ organisationUnits }) => !organisationUnits[selectedOrgUnitId]); + }, [data, selectedOrgUnitId]); + + return { + categoryOptionIsInvalidForOrgUnit, + isLoading, + isError, + }; +}; + diff --git a/src/core_modules/capture-core/reducers/descriptions/newPage.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/newPage.reducerDescription.js index a2888ab230..23fef54a22 100644 --- a/src/core_modules/capture-core/reducers/descriptions/newPage.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/newPage.reducerDescription.js @@ -25,6 +25,10 @@ export const newPageDesc = createReducerDescription( ...state, newPageStatus: newPageStatuses.WITHOUT_PROGRAM_CATEGORY_SELECTED, }), + [newPageActionTypes.NEW_PAGE_CATEGORY_OPTION_INVALID_FOR_ORG_UNIT_VIEW]: state => ({ + ...state, + newPageStatus: newPageStatuses.CATEGORY_OPTION_INVALID_FOR_ORG_UNIT, + }), [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START]: (state, action) => { const { uid } = action.payload; From 00b9aa4bd54168aed4b3bb65c76f28e780a49d8c Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Fri, 9 Aug 2024 13:12:17 +0000 Subject: [PATCH 16/33] chore(release): cut 100.76.1 [skip release] ## [100.76.1](https://github.com/dhis2/capture-app/compare/v100.76.0...v100.76.1) (2024-08-09) ### Bug Fixes * [DHIS2-17632][DHIS2-17633] restrict invalid category combo for orgUnit ([#3738](https://github.com/dhis2/capture-app/issues/3738)) ([bfffe06](https://github.com/dhis2/capture-app/commit/bfffe069e3c16387dc0da6c08ed25b91f3f55bf1)) --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0486b8282..8c44b04a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [100.76.1](https://github.com/dhis2/capture-app/compare/v100.76.0...v100.76.1) (2024-08-09) + + +### Bug Fixes + +* [DHIS2-17632][DHIS2-17633] restrict invalid category combo for orgUnit ([#3738](https://github.com/dhis2/capture-app/issues/3738)) ([bfffe06](https://github.com/dhis2/capture-app/commit/bfffe069e3c16387dc0da6c08ed25b91f3f55bf1)) + # [100.76.0](https://github.com/dhis2/capture-app/compare/v100.75.1...v100.76.0) (2024-08-08) diff --git a/package.json b/package.json index 6f0e8140c6..c3b80adee7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.76.0", + "version": "100.76.1", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.76.0", + "@dhis2/rules-engine-javascript": "100.76.1", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 5d5236d2af..8e9828f187 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.76.0", + "version": "100.76.1", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From e6f2a152569d52c9d97849010b3d9f36806b5c0b Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:56:39 +0200 Subject: [PATCH 17/33] refactor: [DHIS2-17652] Replace Material-UI Avatar (#3719) --- .../CardList/CardListItem.component.js | 10 ++-- .../CardImage/CardImage.component.js | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 src/core_modules/capture-ui/CardImage/CardImage.component.js diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.js b/src/core_modules/capture-core/components/CardList/CardListItem.component.js index 617e7699d3..a1fe4bc5d1 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.js +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.js @@ -3,9 +3,10 @@ import i18n from '@dhis2/d2-i18n'; import React from 'react'; import moment from 'moment'; import type { ComponentType } from 'react'; -import { Avatar, Grid, withStyles } from '@material-ui/core'; +import { Grid, withStyles } from '@material-ui/core'; import { colors, Tag, IconCheckmark16, Tooltip } from '@dhis2/ui'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; +import { CardImage } from '../../../capture-ui/CardImage/CardImage.component'; import type { CardDataElementsInformation, CardProfileImageElementInformation, @@ -63,8 +64,8 @@ const getStyles = (theme: Theme) => ({ flexGrow: 1, }, image: { - width: theme.typography.pxToRem(44), - height: theme.typography.pxToRem(44), + width: theme.typography.pxToRem(54), + height: theme.typography.pxToRem(54), marginRight: theme.typography.pxToRem(8), }, buttonMargin: { @@ -151,7 +152,8 @@ const CardListItemIndex = ({ const imageValue = item.values[imageElement.id]; return (
    - {imageValue && } + {imageValue && } +
    ); }; diff --git a/src/core_modules/capture-ui/CardImage/CardImage.component.js b/src/core_modules/capture-ui/CardImage/CardImage.component.js new file mode 100644 index 0000000000..c4873cbb42 --- /dev/null +++ b/src/core_modules/capture-ui/CardImage/CardImage.component.js @@ -0,0 +1,56 @@ +// @flow +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; + +const sizes = { + extrasmall: { + height: 24, + width: 24, + }, + small: { + height: 36, + width: 36, + }, + medium: { + height: 48, + width: 48, + }, + large: { + height: 72, + width: 72, + }, + extralarge: { + height: 144, + width: 144, + }, +}; + +const styles = { + img: { + borderRadius: '50%', + objectFit: 'cover', + }, + ...sizes, +}; + +type Props = { + imageUrl: string, + dataTest: string, + classes: Object, + className: Object, + size: 'extrasmall' | 'small' | 'medium' | 'large' | 'extralarge', +}; + +const CardImagePlain = ({ imageUrl, dataTest, classes, className, size }: Props) => ( +
    + user avatar +
    +); + + +export const CardImage = withStyles(styles)(CardImagePlain); From bae6e7a847841b13b417870af01d2967c9f02dae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:48:02 +0200 Subject: [PATCH 18/33] chore(deps): bump ejs from 3.1.9 to 3.1.10 (#3749) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 75ea607d80..5c8b6775b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7468,9 +7468,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== ejs@^3.1.5, ejs@^3.1.6: - version "3.1.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" @@ -16067,7 +16067,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16093,6 +16093,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -16187,7 +16196,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17920,7 +17936,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17947,6 +17963,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 09905d73ac622db57c43f8f85c163bcddea62c5f Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:42:36 +0200 Subject: [PATCH 19/33] refactor: [DHIS2-17650] Replace Material-UI Table, TableBody, TableCell, TableHead and TableRow (#3721) * feat: change to dhis ui components * fix: define drag source and drop target * fix: ts error * fix: ts error * fix: ts * fix: restore comments * fix: breaking cypress test * fix: cypress * fix: cypress test * fix: rolleback cypress change in fil * Revert "fix: rolleback cypress change in fil" This reverts commit c609f1a06a8d8a2db83a49a74c852c0a2406a004. * fix: cypress * fix: review change for opacity * fix: review change for hover --- .../AllAccessibleRecordsPage.js | 2 + .../EventWorkingListsUser.js | 3 +- .../TeiWorkingListsUser.js | 9 +- i18n/en.pot | 3 + .../DragDropList/DragDropList.component.js | 54 +++--- .../DragDropListItem.component.js | 167 +++++++++--------- 6 files changed, 125 insertions(+), 113 deletions(-) diff --git a/cypress/e2e/AllAccessibleRecordsPage/AllAccessibleRecordsPage.js b/cypress/e2e/AllAccessibleRecordsPage/AllAccessibleRecordsPage.js index 9ea6ac3f02..dbf29a6a8c 100644 --- a/cypress/e2e/AllAccessibleRecordsPage/AllAccessibleRecordsPage.js +++ b/cypress/e2e/AllAccessibleRecordsPage/AllAccessibleRecordsPage.js @@ -61,6 +61,8 @@ Then('the working list should be updated', () => { .click(); cy.contains('WHOMCH Hemoglobin value') + .parents('tr') + .find('input[type="checkbox"]') .click(); cy.contains('Save') diff --git a/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js index 51cb81153b..c5cb9fc63b 100644 --- a/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js +++ b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/EventWorkingListsUser.js @@ -149,7 +149,8 @@ When('you open the column selector', () => { When('you select Household location and save from the column selector', () => { cy.get('aside[role="dialog"]') .contains('Household location') - .find('input') + .parents('tr') + .find('input[type="checkbox"]') .click(); cy.get('aside[role="dialog"]') diff --git a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js index 5361178c6d..9026a15f3d 100644 --- a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js +++ b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.js @@ -254,7 +254,8 @@ When('you open the column selector', () => { When('you select the organisation unit and save from the column selector', () => { cy.get('aside[role="dialog"]') .contains('Organisation unit') - .find('input') + .parents('tr') + .find('input[type="checkbox"]') .click(); cy.get('aside[role="dialog"]') @@ -616,7 +617,8 @@ When('you select the Foci response program stage', () => { When('you select a data element columns and save from the column selector', () => { cy.get('aside[role="dialog"]') .contains('People included') - .find('input') + .parents('tr') + .find('input[type="checkbox"]') .click(); cy.get('aside[role="dialog"]') @@ -677,7 +679,8 @@ Then('you see scheduledAt filter', () => { When('you select a scheduledAt column and save from the column selector', () => { cy.get('aside[role="dialog"]') .contains('Appointment date') - .find('input') + .parents('tr') + .find('input[type="checkbox"]') .click(); cy.get('aside[role="dialog"]') diff --git a/i18n/en.pot b/i18n/en.pot index 53ce989a6f..cc88503ce6 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -562,6 +562,9 @@ msgstr "Columns to show in table" msgid "Column" msgstr "Column" +msgid "Visible" +msgstr "Visible" + msgid "Update" msgstr "Update" diff --git a/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropList.component.js b/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropList.component.js index a9f242fa89..e8cf16e2ed 100644 --- a/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropList.component.js +++ b/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropList.component.js @@ -1,34 +1,28 @@ // @flow import React, { Component } from 'react'; - import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import update from 'react-addons-update'; - -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; - +import { DataTable, TableHead, TableBody, DataTableRow, DataTableColumnHeader } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; - import { DragDropListItem } from './DragDropListItem.component'; type Props = { listItems: Array, handleUpdateListOrder: (sortedList: Array) => void, - handleToggle: (id: string) => () => void, + handleToggle: (id: string) => () => any, }; -export class DragDropList extends Component { - moveListItem: (dragIndex: any, hoverIndex: any) => void; - constructor(props: Props) { - super(props); - this.moveListItem = this.moveListItem.bind(this); - } +type State = { + isDraggingAny: boolean, +}; + +export class DragDropList extends Component { + state = { + isDraggingAny: false, + }; - moveListItem(dragIndex: any, hoverIndex: any) { + moveListItem = (dragIndex: number, hoverIndex: number) => { const { listItems } = this.props; const dragListItem = listItems[dragIndex]; let sortedList = []; @@ -40,18 +34,29 @@ export class DragDropList extends Component { }); this.props.handleUpdateListOrder(sortedList); - } + }; + + handleDragStart = () => { + this.setState({ isDraggingAny: true }); + }; + + handleDragEnd = () => { + this.setState({ isDraggingAny: false }); + }; render() { const { listItems } = this.props; + const { isDraggingAny } = this.state; return ( - + - - {i18n.t('Column')} - + + + {i18n.t('Column')} + {i18n.t('Visible')} + {listItems.map((item, i) => ( @@ -63,10 +68,13 @@ export class DragDropList extends Component { moveListItem={this.moveListItem} handleToggle={this.props.handleToggle} visible={item.visible} + isDraggingAny={isDraggingAny} + onDragStart={this.handleDragStart} + onDragEnd={this.handleDragEnd} /> ))} -
    +
    ); } diff --git a/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropListItem.component.js b/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropListItem.component.js index d7f63bde76..2f9087aa2f 100644 --- a/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropListItem.component.js +++ b/src/core_modules/capture-core/components/ListView/ColumnSelector/DragDropList/DragDropListItem.component.js @@ -1,107 +1,102 @@ // @flow -import React, { Component } from 'react'; -import { DragSource, DropTarget } from 'react-dnd'; -import { Checkbox, IconReorder24, spacersNum } from '@dhis2/ui'; -import TableCell from '@material-ui/core/TableCell'; +import React, { useRef } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { DataTableRow, DataTableCell, Checkbox } from '@dhis2/ui'; import { withStyles } from '@material-ui/core/styles'; -const styles = () => ({ - checkbox: { - marginTop: spacersNum.dp12, - marginBottom: spacersNum.dp12, +const ItemTypes = { + LISTITEM: 'listItem', +}; + +const styles = { + rowWithoutHover: { + '&:hover > td': { + backgroundColor: 'transparent !important', + }, }, -}); +}; type Props = { id: string, visible: boolean, text: string, - handleToggle: (id: string) => void, - isDragging: () => void, - connectDragSource: (any) => void, - connectDropTarget: (any) => void, - classes: { - checkbox: string, - } + index: number, + handleToggle: (id: string) => any, + moveListItem: (dragIndex: number, hoverIndex: number) => void, + isDraggingAny: boolean, + onDragStart: () => void, + onDragEnd: () => void, + classes: any, }; -const style = { - cursor: 'move', - outline: 'none', -}; +const DragDropListItemPlain = ({ + id, + index, + text, + visible, + handleToggle, + moveListItem, + isDraggingAny, + onDragStart, + onDragEnd, + classes, +}: Props) => { + const ref = useRef(null); -const ItemTypes = { - LISTITEM: 'listItem', -}; + const [, drop] = useDrop({ + accept: ItemTypes.LISTITEM, + hover(item: { id: string, index: number }) { + const dragIndex = item.index; + const hoverIndex = index; -const cardSource = { - beginDrag(props) { - return { - id: props.id, - index: props.index, - }; - }, -}; + // Don't replace items with themselves. + if (dragIndex === hoverIndex) { + return; + } -const cardTarget = { - hover(props, monitor) { - const dragIndex = monitor.getItem().index; - const hoverIndex = props.index; + // Time to actually perform the action + moveListItem(dragIndex, hoverIndex); - // Don't replace items with themselves. - if (dragIndex === hoverIndex) { - return; - } + // Note: we're mutating the item here! + // Generally it's better to avoid mutations, + // but it's good here for the sake of performance + // to avoid expensive index searches. + item.index = hoverIndex; + }, + }); - // Time to actually perform the action. - props.moveListItem(dragIndex, hoverIndex); + const [{ isDragging }, drag] = useDrag({ + type: ItemTypes.LISTITEM, + item: { id, index }, + collect: monitor => ({ + isDragging: monitor.isDragging(), + }), + }); - // Note: we're mutating the monitor item here! - // Generally it's better to avoid mutations, - // but it's good here for the sake of performance - // to avoid expensive index searches. - monitor.getItem().index = hoverIndex; - }, -}; - -class Index extends Component { - render() { - const { text, isDragging, connectDragSource, connectDropTarget } = this.props; - const opacity = isDragging ? 0 : 1; + drag(drop(ref)); - // $FlowFixMe[incompatible-extend] automated comment - return connectDropTarget(connectDragSource( - - - - - - - - - - , - )); - } -} + const opacity = isDragging ? 0 : 1; -export const DragDropListItemPlain = - DragSource( - ItemTypes.LISTITEM, - cardSource, - (connect, monitor) => ({ connectDragSource: connect.dragSource(), isDragging: monitor.isDragging() }), - )(DropTarget( - ItemTypes.LISTITEM, - cardTarget, - connect => ({ connectDropTarget: connect.dropTarget() }), - )(Index)); + return ( + + {text} + + + + + ); +}; export const DragDropListItem = withStyles(styles)(DragDropListItemPlain); From 1b4be20b474f2f5b6c4eb66c37b0c509291b5bf6 Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Tue, 13 Aug 2024 16:59:13 +0200 Subject: [PATCH 20/33] feat: [DHIS2-12288] add enrollment section description (#3750) --- .../DataEntry/FormFoundation/RenderFoundation.js | 5 +++++ .../components/WidgetProfile/hooks/useApiProgram.js | 2 +- .../programs/factory/enrollment/EnrollmentFactory.js | 4 ++++ .../programs/quickStoreOperations/storePrograms.js | 2 +- .../programs/quickStoreOperations/types/apiPrograms.types.js | 3 ++- .../capture-core/storageControllers/cache.types.js | 3 ++- 6 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js index 4e2d0b26e3..6cbedfb764 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/FormFoundation/RenderFoundation.js @@ -177,6 +177,7 @@ const buildSection = async ({ optionSets, sectionCustomLabel, sectionCustomId, + sectionDisplayDescription, querySingleResource, minorServerVersion, }: { @@ -185,6 +186,7 @@ const buildSection = async ({ optionSets: Array, sectionCustomLabel: string, sectionCustomId: string, + sectionDisplayDescription: string, querySingleResource: QuerySingleResource, minorServerVersion: number, }) => { @@ -195,6 +197,7 @@ const buildSection = async ({ const section = new Section((o) => { o.id = sectionCustomId; o.name = sectionCustomLabel; + o.displayDescription = sectionDisplayDescription; }); await buildElementsForSection({ @@ -281,6 +284,7 @@ export const buildFormFoundation = async (program: any, querySingleResource: Que programTrackedEntityAttributes: attributes, sectionCustomLabel: formConfigSection.name ?? sectionMetadata?.displayFormName ?? i18n.t('Profile'), sectionCustomId: formConfigSection.id, + sectionDisplayDescription: sectionMetadata?.displayDescription ?? '', minorServerVersion, trackedEntityAttributes, optionSets, @@ -299,6 +303,7 @@ export const buildFormFoundation = async (program: any, querySingleResource: Que optionSets, sectionCustomLabel: programSection.displayFormName, sectionCustomId: programSection.id, + sectionDisplayDescription: programSection.displayDescription, querySingleResource, minorServerVersion, }); diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.js b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.js index 073ab42e40..fde2d1575c 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.js +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.js @@ -8,7 +8,7 @@ const fields = 'dataEntryForm[id,htmlCode],' + 'categoryCombo[id,displayName,isDefault,categories[id,displayName]],' + 'programIndicators[id,displayName,code,shortName,style,displayInForm,expression,displayDescription,description,filter,program[id]],' + - 'programSections[id, displayFormName, sortOrder, trackedEntityAttributes],' + + 'programSections[id,displayFormName,displayDescription,sortOrder,trackedEntityAttributes],' + 'programRuleVariables[id,displayName,programRuleVariableSourceType,valueType,program[id],programStage[id],dataElement[id],trackedEntityAttribute[id],useCodeForOptionSet],' + 'programStages[id,access,autoGenerateEvent,openAfterEnrollment,generatedByEnrollmentDate,reportDateToUse,minDaysFromStart,displayName,description,executionDateLabel,formType,featureType,validationStrategy,enableUserAssignment,style,' + 'dataEntryForm[id,htmlCode],' + diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js index d865498f0a..50074f6951 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js @@ -181,6 +181,7 @@ export class EnrollmentFactory { cachedProgramTrackedEntityAttributes?: Array, cachedSectionCustomLabel: string, cachedSectionCustomId: string, + description: string, ) { if (!cachedProgramTrackedEntityAttributes?.length) { return null; @@ -189,6 +190,7 @@ export class EnrollmentFactory { const section = new Section((o) => { o.id = cachedSectionCustomId; o.name = cachedSectionCustomLabel; + o.displayDescription = description; o.group = Section.groups.ENROLLMENT; }); @@ -302,6 +304,7 @@ export class EnrollmentFactory { attributes, formConfigSection.name ?? sectionMetadata?.displayFormName ?? i18n.t('Profile'), formConfigSection.id, + sectionMetadata?.displayDescription ?? '', ); section && enrollmentForm.addSection(section); }); @@ -312,6 +315,7 @@ export class EnrollmentFactory { programSection.trackedEntityAttributes.map(id => trackedEntityAttributeDictionary[id]), programSection.displayFormName, programSection.id, + programSection.displayDescription, ); section && enrollmentForm.addSection(section); }); diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js index d0b9487308..529cb771b1 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js @@ -93,7 +93,7 @@ const fieldsParam = 'id,displayName,displayShortName,description,programType,sty 'programStageSections[id,displayName,displayDescription,sortOrder,dataElements[id]],' + // eslint-disable-next-line max-len 'programStageDataElements[compulsory,displayInReports,renderOptionsAsRadio,allowFutureDate,renderType[*],dataElement[id]]]' + -'programSections[id, displayFormName, sortOrder, trackedEntityAttributes],' + +'programSections[id, displayDescription, displayFormName, sortOrder, trackedEntityAttributes],' + // eslint-disable-next-line max-len 'programTrackedEntityAttributes[trackedEntityAttribute[id],displayInList,searchable,mandatory,renderOptionsAsRadio,allowFutureDate]'; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js index d4c2e3024d..1112031950 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js @@ -63,7 +63,8 @@ type apiProgramSections = { id: string, sortOrder: number, displayFormName: string, - trackedEntityAttributes: Array<{ id: string }> + trackedEntityAttributes: Array<{ id: string }>, + description: ?string } type apiOption = { diff --git a/src/core_modules/capture-core/storageControllers/cache.types.js b/src/core_modules/capture-core/storageControllers/cache.types.js index 01c852ec84..121534f5ab 100644 --- a/src/core_modules/capture-core/storageControllers/cache.types.js +++ b/src/core_modules/capture-core/storageControllers/cache.types.js @@ -171,7 +171,8 @@ export type CachedProgramSection = { id: string, displayFormName: string, sortOrder: number, - trackedEntityAttributes: Array + trackedEntityAttributes: Array, + displayDescription: string } export type CachedTrackedEntityType = { From c3ce444db56cbb089c3023aa4c1866cc7f158c1f Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Tue, 13 Aug 2024 17:00:14 +0200 Subject: [PATCH 21/33] fix: [DHIS2-17352] Changing program in event workspace does nothing (#3754) --- .../QuickSelector/Program/ProgramSelector.component.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramSelector.component.js b/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramSelector.component.js index 3af9b00bd8..d6d0ec6ad6 100644 --- a/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramSelector.component.js +++ b/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramSelector.component.js @@ -50,6 +50,7 @@ const ProgramSelectorPlain = ({ const [programsArray, setProgramsArray] = useState>([]); const selectedProgram = selectedProgramId ? programCollection.get(selectedProgramId) : null; const programOptions = getOptions(selectedOrgUnitId, programsArray); + const isMenuDisabled = !handleClickProgram; useEffect(() => { setProgramsArray(Array.from(programCollection.values())); @@ -83,7 +84,8 @@ const ProgramSelectorPlain = ({ noValueMessage={i18n.t('Choose a program')} value={selectedProgram && } open={open} - setOpen={openSelectorBarItem => setOpen(openSelectorBarItem)} + setOpen={openSelectorBarItem => (isMenuDisabled ? null : setOpen(openSelectorBarItem))} + displayOnly={isMenuDisabled} onClearSelectionClick={() => onResetProgramId(resetProgramIdBase())} dataTest="program-selector-container" > From 229c79e330d794115fb459cad3c65dc41a5e5c29 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Tue, 13 Aug 2024 15:55:00 +0000 Subject: [PATCH 22/33] chore(release): cut 100.77.0 [skip release] # [100.77.0](https://github.com/dhis2/capture-app/compare/v100.76.1...v100.77.0) (2024-08-13) ### Bug Fixes * [DHIS2-17352] Changing program in event workspace does nothing ([#3754](https://github.com/dhis2/capture-app/issues/3754)) ([c3ce444](https://github.com/dhis2/capture-app/commit/c3ce444db56cbb089c3023aa4c1866cc7f158c1f)) ### Features * [DHIS2-12288] add enrollment section description ([#3750](https://github.com/dhis2/capture-app/issues/3750)) ([1b4be20](https://github.com/dhis2/capture-app/commit/1b4be20b474f2f5b6c4eb66c37b0c509291b5bf6)) --- CHANGELOG.md | 12 ++++++++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c44b04a8c..f3d02ffe29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [100.77.0](https://github.com/dhis2/capture-app/compare/v100.76.1...v100.77.0) (2024-08-13) + + +### Bug Fixes + +* [DHIS2-17352] Changing program in event workspace does nothing ([#3754](https://github.com/dhis2/capture-app/issues/3754)) ([c3ce444](https://github.com/dhis2/capture-app/commit/c3ce444db56cbb089c3023aa4c1866cc7f158c1f)) + + +### Features + +* [DHIS2-12288] add enrollment section description ([#3750](https://github.com/dhis2/capture-app/issues/3750)) ([1b4be20](https://github.com/dhis2/capture-app/commit/1b4be20b474f2f5b6c4eb66c37b0c509291b5bf6)) + ## [100.76.1](https://github.com/dhis2/capture-app/compare/v100.76.0...v100.76.1) (2024-08-09) diff --git a/package.json b/package.json index c3b80adee7..7fed3d6c03 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.76.1", + "version": "100.77.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.76.1", + "@dhis2/rules-engine-javascript": "100.77.0", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 8e9828f187..1778c10745 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.76.1", + "version": "100.77.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From 2bbc649b2cb638ac7c311bde16b79e1c39b2a65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Storl=C3=B8kken=20Melseth?= Date: Wed, 14 Aug 2024 10:50:36 +0200 Subject: [PATCH 23/33] chore(release): stop support for 2.38 (#3755) To keep a manageable codebase, we have decided to support only the same version range as the backend. However, in contrast to the backend, we will release new features for all the supported versions as long as we have the backend support. BREAKING CHANGE: Bump version to 101.0.0 to facilitate potential fixes for 2.38 --- .../StagesAndEventsWidget.feature | 3 +- .../TeiWorkingListsUser.feature | 8 - d2.config.js | 2 +- .../featuresSupport/support.js | 2 - .../Setup/hooks/useProgramStageFilters.js | 165 +++++++++--------- 5 files changed, 82 insertions(+), 98 deletions(-) diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature index e21de7e069..bdefbaa55c 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/StagesAndEventsWidget.feature @@ -60,8 +60,7 @@ Feature: User interacts with Stages and Events Widget Given you open the enrollment page by typing #enrollment?programId=IpHINAT79UW&orgUnitId=UgYg0YW7ZIh&teiId=fhFQhO0xILJ&enrollmentId=gPDueU02tn8 Then you should see the disabled button New Birth event - # Waiting for pipline to update DB/DB update for 39 and 41 - @user:trackerAutoTestRestricted @v=38 @v=40 @v=42 + @user:trackerAutoTestRestricted Scenario: Program stage is hidden if no data read access And you open the enrollment page by typing #enrollment?enrollmentId=iNEq9d22Nyp&orgUnitId=DiszpKrYNg8&programId=WSGAb5XwJ3Y&teiId=k4ODejBytgv Then the Care at birth program stage should be hidden diff --git a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.feature b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.feature index af604635ee..a80e857fcd 100644 --- a/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.feature +++ b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/TeiWorkingListsUser.feature @@ -131,7 +131,6 @@ Then the new My custom list is created When you delete the name My custom list Then the My custom list is deleted -@v>=39 Scenario: The user can open and select a program stage filter Given you open the main page with Ngelehun and Malaria focus investigation context When you open the program stage filters from the more filters dropdown menu @@ -141,7 +140,6 @@ And you open the column selector And you select a data element columns and save from the column selector Then you see data elements specific filters and columns -@v>=39 Scenario: While in a program stage working list, the user can filter by both TEA and data elements Given you open the main page with Ngelehun, WHO RMNCH Tracker and First antenatal care visit context When you set the enrollment status filter to active @@ -154,7 +152,6 @@ And you set the WHOMCH Smoking filter to No And you apply the current filter Then the list should display 1 row of data -@v>=39 Scenario: While in a program stage working list, the user can sort by both TEA and data elements Given you open the main page with Ngelehun, WHO RMNCH Tracker and First antenatal care visit context And you set the first name filter to u @@ -166,7 +163,6 @@ When you click the WHOMCH Hemoglobin value column header Then the sort arrow should indicate descending order And the list should display data ordered descending by WHOMCH Hemoglobin -@v>=39 Scenario: The user can remove the program stage filter Given you open the main page with Ngelehun and WHO RMNCH Tracker context When you open the program stage filters from the more filters dropdown menu @@ -176,7 +172,6 @@ Then you see program stage working list events When you remove the program stage filter Then you don't see program stage working list events -@v>=39 Scenario: The user can filter the events by scheduledAt date Given you open the main page with Ngelehun and WHO RMNCH Tracker context When you open the program stage filters from the more filters dropdown menu @@ -189,20 +184,17 @@ And you select the events scheduled today And you apply the current filter Then you see the selected option in the scheduledAt filter -@v>=39 Scenario: The program stage working list configureation is kept when navigating Given you open the main page with Ngelehun and WHO RMNCH Tracker context and configure a program stage working list When you open an enrollment event from the working list And you go back using the browser button Then the program stage working list is loaded -@v>=39 Scenario: The program stage working list without a orgUnit selected redirects to a tracker event Given you open the main page with all accesible records in the WHO RMNCH Tracker context and configure a program stage working list When you open an enrollment event from the working list Then the tracker event URL contains the orgUnitId -@v>=39 Scenario: The user can open a program stage list without events Given you open the main page with Ngelehun and WHO RMNCH Tracker context and configure a program stage working list And you set the event visit date to Today diff --git a/d2.config.js b/d2.config.js index 300d6a336b..e909aff221 100644 --- a/d2.config.js +++ b/d2.config.js @@ -4,7 +4,7 @@ const config = { type: 'app', id: '92b75fd0-34cc-451c-942f-3dd0f283bcbd', - minDHIS2Version: '2.38', + minDHIS2Version: '2.39', coreApp: true, entryPoints: { diff --git a/src/core_modules/capture-core-utils/featuresSupport/support.js b/src/core_modules/capture-core-utils/featuresSupport/support.js index 682ec6dd01..637439c74b 100644 --- a/src/core_modules/capture-core-utils/featuresSupport/support.js +++ b/src/core_modules/capture-core-utils/featuresSupport/support.js @@ -1,6 +1,5 @@ // @flow export const FEATURES = Object.freeze({ - programStageWorkingList: 'programStageWorkingList', storeProgramStageWorkingList: 'storeProgramStageWorkingList', multiText: 'multiText', customIcons: 'customIcons', @@ -14,7 +13,6 @@ export const FEATURES = Object.freeze({ // The first minor version that supports the feature const MINOR_VERSION_SUPPORT = Object.freeze({ - [FEATURES.programStageWorkingList]: 39, [FEATURES.storeProgramStageWorkingList]: 40, [FEATURES.multiText]: 41, [FEATURES.customIcons]: 41, diff --git a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/Setup/hooks/useProgramStageFilters.js b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/Setup/hooks/useProgramStageFilters.js index fe9fbe9622..d17f1be4b1 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/Setup/hooks/useProgramStageFilters.js +++ b/src/core_modules/capture-core/components/WorkingLists/TeiWorkingLists/Setup/hooks/useProgramStageFilters.js @@ -1,7 +1,6 @@ // @flow import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; -import { useFeature, FEATURES } from 'capture-core-utils'; import { statusTypes, translatedStatusTypes } from 'capture-core/events/statusTypes'; import { type TrackerProgram, type ProgramStage } from '../../../../../metaData'; import { ADDITIONAL_FILTERS, ADDITIONAL_FILTERS_LABELS } from '../../helpers'; @@ -34,94 +33,90 @@ const useProgramStageDropdowOptions = stages => ); export const useProgramStageFilters = ({ stages }: TrackerProgram, programStageId?: string) => { - const supportsProgramStageWorkingLists = useFeature(FEATURES.programStageWorkingList); const { hideDueDate, occurredAtLabel, scheduledAtLabel } = useProgramStageData(programStageId, stages); const options: Array<{ text: string, value: string }> = useProgramStageDropdowOptions(stages); return useMemo(() => { - if (supportsProgramStageWorkingLists) { - const translatedStatus = translatedStatusTypes(); - return [ - { - id: ADDITIONAL_FILTERS.programStage, - type: 'TEXT', - header: i18n.t(ADDITIONAL_FILTERS_LABELS.programStage), - options, - mainButton: true, - transformRecordsFilter: () => null, + const translatedStatus = translatedStatusTypes(); + return [ + { + id: ADDITIONAL_FILTERS.programStage, + type: 'TEXT', + header: i18n.t(ADDITIONAL_FILTERS_LABELS.programStage), + options, + mainButton: true, + transformRecordsFilter: () => null, + }, + { + id: ADDITIONAL_FILTERS.occurredAt, + type: 'DATE', + header: occurredAtLabel, + disabled: !programStageId, + tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + label: occurredAtLabel, + interpolation: { escapeValue: false }, + }), + transformRecordsFilter: (filter: string) => { + const queryArgs = {}; + const filterParts = filter.split(':'); + const indexGe = filterParts.indexOf('ge'); + const indexLe = filterParts.indexOf('le'); + if (indexGe !== -1 && filterParts[indexGe + 1]) { + queryArgs.occurredAfter = filterParts[indexGe + 1]; + } + if (indexLe !== -1 && filterParts[indexLe + 1]) { + queryArgs.occurredBefore = filterParts[indexLe + 1]; + } + return queryArgs; }, - { - id: ADDITIONAL_FILTERS.occurredAt, - type: 'DATE', - header: occurredAtLabel, - disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { - label: occurredAtLabel, - interpolation: { escapeValue: false }, - }), - transformRecordsFilter: (filter: string) => { - const queryArgs = {}; - const filterParts = filter.split(':'); - const indexGe = filterParts.indexOf('ge'); - const indexLe = filterParts.indexOf('le'); - if (indexGe !== -1 && filterParts[indexGe + 1]) { - queryArgs.occurredAfter = filterParts[indexGe + 1]; - } - if (indexLe !== -1 && filterParts[indexLe + 1]) { - queryArgs.occurredBefore = filterParts[indexLe + 1]; - } - return queryArgs; - }, - }, - { - id: ADDITIONAL_FILTERS.status, - type: 'TEXT', - header: i18n.t(ADDITIONAL_FILTERS_LABELS.status), - options: [ - { text: translatedStatus.ACTIVE, value: statusTypes.ACTIVE }, - { text: translatedStatus.SCHEDULE, value: statusTypes.SCHEDULE }, - { text: translatedStatus.COMPLETED, value: statusTypes.COMPLETED }, - { text: translatedStatus.OVERDUE, value: statusTypes.OVERDUE }, - { text: translatedStatus.SKIPPED, value: statusTypes.SKIPPED }, - ], - disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { - label: ADDITIONAL_FILTERS_LABELS.status, - interpolation: { escapeValue: false }, - }), - transformRecordsFilter: (rawFilter: string) => ({ - status: rawFilter.split(':')[1], - }), - }, - ...(hideDueDate === false - ? [ - { - id: ADDITIONAL_FILTERS.scheduledAt, - type: 'DATE', - header: scheduledAtLabel, - disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { - label: scheduledAtLabel, - interpolation: { escapeValue: false }, - }), - transformRecordsFilter: (filter: string) => { - const queryArgs = {}; - const filterParts = filter.split(':'); - const indexGe = filterParts.indexOf('ge'); - const indexLe = filterParts.indexOf('le'); - if (indexGe !== -1 && filterParts[indexGe + 1]) { - queryArgs.scheduledAfter = filterParts[indexGe + 1]; - } - if (indexLe !== -1 && filterParts[indexLe + 1]) { - queryArgs.scheduledBefore = filterParts[indexLe + 1]; - } - return queryArgs; - }, + }, + { + id: ADDITIONAL_FILTERS.status, + type: 'TEXT', + header: i18n.t(ADDITIONAL_FILTERS_LABELS.status), + options: [ + { text: translatedStatus.ACTIVE, value: statusTypes.ACTIVE }, + { text: translatedStatus.SCHEDULE, value: statusTypes.SCHEDULE }, + { text: translatedStatus.COMPLETED, value: statusTypes.COMPLETED }, + { text: translatedStatus.OVERDUE, value: statusTypes.OVERDUE }, + { text: translatedStatus.SKIPPED, value: statusTypes.SKIPPED }, + ], + disabled: !programStageId, + tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + label: ADDITIONAL_FILTERS_LABELS.status, + interpolation: { escapeValue: false }, + }), + transformRecordsFilter: (rawFilter: string) => ({ + status: rawFilter.split(':')[1], + }), + }, + ...(hideDueDate === false + ? [ + { + id: ADDITIONAL_FILTERS.scheduledAt, + type: 'DATE', + header: scheduledAtLabel, + disabled: !programStageId, + tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + label: scheduledAtLabel, + interpolation: { escapeValue: false }, + }), + transformRecordsFilter: (filter: string) => { + const queryArgs = {}; + const filterParts = filter.split(':'); + const indexGe = filterParts.indexOf('ge'); + const indexLe = filterParts.indexOf('le'); + if (indexGe !== -1 && filterParts[indexGe + 1]) { + queryArgs.scheduledAfter = filterParts[indexGe + 1]; + } + if (indexLe !== -1 && filterParts[indexLe + 1]) { + queryArgs.scheduledBefore = filterParts[indexLe + 1]; + } + return queryArgs; }, - ] - : []), - ]; - } - return []; - }, [programStageId, supportsProgramStageWorkingLists, occurredAtLabel, scheduledAtLabel, hideDueDate, options]); + }, + ] + : []), + ]; + }, [programStageId, occurredAtLabel, scheduledAtLabel, hideDueDate, options]); }; From c08efd041ac4a859f5de41cc55425b25ff742d07 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Wed, 14 Aug 2024 08:55:31 +0000 Subject: [PATCH 24/33] chore(release): cut 101.0.0 [skip release] # [101.0.0](https://github.com/dhis2/capture-app/compare/v100.77.0...v101.0.0) (2024-08-14) ### chore * **release:** stop support for 2.38 ([#3755](https://github.com/dhis2/capture-app/issues/3755)) ([2bbc649](https://github.com/dhis2/capture-app/commit/2bbc649b2cb638ac7c311bde16b79e1c39b2a65f)) ### BREAKING CHANGES * **release:** Bump version to 101.0.0 to facilitate potential fixes for 2.38 --- CHANGELOG.md | 12 ++++++++++++ package.json | 4 ++-- packages/rules-engine/package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d02ffe29..54951f5e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [101.0.0](https://github.com/dhis2/capture-app/compare/v100.77.0...v101.0.0) (2024-08-14) + + +### chore + +* **release:** stop support for 2.38 ([#3755](https://github.com/dhis2/capture-app/issues/3755)) ([2bbc649](https://github.com/dhis2/capture-app/commit/2bbc649b2cb638ac7c311bde16b79e1c39b2a65f)) + + +### BREAKING CHANGES + +* **release:** Bump version to 101.0.0 to facilitate potential fixes for 2.38 + # [100.77.0](https://github.com/dhis2/capture-app/compare/v100.76.1...v100.77.0) (2024-08-13) diff --git a/package.json b/package.json index 7fed3d6c03..a789f90aca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.77.0", + "version": "101.0.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.77.0", + "@dhis2/rules-engine-javascript": "101.0.0", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 1778c10745..38a32c7d08 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.77.0", + "version": "101.0.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { From bad53124de25367fd633c0cfdd50efacb6eb022a Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Wed, 14 Aug 2024 11:32:38 +0200 Subject: [PATCH 25/33] feat: [DHIS2-17655] Two event workspace (#3726) --- i18n/en.pot | 18 +- .../Buttons/OverflowButton.component.js | 2 + .../PageLayout/DefaultPageLayout.constants.js | 6 + .../DefaultEnrollmentLayout.types.js | 1 + .../EnrollmentPageLayout.js | 3 +- .../LayoutComponentConfig.js | 12 ++ .../WidgetEventEditWrapper.js | 17 +- .../FlatListOrgUnitField.js | 19 +++ .../FlatListOrgUnitField/index.js | 3 + .../WidgetTwoEventWorkspace.component.js | 59 +++++++ .../WidgetTwoEventWorkspace.container.js | 157 ++++++++++++++++++ .../WidgetTwoEventWorkspace.types.js | 24 +++ .../hooks/useClientDataValues.js | 97 +++++++++++ .../hooks/useLinkedEventByOriginId.js | 110 ++++++++++++ .../WidgetTwoEventWorkspace/index.js | 3 + .../utils/getDataEntryDetails.js | 74 +++++++++ .../utils/getSubValueForDataValue.js | 60 +++++++ 17 files changed, 659 insertions(+), 6 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/FlatListOrgUnitField/FlatListOrgUnitField.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/FlatListOrgUnitField/index.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.container.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.types.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/hooks/useClientDataValues.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/hooks/useLinkedEventByOriginId.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/index.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.js create mode 100644 src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getSubValueForDataValue.js diff --git a/i18n/en.pot b/i18n/en.pot index cc88503ce6..a24932402c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -35,6 +35,9 @@ msgstr "" "(in the same domain). Please refresh this page if you would like to use " "this version again, but be aware that this will close other versions." +msgid "More" +msgstr "More" + msgid "View {{programName}} dashboard" msgstr "View {{programName}} dashboard" @@ -995,6 +998,9 @@ msgstr "The enrollment event data could not be found" msgid "Loading" msgstr "Loading" +msgid "An error occurred while loading the form" +msgstr "An error occurred while loading the form" + msgid "Possible duplicates found" msgstr "Possible duplicates found" @@ -1537,6 +1543,15 @@ msgstr "{{ scheduledEvents }} scheduled" msgid "Stages and Events" msgstr "Stages and Events" +msgid "An error occurred while loading the widget." +msgstr "An error occurred while loading the widget." + +msgid "View linked event" +msgstr "View linked event" + +msgid "Scheduled" +msgstr "Scheduled" + msgid "Changelog" msgstr "Changelog" @@ -1703,9 +1718,6 @@ msgstr "An error has occured. See log for details" msgid "Scheduled{{ escape }} due {{ time }}" msgstr "Scheduled{{ escape }} due {{ time }}" -msgid "Scheduled" -msgstr "Scheduled" - msgid "Overdue{{ escape }} due {{ time }}" msgstr "Overdue{{ escape }} due {{ time }}" diff --git a/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js b/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js index 476e950611..5f20a47c92 100644 --- a/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js +++ b/src/core_modules/capture-core/components/Buttons/OverflowButton.component.js @@ -1,5 +1,6 @@ // @flow import * as React from 'react'; +import i18n from '@dhis2/d2-i18n'; import { useRef, useState } from 'react'; import { Button, Layer, Popper } from '@dhis2/ui'; @@ -42,6 +43,7 @@ export const OverflowButton = ({ return (