From 205b9f5746f3d9b3d0d28035fd99e40b5649003c Mon Sep 17 00:00:00 2001 From: Eirik Haugstulen Date: Thu, 8 Aug 2024 00:05:39 +0200 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 14/16] 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 15/16] 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 16/16] 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);