Skip to content

Commit

Permalink
feat: plugins in profile widget
Browse files Browse the repository at this point in the history
  • Loading branch information
eirikhaugstulen committed Jul 9, 2024
1 parent bb44911 commit 2ee5572
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const PluginErrorMessages = Object.freeze({
});

export const FormFieldTypes = Object.freeze({
// TODO [DHIS2-17605] - Unified field types
DATA_ELEMENT: 'dataElement',
PLUGIN: 'plugin',
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow
export { getGeneratedUniqueValuesAsync, getUniqueValuesForAttributesWithoutValue } from './getGeneratedUniqueValuesAsync';
export { geometryType, getPossibleTetFeatureTypeKey, buildGeometryProp } from './geometry';
export type { DataEntryFormConfig } from './useMetadataForRegistrationForm';
export { useDataEntryFormConfig, FieldElementObjectTypes } from './useMetadataForRegistrationForm';
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// @flow
export { useMetadataForRegistrationForm, FieldElementObjectTypes } from './useMetadataForRegistrationForm';
export { useDataEntryFormConfig } from './hooks/useDataEntryFormConfig';
export type { DataEntryFormConfig } from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const DataEntry = ({
onSaveExternal,
geometry,
trackedEntityName,
dataEntryFormConfig,
}: Props) => {
const dataEntryId = 'trackedEntityProfile';
const itemId = 'edit';
Expand All @@ -34,6 +35,7 @@ export const DataEntry = ({
dataEntryId,
itemId,
geometry,
dataEntryFormConfig,
});
const { formFoundation } = context;
const { formValidated, errorsMessages, warningsMessages } = useFormValidations(dataEntryId, itemId, saveAttempted);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
// @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 {
ProgramTrackedEntityAttribute,
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) => {
Expand Down Expand Up @@ -62,7 +75,7 @@ const buildMainSection = async ({
trackedEntityType: TrackedEntityType,
trackedEntityAttributes: Array<TrackedEntityAttribute>,
optionSets: Array<OptionSet>,
programTrackedEntityAttributes?: ?Array<ProgramTrackedEntityAttribute>,
programTrackedEntityAttributes?: ?Array<ProgramTrackedEntityAttribute | PluginElement>,
querySingleResource: QuerySingleResource,
minorServerVersion: number,
}) => {
Expand Down Expand Up @@ -97,23 +110,63 @@ const buildElementsForSection = async ({
querySingleResource,
minorServerVersion,
}: {
programTrackedEntityAttributes: Array<ProgramTrackedEntityAttribute>,
programTrackedEntityAttributes: Array<ProgramTrackedEntityAttribute | PluginElement>,
trackedEntityAttributes: Array<TrackedEntityAttribute>,
optionSets: Array<OptionSet>,
section: Section,
querySingleResource: QuerySingleResource,
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;
};
Expand All @@ -127,7 +180,7 @@ const buildSection = async ({
querySingleResource,
minorServerVersion,
}: {
programTrackedEntityAttributes?: Array<ProgramTrackedEntityAttribute>,
programTrackedEntityAttributes?: Array<ProgramTrackedEntityAttribute | PluginElement>,
trackedEntityAttributes: Array<TrackedEntityAttribute>,
optionSets: Array<OptionSet>,
sectionCustomLabel: string,
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// @flow

import type { Geometry } from './helpers/types';
import type {
DataEntryFormConfig,
} from '../../DataEntries/common/TEIAndEnrollment';

export type PlainProps = {|
dataEntryId: string,
Expand All @@ -23,6 +26,7 @@ export type PlainProps = {|
export type Props = {|
programAPI: any,
orgUnitId: string,
dataEntryFormConfig: ?DataEntryFormConfig,
onCancel: () => void,
onDisable: () => void,
clientAttributesWithSubvalues: Array<any>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>({});
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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,6 +34,7 @@ export const useLifecycle = ({
dataEntryId,
itemId,
geometry,
dataEntryFormConfig,
}: {
programAPI: any,
orgUnitId: string,
Expand All @@ -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.
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 2ee5572

Please sign in to comment.