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-16305] Enrollment Overview Plugins #3515

Merged
merged 34 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
09d1db4
feat: support custom config
eirikhaugstulen Dec 7, 2023
82af7c9
feat: make columns optional
eirikhaugstulen Dec 7, 2023
ea0c8d3
feat: improve rendering logic
eirikhaugstulen Dec 7, 2023
25dcdcd
feat: enable per-page layout
eirikhaugstulen Dec 11, 2023
83ccb60
feat: add custom settings
eirikhaugstulen Dec 11, 2023
62785df
feat: use config for EnrollmentAddEventPage
eirikhaugstulen Dec 12, 2023
9be3b1f
feat: use config for enrollmentEditEventPage
eirikhaugstulen Dec 12, 2023
fbfa29d
chore: defaults
eirikhaugstulen Dec 12, 2023
c3ad9fb
chore: tests
eirikhaugstulen Dec 13, 2023
faba67f
chore: temps
eirikhaugstulen Dec 14, 2023
baad124
fix: custom title support
eirikhaugstulen Dec 15, 2023
dcdecd9
feat: full plugins support with experimental release
eirikhaugstulen Jan 2, 2024
37f6ce3
fix: update app-runtime
eirikhaugstulen Jan 9, 2024
e1e6b1f
fix: use form field plugins
eirikhaugstulen Jan 9, 2024
3601b8f
chore: update strings
eirikhaugstulen Jan 9, 2024
1c4ce6f
Merge branch 'eh/feat/DHIS2-15475-SwitchToPluginComponent' into eh/fe…
eirikhaugstulen Jan 17, 2024
7eaecd8
chore: consistent-return
eirikhaugstulen Jan 18, 2024
b386fbd
Merge remote-tracking branch 'origin/master' into eh/feat/DHIS2-16262…
eirikhaugstulen Jan 22, 2024
ec339e3
chore: fix default title
eirikhaugstulen Jan 22, 2024
070ffc1
chore: rename EnrollmentComment
eirikhaugstulen Jan 22, 2024
7adf03d
chore: update cypress tests
eirikhaugstulen Jan 22, 2024
bdbc3b4
chore: update cypress tests
eirikhaugstulen Jan 22, 2024
07c49ce
Merge remote-tracking branch 'origin/master' into eh/feat/DHIS2-15475…
eirikhaugstulen Jan 25, 2024
0d136c4
chore: yarn.lock
eirikhaugstulen Jan 25, 2024
d7d0cc1
Merge remote-tracking branch 'origin/master' into eh/feat/DHIS2-16305…
eirikhaugstulen Jan 25, 2024
a27522a
chore: yarn.lock
eirikhaugstulen Jan 26, 2024
82d9cb3
Merge branch 'eh/feat/DHIS2-15475-SwitchToPluginComponent' into eh/fe…
eirikhaugstulen Jan 26, 2024
06e1716
chore: temp
eirikhaugstulen Jan 26, 2024
df59069
Merge remote-tracking branch 'origin/master' into eh/feat/DHIS2-16262…
eirikhaugstulen Jan 26, 2024
94f92d6
feat: add assignee widget
eirikhaugstulen Jan 26, 2024
271c74d
Merge branch 'eh/feat/DHIS2-16262_CustomEnrollmentOverviewSchema' int…
eirikhaugstulen Jan 26, 2024
f3a3757
fix: pr-review
eirikhaugstulen Jan 30, 2024
378f5aa
Merge remote-tracking branch 'origin/master' into eh/feat/DHIS2-16305…
eirikhaugstulen Feb 4, 2024
bbe7b7f
Merge remote-tracking branch 'origin/master' into eh/feat/DHIS2-16305…
eirikhaugstulen Feb 5, 2024
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
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
// @flow

import { useDataEngine } from '@dhis2/app-runtime';
import { useQuery } from 'react-query';
import { useApiMetadataQuery } from '../../../../../../utils/reactQueryHelpers';

type Props = {|
selectedScopeId: string,
|}

const configQuery = {
dataEntryFormConfigQuery: {
resource: 'dataStore/capture/dataEntryForms',
},
resource: 'dataStore/capture/dataEntryForms',
};

