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); + } +}