From 9c09c3b031f9e9641c0655a09639f3f67ec0910b Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Wed, 6 Feb 2019 14:08:10 +0100 Subject: [PATCH 01/35] Api module for interacting with user data store --- packages/app/src/api/userDataStore.js | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/app/src/api/userDataStore.js diff --git a/packages/app/src/api/userDataStore.js b/packages/app/src/api/userDataStore.js new file mode 100644 index 0000000000..788b75bf43 --- /dev/null +++ b/packages/app/src/api/userDataStore.js @@ -0,0 +1,36 @@ +import { getInstance } from 'd2'; +import { onError } from './index'; + +export const NAMESPACE = 'analytics'; + +export const hasNamespace = async d2 => + await d2.currentUser.dataStore.has(NAMESPACE); + +export const getNamespace = async (d2, hasNamespace) => + hasNamespace + ? await d2.currentUser.dataStore.get(NAMESPACE) + : await d2.currentUser.dataStore.create(NAMESPACE); + +export const apiCreate = async (data, key, namespace) => { + try { + const d2 = await getInstance(); + const ns = + namespace || (await getNamespace(d2, await hasNamespace(d2))); + + return ns.set(key, data); + } catch (error) { + return onError(error); + } +}; + +export const apiFetch = async (key, namespace) => { + try { + const d2 = await getInstance(); + const ns = + namespace || (await getNamespace(d2, await hasNamespace(d2))); + + return ns.get(key); + } catch (error) { + return onError(error); + } +}; From 27019e0c27cdcad0874e868ff85ec997600594bf Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Wed, 6 Feb 2019 14:09:31 +0100 Subject: [PATCH 02/35] Api methods for saving/retrieving analytical object in/from user data store --- packages/app/src/api/analytics.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/api/analytics.js b/packages/app/src/api/analytics.js index 2db5a3978c..ae0541e7fb 100644 --- a/packages/app/src/api/analytics.js +++ b/packages/app/src/api/analytics.js @@ -1,8 +1,11 @@ import { getInstance } from 'd2'; import { FIXED_DIMENSIONS } from '../modules/fixedDimensions'; +import * as userDataStore from './userDataStore'; const peId = FIXED_DIMENSIONS.pe.id; +export const CURRENT_AO_KEY = 'currentAnalyticalObject'; + export const apiDownloadImage = async (type, formData) => { const d2 = await getInstance(); const api = d2.Api.getApi(); @@ -100,3 +103,9 @@ export const apiFetchAnalyticsForYearOverYear = async (current, options) => { yearlySeriesLabels, })); }; + +export const apiSaveAOInUserDataStore = (current, key = CURRENT_AO_KEY) => + userDataStore.apiCreate(current, key); + +export const apiFetchAOFromUserDataStore = (key = CURRENT_AO_KEY) => + userDataStore.apiFetch(key); From 58cfe614f91ff40d9bb69ae2fa094dae256f5e94 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Wed, 6 Feb 2019 14:11:47 +0100 Subject: [PATCH 03/35] MapMenu component --- packages/app/i18n/en.pot | 14 ++- .../app/src/components/MapMenu/MapMenu.js | 87 +++++++++++++++++++ .../app/src/components/MenuBar/MenuBar.js | 2 + 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 packages/app/src/components/MapMenu/MapMenu.js diff --git a/packages/app/i18n/en.pot b/packages/app/i18n/en.pot index 86ba294f94..2b214b0205 100644 --- a/packages/app/i18n/en.pot +++ b/packages/app/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: 2019-01-21T11:22:11.007Z\n" -"PO-Revision-Date: 2019-01-21T11:22:11.007Z\n" +"POT-Creation-Date: 2019-02-06T12:52:28.419Z\n" +"PO-Revision-Date: 2019-02-06T12:52:28.419Z\n" msgid "Rename successful" msgstr "" @@ -44,10 +44,10 @@ msgstr "" msgid "Select All" msgstr "" -msgid "Remove" +msgid "Search dimensions" msgstr "" -msgid "Search dimensions" +msgid "Remove" msgstr "" msgid "Dimension recommended with selected data" @@ -122,6 +122,12 @@ msgstr "" msgid "None selected" msgstr "" +msgid "Map" +msgstr "" + +msgid "Open chart as map" +msgstr "" + msgid "Unsaved chart" msgstr "" diff --git a/packages/app/src/components/MapMenu/MapMenu.js b/packages/app/src/components/MapMenu/MapMenu.js new file mode 100644 index 0000000000..0512c20715 --- /dev/null +++ b/packages/app/src/components/MapMenu/MapMenu.js @@ -0,0 +1,87 @@ +import React, { Component, Fragment } from 'react'; +import Button from '@material-ui/core/Button'; +import Menu from '@material-ui/core/Menu'; +import i18n from '@dhis2/d2-i18n'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { sGetCurrent } from '../../reducers/current'; +import { apiSaveAOInUserDataStore } from '../../api/analytics'; + +export const MAPS_APP_URL = 'dhis-web-maps'; + +export const defaultState = { + anchorEl: null, +}; + +export class MapMenu extends Component { + constructor(props, context) { + super(props); + + this.state = defaultState; + this.d2 = context.d2; + this.baseUrl = context.baseUrl; + } + + toggleMenu = target => this.setState({ anchorEl: target || null }); + + onOpenChartAsMapClick = async () => { + await apiSaveAOInUserDataStore(this.props.current); + + window.location.href = `${ + this.baseUrl + }/${MAPS_APP_URL}?currentAnalyticalObject=true`; + }; + + render() { + return ( + + + this.toggleMenu()} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + getContentAnchorEl={null} + disableAutoFocusItem + > + + + + ); + } +} + +MapMenu.propTypes = { + className: PropTypes.string, +}; + +MapMenu.contextTypes = { + d2: PropTypes.object, + baseUrl: PropTypes.string, +}; + +const mapStateToProps = state => ({ + current: sGetCurrent(state), +}); + +export default connect( + mapStateToProps, + {} +)(MapMenu); diff --git a/packages/app/src/components/MenuBar/MenuBar.js b/packages/app/src/components/MenuBar/MenuBar.js index c71bd007f3..19521ed24f 100644 --- a/packages/app/src/components/MenuBar/MenuBar.js +++ b/packages/app/src/components/MenuBar/MenuBar.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import FileMenu from '@dhis2/d2-ui-file-menu'; +import MapMenu from '../MapMenu/MapMenu'; import { withStyles } from '@material-ui/core/styles'; import UpdateButton from '../UpdateButton/UpdateButton'; @@ -49,6 +50,7 @@ export const MenuBar = ({ classes, ...props }, context) => (
+
); From 4958517953a547a9418a56b90f7b0803449a9bdb Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Thu, 7 Feb 2019 12:10:43 +0100 Subject: [PATCH 04/35] Remove map dropdown menu, use chart type selection section instead --- .../app/src/components/MapMenu/MapMenu.js | 87 ------------------- .../app/src/components/MenuBar/MenuBar.js | 2 - 2 files changed, 89 deletions(-) delete mode 100644 packages/app/src/components/MapMenu/MapMenu.js diff --git a/packages/app/src/components/MapMenu/MapMenu.js b/packages/app/src/components/MapMenu/MapMenu.js deleted file mode 100644 index 0512c20715..0000000000 --- a/packages/app/src/components/MapMenu/MapMenu.js +++ /dev/null @@ -1,87 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import Button from '@material-ui/core/Button'; -import Menu from '@material-ui/core/Menu'; -import i18n from '@dhis2/d2-i18n'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { sGetCurrent } from '../../reducers/current'; -import { apiSaveAOInUserDataStore } from '../../api/analytics'; - -export const MAPS_APP_URL = 'dhis-web-maps'; - -export const defaultState = { - anchorEl: null, -}; - -export class MapMenu extends Component { - constructor(props, context) { - super(props); - - this.state = defaultState; - this.d2 = context.d2; - this.baseUrl = context.baseUrl; - } - - toggleMenu = target => this.setState({ anchorEl: target || null }); - - onOpenChartAsMapClick = async () => { - await apiSaveAOInUserDataStore(this.props.current); - - window.location.href = `${ - this.baseUrl - }/${MAPS_APP_URL}?currentAnalyticalObject=true`; - }; - - render() { - return ( - - - this.toggleMenu()} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - getContentAnchorEl={null} - disableAutoFocusItem - > - - - - ); - } -} - -MapMenu.propTypes = { - className: PropTypes.string, -}; - -MapMenu.contextTypes = { - d2: PropTypes.object, - baseUrl: PropTypes.string, -}; - -const mapStateToProps = state => ({ - current: sGetCurrent(state), -}); - -export default connect( - mapStateToProps, - {} -)(MapMenu); diff --git a/packages/app/src/components/MenuBar/MenuBar.js b/packages/app/src/components/MenuBar/MenuBar.js index 19521ed24f..c71bd007f3 100644 --- a/packages/app/src/components/MenuBar/MenuBar.js +++ b/packages/app/src/components/MenuBar/MenuBar.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import FileMenu from '@dhis2/d2-ui-file-menu'; -import MapMenu from '../MapMenu/MapMenu'; import { withStyles } from '@material-ui/core/styles'; import UpdateButton from '../UpdateButton/UpdateButton'; @@ -50,7 +49,6 @@ export const MenuBar = ({ classes, ...props }, context) => (
-
); From 81d3b1692ad5e1af1d2b61fcc2137074cf7a993f Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Thu, 7 Feb 2019 12:11:16 +0100 Subject: [PATCH 05/35] Globe icon asset --- packages/app/src/assets/GlobeIcon.js | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/app/src/assets/GlobeIcon.js diff --git a/packages/app/src/assets/GlobeIcon.js b/packages/app/src/assets/GlobeIcon.js new file mode 100644 index 0000000000..2b1bb53baa --- /dev/null +++ b/packages/app/src/assets/GlobeIcon.js @@ -0,0 +1,69 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +const GlobeIcon = ({ + style = { width: 24, height: 24, paddingRight: '8px' }, +}) => ( + + icon_chart_GIS + Created with Sketch. + + + + + + + + + + + + + + + + + + + +); + +export default GlobeIcon; From 6b550f1bf2f3cd318f61ddcaa2df15555f255b72 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Thu, 7 Feb 2019 12:13:38 +0100 Subject: [PATCH 06/35] Define "Open as map" visualization type --- packages/app/src/modules/chartTypes.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/app/src/modules/chartTypes.js b/packages/app/src/modules/chartTypes.js index 57e1c654ef..fb462051aa 100644 --- a/packages/app/src/modules/chartTypes.js +++ b/packages/app/src/modules/chartTypes.js @@ -12,6 +12,7 @@ export const GAUGE = 'GAUGE'; export const BUBBLE = 'BUBBLE'; export const YEAR_OVER_YEAR_LINE = 'YEAR_OVER_YEAR_LINE'; export const YEAR_OVER_YEAR_COLUMN = 'YEAR_OVER_YEAR_COLUMN'; +export const OPEN_AS_GEO_MAP = 'OPEN_AS_GEO_MAP'; export const chartTypeDisplayNames = { [COLUMN]: i18n.t('Column'), @@ -25,10 +26,13 @@ export const chartTypeDisplayNames = { [GAUGE]: i18n.t('Gauge'), [YEAR_OVER_YEAR_LINE]: i18n.t('Year over year (line)'), [YEAR_OVER_YEAR_COLUMN]: i18n.t('Year over year (column)'), + [OPEN_AS_GEO_MAP]: i18n.t('Open as: Geo Map'), }; const stackedTypes = [STACKED_COLUMN, STACKED_BAR, AREA]; const yearOverYearTypes = [YEAR_OVER_YEAR_LINE, YEAR_OVER_YEAR_COLUMN]; +const openAsTypes = [OPEN_AS_GEO_MAP]; export const isStacked = type => stackedTypes.includes(type); export const isYearOverYear = type => yearOverYearTypes.includes(type); +export const isOpenAsType = type => openAsTypes.includes(type); From fcf08bbbc2bc12fdbfbcfc9511ca3fdd67d2c84e Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Thu, 7 Feb 2019 12:19:18 +0100 Subject: [PATCH 07/35] Move managing-AO-in-user-data-store requests to api/userDataStore.js --- packages/app/src/api/analytics.js | 9 --------- packages/app/src/api/userDataStore.js | 7 +++++++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/app/src/api/analytics.js b/packages/app/src/api/analytics.js index ae0541e7fb..2db5a3978c 100644 --- a/packages/app/src/api/analytics.js +++ b/packages/app/src/api/analytics.js @@ -1,11 +1,8 @@ import { getInstance } from 'd2'; import { FIXED_DIMENSIONS } from '../modules/fixedDimensions'; -import * as userDataStore from './userDataStore'; const peId = FIXED_DIMENSIONS.pe.id; -export const CURRENT_AO_KEY = 'currentAnalyticalObject'; - export const apiDownloadImage = async (type, formData) => { const d2 = await getInstance(); const api = d2.Api.getApi(); @@ -103,9 +100,3 @@ export const apiFetchAnalyticsForYearOverYear = async (current, options) => { yearlySeriesLabels, })); }; - -export const apiSaveAOInUserDataStore = (current, key = CURRENT_AO_KEY) => - userDataStore.apiCreate(current, key); - -export const apiFetchAOFromUserDataStore = (key = CURRENT_AO_KEY) => - userDataStore.apiFetch(key); diff --git a/packages/app/src/api/userDataStore.js b/packages/app/src/api/userDataStore.js index 788b75bf43..c3b96a877f 100644 --- a/packages/app/src/api/userDataStore.js +++ b/packages/app/src/api/userDataStore.js @@ -2,6 +2,7 @@ import { getInstance } from 'd2'; import { onError } from './index'; export const NAMESPACE = 'analytics'; +export const CURRENT_AO_KEY = 'currentAnalyticalObject'; export const hasNamespace = async d2 => await d2.currentUser.dataStore.has(NAMESPACE); @@ -34,3 +35,9 @@ export const apiFetch = async (key, namespace) => { return onError(error); } }; + +export const apiSaveAOInUserDataStore = (current, key = CURRENT_AO_KEY) => + apiCreate(current, key); + +export const apiFetchAOFromUserDataStore = (key = CURRENT_AO_KEY) => + apiFetch(key); From 589d001b09aff791e56b072baa0e24ba1b5f8efc Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Thu, 7 Feb 2019 12:26:30 +0100 Subject: [PATCH 08/35] Move visualization type menu item into dedicated component --- .../VisualizationTypeMenuItem.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/app/src/components/VisualizationTypeSelector/VisualizationTypeMenuItem.js diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeMenuItem.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeMenuItem.js new file mode 100644 index 0000000000..211aa8d5b5 --- /dev/null +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeMenuItem.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import MenuItem from '@material-ui/core/MenuItem/MenuItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText/ListItemText'; + +import { chartTypeDisplayNames } from '../../modules/chartTypes'; +import VisualizationTypeIcon from './VisualizationTypeIcon'; + +const VisualizationTypeMenuItem = ({ + type, + visualizationType, + styles, + ...props +}) => ( + + + + + + +); + +VisualizationTypeMenuItem.propTypes = { + type: PropTypes.oneOf(Object.keys(chartTypeDisplayNames)), + visualizationType: PropTypes.oneOf(Object.keys(chartTypeDisplayNames)), + styles: PropTypes.object, +}; + +export default VisualizationTypeMenuItem; From 0aeec590e84e3f2b843bc60c78aa75502505f184 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Thu, 7 Feb 2019 12:27:18 +0100 Subject: [PATCH 09/35] Enable support for "Open as: Geo Map" visualization type --- .../VisualizationTypeIcon.js | 5 + .../VisualizationTypeSelector.js | 97 ++++++++++++++----- .../styles/VisualizationTypeSelector.style.js | 6 ++ 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js index 9adbc40215..634b3ceaa2 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js @@ -12,6 +12,8 @@ import AreaIcon from '../../assets/AreaIcon'; import RadarIcon from '../../assets/RadarIcon'; import YearOverYearLineIcon from '../../assets/YearOverYearLineIcon'; import YearOverYearColumnIcon from '../../assets/YearOverYearColumnIcon'; +import GlobeIcon from '../../assets/GlobeIcon'; + import { COLUMN, STACKED_COLUMN, @@ -24,6 +26,7 @@ import { GAUGE, YEAR_OVER_YEAR_LINE, YEAR_OVER_YEAR_COLUMN, + OPEN_AS_GEO_MAP, chartTypeDisplayNames, } from '../../modules/chartTypes'; @@ -49,6 +52,8 @@ const VisualizationTypeIcon = ({ type = COLUMN, style }) => { return ; case YEAR_OVER_YEAR_COLUMN: return ; + case OPEN_AS_GEO_MAP: + return case COLUMN: default: return ; diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index f3824a4c1e..85140b5678 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -1,22 +1,34 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; + import Button from '@material-ui/core/Button'; import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; + import VisualizationTypeIcon from './VisualizationTypeIcon'; -import { chartTypeDisplayNames } from '../../modules/chartTypes'; +import { + chartTypeDisplayNames, + isOpenAsType, + OPEN_AS_GEO_MAP, +} from '../../modules/chartTypes'; import { sGetUiType } from '../../reducers/ui'; +import { sGetCurrent } from '../../reducers/current'; import { acSetUiType } from '../../actions/ui'; +import { apiSaveAOInUserDataStore } from '../../api/userDataStore'; import styles from './styles/VisualizationTypeSelector.style'; +import VisualizationTypeMenuItem from './VisualizationTypeMenuItem'; + +export const MAPS_APP_URL = 'dhis-web-maps'; export class VisualizationTypeSelector extends Component { - state = { - anchorEl: null, - }; + constructor(props, context) { + super(props); + + this.state = { anchorEl: null }; + this.baseUrl = context.baseUrl; + this.chartTypes = this.getChartTypes(); + } handleButtonClick = event => { this.setState({ anchorEl: event.currentTarget }); @@ -27,13 +39,43 @@ export class VisualizationTypeSelector extends Component { this.handleClose(); }; + handleOpenAsMenuItemClick = type => event => { + if (type === OPEN_AS_GEO_MAP) { + this.handleOpenChartAsMapClick(); + } + }; + handleClose = () => { this.setState({ anchorEl: null }); }; + handleOpenChartAsMapClick = async () => { + await apiSaveAOInUserDataStore(this.props.current); + + window.location.href = `${ + this.baseUrl + }/${MAPS_APP_URL}?currentAnalyticalObject=true`; + }; + + getChartTypes = () => { + return Object.keys(chartTypeDisplayNames).reduce( + (result, type) => { + const chartType = isOpenAsType(type) + ? 'openAsTypes' + : 'nativeTypes'; + + result[chartType].push(type); + + return result; + }, + { nativeTypes: [], openAsTypes: [] } + ); + }; + render() { const { anchorEl } = this.state; const { visualizationType } = this.props; + const { nativeTypes, openAsTypes } = this.chartTypes; return ( @@ -59,26 +101,26 @@ export class VisualizationTypeSelector extends Component { style: styles.menu, }} > - {Object.keys(chartTypeDisplayNames).map(type => ( - ( + - - - - - + /> + ))} +
+
+ {openAsTypes.map(type => ( + ))} @@ -90,8 +132,13 @@ VisualizationTypeSelector.propTypes = { visualizationType: PropTypes.oneOf(Object.keys(chartTypeDisplayNames)), }; +VisualizationTypeSelector.contextTypes = { + baseUrl: PropTypes.string, +}; + const mapStateToProps = state => ({ visualizationType: sGetUiType(state), + current: sGetCurrent(state), }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/app/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.style.js b/packages/app/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.style.js index 7381c8c4fe..826f14c0af 100644 --- a/packages/app/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.style.js +++ b/packages/app/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.style.js @@ -17,6 +17,11 @@ export default { maxWidth: '600px', padding: 0, }, + menuDivider: { + border: 'none', + borderBottom: '1px solid', + borderColor: colors.greyLight, + }, menuItem: { height: 120, width: 126, @@ -30,6 +35,7 @@ export default { }, listItemIcon: { margin: 0, + marginBottom: 15, }, listItemSvg: { width: 48, From c5f7847d34e53cd03eb19e459b5ff265b74df6ee Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Mon, 11 Feb 2019 13:03:12 +0100 Subject: [PATCH 10/35] Prevent url from changing on "update" button click if path = "/currentAnalyticalObject" --- packages/app/src/components/UpdateButton/UpdateButton.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/UpdateButton/UpdateButton.js b/packages/app/src/components/UpdateButton/UpdateButton.js index 414ccb0418..b7da9a4900 100644 --- a/packages/app/src/components/UpdateButton/UpdateButton.js +++ b/packages/app/src/components/UpdateButton/UpdateButton.js @@ -10,6 +10,7 @@ import { sGetCurrent } from '../../reducers/current'; import * as fromActions from '../../actions'; import history from '../../modules/history'; import styles from './styles/UpdateButton.style'; +import { CURRENT_AO_KEY } from '../../api/userDataStore'; const UpdateButton = ({ classes, @@ -25,10 +26,16 @@ const UpdateButton = ({ clearLoadError(); onUpdate(ui); + const urlContainsCurrentAOKey = + history.location.pathname === '/' + CURRENT_AO_KEY; + const pathWithoutInterpretation = current && current.id ? `/${current.id}` : '/'; - if (history.location.pathname !== pathWithoutInterpretation) { + if ( + !urlContainsCurrentAOKey && + history.location.pathname !== pathWithoutInterpretation + ) { history.push(pathWithoutInterpretation); } From 60af8b95c00cc846165ad9b6279fdcf26c0a5a38 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Mon, 11 Feb 2019 13:05:26 +0100 Subject: [PATCH 11/35] Remove id, name & displayName attributes from AO before putting it into user data store --- .../VisualizationTypeSelector.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index 85140b5678..b0ac977704 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -21,6 +21,13 @@ import VisualizationTypeMenuItem from './VisualizationTypeMenuItem'; export const MAPS_APP_URL = 'dhis-web-maps'; +export const prepareCurrentAnalyticalObject = current => ({ + ...current, + id: undefined, + name: undefined, + displayName: undefined, +}); + export class VisualizationTypeSelector extends Component { constructor(props, context) { super(props); @@ -50,7 +57,11 @@ export class VisualizationTypeSelector extends Component { }; handleOpenChartAsMapClick = async () => { - await apiSaveAOInUserDataStore(this.props.current); + const currentAnalyticalObject = prepareCurrentAnalyticalObject( + this.props.current + ); + + await apiSaveAOInUserDataStore(currentAnalyticalObject); window.location.href = `${ this.baseUrl From ef910428ac097a6672b0cbcc785c357e9cc9e0d1 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Mon, 11 Feb 2019 13:18:23 +0100 Subject: [PATCH 12/35] Enable support for retrieving and applying chart config from user data store on app start --- packages/app/i18n/en.pot | 10 ++-------- packages/app/src/components/App.js | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/app/i18n/en.pot b/packages/app/i18n/en.pot index 2b214b0205..48ae120350 100644 --- a/packages/app/i18n/en.pot +++ b/packages/app/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: 2019-02-06T12:52:28.419Z\n" -"PO-Revision-Date: 2019-02-06T12:52:28.419Z\n" +"POT-Creation-Date: 2019-02-11T08:33:18.634Z\n" +"PO-Revision-Date: 2019-02-11T08:33:18.634Z\n" msgid "Rename successful" msgstr "" @@ -122,12 +122,6 @@ msgstr "" msgid "None selected" msgstr "" -msgid "Map" -msgstr "" - -msgid "Open chart as map" -msgstr "" - msgid "Unsaved chart" msgstr "" diff --git a/packages/app/src/components/App.js b/packages/app/src/components/App.js index 2ca45e6af4..fe6be0eb6a 100644 --- a/packages/app/src/components/App.js +++ b/packages/app/src/components/App.js @@ -20,10 +20,15 @@ import * as fromActions from '../actions'; import history from '../modules/history'; import defaultMetadata from '../modules/metadata'; import { sGetUi } from '../reducers/ui'; +import { + apiFetchAOFromUserDataStore, + CURRENT_AO_KEY, +} from '../api/userDataStore'; import './App.css'; import './scrollbar.css'; + export class App extends Component { unlisten = null; @@ -59,13 +64,23 @@ export class App extends Component { const { store } = this.context; if (location.pathname.length > 1) { + // /currentAnalyticalObject // /${id}/ // /${id}/interpretation/${interpretationId} const pathParts = location.pathname.slice(1).split('/'); const id = pathParts[0]; const interpretationId = pathParts[2]; + const urlContainsCurrentAOKey = id === CURRENT_AO_KEY; + + if (urlContainsCurrentAOKey) { + const AO = await apiFetchAOFromUserDataStore(); - if (this.refetch(location)) { + this.props.setCurrent(AO); + this.props.setVisualization(AO); + this.props.setUiFromVisualization(AO); + } + + if (!urlContainsCurrentAOKey && this.refetch(location)) { await store.dispatch( fromActions.tDoLoadVisualization( this.props.apiObjectName, @@ -206,6 +221,14 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => ({ onKeyUp: ui => dispatch(fromActions.fromCurrent.acSetCurrentFromUi(ui)), + setCurrent: current => + dispatch(fromActions.fromCurrent.acSetCurrent(current)), + setVisualization: visualization => + dispatch( + fromActions.fromVisualization.acSetVisualization(visualization) + ), + setUiFromVisualization: visualization => + dispatch(fromActions.fromUi.acSetUiFromVisualization(visualization)), }); App.contextTypes = { From aa86d6d49a929605a0a7ec3593da00d33637c88f Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Mon, 11 Feb 2019 13:22:26 +0100 Subject: [PATCH 13/35] Correct name for create/update method in user data store api --- packages/app/src/api/userDataStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/api/userDataStore.js b/packages/app/src/api/userDataStore.js index c3b96a877f..36b7992a86 100644 --- a/packages/app/src/api/userDataStore.js +++ b/packages/app/src/api/userDataStore.js @@ -12,7 +12,7 @@ export const getNamespace = async (d2, hasNamespace) => ? await d2.currentUser.dataStore.get(NAMESPACE) : await d2.currentUser.dataStore.create(NAMESPACE); -export const apiCreate = async (data, key, namespace) => { +export const apiSave = async (data, key, namespace) => { try { const d2 = await getInstance(); const ns = @@ -37,7 +37,7 @@ export const apiFetch = async (key, namespace) => { }; export const apiSaveAOInUserDataStore = (current, key = CURRENT_AO_KEY) => - apiCreate(current, key); + apiSave(current, key); export const apiFetchAOFromUserDataStore = (key = CURRENT_AO_KEY) => apiFetch(key); From 5d4f8b56629fe84c0b48623f67020c8a6f6e966c Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Mon, 11 Feb 2019 13:26:39 +0100 Subject: [PATCH 14/35] prettier --- packages/app/src/components/App.js | 1 - .../VisualizationTypeSelector/VisualizationTypeIcon.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/src/components/App.js b/packages/app/src/components/App.js index fe6be0eb6a..1266b1907e 100644 --- a/packages/app/src/components/App.js +++ b/packages/app/src/components/App.js @@ -28,7 +28,6 @@ import { import './App.css'; import './scrollbar.css'; - export class App extends Component { unlisten = null; diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js index 634b3ceaa2..53ad987acb 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeIcon.js @@ -53,7 +53,7 @@ const VisualizationTypeIcon = ({ type = COLUMN, style }) => { case YEAR_OVER_YEAR_COLUMN: return ; case OPEN_AS_GEO_MAP: - return + return ; case COLUMN: default: return ; From 0a33f5c4b22c40905e676ca006cc957bfee783d8 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Mon, 11 Feb 2019 15:23:51 +0100 Subject: [PATCH 15/35] Move api related "prepareCurrentAnalyticalObject" function into api/userDataStore --- packages/app/src/api/userDataStore.js | 7 +++++++ .../VisualizationTypeSelector.js | 17 +++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/app/src/api/userDataStore.js b/packages/app/src/api/userDataStore.js index 36b7992a86..420eb878e7 100644 --- a/packages/app/src/api/userDataStore.js +++ b/packages/app/src/api/userDataStore.js @@ -12,6 +12,13 @@ export const getNamespace = async (d2, hasNamespace) => ? await d2.currentUser.dataStore.get(NAMESPACE) : await d2.currentUser.dataStore.create(NAMESPACE); +export const prepareCurrentAnalyticalObject = current => ({ + ...current, + id: undefined, + name: undefined, + displayName: undefined, +}); + export const apiSave = async (data, key, namespace) => { try { const d2 = await getInstance(); diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index b0ac977704..b310f7d8c4 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -6,7 +6,6 @@ import Button from '@material-ui/core/Button'; import Menu from '@material-ui/core/Menu'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; -import VisualizationTypeIcon from './VisualizationTypeIcon'; import { chartTypeDisplayNames, isOpenAsType, @@ -15,19 +14,17 @@ import { import { sGetUiType } from '../../reducers/ui'; import { sGetCurrent } from '../../reducers/current'; import { acSetUiType } from '../../actions/ui'; -import { apiSaveAOInUserDataStore } from '../../api/userDataStore'; -import styles from './styles/VisualizationTypeSelector.style'; +import { + apiSaveAOInUserDataStore, + prepareCurrentAnalyticalObject, +} from '../../api/userDataStore'; + import VisualizationTypeMenuItem from './VisualizationTypeMenuItem'; +import VisualizationTypeIcon from './VisualizationTypeIcon'; +import styles from './styles/VisualizationTypeSelector.style'; export const MAPS_APP_URL = 'dhis-web-maps'; -export const prepareCurrentAnalyticalObject = current => ({ - ...current, - id: undefined, - name: undefined, - displayName: undefined, -}); - export class VisualizationTypeSelector extends Component { constructor(props, context) { super(props); From 2f102001c927d56eb035a094bb9c29495e1fd3a9 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Tue, 12 Feb 2019 11:18:05 +0100 Subject: [PATCH 16/35] Tests --- .../src/api/__tests__/userDataStore.spec.js | 22 +++++++++++++++++++ .../app/src/components/__tests__/App.spec.js | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/app/src/api/__tests__/userDataStore.spec.js diff --git a/packages/app/src/api/__tests__/userDataStore.spec.js b/packages/app/src/api/__tests__/userDataStore.spec.js new file mode 100644 index 0000000000..64daa9fa41 --- /dev/null +++ b/packages/app/src/api/__tests__/userDataStore.spec.js @@ -0,0 +1,22 @@ +import { prepareCurrentAnalyticalObject } from '../userDataStore'; + +describe('prepareCurrentAnalyticalObject', () => { + it('removes corresponding attributes', () => { + const unprocessed = { + id: 'SOME_ID', + name: 'Analytical object typical name', + displayName: 'Analytical object typical name', + key: 'value', + }; + const processed = prepareCurrentAnalyticalObject(unprocessed); + const keysToRemove = ['id', 'name', 'displayName']; + + keysToRemove.forEach(key => { + expect(unprocessed[key]).not.toBeUndefined(); + }); + + keysToRemove.forEach(key => { + expect(processed[key]).toBeUndefined(); + }); + }); +}); diff --git a/packages/app/src/components/__tests__/App.spec.js b/packages/app/src/components/__tests__/App.spec.js index f6ced9df2b..3b08a8ae92 100644 --- a/packages/app/src/components/__tests__/App.spec.js +++ b/packages/app/src/components/__tests__/App.spec.js @@ -7,6 +7,7 @@ import * as actions from '../../actions/'; import history from '../../modules/history'; import { getStubContext } from '../../../../../config/testsContext'; +import * as userDataStore from '../../api/userDataStore'; describe('App', () => { let props; @@ -46,11 +47,15 @@ describe('App', () => { }, keyAnalysisRelativePeriod: 'LAST_12_MONTHS', }, + setCurrent: jest.fn(), + setVisualization: jest.fn(), + setUiFromVisualization: jest.fn(), }; shallowApp = undefined; actions.tDoLoadVisualization = jest.fn(); actions.clearVisualization = jest.fn(); + userDataStore.apiFetchAOFromUserDataStore = jest.fn(); }); afterEach(() => { @@ -125,6 +130,23 @@ describe('App', () => { }); }); + it('loads AO from user data store if id equals to "currentAnalyticalObject"', done => { + props.location.pathname = '/' + userDataStore.CURRENT_AO_KEY; + app(); + + setTimeout(() => { + expect( + userDataStore.apiFetchAOFromUserDataStore + ).toBeCalledTimes(1); + + expect(props.setCurrent).toBeCalledTimes(1); + expect(props.setVisualization).toBeCalledTimes(1); + expect(props.setUiFromVisualization).toBeCalledTimes(1); + + done(); + }); + }); + describe('interpretation id in pathname', () => { beforeEach(() => { props.location.pathname = `/applejack/interpretation/xyz123`; From 8af839b378073c147e4259694cecd367766a6926 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Tue, 12 Feb 2019 16:54:23 +0100 Subject: [PATCH 17/35] Tests --- .../src/api/__tests__/userDataStore.spec.js | 143 ++++++++++++++++-- .../__tests__/VisualizationTypeMenuItem.js | 42 +++++ 2 files changed, 170 insertions(+), 15 deletions(-) create mode 100644 packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.js diff --git a/packages/app/src/api/__tests__/userDataStore.spec.js b/packages/app/src/api/__tests__/userDataStore.spec.js index 64daa9fa41..ca6d8e1594 100644 --- a/packages/app/src/api/__tests__/userDataStore.spec.js +++ b/packages/app/src/api/__tests__/userDataStore.spec.js @@ -1,22 +1,135 @@ -import { prepareCurrentAnalyticalObject } from '../userDataStore'; - -describe('prepareCurrentAnalyticalObject', () => { - it('removes corresponding attributes', () => { - const unprocessed = { - id: 'SOME_ID', - name: 'Analytical object typical name', - displayName: 'Analytical object typical name', - key: 'value', +import * as d2lib from 'd2'; +import * as userDataStore from '../userDataStore'; +import { + apiSave, + apiFetch, + hasNamespace, + getNamespace, + apiSaveAOInUserDataStore, + apiFetchAOFromUserDataStore, + prepareCurrentAnalyticalObject, + NAMESPACE, + CURRENT_AO_KEY, +} from '../userDataStore'; + +let mockD2; +let mockNamespace; + +describe('api: user data store', () => { + beforeEach(() => { + mockNamespace = { + get: jest.fn(), + set: jest.fn(), + }; + mockD2 = { + currentUser: { + dataStore: { + has: jest.fn().mockResolvedValue(false), // false default value for test purposes + get: jest.fn().mockResolvedValue(mockNamespace), + create: jest.fn().mockResolvedValue(mockNamespace), + }, + }, }; - const processed = prepareCurrentAnalyticalObject(unprocessed); - const keysToRemove = ['id', 'name', 'displayName']; + d2lib.getInstance = () => Promise.resolve(mockD2); + }); + + describe('prepareCurrentAnalyticalObject', () => { + it('removes corresponding attributes', () => { + const unprocessed = { + id: 'SOME_ID', + name: 'Analytical object typical name', + displayName: 'Analytical object typical name', + key: 'value', + }; + const processed = prepareCurrentAnalyticalObject(unprocessed); + const keysToRemove = ['id', 'name', 'displayName']; + + keysToRemove.forEach(key => { + expect(unprocessed[key]).not.toBeUndefined(); + }); + + keysToRemove.forEach(key => { + expect(processed[key]).toBeUndefined(); + }); + }); + }); + + describe('hasNamespace', () => { + it('uses result of "has" method of d2.currentUser.dataStore object', async () => { + const result = await hasNamespace(mockD2); + + expect(mockD2.currentUser.dataStore.has).toBeCalledTimes(1); + expect(mockD2.currentUser.dataStore.has).toBeCalledWith(NAMESPACE); + expect(result).toEqual(false); + }); + }); + + describe('getNamespace', () => { + it('retrieves and returns namespace if it exists', async () => { + const result = await getNamespace(mockD2, true); + + expect(mockD2.currentUser.dataStore.get).toBeCalledTimes(1); + expect(mockD2.currentUser.dataStore.create).toBeCalledTimes(0); + expect(result).toMatchObject(mockNamespace); + }); + + it('creates and returns namespace if it doesnt exist', async () => { + const result = await getNamespace(mockD2, false); + + expect(mockD2.currentUser.dataStore.get).toBeCalledTimes(0); + expect(mockD2.currentUser.dataStore.create).toBeCalledTimes(1); + expect(result).toMatchObject(mockNamespace); + }); + }); + + describe('apiSave', () => { + it('uses d2 namespace.set for saving data under given key', async () => { + const data = {}; + const key = 'someKey'; + + await apiSave(data, key, mockNamespace); + + expect(mockNamespace.set).toBeCalledTimes(1); + expect(mockNamespace.set).toBeCalledWith(key, data); + }); + }); + + describe('apiFetch', () => { + it('uses d2 namespace.get for retrieving data by given key', async () => { + const key = 'someKey'; + + await apiFetch(key, mockNamespace); - keysToRemove.forEach(key => { - expect(unprocessed[key]).not.toBeUndefined(); + expect(mockNamespace.get).toBeCalledTimes(1); + expect(mockNamespace.get).toBeCalledWith(key); }); + }); + + describe('apiSaveAoInUserDataStore', () => { + beforeEach(() => { + userDataStore.getNamespace = () => Promise.resolve(mockNamespace); + }); + + it('uses default key unless specified', async () => { + const data = {}; + + await apiSaveAOInUserDataStore(data); + + expect(mockNamespace.set).toBeCalledTimes(1); + expect(mockNamespace.set).toBeCalledWith(CURRENT_AO_KEY, data); + }); + }); + + describe('apiFetchAOFromUserDataStore', () => { + beforeEach(() => { + userDataStore.getNamespace = () => Promise.resolve(mockNamespace); + }); + + it('uses default key unless specified', async () => { + await apiFetchAOFromUserDataStore(); - keysToRemove.forEach(key => { - expect(processed[key]).toBeUndefined(); + expect(mockNamespace.get).toBeCalledTimes(1); + expect(mockNamespace.get).toBeCalledWith(CURRENT_AO_KEY); }); }); }); diff --git a/packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.js b/packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.js new file mode 100644 index 0000000000..a14887135e --- /dev/null +++ b/packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VisualizationTypeMenuItem from '../VisualizationTypeMenuItem'; +import MenuItem from '@material-ui/core/MenuItem'; +import VisualizationTypeIcon from '../VisualizationTypeIcon'; + +describe('VisualizationTypeMenuItem component ', () => { + let props; + let shallowElement; + + const element = () => { + if (!shallowElement) { + shallowElement = shallow(); + } + return shallowElement; + }; + + beforeEach(() => { + props = { + type: '', + visualizationType: '', + styles: {}, + }; + shallowElement = undefined; + }); + + it('renders MenuItem', () => { + expect( + element() + .find(MenuItem) + .first().length + ).toEqual(1); + }); + + it('renders VisualizationTypeIcon', () => { + expect( + element() + .find(VisualizationTypeIcon) + .first().length + ).toEqual(1); + }); +}); From 81c2a176da29efb60095aa3d4952d32fe709dc2e Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Wed, 13 Feb 2019 10:43:02 +0100 Subject: [PATCH 18/35] Tests --- ...tionTypeMenuItem.js => VisualizationTypeMenuItem.spec.js} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename packages/app/src/components/VisualizationTypeSelector/__tests__/{VisualizationTypeMenuItem.js => VisualizationTypeMenuItem.spec.js} (89%) diff --git a/packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.js b/packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.spec.js similarity index 89% rename from packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.js rename to packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.spec.js index a14887135e..64650d0168 100644 --- a/packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.js +++ b/packages/app/src/components/VisualizationTypeSelector/__tests__/VisualizationTypeMenuItem.spec.js @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import VisualizationTypeMenuItem from '../VisualizationTypeMenuItem'; import MenuItem from '@material-ui/core/MenuItem'; import VisualizationTypeIcon from '../VisualizationTypeIcon'; +import { COLUMN } from '../../../modules/chartTypes'; describe('VisualizationTypeMenuItem component ', () => { let props; @@ -17,8 +18,8 @@ describe('VisualizationTypeMenuItem component ', () => { beforeEach(() => { props = { - type: '', - visualizationType: '', + type: COLUMN, + visualizationType: COLUMN, styles: {}, }; shallowElement = undefined; From 99c45b37b3b65452c43f44c6ae514fe04a011e96 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Mon, 18 Feb 2019 17:21:25 +0100 Subject: [PATCH 19/35] Append dimension items names to currentAnalyticalObject --- .../src/api/__tests__/userDataStore.spec.js | 178 +++++++++++++++++- packages/app/src/api/userDataStore.js | 29 ++- .../VisualizationTypeSelector.js | 7 +- 3 files changed, 210 insertions(+), 4 deletions(-) diff --git a/packages/app/src/api/__tests__/userDataStore.spec.js b/packages/app/src/api/__tests__/userDataStore.spec.js index ca6d8e1594..cc32732447 100644 --- a/packages/app/src/api/__tests__/userDataStore.spec.js +++ b/packages/app/src/api/__tests__/userDataStore.spec.js @@ -8,9 +8,11 @@ import { apiSaveAOInUserDataStore, apiFetchAOFromUserDataStore, prepareCurrentAnalyticalObject, + removeUnnecessaryAttributesFromAnalyticalObject, NAMESPACE, CURRENT_AO_KEY, } from '../userDataStore'; +import { appendDimensionItemNamesToAnalyticalObject } from '../userDataStore'; let mockD2; let mockNamespace; @@ -34,14 +36,186 @@ describe('api: user data store', () => { }); describe('prepareCurrentAnalyticalObject', () => { - it('removes corresponding attributes', () => { + it('removes unnecessary attributes', () => { + const current = { + id: 'SOME_ID', + name: 'Analytical object typical name', + displayName: 'Analytical object typical name', + key: 'value', + columns: [ + { + dimension: 'dx', + items: [ + { + id: 'Uvn6LCg7dVU', + }, + ], + }, + ], + filters: [ + { + dimension: 'ou', + items: [ + { + id: 'qhqAxPSTUXp', + }, + { + id: 'Vth0fbpFcsO', + }, + ], + }, + ], + rows: [ + { + dimension: 'pe', + items: [ + { + id: '2017', + }, + ], + }, + ], + }; + const metadata = { + Uvn6LCg7dVU: { name: 'ANC 1 Coverage' }, + qhqAxPSTUXp: { name: 'Koinadugu' }, + Vth0fbpFcsO: { name: 'Kono' }, + 2017: { name: '2017' }, + }; + const expected = { + key: 'value', + columns: [ + { + dimension: 'dx', + items: [ + { + id: 'Uvn6LCg7dVU', + name: 'ANC 1 Coverage', + }, + ], + }, + ], + filters: [ + { + dimension: 'ou', + items: [ + { + id: 'qhqAxPSTUXp', + name: 'Koinadugu', + }, + { + id: 'Vth0fbpFcsO', + name: 'Kono', + }, + ], + }, + ], + rows: [ + { + dimension: 'pe', + items: [ + { + id: '2017', + name: '2017', + }, + ], + }, + ], + }; + + const result = prepareCurrentAnalyticalObject(current, metadata); + + expect(expected).toEqual(result); + }); + }); + + describe('appendDimensionItemNamesToAnalyticalObject', () => { + it('appends dimension item names to analytical object', () => { + const current = { + id: 'SOME_ID', + name: 'Analytical object typical name', + displayName: 'Analytical object typical name', + key: 'value', + columns: [ + { + dimension: 'dx', + items: [ + { + id: 'Uvn6LCg7dVU', + }, + ], + }, + ], + filters: [ + { + dimension: 'ou', + items: [ + { + id: 'qhqAxPSTUXp', + }, + { + id: 'Vth0fbpFcsO', + }, + ], + }, + ], + rows: [ + { + dimension: 'pe', + items: [ + { + id: '2017', + }, + ], + }, + ], + }; + const metadata = { + Uvn6LCg7dVU: { name: 'ANC 1 Coverage' }, + qhqAxPSTUXp: { name: 'Koinadugu' }, + Vth0fbpFcsO: { name: 'Kono' }, + 2017: { name: '2017' }, + }; + + const testDimensionItemNamesAreUndefined = dimension => { + dimension.items.forEach(item => { + expect(item.name).toBeUndefined(); + }); + }; + + const testDimensionItemNamesAreNotUndefined = dimension => { + dimension.items.forEach(item => { + expect(item.name).not.toBeUndefined(); + expect(item.name).toEqual(metadata[item.id].name); + }); + }; + + current.columns.forEach(testDimensionItemNamesAreUndefined); + current.filters.forEach(testDimensionItemNamesAreUndefined); + current.rows.forEach(testDimensionItemNamesAreUndefined); + + const processed = appendDimensionItemNamesToAnalyticalObject( + metadata, + current + ); + + processed.columns.forEach(testDimensionItemNamesAreNotUndefined); + processed.filters.forEach(testDimensionItemNamesAreNotUndefined); + processed.rows.forEach(testDimensionItemNamesAreNotUndefined); + }); + }); + + describe('removeUnnecessaryAttributesFromAnalyticalObject', () => { + it('removes unnecessary attributes', () => { const unprocessed = { id: 'SOME_ID', name: 'Analytical object typical name', displayName: 'Analytical object typical name', key: 'value', }; - const processed = prepareCurrentAnalyticalObject(unprocessed); + const processed = removeUnnecessaryAttributesFromAnalyticalObject( + unprocessed + ); const keysToRemove = ['id', 'name', 'displayName']; keysToRemove.forEach(key => { diff --git a/packages/app/src/api/userDataStore.js b/packages/app/src/api/userDataStore.js index 420eb878e7..3b223f603e 100644 --- a/packages/app/src/api/userDataStore.js +++ b/packages/app/src/api/userDataStore.js @@ -1,5 +1,6 @@ import { getInstance } from 'd2'; import { onError } from './index'; +import flow from 'lodash-es/flow'; export const NAMESPACE = 'analytics'; export const CURRENT_AO_KEY = 'currentAnalyticalObject'; @@ -12,13 +13,39 @@ export const getNamespace = async (d2, hasNamespace) => ? await d2.currentUser.dataStore.get(NAMESPACE) : await d2.currentUser.dataStore.create(NAMESPACE); -export const prepareCurrentAnalyticalObject = current => ({ +export const prepareCurrentAnalyticalObject = (current, metadata) => + flow( + removeUnnecessaryAttributesFromAnalyticalObject, + appendDimensionItemNamesToAnalyticalObject.bind(this, metadata) + )(current); + +export const removeUnnecessaryAttributesFromAnalyticalObject = current => ({ ...current, id: undefined, name: undefined, displayName: undefined, }); +export const appendDimensionItemNamesToAnalyticalObject = ( + metadata, + current +) => { + const appendNames = dimension => ({ + ...dimension, + items: dimension.items.map(item => ({ + ...item, + name: metadata[item.id].name, + })), + }); + + return { + ...current, + columns: current.columns.map(appendNames), + filters: current.filters.map(appendNames), + rows: current.rows.map(appendNames), + }; +}; + export const apiSave = async (data, key, namespace) => { try { const d2 = await getInstance(); diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index b310f7d8c4..953200771c 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -13,6 +13,7 @@ import { } from '../../modules/chartTypes'; import { sGetUiType } from '../../reducers/ui'; import { sGetCurrent } from '../../reducers/current'; +import { sGetMetadata } from '../../reducers/metadata'; import { acSetUiType } from '../../actions/ui'; import { apiSaveAOInUserDataStore, @@ -55,7 +56,8 @@ export class VisualizationTypeSelector extends Component { handleOpenChartAsMapClick = async () => { const currentAnalyticalObject = prepareCurrentAnalyticalObject( - this.props.current + this.props.current, + this.props.metadata ); await apiSaveAOInUserDataStore(currentAnalyticalObject); @@ -138,6 +140,8 @@ export class VisualizationTypeSelector extends Component { VisualizationTypeSelector.propTypes = { visualizationType: PropTypes.oneOf(Object.keys(chartTypeDisplayNames)), + current: PropTypes.object, + metadata: PropTypes.object, }; VisualizationTypeSelector.contextTypes = { @@ -147,6 +151,7 @@ VisualizationTypeSelector.contextTypes = { const mapStateToProps = state => ({ visualizationType: sGetUiType(state), current: sGetCurrent(state), + metadata: sGetMetadata(state), }); const mapDispatchToProps = dispatch => ({ From c82bc6a82b9a2392279ae722cdf93bc1a31267d5 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Tue, 19 Feb 2019 17:03:54 +0100 Subject: [PATCH 20/35] Set column as default chart type --- packages/app/src/modules/chartTypes.js | 1 + packages/app/src/modules/ui.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/modules/chartTypes.js b/packages/app/src/modules/chartTypes.js index fb462051aa..a9950ea590 100644 --- a/packages/app/src/modules/chartTypes.js +++ b/packages/app/src/modules/chartTypes.js @@ -33,6 +33,7 @@ const stackedTypes = [STACKED_COLUMN, STACKED_BAR, AREA]; const yearOverYearTypes = [YEAR_OVER_YEAR_LINE, YEAR_OVER_YEAR_COLUMN]; const openAsTypes = [OPEN_AS_GEO_MAP]; +export const defaultChartType = COLUMN; export const isStacked = type => stackedTypes.includes(type); export const isYearOverYear = type => yearOverYearTypes.includes(type); export const isOpenAsType = type => openAsTypes.includes(type); diff --git a/packages/app/src/modules/ui.js b/packages/app/src/modules/ui.js index 200d1531cf..d03961b95a 100644 --- a/packages/app/src/modules/ui.js +++ b/packages/app/src/modules/ui.js @@ -3,6 +3,7 @@ import { YEAR_OVER_YEAR_COLUMN, PIE, GAUGE, + defaultChartType, } from './chartTypes'; import { getDimensionIdsByAxis, getItemIdsByDimension } from './layout'; import { FIXED_DIMENSIONS } from './fixedDimensions'; @@ -16,7 +17,7 @@ const peId = FIXED_DIMENSIONS.pe.id; // Transform from backend model to store.ui format export const getUiFromVisualization = (vis, currentState = {}) => ({ ...currentState, - type: vis.type, + type: vis.type || defaultChartType, options: getOptionsFromVisualization(vis), layout: getDimensionIdsByAxis(vis), itemsByDimension: getItemIdsByDimension(vis), From d2c44e5df2164c5e3bdd21475554bb06414edd6d Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Tue, 19 Feb 2019 18:05:09 +0100 Subject: [PATCH 21/35] Get parent graph map from visualization function --- packages/app/src/modules/layout.js | 1 + packages/app/src/modules/ui.js | 30 ++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/app/src/modules/layout.js b/packages/app/src/modules/layout.js index 0a6f4bc4ba..f01c4fb53b 100644 --- a/packages/app/src/modules/layout.js +++ b/packages/app/src/modules/layout.js @@ -66,6 +66,7 @@ export const getFilteredLayout = (layout, excludedIds) => { export const getItemIdsByDimension = visualization => { const dimensions = getAllDimensions(visualization); + return dimensions.reduce( (map, dim) => ({ ...map, diff --git a/packages/app/src/modules/ui.js b/packages/app/src/modules/ui.js index d03961b95a..fd12b98515 100644 --- a/packages/app/src/modules/ui.js +++ b/packages/app/src/modules/ui.js @@ -5,7 +5,11 @@ import { GAUGE, defaultChartType, } from './chartTypes'; -import { getDimensionIdsByAxis, getItemIdsByDimension } from './layout'; +import { + getDimensionIdsByAxis, + getInverseLayout, + getItemIdsByDimension, +} from './layout'; import { FIXED_DIMENSIONS } from './fixedDimensions'; import { isYearOverYear } from './chartTypes'; import { getOptionsFromVisualization } from './options'; @@ -21,7 +25,10 @@ export const getUiFromVisualization = (vis, currentState = {}) => ({ options: getOptionsFromVisualization(vis), layout: getDimensionIdsByAxis(vis), itemsByDimension: getItemIdsByDimension(vis), - parentGraphMap: vis.parentGraphMap || currentState.parentGraphMap, + parentGraphMap: + vis.parentGraphMap || + getParentGraphMapFromVisualization(vis) || + currentState.parentGraphMap, yearOverYearSeries: isYearOverYear(vis.type) && vis[BASE_FIELD_YEARLY_SERIES] ? vis[BASE_FIELD_YEARLY_SERIES] @@ -65,3 +72,22 @@ export const getAdaptedUiByType = ui => { return ui; } }; + +export const getParentGraphMapFromVisualization = vis => { + const ouId = FIXED_DIMENSIONS.ou.id; + const dimensionIdsByAxis = getDimensionIdsByAxis(vis); + const inverseLayout = getInverseLayout(dimensionIdsByAxis); + const ouAxis = inverseLayout[ouId]; + const ouDimension = vis[ouAxis].find( + dimension => dimension.dimension === ouId + ); + const parentGraphMap = {}; + + ouDimension.items + .filter(orgUnit => orgUnit.path) + .forEach(orgUnit => { + parentGraphMap[orgUnit.id] = orgUnit.path; + }); + + return parentGraphMap; +}; From ab710506bbcb6e1a2da4bbbf80f5f8959e57dd7a Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Tue, 19 Feb 2019 18:16:29 +0100 Subject: [PATCH 22/35] Open as map confirmation dialog component --- .../OpenAsMapConfirmationDialog.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/app/src/components/VisualizationTypeSelector/OpenAsMapConfirmationDialog.js diff --git a/packages/app/src/components/VisualizationTypeSelector/OpenAsMapConfirmationDialog.js b/packages/app/src/components/VisualizationTypeSelector/OpenAsMapConfirmationDialog.js new file mode 100644 index 0000000000..f57eb4ff84 --- /dev/null +++ b/packages/app/src/components/VisualizationTypeSelector/OpenAsMapConfirmationDialog.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Button from '@material-ui/core/Button/Button'; +import Dialog from '@material-ui/core/Dialog/Dialog'; +import DialogActions from '@material-ui/core/DialogActions/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle/DialogTitle'; + +const OpenAsMapConfirmationDialog = ({ + open, + toggleDialog, + handleOpenChartAsMapClick, +}) => ( + + Open as: Geo Map + + + Only a single data dimension will be sent to maps, data element + that comes first in the chart data. You can + reorder data elements in the 'Data' modal. + + + + + + + +); + +OpenAsMapConfirmationDialog.propTypes = { + open: PropTypes.bool.isRequired, + toggleDialog: PropTypes.func.isRequired, + handleOpenChartAsMapClick: PropTypes.func.isRequired, +}; + +export default OpenAsMapConfirmationDialog; From c196e27111a6869bcec7d787859a50c145081e14 Mon Sep 17 00:00:00 2001 From: Ilya Nee Date: Tue, 19 Feb 2019 18:17:17 +0100 Subject: [PATCH 23/35] Show confirmation dialog if user opens chart as map and chart has multiple data (dx) items --- .../VisualizationTypeSelector.js | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index 953200771c..aa49de2c8d 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -2,16 +2,17 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import Button from '@material-ui/core/Button'; import Menu from '@material-ui/core/Menu'; -import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import { chartTypeDisplayNames, isOpenAsType, OPEN_AS_GEO_MAP, } from '../../modules/chartTypes'; -import { sGetUiType } from '../../reducers/ui'; +import { FIXED_DIMENSIONS } from '../../modules/fixedDimensions'; +import { sGetUi, sGetUiType } from '../../reducers/ui'; import { sGetCurrent } from '../../reducers/current'; import { sGetMetadata } from '../../reducers/metadata'; import { acSetUiType } from '../../actions/ui'; @@ -23,14 +24,22 @@ import { import VisualizationTypeMenuItem from './VisualizationTypeMenuItem'; import VisualizationTypeIcon from './VisualizationTypeIcon'; import styles from './styles/VisualizationTypeSelector.style'; +import OpenAsMapConfirmationDialog from './OpenAsMapConfirmationDialog'; + +const dxId = FIXED_DIMENSIONS.dx.id; export const MAPS_APP_URL = 'dhis-web-maps'; +export const defaultState = { + anchorEl: null, + openAsMapConfirmationDialogOpen: false, +}; + export class VisualizationTypeSelector extends Component { constructor(props, context) { super(props); - this.state = { anchorEl: null }; + this.state = defaultState; this.baseUrl = context.baseUrl; this.chartTypes = this.getChartTypes(); } @@ -39,21 +48,21 @@ export class VisualizationTypeSelector extends Component { this.setState({ anchorEl: event.currentTarget }); }; - handleMenuItemClick = type => event => { + handleMenuItemClick = type => () => { this.props.onTypeSelect(type); this.handleClose(); }; - handleOpenAsMenuItemClick = type => event => { + handleOpenAsMenuItemClick = type => () => { if (type === OPEN_AS_GEO_MAP) { - this.handleOpenChartAsMapClick(); + if (this.shouldConfirmOpenAsMap()) { + this.toggleOpenAsMapConfirmationDialogOpen(); + } else { + this.handleOpenChartAsMapClick(); + } } }; - handleClose = () => { - this.setState({ anchorEl: null }); - }; - handleOpenChartAsMapClick = async () => { const currentAnalyticalObject = prepareCurrentAnalyticalObject( this.props.current, @@ -67,6 +76,14 @@ export class VisualizationTypeSelector extends Component { }/${MAPS_APP_URL}?currentAnalyticalObject=true`; }; + handleClose = () => { + this.setState({ anchorEl: null }); + }; + + shouldConfirmOpenAsMap = () => { + return this.props.ui.itemsByDimension[dxId].length > 1; + }; + getChartTypes = () => { return Object.keys(chartTypeDisplayNames).reduce( (result, type) => { @@ -82,13 +99,25 @@ export class VisualizationTypeSelector extends Component { ); }; + toggleOpenAsMapConfirmationDialogOpen = () => { + this.setState({ + openAsMapConfirmationDialogOpen: !this.state + .openAsMapConfirmationDialogOpen, + }); + }; + render() { - const { anchorEl } = this.state; + const { anchorEl, openAsMapConfirmationDialogOpen } = this.state; const { visualizationType } = this.props; const { nativeTypes, openAsTypes } = this.chartTypes; return ( + - - - -); - -OpenAsMapConfirmationDialog.propTypes = { - open: PropTypes.bool.isRequired, - toggleDialog: PropTypes.func.isRequired, - handleOpenChartAsMapClick: PropTypes.func.isRequired, -}; - -export default OpenAsMapConfirmationDialog; diff --git a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index aa49de2c8d..735b3ab9f5 100644 --- a/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/packages/app/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -11,8 +11,7 @@ import { isOpenAsType, OPEN_AS_GEO_MAP, } from '../../modules/chartTypes'; -import { FIXED_DIMENSIONS } from '../../modules/fixedDimensions'; -import { sGetUi, sGetUiType } from '../../reducers/ui'; +import { sGetUiType } from '../../reducers/ui'; import { sGetCurrent } from '../../reducers/current'; import { sGetMetadata } from '../../reducers/metadata'; import { acSetUiType } from '../../actions/ui'; @@ -24,15 +23,11 @@ import { import VisualizationTypeMenuItem from './VisualizationTypeMenuItem'; import VisualizationTypeIcon from './VisualizationTypeIcon'; import styles from './styles/VisualizationTypeSelector.style'; -import OpenAsMapConfirmationDialog from './OpenAsMapConfirmationDialog'; - -const dxId = FIXED_DIMENSIONS.dx.id; export const MAPS_APP_URL = 'dhis-web-maps'; export const defaultState = { anchorEl: null, - openAsMapConfirmationDialogOpen: false, }; export class VisualizationTypeSelector extends Component { @@ -55,11 +50,7 @@ export class VisualizationTypeSelector extends Component { handleOpenAsMenuItemClick = type => () => { if (type === OPEN_AS_GEO_MAP) { - if (this.shouldConfirmOpenAsMap()) { - this.toggleOpenAsMapConfirmationDialogOpen(); - } else { - this.handleOpenChartAsMapClick(); - } + this.handleOpenChartAsMapClick(); } }; @@ -80,10 +71,6 @@ export class VisualizationTypeSelector extends Component { this.setState({ anchorEl: null }); }; - shouldConfirmOpenAsMap = () => { - return this.props.ui.itemsByDimension[dxId].length > 1; - }; - getChartTypes = () => { return Object.keys(chartTypeDisplayNames).reduce( (result, type) => { @@ -99,25 +86,13 @@ export class VisualizationTypeSelector extends Component { ); }; - toggleOpenAsMapConfirmationDialogOpen = () => { - this.setState({ - openAsMapConfirmationDialogOpen: !this.state - .openAsMapConfirmationDialogOpen, - }); - }; - render() { - const { anchorEl, openAsMapConfirmationDialogOpen } = this.state; + const { anchorEl } = this.state; const { visualizationType } = this.props; const { nativeTypes, openAsTypes } = this.chartTypes; return ( -