export const useDataEntryFormConfig = ({ selectedScopeId }: Props) => {
const dataEngine = useDataEngine();


const { data: dataEntryFormConfig, isFetched: configIsFetched } = useQuery(
['dataEntryFormConfig'],
() => dataEngine.query(configQuery),
const { data: dataEntryFormConfig, isFetched: configIsFetched } = useApiMetadataQuery(
['dataEntryFormConfig', selectedScopeId],
configQuery,
{
enabled: !!selectedScopeId,
select: ({ dataEntryFormConfigQuery }) => dataEntryFormConfigQuery?.[selectedScopeId],
cacheTime: Infinity,
staleTime: Infinity,
select: dataEntryFormConfigQuery => dataEntryFormConfigQuery[selectedScopeId],
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@ type DefaultComponents = 'QuickActions'
| 'ProfileWidget'
| 'EnrollmentWidget';

export type ColumnConfig = {
type: $Values<typeof WidgetTypes>,
export type DefaultWidgetColumnConfig = {
type: typeof WidgetTypes.COMPONENT,
name: DefaultComponents,
settings?: Object,
}

export type PluginWidgetColumnConfig = {
type: typeof WidgetTypes.PLUGIN,
source: string,
}

export type ColumnConfig = DefaultWidgetColumnConfig | PluginWidgetColumnConfig;

export type PageLayoutConfig = {
title?: ?string,
leftColumn: ?Array<ColumnConfig>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,70 +1,18 @@
// @flow
import React, { useCallback, useMemo } from 'react';
import log from 'loglevel';
import { errorCreator } from '../../../../../../../capture-core-utils';

import { WidgetTypes } from '../DefaultEnrollmentLayout.constants';
import type { ColumnConfig, PageLayoutConfig, WidgetConfig } from '../DefaultEnrollmentLayout.types';
import { useCallback, useMemo } from 'react';
import type {
ColumnConfig,
PageLayoutConfig,
WidgetConfig,
} from '../DefaultEnrollmentLayout.types';
import { renderWidgets } from '../renderPageComponents';

type Props = {
pageLayout: PageLayoutConfig,
availableWidgets: $ReadOnly<{ [key: string]: WidgetConfig }>,
props: Object,
};

const MemoizedWidgets: { [key: string]: React$ComponentType<any> } = {};
const UnsupportedWidgets: { [key: string]: boolean } = {};

const renderWidget = (widget: ColumnConfig, availableWidgets, props) => {
const { type } = widget;

if (type.toLowerCase() === WidgetTypes.COMPONENT) {
const { name, settings = {} } = widget;
const widgetConfig = availableWidgets[name];

if (!widgetConfig) {
if (!UnsupportedWidgets[name]) {
log.error(errorCreator(`Widget ${name} is not supported`)({ name }));
UnsupportedWidgets[name] = true;
}
return null;
}

const { getProps, shouldHideWidget, getCustomSettings } = widgetConfig;

const hideWidget = shouldHideWidget && shouldHideWidget(props);
if (hideWidget) return null;
let widgetProps = {};

// In case the widget is not supported, we don't want to crash the app
try {
widgetProps = getProps(props);
} catch (error) {
log.error(errorCreator(`Error while getting widget props for widget ${name}`)({ error, props }));
return null;
}
const customSettings = getCustomSettings && getCustomSettings(settings);

let Widget = MemoizedWidgets[name];

if (!Widget) {
Widget = widgetConfig.Component;
MemoizedWidgets[name] = React.memo(Widget);
}

return (
<Widget
{...widgetProps}
{...customSettings}
key={name}
/>
);
}

log.error(errorCreator(`Widget type ${type} is not supported`)({ type }));
return null;
};

export const useWidgetColumns = ({
pageLayout,
availableWidgets,
Expand All @@ -76,7 +24,7 @@ export const useWidgetColumns = ({
} = pageLayout;

const createColumnWidgets = useCallback(column =>
column?.map((widget: ColumnConfig) => renderWidget(widget, availableWidgets, props)).filter(Boolean),
column?.map((widget: ColumnConfig) => renderWidgets(widget, availableWidgets, props)).filter(Boolean),
[availableWidgets, props],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { renderWidgets } from './renderPageComponents';
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// @flow
import React from 'react';
import log from 'loglevel';
import type {
ColumnConfig,
DefaultWidgetColumnConfig,
PluginWidgetColumnConfig, WidgetConfig,
} from '../DefaultEnrollmentLayout.types';
import { errorCreator } from '../../../../../../../capture-core-utils';
import { EnrollmentPlugin } from '../../../EnrollmentPlugin';
import { WidgetTypes } from '../DefaultEnrollmentLayout.constants';

const MemoizedWidgets: { [key: string]: React$ComponentType<any> } = {};
const UnsupportedWidgets: { [key: string]: boolean } = {};

const renderComponent = (
widget: ColumnConfig,
availableWidgets: $ReadOnly<{ [key: string]: WidgetConfig }>,
props: Object,
) => {
// Manually casting the type to DefaultWidgetColumnConfig
const { name, settings = {} } = ((widget: any): DefaultWidgetColumnConfig);
const widgetConfig = availableWidgets[name];

if (!widgetConfig) {
if (!UnsupportedWidgets[name]) {
log.error(errorCreator(`Widget ${name} is not supported`)({ name }));
UnsupportedWidgets[name] = true;
}
return null;
}

const { getProps, shouldHideWidget, getCustomSettings } = widgetConfig;

const hideWidget = shouldHideWidget && shouldHideWidget(props);
if (hideWidget) return null;
let widgetProps = {};

// In case the widget is not supported, we don't want to crash the app
try {
widgetProps = getProps(props);
} catch (error) {
log.error(errorCreator(`Error while getting widget props for widget ${name}`)({ error, props }));
return null;
}
const customSettings = getCustomSettings && getCustomSettings(settings);

let Widget = MemoizedWidgets[name];

if (!Widget) {
Widget = widgetConfig.Component;
MemoizedWidgets[name] = React.memo(Widget);
}

return (
<Widget
{...widgetProps}
{...customSettings}
key={name}
/>
);
};

const getPropsForPlugin = ({ program, enrollmentId, teiId, orgUnitId }) => ({
programId: program.id,
enrollmentId,
teiId,
orgUnitId,
});

const renderPlugin = (
widget: ColumnConfig,
availableWidgets: $ReadOnly<{ [key: string]: WidgetConfig }>,
props: Object,
) => {
// Manually casting the type to PluginWidgetColumnConfig
const { source } = ((widget: any): PluginWidgetColumnConfig);
let PluginWidget = MemoizedWidgets[source];

if (!PluginWidget) {
PluginWidget = EnrollmentPlugin;
MemoizedWidgets[source] = (PluginWidget);
}
const widgetProps = getPropsForPlugin(props);

return (
<PluginWidget
key={source}
pluginSource={source}
{...widgetProps}
/>
);
};

export const renderWidgets = (
widget: ColumnConfig,
availableWidgets: $ReadOnly<{ [key: string]: WidgetConfig }>,
props: Object,
) => {
const { type } = widget;

if (type.toLowerCase() === WidgetTypes.COMPONENT) {
return renderComponent(widget, availableWidgets, props);
} else if (type.toLowerCase() === WidgetTypes.PLUGIN) {
return renderPlugin(widget, availableWidgets, props);
}

log.error(errorCreator(`Widget type ${type} is not supported`)({ type }));
return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @flow
import React, { useEffect, useRef, useState } from 'react';
import { Plugin } from '@dhis2/app-runtime/build/es/experimental';
import { useHistory } from 'react-router-dom';

type EnrollmentPluginProps = {|
enrollmentId: string,
programId?: string,
teiId: string,
orgUnitId: string,
pluginSource: string,
|};

export const EnrollmentPlugin = ({ pluginSource, ...passOnProps }: EnrollmentPluginProps) => {
const [pluginWidth, setPluginWidth] = useState(undefined);
const history = useHistory();
const containerRef = useRef<?HTMLDivElement>();

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 (
<div ref={containerRef}>
<Plugin
pluginSource={pluginSource}
width={pluginWidth}
navigate={history.push}
{...passOnProps}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { EnrollmentPlugin } from './EnrollmentPlugin';
Loading