diff --git a/docs/components/Plugin.md b/docs/components/Plugin.md new file mode 100644 index 000000000..a4ee4dff7 --- /dev/null +++ b/docs/components/Plugin.md @@ -0,0 +1,63 @@ +# Plugin Component + +A wrapper that creates an iframe for a specified plugin and establishes a two-way communication channel with said plugin, allowing you to pass props (including callbacks between an app and a plugin). Note that the plugin must be built using the app-platform with entryPoints.plugin specified in the d2.config.js file. + +## Basic Usage (Defining a plugin within an app) + +Within an app you can specify a plugin (either by providing its short name `pluginShortName`, or by specifying a URL directly (`pluginSource`). If you have provided `pluginSource`, this will take precedence (Note: lookup logic is TBD? Should we allow a URL only in development mode, for example?). + +```jsx +import { Plugin } from '@dhis2/app-runtime' + +// within the app +const MyApp = () => ( + { + console.error(err) + }} + showAlertsInPlugin={true} + numberToPass={'42'} + callbackToPass={({ name }) => { + console.log(`Hi ${name}!`) + }} + /> +) +``` + +## Basic Usage (Using properties from the parent app) + +You must build your plugin with the app-platform. If you have done this, your entry component will be passed the props from the parent app. From the example above, the properties `numberToPass` and `callbackToPass` will be available in the build plugin (when it is rendered with a component). + +```jsx +// your plugin entry point (the plugin itself) + +const MyPlugin = (propsFromParent) => { + const { numberToPass, callbackToPass: sayHi } = propsFromParent + return ( + <> +

{`The meaning of life is: ${numberToPass}`}

