Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [DHIS2-15475] Form Field Plugins #3502

Merged
merged 5 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ msgstr "error encountered during field validation"
msgid "error"
msgstr "error"

msgid "Plugins are not yet available - Please contact your system administrator"
msgstr "Plugins are not yet available - Please contact your system administrator"

msgid "This value is validating"
msgstr "This value is validating"

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"@babel/preset-react": "^7.16.7",
"@badeball/cypress-cucumber-preprocessor": "17.2.1",
"@cypress/webpack-preprocessor": "^6.0.0",
"@dhis2/cli-app-scripts": "^9.0.1",
"@dhis2/cli-app-scripts": "^10.4.0",
"@dhis2/cli-helpers-engine": "^3.2.1",
"@dhis2/cli-style": "^10.4.1",
"@dhis2/cli-utils-cypress": "^9.0.2",
Expand Down Expand Up @@ -132,6 +132,9 @@
"wait-on": "^6.0.1"
},
"resolutions": {
"@dhis2/cli-app-scripts": "^10.4.0",
"@dhis2/app-runtime": "^3.10.2",
"react-scripts": "4.0.3",
"@babel/preset-react": "7.16.7",
"@dhis2/ui": "^9.1.1",
"@js-temporal/polyfill": "0.4.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ export class D2SectionFieldsComponent extends Component<Props> {
component: FormFieldPlugin,
plugin: true,
props: {
pluginId: metaDataElement.id,
name: metaDataElement.name,
pluginSource: metaDataElement.pluginSource,
fieldsMetadata: metaDataElement.fields,
customAttributes: metaDataElement.customAttributes,
formId: props.formId,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,17 +537,21 @@ export class FormBuilder extends React.Component<Props> {
pluginContext,
} = this.props;
const {
fieldsMetadata,
pluginId,
pluginSource,
fieldsMetadata,
customAttributes,
name,
formId,
} = field.props;

return (
<field.component
name={name}
fieldsMetadata={fieldsMetadata}
pluginId={pluginId}
customAttributes={customAttributes}
pluginSource={pluginSource}
fieldsMetadata={fieldsMetadata}
formId={formId}
onUpdateField={this.commitFieldUpdateFromPlugin.bind(this)}
pluginContext={pluginContext}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
// @flow
import React from 'react';
import i18n from '@dhis2/d2-i18n';
import React, { useEffect, useRef, useState } from 'react';
import { Plugin } from '@dhis2/app-runtime/build/es/experimental';
import type { ComponentProps } from './FormFieldPlugin.types';

export const FormFieldPluginComponent = (props: ComponentProps) => {
// eslint-disable-next-line no-unused-vars
const { pluginSource, ...passOnProps } = props;
const containerRef = useRef<?HTMLDivElement>(null);
const [pluginWidth, setPluginWidth] = useState(0);

useEffect(() => {
const { current: container } = containerRef;
if (!container) return () => {};

const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => setPluginWidth(entry.contentRect.width));
});

resizeObserver.observe(container);

// Cleanup function
return () => {
resizeObserver.unobserve(container);
resizeObserver.disconnect();
};
}, [containerRef]);


return (
<p>
{i18n.t('Plugins are not yet available - Please contact your system administrator')}
</p>
<div ref={containerRef}>
<Plugin
pluginSource={pluginSource}
width={pluginWidth}
{...passOnProps}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import React, { useEffect, useMemo } from 'react';
import React, { useMemo } from 'react';
import { FormFieldPluginComponent } from './FormFieldPlugin.component';
import type { ContainerProps } from './FormFieldPlugin.types';
import { usePluginMessages } from './hooks/usePluginMessages';
Expand All @@ -9,7 +9,14 @@ import { formatPluginConfig } from './formatPluginConfig';
import { useLocationQuery } from '../../../utils/routing';

export const FormFieldPlugin = (props: ContainerProps) => {
const { pluginSource, fieldsMetadata, formId, onUpdateField, pluginContext } = props;
const {
pluginSource,
fieldsMetadata,
formId,
onUpdateField,
customAttributes,
pluginContext,
} = props;
const metadataByPluginId = useMemo(() => Object.fromEntries(fieldsMetadata), [fieldsMetadata]);
const configuredPluginIds = useMemo(() => Object.keys(metadataByPluginId), [metadataByPluginId]);
const { orgUnitId } = useLocationQuery();
Expand All @@ -24,20 +31,18 @@ export const FormFieldPlugin = (props: ContainerProps) => {
pluginContext,
});

// Expanding iframe height temporarily to fit content - LIBS-487
useEffect(() => {
const iframe = document.querySelector('iframe');
if (iframe) iframe.style.height = '500px';
}, []);

// Remove ids from plugin metadata before passing to plugin
const formattedMetadata = useMemo(() => {
const metadata = [...fieldsMetadata.entries()];
return metadata.reduce((acc, [pluginId, pluginMetadata]) => {
const formattedPluginMetadata = formatPluginConfig(pluginMetadata, { keysToOmit: ['id'] });
return { ...acc, [pluginId]: formattedPluginMetadata };

return metadata.reduce((acc, [pluginFieldId, pluginMetadata]) => {
const formattedPluginMetadata = formatPluginConfig(pluginMetadata, {
attributes: customAttributes,
keysToOmit: ['id', 'dataElement', 'section'],
});
return { ...acc, [pluginFieldId]: formattedPluginMetadata };
}, {});
}, [fieldsMetadata]);
}, [customAttributes, fieldsMetadata]);

return (
<FormFieldPluginComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type PluginFormFieldMetadata = {|
searchable: ?boolean;
url: ?string;
attributeValues?: { [pluginId: string]: any }

|}

type FieldValueOptions = {|
Expand All @@ -45,10 +46,12 @@ export type PluginContext = {|
|}

export type ContainerProps = {|
pluginId: string,
pluginSource: string,
fieldsMetadata: Map<string, PluginFormFieldMetadata>,
pluginContext: PluginContext,
formId: string,
customAttributes: { [id: string]: { IdFromPlugin: string, IdFromApp: string } },
onUpdateField: (fieldMetadata: PluginFormFieldMetadata, value: any, options?: FieldValueOptions) => void,
|}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@ export const formatPluginConfig = <TConfigReturn = PluginFormFieldMetadata>(
}

// Recursively process nested objects and arrays
if (value && typeof value === 'object') {
if (Array.isArray(value)) {
acc[modifiedKey] = value.map(removeUnderscoreFromObjectAttributes).filter(Boolean);
if (value !== null) {
if (typeof value === 'object') {
if (Array.isArray(value)) {
acc[modifiedKey] = value.map(removeUnderscoreFromObjectAttributes)
.filter(Boolean);
} else {
acc[modifiedKey] = removeUnderscoreFromObjectAttributes(value);
}
} else {
acc[modifiedKey] = removeUnderscoreFromObjectAttributes(value);
acc[modifiedKey] = value;
}
} else {
acc[modifiedKey] = value;
}

return acc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { MetadataByPluginId, PluginContext } from '../FormFieldPlugin.types
export const usePluginValues = (
formId: string,
metadataByPluginId: MetadataByPluginId,
pluginContext: PluginContext,
pluginContext: PluginContext = {},
) => {
const formValuesRedux = useSelector(({ formsValues }) => formsValues[formId]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
/* eslint-disable no-restricted-syntax */

import isFunction from 'd2-utilizr/lib/isFunction';
import type { PluginFormFieldMetadata } from '../../components/D2Form/FormFieldPlugin';
import { DataElement } from '../DataElement';

export class FormFieldPluginConfig {
_id: string;
_name: string;
_pluginSource: string;
_fields: Map<string, PluginFormFieldMetadata>;
_fields: Map<string, DataElement>;
_customAttributes: Map<string, { IdFromPlugin: string, IdFromApp: string }>;

constructor(initFn: ?(_this: FormFieldPluginConfig) => void) {
initFn && isFunction(initFn) && initFn(this);
Expand All @@ -32,23 +33,31 @@ export class FormFieldPluginConfig {
this._name = value;
}

get fields(): Map<string, PluginFormFieldMetadata> {
get fields(): Map<string, DataElement> {
return this._fields;
}

set fields(value: Map<string, PluginFormFieldMetadata>) {
set fields(value: Map<string, DataElement>) {
this._fields = value;
}

get pluginSource(): string {
return this._pluginSource;
}

get customAttributes(): Map<string, { IdFromPlugin: string, IdFromApp: string }> {
return this._customAttributes;
}

set customAttributes(value: Map<string, { IdFromPlugin: string, IdFromApp: string }>) {
this._customAttributes = value;
}

set pluginSource(value: string) {
this._pluginSource = value;
}

addField(idFromPlugin: string, field: PluginFormFieldMetadata) {
addField(idFromPlugin: string, field: DataElement) {
if (!this.fields.has(idFromPlugin)) {
this.fields.set(idFromPlugin, field);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import { FormFieldPluginConfig } from '../../../../metaData/FormFieldPluginConfi
import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/types';
import { FormFieldTypes } from '../../../../components/D2Form/FormFieldPlugin/FormFieldPlugin.const';
import {
formatPluginConfig,
} from '../../../../components/D2Form/FormFieldPlugin/formatPluginConfig';
FieldElementObjectTypes,
} from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm';

export class EnrollmentFactory {
static errorMessages = {
Expand Down Expand Up @@ -141,28 +141,27 @@ export class EnrollmentFactory {
// $FlowFixMe
await cachedProgramTrackedEntityAttributes.asyncForEach(async (trackedEntityAttribute) => {
if (trackedEntityAttribute?.type === FormFieldTypes.PLUGIN) {
const attributes = trackedEntityAttribute.fieldMap
.filter(attributeField => attributeField.objectType === FieldElementObjectTypes.ATTRIBUTE)
.reduce((acc, attribute) => {
acc[attribute.IdFromApp] = attribute;
return acc;
}, {});

const element = new FormFieldPluginConfig((o) => {
o.id = trackedEntityAttribute.id;
o.name = trackedEntityAttribute.name;
o.pluginSource = trackedEntityAttribute.pluginSource;
o.fields = new Map();
o.customAttributes = attributes;
});

const attributes = trackedEntityAttribute.fieldMap
.filter(attributeField => attributeField.objectType === 'Attribute')
.reduce((acc, attribute) => {
acc[attribute.IdFromApp] = attribute;
return acc;
}, {});

await trackedEntityAttribute.fieldMap.asyncForEach(async (field) => {
if (field.objectType) {
const fieldElement = await this.dataElementFactory.build(field);
if (field.objectType && field.objectType === FieldElementObjectTypes.TRACKED_ENTITY_ATTRIBUTE) {
const fieldElement = await this.dataElementFactory.build(field, section);
if (!fieldElement) return;

const fieldMetadata = formatPluginConfig(fieldElement, { attributes });

element.addField(field.IdFromPlugin, fieldMetadata);
element.addField(field.IdFromPlugin, fieldElement);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import type { ConstructorInput } from './teiRegistrationFactory.types';
import { FormFieldPluginConfig } from '../../../../metaData/FormFieldPluginConfig';
import type { DataEntryFormConfig } from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/types';
import { FormFieldTypes } from '../../../../components/D2Form/FormFieldPlugin/FormFieldPlugin.const';
import { formatPluginConfig } from '../../../../components/D2Form/FormFieldPlugin/formatPluginConfig';
import {
FieldElementObjectTypes,
} from '../../../../components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm';
Expand Down Expand Up @@ -123,27 +122,27 @@ export class TeiRegistrationFactory {

await fieldElements.asyncForEach(async (trackedEntityAttribute) => {
if (trackedEntityAttribute?.type === FormFieldTypes.PLUGIN) {
const attributes = trackedEntityAttribute.fieldMap
.filter(attributeField => attributeField.objectType === 'Attribute')
.reduce((acc, attribute) => {
acc[attribute.IdFromApp] = attribute;
return acc;
}, {});
const element = new FormFieldPluginConfig((o) => {
o.id = trackedEntityAttribute.id;
o.name = trackedEntityAttribute.name;
o.pluginSource = trackedEntityAttribute.pluginSource;
o.fields = new Map();
o.customAttributes = attributes;
});

const attributes = trackedEntityAttribute.fieldMap
.filter(attributeField => attributeField.objectType === 'Attribute')
.reduce((acc, attribute) => {
acc[attribute.IdFromApp] = attribute;
return acc;
}, {});

await trackedEntityAttribute.fieldMap.asyncForEach(async (field) => {
if (field.objectType === FieldElementObjectTypes.TRACKED_ENTITY_ATTRIBUTE) {
const dataElement = await this.dataElementFactory.build(field);
if (!dataElement) return;

const fieldMetadata = formatPluginConfig(dataElement, { attributes });
element.addField(field.IdFromPlugin, fieldMetadata);
element.addField(field.IdFromPlugin, dataElement);
}
});

Expand Down
Loading
Loading