From bc66f0a755759a85556be4e00d393faa145437b4 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 19 Dec 2023 11:12:40 +0100 Subject: [PATCH 01/31] feat: add outliers table vis type + layout (DHIS2-16344) --- i18n/en.pot | 7 +- package.json | 5 +- src/assets/OutlierTableIcon.js | 37 + src/components/Layout/Layout.js | 5 +- .../Layout/OutlierTable/OutlierTableLayout.js | 30 + .../styles/OutlierTableLayout.style.js | 5 + .../VisualizationTypeSelector/ListItemIcon.js | 4 + src/modules/layoutValidation.js | 15 + src/modules/visualization.js | 5 + yarn.lock | 850 +++++++++--------- 10 files changed, 534 insertions(+), 429 deletions(-) create mode 100644 src/assets/OutlierTableIcon.js create mode 100644 src/components/Layout/OutlierTable/OutlierTableLayout.js create mode 100644 src/components/Layout/OutlierTable/styles/OutlierTableLayout.style.js diff --git a/i18n/en.pot b/i18n/en.pot index 10985d66d0..a59842c7f6 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-11-13T12:11:28.959Z\n" -"PO-Revision-Date: 2023-11-13T12:11:28.959Z\n" +"POT-Creation-Date: 2023-12-19T14:19:00.918Z\n" +"PO-Revision-Date: 2023-12-19T14:19:00.918Z\n" msgid "All items" msgstr "All items" @@ -1117,6 +1117,9 @@ msgstr "" "View the relationship between two data items at a place or time. " "Recommended for finding outliers." +msgid "Automatically identify extreme outliers in the data" +msgstr "Automatically identify extreme outliers in the data" + msgid "Weeks per year" msgstr "Weeks per year" diff --git a/package.json b/package.json index 2151d1306e..38b89724a2 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", "@dhis2/d2-i18n": "^1.1.0", - "@dhis2/ui": "^8.7.6", + "@dhis2/ui": "^9.2.0", "@krakenjs/post-robot": "^11.0.0", "d2": "^31.9.1", "decode-uri-component": "^0.2.2", @@ -66,5 +66,8 @@ "reselect": "^4.1.7", "styled-jsx": "^4", "whatwg-fetch": "^3.6.2" + }, + "resolutions": { + "@dhis2/ui": "^9.2.0" } } diff --git a/src/assets/OutlierTableIcon.js b/src/assets/OutlierTableIcon.js new file mode 100644 index 0000000000..1e0bca1e60 --- /dev/null +++ b/src/assets/OutlierTableIcon.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const OutlierTableIcon = ({ + style = { paddingRight: '8px', width: 24, height: 24 }, +}) => ( + + + + + + + + + +) + +OutlierTableIcon.propTypes = { + style: PropTypes.object, +} + +export default OutlierTableIcon diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index dc9793b30c..d830d24126 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -3,14 +3,16 @@ import { LAYOUT_TYPE_PIE, LAYOUT_TYPE_YEAR_OVER_YEAR, LAYOUT_TYPE_PIVOT_TABLE, - getLayoutTypeByVisType, LAYOUT_TYPE_SCATTER, + LAYOUT_TYPE_OUTLIER_TABLE, + getLayoutTypeByVisType, } from '@dhis2/analytics' import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' import { sGetUiType } from '../../reducers/ui.js' import DefaultLayout from './DefaultLayout/DefaultLayout.js' +import OutlierTableLayout from './OutlierTable/OutlierTableLayout.js' import PieLayout from './PieLayout/PieLayout.js' import PivotTableLayout from './PivotTableLayout/PivotTableLayout.js' import ScatterLayout from './ScatterLayout/ScatterLayout.js' @@ -22,6 +24,7 @@ const componentMap = { [LAYOUT_TYPE_YEAR_OVER_YEAR]: YearOverYearLayout, [LAYOUT_TYPE_PIVOT_TABLE]: PivotTableLayout, [LAYOUT_TYPE_SCATTER]: ScatterLayout, + [LAYOUT_TYPE_OUTLIER_TABLE]: OutlierTableLayout, } const Layout = ({ visType }) => { diff --git a/src/components/Layout/OutlierTable/OutlierTableLayout.js b/src/components/Layout/OutlierTable/OutlierTableLayout.js new file mode 100644 index 0000000000..ff079ef825 --- /dev/null +++ b/src/components/Layout/OutlierTable/OutlierTableLayout.js @@ -0,0 +1,30 @@ +import { AXIS_ID_COLUMNS } from '@dhis2/analytics' +import React from 'react' +import DefaultAxis from '../DefaultLayout/DefaultAxis.js' +import defaultAxisStyles from '../DefaultLayout/styles/DefaultAxis.style.js' +import defaultLayoutStyles from '../DefaultLayout/styles/DefaultLayout.style.js' +import outlierTableLayoutStyles from './styles/OutlierTableLayout.style.js' + +const Layout = () => ( +
+
+ +
+
+) + +Layout.displayName = 'Layout' + +export default Layout diff --git a/src/components/Layout/OutlierTable/styles/OutlierTableLayout.style.js b/src/components/Layout/OutlierTable/styles/OutlierTableLayout.style.js new file mode 100644 index 0000000000..c8c3cbabf5 --- /dev/null +++ b/src/components/Layout/OutlierTable/styles/OutlierTableLayout.style.js @@ -0,0 +1,5 @@ +export default { + axisGroupLeft: { + flexBasis: '100%', + }, +} diff --git a/src/components/VisualizationTypeSelector/ListItemIcon.js b/src/components/VisualizationTypeSelector/ListItemIcon.js index 7599c6f048..9fcc741b1e 100644 --- a/src/components/VisualizationTypeSelector/ListItemIcon.js +++ b/src/components/VisualizationTypeSelector/ListItemIcon.js @@ -14,6 +14,7 @@ import { VIS_TYPE_SINGLE_VALUE, VIS_TYPE_PIVOT_TABLE, VIS_TYPE_SCATTER, + VIS_TYPE_OUTLIER_TABLE, } from '@dhis2/analytics' import PropTypes from 'prop-types' import React from 'react' @@ -23,6 +24,7 @@ import ColumnIcon from '../../assets/ColumnIcon.js' import GaugeIcon from '../../assets/GaugeIcon.js' import GlobeIcon from '../../assets/GlobeIcon.js' import LineIcon from '../../assets/LineIcon.js' +import OutlierTableIcon from '../../assets/OutlierTableIcon.js' import PieIcon from '../../assets/PieIcon.js' import PivotTableIcon from '../../assets/PivotTableIcon.js' import RadarIcon from '../../assets/RadarIcon.js' @@ -66,6 +68,8 @@ const ListItemIcon = ({ iconType, style }) => { return case VIS_TYPE_SCATTER: return + case VIS_TYPE_OUTLIER_TABLE: + return case VIS_TYPE_COLUMN: default: return diff --git a/src/modules/layoutValidation.js b/src/modules/layoutValidation.js index c2d9776abb..9a7fdf3c2e 100644 --- a/src/modules/layoutValidation.js +++ b/src/modules/layoutValidation.js @@ -8,6 +8,7 @@ import { VIS_TYPE_SINGLE_VALUE, VIS_TYPE_PIVOT_TABLE, VIS_TYPE_SCATTER, + VIS_TYPE_OUTLIER_TABLE, getPredefinedDimensionProp, dimensionIsValid, layoutGetDimension, @@ -128,6 +129,18 @@ const validateScatterLayout = (layout) => { ) } +const validateOutlierTableLayout = (layout) => { + validateDimension( + layoutGetDimension(layout, DIMENSION_ID_DATA), + new NoDataError(layout.type) + ) + + validateDimension( + layoutGetDimension(layout, DIMENSION_ID_PERIOD), + new NoPeriodError(layout.type) + ) +} + export const validateLayout = (layout) => { switch (layout.type) { case VIS_TYPE_PIE: @@ -142,6 +155,8 @@ export const validateLayout = (layout) => { return validatePivotTableLayout(layout) case VIS_TYPE_SCATTER: return validateScatterLayout(layout) + case VIS_TYPE_OUTLIER_TABLE: + return validateOutlierTableLayout(layout) default: return validateDefaultLayout(layout) } diff --git a/src/modules/visualization.js b/src/modules/visualization.js index a762cbc9b1..d3319e4c17 100644 --- a/src/modules/visualization.js +++ b/src/modules/visualization.js @@ -14,6 +14,7 @@ import { VIS_TYPE_YEAR_OVER_YEAR_COLUMN, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_SCATTER, + VIS_TYPE_OUTLIER_TABLE, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import { DEFAULT_CURRENT } from '../reducers/current.js' @@ -36,6 +37,7 @@ export const visTypes = [ VIS_TYPE_YEAR_OVER_YEAR_COLUMN, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_SCATTER, + VIS_TYPE_OUTLIER_TABLE, ] export const getVisTypeDescriptions = () => ({ @@ -84,6 +86,9 @@ export const getVisTypeDescriptions = () => ({ [VIS_TYPE_SCATTER]: i18n.t( 'View the relationship between two data items at a place or time. Recommended for finding outliers.' ), + [VIS_TYPE_OUTLIER_TABLE]: i18n.t( + 'Automatically identify extreme outliers in the data' + ), }) export const getVisualizationFromCurrent = (current) => { diff --git a/yarn.lock b/yarn.lock index bdccf01e0b..72207fe9e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1448,583 +1448,583 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@dhis2-ui/alert@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/alert/-/alert-8.12.4.tgz#bb0b39a7d364661c5c6b4cfdd1611d9732ba18d9" - integrity sha512-YM5tUF728fYZUJvWJP1gKvdHxuXRPMCrIs17tBGO6IYfuproMwUklmnSXE8oIN4yOXqd4b4RkKLNOJsPDbcvfQ== +"@dhis2-ui/alert@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/alert/-/alert-9.2.0.tgz#3b30e90a88a96c617219c60ad8bc16a5969e85d1" + integrity sha512-9uh6vIKlj9vm9wOr/7UvSvxrEgl0p99+FdfFtvCeiOywR2+CCPvd9fRsL/vWKcCraopOriCmTm5PzBUWPuatvg== dependencies: - "@dhis2-ui/portal" "8.12.4" + "@dhis2-ui/portal" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/box@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/box/-/box-8.12.4.tgz#f28202880bb27d277cc7c73714325dafaf38e699" - integrity sha512-OqIcTQCoUHXM9vjAs4X3oQD+oravJoDs2HkeFu5w3XH5E+TrqkUVhcHjHNN8LNLQgC1YQv7qyU/Q21Cra21zMQ== +"@dhis2-ui/box@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/box/-/box-9.2.0.tgz#0f8fd79059d724c075180ea442c860ebff88c802" + integrity sha512-ERNEd8lDAQIGDmTYknWClPGbmWuOpFAnE8XurB6wrkydn0M2+wsIa00q2on6RgY2YaAjVtYZEXr9CfjBtwh04w== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/button@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/button/-/button-8.12.4.tgz#61b574c24741edaf31027a6563f7b030759a4bb1" - integrity sha512-0zpjbbR5neFpsTD/pFStsjjSqBccM0nNAaN8n4fbJAjgmjlzrCf8ERFd1KA6YuWZgKKt/F3sF5qoAKAm+UJ4uw== +"@dhis2-ui/button@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/button/-/button-9.2.0.tgz#8d465f471ca629e1cb0ded01fc68143eb4960c1c" + integrity sha512-YBdIpaOqEgpI0JhHm8XBlpFGDF7O6eiFQ4Noxu131KEyDOf7a+AkIyECVZw6Jsj3n2zrbyzBDw6IndSFSlBqRQ== dependencies: - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/popper" "8.12.4" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/popper" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/calendar@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/calendar/-/calendar-8.12.4.tgz#b10a3066d4f5638939a7dc0ec3a431e37066c1fb" - integrity sha512-F8e6YUqh6RMFehJxI4XiTXkbnHhrOzdRismQageAr1nQmloQPYQEZabI/KVsCec8r0nH9Lrr3D/qNbzLx4irMg== +"@dhis2-ui/calendar@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/calendar/-/calendar-9.2.0.tgz#c3fbcdc622a13dd112de163719cfdcd0f960cfe2" + integrity sha512-6xxkCx66tjfGffGoPnDB6pe9Pvx+UybAGCqof7/0xfHP2vnfdss716imM02HfJRZwp5dNUrXqBO9uuQ1qkp/HA== dependencies: - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/popper" "8.12.4" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/popper" "9.2.0" "@dhis2/multi-calendar-dates" "1.0.2" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/card@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/card/-/card-8.12.4.tgz#e73b936c352dff26ce432adb09c12163ee20cc88" - integrity sha512-PTMVMTvfOAjP2/0AfDHk7iKFlJQkSv/7aE/X+zZz4l8ltWvxuQ6eLH6Az2BneVkkQBO6eYri1q9dVzf1CHpnjA== +"@dhis2-ui/card@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/card/-/card-9.2.0.tgz#1a2d8f2f9becfd9fd43e7e315fda511418698fd1" + integrity sha512-++eLieBc4WsNXD1iQEQaqijnQ7hpKu0HH9cjkwTa46jjI9SoJGdYInvCzZPafB1XuCZ42f08kTcdND2qZWbssQ== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/center@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/center/-/center-8.12.4.tgz#45b2a14b0d37d44026e2a6214713c7abbc740cd3" - integrity sha512-TAX9NnqojEx6Jex5HCGNOACOZ3Cb1EOEFbY++2OQqZDWDri4SMCz1EmTxo81XlJMwesXFhjlBOw5bRfxPlArsA== +"@dhis2-ui/center@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/center/-/center-9.2.0.tgz#f7fbcbd00696d718286f269a64a5bad327160449" + integrity sha512-IBwLld/SQEtT5acjBr3nJnpkXNzypxm9f+QfdGYRq2ZSQmkIv9IUjIvlC25CU72Ksq+N0nWvA8AYp2Qx/l04fg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/checkbox@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/checkbox/-/checkbox-8.12.4.tgz#9aa61bad20cde8de09918dc7a1d972a933fd0554" - integrity sha512-2oacza/ZkUDECPlInbNcyn3jfjSfbyVm0uKpzB/lMBnjdfK3UCsD4PvDPydjaUR6hox8Ae5vtBcRprwyOxx+Kg== +"@dhis2-ui/checkbox@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/checkbox/-/checkbox-9.2.0.tgz#7f27c645bb9d31df4898c9c96cfdabf9ddd01ab3" + integrity sha512-EIGbkdnCLOyW3s7Mh31LdPgghHPaTAt0MJe5ieF/1NycUCfx99NGoR7wXlAlI6wNvjkuTYnYBknERGHUEzY9rA== dependencies: - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/required" "8.12.4" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/required" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/chip@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/chip/-/chip-8.12.4.tgz#24bcb4bc9dcfc146a7dd535d5fe04dff1ea5c528" - integrity sha512-ryN9gOvD9IJHSqgf3xV4TlVRuEGoJ24yVNKa0PVXAEoYAzr/T3Mw7n5R8dCJs+Ykes1u1IcPPq9OTibSZF+3YA== +"@dhis2-ui/chip@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/chip/-/chip-9.2.0.tgz#45b5e55e6d11318bb6917f1bcdee9bf9b67bd620" + integrity sha512-PesbKz0MrXegDAFcGHq34Ast0kM/mJrmExiLua262egYYaRiQXDYeaNuS2TT+qPsPsDva2Pp7kqjdrFRDL2Few== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/cover@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/cover/-/cover-8.12.4.tgz#39534bd9e4a79d18f7701c2dbcc315abd505251f" - integrity sha512-tONUDufYfK4RhnWGPLp5tH5CrVgF3/MNyrGp/emsCtLnLz0hetUoBZctZPjKyL7IFH69PxohXooOCbTZ5jl8AA== +"@dhis2-ui/cover@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/cover/-/cover-9.2.0.tgz#0288dad0ba9caf8c0b77c86868fc4bae3388f6c7" + integrity sha512-ZxcOOuyy/dB4pgjdVzBlgwtVue0HEct2XOiuIKM66F5G0DPBqz6UtLDD+8VRofQwTKcTYQh9kPZPmx/9q65Zcg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/css@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/css/-/css-8.12.4.tgz#6113ac65629a6cfc079b1fe07cd03d19b27a0029" - integrity sha512-vMeQpeMziHAo92zWzNtpXjegXtFZFczZwTknc+xj2AkvBk0uJkbje7iyDXdAOO+okuVBhFtuFXSz7IqXC+4TgA== +"@dhis2-ui/css@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/css/-/css-9.2.0.tgz#ad881b90371c7506690ee2a2b2151c09f793e368" + integrity sha512-dBe9S75+Nj1c9SNRkxU0VWTVwFZJ+vLFKxD1UYaPdbJ2DHD6AD4UJ1YtXgvBvgiJ0wbT1vesh3tKfEUXbnTmkw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/divider@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/divider/-/divider-8.12.4.tgz#8b757573e2af5ff6d74f3beaf6e8f06bc2494874" - integrity sha512-1zp0H1su9M2UVzDNHJWxl+uj3zy2/T8QxZMhNO99y+7QXKZFSFT/7x4ZDxBTGcePVMA1RyQ3FhmI0TaJHXv7HQ== +"@dhis2-ui/divider@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/divider/-/divider-9.2.0.tgz#2f9ea2f3a1f89853963bcffdeef27e5ebc390021" + integrity sha512-z87v4XKIO1IXhWmHYhCQgR7MTiuU+zLMg9Py2OIDxMchVXrdyGSeKCpL9UgOzl/jtHxwoMdnVDzy8sLLzITgsw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/field@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/field/-/field-8.12.4.tgz#f0e3251cad00acb32ea808d12812a1959639a4ea" - integrity sha512-wHkBPc4FaGAvGLnNMktzKs8aHaVByAi0BBjzvG9iqMAz4t7LKjltJ+CBFqGZyrBBc7/JExUOPOB6+6YGn3r8Lw== +"@dhis2-ui/field@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/field/-/field-9.2.0.tgz#d16b443ff168e352b6f5168d2179975377902dd1" + integrity sha512-pZr1LkzOm4TOPdPAen9e3RHQE8Y1uCbdFhP1oc5vg44etmudX7stJuEXP6/DrPm6sHCJRojf90laqPai1vfrHg== dependencies: - "@dhis2-ui/box" "8.12.4" - "@dhis2-ui/help" "8.12.4" - "@dhis2-ui/label" "8.12.4" + "@dhis2-ui/box" "9.2.0" + "@dhis2-ui/help" "9.2.0" + "@dhis2-ui/label" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/file-input@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/file-input/-/file-input-8.12.4.tgz#e5cd0e0081d9fd75a316b57f2d1d830466d1cca4" - integrity sha512-zA71nMufBq6Ch1zMy+xiHooJV+ETBYpSJRLDTCvgFZ+G8NzXke7Pqryc8HPJFEQChWI9w+ihsNUNwaoGWZs16Q== +"@dhis2-ui/file-input@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/file-input/-/file-input-9.2.0.tgz#d48e9d290aea798640b251d915b9a8f51abcf363" + integrity sha512-x+lKilo6aQN320fIAPZMAq13PxKuTb/jhZb7m7pMS0HKsPqfT5rWm1VfmBlN/02BkKsmhsvo/tTe9vxpAic71A== dependencies: - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/label" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/status-icon" "8.12.4" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/label" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/status-icon" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/header-bar@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/header-bar/-/header-bar-8.12.4.tgz#80ff0fa89957cb6ed2a623f0d95464a3453c0451" - integrity sha512-0JdbpL4eNVzBO+n4b0ozkvyKvlQE5TVGL9P/T2Gf+FbhQSkXc2NIbvIguozgEdbOXkEnScRCLAKx9K7Ot2iSuQ== - dependencies: - "@dhis2-ui/box" "8.12.4" - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/center" "8.12.4" - "@dhis2-ui/divider" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/logo" "8.12.4" - "@dhis2-ui/menu" "8.12.4" - "@dhis2-ui/modal" "8.12.4" - "@dhis2-ui/user-avatar" "8.12.4" +"@dhis2-ui/header-bar@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/header-bar/-/header-bar-9.2.0.tgz#83efe93821318decca3bd1a28d30fd3089435b6d" + integrity sha512-7iNKOllQNeYfa9nJjXVqiohcuKEk+RMhuu/7BY8g2oPmwbvS0uc49KiL53cFfAWBST/v5fU1XOpWS/N7PmJr2g== + dependencies: + "@dhis2-ui/box" "9.2.0" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/center" "9.2.0" + "@dhis2-ui/divider" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/logo" "9.2.0" + "@dhis2-ui/menu" "9.2.0" + "@dhis2-ui/modal" "9.2.0" + "@dhis2-ui/user-avatar" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" moment "^2.29.1" prop-types "^15.7.2" -"@dhis2-ui/help@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/help/-/help-8.12.4.tgz#e5d1c4193abf43ad0ac4182516992f10ab83a564" - integrity sha512-fZYZzu5waXz7yuijQiXYrGRYJk6xCvy8HdIKznqjiW4mpKxpd6Gh8nzpYgxz1j9WHgIjIZqj4/sNkMQ2eC4zOw== +"@dhis2-ui/help@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/help/-/help-9.2.0.tgz#76556c0d581fb0f7611fe5c5bb0d9adf35ad0c8e" + integrity sha512-qD3oNEwEb+pT7jsD4ciHtu16KrxMySPWoqco5nJwoGbcZFLw/caEfkBo2IroImD0MxtI0mKt5emD7V2yXRWm7A== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/input@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/input/-/input-8.12.4.tgz#57645a19bf2060c60a2f8280e292ab7ec71b9e02" - integrity sha512-eJu2UmbEOq0IWDf0kFRVOucP31nJtRhUQSXWp+1PkpmmpOXitItqjdxO+IgUs9WSiOBW3HDRtGl40V7pdHFnJQ== +"@dhis2-ui/input@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/input/-/input-9.2.0.tgz#bdf7c72e11b818ed86e1e6335cd373ae037f4ef3" + integrity sha512-0bzF/8pZSMqe5ZN2v0t0/rMTvKWd9kl5MDOy9fRXpX6yoFgfH+j+iIU06eVyqJl3DMqCdInfapJvpJR7MHvd+g== dependencies: - "@dhis2-ui/box" "8.12.4" - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/status-icon" "8.12.4" + "@dhis2-ui/box" "9.2.0" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/status-icon" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/intersection-detector@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/intersection-detector/-/intersection-detector-8.12.4.tgz#94ff379c170d29c43e6c2f4aa21f675f94fd5e5f" - integrity sha512-jW3N2jvbmQIuKmrAKJxAIqb/NmlwXwmNPrN+yXGWCLuuDh1rx8srziGfBxQDaHzjLSZT4AO8596b/Z5TxzTvrA== +"@dhis2-ui/intersection-detector@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/intersection-detector/-/intersection-detector-9.2.0.tgz#31f19bfb8645ebda7d7af6f6e641b40fa1e57888" + integrity sha512-erBoDMhOPmua8eP8bKJNl4WIUUm+Fw3Jj9M0OxC+xia3/Fi8scLUyk0Yek1Y0lGdb4YHJEXqx475dLNjpmiLRQ== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/label@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/label/-/label-8.12.4.tgz#3b1ec6759db4dee9ed8bfcbd3bd4f2c4a69a5c47" - integrity sha512-c+2PM/ijhuHAaFZTo+FxhN2gSZxgI6YsYpn6Nx9SZNBc7YR9gKvdxjSY1xsuD0Q6y1gRmhyjYK8ZatAYgLoJIw== +"@dhis2-ui/label@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/label/-/label-9.2.0.tgz#81ec2de2ea1b9bc7cf25eb8bb036d451d5b61748" + integrity sha512-k9Q4XyIbaNRvCn1+rLcEb/iDi479S3fOEJ2MCT6wsGxr1+Hys7yVw7Ggq0OQ9SLCEwElNQcvj+vWB4dZltXl5Q== dependencies: - "@dhis2-ui/required" "8.12.4" + "@dhis2-ui/required" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/layer@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/layer/-/layer-8.12.4.tgz#8db65c8d5e9f5a35e1e5b897fe7a4cc67d05272b" - integrity sha512-pYnlgVcsGLmJo13H/mfG5SGoNPRtyfVKZY0nUExT1iDJ3A5nT2g8qfw+O38zAK+JBTCGC1yk6M6h4xXwkDOXnw== +"@dhis2-ui/layer@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/layer/-/layer-9.2.0.tgz#7afcb2ef8571eb9c839fc522eb76ead70e395dbc" + integrity sha512-95aFgQYxgJ7GmWY6AOSoAH4BH7wkIyUAioAIRUDwp0mmSJhJxG6P9b1PFqw4koX1zV4/RLoz+NiQ7Twv+03CLQ== dependencies: - "@dhis2-ui/portal" "8.12.4" + "@dhis2-ui/portal" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/legend@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/legend/-/legend-8.12.4.tgz#cd831e98399ffffa6430c93da2831b432f75f0d5" - integrity sha512-Lmss3ITK2xyVHYvPFuWjbWto36+QqETqQR1/k4wPy5UkyedkTVbpX4SHyRv/x2m3To+X/NKB/JNomUn7tqHVYQ== +"@dhis2-ui/legend@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/legend/-/legend-9.2.0.tgz#a051822fd67c1442b6986a86097aca94f3960dd2" + integrity sha512-IfJhu48eu//O9AlHt7IUdsv5E92XG7v/95laFfyQOaGhf1C4BQf11s6yXc3nTFoil/p55oAsZnWp5e7UXcgrhg== dependencies: - "@dhis2-ui/required" "8.12.4" + "@dhis2-ui/required" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/loader@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/loader/-/loader-8.12.4.tgz#e24219c803c38fef57f8808d2c3d281be63edd1d" - integrity sha512-c5ipPqzd6oPyRfTwE7HGGuDCud6Vsw7dkhDE/WnSDOVhxEYaqDqDU6PoNjRZY1el9uG9RV/dPtpPx4qbxX5mVw== +"@dhis2-ui/loader@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/loader/-/loader-9.2.0.tgz#f45fdb19a37182351932cd3e11980d43b4895d40" + integrity sha512-M8tuLE5kgm7oHmIN6par8GfRDpmt+DXFU3cCSZdiYIUM6SQSD8G5LmA8AaIHR5h7l430d93vcw7RQL3eeE/svw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/logo@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/logo/-/logo-8.12.4.tgz#269c4344ab4dd10bda324548c4e731372912d6d9" - integrity sha512-8dH5uaKxB0IFtNXa3T7/NsjsXWKg3nQMQ0V6iUtuPOyJDUgk0D04t0c3NggRPczVWNNc7B+9nEJuEcOVNgc56Q== +"@dhis2-ui/logo@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/logo/-/logo-9.2.0.tgz#0452aadd8e92afc1c558869a08c8e5d1f14ff55f" + integrity sha512-OEFrSpDijeCIhLjJ8vipDGRdihTgj8+iLcPLDrOeRAurlgJZJZiicxBKSuY6uc6p1/9QPccqX2huJjM1uNqo2g== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/menu@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/menu/-/menu-8.12.4.tgz#6358ae12bde6d6897415fb2237a3155fc7e171c8" - integrity sha512-JWy3iiBvXSCrrOXZV92glzBIxCYKEqG1e8Zl2rXa8z58zxN2RCg1apb08p48acNFC7f/PpW/kdQW6rePXo/faQ== +"@dhis2-ui/menu@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/menu/-/menu-9.2.0.tgz#996729545b29def9366a099005a17c781bdca8f6" + integrity sha512-8k15qmoqBWKo1Afj+QXFnDAIoDyAqosvq3j8M/+xM+cSn0H+6e3Q7UOp0ByVQlumoY5DyrT9Z7NojULIjFUifQ== dependencies: - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/divider" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/popper" "8.12.4" - "@dhis2-ui/portal" "8.12.4" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/divider" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/popper" "9.2.0" + "@dhis2-ui/portal" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/modal@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/modal/-/modal-8.12.4.tgz#ad42dc3dc4ae799fe9aff7ca344555bb4b31843d" - integrity sha512-yQhc08AtxLzar7UHIHdgKpWKL/gN4P76QDuKJkWvaD7O2BRfIudQ4ZSWdmzRnK+Qkbiz1xQL+nTNe8YQqU2WYA== +"@dhis2-ui/modal@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/modal/-/modal-9.2.0.tgz#7815efe62be4f482b2e2224ffe92981068b1bae9" + integrity sha512-gp+gCTmtoiLVAhigo1i6msE598qIGnkW6To+dTkUecvxyvni+DZTAulTmL62UtTzzjPjYO0yOqNTWQztbpj1KQ== dependencies: - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/center" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/portal" "8.12.4" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/center" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/portal" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/node@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/node/-/node-8.12.4.tgz#941f883dccdc181f6f3e730647aaed62e01cb784" - integrity sha512-8DUB95g/mJyRkVPvWgJIr74Cm15ECdRL2voa0NilXW3eAErQdtdQiiGUcwdCRjeMIRyAcD4PO6WEbHYad+Tmqg== +"@dhis2-ui/node@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/node/-/node-9.2.0.tgz#3560b3e7394d275c1bce3e800b2654cddcd2adb2" + integrity sha512-xx7P/6V7vq3JLXUUATKGGUCORHqQL74fsGYUd9a0izyUyq4h3pEHL9ZT6Cel+A0d5ODYn/j/Q6fHICZzg55FBQ== dependencies: - "@dhis2-ui/loader" "8.12.4" + "@dhis2-ui/loader" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/notice-box@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/notice-box/-/notice-box-8.12.4.tgz#edf83663f7d5410668915d3590b1d60971a8b74c" - integrity sha512-Q3QP0XRu2ry+QgA5QzS+lEFhU/FH8NWDonYhtGJRpT1fjcnOMfRE5UfOmRDWvGajSCWkic8PsvxzR+tbDbY3BA== +"@dhis2-ui/notice-box@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/notice-box/-/notice-box-9.2.0.tgz#8c1fb4a2a780fea0fde4b8f9eaf8f73957187c99" + integrity sha512-DY3WYXj1hsOsiBHGaNrOeZ8h7SPaXox6iMCTzL/jLvnfmTrH7wy6SHRLQYWg0BMrDflhMJu9qhn0jtzhEXZNMQ== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/organisation-unit-tree@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/organisation-unit-tree/-/organisation-unit-tree-8.12.4.tgz#8c694d2eeb9d76ddea0a3250c8cad33d325251ba" - integrity sha512-fDKSxVvq6udGh+58X+xwCz8jZH4lBdnH/kCPIe61Tlt5Ef8Zk5CRwt5tqiW1REcfRjCeidkXToaWmbfx5o3Bmw== +"@dhis2-ui/organisation-unit-tree@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/organisation-unit-tree/-/organisation-unit-tree-9.2.0.tgz#ce33d91361145d6574ddbef3866b56be06cee6cb" + integrity sha512-PHm908gNGPhq5D655BI4lrB+hMqfISKASjoFCWxG2f9FU64/pvQ+snZQQwQFMAJYMd6FKw4GOP1isKz0jTGNuA== dependencies: - "@dhis2-ui/checkbox" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/node" "8.12.4" + "@dhis2-ui/checkbox" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/node" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/pagination@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/pagination/-/pagination-8.12.4.tgz#390c9d0e243be6f7e884191025dfbdd23d0c62bf" - integrity sha512-U7zMVOCwSgmmobj++uYsmQGwMEa93UMgGBwDbTiQZ3d9akn4bqH3U2ZbEaR/KolxhvIx1aQtHba4zHePGLr/bg== +"@dhis2-ui/pagination@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/pagination/-/pagination-9.2.0.tgz#f38d1ada2bc9f9795dae8f86c52d54f29889dd17" + integrity sha512-jXJNQ8JOPweeMFCsPXgAb8dAx8J/rNTnExL8WA6rfRDWujOojLp8Gu2MrH5jlHRpCBWIl+aJO1I/ZKHekQOeDg== dependencies: - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/select" "8.12.4" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/select" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/popover@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/popover/-/popover-8.12.4.tgz#faa9bd6f3ac2c4ddbc9d553a6aff3e05138ccc83" - integrity sha512-YCyG9WzeghPtaZvylsbazEuFFMKnKcIrmBygAeyk2/V8YVDp5HBM25t8LenuD1N8Scuaxp5VHp/lUY4KiqkApA== +"@dhis2-ui/popover@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/popover/-/popover-9.2.0.tgz#3eccd6abde1da72089aa5feda5669ce4d1aa6f7e" + integrity sha512-7g8AHPrzUuMuv2MXpX5HdwiyO+peSGoq7hg6rHN/VDasIUvGS7vbaV4Xbxfd32fNmpHceBV7gMga31hRNaKtgw== dependencies: - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/popper" "8.12.4" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/popper" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/popper@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/popper/-/popper-8.12.4.tgz#6a8e154be0b4e608fdefafafbf28f854235a7bca" - integrity sha512-V6zQrlVV0yIwV9KaRQKw8LEwP80ahxpLpvXEIETVE1tAmCtHd9u+KbYG/xGU4VbfoXt1KF+cmU1gTUlq/z0Nmg== +"@dhis2-ui/popper@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/popper/-/popper-9.2.0.tgz#6d17ca49d7f0496289e11315d414095d93d179b5" + integrity sha512-6xWNvUQaDu8VE4rCa+uYOheb/4BD/52Cs23w2yt4lLAVrym4kV+0cnpHtlEG1ZuuVrK/yHelMjrmYxn6yJE41Q== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" "@popperjs/core" "^2.10.1" classnames "^2.3.1" prop-types "^15.7.2" react-popper "^2.2.5" resize-observer-polyfill "^1.5.1" -"@dhis2-ui/portal@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/portal/-/portal-8.12.4.tgz#6a854d015919cbd9fc6a8ab560191d03c275e94e" - integrity sha512-+hw6dBkn2csuDGAv1tRxV4rajyYQVg08sIn5jS7zeNWIER7YTrORVb1IbRChWrC3dvOe7SVMfQ8QwxgslemgQg== +"@dhis2-ui/portal@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/portal/-/portal-9.2.0.tgz#12435f3a8886de685fc9ee4540639810eabf0191" + integrity sha512-a5DqmTG+pn4y1aXZWr8wEGlK4xRqdvnOWJVqO/6aebEn4ZtcPJT6yyXouviAM2yoW7+fDj6TZhKrZo3vgMLHlA== dependencies: classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/radio@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/radio/-/radio-8.12.4.tgz#3adf0982c22ee59008f601531ef4b864eccb3d9a" - integrity sha512-ljH3d5XgB7oaL8EZVoWP0LMs2IcY4eVD7DhMtfUYwOtDSDqEDSioXNOh88JSZom1S8pD3mvqelxFdiMO62qLyg== +"@dhis2-ui/radio@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/radio/-/radio-9.2.0.tgz#9223ada90fe2d0eae375e85e0d99e6eb81d5269a" + integrity sha512-rjcz05spFlvRL8fnkO/7/ckznY4agQLl5P7UKSFL3Kz0KxvnocmounTX1BDsm6iQhKKt2HqJCYShpzPhTXavSA== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/required@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/required/-/required-8.12.4.tgz#3c153a850a5bb5f1cd6e3549fa2ea606a538cbd6" - integrity sha512-eUcSq8Lactw2g0fRA7mUFGQi6/fYEmiiGjPuEZAdupfkTZBPaLgwkyb7rXLyYmwVsQm8f00clam41tyO1nMwOQ== +"@dhis2-ui/required@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/required/-/required-9.2.0.tgz#8d76c914c3eb483958284c8ddf52ef7095cc2f42" + integrity sha512-cNoQct7gVbrAdzGDDFLnfBktmwmxAhD48oEZ6z4TE7IrPi2N+idqOiQCTEVKEWmuY7VGv9TV0I/1OLX6oiuSvA== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/segmented-control@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/segmented-control/-/segmented-control-8.12.4.tgz#1c1750c4f9e7f4b8e0473297eab0d46c35e4cbea" - integrity sha512-4Tqy128V567PrINT5/4Vuh1oe3A9TrkS+s3sncX/b7Mp7cqi+SWLwp1f8bUNg1oz2mDWsH1VosgSGLOiZw1xPw== +"@dhis2-ui/segmented-control@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/segmented-control/-/segmented-control-9.2.0.tgz#2b230eea17e35194072544ebbfc68e2c896fb951" + integrity sha512-x4eDqGu4JFfFPQk06mg8YdUCjYeYyXCLmZ0gbGj1Zx03gxpMwb4/nVyGoAxZg31/IVkhBOeAwXlvt+ckNXovgw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/select@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/select/-/select-8.12.4.tgz#91c48ead135e7f2d5ddbaad63a1dc03d9aee0d1a" - integrity sha512-bKyJNAMfqYBjlqd+diSDcfQBoW4S6etopTaJE0fpwYn/Sm3xOLhUrQoo7woipmbKa5W8Hjwvc/uq9NNBZBPKiA== - dependencies: - "@dhis2-ui/box" "8.12.4" - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/checkbox" "8.12.4" - "@dhis2-ui/chip" "8.12.4" - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/popper" "8.12.4" - "@dhis2-ui/status-icon" "8.12.4" +"@dhis2-ui/select@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/select/-/select-9.2.0.tgz#e854ecf6fd2580f9fb904301d1985923c1ade9cf" + integrity sha512-Hv0mRbpJNHJUa2lVivrWBt67iJ+4xTb7KxZkThISkmpQpzbWmhmJWlDbe0L+PdqOsaB2A70/N9HH4dhQyVP8vA== + dependencies: + "@dhis2-ui/box" "9.2.0" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/checkbox" "9.2.0" + "@dhis2-ui/chip" "9.2.0" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/popper" "9.2.0" + "@dhis2-ui/status-icon" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/selector-bar@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/selector-bar/-/selector-bar-8.12.4.tgz#d2bbca5751c1f410dc0ac8ee9b076f697eed63c4" - integrity sha512-5WuSepQ1NUX8V2h9CsseyJQ2GhyBmomyH0X7aYmtksQdImSSUzyjSBufkScA3YIYP83wQmmQEx6/32pPfoWtuA== - dependencies: - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/popper" "8.12.4" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" +"@dhis2-ui/selector-bar@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/selector-bar/-/selector-bar-9.2.0.tgz#23790f850bee1142e7bdffd711a2dea27dc78112" + integrity sha512-HDb/XKuUdD82vDh5mgz+OWIwpKjyiZEqek74y8g2r/AsMIl5Fe2rOyncOUPpGctxYeVp9u3gAlkFbIHd/qpv/w== + dependencies: + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/popper" "9.2.0" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" "@testing-library/react" "^12.1.2" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/sharing-dialog@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/sharing-dialog/-/sharing-dialog-8.12.4.tgz#9df65a459af2ef0f1e6bc551abc765771f4913d0" - integrity sha512-B9GyPvSgtNZ7mv6KcmahG/1vF/7qGH9Elis4keHQpd8rBKLBYtONdxoOPh9a8OkWYAmwI9jVejIWQYVXT0duKg== - dependencies: - "@dhis2-ui/box" "8.12.4" - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/divider" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/menu" "8.12.4" - "@dhis2-ui/modal" "8.12.4" - "@dhis2-ui/notice-box" "8.12.4" - "@dhis2-ui/popper" "8.12.4" - "@dhis2-ui/select" "8.12.4" - "@dhis2-ui/tab" "8.12.4" - "@dhis2-ui/tooltip" "8.12.4" - "@dhis2-ui/user-avatar" "8.12.4" +"@dhis2-ui/sharing-dialog@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/sharing-dialog/-/sharing-dialog-9.2.0.tgz#8818e5119c2113b3a54add3bd56dfe55ad2fc52a" + integrity sha512-h7chsY8XM2kw06r52pGRjS8ZknuwiEkRxuBTXD77G1Loni5TMm6xr0OFr9nSQHSsbcbxydbo2WPRPAu6WjVyfg== + dependencies: + "@dhis2-ui/box" "9.2.0" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/divider" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/menu" "9.2.0" + "@dhis2-ui/modal" "9.2.0" + "@dhis2-ui/notice-box" "9.2.0" + "@dhis2-ui/popper" "9.2.0" + "@dhis2-ui/select" "9.2.0" + "@dhis2-ui/tab" "9.2.0" + "@dhis2-ui/tooltip" "9.2.0" + "@dhis2-ui/user-avatar" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" "@react-hook/size" "^2.1.2" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/status-icon@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/status-icon/-/status-icon-8.12.4.tgz#31eacb7ad5472384338ed3f5f5b405e6d3359635" - integrity sha512-q86WTwS2rGQNr7l0IP+ln0i2LVQm9UuDZKjZRmrtWMwC7zStkPKrZaHKS+XTQErP8OCio7+n8NmpbpAYK31nBw== +"@dhis2-ui/status-icon@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/status-icon/-/status-icon-9.2.0.tgz#5686721ef7dc1b1cd1cca315b11136e0809e568f" + integrity sha512-oILHs38xICDU27C7CXgFAEttU4QJzg0wImKlX4XVJ5z1aGeq6qOW0RUVibF6JtyAppEi9XsYZ+AB1KLEGnDFJw== dependencies: - "@dhis2-ui/loader" "8.12.4" + "@dhis2-ui/loader" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/switch@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/switch/-/switch-8.12.4.tgz#856493c86e726696186ef986efcbae7ef6a53169" - integrity sha512-tjd3a+IKrneA5RqfYOasclPaYRRI378LtWvgi3wzjCL0+dXMBwRMDbDJZz+xH4SjMQryIfy/wcPalJfczPZ4xg== +"@dhis2-ui/switch@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/switch/-/switch-9.2.0.tgz#1fc9ed88546906da11aa19348b8ba8ff2e7c8414" + integrity sha512-IwVUiqxMKZmx7VtEJkyHqGzy4tvRNh4qbhmrFodaH6d9YrbycaFNECbphrT1OywrUBuci0Q2LovZhYKInkeIFg== dependencies: - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/required" "8.12.4" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/required" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/tab@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/tab/-/tab-8.12.4.tgz#3470cf4f5f2b529b6c0e592fd457d08234b48b83" - integrity sha512-AxZabWBQQL9fCB2j4J+P/8eCpKtY5DRyeDM8vEUdxaQQr5dUs7sGkDa8yB5uKyXKC4PdjR0Tpy/w8mE6bv+L3w== +"@dhis2-ui/tab@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/tab/-/tab-9.2.0.tgz#910223a907f8c007921924138306b53c2014f0fd" + integrity sha512-zVxwQ7WgjcrCGd+qWzLx+OtTWlGOIuC+AuSknHz0wpGgW3vawr1rMDA6j129l4O+ezzPs5bw8vN3xUQORbj0Bg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/table@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/table/-/table-8.12.4.tgz#2a108f9fc96d2f45a2723b29dc71b5206f3b75f9" - integrity sha512-RNzXdzgsIWLQTU2aAyS6CZpKvIMTtigztSkv9XGdnpHtWp9EEeo//R1pbLYoNBLEohT8dqUiGdZC36ueeUbv1g== +"@dhis2-ui/table@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/table/-/table-9.2.0.tgz#83d535e6c60ff9c9c3f436e592489a131caef772" + integrity sha512-wep8wPQKRg5vYPGg3HLAvsAEo+lUk3L7wO7axHyzx/XuHR40QXRirpv4tsFVn8VPpSFVq0w/BFGp8ijp3Y6RtA== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/tag@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/tag/-/tag-8.12.4.tgz#2046e456a5b37214100088ad85a9babe56dc76c8" - integrity sha512-ja6nq/uxFaXwjYHtahPv9LEw+3w4ynmKMR1L6JQXEfX++s+/zdqt+TfvYQRICMNq6EJLy1fYWfUT104Nqr37GA== +"@dhis2-ui/tag@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/tag/-/tag-9.2.0.tgz#c6a11fbdfc20151cc75c323e6a6b1470ef064e58" + integrity sha512-qt/FUIFPZkghfF/fhhLHr4oOl54d1Zwy4CKf4cZmO2DSd09h7E7cfIUxXaX/shRHsSmeUhrFP2Nv5LAvu8XVUg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/text-area@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/text-area/-/text-area-8.12.4.tgz#880919128623aaf304c3618382d9843625b34714" - integrity sha512-BiUPu1Lk5dBFkHRa/wB0XMzhC+uLkjmMiOde8N4LTpm5DS3P7TnkRp6hApeWasd3pGf2i9KUWqep+aJ0zilwVQ== +"@dhis2-ui/text-area@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/text-area/-/text-area-9.2.0.tgz#75f279281f1513801148f11c2693537a5e9beed7" + integrity sha512-KLBlerlO3OvBvzTVKKolfghaAMjDPUZkxFsyRvWEIPh1RRxS9ZprunXpBWRkNO2I6U6uwqN0DiBLTNbh/zrafg== dependencies: - "@dhis2-ui/box" "8.12.4" - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/status-icon" "8.12.4" + "@dhis2-ui/box" "9.2.0" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/status-icon" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-icons" "8.12.4" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-icons" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/tooltip@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/tooltip/-/tooltip-8.12.4.tgz#a9eef55407cf4c5e4669d98ea9dc60303fa1153f" - integrity sha512-NUHPyDLYm1gLDNJkFmp5ch7sIKbDW/g68vE0tJQVHkun6buLWAfYeWcDZSyUsavzIDYutm1zyaepP4IS7ZD/9Q== +"@dhis2-ui/tooltip@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/tooltip/-/tooltip-9.2.0.tgz#e7a0827d9ca6b30b69073916f7801d01fb7c9793" + integrity sha512-7Gb/Occ5/Bju95dnxUGzt/Q4129zqGWrxu1+S2uhc0YPRSx83JcG0MivPsVsr0BeU+p+8xwTVdGBOhMmqLpL9Q== dependencies: - "@dhis2-ui/popper" "8.12.4" - "@dhis2-ui/portal" "8.12.4" + "@dhis2-ui/popper" "9.2.0" + "@dhis2-ui/portal" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/transfer@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/transfer/-/transfer-8.12.4.tgz#f1ffc632051fb5a73a29ff54f8f58b257ea89dae" - integrity sha512-lT8D7NzLMJZdSlVGFQGjfos/3F1xE9Pt5fpNvs7hyafjFbAweE80VtIUwRDRUUsgYwUNVHXaxLHzDFrsyU1y9Q== +"@dhis2-ui/transfer@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/transfer/-/transfer-9.2.0.tgz#3624d08100b72143da5639ce231b53d57706ffdc" + integrity sha512-1+SdfeCBr+iOLaXf4gkpNLlysaBsWLOmlVzBdGVqFt4I4SMJrhFrCMUTz4A1Nsw7GpKGds35vGClQUe5LcuVSw== dependencies: - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/intersection-detector" "8.12.4" - "@dhis2-ui/loader" "8.12.4" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/intersection-detector" "9.2.0" + "@dhis2-ui/loader" "9.2.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/user-avatar@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2-ui/user-avatar/-/user-avatar-8.12.4.tgz#8a99554c7b5eba7aa0b704d89260dc211e43865f" - integrity sha512-NVfjI19+4C003RKZ3+VmyBISBpGShl0nfk9goy+AN3ByXEOZhlFo+3+8Rl7aY9ZkOU0cVeLiBMmBOqCmnzGGLg== +"@dhis2-ui/user-avatar@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2-ui/user-avatar/-/user-avatar-9.2.0.tgz#722b4bad239aff8eb2b639ca51bd6528a08da1e4" + integrity sha512-XRop5Mmc5q1GnrM3YgIEdjw0OX7/KA9ZdxRNS4AU7ifYMEjUNutYmq8a2bJ1M6eZfq2DrhRQui9/1E7MvK+Evw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "8.12.4" + "@dhis2/ui-constants" "9.2.0" classnames "^2.3.1" prop-types "^15.7.2" @@ -2286,91 +2286,91 @@ workbox-routing "^6.1.5" workbox-strategies "^6.1.5" -"@dhis2/ui-constants@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2/ui-constants/-/ui-constants-8.12.4.tgz#e805fc67e936fecd1b15b08cb033c5d83c761209" - integrity sha512-R/FzJaZauKfVa/VLeECrwTmGouS4USyDLXs/yLYUq/VJVr7laXZ2Ty29mOjgylaOTyxp0Mv/faj3zuheFEi/xg== +"@dhis2/ui-constants@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2/ui-constants/-/ui-constants-9.2.0.tgz#47506acaec5e4ce28630519a630a05657d2d29f6" + integrity sha512-gMbnVJQJmCPoGJHnY09BoDe6Z1vukzFdUcm0HPYyijs8ZWnclLs+69iVamxhskOnNWgj8hEt/FVs4mfhMcW3Cg== dependencies: prop-types "^15.7.2" -"@dhis2/ui-forms@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2/ui-forms/-/ui-forms-8.12.4.tgz#f64f3e8feacd6e85c291a6d2726eaf355372e2d2" - integrity sha512-atZwS7m5OBFFNdc0sVi3TgxVz/QGVA/v/MvpD091Xtl5m33Vox9MdcMXiGQ5qwXgqMnnWJZ/p/nheiRBFuJOVA== - dependencies: - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/checkbox" "8.12.4" - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/file-input" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/radio" "8.12.4" - "@dhis2-ui/select" "8.12.4" - "@dhis2-ui/switch" "8.12.4" - "@dhis2-ui/text-area" "8.12.4" +"@dhis2/ui-forms@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2/ui-forms/-/ui-forms-9.2.0.tgz#a5651dc5010a495c8a52ef5484e7d44b2583b715" + integrity sha512-eodiPW+ahR5wVsgrl/bFvj2zyeJD+DR9woqys4ZyoaHlKjOdeLqDNbJDnrS+AmHfte5uorF/aWzmEZr825LBVg== + dependencies: + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/checkbox" "9.2.0" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/file-input" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/radio" "9.2.0" + "@dhis2-ui/select" "9.2.0" + "@dhis2-ui/switch" "9.2.0" + "@dhis2-ui/text-area" "9.2.0" "@dhis2/prop-types" "^3.1.2" classnames "^2.3.1" final-form "^4.20.2" prop-types "^15.7.2" react-final-form "^6.5.3" -"@dhis2/ui-icons@8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2/ui-icons/-/ui-icons-8.12.4.tgz#0d5ee1e5a1e9e218d0a5cc9b3263d96ae908c4d5" - integrity sha512-4rFUTqhBEqCEmt9LFLTZCI3YOcu9wbMqrtKT22jFP5DzCfae6oYsYb7duFHshN7WeWt8qVDKn5xDwPLCKdm5Dg== - -"@dhis2/ui@^8.12.3", "@dhis2/ui@^8.7.6": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@dhis2/ui/-/ui-8.12.4.tgz#0a3eb93db073ad7678a3b5669db43c05ee430c3d" - integrity sha512-ryc+0E0g7g460fYfRMuD0tE2gtiV+nJplZZnftWkpwnSuTwgGbvb9QZChtU679NDbrCDyJ4vPqV9ZemY+Vhj0Q== - dependencies: - "@dhis2-ui/alert" "8.12.4" - "@dhis2-ui/box" "8.12.4" - "@dhis2-ui/button" "8.12.4" - "@dhis2-ui/calendar" "8.12.4" - "@dhis2-ui/card" "8.12.4" - "@dhis2-ui/center" "8.12.4" - "@dhis2-ui/checkbox" "8.12.4" - "@dhis2-ui/chip" "8.12.4" - "@dhis2-ui/cover" "8.12.4" - "@dhis2-ui/css" "8.12.4" - "@dhis2-ui/divider" "8.12.4" - "@dhis2-ui/field" "8.12.4" - "@dhis2-ui/file-input" "8.12.4" - "@dhis2-ui/header-bar" "8.12.4" - "@dhis2-ui/help" "8.12.4" - "@dhis2-ui/input" "8.12.4" - "@dhis2-ui/intersection-detector" "8.12.4" - "@dhis2-ui/label" "8.12.4" - "@dhis2-ui/layer" "8.12.4" - "@dhis2-ui/legend" "8.12.4" - "@dhis2-ui/loader" "8.12.4" - "@dhis2-ui/logo" "8.12.4" - "@dhis2-ui/menu" "8.12.4" - "@dhis2-ui/modal" "8.12.4" - "@dhis2-ui/node" "8.12.4" - "@dhis2-ui/notice-box" "8.12.4" - "@dhis2-ui/organisation-unit-tree" "8.12.4" - "@dhis2-ui/pagination" "8.12.4" - "@dhis2-ui/popover" "8.12.4" - "@dhis2-ui/popper" "8.12.4" - "@dhis2-ui/portal" "8.12.4" - "@dhis2-ui/radio" "8.12.4" - "@dhis2-ui/required" "8.12.4" - "@dhis2-ui/segmented-control" "8.12.4" - "@dhis2-ui/select" "8.12.4" - "@dhis2-ui/selector-bar" "8.12.4" - "@dhis2-ui/sharing-dialog" "8.12.4" - "@dhis2-ui/switch" "8.12.4" - "@dhis2-ui/tab" "8.12.4" - "@dhis2-ui/table" "8.12.4" - "@dhis2-ui/tag" "8.12.4" - "@dhis2-ui/text-area" "8.12.4" - "@dhis2-ui/tooltip" "8.12.4" - "@dhis2-ui/transfer" "8.12.4" - "@dhis2-ui/user-avatar" "8.12.4" - "@dhis2/ui-constants" "8.12.4" - "@dhis2/ui-forms" "8.12.4" - "@dhis2/ui-icons" "8.12.4" +"@dhis2/ui-icons@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2/ui-icons/-/ui-icons-9.2.0.tgz#bddf5223cabec93c9499281b2181eb165346b639" + integrity sha512-g9993UGWVLwDcbV+wp3HqrK8AXFu49aped0GpZsQUlGbHIzEl1EgmjiII44N40VbXwVUnqIDmu99wBxpH5Gd+g== + +"@dhis2/ui@^8.12.3", "@dhis2/ui@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@dhis2/ui/-/ui-9.2.0.tgz#33d474cbc7cd95f8a714e019d6c69e144f77f86b" + integrity sha512-nhKwW5bmIfQvt3L16PffFO2NsDk9BgYb91vHx06fPgM56UdwGYSejpax8eU29vE9urmHSkijSpnBqY4buZy6Ow== + dependencies: + "@dhis2-ui/alert" "9.2.0" + "@dhis2-ui/box" "9.2.0" + "@dhis2-ui/button" "9.2.0" + "@dhis2-ui/calendar" "9.2.0" + "@dhis2-ui/card" "9.2.0" + "@dhis2-ui/center" "9.2.0" + "@dhis2-ui/checkbox" "9.2.0" + "@dhis2-ui/chip" "9.2.0" + "@dhis2-ui/cover" "9.2.0" + "@dhis2-ui/css" "9.2.0" + "@dhis2-ui/divider" "9.2.0" + "@dhis2-ui/field" "9.2.0" + "@dhis2-ui/file-input" "9.2.0" + "@dhis2-ui/header-bar" "9.2.0" + "@dhis2-ui/help" "9.2.0" + "@dhis2-ui/input" "9.2.0" + "@dhis2-ui/intersection-detector" "9.2.0" + "@dhis2-ui/label" "9.2.0" + "@dhis2-ui/layer" "9.2.0" + "@dhis2-ui/legend" "9.2.0" + "@dhis2-ui/loader" "9.2.0" + "@dhis2-ui/logo" "9.2.0" + "@dhis2-ui/menu" "9.2.0" + "@dhis2-ui/modal" "9.2.0" + "@dhis2-ui/node" "9.2.0" + "@dhis2-ui/notice-box" "9.2.0" + "@dhis2-ui/organisation-unit-tree" "9.2.0" + "@dhis2-ui/pagination" "9.2.0" + "@dhis2-ui/popover" "9.2.0" + "@dhis2-ui/popper" "9.2.0" + "@dhis2-ui/portal" "9.2.0" + "@dhis2-ui/radio" "9.2.0" + "@dhis2-ui/required" "9.2.0" + "@dhis2-ui/segmented-control" "9.2.0" + "@dhis2-ui/select" "9.2.0" + "@dhis2-ui/selector-bar" "9.2.0" + "@dhis2-ui/sharing-dialog" "9.2.0" + "@dhis2-ui/switch" "9.2.0" + "@dhis2-ui/tab" "9.2.0" + "@dhis2-ui/table" "9.2.0" + "@dhis2-ui/tag" "9.2.0" + "@dhis2-ui/text-area" "9.2.0" + "@dhis2-ui/tooltip" "9.2.0" + "@dhis2-ui/transfer" "9.2.0" + "@dhis2-ui/user-avatar" "9.2.0" + "@dhis2/ui-constants" "9.2.0" + "@dhis2/ui-forms" "9.2.0" + "@dhis2/ui-icons" "9.2.0" prop-types "^15.7.2" "@dnd-kit/accessibility@^3.0.0": From 830ddc271bc247e47cc0b0091a8df08427b8b433 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 19 Dec 2023 16:32:17 +0100 Subject: [PATCH 02/31] feat: use outlierDetection analytics endpoint (DHIS2-16345) --- src/api/analytics.js | 47 ++++++++++++++++++++++++++++++++++++++-- src/modules/analytics.js | 25 ++++++++++++++++++++- src/modules/fetchData.js | 13 +++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/api/analytics.js b/src/api/analytics.js index f6d3b338de..5541e02e7e 100644 --- a/src/api/analytics.js +++ b/src/api/analytics.js @@ -1,13 +1,16 @@ import { Analytics, - VIS_TYPE_PIVOT_TABLE, layoutGetDimensionItems, + VIS_TYPE_PIVOT_TABLE, DIMENSION_ID_PERIOD, DAILY, WEEKLY, WEEKS_THIS_YEAR, } from '@dhis2/analytics' -import { getRelativePeriodTypeUsed } from '../modules/analytics.js' +import { + getRelativePeriodTypeUsed, + getOutlierDetectionHeadersMap, +} from '../modules/analytics.js' const periodId = DIMENSION_ID_PERIOD @@ -24,6 +27,46 @@ export const apiFetchAnalytics = async (dataEngine, visualization, options) => { return [new analyticsEngine.response(rawResponse)] } +export const apiFetchAnalyticsForOutlierTable = async ( + dataEngine, + visualization, + options +) => { + const headersMap = getOutlierDetectionHeadersMap(options) + + const parameters = { ...options } + + const columns = visualization.columns || [] + const headers = [] + + columns.forEach(({ dimension, items }) => { + parameters[dimension] = items.map(({ id }) => id).join(',') + + headers.push(headersMap[dimension]) + }) + + // TODO + // // additional headers depending on the outlier method option + // if (options.outlierAnalysis.outlierMethod === 'MOD_Z_SCORE') { + // headers.push('modifiedzscore', 'median', 'medianabsdeviation') + // } else if (options.outlierAnalysis.outlierMethod === 'Z_SCORE') { + // headers.push('zscore', 'mean', 'stddev') + // } + + // XXX check order for these headers + headers.push('value', 'lowerbound', 'upperbound') + + parameters.headers = headers.join(',') + + const analyticsEngine = Analytics.getAnalytics(dataEngine) + + const req = new analyticsEngine.request().withParameters(parameters) + + const rawResponse = await analyticsEngine.aggregate.getOutliersData(req) + + return [new analyticsEngine.response(rawResponse)] +} + export const apiFetchAnalyticsForYearOverYear = async ( dataEngine, visualization, diff --git a/src/modules/analytics.js b/src/modules/analytics.js index 7bdc725763..92fc15f0b3 100644 --- a/src/modules/analytics.js +++ b/src/modules/analytics.js @@ -1,4 +1,27 @@ -import { getRelativePeriodsOptionsById, WEEKLY, DAILY } from '@dhis2/analytics' +import { + getRelativePeriodsOptionsById, + DIMENSION_ID_DATA, + DIMENSION_ID_ORGUNIT, + DIMENSION_ID_PERIOD, + WEEKLY, + DAILY, +} from '@dhis2/analytics' + +export const outlierDetectionHeadersMap = { + [DIMENSION_ID_DATA]: 'dxname', + [DIMENSION_ID_ORGUNIT]: 'ouname', + [DIMENSION_ID_PERIOD]: 'pename', +} + +export const getOutlierDetectionHeadersMap = ({ showHierarchy }) => { + const map = Object.assign({}, outlierDetectionHeadersMap) + + if (showHierarchy) { + map[DIMENSION_ID_ORGUNIT] = 'ounamehierarchy' + } + + return map +} export const computeYoYMatrix = (responses, relativePeriodTypeUsed) => { const periodGroups = responses.reduce((list, res) => { diff --git a/src/modules/fetchData.js b/src/modules/fetchData.js index d935136fc0..0126e720d0 100644 --- a/src/modules/fetchData.js +++ b/src/modules/fetchData.js @@ -1,11 +1,13 @@ import { isYearOverYear, + isOutlierTable, DIMENSION_ID_PERIOD, layoutGetDimensionItems, ALL_DYNAMIC_DIMENSION_ITEMS, } from '@dhis2/analytics' import { apiFetchAnalyticsForYearOverYear, + apiFetchAnalyticsForOutlierTable, apiFetchAnalytics, } from '../api/analytics.js' import { @@ -94,6 +96,17 @@ export const fetchData = async ({ } } + if (isOutlierTable(adaptedVisualization.type)) { + return { + responses: await apiFetchAnalyticsForOutlierTable( + dataEngine, + adaptedVisualization, + options + ), + extraOptions, + } + } + return { responses: await apiFetchAnalytics( dataEngine, From 3f17c5627f1a92789eac668ae92e1a1e7886cbe7 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 20 Dec 2023 12:20:04 +0100 Subject: [PATCH 03/31] feat: configure options for Outlier table vis type (DHIS2-16347) --- .../Options/OutliersForOutlierTable.js | 96 +++++++++++++++++++ .../Options/OutliersMaxResults.js | 69 +++++++++++++ src/modules/options/config.js | 4 + src/modules/options/outlierTableConfig.js | 36 +++++++ 4 files changed, 205 insertions(+) create mode 100644 src/components/VisualizationOptions/Options/OutliersForOutlierTable.js create mode 100644 src/components/VisualizationOptions/Options/OutliersMaxResults.js create mode 100644 src/modules/options/outlierTableConfig.js diff --git a/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js b/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js new file mode 100644 index 0000000000..6d3a609b81 --- /dev/null +++ b/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js @@ -0,0 +1,96 @@ +import i18n from '@dhis2/d2-i18n' +import { FieldSet, Legend } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import { connect } from 'react-redux' +import { acSetUiOptions } from '../../../actions/ui.js' +import { sGetUiOptions } from '../../../reducers/ui.js' +import { + tabSectionOption, + tabSectionTitle, +} from '../styles/VisualizationOptions.style.js' +import OutlierDetectionMethod from './OutlierDetectionMethod.js' +import { DEFAULT_STATE as OUTLIER_MAX_RESULTS_DEFAULT_STATE } from './OutliersMaxResults.js' + +export const OUTLIER_METHOD_PROP = 'outlierMethod' +export const OUTLIER_THRESHOLD_PROP = 'thresholdFactor' + +const OUTLIER_ANALYSIS_OPTION_NAME = 'outlierAnalysis' + +export const METHOD_STANDARD_Z_SCORE = 'STANDARD_Z_SCORE' +export const METHOD_MODIFIED_Z_SCORE = 'MODIFIED_Z_SCORE' +const methods = [ + { + id: METHOD_STANDARD_Z_SCORE, + label: i18n.t('Z-score / Standard score'), + defaultThreshold: 3, + }, + { + id: METHOD_MODIFIED_Z_SCORE, + label: i18n.t('Modified Z-score'), + defaultThreshold: 3, + }, +] + +export const DEFAULT_STATE = { + [OUTLIER_METHOD_PROP]: METHOD_MODIFIED_Z_SCORE, + [OUTLIER_THRESHOLD_PROP]: 3, +} + +const Outliers = ({ outlierAnalysis, onChange }) => { + const storeProp = (prop, value) => + onChange({ ...outlierAnalysis, [prop]: value }) + + return ( +
+
+ + + {i18n.t('Outlier detection method')} + + +
+ + onChange({ + ...outlierAnalysis, + [OUTLIER_METHOD_PROP]: value, + [OUTLIER_THRESHOLD_PROP]: methods.find( + (item) => item.id === value + ).defaultThreshold, + }) + } + onThresholdChange={(value) => + storeProp(OUTLIER_THRESHOLD_PROP, value) + } + currentMethodId={outlierAnalysis[OUTLIER_METHOD_PROP]} + currentThreshold={ + outlierAnalysis[OUTLIER_THRESHOLD_PROP] + } + /> +
+
+
+ ) +} + +Outliers.propTypes = { + outlierAnalysis: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, +} + +const mapStateToProps = (state) => ({ + // OutliersMaxResults is part of the root option name, so we need to merge the default states + outlierAnalysis: sGetUiOptions(state)[OUTLIER_ANALYSIS_OPTION_NAME] || { + ...DEFAULT_STATE, + ...OUTLIER_MAX_RESULTS_DEFAULT_STATE, + }, +}) + +const mapDispatchToProps = (dispatch) => ({ + onChange: (value) => + dispatch(acSetUiOptions({ [OUTLIER_ANALYSIS_OPTION_NAME]: value })), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(Outliers) diff --git a/src/components/VisualizationOptions/Options/OutliersMaxResults.js b/src/components/VisualizationOptions/Options/OutliersMaxResults.js new file mode 100644 index 0000000000..6bdc22c1be --- /dev/null +++ b/src/components/VisualizationOptions/Options/OutliersMaxResults.js @@ -0,0 +1,69 @@ +import i18n from '@dhis2/d2-i18n' +import { InputField } from '@dhis2/ui' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { acSetUiOptions } from '../../../actions/ui.js' +import { sGetUiOptions } from '../../../reducers/ui.js' +import { tabSectionOption } from '../styles/VisualizationOptions.style.js' +import { DEFAULT_STATE as OUTLIER_METHOD_THRESHOLD_DEFAULT_STATE } from './OutliersForOutlierTable.js' + +const OUTLIER_ANALYSIS_OPTION_NAME = 'outlierAnalysis' +const MIN_VALUE = 1 +const MAX_VALUE = 500 + +export const OUTLIER_MAX_RESULTS_PROP = 'maxResults' +export const DEFAULT_STATE = { + [OUTLIER_MAX_RESULTS_PROP]: 20, +} + +const OutliersMaxResults = () => { + const dispatch = useDispatch() + + const outlierAnalysis = useSelector(sGetUiOptions)[ + OUTLIER_ANALYSIS_OPTION_NAME + ] || { + ...OUTLIER_METHOD_THRESHOLD_DEFAULT_STATE, + ...DEFAULT_STATE, + } + + const onChange = ({ value }) => { + const parsedValue = parseInt(value, 10) + + dispatch( + acSetUiOptions({ + [OUTLIER_ANALYSIS_OPTION_NAME]: { + ...outlierAnalysis, + [OUTLIER_MAX_RESULTS_PROP]: + parsedValue > MAX_VALUE + ? MAX_VALUE + : parsedValue < MIN_VALUE + ? MIN_VALUE + : Math.round(parsedValue), + }, + }) + ) + } + + return ( +
+
+ +
+
+ ) +} + +export default OutliersMaxResults diff --git a/src/modules/options/config.js b/src/modules/options/config.js index b55b1c9d23..2d1f11b614 100644 --- a/src/modules/options/config.js +++ b/src/modules/options/config.js @@ -4,6 +4,7 @@ import { VIS_TYPE_GAUGE, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_SCATTER, + VIS_TYPE_OUTLIER_TABLE, isStacked as isStackedType, isLegendSetType, isMultiType, @@ -13,6 +14,7 @@ import { } from '@dhis2/analytics' import defaultConfig from './defaultConfig.js' import gaugeConfig from './gaugeConfig.js' +import outlierTableConfig from './outlierTableConfig.js' import pieConfig from './pieConfig.js' import pivotTableConfig from './pivotTableConfig.js' import scatterConfig from './scatterConfig.js' @@ -60,6 +62,8 @@ export const getOptionsByType = ({ }) case VIS_TYPE_SCATTER: return scatterConfig() + case VIS_TYPE_OUTLIER_TABLE: + return outlierTableConfig(defaultProps) default: return defaultConfig(defaultProps) } diff --git a/src/modules/options/outlierTableConfig.js b/src/modules/options/outlierTableConfig.js new file mode 100644 index 0000000000..a8820591a1 --- /dev/null +++ b/src/modules/options/outlierTableConfig.js @@ -0,0 +1,36 @@ +import React from 'react' +import DigitGroupSeparator from '../../components/VisualizationOptions/Options/DigitGroupSeparator.js' +import DisplayDensity from '../../components/VisualizationOptions/Options/DisplayDensity.js' +import FontSize from '../../components/VisualizationOptions/Options/FontSize.js' +import Outliers from '../../components/VisualizationOptions/Options/OutliersForOutlierTable.js' +import OutliersMaxResults from '../../components/VisualizationOptions/Options/OutliersMaxResults.js' +import ShowHierarchy from '../../components/VisualizationOptions/Options/ShowHierarchy.js' +import getDataTab from './tabs/data.js' +import getOutliersTab from './tabs/outliers.js' +import getStyleTab from './tabs/style.js' + +export default () => [ + getDataTab([ + { + key: 'data-section-1', + content: React.Children.toArray([]), + }, + ]), + getStyleTab([ + { + key: 'style-section-1', + content: React.Children.toArray([ + , + , + , + , + ]), + }, + ]), + getOutliersTab([ + { + key: 'outliers-section-1', + content: React.Children.toArray([]), + }, + ]), +] From a93e7d0d1d13314ab304bea1a58a5e86e39ad0b9 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 20 Dec 2023 15:42:51 +0100 Subject: [PATCH 04/31] feat: add plugin component for outliers table (DHIS2-16356) --- src/api/analytics.js | 43 +++-- .../VisualizationPlugin/OutlierTablePlugin.js | 149 ++++++++++++++++++ .../VisualizationPlugin.js | 91 +++++++---- .../styles/OutlierTablePlugin.module.css | 54 +++++++ src/modules/analytics.js | 6 +- src/modules/options.js | 17 +- src/reducers/ui.js | 30 +++- 7 files changed, 334 insertions(+), 56 deletions(-) create mode 100644 src/components/VisualizationPlugin/OutlierTablePlugin.js create mode 100644 src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css diff --git a/src/api/analytics.js b/src/api/analytics.js index 5541e02e7e..407a749758 100644 --- a/src/api/analytics.js +++ b/src/api/analytics.js @@ -7,9 +7,16 @@ import { WEEKLY, WEEKS_THIS_YEAR, } from '@dhis2/analytics' +import { + METHOD_MODIFIED_Z_SCORE, + METHOD_STANDARD_Z_SCORE, + OUTLIER_METHOD_PROP, + OUTLIER_THRESHOLD_PROP, +} from '../components/VisualizationOptions/Options/OutliersForOutlierTable.js' +import { OUTLIER_MAX_RESULTS_PROP } from '../components/VisualizationOptions/Options/OutliersMaxResults.js' import { getRelativePeriodTypeUsed, - getOutlierDetectionHeadersMap, + getOutlierTableHeadersMap, } from '../modules/analytics.js' const periodId = DIMENSION_ID_PERIOD @@ -32,9 +39,18 @@ export const apiFetchAnalyticsForOutlierTable = async ( visualization, options ) => { - const headersMap = getOutlierDetectionHeadersMap(options) - - const parameters = { ...options } + const headersMap = getOutlierTableHeadersMap(options) + + const parameters = { + ...options, + maxResults: visualization.outlierAnalysis[OUTLIER_MAX_RESULTS_PROP], + algorithm: + visualization.outlierAnalysis[OUTLIER_METHOD_PROP] === + METHOD_STANDARD_Z_SCORE + ? 'Z_SCORE' + : visualization.outlierAnalysis[OUTLIER_METHOD_PROP], + threshold: visualization.outlierAnalysis[OUTLIER_THRESHOLD_PROP], + } const columns = visualization.columns || [] const headers = [] @@ -45,16 +61,17 @@ export const apiFetchAnalyticsForOutlierTable = async ( headers.push(headersMap[dimension]) }) - // TODO - // // additional headers depending on the outlier method option - // if (options.outlierAnalysis.outlierMethod === 'MOD_Z_SCORE') { - // headers.push('modifiedzscore', 'median', 'medianabsdeviation') - // } else if (options.outlierAnalysis.outlierMethod === 'Z_SCORE') { - // headers.push('zscore', 'mean', 'stddev') - // } + headers.push('value') + + switch (visualization.outlierAnalysis.outlierMethod) { + case METHOD_MODIFIED_Z_SCORE: + headers.push('median', 'modifiedzscore', 'medianabsdeviation') + break + case METHOD_STANDARD_Z_SCORE: + headers.push('mean', 'zscore', 'stddev') + } - // XXX check order for these headers - headers.push('value', 'lowerbound', 'upperbound') + headers.push('lowerbound', 'upperbound') parameters.headers = headers.join(',') diff --git a/src/components/VisualizationPlugin/OutlierTablePlugin.js b/src/components/VisualizationPlugin/OutlierTablePlugin.js new file mode 100644 index 0000000000..0f6faafaca --- /dev/null +++ b/src/components/VisualizationPlugin/OutlierTablePlugin.js @@ -0,0 +1,149 @@ +import { getFixedDimensions } from '@dhis2/analytics' +import { + DataTable, + DataTableBody, + DataTableCell, + DataTableColumnHeader, + DataTableHead, + DataTableRow, +} from '@dhis2/ui' +import cx from 'classnames' +import PropTypes from 'prop-types' +import React from 'react' +import { getOutlierTableHeadersMap } from '../../modules/analytics.js' +import { + DISPLAY_DENSITY_COMFORTABLE, + DISPLAY_DENSITY_COMPACT, + FONT_SIZE_LARGE, + FONT_SIZE_NORMAL, + FONT_SIZE_SMALL, +} from '../../modules/options.js' +import styles from './styles/OutlierTablePlugin.module.css' + +const getFontSizeClass = (fontSize) => { + switch (fontSize) { + case FONT_SIZE_LARGE: + return styles.fontSizeLarge + case FONT_SIZE_SMALL: + return styles.fontSizeSmall + case FONT_SIZE_NORMAL: + default: + return styles.fontSizeNormal + } +} + +const getSizeClass = (displayDensity) => { + switch (displayDensity) { + case DISPLAY_DENSITY_COMFORTABLE: + return styles.sizeComfortable + case DISPLAY_DENSITY_COMPACT: + return styles.sizeCompact + default: + return styles.sizeNormal + } +} + +const OutlierTablePlugin = ({ + responses, + visualization, + //style, + //id: renderCounter, + //onToggleContextualMenu, +}) => { + const data = responses[0] + const headersMap = getOutlierTableHeadersMap({ + showHierarchy: visualization.showHierarchy, + }) + const fixedDimensions = getFixedDimensions() + + const sizeClass = getSizeClass(visualization.displayDensity) + const fontSizeClass = getFontSizeClass(visualization.fontSize) + + const lookupColumnName = (name) => { + const dimensionId = Object.entries(headersMap).find( + (entry) => entry[1] === name + )?.[0] + + if (dimensionId) { + return fixedDimensions[dimensionId]?.name + } + + return undefined + } + + const renderHeaderCell = ({ name, column }) => { + const columnName = lookupColumnName(name) || column + + return ( + + {columnName} + + ) + } + + const renderValueCell = ({ columnIndex, value }) => ( + + {value} + + ) + + return ( +
+ + + + {data.headers.map((header) => renderHeaderCell(header))} + + + + {data.rows.map((row, rowIndex) => ( + + {row.map((value, columnIndex) => + renderValueCell({ + columnIndex, + value, + }) + )} + + ))} + + +
+ ) +} + +OutlierTablePlugin.defaultProps = { + style: {}, +} + +OutlierTablePlugin.propTypes = { + responses: PropTypes.arrayOf(PropTypes.object).isRequired, + //style: PropTypes.object, + visualization: PropTypes.object.isRequired, + // id: PropTypes.number, + //style: PropTypes.object, + // onToggleContextualMenu: PropTypes.func, +} + +export default OutlierTablePlugin diff --git a/src/components/VisualizationPlugin/VisualizationPlugin.js b/src/components/VisualizationPlugin/VisualizationPlugin.js index 5875e674ae..b8c7731783 100644 --- a/src/components/VisualizationPlugin/VisualizationPlugin.js +++ b/src/components/VisualizationPlugin/VisualizationPlugin.js @@ -1,4 +1,5 @@ import { + VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_PIVOT_TABLE, apiFetchOrganisationUnitLevels, convertOuLevelsToUids, @@ -22,6 +23,7 @@ import { fetchData } from '../../modules/fetchData.js' import { getOptionsFromVisualization } from '../../modules/options.js' import ChartPlugin from './ChartPlugin.js' import ContextualMenu from './ContextualMenu.js' +import OutlierTablePlugin from './OutlierTablePlugin.js' import PivotPlugin from './PivotPlugin.js' import styles from './styles/VisualizationPlugin.module.css' @@ -384,42 +386,63 @@ export const VisualizationPlugin = ({ } } + const renderPlugin = () => { + if ( + !fetchResult.visualization.type || + fetchResult.visualization.type === VIS_TYPE_PIVOT_TABLE + ) { + return ( + + ) + } else if (fetchResult.visualization.type === VIS_TYPE_OUTLIER_TABLE) { + return ( + + ) + } else { + return ( + + ) + } + } + return (
-
- {!fetchResult.visualization.type || - fetchResult.visualization.type === VIS_TYPE_PIVOT_TABLE ? ( - - ) : ( - - )} -
+
{renderPlugin()}
{getLegendKey()} {contextualMenuRect && ( { - const map = Object.assign({}, outlierDetectionHeadersMap) +export const getOutlierTableHeadersMap = ({ showHierarchy }) => { + const map = Object.assign({}, outlierTableHeadersMap) if (showHierarchy) { map[DIMENSION_ID_ORGUNIT] = 'ounamehierarchy' diff --git a/src/modules/options.js b/src/modules/options.js index 17bb319ca2..a6966b0f5a 100644 --- a/src/modules/options.js +++ b/src/modules/options.js @@ -13,6 +13,8 @@ export const OPTION_BASE_LINE_ENABLED = 'baseLineEnabled' export const OPTION_BASE_LINE_TITLE = 'baseLineTitle' export const OPTION_BASE_LINE_VALUE = 'baseLineValue' export const OPTION_BASE_LINE_TITLE_FONT_STYLE = 'baseLineTitleFontStyle' +export const OPTION_DISPLAY_DENSITY = 'displayDensity' +export const OPTION_FONT_SIZE = 'fontSize' export const OPTION_TARGET_LINE_ENABLED = 'targetLineEnabled' export const OPTION_TARGET_LINE_TITLE = 'targetLineTitle' export const OPTION_TARGET_LINE_VALUE = 'targetLineValue' @@ -21,6 +23,13 @@ export const OPTION_LEGEND_DISPLAY_STRATEGY = 'legendDisplayStrategy' export const OPTION_LEGEND_DISPLAY_STYLE = 'legendDisplayStyle' export const OPTION_LEGEND_SET = 'legendSet' +export const DISPLAY_DENSITY_COMFORTABLE = 'COMFORTABLE' +export const DISPLAY_DENSITY_NORMAL = 'NORMAL' +export const DISPLAY_DENSITY_COMPACT = 'COMPACT' +export const FONT_SIZE_LARGE = 'LARGE' +export const FONT_SIZE_NORMAL = 'NORMAL' +export const FONT_SIZE_SMALL = 'SMALL' + export const options = { axes: { requestable: false, savable: true, defaultValue: [] }, colorSet: { @@ -102,11 +111,15 @@ export const options = { numberType: { defaultValue: 'VALUE', requestable: false, savable: true }, showHierarchy: { defaultValue: false, requestable: true, savable: true }, displayDensity: { - defaultValue: 'NORMAL', + defaultValue: DISPLAY_DENSITY_NORMAL, + requestable: false, + savable: true, + }, + fontSize: { + defaultValue: FONT_SIZE_NORMAL, requestable: false, savable: true, }, - fontSize: { defaultValue: 'NORMAL', requestable: false, savable: true }, digitGroupSeparator: { defaultValue: 'SPACE', requestable: false, diff --git a/src/reducers/ui.js b/src/reducers/ui.js index d6c7113e53..3ccea423a1 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -16,6 +16,7 @@ import { FONT_STYLE_VERTICAL_AXIS_TITLE, FONT_STYLE_REGRESSION_LINE_LABEL, USER_ORG_UNIT, + VIS_TYPE_OUTLIER_TABLE, } from '@dhis2/analytics' import objectClean from 'd2-utilizr/lib/objectClean' import castArray from 'lodash-es/castArray' @@ -23,6 +24,8 @@ import { TITLE_AUTO, TITLE_CUSTOM, } from '../components/VisualizationOptions/Options/AxisTitle.js' +import { DEFAULT_STATE as OUTLIER_METHOD_THRESHOLD_DEFAULT_STATE } from '../components/VisualizationOptions/Options/OutliersForOutlierTable.js' +import { DEFAULT_STATE as OUTLIER_MAX_RESULTS_DEFAULT_STATE } from '../components/VisualizationOptions/Options/OutliersMaxResults.js' import { getFilteredLayout, getInverseLayout, @@ -99,7 +102,6 @@ export const DEFAULT_UI = { parentGraphMap: {}, activeModalDialog: null, rightSidebarOpen: false, - outlierAnalysis: null, } export const PRESELECTED_YEAR_OVER_YEAR_SERIES = ['THIS_YEAR', 'LAST_YEAR'] @@ -134,12 +136,32 @@ const getPreselectedUi = (options) => { } } +const getDefaultUiByType = (ui) => { + switch (ui.type) { + case VIS_TYPE_OUTLIER_TABLE: { + return { + ...ui, + options: { + ...ui.options, + outlierAnalysis: { + ...(ui.options.outlierAnalysis ?? { + ...OUTLIER_METHOD_THRESHOLD_DEFAULT_STATE, + ...OUTLIER_MAX_RESULTS_DEFAULT_STATE, + }), + }, + }, + } + } + default: { + return { ...ui } + } + } +} + export default (state = DEFAULT_UI, action) => { switch (action.type) { case SET_UI: { - return { - ...action.value, - } + return getDefaultUiByType(action.value) } case SET_UI_FROM_VISUALIZATION: { return getAdaptedUiByType( From a1bdeba130050d58ed534c09f661a2ec8d8d7fcf Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Sat, 23 Dec 2023 11:47:48 +0100 Subject: [PATCH 05/31] chore: update pot file --- i18n/en.pot | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a59842c7f6..a44dfd0386 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-12-19T14:19:00.918Z\n" -"PO-Revision-Date: 2023-12-19T14:19:00.918Z\n" +"POT-Creation-Date: 2024-01-24T10:18:06.443Z\n" +"PO-Revision-Date: 2024-01-24T10:18:06.443Z\n" msgid "All items" msgstr "All items" @@ -581,6 +581,9 @@ msgstr "" msgid "Outlier detection method" msgstr "Outlier detection method" +msgid "Max results" +msgstr "Max results" + msgid "Organisation unit" msgstr "Organisation unit" @@ -706,6 +709,9 @@ msgstr "Change org unit" msgid "{{level}} level in {{orgunit}}" msgstr "{{level}} level in {{orgunit}}" +msgid "Sort descending by {{column}} and update" +msgstr "Sort descending by {{column}} and update" + msgid "Open as Map" msgstr "Open as Map" From c49fdadfac1a207ad4465128be3dcb2eda3dc9cd Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 5 Jan 2024 14:32:56 +0100 Subject: [PATCH 06/31] feat: disable all dim sections except Main (DHIS2-16346) --- .../DimensionsPanel/DndDimensionList.js | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/components/DimensionsPanel/DndDimensionList.js b/src/components/DimensionsPanel/DndDimensionList.js index 28d280f4bc..c00eb14ea1 100644 --- a/src/components/DimensionsPanel/DndDimensionList.js +++ b/src/components/DimensionsPanel/DndDimensionList.js @@ -4,6 +4,7 @@ import { getFixedDimensions, getDynamicDimensions, getPredefinedDimensions, + VIS_TYPE_OUTLIER_TABLE, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' @@ -24,8 +25,17 @@ export class DndDimensionList extends Component { .toLowerCase() .includes(this.props.filterText.toLowerCase()) + getFilteredDimensions = (filter) => + this.props.dimensions.filter(filter).filter(this.nameContainsFilterText) + isSelected = (id) => this.props.selectedIds.includes(id) - isDisabledDimension = (id) => this.props.disallowedDimensions.includes(id) + isDisabledDimension = (id) => + // all dimensions in YOUR DIMENSIONS section need to be disabled for Outlier table + this.props.visType === VIS_TYPE_OUTLIER_TABLE + ? this.props.disallowedDimensions + .concat(this.yourDimensions.map((d) => d.id)) + .includes(id) + : this.props.disallowedDimensions.includes(id) isLockedDimension = (id) => this.props.lockedDimensions.includes(id) isRecommendedDimension = (id) => this.props.recommendedIds.includes(id) @@ -51,25 +61,22 @@ export class DndDimensionList extends Component { ) } - getDimensionItemsByFilter = (filter) => - this.props.dimensions - .filter(filter) - .filter(this.nameContainsFilterText) - .map(this.renderItem) + getDimensionItemsFromList = (dimensionList) => + dimensionList.map(this.renderItem) render() { this.dndIndex = 0 - const fixedDimensions = this.getDimensionItemsByFilter((dimension) => + this.mainDimensions = this.getFilteredDimensions((dimension) => Object.values(getFixedDimensions()).some( (fixedDim) => fixedDim.id === dimension.id ) ) - const dynamicDimensions = this.getDimensionItemsByFilter((dimension) => + this.otherDimensions = this.getFilteredDimensions((dimension) => Object.values(getDynamicDimensions()).some( (dynDim) => dynDim.id === dimension.id ) ) - const nonPredefinedDimensions = this.getDimensionItemsByFilter( + this.yourDimensions = this.getFilteredDimensions( (dimension) => !Object.values(getPredefinedDimensions()).some( (predefDim) => predefDim.id === dimension.id @@ -94,7 +101,9 @@ export class DndDimensionList extends Component { className={styles.list} data-test={`${this.props.dataTest}-fixed-dimensions`} > - {fixedDimensions} + {this.getDimensionItemsFromList( + this.mainDimensions + )}
@@ -105,7 +114,9 @@ export class DndDimensionList extends Component { className={styles.list} data-test={`${this.props.dataTest}-dynamic-dimensions`} > - {dynamicDimensions} + {this.getDimensionItemsFromList( + this.otherDimensions + )}
@@ -116,7 +127,9 @@ export class DndDimensionList extends Component { className={styles.list} data-test={`${this.props.dataTest}-non-predefined-dimensions`} > - {nonPredefinedDimensions} + {this.getDimensionItemsFromList( + this.yourDimensions + )}
@@ -136,6 +149,7 @@ DndDimensionList.propTypes = { lockedDimensions: PropTypes.array, recommendedIds: PropTypes.array, selectedIds: PropTypes.array, + visType: PropTypes.string, onDimensionClick: PropTypes.func, onDimensionOptionsClick: PropTypes.func, } @@ -158,6 +172,7 @@ const mapStateToProps = (state) => ({ recommendedIds: fromReducers.fromRecommendedIds.sGetRecommendedIds(state), disallowedDimensions: getDisallowedDimensionsMemo(state), lockedDimensions: getisLockedDimensionsMemo(state), + visType: fromReducers.fromUi.sGetUiType(state), }) export default connect(mapStateToProps)(DndDimensionList) From 76d159037697fd1bfb42e010b96878cbc2c14daa Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 5 Jan 2024 14:38:48 +0100 Subject: [PATCH 07/31] feat: allow only 1 selected period item (DHIS2-16399) --- i18n/en.pot | 8 +++--- .../DimensionsPanel/Dialogs/DialogManager.js | 26 +++++++++++------ src/components/Layout/Chip.js | 9 ++++-- src/modules/current.js | 28 +++++++++++++++++++ src/reducers/current.js | 4 +++ 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a44dfd0386..fd6cd5b9aa 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -43,11 +43,11 @@ msgid "Add to {{axisName}}" msgstr "Add to {{axisName}}" msgid "" -"'{{visualizationType}}' is intended to show a single data item. Only the " -"first item will be used and saved." +"'{{visualizationType}}' is intended to show a single item for this type of " +"dimension. Only the first item will be used and saved." msgstr "" -"'{{visualizationType}}' is intended to show a single data item. Only the " -"first item will be used and saved." +"'{{visualizationType}}' is intended to show a single item for this type of " +"dimension. Only the first item will be used and saved." msgid "" "'{{visualiationType}}' is intended to show maximum {{maxNumber}} number of " diff --git a/src/components/DimensionsPanel/Dialogs/DialogManager.js b/src/components/DimensionsPanel/Dialogs/DialogManager.js index 1cbf1e885f..30c1be7e43 100644 --- a/src/components/DimensionsPanel/Dialogs/DialogManager.js +++ b/src/components/DimensionsPanel/Dialogs/DialogManager.js @@ -7,6 +7,7 @@ import { DIMENSION_ID_DATA, DIMENSION_ID_PERIOD, DIMENSION_ID_ORGUNIT, + getDimensionMaxNumberOfItems, getAxisMaxNumberOfItems, getDisplayNameByVisType, filterOutPredefinedDimensions, @@ -272,21 +273,27 @@ export class DialogManager extends Component { const visType = type const numberOfItems = selectedItems.length + const dimensionMaxNumberOfItems = getDimensionMaxNumberOfItems( + visType, + dialogId + ) + const axisMaxNumberOfItems = getAxisMaxNumberOfItems( visType, axisId ) - const hasMaxNumberOfItemsRule = !!axisMaxNumberOfItems + const hasMaxNumberOfItemsRule = Boolean( + axisMaxNumberOfItems || dimensionMaxNumberOfItems + ) + const maxNumberOfItems = + axisMaxNumberOfItems || dimensionMaxNumberOfItems - if ( - hasMaxNumberOfItemsRule && - numberOfItems > axisMaxNumberOfItems - ) { + if (hasMaxNumberOfItemsRule && numberOfItems > maxNumberOfItems) { infoBoxMessage = - axisMaxNumberOfItems === 1 + maxNumberOfItems === 1 ? i18n.t( - `'{{visualizationType}}' is intended to show a single data item. Only the first item will be used and saved.`, + `'{{visualizationType}}' is intended to show a single item for this type of dimension. Only the first item will be used and saved.`, { visualizationType: getDisplayNameByVisType(visType), @@ -297,12 +304,12 @@ export class DialogManager extends Component { { visualiationType: getDisplayNameByVisType(visType), - maxNumber: axisMaxNumberOfItems, + maxNumber: maxNumberOfItems, } ) selectedItems.forEach((item, index) => { - item.isActive = index < axisMaxNumberOfItems + item.isActive = index < maxNumberOfItems }) } else if (isScatterAttribute(dialogId) && numberOfItems > 1) { infoBoxMessage = i18n.t( @@ -380,6 +387,7 @@ export class DialogManager extends Component { content = ( getAxisMaxNumberOfItems(type, axisId) + const getMaxNumberOfItems = () => + getAxisMaxNumberOfItems(type, axisId) || + getDimensionMaxNumberOfItems(type, dimensionId) const handleClick = () => { if (!getPredefinedDimensionProp(dimensionId, DIMENSION_PROP_NO_ITEMS)) { @@ -158,7 +162,8 @@ const Chip = ({ {renderChipLabelSuffix()} - {hasAxisTooManyItems(type, axisId, items.length) && + {(hasAxisTooManyItems(type, axisId, items.length) || + hasDimensionTooManyItems(type, dimensionId, items.length)) && WarningIconWrapper} {isLocked && LockIconWrapper} diff --git a/src/modules/current.js b/src/modules/current.js index 96c597a168..6f3a57a3ea 100644 --- a/src/modules/current.js +++ b/src/modules/current.js @@ -6,6 +6,7 @@ import { AXIS_ID_FILTERS, DIMENSION_ID_DATA, DIMENSION_ID_PERIOD, + VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_PIE, dimensionCreate, @@ -138,6 +139,33 @@ export const getItemsByDimensionFromUi = (ui) => { return result } +export const getOutlierTableCurrentFromUi = (state, value) => { + const ui = { + ...value, + layout: { + ...getAdaptedUiLayoutByType(value.layout, VIS_TYPE_OUTLIER_TABLE), + }, + itemsByDimension: getItemsByDimensionFromUi(value), + } + + const axesFromUi = getAxesFromUi(ui) + + // only save the first pe item + const peItems = layoutGetDimensionItems(axesFromUi, DIMENSION_ID_PERIOD) + const outlierTableAxesFromUi = layoutReplaceDimension( + axesFromUi, + DIMENSION_ID_PERIOD, + [peItems[0]] + ) + + return { + ...state, + [BASE_FIELD_TYPE]: ui.type, + ...outlierTableAxesFromUi, + ...getOptionsFromUi(ui), + } +} + export const getSingleValueCurrentFromUi = (state, value) => { const ui = { ...value, diff --git a/src/reducers/current.js b/src/reducers/current.js index d81eca19d0..7a7fca686b 100644 --- a/src/reducers/current.js +++ b/src/reducers/current.js @@ -3,6 +3,7 @@ import { VIS_TYPE_YEAR_OVER_YEAR_COLUMN, VIS_TYPE_PIE, VIS_TYPE_GAUGE, + VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_SCATTER, } from '@dhis2/analytics' @@ -11,6 +12,7 @@ import { getOptionsFromUi, getPieCurrentFromUi, getYearOverYearCurrentFromUi, + getOutlierTableCurrentFromUi, getSingleValueCurrentFromUi, getSeriesFromUi, getScatterCurrentFromUi, @@ -60,6 +62,8 @@ export default (state = DEFAULT_CURRENT, action) => { return getYearOverYearCurrentFromUi(state, action.value.ui) case VIS_TYPE_SCATTER: return getScatterCurrentFromUi(state, action.value.ui) + case VIS_TYPE_OUTLIER_TABLE: + return getOutlierTableCurrentFromUi(state, action.value.ui) default: { return getDefaultFromUi( state, From 87f7801199978935a227da6efa7f58b3d9092400 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 8 Jan 2024 11:37:12 +0100 Subject: [PATCH 08/31] feat: set up feature toggle for outliers table for v41 (DHIS2-16421) --- src/components/MenuBar/MenuBar.js | 97 ++++++++++--------- .../VisualizationTypeSelector.js | 4 +- src/modules/visualization.js | 12 +++ 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/components/MenuBar/MenuBar.js b/src/components/MenuBar/MenuBar.js index f6150c33cb..add7d03de8 100644 --- a/src/components/MenuBar/MenuBar.js +++ b/src/components/MenuBar/MenuBar.js @@ -15,6 +15,7 @@ import history from '../../modules/history.js' import { visTypes, getVisualizationState, + useVisTypesFilterByVersion, STATE_UNSAVED, STATE_DIRTY, } from '../../modules/visualization.js' @@ -48,55 +49,59 @@ const getOnSaveAs = (props) => (details) => const getOnDelete = (props) => () => props.onDeleteVisualization() const getOnError = (props) => (error) => props.onError(error) -const filterVisTypes = [ - { type: VIS_TYPE_GROUP_ALL }, - { type: VIS_TYPE_GROUP_CHARTS, insertDivider: true }, - ...visTypes.map((visType) => ({ - type: visType, - })), -] +const UnconnectedMenuBar = ({ dataTest, ...props }, context) => { + const filterVisTypesByVersion = useVisTypesFilterByVersion() -const UnconnectedMenuBar = ({ dataTest, ...props }, context) => ( - <> - ( - - )} - /> - - ({ + type: visType, + })), + ] + + return ( + <> + ( + + )} /> - + + + - - - - -) + + + + + ) +} UnconnectedMenuBar.propTypes = { apiObjectName: PropTypes.string, diff --git a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index 44c89e2aed..d91b3dc27c 100644 --- a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -16,6 +16,7 @@ import { getAdaptedUiByType } from '../../modules/ui.js' import { visTypes, getVisTypeDescriptions, + useVisTypesFilterByVersion, } from '../../modules/visualization.js' import { sGetCurrent } from '../../reducers/current.js' import { sGetMetadata } from '../../reducers/metadata.js' @@ -31,6 +32,7 @@ const UnconnectedVisualizationTypeSelector = ( context ) => { const baseUrl = context.baseUrl + const filterVisTypesByVersion = useVisTypesFilterByVersion() const [, /* actual value not used */ { set }] = useSetting( USER_DATASTORE_CURRENT_AO_KEY @@ -66,7 +68,7 @@ const UnconnectedVisualizationTypeSelector = (
- {visTypes.map((visType) => ( + {visTypes.filter(filterVisTypesByVersion).map((visType) => ( { return metadata } + +export const useVisTypesFilterByVersion = () => { + const { serverVersion } = useConfig() + + const filterVisTypesByVersion = (visType) => + serverVersion.minor < 41 && visType === VIS_TYPE_OUTLIER_TABLE + ? false + : true + + return filterVisTypesByVersion +} From d2f9e7d8698d7353a296cceee340e3c20f3b3aa1 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 24 Jan 2024 15:19:49 +0100 Subject: [PATCH 09/31] feat: implement sorting (DHIS2-16518) --- i18n/en.pot | 11 ++- src/PluginWrapper.js | 26 +++++- src/actions/ui.js | 11 +++ src/api/analytics.js | 9 ++ src/components/Visualization/Visualization.js | 36 ++++++-- .../Options/OutliersForOutlierTable.js | 89 +++++++++++-------- .../VisualizationPlugin/OutlierTablePlugin.js | 64 +++++++++++-- .../VisualizationPlugin.js | 7 +- src/modules/current.js | 8 ++ src/modules/fields/baseFields.js | 1 + src/modules/ui.js | 14 +++ src/reducers/ui.js | 20 +++++ 12 files changed, 237 insertions(+), 59 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index fd6cd5b9aa..0c73504e40 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: 2024-01-24T10:18:06.443Z\n" -"PO-Revision-Date: 2024-01-24T10:18:06.443Z\n" +"POT-Creation-Date: 2024-01-29T10:57:59.794Z\n" +"PO-Revision-Date: 2024-01-29T10:57:59.794Z\n" msgid "All items" msgstr "All items" @@ -709,8 +709,11 @@ msgstr "Change org unit" msgid "{{level}} level in {{orgunit}}" msgstr "{{level}} level in {{orgunit}}" -msgid "Sort descending by {{column}} and update" -msgstr "Sort descending by {{column}} and update" +msgid "Sort ascending by {{columnName}} and update" +msgstr "Sort ascending by {{columnName}} and update" + +msgid "Sort descending by {{columnName}} and update" +msgstr "Sort descending by {{columnName}} and update" msgid "Open as Map" msgstr "Open as Map" diff --git a/src/PluginWrapper.js b/src/PluginWrapper.js index 52ee038764..2cee6a1783 100644 --- a/src/PluginWrapper.js +++ b/src/PluginWrapper.js @@ -3,7 +3,7 @@ import { CssVariables, CenteredContent, CircularLoader, Layer } from '@dhis2/ui' import postRobot from '@krakenjs/post-robot' import debounce from 'lodash-es/debounce' import PropTypes from 'prop-types' -import React, { useEffect, useLayoutEffect, useState } from 'react' +import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react' import { VisualizationPlugin } from './components/VisualizationPlugin/VisualizationPlugin.js' import { getPWAInstallationStatus } from './modules/getPWAInstallationStatus.js' @@ -79,6 +79,24 @@ const PluginWrapper = () => { const receivePropsFromParent = (event) => setPropsFromParent(event.data) + const onOutlierTableSort = useCallback( + (sorting) => { + const newSorting = { + dimension: sorting.dimension, + direction: sorting.direction.toUpperCase(), + } + + setPropsFromParent({ + ...propsFromParent, + visualization: { + ...propsFromParent.visualization, + sorting: [newSorting], + }, + }) + }, + [propsFromParent] + ) + useEffect(() => { postRobot .send(window.parent, 'getProps') @@ -128,7 +146,11 @@ const PluginWrapper = () => { cacheNow={propsFromParent.recordOnNextLoad} isParentCached={propsFromParent.isParentCached} > - +
diff --git a/src/actions/ui.js b/src/actions/ui.js index df704580f7..5d1f04e8c4 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -2,6 +2,7 @@ import { getDisabledOptions } from '../modules/disabledOptions.js' import { SET_UI, CLEAR_UI, + CLEAR_UI_DATA_SORTING, SET_UI_FROM_VISUALIZATION, SET_UI_TYPE, SET_UI_DISABLED_OPTIONS, @@ -23,6 +24,7 @@ import { UPDATE_UI_SERIES_ITEM, SET_UI_OPTION, SET_UI_OPTION_FONT_STYLE, + SET_UI_DATA_SORTING, sGetUiType, sGetUiOptions, } from '../reducers/ui.js' @@ -67,6 +69,15 @@ export const acSetUiOptionFontStyle = (value) => ({ value, }) +export const acClearUiDataSorting = () => ({ + type: CLEAR_UI_DATA_SORTING, +}) + +export const acSetUiDataSorting = (value) => ({ + type: SET_UI_DATA_SORTING, + value, +}) + export const acUpdateUiSeriesItem = (value) => ({ type: UPDATE_UI_SERIES_ITEM, value, diff --git a/src/api/analytics.js b/src/api/analytics.js index 407a749758..66b8f7b837 100644 --- a/src/api/analytics.js +++ b/src/api/analytics.js @@ -18,6 +18,7 @@ import { getRelativePeriodTypeUsed, getOutlierTableHeadersMap, } from '../modules/analytics.js' +import { getSortingFromVisualization } from '../modules/ui.js' const periodId = DIMENSION_ID_PERIOD @@ -75,6 +76,14 @@ export const apiFetchAnalyticsForOutlierTable = async ( parameters.headers = headers.join(',') + // sorting + const sorting = getSortingFromVisualization(visualization) + + if (sorting) { + parameters.orderBy = sorting.dimension + parameters.sortOrder = sorting.direction + } + const analyticsEngine = Analytics.getAnalytics(dataEngine) const req = new analyticsEngine.request().withParameters(parameters) diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index c619c23760..8fc1c618ba 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -7,7 +7,11 @@ import { acSetChart } from '../../actions/chart.js' import { tSetCurrentFromUi } from '../../actions/current.js' import { acSetLoadError, acSetPluginLoading } from '../../actions/loader.js' import { acAddMetadata } from '../../actions/metadata.js' -import { acSetUiItems, acAddParentGraphMap } from '../../actions/ui.js' +import { + acSetUiItems, + acSetUiDataSorting, + acAddParentGraphMap, +} from '../../actions/ui.js' import { AssignedCategoriesDataElementsError, GenericServerError, @@ -94,6 +98,15 @@ export class UnconnectedVisualization extends Component { onChartGenerated = (svg) => this.props.setChart(svg) + onOutlierTableSort = (sorting) => { + this.props.onLoadingStart() + + this.props.setUiDataSorting(sorting) + + // simulate an update for refreshing the visualization + this.props.setCurrent() + } + onResponsesReceived = (responses) => { const forMetadata = {} @@ -109,14 +122,16 @@ export class UnconnectedVisualization extends Component { throw new ValueTypeError() } - Object.entries(response.metaData.items).forEach(([id, item]) => { - forMetadata[id] = { - id, - name: item.name || item.displayName, - displayName: item.displayName, - dimensionItemType: item.dimensionItemType, + Object.entries(response?.metaData?.items || []).forEach( + ([id, item]) => { + forMetadata[id] = { + id, + name: item.name || item.displayName, + displayName: item.displayName, + dimensionItemType: item.dimensionItemType, + } } - }) + ) }) this.props.addMetadata(forMetadata) @@ -211,6 +226,7 @@ export class UnconnectedVisualization extends Component { visualization={visualization} onChartGenerated={this.onChartGenerated} onLoadingComplete={onLoadingComplete} + onOutlierTableSort={this.onOutlierTableSort} onResponsesReceived={this.onResponsesReceived} onError={this.onError} onDrill={this.onDrill} @@ -232,9 +248,11 @@ UnconnectedVisualization.propTypes = { setChart: PropTypes.func, setCurrent: PropTypes.func, setLoadError: PropTypes.func, + setUiDataSorting: PropTypes.func, setUiItems: PropTypes.func, visualization: PropTypes.object, onLoadingComplete: PropTypes.func, + onLoadingStart: PropTypes.func, } const mapStateToProps = (state) => ({ @@ -247,12 +265,14 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ onLoadingComplete: () => dispatch(acSetPluginLoading(false)), + onLoadingStart: () => dispatch(acSetPluginLoading(true)), addMetadata: (metadata) => dispatch(acAddMetadata(metadata)), addParentGraphMap: (parentGraphMap) => dispatch(acAddParentGraphMap(parentGraphMap)), setChart: (chart) => dispatch(acSetChart(chart)), setLoadError: (error) => dispatch(acSetLoadError(error)), setUiItems: (data) => dispatch(acSetUiItems(data)), + setUiDataSorting: (sorting) => dispatch(acSetUiDataSorting(sorting)), setCurrent: () => dispatch(tSetCurrentFromUi()), }) diff --git a/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js b/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js index 6d3a609b81..e3b9aadf1b 100644 --- a/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js +++ b/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js @@ -1,10 +1,10 @@ import i18n from '@dhis2/d2-i18n' import { FieldSet, Legend } from '@dhis2/ui' -import PropTypes from 'prop-types' import React from 'react' -import { connect } from 'react-redux' -import { acSetUiOptions } from '../../../actions/ui.js' -import { sGetUiOptions } from '../../../reducers/ui.js' +import { useDispatch, useSelector } from 'react-redux' +import { acSetUiDataSorting, acSetUiOptions } from '../../../actions/ui.js' +import { getDefaultSorting } from '../../../modules/ui.js' +import { sGetUi, sGetUiOptions } from '../../../reducers/ui.js' import { tabSectionOption, tabSectionTitle, @@ -37,9 +37,50 @@ export const DEFAULT_STATE = { [OUTLIER_THRESHOLD_PROP]: 3, } -const Outliers = ({ outlierAnalysis, onChange }) => { - const storeProp = (prop, value) => - onChange({ ...outlierAnalysis, [prop]: value }) +const Outliers = () => { + const dispatch = useDispatch() + + const outlierAnalysis = useSelector(sGetUiOptions)[ + OUTLIER_ANALYSIS_OPTION_NAME + ] || { + ...OUTLIER_MAX_RESULTS_DEFAULT_STATE, + ...DEFAULT_STATE, + } + + const sorting = useSelector(sGetUi).sorting + + const onMethodChange = (value) => { + dispatch( + acSetUiOptions({ + [OUTLIER_ANALYSIS_OPTION_NAME]: { + ...outlierAnalysis, + [OUTLIER_METHOD_PROP]: value, + [OUTLIER_THRESHOLD_PROP]: methods.find( + (item) => item.id === value + ).defaultThreshold, + }, + }) + ) + + // reset sorting to avoid sorting on non existing column + // in case the sorting was on a column specific to a method + if ( + sorting?.dimension && + !['value', 'lowerbound', 'upperbound'].includes(sorting.dimension) + ) { + dispatch(acSetUiDataSorting(getDefaultSorting())) + } + } + + const onThresholdChange = (value) => + dispatch( + acSetUiOptions({ + [OUTLIER_ANALYSIS_OPTION_NAME]: { + ...outlierAnalysis, + [OUTLIER_THRESHOLD_PROP]: value, + }, + }) + ) return (
@@ -52,18 +93,8 @@ const Outliers = ({ outlierAnalysis, onChange }) => {
- onChange({ - ...outlierAnalysis, - [OUTLIER_METHOD_PROP]: value, - [OUTLIER_THRESHOLD_PROP]: methods.find( - (item) => item.id === value - ).defaultThreshold, - }) - } - onThresholdChange={(value) => - storeProp(OUTLIER_THRESHOLD_PROP, value) - } + onMethodChange={onMethodChange} + onThresholdChange={onThresholdChange} currentMethodId={outlierAnalysis[OUTLIER_METHOD_PROP]} currentThreshold={ outlierAnalysis[OUTLIER_THRESHOLD_PROP] @@ -75,22 +106,4 @@ const Outliers = ({ outlierAnalysis, onChange }) => { ) } -Outliers.propTypes = { - outlierAnalysis: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, -} - -const mapStateToProps = (state) => ({ - // OutliersMaxResults is part of the root option name, so we need to merge the default states - outlierAnalysis: sGetUiOptions(state)[OUTLIER_ANALYSIS_OPTION_NAME] || { - ...DEFAULT_STATE, - ...OUTLIER_MAX_RESULTS_DEFAULT_STATE, - }, -}) - -const mapDispatchToProps = (dispatch) => ({ - onChange: (value) => - dispatch(acSetUiOptions({ [OUTLIER_ANALYSIS_OPTION_NAME]: value })), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(Outliers) +export default Outliers diff --git a/src/components/VisualizationPlugin/OutlierTablePlugin.js b/src/components/VisualizationPlugin/OutlierTablePlugin.js index 0f6faafaca..1c496910d3 100644 --- a/src/components/VisualizationPlugin/OutlierTablePlugin.js +++ b/src/components/VisualizationPlugin/OutlierTablePlugin.js @@ -1,4 +1,5 @@ import { getFixedDimensions } from '@dhis2/analytics' +import i18n from '@dhis2/d2-i18n' import { DataTable, DataTableBody, @@ -9,7 +10,7 @@ import { } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React from 'react' +import React, { useCallback, useMemo } from 'react' import { getOutlierTableHeadersMap } from '../../modules/analytics.js' import { DISPLAY_DENSITY_COMFORTABLE, @@ -18,6 +19,10 @@ import { FONT_SIZE_NORMAL, FONT_SIZE_SMALL, } from '../../modules/options.js' +import { + getDefaultSorting, + getSortingFromVisualization, +} from '../../modules/ui.js' import styles from './styles/OutlierTablePlugin.module.css' const getFontSizeClass = (fontSize) => { @@ -48,7 +53,7 @@ const OutlierTablePlugin = ({ visualization, //style, //id: renderCounter, - //onToggleContextualMenu, + onDataSorted, }) => { const data = responses[0] const headersMap = getOutlierTableHeadersMap({ @@ -56,6 +61,25 @@ const OutlierTablePlugin = ({ }) const fixedDimensions = getFixedDimensions() + // const isInModal = false // TODO !!filters?.relativePeriodDate + + const defaultSorting = useMemo(() => getDefaultSorting(), []) + + const getSorting = useCallback( + (visualization) => { + const sorting = + getSortingFromVisualization(visualization) || defaultSorting + + return { + sortField: sorting.dimension, + sortDirection: sorting.direction, + } + }, + [defaultSorting] + ) + + const { sortField, sortDirection } = getSorting(visualization) + const sizeClass = getSizeClass(visualization.displayDensity) const fontSizeClass = getFontSizeClass(visualization.fontSize) @@ -71,7 +95,7 @@ const OutlierTablePlugin = ({ return undefined } - const renderHeaderCell = ({ name, column }) => { + const renderHeaderCell = ({ name, column, valueType }) => { const columnName = lookupColumnName(name) || column return ( @@ -80,6 +104,25 @@ const OutlierTablePlugin = ({ top="0" key={name} name={name} + onSortIconClick={valueType !== 'NUMBER' ? undefined : sortData} + sortDirection={ + valueType !== 'NUMBER' + ? undefined + : name === sortField + ? sortDirection + : 'default' + } + sortIconTitle={ + name === sortField && sortDirection === 'desc' + ? i18n.t( + 'Sort ascending by {{columnName}} and update', + { columnName } + ) + : i18n.t( + 'Sort descending by {{columnName}} and update', + { columnName } + ) + } className={cx( styles.headerCell, fontSizeClass, @@ -103,6 +146,17 @@ const OutlierTablePlugin = ({ ) + const sortData = ({ name }) => { + const direction = + sortField === name + ? sortDirection === 'desc' + ? 'asc' + : 'desc' + : 'desc' + + onDataSorted({ dimension: name, direction }) + } + return (
{ @@ -197,7 +198,7 @@ export const VisualizationPlugin = ({ // multiple responses are only for YOY which does not support legends or custom icon // safe to use only the 1st // dx dimensions might not be present, the empty array covers that case - const dxIds = responses[0].metaData.dimensions.dx || [] + const dxIds = responses[0].metaData.dimensions?.dx || [] // DHIS2-10496: show icon on the side of the single value if an icon is assigned in Maintenance app and // the "Show data item icon" option is set in DV options @@ -414,9 +415,9 @@ export const VisualizationPlugin = ({ fetchResult.visualization )} responses={fetchResult.responses} - legendSets={legendSets} id={id} style={transformedStyle} + onDataSorted={onOutlierTableSort} /> ) } else { @@ -469,6 +470,7 @@ VisualizationPlugin.defaultProps = { onChartGenerated: Function.prototype, onError: Function.prototype, onLoadingComplete: Function.prototype, + onOutlierTableSort: Function.prototype, onResponsesReceived: Function.prototype, style: {}, visualization: {}, @@ -484,5 +486,6 @@ VisualizationPlugin.propTypes = { onDrill: PropTypes.func, onError: PropTypes.func, onLoadingComplete: PropTypes.func, + onOutlierTableSort: PropTypes.func, onResponsesReceived: PropTypes.func, } diff --git a/src/modules/current.js b/src/modules/current.js index 6f3a57a3ea..85be0e6f2d 100644 --- a/src/modules/current.js +++ b/src/modules/current.js @@ -163,6 +163,14 @@ export const getOutlierTableCurrentFromUi = (state, value) => { [BASE_FIELD_TYPE]: ui.type, ...outlierTableAxesFromUi, ...getOptionsFromUi(ui), + sorting: ui.sorting + ? [ + { + dimension: ui.sorting.dimension, + direction: ui.sorting.direction.toUpperCase(), + }, + ] + : undefined, } } diff --git a/src/modules/fields/baseFields.js b/src/modules/fields/baseFields.js index 5d856105c3..462ae2420a 100644 --- a/src/modules/fields/baseFields.js +++ b/src/modules/fields/baseFields.js @@ -117,6 +117,7 @@ export const fieldsByType = { getFieldObject('relativePeriods', { excluded: true }), getFieldObject('rows'), getFieldObject('shortName'), + getFieldObject('sorting'), getFieldObject('sortOrder', { option: true }), getFieldObject('subscribed'), getFieldObject('subscribers'), diff --git a/src/modules/ui.js b/src/modules/ui.js index 39b6cc6f10..f91179ec1c 100644 --- a/src/modules/ui.js +++ b/src/modules/ui.js @@ -25,6 +25,11 @@ export const SERIES_ITEM_AXIS_PROP = 'axis' export const ITEM_ATTRIBUTE_VERTICAL = 'VERTICAL' export const ITEM_ATTRIBUTE_HORIZONTAL = 'HORIZONTAL' +export const getDefaultSorting = () => ({ + dimension: 'value', + direction: 'desc', +}) + // Transform from backend model to store.ui format export const getUiFromVisualization = (vis, currentState = {}) => { const visType = vis.type || defaultVisType @@ -178,3 +183,12 @@ export const mergeUiMaps = (destinationMap, sourceMap, propName) => { destinationMap[key][propName] = sourceMap[key] }) } + +export const getSortingFromVisualization = (visualization) => { + return visualization.sorting?.length + ? { + dimension: visualization.sorting[0].dimension, + direction: visualization.sorting[0].direction.toLowerCase(), + } + : undefined +} diff --git a/src/reducers/ui.js b/src/reducers/ui.js index 3ccea423a1..11f49c2fa8 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -55,6 +55,7 @@ import { } from '../modules/options.js' import { getAdaptedUiByType, + getDefaultSorting, getUiFromVisualization, SERIES_ITEM_TYPE_PROP, } from '../modules/ui.js' @@ -66,6 +67,8 @@ export const SET_UI_DISABLED_OPTIONS = 'SET_UI_DISABLED_OPTIONS' export const SET_UI_OPTIONS = 'SET_UI_OPTIONS' export const SET_UI_OPTION = 'SET_UI_OPTION' export const SET_UI_OPTION_FONT_STYLE = 'SET_UI_OPTION_FONT_STYLE' +export const SET_UI_DATA_SORTING = 'SET_UI_DATA_SORTING' +export const CLEAR_UI_DATA_SORTING = 'CLEAR_UI_DATA_SORTING' export const SET_UI_LAYOUT = 'SET_UI_LAYOUT' export const ADD_UI_LAYOUT_DIMENSIONS = 'ADD_UI_LAYOUT_DIMENSIONS' export const REMOVE_UI_LAYOUT_DIMENSIONS = 'REMOVE_UI_LAYOUT_DIMENSIONS' @@ -150,6 +153,9 @@ const getDefaultUiByType = (ui) => { }), }, }, + sorting: { + ...(ui.sorting ?? getDefaultSorting()), + }, } } default: { @@ -476,6 +482,20 @@ export default (state = DEFAULT_UI, action) => { }, } } + case CLEAR_UI_DATA_SORTING: { + return { + ...state, + sorting: undefined, + } + } + case SET_UI_DATA_SORTING: { + return { + ...state, + sorting: { + ...action.value, + }, + } + } case SET_UI_LAYOUT: { return { ...state, From 0d0ebfad5203596eda4a165ad8477160e0374974 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 24 Jan 2024 15:20:41 +0100 Subject: [PATCH 10/31] feat: enable only data elements in data dialog (DHIS2-16433) --- .../DimensionsPanel/Dialogs/DialogManager.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/DimensionsPanel/Dialogs/DialogManager.js b/src/components/DimensionsPanel/Dialogs/DialogManager.js index 30c1be7e43..ccab6397f4 100644 --- a/src/components/DimensionsPanel/Dialogs/DialogManager.js +++ b/src/components/DimensionsPanel/Dialogs/DialogManager.js @@ -4,9 +4,11 @@ import { PeriodDimension, OrgUnitDimension, ouIdHelper, + dataTypeMap, DIMENSION_ID_DATA, DIMENSION_ID_PERIOD, DIMENSION_ID_ORGUNIT, + DIMENSION_TYPE_DATA_ELEMENT, getDimensionMaxNumberOfItems, getAxisMaxNumberOfItems, getDisplayNameByVisType, @@ -22,6 +24,7 @@ import { MONTHLY, BIMONTHLY, ALL_DYNAMIC_DIMENSION_ITEMS, + VIS_TYPE_OUTLIER_TABLE, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import { @@ -259,7 +262,7 @@ export class DialogManager extends Component { } renderDialogContent = () => { - const { displayNameProperty, dialogId, type } = this.props + const { displayNameProperty, dialogId, type: visType } = this.props const dimensionProps = { onSelect: this.selectUiItems, @@ -270,7 +273,6 @@ export class DialogManager extends Component { let infoBoxMessage const axisId = this.props.getAxisIdByDimensionId(dialogId) - const visType = type const numberOfItems = selectedItems.length const dimensionMaxNumberOfItems = getDimensionMaxNumberOfItems( @@ -324,6 +326,16 @@ export class DialogManager extends Component { dialogId === DIMENSION_ID_DATA || isScatterAttribute(dialogId) ) { + const dataTypes = Object.values(dataTypeMap).filter( + ({ id }) => { + if (visType === VIS_TYPE_OUTLIER_TABLE) { + return id === DIMENSION_TYPE_DATA_ELEMENT + } + + return true + } + ) + const onSelect = isScatterAttribute(dialogId) ? (defaultProps) => this.selectUiItems({ @@ -343,11 +355,13 @@ export class DialogManager extends Component { } const dimensionSelector = ( ) const dataTabs = isScatterAttribute(dialogId) ? ( From 3bfdd6d9979ace9fd71e709df50152340b4aafc0 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 26 Jan 2024 16:09:38 +0100 Subject: [PATCH 11/31] fix: adjust download menu and request (DHIS2-16595) --- src/api/analytics.js | 25 +++- .../DownloadMenu/AdvancedSubMenu.js | 1 + src/components/DownloadMenu/DownloadMenu.js | 37 +++-- src/components/DownloadMenu/useDownload.js | 139 +++++++++++------- 4 files changed, 126 insertions(+), 76 deletions(-) diff --git a/src/api/analytics.js b/src/api/analytics.js index 66b8f7b837..2691270213 100644 --- a/src/api/analytics.js +++ b/src/api/analytics.js @@ -35,11 +35,12 @@ export const apiFetchAnalytics = async (dataEngine, visualization, options) => { return [new analyticsEngine.response(rawResponse)] } -export const apiFetchAnalyticsForOutlierTable = async ( - dataEngine, +export const getAnalyticsRequestForOutlierTable = ({ + analyticsEngine, visualization, - options -) => { + options, + forDownload = false, +}) => { const headersMap = getOutlierTableHeadersMap(options) const parameters = { @@ -59,7 +60,7 @@ export const apiFetchAnalyticsForOutlierTable = async ( columns.forEach(({ dimension, items }) => { parameters[dimension] = items.map(({ id }) => id).join(',') - headers.push(headersMap[dimension]) + headers.push(forDownload ? dimension : headersMap[dimension]) }) headers.push('value') @@ -84,9 +85,21 @@ export const apiFetchAnalyticsForOutlierTable = async ( parameters.sortOrder = sorting.direction } + return new analyticsEngine.request().withParameters(parameters) +} + +export const apiFetchAnalyticsForOutlierTable = async ( + dataEngine, + visualization, + options +) => { const analyticsEngine = Analytics.getAnalytics(dataEngine) - const req = new analyticsEngine.request().withParameters(parameters) + const req = getAnalyticsRequestForOutlierTable({ + analyticsEngine, + visualization, + options, + }) const rawResponse = await analyticsEngine.aggregate.getOutliersData(req) diff --git a/src/components/DownloadMenu/AdvancedSubMenu.js b/src/components/DownloadMenu/AdvancedSubMenu.js index 0f536d3aaf..80f6b1dbb5 100644 --- a/src/components/DownloadMenu/AdvancedSubMenu.js +++ b/src/components/DownloadMenu/AdvancedSubMenu.js @@ -24,6 +24,7 @@ export const AdvancedSubMenu = ({ {visType === VIS_TYPE_PIVOT_TABLE ? ( - ) : ( + ) : visType !== VIS_TYPE_OUTLIER_TABLE ? ( - )} + ) : null} - + {visType !== VIS_TYPE_OUTLIER_TABLE && ( + + )} - + {visType !== VIS_TYPE_OUTLIER_TABLE && ( + + )} ) } diff --git a/src/components/DownloadMenu/useDownload.js b/src/components/DownloadMenu/useDownload.js index 07a32c8357..5d15aeb416 100644 --- a/src/components/DownloadMenu/useDownload.js +++ b/src/components/DownloadMenu/useDownload.js @@ -1,7 +1,8 @@ -import { Analytics } from '@dhis2/analytics' +import { Analytics, VIS_TYPE_OUTLIER_TABLE } from '@dhis2/analytics' import { useConfig, useDataEngine, useDataMutation } from '@dhis2/app-runtime' import { useCallback } from 'react' import { useSelector } from 'react-redux' +import { getAnalyticsRequestForOutlierTable } from '../../api/analytics.js' import { sGetChart } from '../../reducers/chart.js' import { sGetCurrent } from '../../reducers/current.js' import { @@ -106,62 +107,87 @@ const useDownload = (relativePeriodDate) => { let req = new analyticsEngine.request() let target = '_top' - switch (type) { - case DOWNLOAD_TYPE_TABLE: - req = req - .fromVisualization(visualization) - .withFormat(format) - .withTableLayout() - .withColumns(columns.join(';')) - .withRows(rows.join(';')) - - req = addCommonParameters(req, visualization, { - relativePeriodDate, - }) - - if (visualization.hideEmptyColumns) { - req = req.withHideEmptyColumns() - } - - if (visualization.hideEmptyRows) { - req = req.withHideEmptyRows() - } - - if (visualization.showHierarchy) { - req = req.withShowHierarchy() - } - - target = format === FILE_FORMAT_HTML_CSS ? '_blank' : '_top' - - break - case DOWNLOAD_TYPE_PLAIN: - req = req - .fromVisualization( - visualization, - path === 'dataValueSet' + if (visType === VIS_TYPE_OUTLIER_TABLE) { + // only DOWNLOAD_TYPE_PLAIN is enabled + // open JSON in new tab + target = [FILE_FORMAT_CSV, FILE_FORMAT_XLS].includes(format) + ? '_top' + : '_blank' + + req = getAnalyticsRequestForOutlierTable({ + analyticsEngine, + visualization, + options: { showHierarchy: visualization.showHierarchy }, + forDownload: true, + }) + + // TODO withRelativePeriodDate + + req = req + .withFormat(format) + .withOutputIdScheme(idScheme) + .withPath('outlierDetection') + } else { + switch (type) { + case DOWNLOAD_TYPE_TABLE: + req = req + .fromVisualization(visualization) + .withFormat(format) + .withTableLayout() + .withColumns(columns.join(';')) + .withRows(rows.join(';')) + + req = addCommonParameters(req, visualization, { + relativePeriodDate, + }) + + if (visualization.hideEmptyColumns) { + req = req.withHideEmptyColumns() + } + + if (visualization.hideEmptyRows) { + req = req.withHideEmptyRows() + } + + if (visualization.showHierarchy) { + req = req.withShowHierarchy() + } + + target = + format === FILE_FORMAT_HTML_CSS ? '_blank' : '_top' + + break + case DOWNLOAD_TYPE_PLAIN: + req = req + .fromVisualization( + visualization, + path === 'dataValueSet' + ) + .withFormat(format) + .withShowHierarchy(visualization.showHierarchy) + .withHierarchyMeta(visualization.showHierarchy) + .withIncludeMetadataDetails(true) + .withIncludeNumDen() + + req = addCommonParameters(req, visualization, { + relativePeriodDate, + }) + + if (path) { + req = req.withPath(path) + } + + if (idScheme) { + req = req.withOutputIdScheme(idScheme) + } + + target = [FILE_FORMAT_CSV, FILE_FORMAT_XLS].includes( + format ) - .withFormat(format) - .withShowHierarchy(visualization.showHierarchy) - .withHierarchyMeta(visualization.showHierarchy) - .withIncludeMetadataDetails(true) - .withIncludeNumDen() - - req = addCommonParameters(req, visualization, { - relativePeriodDate, - }) - - if (path) { - req = req.withPath(path) - } - - if (idScheme) { - req = req.withOutputIdScheme(idScheme) - } - - target = [FILE_FORMAT_CSV, FILE_FORMAT_XLS].includes(format) - ? '_top' - : '_blank' - break + ? '_top' + : '_blank' + break + } } const url = new URL( @@ -182,6 +208,7 @@ const useDownload = (relativePeriodDate) => { relativePeriodDate, rows, visualization, + visType, ] ) From 3d2ba78af43aaf1607aee64b7eed416823a622dc Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 9 Feb 2024 10:23:06 +0100 Subject: [PATCH 12/31] fix: add missing period at the end of the text --- i18n/en.pot | 8 ++++---- src/modules/visualization.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0c73504e40..651e3fb869 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: 2024-01-29T10:57:59.794Z\n" -"PO-Revision-Date: 2024-01-29T10:57:59.794Z\n" +"POT-Creation-Date: 2024-02-09T13:14:55.751Z\n" +"PO-Revision-Date: 2024-02-09T13:14:55.751Z\n" msgid "All items" msgstr "All items" @@ -1126,8 +1126,8 @@ msgstr "" "View the relationship between two data items at a place or time. " "Recommended for finding outliers." -msgid "Automatically identify extreme outliers in the data" -msgstr "Automatically identify extreme outliers in the data" +msgid "Automatically identify extreme outliers in the data." +msgstr "Automatically identify extreme outliers in the data." msgid "Weeks per year" msgstr "Weeks per year" diff --git a/src/modules/visualization.js b/src/modules/visualization.js index 5b1ba6574f..ca94625cbb 100644 --- a/src/modules/visualization.js +++ b/src/modules/visualization.js @@ -88,7 +88,7 @@ export const getVisTypeDescriptions = () => ({ 'View the relationship between two data items at a place or time. Recommended for finding outliers.' ), [VIS_TYPE_OUTLIER_TABLE]: i18n.t( - 'Automatically identify extreme outliers in the data' + 'Automatically identify extreme outliers in the data.' ), }) From 09f3f8f8a427992d01871762666aeab01a45c92f Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 9 Feb 2024 16:27:36 +0100 Subject: [PATCH 13/31] fix: pass relativePeriodDate and displayProperty in donwload (DHIS2-16769) --- src/components/DownloadMenu/useDownload.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/DownloadMenu/useDownload.js b/src/components/DownloadMenu/useDownload.js index 5d15aeb416..0d4a4e10dc 100644 --- a/src/components/DownloadMenu/useDownload.js +++ b/src/components/DownloadMenu/useDownload.js @@ -5,6 +5,7 @@ import { useSelector } from 'react-redux' import { getAnalyticsRequestForOutlierTable } from '../../api/analytics.js' import { sGetChart } from '../../reducers/chart.js' import { sGetCurrent } from '../../reducers/current.js' +import { sGetSettingsDisplayProperty } from '../../reducers/settings.js' import { sGetUiType, sGetUiLayoutColumns, @@ -55,6 +56,7 @@ const addCommonParameters = (req, visualization, options) => { } const useDownload = (relativePeriodDate) => { + const displayProperty = useSelector(sGetSettingsDisplayProperty) const visualization = useSelector(sGetCurrent) const visType = useSelector(sGetUiType) const chart = useSelector(sGetChart) @@ -121,7 +123,13 @@ const useDownload = (relativePeriodDate) => { forDownload: true, }) - // TODO withRelativePeriodDate + if (relativePeriodDate) { + req = req.withRelativePeriodDate(relativePeriodDate) + } + + if (displayProperty) { + req = req.withDisplayProperty(displayProperty) + } req = req .withFormat(format) @@ -205,6 +213,7 @@ const useDownload = (relativePeriodDate) => { analyticsEngine, baseUrl, columns, + displayProperty, relativePeriodDate, rows, visualization, From 24ef5dcc9acfbf8498600cdf97bc05ac77843ceb Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 12 Feb 2024 14:17:27 +0100 Subject: [PATCH 14/31] fix: support sorting in both interpretation modal and plugin (DHIS2-16772) --- src/PluginWrapper.js | 25 ++-------------- .../InterpretationModal.js | 4 +-- src/components/Visualization/Visualization.js | 4 +-- .../VisualizationPlugin.js | 8 ++--- .../VisualizationPluginWrapper.js | 29 +++++++++++++++++++ 5 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 src/components/VisualizationPlugin/VisualizationPluginWrapper.js diff --git a/src/PluginWrapper.js b/src/PluginWrapper.js index 2cee6a1783..0365f0af28 100644 --- a/src/PluginWrapper.js +++ b/src/PluginWrapper.js @@ -3,8 +3,8 @@ import { CssVariables, CenteredContent, CircularLoader, Layer } from '@dhis2/ui' import postRobot from '@krakenjs/post-robot' import debounce from 'lodash-es/debounce' import PropTypes from 'prop-types' -import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react' -import { VisualizationPlugin } from './components/VisualizationPlugin/VisualizationPlugin.js' +import React, { useEffect, useLayoutEffect, useState } from 'react' +import { VisualizationPluginWrapper } from './components/VisualizationPlugin/VisualizationPluginWrapper.js' import { getPWAInstallationStatus } from './modules/getPWAInstallationStatus.js' const LoadingMask = () => { @@ -79,24 +79,6 @@ const PluginWrapper = () => { const receivePropsFromParent = (event) => setPropsFromParent(event.data) - const onOutlierTableSort = useCallback( - (sorting) => { - const newSorting = { - dimension: sorting.dimension, - direction: sorting.direction.toUpperCase(), - } - - setPropsFromParent({ - ...propsFromParent, - visualization: { - ...propsFromParent.visualization, - sorting: [newSorting], - }, - }) - }, - [propsFromParent] - ) - useEffect(() => { postRobot .send(window.parent, 'getProps') @@ -146,10 +128,9 @@ const PluginWrapper = () => { cacheNow={propsFromParent.recordOnNextLoad} isParentCached={propsFromParent.isParentCached} > - diff --git a/src/components/InterpretationModal/InterpretationModal.js b/src/components/InterpretationModal/InterpretationModal.js index 71b61d709b..8a60ca4965 100644 --- a/src/components/InterpretationModal/InterpretationModal.js +++ b/src/components/InterpretationModal/InterpretationModal.js @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react' import { useSelector } from 'react-redux' import { sGetCurrent } from '../../reducers/current.js' import { ModalDownloadDropdown } from '../DownloadMenu/index.js' -import { VisualizationPlugin } from '../VisualizationPlugin/VisualizationPlugin.js' +import { VisualizationPluginWrapper } from '../VisualizationPlugin/VisualizationPluginWrapper.js' import { useInterpretationQueryParams, removeInterpretationQueryParams, @@ -30,7 +30,7 @@ const InterpretationModal = ({ onInterpretationUpdate }, context) => { onResponsesReceived={() => setIsVisualizationLoading(false)} visualization={visualization} downloadMenuComponent={ModalDownloadDropdown} - pluginComponent={VisualizationPlugin} + pluginComponent={VisualizationPluginWrapper} /> ) : null } diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index 8fc1c618ba..bb1881ea1c 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -98,7 +98,7 @@ export class UnconnectedVisualization extends Component { onChartGenerated = (svg) => this.props.setChart(svg) - onOutlierTableSort = (sorting) => { + onDataSorted = (sorting) => { this.props.onLoadingStart() this.props.setUiDataSorting(sorting) @@ -226,7 +226,7 @@ export class UnconnectedVisualization extends Component { visualization={visualization} onChartGenerated={this.onChartGenerated} onLoadingComplete={onLoadingComplete} - onOutlierTableSort={this.onOutlierTableSort} + onDataSorted={this.onDataSorted} onResponsesReceived={this.onResponsesReceived} onError={this.onError} onDrill={this.onDrill} diff --git a/src/components/VisualizationPlugin/VisualizationPlugin.js b/src/components/VisualizationPlugin/VisualizationPlugin.js index a903eb76ff..93664880d6 100644 --- a/src/components/VisualizationPlugin/VisualizationPlugin.js +++ b/src/components/VisualizationPlugin/VisualizationPlugin.js @@ -37,7 +37,7 @@ export const VisualizationPlugin = ({ onChartGenerated, onError, onLoadingComplete, - onOutlierTableSort, + onDataSorted, onResponsesReceived, onDrill, }) => { @@ -417,7 +417,7 @@ export const VisualizationPlugin = ({ responses={fetchResult.responses} id={id} style={transformedStyle} - onDataSorted={onOutlierTableSort} + onDataSorted={onDataSorted} /> ) } else { @@ -470,7 +470,7 @@ VisualizationPlugin.defaultProps = { onChartGenerated: Function.prototype, onError: Function.prototype, onLoadingComplete: Function.prototype, - onOutlierTableSort: Function.prototype, + onDataSorted: Function.prototype, onResponsesReceived: Function.prototype, style: {}, visualization: {}, @@ -483,9 +483,9 @@ VisualizationPlugin.propTypes = { id: PropTypes.number, style: PropTypes.object, onChartGenerated: PropTypes.func, + onDataSorted: PropTypes.func, onDrill: PropTypes.func, onError: PropTypes.func, onLoadingComplete: PropTypes.func, - onOutlierTableSort: PropTypes.func, onResponsesReceived: PropTypes.func, } diff --git a/src/components/VisualizationPlugin/VisualizationPluginWrapper.js b/src/components/VisualizationPlugin/VisualizationPluginWrapper.js new file mode 100644 index 0000000000..ae763a1fe4 --- /dev/null +++ b/src/components/VisualizationPlugin/VisualizationPluginWrapper.js @@ -0,0 +1,29 @@ +import React, { useCallback, useState } from 'react' +import { VisualizationPlugin } from '../VisualizationPlugin/VisualizationPlugin.js' + +// handle internal state for features that need to work without the app's Redux store +const VisualizationPluginWrapper = (props) => { + const [pluginProps, setPluginProps] = useState(props) + + const onDataSorted = useCallback( + (sorting) => { + const newSorting = { + dimension: sorting.dimension, + direction: sorting.direction.toUpperCase(), + } + + setPluginProps({ + ...pluginProps, + visualization: { + ...pluginProps.visualization, + sorting: [newSorting], + }, + }) + }, + [pluginProps] + ) + + return +} + +export { VisualizationPluginWrapper } From be7e4dbb71bab55c14fa4af8efb44ec38c350e65 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 12 Feb 2024 14:46:04 +0100 Subject: [PATCH 15/31] fix: respect DGS from options (DHIS2-16773) --- .../VisualizationPlugin/OutlierTablePlugin.js | 85 +++++++++++++++---- .../styles/OutlierTablePlugin.module.css | 3 + 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/components/VisualizationPlugin/OutlierTablePlugin.js b/src/components/VisualizationPlugin/OutlierTablePlugin.js index 1c496910d3..0e07a5c1cd 100644 --- a/src/components/VisualizationPlugin/OutlierTablePlugin.js +++ b/src/components/VisualizationPlugin/OutlierTablePlugin.js @@ -1,4 +1,18 @@ -import { getFixedDimensions } from '@dhis2/analytics' +import { + formatValue, + getFixedDimensions, + VALUE_TYPE_NUMBER, + VALUE_TYPE_INTEGER, + VALUE_TYPE_INTEGER_POSITIVE, + VALUE_TYPE_INTEGER_NEGATIVE, + VALUE_TYPE_INTEGER_ZERO_OR_POSITIVE, + VALUE_TYPE_PERCENTAGE, + VALUE_TYPE_UNIT_INTERVAL, + VALUE_TYPE_TIME, + VALUE_TYPE_DATE, + VALUE_TYPE_DATETIME, + VALUE_TYPE_TEXT, +} from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import { DataTable, @@ -95,6 +109,20 @@ const OutlierTablePlugin = ({ return undefined } + const cellValueShouldNotWrap = (header) => + [ + VALUE_TYPE_NUMBER, + VALUE_TYPE_INTEGER, + VALUE_TYPE_INTEGER_POSITIVE, + VALUE_TYPE_INTEGER_NEGATIVE, + VALUE_TYPE_INTEGER_ZERO_OR_POSITIVE, + VALUE_TYPE_PERCENTAGE, + VALUE_TYPE_UNIT_INTERVAL, + VALUE_TYPE_TIME, + VALUE_TYPE_DATE, + VALUE_TYPE_DATETIME, + ].includes(header.valueType) + const renderHeaderCell = ({ name, column, valueType }) => { const columnName = lookupColumnName(name) || column @@ -104,9 +132,11 @@ const OutlierTablePlugin = ({ top="0" key={name} name={name} - onSortIconClick={valueType !== 'NUMBER' ? undefined : sortData} + onSortIconClick={ + valueType !== VALUE_TYPE_NUMBER ? undefined : sortData + } sortDirection={ - valueType !== 'NUMBER' + valueType !== VALUE_TYPE_NUMBER ? undefined : name === sortField ? sortDirection @@ -136,15 +166,17 @@ const OutlierTablePlugin = ({ ) } - const renderValueCell = ({ columnIndex, value }) => ( - - {value} - - ) + const formatValueCell = (value, header) => + formatValue( + value, + header.valueType || VALUE_TYPE_TEXT, + header.optionSet + ? {} + : { + digitGroupSeparator: visualization.digitGroupSeparator, + skipRounding: false, + } + ) const sortData = ({ name }) => { const direction = @@ -173,12 +205,29 @@ const OutlierTablePlugin = ({ {data.rows.map((row, rowIndex) => ( - {row.map((value, columnIndex) => - renderValueCell({ - columnIndex, - value, - }) - )} + {row.map((value, columnIndex) => ( + + {formatValueCell( + value, + data.headers[columnIndex] + )} + + ))} ))} diff --git a/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css b/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css index 59563b7ef9..8f8e24a877 100644 --- a/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css +++ b/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css @@ -43,6 +43,9 @@ font-size: 11px; line-height: 12px; } +.dataTable .cell.nowrap { + white-space: nowrap; +} .dataTable .cell.sizeComfortable { padding: 10px 8px 8px; } From f8cd2adf4b8ad13b6bfa9ce30ee5cbff733800b7 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 13 Feb 2024 16:38:47 +0100 Subject: [PATCH 16/31] fix: use a better message for no outliers (DHIS2-16798) --- i18n/en.pot | 10 ++++++++-- src/components/Visualization/Visualization.js | 11 ++++++++++- src/modules/error.js | 12 ++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 651e3fb869..3315e93127 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: 2024-02-09T13:14:55.751Z\n" -"PO-Revision-Date: 2024-02-09T13:14:55.751Z\n" +"POT-Creation-Date: 2024-02-13T15:33:28.483Z\n" +"PO-Revision-Date: 2024-02-13T15:33:28.483Z\n" msgid "All items" msgstr "All items" @@ -864,6 +864,12 @@ msgstr "" msgid "There's a syntax problem with the analytics request." msgstr "There's a syntax problem with the analytics request." +msgid "No outliers found" +msgstr "No outliers found" + +msgid "There were no outliers found for the selected data items and options." +msgstr "There were no outliers found for the selected data items and options." + msgid "or" msgstr "or" diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index bb1881ea1c..8d3f402f62 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -1,4 +1,8 @@ -import { DIMENSION_ID_DATA, VIS_TYPE_PIVOT_TABLE } from '@dhis2/analytics' +import { + DIMENSION_ID_DATA, + VIS_TYPE_OUTLIER_TABLE, + VIS_TYPE_PIVOT_TABLE, +} from '@dhis2/analytics' import debounce from 'lodash-es/debounce' import PropTypes from 'prop-types' import React, { Component, Fragment } from 'react' @@ -25,6 +29,7 @@ import { ValueTypeError, AnalyticsGenerationError, AnalyticsRequestError, + NoOutliersError, } from '../../modules/error.js' import { removeLastPathSegment } from '../../modules/orgUnit.js' import { sGetCurrent } from '../../reducers/current.js' @@ -139,6 +144,10 @@ export class UnconnectedVisualization extends Component { if ( !responses.some((response) => response.rows && response.rows.length) ) { + if (this.props.visualization.type === VIS_TYPE_OUTLIER_TABLE) { + throw new NoOutliersError() + } + throw new EmptyResponseError() } } diff --git a/src/modules/error.js b/src/modules/error.js index 07160d1aa9..06110ae409 100644 --- a/src/modules/error.js +++ b/src/modules/error.js @@ -357,6 +357,18 @@ export class AnalyticsRequestError extends VisualizationError { } } +export class NoOutliersError extends VisualizationError { + constructor() { + super( + EmptyBox, + i18n.t('No outliers found'), + i18n.t( + 'There were no outliers found for the selected data items and options.' + ) + ) + } +} + export const genericErrorTitle = i18n.t('Something went wrong') const getAvailableAxesDescription = (visType) => { From 1d5a245eb089ab8b8eaafa31405206aecb226124 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 14 Feb 2024 10:19:46 +0100 Subject: [PATCH 17/31] fix: add loading spinner in plugin/modal (DHIS2-16770) --- .../VisualizationPluginWrapper.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/VisualizationPlugin/VisualizationPluginWrapper.js b/src/components/VisualizationPlugin/VisualizationPluginWrapper.js index ae763a1fe4..96503b4f8d 100644 --- a/src/components/VisualizationPlugin/VisualizationPluginWrapper.js +++ b/src/components/VisualizationPlugin/VisualizationPluginWrapper.js @@ -1,12 +1,16 @@ +import { CenteredContent, CircularLoader, Layer } from '@dhis2/ui' import React, { useCallback, useState } from 'react' import { VisualizationPlugin } from '../VisualizationPlugin/VisualizationPlugin.js' // handle internal state for features that need to work without the app's Redux store const VisualizationPluginWrapper = (props) => { const [pluginProps, setPluginProps] = useState(props) + const [isLoading, setIsLoading] = useState(false) const onDataSorted = useCallback( (sorting) => { + setIsLoading(true) + const newSorting = { dimension: sorting.dimension, direction: sorting.direction.toUpperCase(), @@ -23,7 +27,24 @@ const VisualizationPluginWrapper = (props) => { [pluginProps] ) - return + const onLoadingComplete = () => setIsLoading(false) + + return ( + <> + {isLoading && ( + + + + + + )} + + + ) } export { VisualizationPluginWrapper } From 293639c1f78a05a3111e40aaef1a8abca386929e Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 14 Feb 2024 10:21:25 +0100 Subject: [PATCH 18/31] fix: allow table to scroll (DHIS2-16768) --- .../VisualizationPlugin/OutlierTablePlugin.js | 10 ++++++++-- .../VisualizationPlugin/VisualizationPlugin.js | 1 + .../styles/OutlierTablePlugin.module.css | 1 + .../styles/VisualizationPlugin.module.css | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/VisualizationPlugin/OutlierTablePlugin.js b/src/components/VisualizationPlugin/OutlierTablePlugin.js index 0e07a5c1cd..7d662f676d 100644 --- a/src/components/VisualizationPlugin/OutlierTablePlugin.js +++ b/src/components/VisualizationPlugin/OutlierTablePlugin.js @@ -63,6 +63,7 @@ const getSizeClass = (displayDensity) => { } const OutlierTablePlugin = ({ + filters, responses, visualization, //style, @@ -75,8 +76,6 @@ const OutlierTablePlugin = ({ }) const fixedDimensions = getFixedDimensions() - // const isInModal = false // TODO !!filters?.relativePeriodDate - const defaultSorting = useMemo(() => getDefaultSorting(), []) const getSorting = useCallback( @@ -97,6 +96,11 @@ const OutlierTablePlugin = ({ const sizeClass = getSizeClass(visualization.displayDensity) const fontSizeClass = getFontSizeClass(visualization.fontSize) + const isInModal = !!filters?.relativePeriodDate + + const getDataTableScrollHeight = (isInModal) => + isInModal ? 'calc(100vh - 285px)' : '100%' + const lookupColumnName = (name) => { const dimensionId = Object.entries(headersMap).find( (entry) => entry[1] === name @@ -192,6 +196,7 @@ const OutlierTablePlugin = ({ return (
Date: Wed, 14 Feb 2024 16:08:52 +0100 Subject: [PATCH 19/31] fix: support translations for column labels and tooltips (DHIS2-16774) --- i18n/en.pot | 28 ++++++- src/api/analytics.js | 11 ++- .../VisualizationPlugin/OutlierTablePlugin.js | 31 +++----- src/modules/analytics.js | 73 +++++++++++++++++-- 4 files changed, 112 insertions(+), 31 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3315e93127..e7344df04a 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: 2024-02-13T15:33:28.483Z\n" -"PO-Revision-Date: 2024-02-13T15:33:28.483Z\n" +"POT-Creation-Date: 2024-02-16T10:12:05.207Z\n" +"PO-Revision-Date: 2024-02-16T10:12:05.207Z\n" msgid "All items" msgstr "All items" @@ -721,6 +721,30 @@ msgstr "Open as Map" msgid "Visually plot data on a world map. Data elements use separate map layers." msgstr "Visually plot data on a world map. Data elements use separate map layers." +msgid "Category option combination" +msgstr "Category option combination" + +msgid "Absolute deviation" +msgstr "Absolute deviation" + +msgid "Median" +msgstr "Median" + +msgid "Median absolute deviation" +msgstr "Median absolute deviation" + +msgid "Z-score" +msgstr "Z-score" + +msgid "Mean" +msgstr "Mean" + +msgid "The lower boundary" +msgstr "The lower boundary" + +msgid "The upper boundary" +msgstr "The upper boundary" + msgid "Not supported when using cumulative values" msgstr "Not supported when using cumulative values" diff --git a/src/api/analytics.js b/src/api/analytics.js index 2691270213..60504ee269 100644 --- a/src/api/analytics.js +++ b/src/api/analytics.js @@ -2,6 +2,7 @@ import { Analytics, layoutGetDimensionItems, VIS_TYPE_PIVOT_TABLE, + DIMENSION_ID_DATA, DIMENSION_ID_PERIOD, DAILY, WEEKLY, @@ -16,7 +17,7 @@ import { import { OUTLIER_MAX_RESULTS_PROP } from '../components/VisualizationOptions/Options/OutliersMaxResults.js' import { getRelativePeriodTypeUsed, - getOutlierTableHeadersMap, + getOutlierTableDimensionIdHeaderMap, } from '../modules/analytics.js' import { getSortingFromVisualization } from '../modules/ui.js' @@ -41,7 +42,7 @@ export const getAnalyticsRequestForOutlierTable = ({ options, forDownload = false, }) => { - const headersMap = getOutlierTableHeadersMap(options) + const dimensionIdHeaderMap = getOutlierTableDimensionIdHeaderMap(options) const parameters = { ...options, @@ -60,7 +61,11 @@ export const getAnalyticsRequestForOutlierTable = ({ columns.forEach(({ dimension, items }) => { parameters[dimension] = items.map(({ id }) => id).join(',') - headers.push(forDownload ? dimension : headersMap[dimension]) + headers.push(forDownload ? dimension : dimensionIdHeaderMap[dimension]) + + if (dimension === DIMENSION_ID_DATA) { + headers.push('cocname') + } }) headers.push('value') diff --git a/src/components/VisualizationPlugin/OutlierTablePlugin.js b/src/components/VisualizationPlugin/OutlierTablePlugin.js index 7d662f676d..79006eec08 100644 --- a/src/components/VisualizationPlugin/OutlierTablePlugin.js +++ b/src/components/VisualizationPlugin/OutlierTablePlugin.js @@ -1,6 +1,5 @@ import { formatValue, - getFixedDimensions, VALUE_TYPE_NUMBER, VALUE_TYPE_INTEGER, VALUE_TYPE_INTEGER_POSITIVE, @@ -21,11 +20,12 @@ import { DataTableColumnHeader, DataTableHead, DataTableRow, + Tooltip, } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' import React, { useCallback, useMemo } from 'react' -import { getOutlierTableHeadersMap } from '../../modules/analytics.js' +import { getOutlierTableHeadersDetails } from '../../modules/analytics.js' import { DISPLAY_DENSITY_COMFORTABLE, DISPLAY_DENSITY_COMPACT, @@ -71,11 +71,9 @@ const OutlierTablePlugin = ({ onDataSorted, }) => { const data = responses[0] - const headersMap = getOutlierTableHeadersMap({ + const headersDetails = getOutlierTableHeadersDetails({ showHierarchy: visualization.showHierarchy, }) - const fixedDimensions = getFixedDimensions() - const defaultSorting = useMemo(() => getDefaultSorting(), []) const getSorting = useCallback( @@ -101,18 +99,6 @@ const OutlierTablePlugin = ({ const getDataTableScrollHeight = (isInModal) => isInModal ? 'calc(100vh - 285px)' : '100%' - const lookupColumnName = (name) => { - const dimensionId = Object.entries(headersMap).find( - (entry) => entry[1] === name - )?.[0] - - if (dimensionId) { - return fixedDimensions[dimensionId]?.name - } - - return undefined - } - const cellValueShouldNotWrap = (header) => [ VALUE_TYPE_NUMBER, @@ -127,8 +113,9 @@ const OutlierTablePlugin = ({ VALUE_TYPE_DATETIME, ].includes(header.valueType) - const renderHeaderCell = ({ name, column, valueType }) => { - const columnName = lookupColumnName(name) || column + const renderHeaderCell = ({ name, valueType }) => { + const columnName = headersDetails[name]?.label + const tooltipContent = headersDetails[name]?.tooltip return ( - {columnName} + {tooltipContent ? ( + {columnName} + ) : ( + columnName + )} ) } diff --git a/src/modules/analytics.js b/src/modules/analytics.js index 09bf83bd06..77f47ec1dd 100644 --- a/src/modules/analytics.js +++ b/src/modules/analytics.js @@ -1,4 +1,5 @@ import { + getFixedDimensions, getRelativePeriodsOptionsById, DIMENSION_ID_DATA, DIMENSION_ID_ORGUNIT, @@ -6,6 +7,7 @@ import { WEEKLY, DAILY, } from '@dhis2/analytics' +import i18n from '@dhis2/d2-i18n' export const outlierTableHeadersMap = { [DIMENSION_ID_DATA]: 'dxname', @@ -13,14 +15,73 @@ export const outlierTableHeadersMap = { [DIMENSION_ID_PERIOD]: 'pename', } -export const getOutlierTableHeadersMap = ({ showHierarchy }) => { - const map = Object.assign({}, outlierTableHeadersMap) +export const getOutlierTableDimensionIdHeaderMap = ({ showHierarchy }) => ({ + [DIMENSION_ID_DATA]: 'dxname', + [DIMENSION_ID_ORGUNIT]: showHierarchy ? 'ounamehierarchy' : 'ouname', + [DIMENSION_ID_PERIOD]: 'pename', +}) - if (showHierarchy) { - map[DIMENSION_ID_ORGUNIT] = 'ounamehierarchy' - } +export const getOutlierTableHeadersDetails = () => { + const fixedDimensions = getFixedDimensions() - return map + return { + dxname: { + label: fixedDimensions[DIMENSION_ID_DATA]?.name, + }, + ouname: { + label: fixedDimensions[DIMENSION_ID_ORGUNIT]?.name, + }, + ounamehierarchy: { + label: fixedDimensions[DIMENSION_ID_ORGUNIT]?.name, + }, + pename: { + label: fixedDimensions[DIMENSION_ID_PERIOD]?.name, + }, + cocname: { + label: i18n.t('Category option combination'), + tooltip: 'TODO', + }, + value: { + label: i18n.t('Value'), + tooltip: i18n.t('Value'), + }, + absdev: { + label: i18n.t('Absolute deviation'), + tooltip: 'TODO', + }, + modifiedzscore: { + label: i18n.t('Modified Z-score'), + tooltip: i18n.t('Modified Z-score'), + }, + median: { + label: i18n.t('Median'), + tooltip: i18n.t('Median'), + }, + medianabsdeviation: { + label: i18n.t('Median absolute deviation'), + tooltip: 'TODO', + }, + zscore: { + label: i18n.t('Z-score'), + tooltip: 'TODO', + }, + mean: { + label: i18n.t('Mean'), + tooltip: i18n.t('Mean'), + }, + stddev: { + label: i18n.t('Standard deviation'), + tooltip: 'TODO', + }, + lowerbound: { + label: i18n.t('Min'), + tooltip: i18n.t('The lower boundary'), + }, + upperbound: { + label: i18n.t('Max'), + tooltip: i18n.t('The upper boundary'), + }, + } } export const computeYoYMatrix = (responses, relativePeriodTypeUsed) => { From 009df59a218c276b7762e5a1601ee3d1519058b3 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Thu, 15 Feb 2024 15:18:00 +0100 Subject: [PATCH 20/31] fix: resize table to fit available space (DHIS2-16771) --- .../VisualizationPlugin/OutlierTablePlugin.js | 144 ++++++++++++------ .../VisualizationPlugin.js | 6 +- .../styles/OutlierTablePlugin.module.css | 6 + 3 files changed, 108 insertions(+), 48 deletions(-) diff --git a/src/components/VisualizationPlugin/OutlierTablePlugin.js b/src/components/VisualizationPlugin/OutlierTablePlugin.js index 79006eec08..ee94031ecf 100644 --- a/src/components/VisualizationPlugin/OutlierTablePlugin.js +++ b/src/components/VisualizationPlugin/OutlierTablePlugin.js @@ -24,7 +24,7 @@ import { } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React, { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo, useRef, useState } from 'react' import { getOutlierTableHeadersDetails } from '../../modules/analytics.js' import { DISPLAY_DENSITY_COMFORTABLE, @@ -66,16 +66,21 @@ const OutlierTablePlugin = ({ filters, responses, visualization, - //style, - //id: renderCounter, onDataSorted, }) => { const data = responses[0] const headersDetails = getOutlierTableHeadersDetails({ showHierarchy: visualization.showHierarchy, }) - const defaultSorting = useMemo(() => getDefaultSorting(), []) + const containerRef = useRef(null) + const [measuredDimensions, setMeasuredDimensions] = useState({ + containerMaxWidth: 0, + paginationMaxWidth: 0, + noticeBoxMaxWidth: 0, + }) + + const defaultSorting = useMemo(() => getDefaultSorting(), []) const getSorting = useCallback( (visualization) => { const sorting = @@ -96,6 +101,41 @@ const OutlierTablePlugin = ({ const isInModal = !!filters?.relativePeriodDate + const onResize = useCallback(() => { + if (!containerRef?.current || containerRef.current.clientWidth === 0) { + return + } + const containerInnerWidth = containerRef.current.clientWidth + const scrollBox = containerRef.current.querySelector('.tablescrollbox') + const scrollbarWidth = scrollBox.offsetWidth - scrollBox.clientWidth + const containerMaxWidth = containerInnerWidth - scrollbarWidth + + setMeasuredDimensions({ + containerMaxWidth, + paginationMaxWidth: containerMaxWidth - scrollbarWidth, + noticeBoxMaxWidth: scrollBox.offsetWidth, + }) + }, []) + + const sizeObserver = useMemo( + () => new window.ResizeObserver(onResize), + [onResize] + ) + + const mountAndObserveContainerRef = useCallback( + (node) => { + if (node === null) { + return + } + + containerRef.current = node + sizeObserver.observe(node) + + return sizeObserver.disconnect + }, + [sizeObserver] + ) + const getDataTableScrollHeight = (isInModal) => isInModal ? 'calc(100vh - 285px)' : '100%' @@ -185,49 +225,61 @@ const OutlierTablePlugin = ({ } return ( -
- +
- - - {data.headers.map((header) => renderHeaderCell(header))} - - - - {data.rows.map((row, rowIndex) => ( - - {row.map((value, columnIndex) => ( - - {formatValueCell( - value, - data.headers[columnIndex] - )} - - ))} + + + + {data.headers.map((header) => + renderHeaderCell(header) + )} - ))} - - + + + {data.rows.map((row, rowIndex) => ( + + {row.map((value, columnIndex) => ( + + {formatValueCell( + value, + data.headers[columnIndex] + )} + + ))} + + ))} + + +
) } @@ -239,10 +291,8 @@ OutlierTablePlugin.defaultProps = { OutlierTablePlugin.propTypes = { responses: PropTypes.arrayOf(PropTypes.object).isRequired, - //style: PropTypes.object, visualization: PropTypes.object.isRequired, filters: PropTypes.object, - // id: PropTypes.number, onDataSorted: PropTypes.func, } diff --git a/src/components/VisualizationPlugin/VisualizationPlugin.js b/src/components/VisualizationPlugin/VisualizationPlugin.js index 8c0e147249..0ae9c965fa 100644 --- a/src/components/VisualizationPlugin/VisualizationPlugin.js +++ b/src/components/VisualizationPlugin/VisualizationPlugin.js @@ -334,12 +334,16 @@ export const VisualizationPlugin = ({ } : style - // force height when no value available otherwise the PivotTable container sets 0 as height hiding the table content + // force wdth and height when no value available otherwise the PivotTable container sets 0 as height hiding the table content // and Highcharts does not render correctly the chart/legend if (!transformedStyle.height) { transformedStyle.height = size.height || '100%' } + if (!transformedStyle.width) { + transformedStyle.width = size.width || '100%' + } + const getLegendKey = () => { if (hasLegendSet && forDashboard) { return ( diff --git a/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css b/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css index cfef62a99e..539d0aac6f 100644 --- a/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css +++ b/src/components/VisualizationPlugin/styles/OutlierTablePlugin.module.css @@ -2,8 +2,14 @@ display: flex; gap: var(--spacers-dp4); min-width: 0; + width: 100%; margin: 0 var(--spacers-dp4) var(--spacers-dp4); } +.visualizationContainer { + display: flex; + flex-direction: column; + max-width: 100%; +} /* Table header cells in various sizes */ .dataTable .headerCell.dimensionModalHandler { From 349c01e12ec832a2ea685233b91a245e2cd395d7 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 20 Feb 2024 13:33:31 +0100 Subject: [PATCH 21/31] fix: filter out non data element items (DHIS2-16857) --- i18n/en.pot | 14 ++++++-- .../DimensionsPanel/Dialogs/DialogManager.js | 25 +++++++++++++ src/components/Layout/Chip.js | 35 ++++++++++++++----- src/components/Layout/TooltipContent.js | 5 ++- src/modules/current.js | 29 +++++++++++---- src/reducers/current.js | 6 +++- 6 files changed, 94 insertions(+), 20 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index e7344df04a..adb78c46b0 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: 2024-02-16T10:12:05.207Z\n" -"PO-Revision-Date: 2024-02-16T10:12:05.207Z\n" +"POT-Creation-Date: 2024-02-21T11:32:10.000Z\n" +"PO-Revision-Date: 2024-02-21T11:32:10.000Z\n" msgid "All items" msgstr "All items" @@ -63,6 +63,13 @@ msgstr "" "'Scatter' is intended to show a single data item per axis. Only the first " "item will be used and saved." +msgid "" +"'Outlier table' can only use data elements. Only data elements will be used " +"and saved." +msgstr "" +"'Outlier table' can only use data elements. Only data elements will be used " +"and saved." + msgid "Vertical" msgstr "Vertical" @@ -183,6 +190,9 @@ msgstr "horizontal" msgid "None selected" msgstr "None selected" +msgid "None in use" +msgstr "None in use" + msgid "Only '{{- name}}' in use" msgstr "Only '{{- name}}' in use" diff --git a/src/components/DimensionsPanel/Dialogs/DialogManager.js b/src/components/DimensionsPanel/Dialogs/DialogManager.js index ccab6397f4..6535f0d363 100644 --- a/src/components/DimensionsPanel/Dialogs/DialogManager.js +++ b/src/components/DimensionsPanel/Dialogs/DialogManager.js @@ -9,6 +9,7 @@ import { DIMENSION_ID_PERIOD, DIMENSION_ID_ORGUNIT, DIMENSION_TYPE_DATA_ELEMENT, + DIMENSION_TYPE_DATA_ELEMENT_OPERAND, getDimensionMaxNumberOfItems, getAxisMaxNumberOfItems, getDisplayNameByVisType, @@ -321,6 +322,7 @@ export class DialogManager extends Component { item.isActive = index < 1 }) } + let content = null if ( dialogId === DIMENSION_ID_DATA || @@ -353,6 +355,29 @@ export class DialogManager extends Component { }, }) } + + if (visType === VIS_TYPE_OUTLIER_TABLE) { + let showInfo = false + + selectedItems.forEach((item) => { + if ( + ![ + DIMENSION_TYPE_DATA_ELEMENT, + DIMENSION_TYPE_DATA_ELEMENT_OPERAND, + ].includes(item.type) + ) { + item.isActive = false + showInfo = true + } + }) + + if (showInfo) { + infoBoxMessage = i18n.t( + `'Outlier table' can only use data elements. Only data elements will be used and saved.` + ) + } + } + const dimensionSelector = ( ) - const isSplitAxis = - type === VIS_TYPE_SCATTER && dimensionId === DIMENSION_ID_DATA - const getMaxNumberOfItems = () => getAxisMaxNumberOfItems(type, axisId) || getDimensionMaxNumberOfItems(type, dimensionId) + let activeItemIds = getMaxNumberOfItems() + ? items.slice(0, getMaxNumberOfItems()) + : items + + // filter out non DATA_ELEMENT types for Outlier table vis type + if (type === VIS_TYPE_OUTLIER_TABLE && dimensionId === DIMENSION_ID_DATA) { + activeItemIds = activeItemIds.filter((id) => + [ + DIMENSION_TYPE_DATA_ELEMENT, + DIMENSION_TYPE_DATA_ELEMENT_OPERAND, + ].includes(metadata[id]?.dimensionItemType) + ) + } + + const hasWarning = + hasAxisTooManyItems(type, axisId, items.length) || + hasDimensionTooManyItems(type, dimensionId, items.length) || + items.length > activeItemIds.length + + const isSplitAxis = + type === VIS_TYPE_SCATTER && dimensionId === DIMENSION_ID_DATA + const handleClick = () => { if (!getPredefinedDimensionProp(dimensionId, DIMENSION_PROP_NO_ITEMS)) { onClick() @@ -124,9 +146,6 @@ const Chip = ({ } const renderTooltipContent = () => { - const activeItemIds = getMaxNumberOfItems() - ? items.slice(0, getMaxNumberOfItems()) - : items const lockedLabel = isLocked ? i18n.t( `{{dimensionName}} is locked to {{axisName}} for {{visTypeName}}`, @@ -162,9 +181,7 @@ const Chip = ({ {renderChipLabelSuffix()} - {(hasAxisTooManyItems(type, axisId, items.length) || - hasDimensionTooManyItems(type, dimensionId, items.length)) && - WarningIconWrapper} + {hasWarning && WarningIconWrapper} {isLocked && LockIconWrapper} ) diff --git a/src/components/Layout/TooltipContent.js b/src/components/Layout/TooltipContent.js index f1e2ed7b32..710b5b41ce 100644 --- a/src/components/Layout/TooltipContent.js +++ b/src/components/Layout/TooltipContent.js @@ -9,6 +9,7 @@ import { styles } from './styles/Tooltip.style.js' const labels = { noneSelected: () => i18n.t('None selected'), + noneInUse: () => i18n.t('None in use'), onlyOneInUse: (name) => i18n.t("Only '{{- name}}' in use", { name }), onlyLimitedNumberInUse: (number) => i18n.t("Only '{{number}}' in use", { number }), @@ -25,7 +26,9 @@ export const TooltipContent = ({ const hasAllItemsSelected = itemIds.includes(ALL_DYNAMIC_DIMENSION_ITEMS) const getWarningLabel = () => { const warningLabel = - itemIds.length === 1 + itemIds.length === 0 + ? labels.noneInUse() + : itemIds.length === 1 ? labels.onlyOneInUse( metadata[itemIds[0]] ? metadata[itemIds[0]].name diff --git a/src/modules/current.js b/src/modules/current.js index 85be0e6f2d..710ae837c9 100644 --- a/src/modules/current.js +++ b/src/modules/current.js @@ -6,6 +6,8 @@ import { AXIS_ID_FILTERS, DIMENSION_ID_DATA, DIMENSION_ID_PERIOD, + DIMENSION_TYPE_DATA_ELEMENT, + DIMENSION_TYPE_DATA_ELEMENT_OPERAND, VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_PIE, @@ -139,7 +141,7 @@ export const getItemsByDimensionFromUi = (ui) => { return result } -export const getOutlierTableCurrentFromUi = (state, value) => { +export const getOutlierTableCurrentFromUi = (state, value, metadata) => { const ui = { ...value, layout: { @@ -150,13 +152,26 @@ export const getOutlierTableCurrentFromUi = (state, value) => { const axesFromUi = getAxesFromUi(ui) - // only save the first pe item const peItems = layoutGetDimensionItems(axesFromUi, DIMENSION_ID_PERIOD) - const outlierTableAxesFromUi = layoutReplaceDimension( - axesFromUi, - DIMENSION_ID_PERIOD, - [peItems[0]] - ) + const dxItems = layoutGetDimensionItems(axesFromUi, DIMENSION_ID_DATA) + + const outlierTableAxesFromUi = + // only save the first pe item + layoutReplaceDimension( + // only save data element and data element operand dx items + layoutReplaceDimension( + axesFromUi, + DIMENSION_ID_DATA, + dxItems.filter(({ id }) => + [ + DIMENSION_TYPE_DATA_ELEMENT, + DIMENSION_TYPE_DATA_ELEMENT_OPERAND, + ].includes(metadata[id]?.dimensionItemType) + ) + ), + DIMENSION_ID_PERIOD, + [peItems[0]] + ) return { ...state, diff --git a/src/reducers/current.js b/src/reducers/current.js index 7a7fca686b..082d524db6 100644 --- a/src/reducers/current.js +++ b/src/reducers/current.js @@ -63,7 +63,11 @@ export default (state = DEFAULT_CURRENT, action) => { case VIS_TYPE_SCATTER: return getScatterCurrentFromUi(state, action.value.ui) case VIS_TYPE_OUTLIER_TABLE: - return getOutlierTableCurrentFromUi(state, action.value.ui) + return getOutlierTableCurrentFromUi( + state, + action.value.ui, + action.value.metadata + ) default: { return getDefaultFromUi( state, From 34e124af4a952686e07455531c454cb319e6fcf9 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 21 Feb 2024 11:05:18 +0100 Subject: [PATCH 22/31] feat: implement skipRounding option (DHIS2-16870) --- src/components/DownloadMenu/useDownload.js | 5 ++++- src/modules/options/outlierTableConfig.js | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/DownloadMenu/useDownload.js b/src/components/DownloadMenu/useDownload.js index 0d4a4e10dc..6776a08c17 100644 --- a/src/components/DownloadMenu/useDownload.js +++ b/src/components/DownloadMenu/useDownload.js @@ -119,7 +119,10 @@ const useDownload = (relativePeriodDate) => { req = getAnalyticsRequestForOutlierTable({ analyticsEngine, visualization, - options: { showHierarchy: visualization.showHierarchy }, + options: { + showHierarchy: visualization.showHierarchy, + skipRounding: visualization.skipRounding, + }, forDownload: true, }) diff --git a/src/modules/options/outlierTableConfig.js b/src/modules/options/outlierTableConfig.js index a8820591a1..2e29b45619 100644 --- a/src/modules/options/outlierTableConfig.js +++ b/src/modules/options/outlierTableConfig.js @@ -5,16 +5,20 @@ import FontSize from '../../components/VisualizationOptions/Options/FontSize.js' import Outliers from '../../components/VisualizationOptions/Options/OutliersForOutlierTable.js' import OutliersMaxResults from '../../components/VisualizationOptions/Options/OutliersMaxResults.js' import ShowHierarchy from '../../components/VisualizationOptions/Options/ShowHierarchy.js' +import SkipRounding from '../../components/VisualizationOptions/Options/SkipRounding.js' +import getDisplayTemplate from './sections/templates/display.js' import getDataTab from './tabs/data.js' import getOutliersTab from './tabs/outliers.js' import getStyleTab from './tabs/style.js' export default () => [ getDataTab([ - { - key: 'data-section-1', - content: React.Children.toArray([]), - }, + getDisplayTemplate({ + content: React.Children.toArray([ + , + , + ]), + }), ]), getStyleTab([ { From 5b1907cbdb9debb5cd9e6c1a64fa6fe38f9d701b Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 21 Feb 2024 15:06:57 +0100 Subject: [PATCH 23/31] test: fix failing test and update snapshot --- .../DimensionsPanel/Dialogs/__tests__/DialogManager.spec.js | 2 ++ .../Dialogs/__tests__/__snapshots__/DialogManager.spec.js.snap | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/components/DimensionsPanel/Dialogs/__tests__/DialogManager.spec.js b/src/components/DimensionsPanel/Dialogs/__tests__/DialogManager.spec.js index 366a27a994..898b2c1468 100644 --- a/src/components/DimensionsPanel/Dialogs/__tests__/DialogManager.spec.js +++ b/src/components/DimensionsPanel/Dialogs/__tests__/DialogManager.spec.js @@ -22,7 +22,9 @@ jest.mock('@dhis2/analytics', () => { DIMENSION_ID_DATA: dataId, DIMENSION_ID_PERIOD: periodId, DIMENSION_ID_ORGUNIT: ouId, + dataTypeMap: {}, getAxisMaxNumberOfItems: () => {}, + getDimensionMaxNumberOfItems: () => 1, filterOutPredefinedDimensions: () => [], getPredefinedDimensions: () => {}, getPredefinedDimensionProp: () => {}, diff --git a/src/components/DimensionsPanel/Dialogs/__tests__/__snapshots__/DialogManager.spec.js.snap b/src/components/DimensionsPanel/Dialogs/__tests__/__snapshots__/DialogManager.spec.js.snap index fc7ccfa69c..bfba0f2f6c 100644 --- a/src/components/DimensionsPanel/Dialogs/__tests__/__snapshots__/DialogManager.spec.js.snap +++ b/src/components/DimensionsPanel/Dialogs/__tests__/__snapshots__/DialogManager.spec.js.snap @@ -83,6 +83,7 @@ exports[`The DialogManager component renders OUDimension content with display:no />
Date: Wed, 21 Feb 2024 15:32:26 +0100 Subject: [PATCH 24/31] fix: add help text on Max results option (DHIS2-16873) --- i18n/en.pot | 11 +++++++++-- .../Options/OutliersMaxResults.js | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index adb78c46b0..23d1507814 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: 2024-02-21T11:32:10.000Z\n" -"PO-Revision-Date: 2024-02-21T11:32:10.000Z\n" +"POT-Creation-Date: 2024-02-21T14:23:34.018Z\n" +"PO-Revision-Date: 2024-02-21T14:23:34.018Z\n" msgid "All items" msgstr "All items" @@ -594,6 +594,13 @@ msgstr "Outlier detection method" msgid "Max results" msgstr "Max results" +msgid "" +"The maximum number of outlier values to show in the table. Must be between " +"1-500." +msgstr "" +"The maximum number of outlier values to show in the table. Must be between " +"1-500." + msgid "Organisation unit" msgstr "Organisation unit" diff --git a/src/components/VisualizationOptions/Options/OutliersMaxResults.js b/src/components/VisualizationOptions/Options/OutliersMaxResults.js index 6bdc22c1be..6f89afe1ae 100644 --- a/src/components/VisualizationOptions/Options/OutliersMaxResults.js +++ b/src/components/VisualizationOptions/Options/OutliersMaxResults.js @@ -53,6 +53,9 @@ const OutliersMaxResults = () => { value={outlierAnalysis[ OUTLIER_MAX_RESULTS_PROP ]?.toString()} + helpText={i18n.t( + 'The maximum number of outlier values to show in the table. Must be between 1-500.' + )} min={MIN_VALUE.toString()} max={MAX_VALUE.toString()} step={'1'} From 2b77b96d02ac1f8558078f731363fec5318faac0 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 26 Feb 2024 13:31:27 +0100 Subject: [PATCH 25/31] fix: update various texts --- i18n/en.pot | 95 +++++++++++++++---- .../DimensionsPanel/Dialogs/DialogManager.js | 2 +- src/components/Layout/TooltipContent.js | 2 +- src/modules/analytics.js | 32 ++++--- src/modules/visualization.js | 4 +- 5 files changed, 102 insertions(+), 33 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 23d1507814..437ec605a4 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: 2024-02-21T14:23:34.018Z\n" -"PO-Revision-Date: 2024-02-21T14:23:34.018Z\n" +"POT-Creation-Date: 2024-03-01T08:28:43.727Z\n" +"PO-Revision-Date: 2024-03-01T08:28:43.727Z\n" msgid "All items" msgstr "All items" @@ -64,11 +64,11 @@ msgstr "" "item will be used and saved." msgid "" -"'Outlier table' can only use data elements. Only data elements will be used " -"and saved." +"'Outlier table' shows values from data elements only. Only data elements " +"will be used and saved." msgstr "" -"'Outlier table' can only use data elements. Only data elements will be used " -"and saved." +"'Outlier table' shows values from data elements only. Only data elements " +"will be used and saved." msgid "Vertical" msgstr "Vertical" @@ -196,8 +196,8 @@ msgstr "None in use" msgid "Only '{{- name}}' in use" msgstr "Only '{{- name}}' in use" -msgid "Only '{{number}}' in use" -msgstr "Only '{{number}}' in use" +msgid "Only {{number}} in use" +msgstr "Only {{number}} in use" msgid "All items are selected" msgstr "All items are selected" @@ -744,23 +744,82 @@ msgstr "Category option combination" msgid "Absolute deviation" msgstr "Absolute deviation" +msgid "" +"A measure of the absolute difference between each data point and a central " +"value, usually the mean or median, providing a straightforward " +"understanding of dispersion in the dataset." +msgstr "" +"A measure of the absolute difference between each data point and a central " +"value, usually the mean or median, providing a straightforward " +"understanding of dispersion in the dataset." + +msgid "" +"A measure of how far a data point deviates from the median, using the " +"median absolute deviation instead of the standard deviation, making it " +"robust against outliers." +msgstr "" +"A measure of how far a data point deviates from the median, using the " +"median absolute deviation instead of the standard deviation, making it " +"robust against outliers." + msgid "Median" msgstr "Median" +msgid "" +"The middle value in a dataset when the values are arranged in ascending or " +"descending order. It's a robust measure of central tendency that is less " +"affected by outliers compared to the mean." +msgstr "" +"The middle value in a dataset when the values are arranged in ascending or " +"descending order. It's a robust measure of central tendency that is less " +"affected by outliers compared to the mean." + msgid "Median absolute deviation" msgstr "Median absolute deviation" +msgid "" +"A robust measure of variability, found by calculating the median of the " +"absolute differences between each data point and the overall median. It's " +"less influenced by outliers compared to other measures like the standard " +"deviation." +msgstr "" +"A robust measure of variability, found by calculating the median of the " +"absolute differences between each data point and the overall median. It's " +"less influenced by outliers compared to other measures like the standard " +"deviation." + msgid "Z-score" msgstr "Z-score" +msgid "" +"A measure of how many standard deviations a data point is from the mean of " +"a dataset, providing insight into how unusual or typical that data point is " +"relative to the rest of the distribution." +msgstr "" +"A measure of how many standard deviations a data point is from the mean of " +"a dataset, providing insight into how unusual or typical that data point is " +"relative to the rest of the distribution." + msgid "Mean" msgstr "Mean" -msgid "The lower boundary" -msgstr "The lower boundary" +msgid "Average of the value over time." +msgstr "Average of the value over time." + +msgid "" +"A measure of how dispersed the data is in relation to the mean. Low " +"standard deviation indicates data are clustered tightly around the mean, " +"and high standard deviation indicates data are more spread out." +msgstr "" +"A measure of how dispersed the data is in relation to the mean. Low " +"standard deviation indicates data are clustered tightly around the mean, " +"and high standard deviation indicates data are more spread out." + +msgid "Minimum score threshold" +msgstr "Minimum score threshold" -msgid "The upper boundary" -msgstr "The upper boundary" +msgid "Maximum score threshold" +msgstr "Maximum score threshold" msgid "Not supported when using cumulative values" msgstr "Not supported when using cumulative values" @@ -1167,14 +1226,14 @@ msgid "Display a single value. Recommend relative period to show latest data." msgstr "Display a single value. Recommend relative period to show latest data." msgid "" -"View the relationship between two data items at a place or time. " -"Recommended for finding outliers." +"Compare the relationship between two data items across multiple places. " +"Recommended for visualizing outliers." msgstr "" -"View the relationship between two data items at a place or time. " -"Recommended for finding outliers." +"Compare the relationship between two data items across multiple places. " +"Recommended for visualizing outliers." -msgid "Automatically identify extreme outliers in the data." -msgstr "Automatically identify extreme outliers in the data." +msgid "Automatically identify extreme outliers based on historical data." +msgstr "Automatically identify extreme outliers based on historical data." msgid "Weeks per year" msgstr "Weeks per year" diff --git a/src/components/DimensionsPanel/Dialogs/DialogManager.js b/src/components/DimensionsPanel/Dialogs/DialogManager.js index 6535f0d363..e3844578d0 100644 --- a/src/components/DimensionsPanel/Dialogs/DialogManager.js +++ b/src/components/DimensionsPanel/Dialogs/DialogManager.js @@ -373,7 +373,7 @@ export class DialogManager extends Component { if (showInfo) { infoBoxMessage = i18n.t( - `'Outlier table' can only use data elements. Only data elements will be used and saved.` + `'Outlier table' shows values from data elements only. Only data elements will be used and saved.` ) } } diff --git a/src/components/Layout/TooltipContent.js b/src/components/Layout/TooltipContent.js index 710b5b41ce..150a591966 100644 --- a/src/components/Layout/TooltipContent.js +++ b/src/components/Layout/TooltipContent.js @@ -12,7 +12,7 @@ const labels = { noneInUse: () => i18n.t('None in use'), onlyOneInUse: (name) => i18n.t("Only '{{- name}}' in use", { name }), onlyLimitedNumberInUse: (number) => - i18n.t("Only '{{number}}' in use", { number }), + i18n.t('Only {{number}} in use', { number }), allItems: () => i18n.t('All items are selected'), } diff --git a/src/modules/analytics.js b/src/modules/analytics.js index 77f47ec1dd..7773060f33 100644 --- a/src/modules/analytics.js +++ b/src/modules/analytics.js @@ -39,47 +39,57 @@ export const getOutlierTableHeadersDetails = () => { }, cocname: { label: i18n.t('Category option combination'), - tooltip: 'TODO', }, value: { label: i18n.t('Value'), - tooltip: i18n.t('Value'), }, absdev: { label: i18n.t('Absolute deviation'), - tooltip: 'TODO', + tooltip: i18n.t( + 'A measure of the absolute difference between each data point and a central value, usually the mean or median, providing a straightforward understanding of dispersion in the dataset.' + ), }, modifiedzscore: { label: i18n.t('Modified Z-score'), - tooltip: i18n.t('Modified Z-score'), + tooltip: i18n.t( + 'A measure of how far a data point deviates from the median, using the median absolute deviation instead of the standard deviation, making it robust against outliers.' + ), }, median: { label: i18n.t('Median'), - tooltip: i18n.t('Median'), + tooltip: i18n.t( + "The middle value in a dataset when the values are arranged in ascending or descending order. It's a robust measure of central tendency that is less affected by outliers compared to the mean." + ), }, medianabsdeviation: { label: i18n.t('Median absolute deviation'), - tooltip: 'TODO', + tooltip: i18n.t( + "A robust measure of variability, found by calculating the median of the absolute differences between each data point and the overall median. It's less influenced by outliers compared to other measures like the standard deviation." + ), }, zscore: { label: i18n.t('Z-score'), - tooltip: 'TODO', + tooltip: i18n.t( + 'A measure of how many standard deviations a data point is from the mean of a dataset, providing insight into how unusual or typical that data point is relative to the rest of the distribution.' + ), }, mean: { label: i18n.t('Mean'), - tooltip: i18n.t('Mean'), + tooltip: i18n.t('Average of the value over time.'), }, stddev: { label: i18n.t('Standard deviation'), - tooltip: 'TODO', + tooltip: i18n.t( + 'A measure of how dispersed the data is in relation to the mean. Low standard deviation indicates data are clustered tightly around the mean, and high standard deviation indicates data are more spread out.' + ), }, lowerbound: { label: i18n.t('Min'), - tooltip: i18n.t('The lower boundary'), + tooltip: i18n.t('Minimum score threshold'), }, upperbound: { label: i18n.t('Max'), - tooltip: i18n.t('The upper boundary'), + tooltip: i18n.t('Maximum score threshold'), }, } } diff --git a/src/modules/visualization.js b/src/modules/visualization.js index ca94625cbb..b58247bec2 100644 --- a/src/modules/visualization.js +++ b/src/modules/visualization.js @@ -85,10 +85,10 @@ export const getVisTypeDescriptions = () => ({ 'Display a single value. Recommend relative period to show latest data.' ), [VIS_TYPE_SCATTER]: i18n.t( - 'View the relationship between two data items at a place or time. Recommended for finding outliers.' + 'Compare the relationship between two data items across multiple places. Recommended for visualizing outliers.' ), [VIS_TYPE_OUTLIER_TABLE]: i18n.t( - 'Automatically identify extreme outliers in the data.' + 'Automatically identify extreme outliers based on historical data.' ), }) From bbd6781f00841f15a0a184dfe4c137263231d1fb Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 27 Feb 2024 10:27:07 +0100 Subject: [PATCH 26/31] fix: fix crash when switching Scatter/Outlier (DHIS2-16895) The option data is shared but the defaults are different. Scatter has IQR method and extreme lines in addition. --- .../VisualizationOptions/Options/Outliers.js | 3 ++ .../Options/OutliersForOutlierTable.js | 26 +++++++++++---- src/modules/ui.js | 33 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/components/VisualizationOptions/Options/Outliers.js b/src/components/VisualizationOptions/Options/Outliers.js index dca437745c..b670213d8e 100644 --- a/src/components/VisualizationOptions/Options/Outliers.js +++ b/src/components/VisualizationOptions/Options/Outliers.js @@ -56,6 +56,9 @@ const DEFAULT_STATE = { const dataTest = 'option-outliers' const Outliers = ({ outlierAnalysis, onChange }) => { + // initialise extremeLines if empty (ie. option saved in Outlier table) + outlierAnalysis.extremeLines ??= DEFAULT_STATE.extremeLines + const storeProp = (prop, value) => onChange({ ...outlierAnalysis, [prop]: value }) diff --git a/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js b/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js index e3b9aadf1b..26142b205a 100644 --- a/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js +++ b/src/components/VisualizationOptions/Options/OutliersForOutlierTable.js @@ -15,7 +15,7 @@ import { DEFAULT_STATE as OUTLIER_MAX_RESULTS_DEFAULT_STATE } from './OutliersMa export const OUTLIER_METHOD_PROP = 'outlierMethod' export const OUTLIER_THRESHOLD_PROP = 'thresholdFactor' -const OUTLIER_ANALYSIS_OPTION_NAME = 'outlierAnalysis' +export const OUTLIER_ANALYSIS_OPTION_NAME = 'outlierAnalysis' export const METHOD_STANDARD_Z_SCORE = 'STANDARD_Z_SCORE' export const METHOD_MODIFIED_Z_SCORE = 'MODIFIED_Z_SCORE' @@ -40,11 +40,25 @@ export const DEFAULT_STATE = { const Outliers = () => { const dispatch = useDispatch() - const outlierAnalysis = useSelector(sGetUiOptions)[ - OUTLIER_ANALYSIS_OPTION_NAME - ] || { - ...OUTLIER_MAX_RESULTS_DEFAULT_STATE, - ...DEFAULT_STATE, + let outlierAnalysis = + useSelector(sGetUiOptions)[OUTLIER_ANALYSIS_OPTION_NAME] + + if ( + !outlierAnalysis || + !methods + .map(({ id }) => id) + .includes(outlierAnalysis[OUTLIER_METHOD_PROP]) + ) { + outlierAnalysis = { + ...OUTLIER_MAX_RESULTS_DEFAULT_STATE, + ...DEFAULT_STATE, + } + + dispatch( + acSetUiOptions({ + [OUTLIER_ANALYSIS_OPTION_NAME]: outlierAnalysis, + }) + ) } const sorting = useSelector(sGetUi).sorting diff --git a/src/modules/ui.js b/src/modules/ui.js index f91179ec1c..4b234d6964 100644 --- a/src/modules/ui.js +++ b/src/modules/ui.js @@ -6,6 +6,7 @@ import { layoutGetDimensionIdItemIdsObject, VIS_TYPE_YEAR_OVER_YEAR_LINE, VIS_TYPE_YEAR_OVER_YEAR_COLUMN, + VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_PIVOT_TABLE, VIS_TYPE_SCATTER, defaultVisType, @@ -14,6 +15,14 @@ import { VIS_TYPE_GAUGE, VIS_TYPE_SINGLE_VALUE, } from '@dhis2/analytics' +import { + DEFAULT_STATE as OUTLIER_METHOD_THRESHOLD_DEFAULT_STATE, + METHOD_MODIFIED_Z_SCORE, + METHOD_STANDARD_Z_SCORE, + OUTLIER_ANALYSIS_OPTION_NAME, + OUTLIER_METHOD_PROP, +} from '../components/VisualizationOptions/Options/OutliersForOutlierTable.js' +import { DEFAULT_STATE as OUTLIER_MAX_RESULTS_DEFAULT_STATE } from '../components/VisualizationOptions/Options/OutliersMaxResults.js' import { getDisabledOptions } from './disabledOptions.js' import { BASE_FIELD_YEARLY_SERIES } from './fields/baseFields.js' import { getInverseLayout } from './layout.js' @@ -111,6 +120,27 @@ const scatterUiAdapter = (ui) => { return adaptedUi } +// Transform from store.ui to outlier table format +const outlierTableUiAdapter = (ui) => { + const adaptedUi = defaultUiAdapter(ui) + console.log('outlier ui adapter') + const outlierAnalysis = ui.options?.[OUTLIER_ANALYSIS_OPTION_NAME] + + if ( + !outlierAnalysis || + ![METHOD_STANDARD_Z_SCORE, METHOD_MODIFIED_Z_SCORE].includes( + outlierAnalysis[OUTLIER_METHOD_PROP] + ) + ) { + adaptedUi.options[OUTLIER_ANALYSIS_OPTION_NAME] = { + ...OUTLIER_METHOD_THRESHOLD_DEFAULT_STATE, + ...OUTLIER_MAX_RESULTS_DEFAULT_STATE, + } + } + + return adaptedUi +} + export const getAdaptedUiByType = (ui) => { let adaptedUi @@ -130,6 +160,9 @@ export const getAdaptedUiByType = (ui) => { case VIS_TYPE_SCATTER: adaptedUi = scatterUiAdapter(ui) break + case VIS_TYPE_OUTLIER_TABLE: + adaptedUi = outlierTableUiAdapter(ui) + break default: adaptedUi = defaultUiAdapter(ui) break From 748ab173a98cbb66c309af33e8aab273de9ce2be Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 27 Feb 2024 14:25:33 +0100 Subject: [PATCH 27/31] fix: avoid removing trailing 0 while typing (DHIS2-16896) --- .../VisualizationOptions/Options/OutlierDetectionMethod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js b/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js index 1055781ce9..4f7cb0f951 100644 --- a/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js +++ b/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js @@ -38,7 +38,7 @@ const OutlierDetectionMethod = ({ label={i18n.t('Threshold factor')} min="0" step="0.5" - onChange={({ value }) => onThresholdChange(Number(value))} + onChange={({ value }) => onThresholdChange(value)} value={currentThreshold?.toString() || ''} helpText={i18n.t( 'A high value is more sensitive so fewer data items will be identified as outliers' From ce717fa8bc6027ca16fad132f526c159c453116e Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 27 Feb 2024 15:09:40 +0100 Subject: [PATCH 28/31] fix: show custom error when no data in use (DHIS2-16892) --- src/components/Visualization/Visualization.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index 8d3f402f62..f25e90ee08 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -91,6 +91,9 @@ export class UnconnectedVisualization extends Component { case 'E7145': error = new AnalyticsRequestError() break + case 'E2200': + error = new NoDataError(this.props.visualization.type) + break default: error = response } From 1c76a2aa73f2616768a82974d20fb17fe79b2d0d Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 5 Mar 2024 10:49:14 +0100 Subject: [PATCH 29/31] fix: re-render plugin when props change This fixes the issue with the visualization type not changing when using the View as options in dashboard item context menu. --- .../VisualizationPlugin/VisualizationPluginWrapper.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/VisualizationPlugin/VisualizationPluginWrapper.js b/src/components/VisualizationPlugin/VisualizationPluginWrapper.js index 96503b4f8d..891ff36d8e 100644 --- a/src/components/VisualizationPlugin/VisualizationPluginWrapper.js +++ b/src/components/VisualizationPlugin/VisualizationPluginWrapper.js @@ -1,5 +1,5 @@ import { CenteredContent, CircularLoader, Layer } from '@dhis2/ui' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { VisualizationPlugin } from '../VisualizationPlugin/VisualizationPlugin.js' // handle internal state for features that need to work without the app's Redux store @@ -27,6 +27,8 @@ const VisualizationPluginWrapper = (props) => { [pluginProps] ) + useEffect(() => setPluginProps(props), [props]) + const onLoadingComplete = () => setIsLoading(false) return ( From 6c7b36b5a04ee880bca10beb6f476d91fb28f98b Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 5 Mar 2024 10:59:08 +0100 Subject: [PATCH 30/31] chore: remove debug console log --- src/modules/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/ui.js b/src/modules/ui.js index 4b234d6964..374210c18b 100644 --- a/src/modules/ui.js +++ b/src/modules/ui.js @@ -123,7 +123,7 @@ const scatterUiAdapter = (ui) => { // Transform from store.ui to outlier table format const outlierTableUiAdapter = (ui) => { const adaptedUi = defaultUiAdapter(ui) - console.log('outlier ui adapter') + const outlierAnalysis = ui.options?.[OUTLIER_ANALYSIS_OPTION_NAME] if ( From 2db39fe2bae9dfe677c256b6bc524717713ec8d3 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 5 Mar 2024 14:10:03 +0100 Subject: [PATCH 31/31] chore: use alpha version of analytics --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 38b89724a2..005cecc5dc 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.0" }, "dependencies": { - "@dhis2/analytics": "^26.3.0", + "@dhis2/analytics": "999.9.9-outlier-table.alpha.4", "@dhis2/app-runtime": "^3.7.0", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/yarn.lock b/yarn.lock index 72207fe9e3..c5a6901acf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2028,10 +2028,10 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-26.3.0.tgz#ada2fe27442f19704fa704e334546416c85ea1d6" - integrity sha512-B/pUh8K8wyivL4yBiwqPoQ94pMWwCqh0xu3Uak4jmJqS+jO0slUlyDLtAmXU/jqRlRgRg1nR4u18npjd511Q7A== +"@dhis2/analytics@999.9.9-outlier-table.alpha.4": + version "999.9.9-outlier-table.alpha.4" + resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-999.9.9-outlier-table.alpha.4.tgz#a5ca4dc5cc8d1e819d9462c69a1d12149b9351c7" + integrity sha512-JtvJxqRQSEDoo2+Jq+bo0NIb2gq0hqCQfCpnTpET08uwkYFAwu6YKma2Z0rmK3A9IM0wwqSJNx3a7Cje7Px9BQ== dependencies: "@dhis2/d2-ui-rich-text" "^7.4.1" "@dhis2/multi-calendar-dates" "1.0.0"