+ + + ) +} +``` + +## Plugin Props (reserved props) + +| Name | Type | Required | Description | +| :--------------------: | :------------: | :---------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **pluginShortName** | _string_ | _required_ if `pluginSource` is not provided | The shortName of the app/plugin you wish to load (matching the result from api/apps). Used to look up the plugin entry point. If this is not provided, `pluginSource` must be provided. `pluginSource` will take precedence if provided. | +| **pluginSource** | _string_ (url) | _required_ if `pluginShortName` is not provided | The URL of the plugin. If this is not provided, `pluginShortName` must be provided. | +| **onError** | _Function_ | _optional_ | Callback function to be called when an error in the plugin triggers an error boundary. You can use this to pass an error back up to the app and create a custom handling/UX if errors occur in the plugin. In general, it is recommended that you use the plugin's built-in error boundaries | +| **showAlertsInPlugin** | _boolean_ | _optional_ | If `true`, any alerts within the plugin (defined with the `useAlert` hook) will be rendered within the iframe. By default, this is `false`. It is recommended, in general, that you do not override this and allow alerts to be hoisted up to the app level | + +## Plugin Props (custom props) + +You can specify pass any other props on the component and these will be passed down to the plugin (provided it was built with app-platform). When props are updated, they will be passed back down to the plugin. This mimics the behaviour of a normal React component, and hence you should provide stable references as needed to prevent rerendering. + +## Extended example + +See these links for an extended example of how component can be used within an [app](https://github.com/tomzemp/workingplugin/blob/plugin-wrapper-in-platform/src/App.js) and consumed within the [plugin](https://github.com/tomzemp/workingplugin/blob/plugin-wrapper-in-platform/src/Plugin.js). diff --git a/examples/cra/package.json b/examples/cra/package.json index 4553c9729..e7eec2caf 100644 --- a/examples/cra/package.json +++ b/examples/cra/package.json @@ -16,7 +16,8 @@ "@dhis2/app-service-alerts": "file:../../services/alerts", "@dhis2/app-service-config": "file:../../services/config", "@dhis2/app-service-data": "file:../../services/data", - "@dhis2/app-service-offline": "file:../../services/offline" + "@dhis2/app-service-offline": "file:../../services/offline", + "@dhis2/app-service-plugin": "file:../../services/plugin" }, "scripts": { "start": "react-scripts start", diff --git a/examples/cra/yarn.lock b/examples/cra/yarn.lock index 12bc2d9f0..7fe01a14c 100644 --- a/examples/cra/yarn.lock +++ b/examples/cra/yarn.lock @@ -1054,29 +1054,34 @@ integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg== "@dhis2/app-runtime@file:../../runtime": - version "3.6.1" + version "3.9.0" dependencies: - "@dhis2/app-service-alerts" "3.6.1" - "@dhis2/app-service-config" "3.6.1" - "@dhis2/app-service-data" "3.6.1" - "@dhis2/app-service-offline" "3.6.1" + "@dhis2/app-service-alerts" "3.9.0" + "@dhis2/app-service-config" "3.9.0" + "@dhis2/app-service-data" "3.9.0" + "@dhis2/app-service-offline" "3.9.0" -"@dhis2/app-service-alerts@3.6.1", "@dhis2/app-service-alerts@file:../../services/alerts": - version "3.6.1" +"@dhis2/app-service-alerts@3.9.0", "@dhis2/app-service-alerts@file:../../services/alerts": + version "3.9.0" -"@dhis2/app-service-config@3.6.1", "@dhis2/app-service-config@file:../../services/config": - version "3.6.1" +"@dhis2/app-service-config@3.9.0", "@dhis2/app-service-config@file:../../services/config": + version "3.9.0" -"@dhis2/app-service-data@3.6.1", "@dhis2/app-service-data@file:../../services/data": - version "3.6.1" +"@dhis2/app-service-data@3.9.0", "@dhis2/app-service-data@file:../../services/data": + version "3.9.0" dependencies: react-query "^3.13.11" -"@dhis2/app-service-offline@3.6.1", "@dhis2/app-service-offline@file:../../services/offline": - version "3.6.1" +"@dhis2/app-service-offline@3.9.0", "@dhis2/app-service-offline@file:../../services/offline": + version "3.9.0" dependencies: lodash "^4.17.21" +"@dhis2/app-service-plugin@file:../../services/plugin": + version "3.7.0" + dependencies: + post-robot "^10.0.46" + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -2259,6 +2264,15 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +belter@^1.0.41: + version "1.0.190" + resolved "https://registry.yarnpkg.com/belter/-/belter-1.0.190.tgz#491857550ef240d9c66b56fc637991f5c3089966" + integrity sha512-jz05FHrO+bwitdI6JxV5ESyRdVhTcwMWQ7L4o+q/R4LNJFQrG58sp9EiwsSjhbihhiyYFcmmCMRRagxte6igtw== + dependencies: + cross-domain-safe-weakmap "^1" + cross-domain-utils "^2" + zalgo-promise "^1" + big-integer@^1.6.16: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" @@ -3143,6 +3157,20 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-domain-safe-weakmap@^1, cross-domain-safe-weakmap@^1.0.1: + version "1.0.29" + resolved "https://registry.yarnpkg.com/cross-domain-safe-weakmap/-/cross-domain-safe-weakmap-1.0.29.tgz#0847975c27d9e1cc840f24c1745311958df98022" + integrity sha512-VLoUgf2SXnf3+na8NfeUFV59TRZkIJqCIATaMdbhccgtnTlSnHXkyTRwokngEGYdQXx8JbHT9GDYitgR2sdjuA== + dependencies: + cross-domain-utils "^2.0.0" + +cross-domain-utils@^2, cross-domain-utils@^2.0.0: + version "2.0.38" + resolved "https://registry.yarnpkg.com/cross-domain-utils/-/cross-domain-utils-2.0.38.tgz#2eaf321c4dfdb61596805ca4233fde4400cb6377" + integrity sha512-zZfi3+2EIR9l4chrEiXI2xFleyacsJf8YMLR1eJ0Veb5FTMXeJ3DpxDjZkto2FhL/g717WSELqbptNSo85UJDw== + dependencies: + zalgo-promise "^1.0.11" + cross-spawn@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" @@ -7718,6 +7746,17 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +post-robot@^10.0.46: + version "10.0.46" + resolved "https://registry.yarnpkg.com/post-robot/-/post-robot-10.0.46.tgz#39cea5b51033729390fc7c90be3285cd285f0377" + integrity sha512-EgVJiuvI4iRWDZvzObWes0X/n8olWBEJWxlSw79zmhpgkigX8UsVL4VOBhVtoJKwf0Y9qP9g2zOONw1rv80QbA== + dependencies: + belter "^1.0.41" + cross-domain-safe-weakmap "^1.0.1" + cross-domain-utils "^2.0.0" + universal-serialize "^1.0.4" + zalgo-promise "^1.0.3" + postcss-attribute-case-insensitive@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" @@ -10326,6 +10365,11 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +universal-serialize@^1.0.4: + version "1.0.10" + resolved "https://registry.yarnpkg.com/universal-serialize/-/universal-serialize-1.0.10.tgz#3279bb30f47290ea479f45135620f98fa9d3f3a6" + integrity sha512-FdouA4xSFa0fudk1+z5vLWtxZCoC0Q9lKYV3uUdFl7DttNfolmiw2ASr5ddY+/Yz6Isr68u3IqC9XMSwMP+Pow== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -11043,3 +11087,8 @@ yargs@^15.3.1: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^18.1.1" + +zalgo-promise@^1, zalgo-promise@^1.0.11, zalgo-promise@^1.0.3: + version "1.0.48" + resolved "https://registry.yarnpkg.com/zalgo-promise/-/zalgo-promise-1.0.48.tgz#9e33eef502d5ed9f5a09fc5728c833c3e87afa2e" + integrity sha512-LLHANmdm53+MucY9aOFIggzYtUdkSBFxUsy4glTTQYNyK6B3uCPWTbfiGvSrEvLojw0mSzyFJ1/RRLv+QMNdzQ== diff --git a/examples/query-playground/package.json b/examples/query-playground/package.json index ccfa94237..60a58f94b 100644 --- a/examples/query-playground/package.json +++ b/examples/query-playground/package.json @@ -29,6 +29,7 @@ "@dhis2/app-service-alerts": "file:../../services/alerts", "@dhis2/app-service-config": "file:../../services/config", "@dhis2/app-service-data": "file:../../services/data", - "@dhis2/app-service-offline": "file:../../services/offline" + "@dhis2/app-service-offline": "file:../../services/offline", + "@dhis2/app-service-plugin": "file:../../services/plugin" } } diff --git a/examples/query-playground/yarn.lock b/examples/query-playground/yarn.lock index 34974816f..c2a4509bb 100644 --- a/examples/query-playground/yarn.lock +++ b/examples/query-playground/yarn.lock @@ -1790,39 +1790,44 @@ moment "^2.24.0" "@dhis2/app-runtime@*": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.7.0.tgz#be738255ae95aaf597f9f59d1a56566b1a035dae" - integrity sha512-lKHgjaVVnCC5xxThckTVS0EH5mQlrSOdM8bC/7BSz5r9ztgLXJGUgxbnOvVbh95S/zFS9eyDvIls1HGum/MHTQ== + version "3.8.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.8.0.tgz#4ec7fc4ec6647dc8428e3c0d2e14b2d188a993b9" + integrity sha512-f5M1RfUJb4yZaPDywTfogVXjzWcuYGJ7JQzny6iWXrJu1+qrRKYbfFutYNhB+4bXD4bB59DelHWqVaHtrGvbVA== dependencies: - "@dhis2/app-service-alerts" "3.7.0" - "@dhis2/app-service-config" "3.7.0" - "@dhis2/app-service-data" "3.7.0" - "@dhis2/app-service-offline" "3.7.0" + "@dhis2/app-service-alerts" "3.8.0" + "@dhis2/app-service-config" "3.8.0" + "@dhis2/app-service-data" "3.8.0" + "@dhis2/app-service-offline" "3.8.0" "@dhis2/app-runtime@^2.2.2", "@dhis2/app-runtime@file:../../runtime": - version "3.6.1" + version "3.9.0" dependencies: - "@dhis2/app-service-alerts" "3.6.1" - "@dhis2/app-service-config" "3.6.1" - "@dhis2/app-service-data" "3.6.1" - "@dhis2/app-service-offline" "3.6.1" + "@dhis2/app-service-alerts" "3.9.0" + "@dhis2/app-service-config" "3.9.0" + "@dhis2/app-service-data" "3.9.0" + "@dhis2/app-service-offline" "3.9.0" -"@dhis2/app-service-alerts@3.6.1", "@dhis2/app-service-alerts@3.7.0", "@dhis2/app-service-alerts@file:../../services/alerts": - version "3.6.1" +"@dhis2/app-service-alerts@3.8.0", "@dhis2/app-service-alerts@3.9.0", "@dhis2/app-service-alerts@file:../../services/alerts": + version "3.9.0" -"@dhis2/app-service-config@3.6.1", "@dhis2/app-service-config@3.7.0", "@dhis2/app-service-config@file:../../services/config": - version "3.6.1" +"@dhis2/app-service-config@3.8.0", "@dhis2/app-service-config@3.9.0", "@dhis2/app-service-config@file:../../services/config": + version "3.9.0" -"@dhis2/app-service-data@3.6.1", "@dhis2/app-service-data@3.7.0", "@dhis2/app-service-data@file:../../services/data": - version "3.6.1" +"@dhis2/app-service-data@3.8.0", "@dhis2/app-service-data@3.9.0", "@dhis2/app-service-data@file:../../services/data": + version "3.9.0" dependencies: react-query "^3.13.11" -"@dhis2/app-service-offline@3.6.1", "@dhis2/app-service-offline@3.7.0", "@dhis2/app-service-offline@file:../../services/offline": - version "3.6.1" +"@dhis2/app-service-offline@3.8.0", "@dhis2/app-service-offline@3.9.0", "@dhis2/app-service-offline@file:../../services/offline": + version "3.9.0" dependencies: lodash "^4.17.21" +"@dhis2/app-service-plugin@file:../../services/plugin": + version "3.7.0" + dependencies: + post-robot "^10.0.46" + "@dhis2/app-shell@5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@dhis2/app-shell/-/app-shell-5.2.0.tgz#19fc3c6b18ea18048d3cdd1680ce535417edb6b3" @@ -3369,6 +3374,15 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +belter@^1.0.41: + version "1.0.190" + resolved "https://registry.yarnpkg.com/belter/-/belter-1.0.190.tgz#491857550ef240d9c66b56fc637991f5c3089966" + integrity sha512-jz05FHrO+bwitdI6JxV5ESyRdVhTcwMWQ7L4o+q/R4LNJFQrG58sp9EiwsSjhbihhiyYFcmmCMRRagxte6igtw== + dependencies: + cross-domain-safe-weakmap "^1" + cross-domain-utils "^2" + zalgo-promise "^1" + big-integer@^1.6.16: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" @@ -4432,6 +4446,20 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-domain-safe-weakmap@^1, cross-domain-safe-weakmap@^1.0.1: + version "1.0.29" + resolved "https://registry.yarnpkg.com/cross-domain-safe-weakmap/-/cross-domain-safe-weakmap-1.0.29.tgz#0847975c27d9e1cc840f24c1745311958df98022" + integrity sha512-VLoUgf2SXnf3+na8NfeUFV59TRZkIJqCIATaMdbhccgtnTlSnHXkyTRwokngEGYdQXx8JbHT9GDYitgR2sdjuA== + dependencies: + cross-domain-utils "^2.0.0" + +cross-domain-utils@^2, cross-domain-utils@^2.0.0: + version "2.0.38" + resolved "https://registry.yarnpkg.com/cross-domain-utils/-/cross-domain-utils-2.0.38.tgz#2eaf321c4dfdb61596805ca4233fde4400cb6377" + integrity sha512-zZfi3+2EIR9l4chrEiXI2xFleyacsJf8YMLR1eJ0Veb5FTMXeJ3DpxDjZkto2FhL/g717WSELqbptNSo85UJDw== + dependencies: + zalgo-promise "^1.0.11" + cross-spawn@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" @@ -9621,6 +9649,17 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +post-robot@^10.0.46: + version "10.0.46" + resolved "https://registry.yarnpkg.com/post-robot/-/post-robot-10.0.46.tgz#39cea5b51033729390fc7c90be3285cd285f0377" + integrity sha512-EgVJiuvI4iRWDZvzObWes0X/n8olWBEJWxlSw79zmhpgkigX8UsVL4VOBhVtoJKwf0Y9qP9g2zOONw1rv80QbA== + dependencies: + belter "^1.0.41" + cross-domain-safe-weakmap "^1.0.1" + cross-domain-utils "^2.0.0" + universal-serialize "^1.0.4" + zalgo-promise "^1.0.3" + postcss-attribute-case-insensitive@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" @@ -12675,6 +12714,11 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +universal-serialize@^1.0.4: + version "1.0.10" + resolved "https://registry.yarnpkg.com/universal-serialize/-/universal-serialize-1.0.10.tgz#3279bb30f47290ea479f45135620f98fa9d3f3a6" + integrity sha512-FdouA4xSFa0fudk1+z5vLWtxZCoC0Q9lKYV3uUdFl7DttNfolmiw2ASr5ddY+/Yz6Isr68u3IqC9XMSwMP+Pow== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -13509,6 +13553,11 @@ yargs@^15.0.0, yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.1" +zalgo-promise@^1, zalgo-promise@^1.0.11, zalgo-promise@^1.0.3: + version "1.0.48" + resolved "https://registry.yarnpkg.com/zalgo-promise/-/zalgo-promise-1.0.48.tgz#9e33eef502d5ed9f5a09fc5728c833c3e87afa2e" + integrity sha512-LLHANmdm53+MucY9aOFIggzYtUdkSBFxUsy4glTTQYNyK6B3uCPWTbfiGvSrEvLojw0mSzyFJ1/RRLv+QMNdzQ== + zip-stream@^2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b" diff --git a/runtime/src/Provider.tsx b/runtime/src/Provider.tsx index 19ad016d8..312af148c 100644 --- a/runtime/src/Provider.tsx +++ b/runtime/src/Provider.tsx @@ -9,14 +9,24 @@ type ProviderInput = { config: Config children: React.ReactNode offlineInterface?: any // temporary until offline service has types + plugin: boolean + parentAlertsAdd: any + showAlertsInPlugin: boolean } export const Provider = ({ config, children, offlineInterface, + plugin, + parentAlertsAdd, + showAlertsInPlugin, }: ProviderInput) => ( - + {children} diff --git a/runtime/src/index.ts b/runtime/src/index.ts index 5bf44ba37..3c4ab7cd4 100644 --- a/runtime/src/index.ts +++ b/runtime/src/index.ts @@ -23,4 +23,6 @@ export { clearSensitiveCaches, } from '@dhis2/app-service-offline' +export { Plugin } from '@dhis2/app-service-plugin' + export { Provider } from './Provider' diff --git a/services/alerts/src/AlertsManagerContext.ts b/services/alerts/src/AlertsManagerContext.ts index 0c3ed4d8b..9c3527fa9 100644 --- a/services/alerts/src/AlertsManagerContext.ts +++ b/services/alerts/src/AlertsManagerContext.ts @@ -9,6 +9,9 @@ const placeholder = () => { const defaultAlertsManager: AlertsManager = { add: placeholder, + plugin: false, + parentAlertsAdd: null, + showAlertsInPlugin: false, } export const AlertsManagerContext = diff --git a/services/alerts/src/AlertsProvider.tsx b/services/alerts/src/AlertsProvider.tsx index e59cbf766..fc8876ce4 100644 --- a/services/alerts/src/AlertsProvider.tsx +++ b/services/alerts/src/AlertsProvider.tsx @@ -5,17 +5,25 @@ import { makeAlertsManager } from './makeAlertsManager' import type { Alert, AlertsManager } from './types' export const AlertsProvider = ({ + plugin, + parentAlertsAdd, + showAlertsInPlugin, children, }: { + plugin: boolean + parentAlertsAdd: any + showAlertsInPlugin: boolean children: React.ReactNode }): ReactElement => { const [alerts, setAlerts] = useState([]) const [alertsManager] = useState(() => - makeAlertsManager(setAlerts) + makeAlertsManager(setAlerts, plugin) ) return ( - + {children} diff --git a/services/alerts/src/index.ts b/services/alerts/src/index.ts index f2e0367bd..f02489ab6 100644 --- a/services/alerts/src/index.ts +++ b/services/alerts/src/index.ts @@ -1,3 +1,4 @@ export { AlertsProvider } from './AlertsProvider' export { useAlerts } from './useAlerts' export { useAlert } from './useAlert' +export { AlertsManagerContext } from './AlertsManagerContext' diff --git a/services/alerts/src/makeAlertsManager.ts b/services/alerts/src/makeAlertsManager.ts index b97bf264e..6963d58db 100644 --- a/services/alerts/src/makeAlertsManager.ts +++ b/services/alerts/src/makeAlertsManager.ts @@ -4,7 +4,8 @@ const toVisibleAlertsArray = (alertsMap: AlertsMap) => Array.from(alertsMap.values()) export const makeAlertsManager = ( - setAlerts: React.Dispatch> + setAlerts: React.Dispatch>, + plugin: boolean ): AlertsManager => { const alertsMap: AlertsMap = new Map() let id = 0 @@ -29,5 +30,6 @@ export const makeAlertsManager = ( return { add, + plugin, } } diff --git a/services/alerts/src/types.ts b/services/alerts/src/types.ts index 73c563852..654a0560b 100644 --- a/services/alerts/src/types.ts +++ b/services/alerts/src/types.ts @@ -15,4 +15,7 @@ export type AlertsMap = Map export type AlertsManager = { add: (alert: Alert, alertRef: AlertRef) => Alert + plugin: boolean + parentAlertsAdd?: any + showAlertsInPlugin?: boolean } diff --git a/services/alerts/src/useAlert.ts b/services/alerts/src/useAlert.ts index 87454cace..adc84e211 100644 --- a/services/alerts/src/useAlert.ts +++ b/services/alerts/src/useAlert.ts @@ -6,7 +6,8 @@ export const useAlert = ( message: string | ((props: any) => string), options: AlertOptions | ((props: any) => AlertOptions) = {} ): { show: (props?: any) => void; hide: () => void } => { - const { add }: AlertsManager = useContext(AlertsManagerContext) + const { add, plugin, parentAlertsAdd, showAlertsInPlugin }: AlertsManager = + useContext(AlertsManagerContext) const alertRef = useRef(null) const show = useCallback( @@ -17,15 +18,25 @@ export const useAlert = ( const resolvedOptions = typeof options === 'function' ? options(props) : options - alertRef.current = add( - { - message: resolvedMessage, - options: resolvedOptions, - }, - alertRef - ) + if (plugin && parentAlertsAdd && !showAlertsInPlugin) { + alertRef.current = parentAlertsAdd( + { + message: resolvedMessage, + options: resolvedOptions, + }, + alertRef + ) + } else { + alertRef.current = add( + { + message: resolvedMessage, + options: resolvedOptions, + }, + alertRef + ) + } }, - [add, message, options] + [add, parentAlertsAdd, message, options, plugin, showAlertsInPlugin] ) const hide = useCallback(() => { diff --git a/services/plugin/.gitignore b/services/plugin/.gitignore new file mode 100644 index 000000000..6570aa5cb --- /dev/null +++ b/services/plugin/.gitignore @@ -0,0 +1,5 @@ +# DHIS2 Platform +node_modules +.d2 +src/locales +build \ No newline at end of file diff --git a/services/plugin/README.md b/services/plugin/README.md new file mode 100644 index 000000000..589924263 --- /dev/null +++ b/services/plugin/README.md @@ -0,0 +1,11 @@ +# DHIS2 App Data Service + +Application configuration for [DHIS2](https://dhis2.org) applications + +This library is intended for use with the [DHIS2 Application Platform](https://github.com/dhis2/app-platform). + +## Installation + +This package is internal to `@dhis2/app-runtime` and generally should not be installed independently. + +See [the docs](https://runtime.dhis2.nu) for more. diff --git a/services/plugin/d2.config.js b/services/plugin/d2.config.js new file mode 100644 index 000000000..84bec20f1 --- /dev/null +++ b/services/plugin/d2.config.js @@ -0,0 +1,9 @@ +const config = { + type: 'lib', + + entryPoints: { + lib: './src/index.ts', + }, +} + +module.exports = config diff --git a/services/plugin/jest.config.js b/services/plugin/jest.config.js new file mode 100644 index 000000000..66cd80913 --- /dev/null +++ b/services/plugin/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + collectCoverageFrom: [ + 'src/**/*.(js|jsx|ts|tsx)', + '!src/index.ts', + '!src/types.ts', + ], + + // Setup react-testing-library + setupFilesAfterEnv: ['/src/setupRTL.ts'], +} diff --git a/services/plugin/package.json b/services/plugin/package.json new file mode 100644 index 000000000..f60aedb70 --- /dev/null +++ b/services/plugin/package.json @@ -0,0 +1,48 @@ +{ + "name": "@dhis2/app-service-plugin", + "version": "3.7.0", + "main": "./build/cjs/index.js", + "module": "./build/es/index.js", + "types": "build/types/index.d.ts", + "exports": { + "import": "./build/es/index.js", + "require": "./build/cjs/index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/dhis2/app-runtime.git", + "directory": "services/plugin" + }, + "author": "Austin McGee ", + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + }, + "files": [ + "build/**" + ], + "dependencies": { + "post-robot": "^10.0.46" + }, + "peerDependencies": { + "prop-types": "^15.7.2", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "@dhis2/app-service-alerts": "3.7.0", + "@dhis2/app-service-data": "3.7.0" + }, + "devDependencies": { + "@types/post-robot": "^10.0.3" + }, + "scripts": { + "clean": "rimraf ./build/*", + "build:types": "tsc --emitDeclarationOnly --outDir ./build/types", + "build:package": "d2-app-scripts build", + "build": "concurrently -n build,types \"yarn build:package\" \"yarn build:types\"", + "watch": "NODE_ENV=development concurrently -n build,types \"yarn build:package --watch\" \"yarn build:types --watch\"", + "type-check": "tsc --noEmit --allowJs --checkJs", + "type-check:watch": "yarn type-check --watch", + "test": "d2-app-scripts test", + "coverage": "yarn test --coverage" + } +} diff --git a/services/plugin/src/Plugin.tsx b/services/plugin/src/Plugin.tsx new file mode 100644 index 000000000..3fb2f149e --- /dev/null +++ b/services/plugin/src/Plugin.tsx @@ -0,0 +1,120 @@ +import { AlertsManagerContext } from '@dhis2/app-service-alerts' +import { useDataQuery } from '@dhis2/app-service-data' +import postRobot from 'post-robot' +import React, { useContext, useEffect, useRef, useState } from 'react' +import PluginError from './PluginError' + +const appsInfoQuery = { + apps: { + resource: 'apps', + }, +} + +// sample logic subject to change depending on actual endpoint details +const getPluginEntryPoint = ({ + apps, + appShortName, +}: { + apps: any + appShortName?: string +}): string => { + return apps.find( + ({ short_name }: { short_name: string }) => + short_name && short_name === appShortName + )?.pluginLaunchUrl +} + +export const Plugin = ({ + pluginSource, + pluginShortName, + ...propsToPass +}: { + pluginSource?: string + pluginShortName?: string + propsToPass: any +}): JSX.Element => { + const iframeRef = useRef(null) + + const { add: alertsAdd } = useContext(AlertsManagerContext) + + const { data } = useDataQuery(appsInfoQuery) + const pluginEntryPoint = + pluginSource ?? + getPluginEntryPoint({ + apps: data?.apps || [], + appShortName: pluginShortName, + }) + + const [communicationReceived, setCommunicationReceived] = + useState(false) + + const [inErrorState, setInErrorState] = useState(false) + + useEffect(() => { + if (iframeRef?.current) { + const iframeProps = { + ...propsToPass, + alertsAdd, + setInErrorState, + setCommunicationReceived, + } + + // if iframe has not sent initial request, set up a listener + if (!communicationReceived && !inErrorState) { + const listener = postRobot.on( + 'getPropsFromParent', + // listen for messages coming only from the iframe rendered by this component + { window: iframeRef.current.contentWindow }, + (): any => { + setCommunicationReceived(true) + return iframeProps + } + ) + return () => listener.cancel() + } + + // if iframe has sent initial request, send new props + if ( + communicationReceived && + iframeRef.current.contentWindow && + !inErrorState + ) { + postRobot + .send( + iframeRef.current.contentWindow, + 'updated', + iframeProps + ) + .catch((err) => { + // log postRobot errors, but do not bubble them up + console.error(err) + }) + } + } + }, [propsToPass, communicationReceived, inErrorState, alertsAdd]) + + if (data && !pluginEntryPoint) { + return ( + + ) + } + + if (pluginEntryPoint) { + return ( + + ) + } + + return <> +} diff --git a/services/plugin/src/PluginError.tsx b/services/plugin/src/PluginError.tsx new file mode 100644 index 000000000..2883f88a5 --- /dev/null +++ b/services/plugin/src/PluginError.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +// PLACEHOLDER plugin error component (e.g. for dealing with missing/inaccessible plugin) +// note that d2-i18n does not work with typescript projects, so we cannot currently translate + +const PluginError = ({ + missingEntryPoint, + appShortName, +}: { + missingEntryPoint: boolean + appShortName?: string +}) => ( + <> +

Plugin unavailable

+ {missingEntryPoint ? ( + <> +

{`You do not have access to the requested plugin ${appShortName}, or it is not installed`}

+ + ) : null} + +) + +export default PluginError diff --git a/services/plugin/src/__tests__/integration.test.tsx b/services/plugin/src/__tests__/integration.test.tsx new file mode 100644 index 000000000..b5270713f --- /dev/null +++ b/services/plugin/src/__tests__/integration.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' +import PluginError from '../PluginError' + +// empty tests (to no trigger test failure) +describe('', () => { + it('should render without failing', async () => { + const missingEntryPoint = false + const appShortName = 'some_app' + const wrapper = () => ( + + ) + }) +}) diff --git a/services/plugin/src/index.ts b/services/plugin/src/index.ts new file mode 100644 index 000000000..2321646ed --- /dev/null +++ b/services/plugin/src/index.ts @@ -0,0 +1 @@ +export { Plugin } from './Plugin' diff --git a/services/plugin/src/setupRTL.ts b/services/plugin/src/setupRTL.ts new file mode 100644 index 000000000..0deaa77ec --- /dev/null +++ b/services/plugin/src/setupRTL.ts @@ -0,0 +1,5 @@ +import '@testing-library/jest-dom/extend-expect' + +process.on('unhandledRejection', (err) => { + throw err +}) diff --git a/services/plugin/src/types.ts b/services/plugin/src/types.ts new file mode 100644 index 000000000..f00b281de --- /dev/null +++ b/services/plugin/src/types.ts @@ -0,0 +1,3 @@ +import { ReactNode } from 'react' + +// file is a placeholder to allow build diff --git a/services/plugin/tsconfig.json b/services/plugin/tsconfig.json new file mode 100644 index 000000000..3e229a39d --- /dev/null +++ b/services/plugin/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "exclude": [ + "src/setupRTL.ts", + "src/__tests__", + "**/*.test.ts", + "**/*.test.tsx" + ] +} diff --git a/yarn.lock b/yarn.lock index 47ed2bb2d..812c0e6c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3961,6 +3961,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/post-robot@^10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@types/post-robot/-/post-robot-10.0.3.tgz#d1429085f2faf4c87f841dab4e51472457edbf31" + integrity sha512-y8ysuxddaG8V/oA1Ay6Err7nSADRa9Bv1rl0ZQpJ0qgdIQ7ks3CHcOsYL4qE8w75+/XYDS94dBeXDs0xexm3tA== + "@types/prettier@^2.1.5": version "2.7.2" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" @@ -5267,6 +5272,15 @@ bcryptjs@^2.3.0: resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= +belter@^1.0.41: + version "1.0.190" + resolved "https://registry.yarnpkg.com/belter/-/belter-1.0.190.tgz#491857550ef240d9c66b56fc637991f5c3089966" + integrity sha512-jz05FHrO+bwitdI6JxV5ESyRdVhTcwMWQ7L4o+q/R4LNJFQrG58sp9EiwsSjhbihhiyYFcmmCMRRagxte6igtw== + dependencies: + cross-domain-safe-weakmap "^1" + cross-domain-utils "^2" + zalgo-promise "^1" + bfj@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" @@ -6263,6 +6277,20 @@ crc@^3.4.4: dependencies: buffer "^5.1.0" +cross-domain-safe-weakmap@^1, cross-domain-safe-weakmap@^1.0.1: + version "1.0.29" + resolved "https://registry.yarnpkg.com/cross-domain-safe-weakmap/-/cross-domain-safe-weakmap-1.0.29.tgz#0847975c27d9e1cc840f24c1745311958df98022" + integrity sha512-VLoUgf2SXnf3+na8NfeUFV59TRZkIJqCIATaMdbhccgtnTlSnHXkyTRwokngEGYdQXx8JbHT9GDYitgR2sdjuA== + dependencies: + cross-domain-utils "^2.0.0" + +cross-domain-utils@^2, cross-domain-utils@^2.0.0: + version "2.0.38" + resolved "https://registry.yarnpkg.com/cross-domain-utils/-/cross-domain-utils-2.0.38.tgz#2eaf321c4dfdb61596805ca4233fde4400cb6377" + integrity sha512-zZfi3+2EIR9l4chrEiXI2xFleyacsJf8YMLR1eJ0Veb5FTMXeJ3DpxDjZkto2FhL/g717WSELqbptNSo85UJDw== + dependencies: + zalgo-promise "^1.0.11" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -11926,6 +11954,17 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +post-robot@^10.0.46: + version "10.0.46" + resolved "https://registry.yarnpkg.com/post-robot/-/post-robot-10.0.46.tgz#39cea5b51033729390fc7c90be3285cd285f0377" + integrity sha512-EgVJiuvI4iRWDZvzObWes0X/n8olWBEJWxlSw79zmhpgkigX8UsVL4VOBhVtoJKwf0Y9qP9g2zOONw1rv80QbA== + dependencies: + belter "^1.0.41" + cross-domain-safe-weakmap "^1.0.1" + cross-domain-utils "^2.0.0" + universal-serialize "^1.0.4" + zalgo-promise "^1.0.3" + postcss-attribute-case-insensitive@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" @@ -15251,6 +15290,11 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +universal-serialize@^1.0.4: + version "1.0.10" + resolved "https://registry.yarnpkg.com/universal-serialize/-/universal-serialize-1.0.10.tgz#3279bb30f47290ea479f45135620f98fa9d3f3a6" + integrity sha512-FdouA4xSFa0fudk1+z5vLWtxZCoC0Q9lKYV3uUdFl7DttNfolmiw2ASr5ddY+/Yz6Isr68u3IqC9XMSwMP+Pow== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -16119,6 +16163,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zalgo-promise@^1, zalgo-promise@^1.0.11, zalgo-promise@^1.0.3: + version "1.0.48" + resolved "https://registry.yarnpkg.com/zalgo-promise/-/zalgo-promise-1.0.48.tgz#9e33eef502d5ed9f5a09fc5728c833c3e87afa2e" + integrity sha512-LLHANmdm53+MucY9aOFIggzYtUdkSBFxUsy4glTTQYNyK6B3uCPWTbfiGvSrEvLojw0mSzyFJ1/RRLv+QMNdzQ== + zip-stream@^2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b"