diff --git a/src/app/app.jsx b/src/app/app.jsx
index d77c52b033..463f93a964 100644
--- a/src/app/app.jsx
+++ b/src/app/app.jsx
@@ -15,6 +15,7 @@ import {
} from '../shared/index.js'
import { Layout } from './layout/index.js'
import LoadApp from './load-app.jsx'
+import { LegacyDhis2BridgeProvider } from '../shared/legacy-dhis2-bridge/legacy-dhis2-bridge-provider'
const idSidebarMap = {
[contextualHelpSidebarId]: ContextualHelpSidebar,
@@ -44,12 +45,14 @@ const App = () => {
return (
- }
- showSidebar={!!id}
- />
+
+ }
+ showSidebar={!!id}
+ />
+
)
}
diff --git a/src/data-workspace/custom-form/custom-form.jsx b/src/data-workspace/custom-form/custom-form.jsx
index afe96b0306..8bbecdaaf3 100644
--- a/src/data-workspace/custom-form/custom-form.jsx
+++ b/src/data-workspace/custom-form/custom-form.jsx
@@ -1,15 +1,11 @@
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useRef } from 'react'
import useCustomForm from '../../custom-forms/use-custom-form.js'
import { useMetadata } from '../../shared/index.js'
import styles from './custom-form.module.css'
import { parseHtmlToReact } from './parse-html-to-react.jsx'
+import { useRunInlineScripts } from "../../shared/legacy-dhis2-bridge/use-run-inline-scripts";
-/**
- * This implementation of custom forms only supports custom
- * HTML and CSS. It does not support custom logic (JavaScript).
- * For more info see ./docs/custom-froms.md
- */
export const CustomForm = ({ dataSet }) => {
const { data: customForm } = useCustomForm({
id: dataSet.dataEntryForm.id,
@@ -17,8 +13,16 @@ export const CustomForm = ({ dataSet }) => {
})
const { data: metadata } = useMetadata()
+ const containerRef = useRef(null)
+
+ useRunInlineScripts({
+ containerRef,
+ dataSetId: dataSet.id
+ }, [customForm?.htmlCode, dataSet.id])
+
+
return customForm ? (
-
+
{parseHtmlToReact(customForm.htmlCode, metadata)}
) : null
diff --git a/src/data-workspace/custom-form/parse-html-to-react.jsx b/src/data-workspace/custom-form/parse-html-to-react.jsx
index f8fcff4fc4..4fa48306d4 100644
--- a/src/data-workspace/custom-form/parse-html-to-react.jsx
+++ b/src/data-workspace/custom-form/parse-html-to-react.jsx
@@ -12,8 +12,8 @@ export const parseHtmlToReact = (htmlCode, metadata) =>
case 'td':
return replaceTdNode(domNode, metadata)
case 'script':
- // remove script tags
- return <>>
+ // Always allow scripts to pass through, but execute them manually in CustomForm
+ return undefined
default:
return undefined
}
diff --git a/src/data-workspace/inputs/generic-input.jsx b/src/data-workspace/inputs/generic-input.jsx
index 29935d4588..e7e2aa79d8 100644
--- a/src/data-workspace/inputs/generic-input.jsx
+++ b/src/data-workspace/inputs/generic-input.jsx
@@ -114,6 +114,7 @@ export const GenericInput = ({
return (
{
+ initializeDhis2Bridge()
+ }, [])
+
+ useEffect(() => {
+ if (typeof window === 'undefined' || !window.isLegacyDhis2Bridge) return
+ updateDhis2Bridge(orgUnitId, dataSetId, selectedPeriod)
+ }, [orgUnitId, dataSetId, selectedPeriod])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useLegacyDhis2BridgeContext() {
+ const ctx = useContext(LegacyDhis2BridgeContext)
+ if (!ctx)
+ throw new Error(
+ 'useLegacyDhis2BridgeContext must be used within a LegacyDhis2BridgeProvider'
+ )
+ return ctx
+}
+
+function useLegacyDhis2Bridge() {
+ const emit = useCustomEvent({
+ target: typeof window !== 'undefined' ? window : null,
+ });
+
+ return {
+ dhis2: typeof window !== 'undefined' ? window.dhis2 : undefined,
+ emit,
+ }
+}
+
+export function initializeDhis2Bridge() {
+ if (typeof window === 'undefined') return
+
+ // Only skip if bridge already initialized it
+ if (window.isLegacyDhis2Bridge && window.dhis2) return
+
+ window.dhis2 = {
+ util: {
+ on(type, handler) {
+ window.removeEventListener(type, handler)
+ window.addEventListener(type, handler)
+ },
+ off(type, handler) {
+ window.removeEventListener(type, handler)
+ },
+ },
+ de: {
+ event: { ...DE_EVENTS },
+ currentOrganisationUnitId: null,
+ currentDataSetId: null,
+ getSelectedPeriod() {
+ return null
+ },
+ },
+ }
+ window.isLegacyDhis2Bridge = true
+ console.info('Legacy DHIS2 bridge initialized')
+}
+
+function updateDhis2Bridge(orgUnitId, dataSetId, selectedPeriod) {
+ if (typeof window === 'undefined' || !window.isLegacyDhis2Bridge) return
+
+ if (window.dhis2?.de) {
+ window.dhis2.de.currentOrganisationUnitId = orgUnitId
+ window.dhis2.de.currentDataSetId = dataSetId
+ window.dhis2.de.getSelectedPeriod = () => selectedPeriod
+ }
+}
diff --git a/src/shared/legacy-dhis2-bridge/legacy-events.js b/src/shared/legacy-dhis2-bridge/legacy-events.js
new file mode 100644
index 0000000000..443d0f2480
--- /dev/null
+++ b/src/shared/legacy-dhis2-bridge/legacy-events.js
@@ -0,0 +1,18 @@
+//copied from window.dhis2.de.event in DHIS2 2.41
+export const DE_EVENTS = {
+ completed: 'dhis2.de.event.completed',
+ dataValueSaved: 'dhis2.de.event.dataValueSaved',
+ dataValuesLoaded: 'dhis2.de.event.dataValuesLoaded',
+ formLoaded: 'dhis2.de.event.formLoaded',
+ formReady: 'dhis2.de.event.formReady',
+ uncompleted: 'dhis2.de.event.uncompleted',
+ validationError: 'dhis2.de.event.validationError',
+ validationSucces: 'dhis2.de.event.validationSuccess',
+}
+
+//can replace form triggers
+export const FIELD_EVENTS = {
+ period: 'dhis2.de.event.periodChanged',
+ orgUnit: 'dhis2.de.event.orgUnitChanged',
+ dataSet: 'dhis2.de.event.dataSetChanged',
+}
diff --git a/src/shared/legacy-dhis2-bridge/use-emit.js b/src/shared/legacy-dhis2-bridge/use-emit.js
new file mode 100644
index 0000000000..e022a77d08
--- /dev/null
+++ b/src/shared/legacy-dhis2-bridge/use-emit.js
@@ -0,0 +1,44 @@
+import { useCallback, useEffect, useRef } from 'react';
+
+export function useCustomEvent(opts) {
+ const { target = typeof window !== 'undefined' ? window : null } =
+ opts || {}
+
+ return useCallback(
+ (type, detail) => {
+ if (!target) return false
+ const evt = new CustomEvent(type, {
+ detail
+ })
+ return target.dispatchEvent(evt)
+ },
+ [target]
+ )
+}
+
+export function useEmitOnSet(setFn, { eventName, target, mapDetail }) {
+ const emit = useCustomEvent({ target });
+
+ return useCallback((value) => {
+ setFn(value);
+ const detail = typeof mapDetail === "function" ? mapDetail(value) : value;
+ emit(eventName, detail);
+ }, [setFn, emit, eventName, mapDetail]);
+}
+
+export function useEmitOnChange(value, { eventName, target, mapDetail, fireOnMount = false }) {
+ const emit = useCustomEvent({ target });
+ const isFirstRender = useRef(true);
+
+ useEffect(() => {
+ if (isFirstRender.current) {
+ isFirstRender.current = false;
+ if (!fireOnMount) {
+ return;
+ }
+ }
+
+ const detail = typeof mapDetail === "function" ? mapDetail(value) : value;
+ emit(eventName, detail);
+ }, [value, emit, eventName, mapDetail, fireOnMount]);
+}
diff --git a/src/shared/legacy-dhis2-bridge/use-run-inline-scripts.js b/src/shared/legacy-dhis2-bridge/use-run-inline-scripts.js
new file mode 100644
index 0000000000..8d148da410
--- /dev/null
+++ b/src/shared/legacy-dhis2-bridge/use-run-inline-scripts.js
@@ -0,0 +1,46 @@
+import { useEffect } from 'react';
+import { DE_EVENTS } from './legacy-events';
+import { useCustomEvent } from './use-emit';
+
+export function useRunInlineScripts(
+ { containerRef, dataSetId }, deps
+) {
+ const emit = useCustomEvent();
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ let cancelled = false;
+ (async () => {
+ await runInlineScripts(container);
+ if (!cancelled) emit(DE_EVENTS.formLoaded, dataSetId);
+ })();
+
+ return () => { cancelled = true; };
+ }, deps);
+}
+
+function runInlineScripts(container) {
+ const scripts = [...container.querySelectorAll('script:not([src])')];
+
+ return scripts.reduce((chain, script) => {
+ return chain.then(() => new Promise((resolve) => {
+ const injScript = document.createElement('script');
+ copyAttrs(script, injScript);
+
+ injScript.text = script.text || script.innerHTML || '';
+
+ if (script.parentNode) script.parentNode.replaceChild(injScript, script);
+
+ Promise.resolve().then(resolve)
+ }));
+ }, Promise.resolve());
+}
+
+function copyAttrs(src, dst) {
+ for (const { name, value } of Array.from(src.attributes)) {
+ if (name === 'type' || name === 'async' || name === 'defer') continue;
+ dst.setAttribute(name, value);
+ }
+}