From 7f3eb1736038a4743b987b683115da12a7165d6d Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 3 Mar 2023 17:09:41 +0100 Subject: [PATCH] refactor: iframe plugin support (#2110) * chore: convert to functional component * feat: enable EVENT_VISUALIZATION dashboard item types * feat: allow iframe plugin overrides for dev/test * feat: render LL plugin in iframe * chore: add dependency for post-robot * chore: update pot file * fix: lookup in installed apps for the plugin URL to use in the iframe * chore: remove TODO comment * chore: update pot file * chore: add handy deduplicate yarn command * fix: fix ui dependency * chore: run prettier * refactor: rewrite condition for better readability * fix: remove Suspense which might interfere with the iframe * fix: ensure the selector content fits in the container This is a ui issue: https://github.com/dhis2/ui/pull/1152. * refactor: use Layer + Popper to avoid bottom double border * fix: remove redundant overflow rule, fixed in ui This reverts commit a36ab37ae0d5aed0495d0fc365fe4da5236594d4. * chore: deduplicate yarn.lock * refactor: preserve height/width if provided, update props passed * refactor: use iframe for DV plugin * chore: remove unused import * feat: add recording props to plugin (#2125) * fix(pwa): make cache filters more specifc * feat(plugins): add `record` prop to iframe plugins when caching dashboard * feat: remove cached plugin data with dashboard * chore: run Prettier * test: fix failing tests * chore: remove unused code * chore: run Prettier * fix: pass correct prop for display property Also do not pass the whole userSettings object, not used in the plugins where the only thing really needed is the displayProperty for the analytics request. * chore: deduplicate yarn.lock * fix: add more paths to omit from cache Replaced deprecated config key with new one. * fix: show error when iframe src is not available Also avoid adding postRobot handlers when the iframeRef is not available, this removes some extra errors. * fix: do not pass filtered AO to LL plugin Filters don't work in LL without modifications. For now just disable filtering for LL items. * fix: set error when pluginLaunchUrl is not available The app could be installed and the entry in apps api returned, but the plugin launch URL can be missing, in that case the plugin cannot be loaded in the iframe. * refactor: use specific reducer selector * fix: remove View As in context menu for LL items * fix: clear selected from Redux when editing a new dashboard * feat: add tags on item header, used for filters in LL For LL items we don't apply filters. Show a tag in the item header to indicate this to the user. * feat: add overlay on LL items to inform about not applied filters * refactor: move the overlay handling to the parent component In this way both the tag in the item header and the overlay in the item content can be controlled at the same time allowing for toggling the tag depending on whether the overlay is shown or not. * test: update snapshots * fix: remove tag icon and fix styles Approved by Joe. * chore: update pot file * fix: remove context menu when plugin is not installed * fetch schema for eventVisualizations, required by the interpretations component * fix: rename Event visualizations to Line lists This appears as section title in the dashboard item search. * feat: add custom message for when the plugin is not installed More friendly than the generic error message. In this case there isn't any problem with the dashboard item, but the plugin for rendering the item is not installed. We inform the user about it and suggest to install the missing app from the app hub. * refactor: propagate d2 needed to check installed apps * feat: use iframe for Map items * chore: update d2-ui-interpretations component * fix: update iframe src and props when using View As * chore: fix linting issue * fix: fix re-render of visualizations when interpretations panel toggles (#2199) * fix: add app id for DV when fetched from installed apps * fix: detect plugin availability via plugin launch URL * fix: add fallback to bundled plugin path for core apps This addresses the case of core apps plugins which are always served from the bundled app's path, instead of from api/apps. For core (bundled) apps: * first we look at the api/apps, if there is an entry there for the app it means it has been manually installed from the app hub overriding the bundled version. The plugin URL in this case is not in the format api/apps but dhis-web-*. * if no entry is returned from api/apps we use the "hardcoded" path (as before) thus using the bundled version of the plugin. This should work for situations where a new version of a core app is installed or uninstalled from the app hub. * chore: remove accidentally committed temp files * fix: use base url without api version This should solve the issue of bundled apps plugin not working in iframes. * fix: deduplicate yarn.lock * fix: attempt at fixing the sizing issue in DV iframe content * feat: map plugin (#2229) --------- Co-authored-by: Kai Vandivier <49666798+KaiVandivier@users.noreply.github.com> Co-authored-by: Jen Jones Arnesen --- d2.config.js | 17 +- i18n/en.pot | 35 ++- package.json | 10 +- src/AppWrapper.js | 1 + src/api/metadata.js | 3 + .../__tests__/__snapshots__/Item.spec.js.snap | 29 ++- src/components/Item/Item.js | 7 +- src/components/Item/ItemHeader/ItemHeader.js | 9 +- .../Item/ItemHeader/ViewItemTags.js | 12 + .../ItemHeader/styles/ItemHeader.module.css | 7 +- src/components/Item/VisualizationItem/Item.js | 36 ++- .../ItemContextMenu/ItemContextMenu.js | 2 + .../ItemContextMenu/ViewAsMenuItems.js | 7 +- .../Visualization/DataVisualizerPlugin.js | 72 ----- .../Visualization/DefaultPlugin.js | 2 + .../Visualization/IframePlugin.js | 201 ++++++++++++++ .../Visualization/MapPlugin.js | 104 +------- .../Visualization/MissingPluginMessage.js | 41 +++ .../Visualization/Visualization.js | 245 ++++++++++-------- .../VisualizationErrorMessage.js | 16 +- .../__tests__/Visualization.spec.js | 50 ++-- .../__snapshots__/Visualization.spec.js.snap | 176 +++++++++++-- .../Visualization/getFilteredVisualization.js | 27 +- .../VisualizationItem/Visualization/plugin.js | 46 +++- ...gin.module.css => IframePlugin.module.css} | 0 .../styles/Visualization.module.css | 12 + .../__tests__/__snapshots__/Item.spec.js.snap | 18 +- src/components/styles/ItemGrid.css | 6 +- src/modules/itemTypes.js | 15 ++ src/modules/localStorage.js | 5 + src/pages/edit/ItemSelector/ItemSelector.js | 32 +-- .../edit/ItemSelector/selectableItems.js | 2 + src/pages/edit/NewDashboard.js | 13 +- yarn.lock | 96 +++++-- 34 files changed, 930 insertions(+), 424 deletions(-) create mode 100644 src/components/Item/ItemHeader/ViewItemTags.js delete mode 100644 src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js create mode 100644 src/components/Item/VisualizationItem/Visualization/IframePlugin.js create mode 100644 src/components/Item/VisualizationItem/Visualization/MissingPluginMessage.js rename src/components/Item/VisualizationItem/Visualization/styles/{DataVisualizerPlugin.module.css => IframePlugin.module.css} (100%) create mode 100644 src/components/Item/VisualizationItem/Visualization/styles/Visualization.module.css diff --git a/d2.config.js b/d2.config.js index 863a4c6ca..1b938e8aa 100644 --- a/d2.config.js +++ b/d2.config.js @@ -7,12 +7,17 @@ const config = { pwa: { enabled: true, caching: { - patternsToOmit: [ - 'dashboards/[a-zA-Z0-9]*', - 'visualizations', - 'analytics', - 'geoFeatures', - 'cartodb-basemaps-a.global.ssl.fastly.net', + patternsToOmitFromAppShell: [ + // Make these specific as possible -- + // unspecific ones have caused errors on instances with + // 'analytics' in the name, for example + /\/api\/(\d+\/)?dashboards\/[a-zA-Z0-9]*/, + /\/api\/(\d+\/)?maps/, + /\/api\/(\d+\/)?eventVisualizations/, + /\/api\/(\d+\/)?visualizations/, + /\/api\/(\d+\/)?analytics/, + /\/api\/(\d+\/)?geoFeatures/, + /cartodb-basemaps-[a-c]\.global\.ssl\.fastly\.net/, ], }, }, diff --git a/i18n/en.pot b/i18n/en.pot index a0bd998ab..a60ab6e98 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-02-10T10:21:06.817Z\n" -"PO-Revision-Date: 2023-02-10T10:21:06.817Z\n" +"POT-Creation-Date: 2023-02-14T12:38:49.529Z\n" +"PO-Revision-Date: 2023-02-14T12:38:49.529Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" @@ -62,6 +62,12 @@ msgstr "Text item" msgid "Add text here" msgstr "Add text here" +msgid "Filters are not applied to line list dashboard items" +msgstr "Filters are not applied to line list dashboard items" + +msgid "Filters not applied" +msgstr "Filters not applied" + msgid "There was a problem loading this dashboard item" msgstr "There was a problem loading this dashboard item" @@ -98,12 +104,30 @@ msgstr "There was a problem loading interpretations for this item" msgid "Maps with Earth Engine layers cannot be displayed when offline" msgstr "Maps with Earth Engine layers cannot be displayed when offline" -msgid "Unable to load the plugin for this item" -msgstr "Unable to load the plugin for this item" +msgid "The plugin for rendering this item is not available" +msgstr "The plugin for rendering this item is not available" + +msgid "Install the {{appName}} app from the App Hub" +msgstr "Install the {{appName}} app from the App Hub" + +msgid "The plugin for rendering this item is not available" +msgstr "The plugin for rendering this item is not available" + +msgid "Install the {{appName}} app from the App Hub" +msgstr "Install the {{appName}} app from the App Hub" msgid "No data to display" msgstr "No data to display" +msgid "Filters are not applied to line list dashboard items." +msgstr "Filters are not applied to line list dashboard items." + +msgid "Show without filters" +msgstr "Show without filters" + +msgid "Unable to load the plugin for this item" +msgstr "Unable to load the plugin for this item" + msgid "There was an error loading data for this item" msgstr "There was an error loading data for this item" @@ -131,6 +155,9 @@ msgstr "Event reports" msgid "Event charts" msgstr "Event charts" +msgid "Line lists" +msgstr "Line lists" + msgid "Apps" msgstr "Apps" diff --git a/package.json b/package.json index 536b398c2..e2a7fcfab 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,13 @@ "@dhis2/app-runtime": "^3.7.0", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/d2-i18n": "^1.1.1", - "@dhis2/d2-ui-core": "^7.3.3", - "@dhis2/d2-ui-interpretations": "^7.4.0", - "@dhis2/d2-ui-mentions-wrapper": "^7.3.3", - "@dhis2/d2-ui-rich-text": "^7.3.3", + "@dhis2/d2-ui-core": "^7.4.1", + "@dhis2/d2-ui-interpretations": "^7.4.1", + "@dhis2/d2-ui-mentions-wrapper": "^7.4.1", + "@dhis2/d2-ui-rich-text": "^7.4.1", "@dhis2/data-visualizer-plugin": "^39.2.10", "@dhis2/ui": "^8.11.2", + "@krakenjs/post-robot": "^11.0.0", "classnames": "^2.3.2", "d2": "^31.10.0", "d2-utilizr": "^0.2.16", @@ -33,6 +34,7 @@ "styled-jsx": "^4.0.1" }, "scripts": { + "deduplicate": "d2-app-scripts deduplicate", "start": "d2-app-scripts start", "coverage": "npm test -- --coverage", "lint": "d2-style check", diff --git a/src/AppWrapper.js b/src/AppWrapper.js index a944767c2..450e3786d 100644 --- a/src/AppWrapper.js +++ b/src/AppWrapper.js @@ -18,6 +18,7 @@ const d2Config = { 'report', 'eventChart', 'eventReport', + 'eventVisualization', 'dashboard', 'organisationUnit', 'userGroup', diff --git a/src/api/metadata.js b/src/api/metadata.js index 65f11a551..4f524a107 100644 --- a/src/api/metadata.js +++ b/src/api/metadata.js @@ -73,6 +73,9 @@ export const getFavoritesFields = () => [ `map[${getFavoriteFields({ withDimensions: false }).join(',')}]`, `eventReport[${getFavoriteFields({ withDimensions: false }).join(',')}]`, `eventChart[${getFavoriteFields({ withDimensions: false }).join(',')}]`, + `eventVisualization[${getFavoriteFields({ withDimensions: false }).join( + ',' + )}]`, ] // List item diff --git a/src/components/Item/AppItem/__tests__/__snapshots__/Item.spec.js.snap b/src/components/Item/AppItem/__tests__/__snapshots__/Item.spec.js.snap index e1ea7abb4..18ef8d42c 100644 --- a/src/components/Item/AppItem/__tests__/__snapshots__/Item.spec.js.snap +++ b/src/components/Item/AppItem/__tests__/__snapshots__/Item.spec.js.snap @@ -10,6 +10,9 @@ exports[`renders a valid App item in view mode 1`] = ` > Scorecard

+
+ class="itemActionsWrap" + > +
+
Scorecard

+
+ class="itemActionsWrap" + > +
+
unknownApp app not found

+
{ case MAP: case EVENT_CHART: case EVENT_REPORT: + case EVENT_VISUALIZATION: return VisualizationItem case MESSAGES: return MessagesItem @@ -58,9 +61,11 @@ const getGridItem = (type) => { } export const Item = (props) => { + const { d2 } = useD2() + const GridItem = getGridItem(props.item.type) - return + return } Item.propTypes = { diff --git a/src/components/Item/ItemHeader/ItemHeader.js b/src/components/Item/ItemHeader/ItemHeader.js index d3b8b5cf9..76e27201d 100644 --- a/src/components/Item/ItemHeader/ItemHeader.js +++ b/src/components/Item/ItemHeader/ItemHeader.js @@ -6,6 +6,7 @@ import EditItemActions from './EditItemActions.js' import PrintItemInfo from './PrintItemInfo.js' import classes from './styles/ItemHeader.module.css' import ViewItemActions from './ViewItemActions.js' +import ViewItemTags from './ViewItemTags.js' const getItemActionsMap = (isShortened) => { return { @@ -16,12 +17,15 @@ const getItemActionsMap = (isShortened) => { } const ItemHeader = React.forwardRef( - ({ dashboardMode, title, isShortened, ...rest }, ref) => { + ({ dashboardMode, title, isShortened, tags, ...rest }, ref) => { const Actions = getItemActionsMap(isShortened)[dashboardMode] return (

{title}

- {Actions ? : null} +
+ {tags ? : null} + {Actions ? : null} +
) } @@ -32,6 +36,7 @@ ItemHeader.displayName = 'ItemHeader' ItemHeader.propTypes = { dashboardMode: PropTypes.string, isShortened: PropTypes.bool, + tags: PropTypes.node, title: PropTypes.string, } diff --git a/src/components/Item/ItemHeader/ViewItemTags.js b/src/components/Item/ItemHeader/ViewItemTags.js new file mode 100644 index 000000000..e1477faf1 --- /dev/null +++ b/src/components/Item/ItemHeader/ViewItemTags.js @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types' +import React from 'react' +import classes from './styles/ItemHeader.module.css' + +const ViewItemTags = ({ tags }) => + tags ?
{tags}
: null + +ViewItemTags.propTypes = { + tags: PropTypes.node, +} + +export default ViewItemTags diff --git a/src/components/Item/ItemHeader/styles/ItemHeader.module.css b/src/components/Item/ItemHeader/styles/ItemHeader.module.css index 2124600ae..658fd4a2d 100644 --- a/src/components/Item/ItemHeader/styles/ItemHeader.module.css +++ b/src/components/Item/ItemHeader/styles/ItemHeader.module.css @@ -13,10 +13,13 @@ align-self: center; } -.itemActionsWrap { - margin-left: auto; +.itemHeaderRightWrap { + display: flex; flex-shrink: 0; + align-items: center; + margin-left: auto; } + .itemActionsWrap button { margin-left: var(--spacers-dp8); } diff --git a/src/components/Item/VisualizationItem/Item.js b/src/components/Item/VisualizationItem/Item.js index 9ba60efbb..00aec655d 100644 --- a/src/components/Item/VisualizationItem/Item.js +++ b/src/components/Item/VisualizationItem/Item.js @@ -1,4 +1,5 @@ import i18n from '@dhis2/d2-i18n' +import { Tag, Tooltip } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { Component } from 'react' import { connect } from 'react-redux' @@ -19,6 +20,7 @@ import { import { getDataStatisticsName, getItemTypeForVis, + EVENT_VISUALIZATION, } from '../../../modules/itemTypes.js' import { sGetIsEditing } from '../../../reducers/editDashboard.js' import { sGetItemActiveType } from '../../../reducers/itemActiveTypes.js' @@ -44,6 +46,7 @@ class Item extends Component { showFooter: false, configLoaded: false, loadItemFailed: false, + showNoFiltersOverlay: this.props.item?.type === EVENT_VISUALIZATION, } constructor(props) { @@ -105,6 +108,9 @@ class Item extends Component { return !!(el?.requestFullscreen || el?.webkitRequestFullscreen) } + onClickNoFiltersOverlay = () => + this.setState({ showNoFiltersOverlay: false }) + onToggleFullscreen = () => { if (!isElementFullscreen(this.props.item.id)) { const el = getGridItemElement(this.props.item.id) @@ -177,11 +183,12 @@ class Item extends Component { render() { const { item, dashboardMode, itemFilters } = this.props - const { showFooter } = this.state + const { showFooter, showNoFiltersOverlay } = this.state + const originalType = getItemTypeForVis(item) const activeType = this.getActiveType() const actionButtons = - pluginIsAvailable(activeType || item.type) && + pluginIsAvailable(activeType || item.type, this.props.d2) && isViewMode(dashboardMode) ? ( ) : null + const tags = + isViewMode(dashboardMode) && + Object.keys(itemFilters).length && + !showNoFiltersOverlay && + activeType === EVENT_VISUALIZATION ? ( + + {i18n.t('Filters not applied')} + + ) : null + return ( <> ( )} @@ -245,6 +276,7 @@ class Item extends Component { Item.propTypes = { activeType: PropTypes.string, + d2: PropTypes.object, dashboardMode: PropTypes.string, gridWidth: PropTypes.number, isEditing: PropTypes.bool, diff --git a/src/components/Item/VisualizationItem/ItemContextMenu/ItemContextMenu.js b/src/components/Item/VisualizationItem/ItemContextMenu/ItemContextMenu.js index dbc03443d..44372943f 100644 --- a/src/components/Item/VisualizationItem/ItemContextMenu/ItemContextMenu.js +++ b/src/components/Item/VisualizationItem/ItemContextMenu/ItemContextMenu.js @@ -25,6 +25,7 @@ import { getAppName, itemTypeMap, getItemTypeForVis, + EVENT_VISUALIZATION, } from '../../../../modules/itemTypes.js' import { isSmallScreen } from '../../../../modules/smallScreen.js' import MenuItem from '../../../MenuItemWithTooltip.js' @@ -85,6 +86,7 @@ const ItemContextMenu = (props) => { allowVisViewAs && !isSingleValue(type) && !isYearOverYear(type) && + item.type !== EVENT_VISUALIZATION && type !== VIS_TYPE_GAUGE && type !== VIS_TYPE_PIE diff --git a/src/components/Item/VisualizationItem/ItemContextMenu/ViewAsMenuItems.js b/src/components/Item/VisualizationItem/ItemContextMenu/ViewAsMenuItems.js index c8494be52..8e13c190f 100644 --- a/src/components/Item/VisualizationItem/ItemContextMenu/ViewAsMenuItems.js +++ b/src/components/Item/VisualizationItem/ItemContextMenu/ViewAsMenuItems.js @@ -8,6 +8,7 @@ import { REPORT_TABLE, EVENT_CHART, EVENT_REPORT, + EVENT_VISUALIZATION, isTrackerDomainType, hasMapView, } from '../../../../modules/itemTypes.js' @@ -34,7 +35,7 @@ const ViewAsMenuItems = ({ return ( <> - {activeType !== CHART && activeType !== EVENT_CHART && ( + {![CHART, EVENT_CHART].includes(activeType) && ( } /> )} - {activeType !== REPORT_TABLE && activeType !== EVENT_REPORT && ( + {![REPORT_TABLE, EVENT_REPORT, EVENT_VISUALIZATION].includes( + activeType + ) && ( - import( - /* webpackChunkName: "data-visualizer-plugin" */ /* webpackPrefetch: true */ '@dhis2/data-visualizer-plugin' - ) -) - -const DataVisualizerPlugin = ({ - filterVersion, - item, - style, - visualization, - dashboardMode, -}) => { - const { userSettings } = useUserSettings() - const [visualizationLoaded, setVisualizationLoaded] = useState(false) - const [error, setError] = useState(false) - - const onLoadingComplete = useCallback( - () => setVisualizationLoaded(true), - [] - ) - - const onError = () => setError(true) - - useEffect(() => { - setError(false) - }, [filterVersion, visualization.type]) - - if (error) { - return ( -
- -
- ) - } - - return ( - }> - {!visualizationLoaded && } -
- -
-
- ) -} - -DataVisualizerPlugin.propTypes = { - dashboardMode: PropTypes.string, - filterVersion: PropTypes.string, - item: PropTypes.object, - style: PropTypes.object, - visualization: PropTypes.object, -} - -export default DataVisualizerPlugin diff --git a/src/components/Item/VisualizationItem/Visualization/DefaultPlugin.js b/src/components/Item/VisualizationItem/Visualization/DefaultPlugin.js index a717ddeec..3d39eca36 100644 --- a/src/components/Item/VisualizationItem/Visualization/DefaultPlugin.js +++ b/src/components/Item/VisualizationItem/Visualization/DefaultPlugin.js @@ -30,6 +30,7 @@ const DefaultPlugin = ({ load(item, visualization, { credentials, activeType, + d2, options, }) @@ -56,6 +57,7 @@ const DefaultPlugin = ({ load(item, visualization, { credentials, activeType, + d2, options, }) } diff --git a/src/components/Item/VisualizationItem/Visualization/IframePlugin.js b/src/components/Item/VisualizationItem/Visualization/IframePlugin.js new file mode 100644 index 000000000..a0c0d7719 --- /dev/null +++ b/src/components/Item/VisualizationItem/Visualization/IframePlugin.js @@ -0,0 +1,201 @@ +import { useD2 } from '@dhis2/app-runtime-adapter-d2' +import postRobot from '@krakenjs/post-robot' +import PropTypes from 'prop-types' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + CHART, + REPORT_TABLE, + VISUALIZATION, +} from '../../../../modules/itemTypes.js' +import { getPluginOverrides } from '../../../../modules/localStorage.js' +import { useCacheableSection } from '../../../../modules/useCacheableSection.js' +import { useUserSettings } from '../../../UserSettingsProvider.js' +import MissingPluginMessage from './MissingPluginMessage.js' +import { getPluginLaunchUrl } from './plugin.js' +import classes from './styles/IframePlugin.module.css' +import VisualizationErrorMessage from './VisualizationErrorMessage.js' + +const IframePlugin = ({ + activeType, + filterVersion, + style, + visualization, + dashboardMode, + dashboardId, + itemId, + itemType, +}) => { + const { d2 } = useD2() + + const { userSettings } = useUserSettings() + const iframeRef = useRef() + const [error, setError] = useState(null) + + // When this mounts, check if the dashboard is recording + const { isCached, recordingState } = useCacheableSection(dashboardId) + const [recordOnNextLoad, setRecordOnNextLoad] = useState( + recordingState === 'recording' + ) + + const onError = () => setError('plugin') + + const pluginProps = useMemo( + () => ({ + isVisualizationLoaded: true, + forDashboard: true, + displayProperty: userSettings.displayProperty, + visualization, + style, + onError, + + // For caching: --- + // Add user & dashboard IDs to cache ID to avoid removing a cached + // plugin that might be used in another dashboard also + // TODO: May also want user ID too for multi-user situations + cacheId: `${dashboardId}-${itemId}`, + isParentCached: isCached, + recordOnNextLoad: recordOnNextLoad, + }), + [ + userSettings, + visualization, + dashboardId, + itemId, + isCached, + recordOnNextLoad, + style, + ] + ) + + useEffect(() => { + // Tell plugin to remove cached data if this dashboard has been removed + // from offline storage + if (iframeRef?.current && !isCached) { + postRobot + .send(iframeRef.current.contentWindow, 'removeCachedData') + .catch((err) => { + // catch error if iframe hasn't loaded yet + const msg = 'No handler found for post message:' + if (err.message.startsWith(msg)) { + return + } + console.error(err) + }) + } + }, [isCached]) + + useEffect(() => { + if (iframeRef?.current) { + const listener = postRobot.on( + 'getProps', + // listen for messages coming only from the iframe rendered by this component + { window: iframeRef.current.contentWindow }, + () => { + if (recordOnNextLoad) { + // Avoid recording unnecessarily, + // e.g. if plugin re-requests props for some reason + setRecordOnNextLoad(false) + } + + return pluginProps + } + ) + + return () => listener.cancel() + } + }, [recordOnNextLoad, pluginProps]) + + useEffect(() => { + if (iframeRef.current?.contentWindow) { + postRobot.send( + iframeRef.current.contentWindow, + 'newProps', + pluginProps + ) + } + }, [pluginProps]) + + useEffect(() => { + setError(null) + }, [filterVersion, visualization.type]) + + const getIframeSrc = useCallback(() => { + const pluginType = [CHART, REPORT_TABLE].includes(activeType) + ? VISUALIZATION + : activeType + + // 1. check if there is an override for the plugin + const pluginOverrides = getPluginOverrides() + + if (pluginOverrides && pluginOverrides[pluginType]) { + return pluginOverrides[pluginType] + } + + // 2. check if there is an installed app for the pluginType + // and use its plugin launch URL + const pluginLaunchUrl = getPluginLaunchUrl(pluginType, d2) + + if (pluginLaunchUrl) { + return pluginLaunchUrl + } + + setError('missing-plugin') + + return + }, [activeType, d2]) + + if (error) { + return error === 'missing-plugin' ? ( +
+ +
+ ) : ( +
+ +
+ ) + } + + const iframeSrc = getIframeSrc() + + return ( +
+ {iframeSrc ? ( + + ) : null} +
+ ) +} + +IframePlugin.propTypes = { + activeType: PropTypes.string, + dashboardId: PropTypes.string, + dashboardMode: PropTypes.string, + filterVersion: PropTypes.string, + itemId: PropTypes.string, + itemType: PropTypes.string, + style: PropTypes.object, + visualization: PropTypes.object, +} + +// Memoize the whole component to avoid re-rendering when the parent component re-renders. +// This happens when the interpretations panel is toggled because the `item` prop changes (height) +// causing the `Item` component to re-render. + +export default React.memo(IframePlugin) diff --git a/src/components/Item/VisualizationItem/Visualization/MapPlugin.js b/src/components/Item/VisualizationItem/Visualization/MapPlugin.js index da5059ccf..2ece5f85d 100644 --- a/src/components/Item/VisualizationItem/Visualization/MapPlugin.js +++ b/src/components/Item/VisualizationItem/Visualization/MapPlugin.js @@ -1,84 +1,16 @@ import { useOnlineStatus } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' -import React, { useState, useEffect } from 'react' -import { MAP } from '../../../../modules/itemTypes.js' -import getVisualizationContainerDomId from '../getVisualizationContainerDomId.js' -import { isElementFullscreen } from '../isElementFullscreen.js' -import DefaultPlugin from './DefaultPlugin.js' +import React from 'react' +import IframePlugin from './IframePlugin.js' import NoVisualizationMessage from './NoVisualizationMessage.js' -import { pluginIsAvailable, getPlugin, unmount } from './plugin.js' - -const mapViewIsThematicOrEvent = (mapView) => - mapView.layer.includes('thematic') || mapView.layer.includes('event') const mapViewIsEELayer = (mapView) => mapView.layer.includes('earthEngine') -const MapPlugin = ({ - visualization, - applyFilters, - availableHeight, - availableWidth, - gridWidth, - itemFilters, - ...props -}) => { +const MapPlugin = ({ visualization, style, ...pluginProps }) => { const { offline } = useOnlineStatus() - const [initialized, setInitialized] = useState(false) - - useEffect(() => { - const resizeMap = async (id, isFullscreen) => { - const plugin = await getPlugin(MAP) - plugin?.resize && - plugin.resize(getVisualizationContainerDomId(id), isFullscreen) - } - - resizeMap(props.item.id, isElementFullscreen(props.item.id)) - }, [availableHeight, availableWidth, gridWidth]) - - // The function returned from this effect is run when this component unmounts - useEffect(() => () => unmount(props.item, MAP), []) - - useEffect(() => { - const setMapOfflineStatus = async (offlineStatus) => { - const plugin = await getPlugin(MAP) - plugin?.setOfflineStatus && plugin.setOfflineStatus(offlineStatus) - } - - !offline && setInitialized(true) - setMapOfflineStatus(offline) - }, [offline]) - - const getVisualization = () => { - if (props.item.type === MAP) { - // apply filters only to thematic and event layers - // for maps AO - const mapViews = visualization.mapViews.map((mapView) => { - if (mapViewIsThematicOrEvent(mapView)) { - return applyFilters(mapView, itemFilters) - } - - return mapView - }) - - return { - ...visualization, - mapViews, - } - } else { - // this is the case of a non map AO passed to the maps plugin - // due to a visualization type switch in dashboard item - // maps plugin takes care of converting the AO to a suitable format - return applyFilters(visualization, itemFilters) - } - } - - if ( - offline && - !initialized && - visualization.mapViews?.find(mapViewIsEELayer) - ) { + if (offline && visualization.mapViews?.find(mapViewIsEELayer)) { return ( - - - ) : ( - ) } MapPlugin.propTypes = { - applyFilters: PropTypes.func, - availableHeight: PropTypes.number, - availableWidth: PropTypes.number, - gridWidth: PropTypes.number, - isFullscreen: PropTypes.bool, - item: PropTypes.object, - itemFilters: PropTypes.object, + style: PropTypes.object, visualization: PropTypes.object, } diff --git a/src/components/Item/VisualizationItem/Visualization/MissingPluginMessage.js b/src/components/Item/VisualizationItem/Visualization/MissingPluginMessage.js new file mode 100644 index 000000000..f8a2f2da2 --- /dev/null +++ b/src/components/Item/VisualizationItem/Visualization/MissingPluginMessage.js @@ -0,0 +1,41 @@ +import i18n from '@dhis2/d2-i18n' +import { Center } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import { isPrintMode } from '../../../../modules/dashboardModes.js' +import { getAppName } from '../../../../modules/itemTypes.js' +import classes from './styles/VisualizationErrorMessage.module.css' + +const MissingPluginMessage = ({ itemType, dashboardMode }) => { + return ( +
+

+ {i18n.t('The plugin for rendering this item is not available')} +

+ {!isPrintMode(dashboardMode) ? ( +

+ e.stopPropagation()} + target="_blank" + rel="noopener noreferrer" + href="/dhis-web-app-management/index.html#/app-hub" + > + {i18n.t( + 'Install the {{appName}} app from the App Hub', + { + appName: getAppName(itemType), + } + )} + +

+ ) : null} +
+ ) +} + +MissingPluginMessage.propTypes = { + dashboardMode: PropTypes.string, + itemType: PropTypes.string, +} + +export default MissingPluginMessage diff --git a/src/components/Item/VisualizationItem/Visualization/Visualization.js b/src/components/Item/VisualizationItem/Visualization/Visualization.js index ebacd1d63..5d0040688 100644 --- a/src/components/Item/VisualizationItem/Visualization/Visualization.js +++ b/src/components/Item/VisualizationItem/Visualization/Visualization.js @@ -1,120 +1,166 @@ +import { useD2 } from '@dhis2/app-runtime-adapter-d2' import i18n from '@dhis2/d2-i18n' +import { Button, Cover, IconInfo24, colors } from '@dhis2/ui' import uniqueId from 'lodash/uniqueId.js' import PropTypes from 'prop-types' -import React from 'react' -import { connect } from 'react-redux' -import { isEditMode } from '../../../../modules/dashboardModes.js' -import { getVisualizationId } from '../../../../modules/item.js' +import React, { useMemo } from 'react' +import { useSelector } from 'react-redux' import { VISUALIZATION, + EVENT_VISUALIZATION, MAP, CHART, REPORT_TABLE, - getItemTypeForVis, } from '../../../../modules/itemTypes.js' -import { - sGetItemFiltersRoot, - DEFAULT_STATE_ITEM_FILTERS, -} from '../../../../reducers/itemFilters.js' -import { sGetVisualization } from '../../../../reducers/visualizations.js' -import memoizeOne from '../memoizeOne.js' -import DataVisualizerPlugin from './DataVisualizerPlugin.js' +import { sGetSelectedId } from '../../../../reducers/selected.js' import getFilteredVisualization from './getFilteredVisualization.js' import getVisualizationConfig from './getVisualizationConfig.js' +import IframePlugin from './IframePlugin.js' import LegacyPlugin from './LegacyPlugin.js' import MapPlugin from './MapPlugin.js' import NoVisualizationMessage from './NoVisualizationMessage.js' import { pluginIsAvailable } from './plugin.js' +import classes from './styles/Visualization.module.css' -class Visualization extends React.Component { - constructor(props) { - super(props) +const Visualization = ({ + visualization, + activeType, + item, + itemFilters, + availableHeight, + availableWidth, + dashboardMode, + originalType, + showNoFiltersOverlay, + onClickNoFiltersOverlay, + ...rest +}) => { + const { d2 } = useD2() + const dashboardId = useSelector(sGetSelectedId) - this.memoizedGetFilteredVisualization = memoizeOne( - getFilteredVisualization - ) - this.memoizedGetVisualizationConfig = memoizeOne(getVisualizationConfig) + // NOTE: + // The following is all memoized because the IframePlugin (and potentially others) + // are wrapped in React.memo() to avoid unnecessary re-renders + // The main problem here was `item` which changes height when the interpretations panel is toggled + // causing all the chain of components to re-render. + // The only dependency using `item` is `item.id` which doesn't change so the memoized plugin props + // should also always be the same regardless of the `item` details. - this.getFilterVersion = memoizeOne(() => uniqueId()) - } + const style = useMemo( + () => ({ + height: availableHeight, + width: availableWidth || undefined, + }), + [availableHeight, availableWidth] + ) + + const visualizationConfig = useMemo( + () => getVisualizationConfig(visualization, originalType, activeType), + [visualization, activeType, originalType] + ) + + const filteredVisualization = useMemo( + () => + getFilteredVisualization( + visualizationConfig, + itemFilters, + originalType + ), + [visualizationConfig, itemFilters, originalType] + ) - render() { - const { visualization, activeType, item, itemFilters, ...rest } = - this.props + const filterVersion = useMemo(() => uniqueId(), []) - if (!visualization) { + const iFramePluginProps = useMemo( + () => ({ + originalType, + activeType, + style, + filterVersion, + dashboardMode, + dashboardId, + itemId: item.id, + itemType: item.type, + }), + [ + originalType, + activeType, + style, + filterVersion, + dashboardMode, + dashboardId, + item.id, + item.type, + ] + ) + + if (!visualization) { + return + } + + switch (activeType) { + case CHART: + case REPORT_TABLE: + case VISUALIZATION: { return ( - ) } - - const style = { height: this.props.availableHeight } - if (this.props.availableWidth) { - style.width = this.props.availableWidth + case EVENT_VISUALIZATION: { + return ( + <> + {showNoFiltersOverlay ? ( + +
+ + {i18n.t( + 'Filters are not applied to line list dashboard items.' + )} + +
+
+ ) : null} + + + ) } - const visualizationConfig = this.memoizedGetVisualizationConfig( - visualization, - getItemTypeForVis(item), - activeType - ) - - const filterVersion = this.getFilterVersion(itemFilters) + case MAP: { + return ( + + ) + } - switch (activeType) { - case VISUALIZATION: - case CHART: - case REPORT_TABLE: { - return ( - - ) - } - case MAP: { - return ( - - ) - } - default: { - return pluginIsAvailable(activeType || item.type) ? ( - - ) : ( - - ) - } + default: { + return pluginIsAvailable(activeType || item.type, d2) ? ( + + ) : ( + + ) } } } @@ -126,21 +172,10 @@ Visualization.propTypes = { dashboardMode: PropTypes.string, item: PropTypes.object, itemFilters: PropTypes.object, + originalType: PropTypes.string, + showNoFiltersOverlay: PropTypes.bool, visualization: PropTypes.object, + onClickNoFiltersOverlay: PropTypes.func, } -const mapStateToProps = (state, ownProps) => { - const itemFilters = !isEditMode(ownProps.dashboardMode) - ? sGetItemFiltersRoot(state) - : DEFAULT_STATE_ITEM_FILTERS - - return { - itemFilters, - visualization: sGetVisualization( - state, - getVisualizationId(ownProps.item) - ), - } -} - -export default connect(mapStateToProps)(Visualization) +export default Visualization diff --git a/src/components/Item/VisualizationItem/Visualization/VisualizationErrorMessage.js b/src/components/Item/VisualizationItem/Visualization/VisualizationErrorMessage.js index c8d652708..dee442a33 100644 --- a/src/components/Item/VisualizationItem/Visualization/VisualizationErrorMessage.js +++ b/src/components/Item/VisualizationItem/Visualization/VisualizationErrorMessage.js @@ -4,7 +4,6 @@ import { colors } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import { isPrintMode } from '../../../../modules/dashboardModes.js' -import { getVisualizationId } from '../../../../modules/item.js' import { getAppName, itemTypeMap } from '../../../../modules/itemTypes.js' import classes from './styles/VisualizationErrorMessage.module.css' @@ -23,11 +22,15 @@ const getErrorIcon = () => ( ) -const VisualizationErrorMessage = ({ item, dashboardMode }) => { +const VisualizationErrorMessage = ({ + itemType, + dashboardMode, + visualizationId, +}) => { const { baseUrl } = useConfig() - const visHref = `${baseUrl}/${itemTypeMap[item.type].appUrl( - getVisualizationId(item) + const visHref = `${baseUrl}/${itemTypeMap[itemType].appUrl( + visualizationId )}` return ( @@ -45,7 +48,7 @@ const VisualizationErrorMessage = ({ item, dashboardMode }) => { href={visHref} > {i18n.t('Open this item in {{appName}}', { - appName: getAppName(item.type), + appName: getAppName(itemType), })}

@@ -56,7 +59,8 @@ const VisualizationErrorMessage = ({ item, dashboardMode }) => { VisualizationErrorMessage.propTypes = { dashboardMode: PropTypes.string, - item: PropTypes.object, + itemType: PropTypes.string, + visualizationId: PropTypes.string, } export default VisualizationErrorMessage diff --git a/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js b/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js index b51fd6df7..f7153b32d 100644 --- a/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js +++ b/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js @@ -4,13 +4,16 @@ import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' import Visualization from '../Visualization.js' -jest.mock( - '../DataVisualizerPlugin', - () => - function MockVisualizationPlugin() { - return
- } -) +jest.mock('@dhis2/app-runtime-adapter-d2', () => { + return { + useD2: jest.fn(() => ({ + d2: { + currentUser: { username: 'rainbowDash' }, + system: { installedApps: {} }, + }, + })), + } +}) jest.mock( '../MapPlugin', @@ -27,10 +30,21 @@ jest.mock( } ) +jest.mock( + '../IframePlugin', + () => + function MockIframePlugin() { + return
+ } +) + const mockStore = configureMockStore() const DEFAULT_STORE_WITH_ONE_ITEM = { visualizations: { rainbowVis: { rows: [], columns: [], filters: [] } }, itemFilters: {}, + selected: { + id: 'test-dashboard', + }, } test('renders a MapPlugin when activeType is MAP', () => { @@ -105,24 +119,6 @@ test('renders active type MAP rather than original type REPORT_TABLE', () => { expect(container).toMatchSnapshot() }) -test('renders active type REPORT_TABLE rather than original type MAP', () => { - const { container } = render( - - - - ) - expect(container).toMatchSnapshot() -}) - test('renders a DefaultPlugin when activeType is EVENT_CHART', () => { const { container } = render( @@ -162,6 +158,10 @@ test('renders a DefaultPlugin when activeType is EVENT_REPORT', () => { test('renders NoVisMessage when no visualization', () => { const store = { visualizations: {}, + itemFilters: {}, + selected: { + id: 'test-dashboard', + }, } const { container } = render( diff --git a/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap b/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap index d89239e26..a41b45a4e 100644 --- a/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap +++ b/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap @@ -32,56 +32,180 @@ exports[`renders NoVisMessage when no visualization 1`] = ` exports[`renders a DefaultPlugin when activeType is EVENT_CHART 1`] = `
-
+

+ + + + + + + No data to display + +

`; exports[`renders a DefaultPlugin when activeType is EVENT_REPORT 1`] = `
-
+

+ + + + + + + No data to display + +

`; exports[`renders a MapPlugin when activeType is MAP 1`] = `
-
+

+ + + + + + + No data to display + +

`; exports[`renders a VisualizationPlugin for CHART 1`] = `
-
+

+ + + + + + + No data to display + +

`; exports[`renders a VisualizationPlugin for REPORT_TABLE 1`] = `
-
+

+ + + + + + + No data to display + +

`; exports[`renders active type MAP rather than original type REPORT_TABLE 1`] = `
-
-
-`; - -exports[`renders active type REPORT_TABLE rather than original type MAP 1`] = ` -
-
+

+ + + + + + + No data to display + +

`; diff --git a/src/components/Item/VisualizationItem/Visualization/getFilteredVisualization.js b/src/components/Item/VisualizationItem/Visualization/getFilteredVisualization.js index 249c9b0ad..fe7526108 100644 --- a/src/components/Item/VisualizationItem/Visualization/getFilteredVisualization.js +++ b/src/components/Item/VisualizationItem/Visualization/getFilteredVisualization.js @@ -1,8 +1,33 @@ -const getFilteredVisualization = (visualization, filters) => { +import { MAP } from '../../../../modules/itemTypes.js' + +const mapViewIsThematicOrEvent = (mapView) => + mapView.layer.includes('thematic') || mapView.layer.includes('event') + +const getFilteredMap = (visualization, filters) => { + // apply filters only to thematic and event layers + const mapViews = visualization.mapViews.map((mapView) => { + if (mapViewIsThematicOrEvent(mapView)) { + return getFilteredVisualization(mapView, filters) + } + + return mapView + }) + + return { + ...visualization, + mapViews, + } +} + +const getFilteredVisualization = (visualization, filters, originalType) => { if (!Object.keys(filters).length) { return visualization } + if (originalType === MAP) { + return getFilteredMap(visualization, filters) + } + // deep clone objects in filters to avoid changing the visualization in the Redux store const visRows = visualization.rows.map((obj) => ({ ...obj })) const visColumns = visualization.columns.map((obj) => ({ ...obj })) diff --git a/src/components/Item/VisualizationItem/Visualization/plugin.js b/src/components/Item/VisualizationItem/Visualization/plugin.js index 5c5634149..88682bc43 100644 --- a/src/components/Item/VisualizationItem/Visualization/plugin.js +++ b/src/components/Item/VisualizationItem/Visualization/plugin.js @@ -1,4 +1,5 @@ import { + itemTypeMap, REPORT_TABLE, CHART, VISUALIZATION, @@ -11,19 +12,44 @@ import { loadExternalScript } from './loadExternalScript.js' //external plugins const itemTypeToGlobalVariable = { - [MAP]: 'mapPlugin', [EVENT_REPORT]: 'eventReportPlugin', [EVENT_CHART]: 'eventChartPlugin', } const itemTypeToScriptPath = { - [MAP]: '/dhis-web-maps/map.js', [EVENT_REPORT]: '/dhis-web-event-reports/eventreport.js', [EVENT_CHART]: '/dhis-web-event-visualizer/eventchart.js', } const hasIntegratedPlugin = (type) => - [CHART, REPORT_TABLE, VISUALIZATION].includes(type) + [CHART, REPORT_TABLE, VISUALIZATION, MAP].includes(type) + +export const getPluginLaunchUrl = (type, d2) => { + // 1. lookup in api/apps for the "manually installed" app, this can be a new version for a core (bundled) app + // 2. fallback to default hardcoded path for the core (bundled) apps + const baseUrl = d2.system.systemInfo.instanceBaseUrl + const apps = d2.system.installedApps + const appKey = itemTypeMap[type].appKey + + const appDetails = appKey && apps.find((app) => app.key === appKey) + + if (appDetails) { + return appDetails.pluginLaunchUrl + } + + if (hasIntegratedPlugin(type)) { + switch (type) { + case CHART: + case REPORT_TABLE: + case VISUALIZATION: { + return `${baseUrl}/dhis-web-data-visualizer/plugin.html` + } + case MAP: { + return `${baseUrl}/dhis-web-maps/plugin.html` + } + } + } +} export const getPlugin = async (type) => { if (hasIntegratedPlugin(type)) { @@ -64,11 +90,13 @@ const fetchPlugin = async (type, baseUrl) => { return await scriptsPromise } -export const pluginIsAvailable = (type) => - hasIntegratedPlugin(type) || itemTypeToGlobalVariable[type] +export const pluginIsAvailable = (type, d2) => + hasIntegratedPlugin(type) || + Boolean(getPluginLaunchUrl(type, d2)) || + Boolean(itemTypeToGlobalVariable[type]) -const loadPlugin = async (type, config, credentials) => { - if (!pluginIsAvailable(type)) { +const loadPlugin = async ({ type, config, credentials, d2 }) => { + if (!pluginIsAvailable(type, d2)) { return } @@ -90,7 +118,7 @@ const loadPlugin = async (type, config, credentials) => { export const load = async ( item, visualization, - { credentials, activeType, options = {} } + { credentials, activeType, d2, options = {} } ) => { const config = { ...visualization, @@ -99,7 +127,7 @@ export const load = async ( } const type = activeType || item.type - await loadPlugin(type, config, credentials) + await loadPlugin({ type, config, credentials, d2 }) } export const unmount = async (item, activeType) => { diff --git a/src/components/Item/VisualizationItem/Visualization/styles/DataVisualizerPlugin.module.css b/src/components/Item/VisualizationItem/Visualization/styles/IframePlugin.module.css similarity index 100% rename from src/components/Item/VisualizationItem/Visualization/styles/DataVisualizerPlugin.module.css rename to src/components/Item/VisualizationItem/Visualization/styles/IframePlugin.module.css diff --git a/src/components/Item/VisualizationItem/Visualization/styles/Visualization.module.css b/src/components/Item/VisualizationItem/Visualization/styles/Visualization.module.css new file mode 100644 index 000000000..707f5ed23 --- /dev/null +++ b/src/components/Item/VisualizationItem/Visualization/styles/Visualization.module.css @@ -0,0 +1,12 @@ +.noFiltersOverlay { + display: flex; + flex-direction: column; + gap: var(--spacers-dp8); + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + background-color: rgba(255, 255, 255, 0.92); + font-size: 13px; + color: var(--colors-grey900); +} diff --git a/src/components/Item/VisualizationItem/__tests__/__snapshots__/Item.spec.js.snap b/src/components/Item/VisualizationItem/__tests__/__snapshots__/Item.spec.js.snap index 09be52a40..f073653d9 100644 --- a/src/components/Item/VisualizationItem/__tests__/__snapshots__/Item.spec.js.snap +++ b/src/components/Item/VisualizationItem/__tests__/__snapshots__/Item.spec.js.snap @@ -11,11 +11,15 @@ exports[`Visualization/Item renders edit mode 1`] = ` Fancy Chart

+ class="itemActionsWrap" + > +
+
+ class="itemHeaderRightWrap" + > +
+
`dhis-web-data-visualizer/#/${id}`, appName: 'Data Visualizer', + appKey: 'data-visualizer', defaultItemCount: 10, }, [REPORT_TABLE]: { @@ -123,6 +125,18 @@ export const itemTypeMap = { appUrl: (id) => `dhis-web-event-visualizer/?id=${id}`, appName: 'Event Visualizer', }, + [EVENT_VISUALIZATION]: { + id: EVENT_VISUALIZATION, + endPointName: 'eventVisualizations', + propName: 'eventVisualization', + pluralTitle: i18n.t('Line lists'), + domainType: DOMAIN_TYPE_TRACKER, + isVisualizationType: true, + // TODO change to the path for the bundled app + appUrl: (id) => `api/apps/line-listing/index.html#/${id}`, + appName: 'Line Listing', + appKey: 'line-listing', + }, [APP]: { endPointName: 'apps', propName: 'appKey', @@ -206,6 +220,7 @@ export const getItemIcon = (type) => { return IconFileDocument24 case CHART: case EVENT_CHART: + case EVENT_VISUALIZATION: return IconVisualizationColumn24 case MAP: return IconWorld24 diff --git a/src/modules/localStorage.js b/src/modules/localStorage.js index 4255dc74d..048a59109 100644 --- a/src/modules/localStorage.js +++ b/src/modules/localStorage.js @@ -4,3 +4,8 @@ export const getPreferredDashboardId = (username) => export const storePreferredDashboardId = (username, dashboardId) => { localStorage.setItem(`dhis2.dashboard.current.${username}`, dashboardId) } + +export const getPluginOverrides = () => + (process.env.NODE_ENV !== 'production' && + JSON.parse(localStorage.getItem('dhis2.dashboard.pluginOverrides'))) || + undefined diff --git a/src/pages/edit/ItemSelector/ItemSelector.js b/src/pages/edit/ItemSelector/ItemSelector.js index 39f93bc41..5b104b887 100644 --- a/src/pages/edit/ItemSelector/ItemSelector.js +++ b/src/pages/edit/ItemSelector/ItemSelector.js @@ -1,5 +1,5 @@ import { useDataQuery } from '@dhis2/app-runtime' -import { Popover, FlyoutMenu } from '@dhis2/ui' +import { Layer, Popper, FlyoutMenu } from '@dhis2/ui' import React, { useState, useEffect, createRef } from 'react' import { itemTypeMap, getDefaultItemCount } from '../../../modules/itemTypes.js' import useDebounce from '../../../modules/useDebounce.js' @@ -107,23 +107,19 @@ const ItemSelector = () => { /> {isOpen && ( - -
- - {getMenuGroups()} - -
-
+ + +
+ + {getMenuGroups()} + +
+
+
)} ) diff --git a/src/pages/edit/ItemSelector/selectableItems.js b/src/pages/edit/ItemSelector/selectableItems.js index 0e310f2f6..115886c3b 100644 --- a/src/pages/edit/ItemSelector/selectableItems.js +++ b/src/pages/edit/ItemSelector/selectableItems.js @@ -4,6 +4,7 @@ import { MAP, EVENT_CHART, EVENT_REPORT, + EVENT_VISUALIZATION, REPORTS, RESOURCES, APP, @@ -42,6 +43,7 @@ export const categorizedItems = [ MAP, EVENT_REPORT, EVENT_CHART, + EVENT_VISUALIZATION, REPORTS, RESOURCES, APP, diff --git a/src/pages/edit/NewDashboard.js b/src/pages/edit/NewDashboard.js index 7757e6ead..b3f2eff0e 100644 --- a/src/pages/edit/NewDashboard.js +++ b/src/pages/edit/NewDashboard.js @@ -2,9 +2,10 @@ import i18n from '@dhis2/d2-i18n' import cx from 'classnames' import PropTypes from 'prop-types' import React, { useState, useEffect } from 'react' -import { connect } from 'react-redux' +import { connect, useDispatch } from 'react-redux' import { Redirect } from 'react-router-dom' import { acSetEditNewDashboard } from '../../actions/editDashboard.js' +import { acClearSelected } from '../../actions/selected.js' import DashboardContainer from '../../components/DashboardContainer.js' import Notice from '../../components/Notice.js' import { useWindowDimensions } from '../../components/WindowDimensionsProvider.js' @@ -18,6 +19,7 @@ import classes from './styles/NewDashboard.module.css' import TitleBar from './TitleBar.js' const NewDashboard = (props) => { + const dispatch = useDispatch() const { width } = useWindowDimensions() const [redirectUrl, setRedirectUrl] = useState(null) @@ -27,7 +29,9 @@ const NewDashboard = (props) => { return } setHeaderbarVisible(true) - props.setNewDashboard() + + dispatch(acSetEditNewDashboard()) + dispatch(acClearSelected()) }, []) if (redirectUrl) { @@ -63,13 +67,10 @@ const NewDashboard = (props) => { NewDashboard.propTypes = { isPrintPreviewView: PropTypes.bool, - setNewDashboard: PropTypes.func, } const mapStateToProps = (state) => ({ isPrintPreviewView: sGetIsPrintPreviewView(state), }) -export default connect(mapStateToProps, { - setNewDashboard: acSetEditNewDashboard, -})(NewDashboard) +export default connect(mapStateToProps)(NewDashboard) diff --git a/yarn.lock b/yarn.lock index 8ee47654c..30f1fb0f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2372,10 +2372,10 @@ i18next "^10.3" moment "^2.24.0" -"@dhis2/d2-ui-core@7.4.0", "@dhis2/d2-ui-core@^7.3.3": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-core/-/d2-ui-core-7.4.0.tgz#796afffbca4cf7f76a500d979e53c01abe8ebcb2" - integrity sha512-lVk3q2PV3Xb0RoeCcK/UgR2zqymj3SUix4qQXm+5HLZww6UtP8sTj+QcEEuVbPBqHPHaXnSOsKgtoD02QJfm4A== +"@dhis2/d2-ui-core@7.4.1", "@dhis2/d2-ui-core@^7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-core/-/d2-ui-core-7.4.1.tgz#1040e720afe550ac6a09af7fa7bd4061d73201f2" + integrity sha512-5NCb8xdcaySgF81SFZsK8hFimaSTig1A4F8vuID8EPcIiqJMf+Mqvw+2OvEngEouQ+KjCr1VBX7Lf7TvHGi/Ww== dependencies: babel-runtime "^6.26.0" d2 "~31.7" @@ -2383,14 +2383,14 @@ material-ui "^0.20.0" rxjs "^5.5.7" -"@dhis2/d2-ui-interpretations@^7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-interpretations/-/d2-ui-interpretations-7.4.0.tgz#c712a26886137009c32ab56e9b6d9e06ac90c424" - integrity sha512-54LYUNwwCd3L8HYlg8gv1URjbfM1FPVSJoUieeyhtdidpTFUPPoc9Ud4kgrq+Y2QJe/mgh8Dx0RYyqoNLrRmtw== +"@dhis2/d2-ui-interpretations@^7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-interpretations/-/d2-ui-interpretations-7.4.1.tgz#813523753985b53108d09c03999f5ea16b5b4759" + integrity sha512-OWRevE8CHXoVoQRzCNx35akZCgLGVGIBMeTzN1+Yflfveyf0/HVMi7rvVowzB3zSfJ6W1H8kpUdyPYyZy/QHoQ== dependencies: - "@dhis2/d2-ui-mentions-wrapper" "7.4.0" - "@dhis2/d2-ui-rich-text" "7.4.0" - "@dhis2/d2-ui-sharing-dialog" "7.4.0" + "@dhis2/d2-ui-mentions-wrapper" "7.4.1" + "@dhis2/d2-ui-rich-text" "7.4.1" + "@dhis2/d2-ui-sharing-dialog" "7.4.1" "@material-ui/core" "^3.3.1" "@material-ui/icons" "^3.0.1" babel-runtime "^6.26.0" @@ -2400,30 +2400,30 @@ prop-types "^15.5.10" react-portal "^4.1.5" -"@dhis2/d2-ui-mentions-wrapper@7.4.0", "@dhis2/d2-ui-mentions-wrapper@^7.3.3": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-mentions-wrapper/-/d2-ui-mentions-wrapper-7.4.0.tgz#5cd2334aa70c877885fa7b9cdfb4a1165d5423c5" - integrity sha512-QPAxLoGa1FC6AlouvxE7ORLku8VdwRNnPkm4TeNBwmIDoHjg+c5EQ+Sc07NJykSt1f3l/LG+sAQxfFTfRAS6Sg== +"@dhis2/d2-ui-mentions-wrapper@7.4.1", "@dhis2/d2-ui-mentions-wrapper@^7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-mentions-wrapper/-/d2-ui-mentions-wrapper-7.4.1.tgz#666f9414e5d86ad4455c308cb7a262eeab36b3c8" + integrity sha512-55BtWL04AK0e4Jv99/ByAisvKqJLMFtU7bJTyP8pGfUMBy/oCssAZqG6AzgsfaTd72yfNeEsTjLnIYc46paiEQ== dependencies: "@material-ui/core" "^3.3.1" lodash "^4.17.10" prop-types "^15.6.2" -"@dhis2/d2-ui-rich-text@7.4.0", "@dhis2/d2-ui-rich-text@^7.3.3", "@dhis2/d2-ui-rich-text@^7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-rich-text/-/d2-ui-rich-text-7.4.0.tgz#e1fb6eff7309ea80a6af3bee2a247c9f93ea721d" - integrity sha512-q8woPPyYHiqQfNtujuCQH8eq7zRcN0140XqrKHw1DXF/fnqmrcVDTNZkPSaWMuUsdPhcgTHcnuEySP0MRAXv6g== +"@dhis2/d2-ui-rich-text@7.4.1", "@dhis2/d2-ui-rich-text@^7.4.0", "@dhis2/d2-ui-rich-text@^7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-rich-text/-/d2-ui-rich-text-7.4.1.tgz#8764208c59c6758bf34765b1dbe01762ce435d11" + integrity sha512-/n5nE0b4EDI/kX0/aN+vFDOswoWT5JQ3lwtHsUxailvnEHMu4/3l27Q38Z+5qhKwl+jYNB9GOFxWoSiymUgBbw== dependencies: babel-runtime "^6.26.0" markdown-it "^8.4.2" prop-types "^15.6.2" -"@dhis2/d2-ui-sharing-dialog@7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-sharing-dialog/-/d2-ui-sharing-dialog-7.4.0.tgz#1910f96bbfb4c706b1fedc677e8a28e62599dc73" - integrity sha512-5ebCQ7WxhD6y0gyPeOUiFb4XiKPJI4gZfRFmcuNLphu57TkcWj6HxWF3cDv+DYM0BS8c9Pq2iSCThay5nPFYDA== +"@dhis2/d2-ui-sharing-dialog@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-sharing-dialog/-/d2-ui-sharing-dialog-7.4.1.tgz#66b793802a317f8bd9c2e755525440c141114ce0" + integrity sha512-471V/OWy6drFwpDiZ8xEWwhxgdf3JoJfusifZBwyINrghZzgxOWf3yHBBVccWNKIsZK2kIEB19iSMNn2vch92Q== dependencies: - "@dhis2/d2-ui-core" "7.4.0" + "@dhis2/d2-ui-core" "7.4.1" "@material-ui/core" "^3.3.1" "@material-ui/icons" "^3.0.1" babel-runtime "^6.26.0" @@ -2940,6 +2940,48 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== +"@krakenjs/belter@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@krakenjs/belter/-/belter-2.1.2.tgz#b51c5a2d0652279262eb2fa79a6e26b12a868867" + integrity sha512-9qg3xsfcMtljHyxYuin8/KDeqG80ecHCAbuqbHd8Cz0CszK4JvYS3tsmCzYnp6zfnR9uSRCuoj/dKWhZcJtqbg== + dependencies: + "@krakenjs/cross-domain-safe-weakmap" "^2.0.2" + "@krakenjs/cross-domain-utils" "^3.0.2" + "@krakenjs/zalgo-promise" "^2.0.0" + +"@krakenjs/cross-domain-safe-weakmap@^2.0.0", "@krakenjs/cross-domain-safe-weakmap@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@krakenjs/cross-domain-safe-weakmap/-/cross-domain-safe-weakmap-2.0.3.tgz#eb607534c14bd8bc2f3456d993618361fb38489f" + integrity sha512-WsGi6347ddZ9Y0HoBuTYCX2QTAHxYVaUs2T/0n8XJKXZOEJPnLWlW6eYAOgyyuUsYusWMAkYv00fvfxAlTU8/w== + dependencies: + "@krakenjs/cross-domain-utils" "^3.0.2" + +"@krakenjs/cross-domain-utils@^3.0.0", "@krakenjs/cross-domain-utils@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@krakenjs/cross-domain-utils/-/cross-domain-utils-3.0.2.tgz#cbbd3323e65e831debc351549c0b8972df0382d5" + integrity sha512-uNTQrAevzKtOu13UZzamCR6rKX2vpdb6g/gVXMJVSMvAlYhxBN6mOD6U56DmSdBJDKvLSYZbIYUkBh2inDyIXQ== + +"@krakenjs/post-robot@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@krakenjs/post-robot/-/post-robot-11.0.0.tgz#98a285b70db2bac2c58f297cd8403a4cc2518a6a" + integrity sha512-t+IlQCrwzLa1IWxEvdfr9r/xgOmQoykVQ1rtEukw1LYVHPU3p3eDDC5djODE7ErWYDbkYncrHcbDEh6Gc/G9wQ== + dependencies: + "@krakenjs/belter" "^2.0.0" + "@krakenjs/cross-domain-safe-weakmap" "^2.0.0" + "@krakenjs/cross-domain-utils" "^3.0.0" + "@krakenjs/universal-serialize" "^2.0.0" + "@krakenjs/zalgo-promise" "^2.0.0" + +"@krakenjs/universal-serialize@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@krakenjs/universal-serialize/-/universal-serialize-2.0.0.tgz#c4a508ec53f6b412b1032e78c570b87f4f2c7763" + integrity sha512-Qd9W2iaP5lMTzXPETomBDAaqbgHYkEjJKcQ+3eNC8EwWGCHFk3/+uQ41B+QYfSZqRoz8AEjdNHOps7nDEypAyw== + +"@krakenjs/zalgo-promise@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@krakenjs/zalgo-promise/-/zalgo-promise-2.0.1.tgz#36b4225a566f0a0903a8d771a11a9efc131c6987" + integrity sha512-n30eknZjD7z8/joFqjI8FIDZ0yJPZHcQBce1B3tAumwNZL0C42Ta/w37MfthxHV61JHEFGfy7b727h/kzagJDA== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -3047,9 +3089,9 @@ source-map "^0.7.3" "@popperjs/core@^2.10.1": - version "2.11.5" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" - integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== + version "2.11.6" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" + integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== "@react-hook/latest@^1.0.2": version "1.0.3"