From b6eb3f739621b1e66477909c5effec2808f63d2f Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 18 Oct 2023 11:02:23 +0200 Subject: [PATCH 01/57] feat: switch to ui table with horizontal scrolling --- src/components/datatable/BottomPanel.js | 6 +- src/components/datatable/UiDataTable.js | 121 ++++++++++++++++++ .../datatable/styles/UiDataTable.module.css | 8 ++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/components/datatable/UiDataTable.js create mode 100644 src/components/datatable/styles/UiDataTable.module.css diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 62160bcbf..4087905f5 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -8,7 +8,7 @@ import { LAYERS_PANEL_WIDTH, RIGHT_PANEL_WIDTH, } from '../../constants/layout.js' -import DataTable from '../datatable/DataTable.js' +import DataTable from '../datatable/UiDataTable.js' import { useWindowDimensions } from '../WindowDimensionsProvider.js' import ResizeHandle from './ResizeHandle.js' import styles from './styles/BottomPanel.module.css' @@ -39,7 +39,7 @@ const BottomPanel = () => {
{ onResize={onResize} onResizeEnd={(height) => dispatch(resizeDataTable(height))} /> - +
) } diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js new file mode 100644 index 000000000..b2371da1a --- /dev/null +++ b/src/components/datatable/UiDataTable.js @@ -0,0 +1,121 @@ +import i18n from '@dhis2/d2-i18n' +import { + DataTable, + DataTableRow, + DataTableCell, + DataTableColumnHeader, + DataTableHead, + DataTableBody, + // DataTableFoot, + // Pagination, + // Tooltip, +} from '@dhis2/ui' +import cx from 'classnames' +// import propTypes from 'prop-types' +import React from 'react' +import { useSelector } from 'react-redux' +import styles from './styles/UiDataTable.module.css' + +const Table = () => { + const map = useSelector((state) => state.map) + const dataTable = useSelector((state) => state.dataTable) + // const allAggregations = useSelector((state) => state.aggregations) + // const feature = useSelector((state) => state.feature) + + const layer = map.mapViews.find((l) => l.id === dataTable) + + if (!layer) { + return No data + } + + // const aggregations = allAggregations[layer?.id] + + const { data } = layer + + console.log('jj data', data) + + const thematicHeaders = [ + i18n.t('Index'), + i18n.t('Name'), + i18n.t('Id'), + i18n.t('Value'), + i18n.t('Legend'), + i18n.t('Range'), + i18n.t('Level'), + i18n.t('Parent'), + i18n.t('Type'), + i18n.t('Color'), + i18n.t('Name2'), + i18n.t('Id2'), + i18n.t('Value2'), + i18n.t('Legend2'), + i18n.t('Range2'), + i18n.t('Level2'), + i18n.t('Parent2'), + i18n.t('Type2'), + i18n.t('Color2'), + ] + + const rows = data.map((row, index) => { + return [ + index, + row.properties.name, + row.properties.id, + row.properties.value, + row.properties.legend, + row.properties.range, + row.properties.level, + row.properties.parentName, + row.properties.type, + row.properties.color, + row.properties.name, + row.properties.id, + row.properties.value, + row.properties.legend, + row.properties.range, + row.properties.level, + row.properties.parentName, + row.properties.type, + row.properties.color, + ] + }) + + return ( + + + + {thematicHeaders.map((header, index) => ( + + {header} + + ))} + + + + {rows.map((row, index) => ( + + {row.map((value, cellindex) => ( + + {value} + + ))} + + ))} + + + ) +} + +export default Table diff --git a/src/components/datatable/styles/UiDataTable.module.css b/src/components/datatable/styles/UiDataTable.module.css new file mode 100644 index 000000000..0a8a75e4e --- /dev/null +++ b/src/components/datatable/styles/UiDataTable.module.css @@ -0,0 +1,8 @@ +td.sizeClass { + padding: 7px 5px; +} + +td.fontClass { + font-size: 11px; +} + From 7367aa337076f8fe10098933be9a5a5a9b8368cd Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 18 Oct 2023 14:22:04 +0200 Subject: [PATCH 02/57] feat: sorting on all but the index column --- src/components/datatable/UiDataTable.js | 175 +++++++++++++++--------- 1 file changed, 114 insertions(+), 61 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index b2371da1a..4275e6fa8 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -11,87 +11,140 @@ import { // Tooltip, } from '@dhis2/ui' import cx from 'classnames' -// import propTypes from 'prop-types' -import React from 'react' +import React, { useState, useEffect, useReducer, useCallback } from 'react' import { useSelector } from 'react-redux' import styles from './styles/UiDataTable.module.css' +const ASCENDING = 'asc' +const DESCENDING = 'desc' + +const thematicHeaders = [ + { name: i18n.t('Index'), dataKey: 'index' }, + { name: i18n.t('Name'), dataKey: 'name' }, + { name: i18n.t('Id'), dataKey: 'id' }, + { name: i18n.t('Value'), dataKey: 'value' }, + { name: i18n.t('Legend'), dataKey: 'legend' }, + { name: i18n.t('Range'), dataKey: 'range' }, + { name: i18n.t('Level'), dataKey: 'level' }, + { name: i18n.t('Parent'), dataKey: 'parentName' }, + { name: i18n.t('Type'), dataKey: 'type' }, + { name: i18n.t('Color'), dataKey: 'color' }, + { name: i18n.t('Name2'), dataKey: 'name2' }, + { name: i18n.t('Id2'), dataKey: 'id2' }, + { name: i18n.t('Value2'), dataKey: 'value2' }, + { name: i18n.t('Legend2'), dataKey: 'legend2' }, + { name: i18n.t('Range2'), dataKey: 'range2' }, + { name: i18n.t('Level2'), dataKey: 'level2' }, + { name: i18n.t('Parent2'), dataKey: 'parentName2' }, + { name: i18n.t('Type2'), dataKey: 'type2' }, + { name: i18n.t('Color2'), dataKey: 'color2' }, +] + const Table = () => { - const map = useSelector((state) => state.map) - const dataTable = useSelector((state) => state.dataTable) + const { mapViews } = useSelector((state) => state.map) + const activeLayerId = useSelector((state) => state.dataTable) // const allAggregations = useSelector((state) => state.aggregations) // const feature = useSelector((state) => state.feature) + const [{ sortField, sortDirection }, setSorting] = useReducer( + (sorting, newSorting) => ({ ...sorting, ...newSorting }), + { + sortField: 'name', + sortDirection: ASCENDING, + } + ) - const layer = map.mapViews.find((l) => l.id === dataTable) + const layer = mapViews.find((l) => l.id === activeLayerId) + const { data } = layer + const [rows, setRows] = useState([]) - if (!layer) { - return No data - } + useEffect(() => { + // update the sorting - // const aggregations = allAggregations[layer?.id] + if (!data) { + return + } - const { data } = layer + data.sort((a, b) => { + a = a.properties[sortField] + b = b.properties[sortField] + + if (typeof a === 'number') { + return sortDirection === ASCENDING ? a - b : b - a + } + + if (a !== undefined) { + return sortDirection === ASCENDING + ? a.localeCompare(b) + : b.localeCompare(a) + } + + return 0 + }) + + setRows( + data.map((row, index) => { + return [ + index, + row.properties.name, + row.properties.id, + row.properties.value, + row.properties.legend, + row.properties.range, + row.properties.level, + row.properties.parentName, + row.properties.type, + row.properties.color, + row.properties.name, + row.properties.id, + row.properties.value, + row.properties.legend, + row.properties.range, + row.properties.level, + row.properties.parentName, + row.properties.type, + row.properties.color, + ] + }) + ) + }, [data, sortField, sortDirection]) + + // const aggregations = allAggregations[layer.id] + + const sortData = useCallback( + ({ name }) => { + setSorting({ + sortField: name, + sortDirection: + sortDirection === ASCENDING ? DESCENDING : ASCENDING, + }) + }, + [sortDirection] + ) - console.log('jj data', data) - - const thematicHeaders = [ - i18n.t('Index'), - i18n.t('Name'), - i18n.t('Id'), - i18n.t('Value'), - i18n.t('Legend'), - i18n.t('Range'), - i18n.t('Level'), - i18n.t('Parent'), - i18n.t('Type'), - i18n.t('Color'), - i18n.t('Name2'), - i18n.t('Id2'), - i18n.t('Value2'), - i18n.t('Legend2'), - i18n.t('Range2'), - i18n.t('Level2'), - i18n.t('Parent2'), - i18n.t('Type2'), - i18n.t('Color2'), - ] - - const rows = data.map((row, index) => { - return [ - index, - row.properties.name, - row.properties.id, - row.properties.value, - row.properties.legend, - row.properties.range, - row.properties.level, - row.properties.parentName, - row.properties.type, - row.properties.color, - row.properties.name, - row.properties.id, - row.properties.value, - row.properties.legend, - row.properties.range, - row.properties.level, - row.properties.parentName, - row.properties.type, - row.properties.color, - ] - }) + // console.log('jj render table with sorting', sortField, sortDirection) return ( - {thematicHeaders.map((header, index) => ( + {thematicHeaders.map(({ name, dataKey }, index) => ( - {header} + {name} ))} From eb141d6143cd9266159a065f5dc70c2409f77831 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 18 Oct 2023 15:15:55 +0200 Subject: [PATCH 03/57] feat: sort by index as well --- src/components/datatable/UiDataTable.js | 77 ++++++++++++++----------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index 4275e6fa8..0e615aac6 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -11,7 +11,13 @@ import { // Tooltip, } from '@dhis2/ui' import cx from 'classnames' -import React, { useState, useEffect, useReducer, useCallback } from 'react' +import React, { + useState, + useEffect, + useReducer, + useCallback, + useMemo, +} from 'react' import { useSelector } from 'react-redux' import styles from './styles/UiDataTable.module.css' @@ -55,23 +61,30 @@ const Table = () => { const layer = mapViews.find((l) => l.id === activeLayerId) const { data } = layer + + const rawData = useMemo( + () => + data && + data.map((d, index) => { + return Object.assign({ index, ...d.properties }) + }), + [data] + ) const [rows, setRows] = useState([]) useEffect(() => { - // update the sorting - - if (!data) { + if (!rawData) { return } - data.sort((a, b) => { - a = a.properties[sortField] - b = b.properties[sortField] + rawData.sort((a, b) => { + a = a[sortField] + b = b[sortField] if (typeof a === 'number') { return sortDirection === ASCENDING ? a - b : b - a } - + // TODO: Make sure sorting works across different locales - use lib method if (a !== undefined) { return sortDirection === ASCENDING ? a.localeCompare(b) @@ -82,31 +95,29 @@ const Table = () => { }) setRows( - data.map((row, index) => { - return [ - index, - row.properties.name, - row.properties.id, - row.properties.value, - row.properties.legend, - row.properties.range, - row.properties.level, - row.properties.parentName, - row.properties.type, - row.properties.color, - row.properties.name, - row.properties.id, - row.properties.value, - row.properties.legend, - row.properties.range, - row.properties.level, - row.properties.parentName, - row.properties.type, - row.properties.color, - ] - }) + rawData.map((row) => [ + row.index, + row.name, + row.id, + row.value, + row.legend, + row.range, + row.level, + row.parentName, + row.type, + row.color, + row.name, + row.id, + row.value, + row.legend, + row.range, + row.level, + row.parentName, + row.type, + row.color, + ]) ) - }, [data, sortField, sortDirection]) + }, [rawData, sortField, sortDirection]) // const aggregations = allAggregations[layer.id] @@ -121,8 +132,6 @@ const Table = () => { [sortDirection] ) - // console.log('jj render table with sorting', sortField, sortDirection) - return ( From e77d7fdd1df79c4fbf0e24bb5c30cbd2defc1626 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 19 Oct 2023 10:21:09 +0200 Subject: [PATCH 04/57] chore: set bgcolor and font color for color cell --- src/components/datatable/UiDataTable.js | 120 ++++++++++++------ .../datatable/styles/UiDataTable.module.css | 4 + 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index 0e615aac6..b5d377e50 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -19,12 +19,65 @@ import React, { useMemo, } from 'react' import { useSelector } from 'react-redux' +import { + EVENT_LAYER, + THEMATIC_LAYER, + ORG_UNIT_LAYER, + // EARTH_ENGINE_LAYER, +} from '../../constants/layers.js' +import { isDarkColor } from '../../util/colors.js' import styles from './styles/UiDataTable.module.css' const ASCENDING = 'asc' const DESCENDING = 'desc' -const thematicHeaders = [ +const getThematicHeaders = () => [ + { name: i18n.t('Index'), dataKey: 'index' }, + { name: i18n.t('Name'), dataKey: 'name' }, + { name: i18n.t('Id'), dataKey: 'id' }, + { name: i18n.t('Value'), dataKey: 'value' }, + { name: i18n.t('Legend'), dataKey: 'legend' }, + { name: i18n.t('Range'), dataKey: 'range' }, + { name: i18n.t('Level'), dataKey: 'level' }, + { name: i18n.t('Parent'), dataKey: 'parentName' }, + { name: i18n.t('Type'), dataKey: 'type' }, + { name: i18n.t('Color'), dataKey: 'color' }, + // { name: i18n.t('Name2'), dataKey: 'name2' }, + // { name: i18n.t('Id2'), dataKey: 'id2' }, + // { name: i18n.t('Value2'), dataKey: 'value2' }, + // { name: i18n.t('Legend2'), dataKey: 'legend2' }, + // { name: i18n.t('Range2'), dataKey: 'range2' }, + // { name: i18n.t('Level2'), dataKey: 'level2' }, + // { name: i18n.t('Parent2'), dataKey: 'parentName2' }, + // { name: i18n.t('Type2'), dataKey: 'type2' }, + // { name: i18n.t('Color2'), dataKey: 'color2' }, +] + +const thematicFields = [ + 'index', + 'name', + 'id', + 'value', + 'legend', + 'range', + 'level', + 'parentName', + 'type', + 'color', +] + +const getEventHeaders = (layer) => { + return [ + { name: i18n.t('Index'), dataKey: 'index' }, + { name: i18n.t('Org unit'), dataKey: 'ouname' }, + { name: i18n.t('Id'), dataKey: 'id' }, + { name: i18n.t('Event time'), dataKey: 'eventdate' }, + { name: i18n.t('Type'), dataKey: 'type' }, + // { name: i18n.t('Color'), dataKey: 'color' }, + ] +} + +const getOrgUnitHeaders = () => [ { name: i18n.t('Index'), dataKey: 'index' }, { name: i18n.t('Name'), dataKey: 'name' }, { name: i18n.t('Id'), dataKey: 'id' }, @@ -35,15 +88,6 @@ const thematicHeaders = [ { name: i18n.t('Parent'), dataKey: 'parentName' }, { name: i18n.t('Type'), dataKey: 'type' }, { name: i18n.t('Color'), dataKey: 'color' }, - { name: i18n.t('Name2'), dataKey: 'name2' }, - { name: i18n.t('Id2'), dataKey: 'id2' }, - { name: i18n.t('Value2'), dataKey: 'value2' }, - { name: i18n.t('Legend2'), dataKey: 'legend2' }, - { name: i18n.t('Range2'), dataKey: 'range2' }, - { name: i18n.t('Level2'), dataKey: 'level2' }, - { name: i18n.t('Parent2'), dataKey: 'parentName2' }, - { name: i18n.t('Type2'), dataKey: 'type2' }, - { name: i18n.t('Color2'), dataKey: 'color2' }, ] const Table = () => { @@ -62,6 +106,18 @@ const Table = () => { const layer = mapViews.find((l) => l.id === activeLayerId) const { data } = layer + const getHeaders = () => { + if (layer.layer === THEMATIC_LAYER) { + return getThematicHeaders() + } else if (layer.layer === EVENT_LAYER) { + return getEventHeaders(layer) + } else if (layer.layer === ORG_UNIT_LAYER) { + return getOrgUnitHeaders(layer) + } + // else if (layer.layer === EARTH_ENGINE_LAYER) { + // } + } + const rawData = useMemo( () => data && @@ -95,27 +151,12 @@ const Table = () => { }) setRows( - rawData.map((row) => [ - row.index, - row.name, - row.id, - row.value, - row.legend, - row.range, - row.level, - row.parentName, - row.type, - row.color, - row.name, - row.id, - row.value, - row.legend, - row.range, - row.level, - row.parentName, - row.type, - row.color, - ]) + rawData.map((item) => { + return thematicFields.map((key) => ({ + value: item[key], + dataKey: key, + })) + }) ) }, [rawData, sortField, sortDirection]) @@ -136,7 +177,7 @@ const Table = () => { - {thematicHeaders.map(({ name, dataKey }, index) => ( + {getHeaders().map(({ name, dataKey }, index) => ( { {rows.map((row, index) => ( - {row.map((value, cellindex) => ( + {row.map(({ dataKey, value }) => ( {value} diff --git a/src/components/datatable/styles/UiDataTable.module.css b/src/components/datatable/styles/UiDataTable.module.css index 0a8a75e4e..e7ee42688 100644 --- a/src/components/datatable/styles/UiDataTable.module.css +++ b/src/components/datatable/styles/UiDataTable.module.css @@ -6,3 +6,7 @@ td.fontClass { font-size: 11px; } +td.darkText { + color: var(--colors-white); +} + From f47bc15851b99ce70c1161b33f9927f9c81a9352 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 19 Oct 2023 11:38:07 +0200 Subject: [PATCH 05/57] chore: lowercase color code --- src/components/datatable/UiDataTable.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index b5d377e50..01b40c32a 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -218,7 +218,9 @@ const Table = () => { dataKey === 'color' ? value : null } > - {value} + {dataKey === 'color' + ? value.toLowerCase() + : value} ))} From 4c0cfcd61e2b848920923e13b9d3156ebd686f11 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 19 Oct 2023 15:40:31 +0200 Subject: [PATCH 06/57] chore: filtering --- src/components/datatable/FilterInput.js | 57 +++----- src/components/datatable/UiDataTable.js | 130 +++++++++--------- .../datatable/styles/FilterInput.module.css | 7 - .../datatable/styles/UiDataTable.module.css | 5 + 4 files changed, 94 insertions(+), 105 deletions(-) diff --git a/src/components/datatable/FilterInput.js b/src/components/datatable/FilterInput.js index 109745bc0..a78f38250 100644 --- a/src/components/datatable/FilterInput.js +++ b/src/components/datatable/FilterInput.js @@ -1,19 +1,27 @@ +import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' import React from 'react' -import { connect } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { setDataFilter, clearDataFilter } from '../../actions/dataFilters.js' import styles from './styles/FilterInput.module.css' // http://adazzle.github.io/react-data-grid/examples.html#/custom-filters // https://github.com/adazzle/react-data-grid/tree/master/packages/react-data-grid-addons/src/cells/headerCells/filters -const FilterInput = ({ - layerId, - type, - dataKey, - filters, - setDataFilter, - clearDataFilter, -}) => { +const FilterInput = ({ type, dataKey }) => { + const dispatch = useDispatch() + const dataTable = useSelector((state) => state.dataTable) + const map = useSelector((state) => state.map) + + const overlay = + dataTable && map.mapViews.filter((layer) => layer.id === dataTable)[0] + + let layerId + let filters + if (overlay) { + layerId = overlay.id + filters = overlay.dataFilters || {} + } + const filterValue = filters[dataKey] || '' // https://stackoverflow.com/questions/36683770/react-how-to-get-the-value-of-an-input-field @@ -21,16 +29,17 @@ const FilterInput = ({ const value = evt.target.value if (value !== '') { - setDataFilter(layerId, dataKey, value) + dispatch(setDataFilter(layerId, dataKey, value)) } else { - clearDataFilter(layerId, dataKey, value) + dispatch(clearDataFilter(layerId, dataKey, value)) } } + // TODO: Support more field types return ( 3&<8' : 'Search'} // TODO: Support more field types + placeholder={type === 'number' ? '2,>3&<8' : i18n.t('Search')} value={filterValue} onClick={(evt) => evt.stopPropagation()} onChange={onChange} @@ -39,30 +48,8 @@ const FilterInput = ({ } FilterInput.propTypes = { - clearDataFilter: PropTypes.func.isRequired, dataKey: PropTypes.string.isRequired, - filters: PropTypes.object.isRequired, - layerId: PropTypes.string.isRequired, - setDataFilter: PropTypes.func.isRequired, type: PropTypes.string.isRequired, } -// Avoid needing to pass filter and actions to every input field -const mapStateToProps = ({ dataTable, map }) => { - const overlay = dataTable - ? map.mapViews.filter((layer) => layer.id === dataTable)[0] - : null - - if (overlay) { - return { - layerId: overlay.id, - filters: overlay.dataFilters || {}, - } - } - - return null -} - -export default connect(mapStateToProps, { setDataFilter, clearDataFilter })( - FilterInput -) +export default FilterInput diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index 01b40c32a..e79aed919 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -6,9 +6,6 @@ import { DataTableColumnHeader, DataTableHead, DataTableBody, - // DataTableFoot, - // Pagination, - // Tooltip, } from '@dhis2/ui' import cx from 'classnames' import React, { @@ -26,6 +23,8 @@ import { // EARTH_ENGINE_LAYER, } from '../../constants/layers.js' import { isDarkColor } from '../../util/colors.js' +import { filterData } from '../../util/filter.js' +import FilterInput from './FilterInput.js' import styles from './styles/UiDataTable.module.css' const ASCENDING = 'asc' @@ -33,15 +32,15 @@ const DESCENDING = 'desc' const getThematicHeaders = () => [ { name: i18n.t('Index'), dataKey: 'index' }, - { name: i18n.t('Name'), dataKey: 'name' }, - { name: i18n.t('Id'), dataKey: 'id' }, - { name: i18n.t('Value'), dataKey: 'value' }, - { name: i18n.t('Legend'), dataKey: 'legend' }, - { name: i18n.t('Range'), dataKey: 'range' }, - { name: i18n.t('Level'), dataKey: 'level' }, - { name: i18n.t('Parent'), dataKey: 'parentName' }, - { name: i18n.t('Type'), dataKey: 'type' }, - { name: i18n.t('Color'), dataKey: 'color' }, + { name: i18n.t('Name'), dataKey: 'name', type: 'string' }, + { name: i18n.t('Id'), dataKey: 'id', type: 'string' }, + { name: i18n.t('Value'), dataKey: 'value', type: 'number' }, + { name: i18n.t('Legend'), dataKey: 'legend', type: 'string' }, + { name: i18n.t('Range'), dataKey: 'range', type: 'string' }, + { name: i18n.t('Level'), dataKey: 'level', type: 'number' }, + { name: i18n.t('Parent'), dataKey: 'parentName', type: 'string' }, + { name: i18n.t('Type'), dataKey: 'type', type: 'string' }, + { name: i18n.t('Color'), dataKey: 'color', type: 'string' }, // { name: i18n.t('Name2'), dataKey: 'name2' }, // { name: i18n.t('Id2'), dataKey: 'id2' }, // { name: i18n.t('Value2'), dataKey: 'value2' }, @@ -53,19 +52,6 @@ const getThematicHeaders = () => [ // { name: i18n.t('Color2'), dataKey: 'color2' }, ] -const thematicFields = [ - 'index', - 'name', - 'id', - 'value', - 'legend', - 'range', - 'level', - 'parentName', - 'type', - 'color', -] - const getEventHeaders = (layer) => { return [ { name: i18n.t('Index'), dataKey: 'index' }, @@ -90,10 +76,24 @@ const getOrgUnitHeaders = () => [ { name: i18n.t('Color'), dataKey: 'color' }, ] +const getHeaders = (layer) => { + if (layer.layer === THEMATIC_LAYER) { + return getThematicHeaders() + } else if (layer.layer === EVENT_LAYER) { + return getEventHeaders(layer) + } else if (layer.layer === ORG_UNIT_LAYER) { + return getOrgUnitHeaders(layer) + } + // else if (layer.layer === EARTH_ENGINE_LAYER) { + // } +} + +const EMPTY_AGGREGATIONS = {} + const Table = () => { const { mapViews } = useSelector((state) => state.map) const activeLayerId = useSelector((state) => state.dataTable) - // const allAggregations = useSelector((state) => state.aggregations) + const allAggregations = useSelector((state) => state.aggregations) // const feature = useSelector((state) => state.feature) const [{ sortField, sortDirection }, setSorting] = useReducer( (sorting, newSorting) => ({ ...sorting, ...newSorting }), @@ -104,36 +104,33 @@ const Table = () => { ) const layer = mapViews.find((l) => l.id === activeLayerId) - const { data } = layer - - const getHeaders = () => { - if (layer.layer === THEMATIC_LAYER) { - return getThematicHeaders() - } else if (layer.layer === EVENT_LAYER) { - return getEventHeaders(layer) - } else if (layer.layer === ORG_UNIT_LAYER) { - return getOrgUnitHeaders(layer) + const aggregations = allAggregations[layer.id] || EMPTY_AGGREGATIONS + const { data, dataFilters } = layer + + const rows = useMemo(() => { + if (!data) { + return [] } - // else if (layer.layer === EARTH_ENGINE_LAYER) { - // } - } - const rawData = useMemo( - () => - data && - data.map((d, index) => { - return Object.assign({ index, ...d.properties }) - }), - [data] - ) - const [rows, setRows] = useState([]) + const indexedData = data + .map((d, i) => ({ + index: i, + ...d, + })) + .filter((d) => !d.properties.hasAdditionalGeometry) + .map((d, i) => { + return { + ...(d.properties || d), + ...aggregations[d.id], + index: d.index, + i, + } + }) - useEffect(() => { - if (!rawData) { - return - } + const filteredData = filterData(indexedData, dataFilters) - rawData.sort((a, b) => { + //sort + filteredData.sort((a, b) => { a = a[sortField] b = b[sortField] @@ -150,17 +147,13 @@ const Table = () => { return 0 }) - setRows( - rawData.map((item) => { - return thematicFields.map((key) => ({ - value: item[key], - dataKey: key, - })) - }) + return filteredData.map((item) => + getThematicHeaders().map(({ dataKey }) => ({ + value: item[dataKey], + dataKey, + })) ) - }, [rawData, sortField, sortDirection]) - - // const aggregations = allAggregations[layer.id] + }, [data, dataFilters, aggregations, sortField, sortDirection]) const sortData = useCallback( ({ name }) => { @@ -177,10 +170,11 @@ const Table = () => { - {getHeaders().map(({ name, dataKey }, index) => ( + {getHeaders(layer).map(({ name, dataKey, type }, index) => ( { sortIconTitle={i18n.t('Sort by {{column}}', { column: name, })} + onFilterIconClick={type && Function.prototype} + showFilter={!!type} + filter={ + type && ( + + ) + } > {name} diff --git a/src/components/datatable/styles/FilterInput.module.css b/src/components/datatable/styles/FilterInput.module.css index 91807f2e2..773c179ef 100644 --- a/src/components/datatable/styles/FilterInput.module.css +++ b/src/components/datatable/styles/FilterInput.module.css @@ -1,12 +1,5 @@ .filterInput { - position: absolute; - left: 0; - top: 22px; - box-sizing: border-box; width: 100%; - height: 24px; - font-weight: normal; - padding-left: 5px; } .filterInput::placeholder { diff --git a/src/components/datatable/styles/UiDataTable.module.css b/src/components/datatable/styles/UiDataTable.module.css index e7ee42688..a2272aa54 100644 --- a/src/components/datatable/styles/UiDataTable.module.css +++ b/src/components/datatable/styles/UiDataTable.module.css @@ -10,3 +10,8 @@ td.darkText { color: var(--colors-white); } +/* Hide the filter icon */ +.columnHeader > span > span > button:last-of-type { + visibility: hidden; +} + From 21499dcb9c6594911df942801b1d479d266d452c Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Fri, 20 Oct 2023 10:37:32 +0200 Subject: [PATCH 07/57] chore: event layer table --- src/components/datatable/UiDataTable.js | 172 +++++++++++++----- .../datatable/styles/UiDataTable.module.css | 12 ++ 2 files changed, 139 insertions(+), 45 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index e79aed919..d1ec2d20f 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -8,20 +8,26 @@ import { DataTableBody, } from '@dhis2/ui' import cx from 'classnames' +import { isValidUid } from 'd2/uid' // TODO replace +import { debounce } from 'lodash/fp' import React, { - useState, + // useState, useEffect, useReducer, useCallback, useMemo, } from 'react' -import { useSelector } from 'react-redux' +import { useSelector, useDispatch } from 'react-redux' +import { closeDataTable } from '../../actions/dataTable.js' +import { highlightFeature } from '../../actions/feature.js' +import { setOrgUnitProfile } from '../../actions/orgUnits.js' import { EVENT_LAYER, THEMATIC_LAYER, ORG_UNIT_LAYER, // EARTH_ENGINE_LAYER, } from '../../constants/layers.js' +import { numberValueTypes } from '../../constants/valueTypes.js' import { isDarkColor } from '../../util/colors.js' import { filterData } from '../../util/filter.js' import FilterInput from './FilterInput.js' @@ -32,35 +38,63 @@ const DESCENDING = 'desc' const getThematicHeaders = () => [ { name: i18n.t('Index'), dataKey: 'index' }, - { name: i18n.t('Name'), dataKey: 'name', type: 'string' }, - { name: i18n.t('Id'), dataKey: 'id', type: 'string' }, - { name: i18n.t('Value'), dataKey: 'value', type: 'number' }, - { name: i18n.t('Legend'), dataKey: 'legend', type: 'string' }, - { name: i18n.t('Range'), dataKey: 'range', type: 'string' }, - { name: i18n.t('Level'), dataKey: 'level', type: 'number' }, - { name: i18n.t('Parent'), dataKey: 'parentName', type: 'string' }, - { name: i18n.t('Type'), dataKey: 'type', type: 'string' }, - { name: i18n.t('Color'), dataKey: 'color', type: 'string' }, - // { name: i18n.t('Name2'), dataKey: 'name2' }, - // { name: i18n.t('Id2'), dataKey: 'id2' }, - // { name: i18n.t('Value2'), dataKey: 'value2' }, - // { name: i18n.t('Legend2'), dataKey: 'legend2' }, - // { name: i18n.t('Range2'), dataKey: 'range2' }, - // { name: i18n.t('Level2'), dataKey: 'level2' }, - // { name: i18n.t('Parent2'), dataKey: 'parentName2' }, - // { name: i18n.t('Type2'), dataKey: 'type2' }, - // { name: i18n.t('Color2'), dataKey: 'color2' }, + { name: i18n.t('Name'), dataKey: 'name', type: TYPE_STRING }, + { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, + { name: i18n.t('Value'), dataKey: 'value', type: TYPE_NUMBER }, + { name: i18n.t('Legend'), dataKey: 'legend', type: TYPE_STRING }, + { name: i18n.t('Range'), dataKey: 'range', type: TYPE_STRING }, + { name: i18n.t('Level'), dataKey: 'level', type: TYPE_NUMBER }, + { name: i18n.t('Parent'), dataKey: 'parentName', type: TYPE_STRING }, + { name: i18n.t('Type'), dataKey: 'type', type: TYPE_STRING }, + { + name: i18n.t('Color'), + dataKey: 'color', + type: TYPE_STRING, + renderer: 'rendercolor', + }, ] +const TYPE_NUMBER = 'number' +const TYPE_STRING = 'string' +const TYPE_DATE = 'date' + const getEventHeaders = (layer) => { - return [ + const defaultFieldsStart = [ { name: i18n.t('Index'), dataKey: 'index' }, - { name: i18n.t('Org unit'), dataKey: 'ouname' }, - { name: i18n.t('Id'), dataKey: 'id' }, - { name: i18n.t('Event time'), dataKey: 'eventdate' }, - { name: i18n.t('Type'), dataKey: 'type' }, - // { name: i18n.t('Color'), dataKey: 'color' }, + { name: i18n.t('Org unit'), dataKey: 'ouname', type: TYPE_STRING }, + { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, + { + name: i18n.t('Event time'), + dataKey: 'eventdate', + type: TYPE_DATE, + renderer: 'formatTime...', + }, ] + + const { headers = [] } = layer + + const customFields = headers + .filter(({ name }) => isValidUid(name)) + .map(({ name, column, valueType }) => ({ + name: column, + dataKey: name, + type: numberValueTypes.includes(valueType) + ? TYPE_NUMBER + : TYPE_STRING, + })) + + const defaultFieldsEnd = [{ name: i18n.t('Type'), dataKey: 'type' }] + + if (layer.styleDataItem) { + defaultFieldsEnd.push({ + name: i18n.t('Color'), + dataKey: 'color', + type: TYPE_STRING, + renderer: 'rendercolor', + }) + } + + return defaultFieldsStart.concat(customFields).concat(defaultFieldsEnd) } const getOrgUnitHeaders = () => [ @@ -76,13 +110,13 @@ const getOrgUnitHeaders = () => [ { name: i18n.t('Color'), dataKey: 'color' }, ] -const getHeaders = (layer) => { +const getHeaders = (layer, styleDataItem) => { if (layer.layer === THEMATIC_LAYER) { return getThematicHeaders() } else if (layer.layer === EVENT_LAYER) { - return getEventHeaders(layer) + return getEventHeaders(layer, styleDataItem) } else if (layer.layer === ORG_UNIT_LAYER) { - return getOrgUnitHeaders(layer) + return getOrgUnitHeaders(layer, styleDataItem) } // else if (layer.layer === EARTH_ENGINE_LAYER) { // } @@ -94,7 +128,8 @@ const Table = () => { const { mapViews } = useSelector((state) => state.map) const activeLayerId = useSelector((state) => state.dataTable) const allAggregations = useSelector((state) => state.aggregations) - // const feature = useSelector((state) => state.feature) + const dispatch = useDispatch() + const feature = useSelector((state) => state.feature) const [{ sortField, sortDirection }, setSorting] = useReducer( (sorting, newSorting) => ({ ...sorting, ...newSorting }), { @@ -105,27 +140,26 @@ const Table = () => { const layer = mapViews.find((l) => l.id === activeLayerId) const aggregations = allAggregations[layer.id] || EMPTY_AGGREGATIONS - const { data, dataFilters } = layer const rows = useMemo(() => { - if (!data) { + if (!layer) { return [] } + const { data, dataFilters } = layer + const indexedData = data .map((d, i) => ({ index: i, ...d, })) .filter((d) => !d.properties.hasAdditionalGeometry) - .map((d, i) => { - return { - ...(d.properties || d), - ...aggregations[d.id], - index: d.index, - i, - } - }) + .map((d, i) => ({ + ...(d.properties || d), + ...aggregations[d.id], + index: d.index, + i, + })) const filteredData = filterData(indexedData, dataFilters) @@ -134,7 +168,7 @@ const Table = () => { a = a[sortField] b = b[sortField] - if (typeof a === 'number') { + if (typeof a === TYPE_NUMBER) { return sortDirection === ASCENDING ? a - b : b - a } // TODO: Make sure sorting works across different locales - use lib method @@ -148,12 +182,19 @@ const Table = () => { }) return filteredData.map((item) => - getThematicHeaders().map(({ dataKey }) => ({ + getHeaders(layer).map(({ dataKey }) => ({ value: item[dataKey], dataKey, })) ) - }, [data, dataFilters, aggregations, sortField, sortDirection]) + }, [layer, aggregations, sortField, sortDirection]) + + useEffect(() => { + // TODO - improve and test + if (rows !== null && !rows.length) { + dispatch(closeDataTable()) + } + }, [rows, dispatch]) const sortData = useCallback( ({ name }) => { @@ -166,6 +207,43 @@ const Table = () => { [sortDirection] ) + if (layer.serverCluster) { + return ( +
+ {i18n.t( + 'Data table is not supported when events are grouped on the server.' + )} +
+ ) + } + + const onTableRowClick = (row) => { + const id = row.find((r) => r.dataKey === 'id')?.value + id && dispatch(setOrgUnitProfile(id)) + } + + //TODO + // Debounce needed as event is triggered multiple times for the same row + const highlightMapFeature = debounce(50, (id) => { + if (!id || !feature || id !== feature.id) { + dispatch( + highlightFeature( + id + ? { + id, + layerId: layer.id, + origin: 'table', + } + : null + ) + ) + } + }) + + // TODO - need this implemented in ui + // const onMouseOver = (row) => console.log('row', row) + // const onRowMouseOut = () => highlightMapFeature() + return ( @@ -205,7 +283,10 @@ const Table = () => { {rows.map((row, index) => ( - + onMouseOver(row)} + > {row.map(({ dataKey, value }) => ( { backgroundColor={ dataKey === 'color' ? value : null } + onClick={() => onTableRowClick(row)} > {dataKey === 'color' - ? value.toLowerCase() + ? value?.toLowerCase() : value} ))} diff --git a/src/components/datatable/styles/UiDataTable.module.css b/src/components/datatable/styles/UiDataTable.module.css index a2272aa54..5aa509a4d 100644 --- a/src/components/datatable/styles/UiDataTable.module.css +++ b/src/components/datatable/styles/UiDataTable.module.css @@ -15,3 +15,15 @@ td.darkText { visibility: hidden; } + +/* TODO */ +.noSupport { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + color: #333; + font-style: italic; + line-height: 30px; +} + From 905a2339b9f3ed64a11eaa8840d78083ad5e745d Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 24 Jan 2024 16:19:40 +0100 Subject: [PATCH 08/57] chore: localization --- i18n/en.pot | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 4ae87b4de..12b8caa48 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-11T09:20:59.829Z\n" -"PO-Revision-Date: 2024-01-11T09:20:59.829Z\n" +"POT-Creation-Date: 2024-01-24T15:10:55.357Z\n" +"PO-Revision-Date: 2024-01-24T15:10:55.357Z\n" msgid "Untitled map, {{date}}" msgstr "Untitled map, {{date}}" @@ -161,6 +161,12 @@ msgstr "Type" msgid "Data table is not supported when events are grouped on the server." msgstr "Data table is not supported when events are grouped on the server." +msgid "Search" +msgstr "Search" + +msgid "Sort by {{column}}" +msgstr "Sort by {{column}}" + msgid "Items" msgstr "Items" From e0836fbad633380e773a91b4717de8b06d6f1b96 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 25 Jan 2024 11:31:23 +0100 Subject: [PATCH 09/57] chore: update yarn.lock --- yarn.lock | 142 ++++++------------------------------------------------ 1 file changed, 16 insertions(+), 126 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7add303b8..9921a979d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,14 +26,7 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/code-frame@^7.22.13": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.8.3": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== @@ -76,17 +69,7 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.0" -"@babel/generator@^7.21.0", "@babel/generator@^7.7.2": - version "7.21.1" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.1.tgz#951cc626057bc0af2c35cd23e9c64d384dea83dd" - integrity sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA== - dependencies: - "@babel/types" "^7.21.0" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.23.0": +"@babel/generator@^7.21.0", "@babel/generator@^7.23.0", "@babel/generator@^7.7.2": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== @@ -156,12 +139,7 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-environment-visitor@^7.22.20": +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== @@ -173,15 +151,7 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" - integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== - dependencies: - "@babel/template" "^7.20.7" - "@babel/types" "^7.21.0" - -"@babel/helper-function-name@^7.23.0": +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0", "@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== @@ -189,14 +159,7 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.23.0" -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-hoist-variables@^7.22.5": +"@babel/helper-hoist-variables@^7.18.6", "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== @@ -279,36 +242,19 @@ dependencies: "@babel/types" "^7.20.0" -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-split-export-declaration@^7.22.6": +"@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.22.6": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== dependencies: "@babel/types" "^7.22.5" -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-string-parser@^7.22.5": +"@babel/helper-string-parser@^7.19.4", "@babel/helper-string-parser@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== -"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-identifier@^7.22.20": +"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== @@ -337,16 +283,7 @@ "@babel/traverse" "^7.21.0" "@babel/types" "^7.21.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.22.13": +"@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6", "@babel/highlight@^7.22.13": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== @@ -355,12 +292,7 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.0", "@babel/parser@^7.7.0": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" - integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== - -"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.0", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0", "@babel/parser@^7.7.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== @@ -1141,30 +1073,14 @@ core-js "^2.6.12" regenerator-runtime "^0.13.11" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.0", "@babel/runtime@^7.16.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" - integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.7.6": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.0", "@babel/runtime@^7.16.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/template@^7.22.15": +"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== @@ -1197,16 +1113,7 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" - integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== @@ -8308,12 +8215,7 @@ flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.14.7, follow-redirects@^1.14.9: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - -follow-redirects@^1.15.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.7, follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.3" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== @@ -14125,14 +14027,7 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" -rxjs@^7.5.1: - version "7.8.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== - dependencies: - tslib "^2.1.0" - -rxjs@^7.8.1: +rxjs@^7.5.1, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -15539,16 +15434,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== -tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== - tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From b315e27116b4d1585bad6704fa5c202c6425b0fe Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 25 Jan 2024 11:47:29 +0100 Subject: [PATCH 10/57] feat: add row hover and click behaviour --- src/components/datatable/UiDataTable.js | 97 ++++++++++++++----------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index d1ec2d20f..799c03c53 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -9,16 +9,8 @@ import { } from '@dhis2/ui' import cx from 'classnames' import { isValidUid } from 'd2/uid' // TODO replace -import { debounce } from 'lodash/fp' -import React, { - // useState, - useEffect, - useReducer, - useCallback, - useMemo, -} from 'react' +import React, { useReducer, useCallback, useMemo } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { closeDataTable } from '../../actions/dataTable.js' import { highlightFeature } from '../../actions/feature.js' import { setOrgUnitProfile } from '../../actions/orgUnits.js' import { @@ -189,12 +181,15 @@ const Table = () => { ) }, [layer, aggregations, sortField, sortDirection]) - useEffect(() => { - // TODO - improve and test - if (rows !== null && !rows.length) { - dispatch(closeDataTable()) - } - }, [rows, dispatch]) + // TODO: I (hendrik) disabled this effect, because it causes a bug: + // When a filter parameter is supplied that cause the rows.length to be 0, + // The tables closes unexpectedly and it is not possible to get it back + // TODO - improve and test + // useEffect(() => { + // if (rows !== null && !rows.length) { + // dispatch(closeDataTable()) + // } + // }, [rows, dispatch]) const sortData = useCallback( ({ name }) => { @@ -207,6 +202,46 @@ const Table = () => { [sortDirection] ) + const onTableRowClick = useCallback( + (row) => { + const id = row.find((r) => r.dataKey === 'id')?.value + id && dispatch(setOrgUnitProfile(id)) + }, + [dispatch] + ) + + const highlightFeatureOnMouseEnter = useCallback( + (row) => { + const id = row.find((r) => r.dataKey === 'id')?.value + if (!id || !feature || id !== feature.id) { + dispatch( + highlightFeature( + id + ? { + id, + layerId: layer.id, + origin: 'table', + } + : null + ) + ) + } + }, + [feature, dispatch, layer.id] + ) + const clearFeatureHighlightOnMouseLeave = useCallback( + (event) => { + const nextElement = event.toElement ?? event.relatedTarget + // When hovering to the next row the next element is a `TD` + // If this is the case `highlightFeatureOnMouseEnter` will + // fire and the highlight does not need to be cleared + if (nextElement.tagName !== 'TD') { + dispatch(highlightFeature(null)) + } + }, + [dispatch] + ) + if (layer.serverCluster) { return (
@@ -217,33 +252,6 @@ const Table = () => { ) } - const onTableRowClick = (row) => { - const id = row.find((r) => r.dataKey === 'id')?.value - id && dispatch(setOrgUnitProfile(id)) - } - - //TODO - // Debounce needed as event is triggered multiple times for the same row - const highlightMapFeature = debounce(50, (id) => { - if (!id || !feature || id !== feature.id) { - dispatch( - highlightFeature( - id - ? { - id, - layerId: layer.id, - origin: 'table', - } - : null - ) - ) - } - }) - - // TODO - need this implemented in ui - // const onMouseOver = (row) => console.log('row', row) - // const onRowMouseOut = () => highlightMapFeature() - return ( @@ -285,7 +293,9 @@ const Table = () => { {rows.map((row, index) => ( onMouseOver(row)} + onMouseEnter={() => highlightFeatureOnMouseEnter(row)} + onMouseLeave={clearFeatureHighlightOnMouseLeave} + onClick={() => onTableRowClick(row)} > {row.map(({ dataKey, value }) => ( { backgroundColor={ dataKey === 'color' ? value : null } - onClick={() => onTableRowClick(row)} > {dataKey === 'color' ? value?.toLowerCase() From 95794e3c4a4677450f12bda99f3ecbdff45bec0e Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 25 Jan 2024 16:10:09 +0100 Subject: [PATCH 11/57] feat: all supported layer types now showing in table --- src/components/datatable/UiDataTable.js | 197 ++++++++++++++++-------- 1 file changed, 136 insertions(+), 61 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index 799c03c53..c74110bf1 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -17,101 +17,174 @@ import { EVENT_LAYER, THEMATIC_LAYER, ORG_UNIT_LAYER, - // EARTH_ENGINE_LAYER, + EARTH_ENGINE_LAYER, + FACILITY_LAYER, } from '../../constants/layers.js' import { numberValueTypes } from '../../constants/valueTypes.js' import { isDarkColor } from '../../util/colors.js' +import { hasClasses, getPrecision } from '../../util/earthEngine.js' import { filterData } from '../../util/filter.js' +import { numberPrecision } from '../../util/numbers.js' import FilterInput from './FilterInput.js' import styles from './styles/UiDataTable.module.css' const ASCENDING = 'asc' const DESCENDING = 'desc' -const getThematicHeaders = () => [ - { name: i18n.t('Index'), dataKey: 'index' }, - { name: i18n.t('Name'), dataKey: 'name', type: TYPE_STRING }, - { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, - { name: i18n.t('Value'), dataKey: 'value', type: TYPE_NUMBER }, - { name: i18n.t('Legend'), dataKey: 'legend', type: TYPE_STRING }, - { name: i18n.t('Range'), dataKey: 'range', type: TYPE_STRING }, - { name: i18n.t('Level'), dataKey: 'level', type: TYPE_NUMBER }, - { name: i18n.t('Parent'), dataKey: 'parentName', type: TYPE_STRING }, - { name: i18n.t('Type'), dataKey: 'type', type: TYPE_STRING }, - { +const TYPE_NUMBER = 'number' +const TYPE_STRING = 'string' +const TYPE_DATE = 'date' + +const defaultFieldsMap = () => ({ + index: { name: i18n.t('Index'), dataKey: 'index' }, + name: { name: i18n.t('Name'), dataKey: 'name', type: TYPE_STRING }, + id: { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, + level: { name: i18n.t('Level'), dataKey: 'level', type: TYPE_NUMBER }, + parentName: { + name: i18n.t('Parent'), + dataKey: 'parentName', + type: TYPE_STRING, + }, + type: { name: i18n.t('Type'), dataKey: 'type', type: TYPE_STRING }, + value: { name: i18n.t('Value'), dataKey: 'value', type: TYPE_NUMBER }, + legend: { name: i18n.t('Legend'), dataKey: 'legend', type: TYPE_STRING }, + range: { name: i18n.t('Range'), dataKey: 'range', type: TYPE_STRING }, + ouname: { name: i18n.t('Org unit'), dataKey: 'ouname', type: TYPE_STRING }, + eventdate: { + name: i18n.t('Event time'), + dataKey: 'eventdate', + type: TYPE_DATE, + renderer: 'formatTime...', + }, + color: { name: i18n.t('Color'), dataKey: 'color', type: TYPE_STRING, renderer: 'rendercolor', }, -] +}) -const TYPE_NUMBER = 'number' -const TYPE_STRING = 'string' -const TYPE_DATE = 'date' +const getThematicHeaders = () => + [ + 'index', + 'name', + 'id', + 'value', + 'legend', + 'range', + 'level', + 'parentName', + 'type', + 'color', + ].map((field) => defaultFieldsMap()[field]) const getEventHeaders = (layer) => { - const defaultFieldsStart = [ - { name: i18n.t('Index'), dataKey: 'index' }, - { name: i18n.t('Org unit'), dataKey: 'ouname', type: TYPE_STRING }, - { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, - { - name: i18n.t('Event time'), - dataKey: 'eventdate', - type: TYPE_DATE, - renderer: 'formatTime...', - }, - ] + const fields = ['index', 'ouname', 'id', 'eventdate'].map( + (field) => defaultFieldsMap()[field] + ) const { headers = [] } = layer const customFields = headers .filter(({ name }) => isValidUid(name)) - .map(({ name, column, valueType }) => ({ - name: column, - dataKey: name, + .map(({ name: dataKey, column: name, valueType }) => ({ + name, + dataKey, type: numberValueTypes.includes(valueType) ? TYPE_NUMBER : TYPE_STRING, })) - const defaultFieldsEnd = [{ name: i18n.t('Type'), dataKey: 'type' }] + customFields.push([defaultFieldsMap().type]) + + if (layer.styleDataItem) { + customFields.push(defaultFieldsMap().color) + } + + return fields.concat(customFields) +} +const getOrgUnitHeaders = (layer) => { + const fields = ['index', 'name', 'id', 'level', 'parentName', 'type'] if (layer.styleDataItem) { - defaultFieldsEnd.push({ - name: i18n.t('Color'), - dataKey: 'color', - type: TYPE_STRING, - renderer: 'rendercolor', + fields.push('color') + } + + return fields.map((field) => defaultFieldsMap()[field]) +} + +const getFacilityHeaders = (layer) => { + const fields = ['index', 'name', 'id', 'type'] + if (layer.styleDataItem) { + fields.push('color') + } + return fields.map((field) => defaultFieldsMap()[field]) +} + +const toTitleCase = (str) => + str.replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ) + +const getEarthEngineHeaders = ( + { aggregationType, legend, styleDataItem }, + data +) => { + const { title, items } = legend + + const defaultFields = ['index', 'name', 'id', 'type'].map( + (field) => defaultFieldsMap()[field] + ) + + let customFields = [] + + if (hasClasses(aggregationType) && items) { + customFields = items.map(({ id, name }) => ({ + name, + dataKey: String(id), + roundFn: numberPrecision(2), + type: TYPE_NUMBER, + })) + } else if (Array.isArray(aggregationType) && aggregationType.length) { + customFields = aggregationType.map((type) => { + let roundFn = null + if (data?.length) { + const precision = getPrecision(data.map((d) => d[type])) + roundFn = numberPrecision(precision) + } + return { + name: toTitleCase(`${type} ${title}`), + dataKey: type, + roundFn, + type: TYPE_NUMBER, + } }) } - return defaultFieldsStart.concat(customFields).concat(defaultFieldsEnd) + if (styleDataItem) { + customFields.push(defaultFieldsMap().color) + } + + return defaultFields.concat(customFields) } -const getOrgUnitHeaders = () => [ - { name: i18n.t('Index'), dataKey: 'index' }, - { name: i18n.t('Name'), dataKey: 'name' }, - { name: i18n.t('Id'), dataKey: 'id' }, - { name: i18n.t('Value'), dataKey: 'value' }, - { name: i18n.t('Legend'), dataKey: 'legend' }, - { name: i18n.t('Range'), dataKey: 'range' }, - { name: i18n.t('Level'), dataKey: 'level' }, - { name: i18n.t('Parent'), dataKey: 'parentName' }, - { name: i18n.t('Type'), dataKey: 'type' }, - { name: i18n.t('Color'), dataKey: 'color' }, -] - -const getHeaders = (layer, styleDataItem) => { - if (layer.layer === THEMATIC_LAYER) { - return getThematicHeaders() - } else if (layer.layer === EVENT_LAYER) { - return getEventHeaders(layer, styleDataItem) - } else if (layer.layer === ORG_UNIT_LAYER) { - return getOrgUnitHeaders(layer, styleDataItem) +const getHeaders = (layer, data) => { + switch (layer.layer) { + case THEMATIC_LAYER: + return getThematicHeaders() + case EVENT_LAYER: + return getEventHeaders(layer) + case ORG_UNIT_LAYER: + return getOrgUnitHeaders(layer) + case EARTH_ENGINE_LAYER: + return getEarthEngineHeaders(layer, data) + case FACILITY_LAYER: + return getFacilityHeaders(layer) + default: + // TODO - throw error? + return [] } - // else if (layer.layer === EARTH_ENGINE_LAYER) { - // } } const EMPTY_AGGREGATIONS = {} @@ -173,9 +246,11 @@ const Table = () => { return 0 }) + const headers = getHeaders(layer, filteredData) + return filteredData.map((item) => - getHeaders(layer).map(({ dataKey }) => ({ - value: item[dataKey], + headers.map(({ dataKey, roundFn }) => ({ + value: roundFn ? roundFn(item[dataKey]) : item[dataKey], dataKey, })) ) From 58f8247a414681f1e53f369fad0b2299d31c19c9 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 25 Jan 2024 16:41:34 +0100 Subject: [PATCH 12/57] feat: virtually scrolling table with dhis2 ui components --- package.json | 1 + src/components/datatable/BottomPanel.js | 2 +- src/components/datatable/UiDataTable.js | 109 ++++++++++++++++-------- yarn.lock | 13 ++- 4 files changed, 84 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 0678986a0..352aa783f 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "react-redux": "^8.1.2", "react-sortable-hoc": "^1.11.0", "react-virtualized": "^9.22.5", + "react-virtuoso": "^4.6.2", "redux": "^4.2.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.4.2", diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 4087905f5..4512614a8 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -53,7 +53,7 @@ const BottomPanel = () => { onResize={onResize} onResizeEnd={(height) => dispatch(resizeDataTable(height))} /> - +
) } diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index 799c03c53..f295dfcf3 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -9,8 +9,10 @@ import { } from '@dhis2/ui' import cx from 'classnames' import { isValidUid } from 'd2/uid' // TODO replace +import PropTypes from 'prop-types' import React, { useReducer, useCallback, useMemo } from 'react' import { useSelector, useDispatch } from 'react-redux' +import { TableVirtuoso } from 'react-virtuoso' import { highlightFeature } from '../../actions/feature.js' import { setOrgUnitProfile } from '../../actions/orgUnits.js' import { @@ -25,6 +27,38 @@ import { filterData } from '../../util/filter.js' import FilterInput from './FilterInput.js' import styles from './styles/UiDataTable.module.css' +const DataTableRowWithVirtuosoContext = ({ context, item, ...props }) => { + return ( + context.onClick(item)} + onMouseEnter={() => context.onMouseEnter(item)} + onMouseLeave={context.onMouseLeave} + {...props} + /> + ) +} + +DataTableRowWithVirtuosoContext.propTypes = { + context: PropTypes.shape({ + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + }), + item: PropTypes.arrayOf( + PropTypes.shape({ + dataKey: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + }) + ), +} + +const TableComponents = { + Table: DataTable, + TableBody: DataTableBody, + TableHead: DataTableHead, + TableRow: DataTableRowWithVirtuosoContext, +} + const ASCENDING = 'asc' const DESCENDING = 'desc' @@ -116,7 +150,7 @@ const getHeaders = (layer, styleDataItem) => { const EMPTY_AGGREGATIONS = {} -const Table = () => { +const Table = ({ height }) => { const { mapViews } = useSelector((state) => state.map) const activeLayerId = useSelector((state) => state.dataTable) const allAggregations = useSelector((state) => state.aggregations) @@ -242,6 +276,19 @@ const Table = () => { [dispatch] ) + const tableContext = useMemo( + () => ({ + onClick: onTableRowClick, + onMouseEnter: highlightFeatureOnMouseEnter, + onMouseLeave: clearFeatureHighlightOnMouseLeave, + }), + [ + onTableRowClick, + highlightFeatureOnMouseEnter, + clearFeatureHighlightOnMouseLeave, + ] + ) + if (layer.serverCluster) { return (
@@ -253,8 +300,12 @@ const Table = () => { } return ( - - + ( {getHeaders(layer).map(({ name, dataKey, type }, index) => ( { ))} - - - {rows.map((row, index) => ( - highlightFeatureOnMouseEnter(row)} - onMouseLeave={clearFeatureHighlightOnMouseLeave} - onClick={() => onTableRowClick(row)} + )} + itemContent={(_, row) => + row.map(({ dataKey, value }) => ( + - {row.map(({ dataKey, value }) => ( - - {dataKey === 'color' - ? value?.toLowerCase() - : value} - - ))} - - ))} - - + {dataKey === 'color' ? value?.toLowerCase() : value} + + )) + } + /> ) } +Table.propTypes = { + height: PropTypes.number, +} + export default Table diff --git a/yarn.lock b/yarn.lock index 9921a979d..417da6d33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -249,12 +249,12 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-string-parser@^7.19.4", "@babel/helper-string-parser@^7.22.5": +"@babel/helper-string-parser@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== -"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.20": +"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== @@ -283,7 +283,7 @@ "@babel/traverse" "^7.21.0" "@babel/types" "^7.21.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6", "@babel/highlight@^7.22.13": +"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.13": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== @@ -5414,7 +5414,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -13482,6 +13482,11 @@ react-virtualized@^9.22.5: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" +react-virtuoso@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.6.2.tgz#74b59ebe3260e1f73e92340ffec84a6853285a12" + integrity sha512-vvlqvzPif+MvBrJ09+hJJrVY0xJK9yran+A+/1iwY78k0YCVKsyoNPqoLxOxzYPggspNBNXqUXEcvckN29OxyQ== + react@^16.14.0, react@^16.8.6: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" From 2c8ce4faaa480ed026879bdbd627b2f41eca07e7 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 25 Jan 2024 17:17:18 +0100 Subject: [PATCH 13/57] chore: remove redundant props --- src/components/datatable/UiDataTable.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index f295dfcf3..59abe23ff 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -309,12 +309,8 @@ const Table = ({ height }) => { {getHeaders(layer).map(({ name, dataKey, type }, index) => ( Date: Fri, 26 Jan 2024 09:45:47 +0100 Subject: [PATCH 14/57] chore: more table logic to hook --- src/components/datatable/UiDataTable.js | 245 +---------------------- src/components/datatable/useTableData.js | 236 ++++++++++++++++++++++ 2 files changed, 245 insertions(+), 236 deletions(-) create mode 100644 src/components/datatable/useTableData.js diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index b6f30f04d..67b60e3b2 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -8,27 +8,19 @@ import { DataTableBody, } from '@dhis2/ui' import cx from 'classnames' -import { isValidUid } from 'd2/uid' // TODO replace import PropTypes from 'prop-types' import React, { useReducer, useCallback, useMemo } from 'react' import { useSelector, useDispatch } from 'react-redux' import { TableVirtuoso } from 'react-virtuoso' import { highlightFeature } from '../../actions/feature.js' import { setOrgUnitProfile } from '../../actions/orgUnits.js' -import { - EVENT_LAYER, - THEMATIC_LAYER, - ORG_UNIT_LAYER, - EARTH_ENGINE_LAYER, - FACILITY_LAYER, -} from '../../constants/layers.js' -import { numberValueTypes } from '../../constants/valueTypes.js' import { isDarkColor } from '../../util/colors.js' -import { hasClasses, getPrecision } from '../../util/earthEngine.js' -import { filterData } from '../../util/filter.js' -import { numberPrecision } from '../../util/numbers.js' import FilterInput from './FilterInput.js' import styles from './styles/UiDataTable.module.css' +import { useTableData } from './useTableData.js' + +const ASCENDING = 'asc' +const DESCENDING = 'desc' const DataTableRowWithVirtuosoContext = ({ context, item, ...props }) => { return ( @@ -62,171 +54,10 @@ const TableComponents = { TableRow: DataTableRowWithVirtuosoContext, } -const ASCENDING = 'asc' -const DESCENDING = 'desc' - -const TYPE_NUMBER = 'number' -const TYPE_STRING = 'string' -const TYPE_DATE = 'date' - -const defaultFieldsMap = () => ({ - index: { name: i18n.t('Index'), dataKey: 'index' }, - name: { name: i18n.t('Name'), dataKey: 'name', type: TYPE_STRING }, - id: { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, - level: { name: i18n.t('Level'), dataKey: 'level', type: TYPE_NUMBER }, - parentName: { - name: i18n.t('Parent'), - dataKey: 'parentName', - type: TYPE_STRING, - }, - type: { name: i18n.t('Type'), dataKey: 'type', type: TYPE_STRING }, - value: { name: i18n.t('Value'), dataKey: 'value', type: TYPE_NUMBER }, - legend: { name: i18n.t('Legend'), dataKey: 'legend', type: TYPE_STRING }, - range: { name: i18n.t('Range'), dataKey: 'range', type: TYPE_STRING }, - ouname: { name: i18n.t('Org unit'), dataKey: 'ouname', type: TYPE_STRING }, - eventdate: { - name: i18n.t('Event time'), - dataKey: 'eventdate', - type: TYPE_DATE, - renderer: 'formatTime...', - }, - color: { - name: i18n.t('Color'), - dataKey: 'color', - type: TYPE_STRING, - renderer: 'rendercolor', - }, -}) - -const getThematicHeaders = () => - [ - 'index', - 'name', - 'id', - 'value', - 'legend', - 'range', - 'level', - 'parentName', - 'type', - 'color', - ].map((field) => defaultFieldsMap()[field]) - -const getEventHeaders = (layer) => { - const fields = ['index', 'ouname', 'id', 'eventdate'].map( - (field) => defaultFieldsMap()[field] - ) - - const { headers = [] } = layer - - const customFields = headers - .filter(({ name }) => isValidUid(name)) - .map(({ name: dataKey, column: name, valueType }) => ({ - name, - dataKey, - type: numberValueTypes.includes(valueType) - ? TYPE_NUMBER - : TYPE_STRING, - })) - - customFields.push([defaultFieldsMap().type]) - - if (layer.styleDataItem) { - customFields.push(defaultFieldsMap().color) - } - - return fields.concat(customFields) -} - -const getOrgUnitHeaders = (layer) => { - const fields = ['index', 'name', 'id', 'level', 'parentName', 'type'] - if (layer.styleDataItem) { - fields.push('color') - } - - return fields.map((field) => defaultFieldsMap()[field]) -} - -const getFacilityHeaders = (layer) => { - const fields = ['index', 'name', 'id', 'type'] - if (layer.styleDataItem) { - fields.push('color') - } - return fields.map((field) => defaultFieldsMap()[field]) -} - -const toTitleCase = (str) => - str.replace( - /\w\S*/g, - (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() - ) - -const getEarthEngineHeaders = ( - { aggregationType, legend, styleDataItem }, - data -) => { - const { title, items } = legend - - const defaultFields = ['index', 'name', 'id', 'type'].map( - (field) => defaultFieldsMap()[field] - ) - - let customFields = [] - - if (hasClasses(aggregationType) && items) { - customFields = items.map(({ id, name }) => ({ - name, - dataKey: String(id), - roundFn: numberPrecision(2), - type: TYPE_NUMBER, - })) - } else if (Array.isArray(aggregationType) && aggregationType.length) { - customFields = aggregationType.map((type) => { - let roundFn = null - if (data?.length) { - const precision = getPrecision(data.map((d) => d[type])) - roundFn = numberPrecision(precision) - } - return { - name: toTitleCase(`${type} ${title}`), - dataKey: type, - roundFn, - type: TYPE_NUMBER, - } - }) - } - - if (styleDataItem) { - customFields.push(defaultFieldsMap().color) - } - - return defaultFields.concat(customFields) -} - -const getHeaders = (layer, data) => { - switch (layer.layer) { - case THEMATIC_LAYER: - return getThematicHeaders() - case EVENT_LAYER: - return getEventHeaders(layer) - case ORG_UNIT_LAYER: - return getOrgUnitHeaders(layer) - case EARTH_ENGINE_LAYER: - return getEarthEngineHeaders(layer, data) - case FACILITY_LAYER: - return getFacilityHeaders(layer) - default: - // TODO - throw error? - return [] - } -} - -const EMPTY_AGGREGATIONS = {} - const Table = ({ height }) => { const { mapViews } = useSelector((state) => state.map) const activeLayerId = useSelector((state) => state.dataTable) - const allAggregations = useSelector((state) => state.aggregations) + const dispatch = useDispatch() const feature = useSelector((state) => state.feature) const [{ sortField, sortDirection }, setSorting] = useReducer( @@ -238,67 +69,6 @@ const Table = ({ height }) => { ) const layer = mapViews.find((l) => l.id === activeLayerId) - const aggregations = allAggregations[layer.id] || EMPTY_AGGREGATIONS - - const rows = useMemo(() => { - if (!layer) { - return [] - } - - const { data, dataFilters } = layer - - const indexedData = data - .map((d, i) => ({ - index: i, - ...d, - })) - .filter((d) => !d.properties.hasAdditionalGeometry) - .map((d, i) => ({ - ...(d.properties || d), - ...aggregations[d.id], - index: d.index, - i, - })) - - const filteredData = filterData(indexedData, dataFilters) - - //sort - filteredData.sort((a, b) => { - a = a[sortField] - b = b[sortField] - - if (typeof a === TYPE_NUMBER) { - return sortDirection === ASCENDING ? a - b : b - a - } - // TODO: Make sure sorting works across different locales - use lib method - if (a !== undefined) { - return sortDirection === ASCENDING - ? a.localeCompare(b) - : b.localeCompare(a) - } - - return 0 - }) - - const headers = getHeaders(layer, filteredData) - - return filteredData.map((item) => - headers.map(({ dataKey, roundFn }) => ({ - value: roundFn ? roundFn(item[dataKey]) : item[dataKey], - dataKey, - })) - ) - }, [layer, aggregations, sortField, sortDirection]) - - // TODO: I (hendrik) disabled this effect, because it causes a bug: - // When a filter parameter is supplied that cause the rows.length to be 0, - // The tables closes unexpectedly and it is not possible to get it back - // TODO - improve and test - // useEffect(() => { - // if (rows !== null && !rows.length) { - // dispatch(closeDataTable()) - // } - // }, [rows, dispatch]) const sortData = useCallback( ({ name }) => { @@ -311,6 +81,8 @@ const Table = ({ height }) => { [sortDirection] ) + const { headers, rows } = useTableData({ layer, sortField, sortDirection }) + const onTableRowClick = useCallback( (row) => { const id = row.find((r) => r.dataKey === 'id')?.value @@ -382,7 +154,7 @@ const Table = ({ height }) => { data={rows} fixedHeaderContent={() => ( - {getHeaders(layer).map(({ name, dataKey, type }, index) => ( + {headers.map(({ name, dataKey, type }, index) => ( { })} onFilterIconClick={type && Function.prototype} showFilter={!!type} + name={dataKey} filter={ type && ( ({ + index: { name: i18n.t('Index'), dataKey: 'index' }, + name: { name: i18n.t('Name'), dataKey: 'name', type: TYPE_STRING }, + id: { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, + level: { name: i18n.t('Level'), dataKey: 'level', type: TYPE_NUMBER }, + parentName: { + name: i18n.t('Parent'), + dataKey: 'parentName', + type: TYPE_STRING, + }, + type: { name: i18n.t('Type'), dataKey: 'type', type: TYPE_STRING }, + value: { name: i18n.t('Value'), dataKey: 'value', type: TYPE_NUMBER }, + legend: { name: i18n.t('Legend'), dataKey: 'legend', type: TYPE_STRING }, + range: { name: i18n.t('Range'), dataKey: 'range', type: TYPE_STRING }, + ouname: { name: i18n.t('Org unit'), dataKey: 'ouname', type: TYPE_STRING }, + eventdate: { + name: i18n.t('Event time'), + dataKey: 'eventdate', + type: TYPE_DATE, + renderer: 'formatTime...', + }, + color: { + name: i18n.t('Color'), + dataKey: 'color', + type: TYPE_STRING, + renderer: 'rendercolor', + }, +}) + +const getThematicHeaders = () => + [ + 'index', + 'name', + 'id', + 'value', + 'legend', + 'range', + 'level', + 'parentName', + 'type', + 'color', + ].map((field) => defaultFieldsMap()[field]) + +const getEventHeaders = ({ layerHeaders = [], styleDataItem }) => { + const fields = ['index', 'ouname', 'id', 'eventdate'].map( + (field) => defaultFieldsMap()[field] + ) + + const customFields = layerHeaders + .filter(({ name }) => isValidUid(name)) + .map(({ name: dataKey, column: name, valueType }) => ({ + name, + dataKey, + type: numberValueTypes.includes(valueType) + ? TYPE_NUMBER + : TYPE_STRING, + })) + + customFields.push([defaultFieldsMap().type]) + + if (styleDataItem) { + customFields.push(defaultFieldsMap().color) + } + + return fields.concat(customFields) +} + +const getOrgUnitHeaders = () => + ['index', 'name', 'id', 'level', 'parentName', 'type'].map( + (field) => defaultFieldsMap()[field] + ) + +const getFacilityHeaders = () => + ['index', 'name', 'id', 'type'].map((field) => defaultFieldsMap()[field]) + +const toTitleCase = (str) => + str.replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ) + +const getEarthEngineHeaders = ({ aggregationType, legend, data }) => { + const { title, items } = legend + + let customFields = [] + + if (hasClasses(aggregationType) && items) { + customFields = items.map(({ id, name }) => ({ + name, + dataKey: String(id), + roundFn: numberPrecision(2), + type: TYPE_NUMBER, + })) + } else if (Array.isArray(aggregationType) && aggregationType.length) { + customFields = aggregationType.map((type) => { + let roundFn = null + if (data?.length) { + const precision = getPrecision(data.map((d) => d[type])) + roundFn = numberPrecision(precision) + } + return { + name: toTitleCase(`${type} ${title}`), + dataKey: type, + roundFn, + type: TYPE_NUMBER, + } + }) + } + + return ['index', 'name', 'id', 'type'] + .map((field) => defaultFieldsMap()[field]) + .concat(customFields) +} + +const EMPTY_AGGREGATIONS = {} +const EMPTY_LAYER = {} + +export const useTableData = ({ layer, sortField, sortDirection }) => { + const allAggregations = useSelector((state) => state.aggregations) + const aggregations = allAggregations[layer.id] || EMPTY_AGGREGATIONS + + const { + layer: layerType, + aggregationType, + legend, + styleDataItem, + data, + dataFilters, + headers: layerHeaders, + } = layer || EMPTY_LAYER + + const indexedData = useMemo(() => { + return data + .map((d, i) => ({ + index: i, + ...d, + })) + .filter((d) => !d.properties.hasAdditionalGeometry) + .map((d, i) => ({ + ...(d.properties || d), + ...aggregations[d.id], + index: d.index, + i, + })) + }, [data, aggregations]) + + const headers = useMemo(() => { + if (!layerType) { + return [] + } + + switch (layerType) { + case THEMATIC_LAYER: + return getThematicHeaders() + case EVENT_LAYER: + return getEventHeaders({ layerHeaders, styleDataItem }) + case ORG_UNIT_LAYER: + return getOrgUnitHeaders() + case EARTH_ENGINE_LAYER: + return getEarthEngineHeaders({ + aggregationType, + legend, + data: indexedData, + }) + case FACILITY_LAYER: + return getFacilityHeaders() + default: + // TODO - throw error? + return [] + } + }, [ + layerType, + aggregationType, + legend, + styleDataItem, + indexedData, + layerHeaders, + ]) + + const rows = useMemo(() => { + if (!indexedData.length && !headers?.length) { + return [] + } + + const filteredData = filterData(indexedData, dataFilters) + + //sort + filteredData.sort((a, b) => { + a = a[sortField] + b = b[sortField] + + if (typeof a === TYPE_NUMBER) { + return sortDirection === ASCENDING ? a - b : b - a + } + // TODO: Make sure sorting works across different locales - use lib method + if (a !== undefined) { + return sortDirection === ASCENDING + ? a.localeCompare(b) + : b.localeCompare(a) + } + + return 0 + }) + + return filteredData.map((item) => + headers.map(({ dataKey, roundFn }) => ({ + value: roundFn ? roundFn(item[dataKey]) : item[dataKey], + dataKey, + })) + ) + }, [indexedData, dataFilters, sortField, sortDirection, headers]) + + return { headers, rows } +} From e4b749f7c0e54f2fb22b95e0c424d1b205f9b979 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Fri, 26 Jan 2024 10:52:37 +0100 Subject: [PATCH 15/57] chore: use constants --- src/components/datatable/useTableData.js | 77 ++++++++++++++---------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 29969bb2c..2d67b27e1 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -20,30 +20,43 @@ const TYPE_NUMBER = 'number' const TYPE_STRING = 'string' const TYPE_DATE = 'date' +const INDEX = 'index' +const NAME = 'name' +const ID = 'id' +const VALUE = 'value' +const LEGEND = 'legend' +const RANGE = 'range' +const LEVEL = 'level' +const PARENT_NAME = 'parentName' +const TYPE = 'type' +const COLOR = 'color' +const OUNAME = 'ouname' +const EVENTDATE = 'eventdate' + const defaultFieldsMap = () => ({ - index: { name: i18n.t('Index'), dataKey: 'index' }, - name: { name: i18n.t('Name'), dataKey: 'name', type: TYPE_STRING }, - id: { name: i18n.t('Id'), dataKey: 'id', type: TYPE_STRING }, - level: { name: i18n.t('Level'), dataKey: 'level', type: TYPE_NUMBER }, - parentName: { + [INDEX]: { name: i18n.t('Index'), dataKey: INDEX }, + [NAME]: { name: i18n.t('Name'), dataKey: NAME, type: TYPE_STRING }, + [ID]: { name: i18n.t('Id'), dataKey: ID, type: TYPE_STRING }, + [LEVEL]: { name: i18n.t('Level'), dataKey: LEVEL, type: TYPE_NUMBER }, + [PARENT_NAME]: { name: i18n.t('Parent'), - dataKey: 'parentName', + dataKey: PARENT_NAME, type: TYPE_STRING, }, - type: { name: i18n.t('Type'), dataKey: 'type', type: TYPE_STRING }, - value: { name: i18n.t('Value'), dataKey: 'value', type: TYPE_NUMBER }, - legend: { name: i18n.t('Legend'), dataKey: 'legend', type: TYPE_STRING }, - range: { name: i18n.t('Range'), dataKey: 'range', type: TYPE_STRING }, - ouname: { name: i18n.t('Org unit'), dataKey: 'ouname', type: TYPE_STRING }, - eventdate: { + [TYPE]: { name: i18n.t('Type'), dataKey: TYPE, type: TYPE_STRING }, + [VALUE]: { name: i18n.t('Value'), dataKey: VALUE, type: TYPE_NUMBER }, + [LEGEND]: { name: i18n.t('Legend'), dataKey: LEGEND, type: TYPE_STRING }, + [RANGE]: { name: i18n.t('Range'), dataKey: RANGE, type: TYPE_STRING }, + [OUNAME]: { name: i18n.t('Org unit'), dataKey: OUNAME, type: TYPE_STRING }, + [EVENTDATE]: { name: i18n.t('Event time'), - dataKey: 'eventdate', + dataKey: EVENTDATE, type: TYPE_DATE, renderer: 'formatTime...', }, - color: { + [COLOR]: { name: i18n.t('Color'), - dataKey: 'color', + dataKey: COLOR, type: TYPE_STRING, renderer: 'rendercolor', }, @@ -51,20 +64,20 @@ const defaultFieldsMap = () => ({ const getThematicHeaders = () => [ - 'index', - 'name', - 'id', - 'value', - 'legend', - 'range', - 'level', - 'parentName', - 'type', - 'color', + INDEX, + NAME, + ID, + VALUE, + LEGEND, + RANGE, + LEVEL, + PARENT_NAME, + TYPE, + COLOR, ].map((field) => defaultFieldsMap()[field]) const getEventHeaders = ({ layerHeaders = [], styleDataItem }) => { - const fields = ['index', 'ouname', 'id', 'eventdate'].map( + const fields = [INDEX, OUNAME, ID, EVENTDATE].map( (field) => defaultFieldsMap()[field] ) @@ -78,22 +91,22 @@ const getEventHeaders = ({ layerHeaders = [], styleDataItem }) => { : TYPE_STRING, })) - customFields.push([defaultFieldsMap().type]) + customFields.push([defaultFieldsMap()[TYPE]]) if (styleDataItem) { - customFields.push(defaultFieldsMap().color) + customFields.push(defaultFieldsMap()[COLOR]) } return fields.concat(customFields) } const getOrgUnitHeaders = () => - ['index', 'name', 'id', 'level', 'parentName', 'type'].map( + [INDEX, NAME, ID, LEVEL, PARENT_NAME, TYPE].map( (field) => defaultFieldsMap()[field] ) const getFacilityHeaders = () => - ['index', 'name', 'id', 'type'].map((field) => defaultFieldsMap()[field]) + [INDEX, NAME, ID, TYPE].map((field) => defaultFieldsMap()[field]) const toTitleCase = (str) => str.replace( @@ -129,7 +142,7 @@ const getEarthEngineHeaders = ({ aggregationType, legend, data }) => { }) } - return ['index', 'name', 'id', 'type'] + return [INDEX, NAME, ID, TYPE] .map((field) => defaultFieldsMap()[field]) .concat(customFields) } @@ -200,7 +213,7 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { ]) const rows = useMemo(() => { - if (!indexedData.length && !headers?.length) { + if (!indexedData.length || !headers?.length) { return [] } From db0e2f24310c717983f946b3224db6ae46010763 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Fri, 26 Jan 2024 15:53:33 +0100 Subject: [PATCH 16/57] chore: add tests for useTableData hook --- .../__snapshots__/useTableData.spec.js.snap | 387 ++++++++++++++++++ .../datatable/__tests__/useTableData.spec.js | 363 ++++++++++++++++ src/components/datatable/useTableData.js | 44 +- 3 files changed, 773 insertions(+), 21 deletions(-) create mode 100644 src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap create mode 100644 src/components/datatable/__tests__/useTableData.spec.js diff --git a/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap b/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap new file mode 100644 index 000000000..f3c67d054 --- /dev/null +++ b/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap @@ -0,0 +1,387 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useTableData gets headers and rows for EE population age groups layer 1`] = ` +
+
    +
  • + name=Index, dataKey=index, type=undefined and roundFn=undefined +
  • +
  • + name=Name, dataKey=name, type=string and roundFn=undefined +
  • +
  • + name=Id, dataKey=id, type=string and roundFn=undefined +
  • +
  • + name=Type, dataKey=type, type=string and roundFn=undefined +
  • +
  • + name=Sum Population Age Groups, dataKey=sum, type=number and roundFn=n => Math.round(n * m) / m +
  • +
  • + name=Mean Population Age Groups, dataKey=mean, type=number and roundFn=n => Math.round(n * m) / m +
  • +
+
    +
      +
    • + value=0 and dataKey=index +
    • +
    • + value=Badija and dataKey=name +
    • +
    • + value=boOU and dataKey=id +
    • +
    • + value=Polygon and dataKey=type +
    • +
    • + value=2517 and dataKey=sum +
    • +
    • + value=3.976 and dataKey=mean +
    • +
    +
      +
    • + value=1 and dataKey=index +
    • +
    • + value=Baoma and dataKey=name +
    • +
    • + value=baomaOU and dataKey=id +
    • +
    • + value=Polygon and dataKey=type +
    • +
    • + value=9113 and dataKey=sum +
    • +
    • + value=6.003 and dataKey=mean +
    • +
    +
+
+`; + +exports[`useTableData gets headers and rows for EE population layer 1`] = ` +
+
    +
  • + name=Index, dataKey=index, type=undefined and roundFn=undefined +
  • +
  • + name=Name, dataKey=name, type=string and roundFn=undefined +
  • +
  • + name=Id, dataKey=id, type=string and roundFn=undefined +
  • +
  • + name=Type, dataKey=type, type=string and roundFn=undefined +
  • +
  • + name=Sum Population, dataKey=sum, type=number and roundFn=n => Math.round(n * m) / m +
  • +
  • + name=Mean Population, dataKey=mean, type=number and roundFn=n => Math.round(n * m) / m +
  • +
+
    +
      +
    • + value=0 and dataKey=index +
    • +
    • + value=Bo and dataKey=name +
    • +
    • + value=boOu and dataKey=id +
    • +
    • + value=Polygon and dataKey=type +
    • +
    • + value=851091 and dataKey=sum +
    • +
    • + value=47.35 and dataKey=mean +
    • +
    +
      +
    • + value=1 and dataKey=index +
    • +
    • + value=Bombali and dataKey=name +
    • +
    • + value=bombaliOU and dataKey=id +
    • +
    • + value=Polygon and dataKey=type +
    • +
    • + value=585872 and dataKey=sum +
    • +
    • + value=27.35 and dataKey=mean +
    • +
    +
+
+`; + +exports[`useTableData gets headers and rows for event layer 1`] = ` +
+
    +
  • + name=Index, dataKey=index, type=undefined and roundFn=undefined +
  • +
  • + name=Org unit, dataKey=ouname, type=string and roundFn=undefined +
  • +
  • + name=Id, dataKey=id, type=string and roundFn=undefined +
  • +
  • + name=Event time, dataKey=eventdate, type=date and roundFn=undefined +
  • +
  • + name=Last updated on, dataKey=lastupdated, type=string and roundFn=undefined +
  • +
  • + name=Event status, dataKey=eventstatus, type=string and roundFn=undefined +
  • +
  • + name=Gender, dataKey=oZg33kd9taw, type=string and roundFn=undefined +
  • +
  • + name=Type, dataKey=type, type=string and roundFn=undefined +
  • +
+
    +
      +
    • + value=0 and dataKey=index +
    • +
    • + value=Lumley Hospital and dataKey=ouname +
    • +
    • + value=a9712323629 and dataKey=id +
    • +
    • + value=2023-05-15 00:00:00.0 and dataKey=eventdate +
    • +
    • + value=2018-04-12 20:58:51.31 and dataKey=lastupdated +
    • +
    • + value=ACTIVE and dataKey=eventstatus +
    • +
    • + value=Female and dataKey=oZg33kd9taw +
    • +
    • + value=Point and dataKey=type +
    • +
    +
+
+`; + +exports[`useTableData gets headers and rows for facility layer 1`] = ` +
+
    +
  • + name=Index, dataKey=index, type=undefined and roundFn=undefined +
  • +
  • + name=Name, dataKey=name, type=string and roundFn=undefined +
  • +
  • + name=Id, dataKey=id, type=string and roundFn=undefined +
  • +
  • + name=Type, dataKey=type, type=string and roundFn=undefined +
  • +
+
    +
      +
    • + value=0 and dataKey=index +
    • +
    • + value=Facility 1 and dataKey=name +
    • +
    • + value=facility-1 and dataKey=id +
    • +
    • + value=Point and dataKey=type +
    • +
    +
+
+`; + +exports[`useTableData gets headers and rows for orgUnit layer 1`] = ` +
+
    +
  • + name=Index, dataKey=index, type=undefined and roundFn=undefined +
  • +
  • + name=Name, dataKey=name, type=string and roundFn=undefined +
  • +
  • + name=Id, dataKey=id, type=string and roundFn=undefined +
  • +
  • + name=Level, dataKey=level, type=number and roundFn=undefined +
  • +
  • + name=Parent, dataKey=parentName, type=string and roundFn=undefined +
  • +
  • + name=Type, dataKey=type, type=string and roundFn=undefined +
  • +
+
    +
      +
    • + value=0 and dataKey=index +
    • +
    • + value=OrgUnitName 1 and dataKey=name +
    • +
    • + value=orgunit-id-1 and dataKey=id +
    • +
    • + value=3 and dataKey=level +
    • +
    • + value=Bo and dataKey=parentName +
    • +
    • + value=MultiPolygon and dataKey=type +
    • +
    +
+
+`; + +exports[`useTableData gets headers and rows for thematic layer 1`] = ` +
+
    +
  • + name=Index, dataKey=index, type=undefined and roundFn=undefined +
  • +
  • + name=Name, dataKey=name, type=string and roundFn=undefined +
  • +
  • + name=Id, dataKey=id, type=string and roundFn=undefined +
  • +
  • + name=Value, dataKey=value, type=number and roundFn=undefined +
  • +
  • + name=Legend, dataKey=legend, type=string and roundFn=undefined +
  • +
  • + name=Range, dataKey=range, type=string and roundFn=undefined +
  • +
  • + name=Level, dataKey=level, type=number and roundFn=undefined +
  • +
  • + name=Parent, dataKey=parentName, type=string and roundFn=undefined +
  • +
  • + name=Type, dataKey=type, type=string and roundFn=undefined +
  • +
  • + name=Color, dataKey=color, type=string and roundFn=undefined +
  • +
+
    +
      +
    • + value=0 and dataKey=index +
    • +
    • + value=Ngelehun CHC and dataKey=name +
    • +
    • + value=thematicId-1 and dataKey=id +
    • +
    • + value=106.3 and dataKey=value +
    • +
    • + value=Great and dataKey=legend +
    • +
    • + value=90 - 120 and dataKey=range +
    • +
    • + value=4 and dataKey=level +
    • +
    • + value=Badjia and dataKey=parentName +
    • +
    • + value=Point and dataKey=type +
    • +
    • + value=#FFFFB2 and dataKey=color +
    • +
    +
+
+`; diff --git a/src/components/datatable/__tests__/useTableData.spec.js b/src/components/datatable/__tests__/useTableData.spec.js new file mode 100644 index 000000000..861535a2a --- /dev/null +++ b/src/components/datatable/__tests__/useTableData.spec.js @@ -0,0 +1,363 @@ +import { render } from '@testing-library/react' +import PropTypes from 'prop-types' +import React from 'react' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import { useTableData } from '../useTableData.js' + +jest.mock('../../map/MapApi.js', () => ({ + loadEarthEngineWorker: jest.fn(), +})) + +const Table = ({ layer, sortField, sortDirection }) => { + const { headers, rows } = useTableData({ layer, sortField, sortDirection }) + + return ( + <> +
    + {headers.map((header) => ( +
  • {`name=${header.name}, dataKey=${header.dataKey}, type=${header.type} and roundFn=${header.roundFn}`}
  • + ))} +
+
    + {rows.map((row, index) => ( +
      + {row.map((r) => ( +
    • {`value=${r.value} and dataKey=${r.dataKey}`}
    • + ))} +
    + ))} +
+ + ) +} + +Table.propTypes = { + layer: PropTypes.object, + sortDirection: PropTypes.string, + sortField: PropTypes.string, +} + +const mockStore = configureMockStore() + +describe('useTableData', () => { + test('gets headers and rows for facility layer', () => { + const store = { + aggregations: {}, + } + const layer = { + layer: 'facility', + dataFilters: null, + data: [ + { + properties: { + id: 'facility-1', + name: 'Facility 1', + type: 'Point', + }, + }, + ], + } + const { container } = render( + + + + ) + + expect(container).toMatchSnapshot() + }) + + test('gets headers and rows for orgUnit layer', () => { + const store = { + aggregations: {}, + } + const layer = { + layer: 'orgUnit', + dataFilters: null, + data: [ + { + properties: { + id: 'orgunit-id-1', + name: 'OrgUnitName 1', + type: 'MultiPolygon', + level: 3, + parentName: 'Bo', + }, + }, + ], + } + const { container } = render( + +
+ + ) + + expect(container).toMatchSnapshot() + }) + + test('gets headers and rows for thematic layer', () => { + const store = { + aggregations: {}, + } + const layer = { + layer: 'thematic', + dataFilters: null, + data: [ + { + properties: { + type: 'Point', + id: 'thematicId-1', + name: 'Ngelehun CHC', + level: 4, + parentName: 'Badjia', + color: '#FFFFB2', + legend: 'Great', + range: '90 - 120', + value: 106.3, + }, + }, + ], + } + const { container } = render( + +
+ + ) + + expect(container).toMatchSnapshot() + }) + + test('gets headers and rows for event layer', () => { + const store = { + aggregations: {}, + } + const layer = { + layer: 'event', + dataFilters: null, + headers: [ + { + name: 'ps', + column: 'Program stage', + valueType: 'TEXT', + }, + { + name: 'eventdate', + column: 'Report date', + valueType: 'DATE', + }, + { + name: 'lastupdated', + column: 'Last updated on', + valueType: 'DATE', + }, + { + name: 'eventstatus', + column: 'Event status', + valueType: 'TEXT', + }, + { + name: 'oZg33kd9taw', + column: 'Gender', + valueType: 'TEXT', + }, + ], + + data: [ + { + properties: { + id: 'a9712323629', + type: 'Point', + ps: 'pTo4uMt3xur', + eventdate: '2023-05-15 00:00:00.0', + lastupdated: '2018-04-12 20:58:51.31', + ouname: 'Lumley Hospital', + eventstatus: 'ACTIVE', + oZg33kd9taw: 'Female', + value: 'Female', + color: '#ff7f00', + }, + }, + ], + } + const { container } = render( + +
+ + ) + + expect(container).toMatchSnapshot() + }) + + test('gets headers and rows for EE population layer', () => { + const store = { + aggregations: { + eelayerid: { + boOU: { + mean: 47.34593724212383, + sum: 851090.567864418, + }, + bombaliOU: { + mean: 27.347820392739166, + sum: 585872.3562736511, + }, + }, + }, + } + + const layer = { + layer: 'earthEngine', + aggregationType: ['sum', 'mean'], + legend: { + title: 'Population', + }, + id: 'eelayerid', + dataFilters: null, + data: [ + { + id: 'boOU', + properties: { + type: 'Polygon', + id: 'boOu', + name: 'Bo', + }, + }, + { + id: 'bombaliOU', + properties: { + type: 'Polygon', + id: 'bombaliOU', + name: 'Bombali', + }, + }, + ], + } + const { container } = render( + +
+ + ) + + expect(container).toMatchSnapshot() + }) + + test('gets headers and rows for EE population age groups layer', () => { + const store = { + aggregations: { + eelayerid: { + badijaOU: { + M_0_mean: 0.4416957503717281, + M_0_sum: 279.5934099853039, + M_1_mean: 1.667343524395007, + M_1_sum: 1055.4284509420395, + M_5_mean: 1.8668244672235907, + M_5_sum: 1181.699887752533, + mean: 3.975863741990326, + sum: 2516.7217486798763, + }, + baomaOU: { + M_0_mean: 0.6669754306043404, + M_0_sum: 1012.4687036573887, + M_1_mean: 2.517744771694477, + M_1_sum: 3821.9365634322166, + M_5_mean: 2.818359887764859, + M_5_sum: 4278.270309627056, + mean: 6.003080090063677, + sum: 9112.675576716661, + }, + }, + }, + } + + const layer = { + layer: 'earthEngine', + name: 'Population age groups', + aggregationType: ['sum', 'mean'], + id: 'eelayerid', + legend: { + title: 'Population age groups', + groups: [ + { + id: 'M_0', + name: 'Male 0 - 1 years', + }, + { + id: 'M_1', + name: 'Male 1 - 4 years', + }, + { + id: 'M_5', + name: 'Male 5 - 9 years', + }, + ], + items: [ + { + color: '#fee5d9', + from: 0, + to: 10, + name: '0 - 10', + }, + { + color: '#fcbba1', + from: 10, + to: 20, + name: '10 - 20', + }, + { + color: '#fc9272', + from: 20, + to: 30, + name: '20 - 30', + }, + { + color: '#fb6a4a', + from: 30, + to: 40, + name: '30 - 40', + }, + { + color: '#de2d26', + from: 40, + to: 50, + name: '40 - 50', + }, + { + color: '#a50f15', + from: 50, + name: '> 50', + }, + ], + }, + data: [ + { + id: 'badijaOU', + properties: { + type: 'Polygon', + id: 'boOU', + name: 'Badija', + }, + }, + { + type: 'Feature', + id: 'baomaOU', + properties: { + type: 'Polygon', + id: 'baomaOU', + name: 'Baoma', + }, + }, + ], + } + + const { container } = render( + +
+ + ) + + expect(container).toMatchSnapshot() + }) +}) diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 2d67b27e1..df5e5f550 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -91,7 +91,7 @@ const getEventHeaders = ({ layerHeaders = [], styleDataItem }) => { : TYPE_STRING, })) - customFields.push([defaultFieldsMap()[TYPE]]) + customFields.push(defaultFieldsMap()[TYPE]) if (styleDataItem) { customFields.push(defaultFieldsMap()[COLOR]) @@ -164,23 +164,25 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { headers: layerHeaders, } = layer || EMPTY_LAYER - const indexedData = useMemo(() => { - return data - .map((d, i) => ({ - index: i, - ...d, - })) - .filter((d) => !d.properties.hasAdditionalGeometry) - .map((d, i) => ({ - ...(d.properties || d), - ...aggregations[d.id], - index: d.index, - i, - })) - }, [data, aggregations]) + const dataWithAggregations = useMemo( + () => + data + .map((d, i) => ({ + index: i, + ...d, + })) + .filter((d) => !d.properties.hasAdditionalGeometry) + .map((d, i) => ({ + ...(d.properties || d), + ...aggregations[d.id], + index: d.index, + i, + })), + [data, aggregations] + ) const headers = useMemo(() => { - if (!layerType) { + if (!layerType || !dataWithAggregations.length) { return [] } @@ -195,7 +197,7 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { return getEarthEngineHeaders({ aggregationType, legend, - data: indexedData, + data: dataWithAggregations, }) case FACILITY_LAYER: return getFacilityHeaders() @@ -208,16 +210,16 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { aggregationType, legend, styleDataItem, - indexedData, + dataWithAggregations, layerHeaders, ]) const rows = useMemo(() => { - if (!indexedData.length || !headers?.length) { + if (!dataWithAggregations.length || !headers?.length) { return [] } - const filteredData = filterData(indexedData, dataFilters) + const filteredData = filterData(dataWithAggregations, dataFilters) //sort filteredData.sort((a, b) => { @@ -243,7 +245,7 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { dataKey, })) ) - }, [indexedData, dataFilters, sortField, sortDirection, headers]) + }, [dataWithAggregations, dataFilters, sortField, sortDirection, headers]) return { headers, rows } } From f9dbac82dcd87ff9915ec60f341670e1a9dfa72f Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 29 Jan 2024 11:16:04 +0100 Subject: [PATCH 17/57] chore: update cypress test --- cypress/integration/dataTable.cy.js | 97 ++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/cypress/integration/dataTable.cy.js b/cypress/integration/dataTable.cy.js index ecfff1d48..136798eaf 100644 --- a/cypress/integration/dataTable.cy.js +++ b/cypress/integration/dataTable.cy.js @@ -1,5 +1,15 @@ import { EXTENDED_TIMEOUT } from '../support/util.js' +Cypress.on('uncaught:exception', (err) => { + if ( + err.message.includes( + 'ResizeObserver loop completed with undelivered notifications.' + ) + ) { + return false + } +}) + const map = { id: 'eDlFx0jTtV9', name: 'ANC: LLITN Cov Chiefdom this year', @@ -8,7 +18,7 @@ const map = { } describe('data table', () => { - it('opens data table', () => { + it('opens data table and filters and sorts', () => { cy.visit(`/#/${map.id}`, EXTENDED_TIMEOUT) cy.get('canvas', EXTENDED_TIMEOUT).should('be.visible') @@ -29,19 +39,90 @@ describe('data table', () => { // check number of columns cy.getByDataTest('bottom-panel') - .find('[role="columnheader"]') + .findByDataTest('dhis2-uicore-datatablecellhead') .should('have.length', 10) - // try the filtering + // Filter by name cy.getByDataTest('bottom-panel') - .find('[role="columnheader"]') + .findByDataTest('dhis2-uicore-datatablecellhead') .containsExact('Name') .siblings('input') - .type('Kakua') + .type('bar') + + // check that the filter returned the correct number of rows + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .should('have.length', 7) + + // confirm that the sort order is initially ascending by Name + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .first() + .find('td') + .eq(1) + .should('contain', 'Bargbe') + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .last() + .find('td') + .eq(1) + .should('contain', 'Upper Bambara') + + // Sort by name + cy.get('button[title="Sort by Name"]').click() + + // confirm that the rows are sorted by Name descending + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .first() + .find('td') + .eq(1) + .should('contain', 'Upper Bambara') + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .last() + .find('td') + .eq(1) + .should('contain', 'Bargbe') + + // filter by Value (numeric) + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-datatablecellhead') + .containsExact('Value') + .siblings('input') + .type('>26') + + // check that the (combined) filter returned the correct number of rows + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .should('have.length', 5) + + // Sort by value + cy.get('button[title="Sort by Value"]').click() + + // check that the rows are sorted by Value ascending + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .first() + .find('td') + .eq(3) + .should('contain', '35') - // check that the filter worked cy.getByDataTest('bottom-panel') - .find('.ReactVirtualized__Table__row') - .should('have.length', 1) + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .last() + .find('td') + .eq(3) + .should('contain', '76') }) }) From 8b6809cb8ca11fca8755ede6d1aaf9dd1fd0f0be Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Mon, 29 Jan 2024 12:10:29 +0100 Subject: [PATCH 18/57] fix: align filter inputs at the bottom of the th --- src/components/datatable/UiDataTable.js | 2 +- .../datatable/styles/UiDataTable.module.css | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index 67b60e3b2..f8559f47b 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -48,7 +48,7 @@ DataTableRowWithVirtuosoContext.propTypes = { } const TableComponents = { - Table: DataTable, + Table: (props) => , TableBody: DataTableBody, TableHead: DataTableHead, TableRow: DataTableRowWithVirtuosoContext, diff --git a/src/components/datatable/styles/UiDataTable.module.css b/src/components/datatable/styles/UiDataTable.module.css index 5aa509a4d..3e5e7b1e1 100644 --- a/src/components/datatable/styles/UiDataTable.module.css +++ b/src/components/datatable/styles/UiDataTable.module.css @@ -1,3 +1,7 @@ +.dataTable { + height: 1px; +} + td.sizeClass { padding: 7px 5px; } @@ -10,12 +14,17 @@ td.darkText { color: var(--colors-white); } +.columnHeader > :global(span.container) { + justify-content: space-between; +} /* Hide the filter icon */ -.columnHeader > span > span > button:last-of-type { +.columnHeader + > :global(span.container) + > :global(span.top) + > button:last-of-type { visibility: hidden; } - /* TODO */ .noSupport { position: absolute; @@ -26,4 +35,3 @@ td.darkText { font-style: italic; line-height: 30px; } - From 9ed22e96401a6971d8d83a8474e9b4e2c393fc92 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 29 Jan 2024 13:30:46 +0100 Subject: [PATCH 19/57] chore: remove d2 from useTableData --- src/components/datatable/useTableData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index df5e5f550..023279b7b 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -1,5 +1,4 @@ import i18n from '@dhis2/d2-i18n' -import { isValidUid } from 'd2/uid' // TODO replace import { useMemo } from 'react' import { useSelector } from 'react-redux' import { @@ -12,6 +11,7 @@ import { import { numberValueTypes } from '../../constants/valueTypes.js' import { hasClasses, getPrecision } from '../../util/earthEngine.js' import { filterData } from '../../util/filter.js' +import { isValidUid } from '../../util/helpers.js' import { numberPrecision } from '../../util/numbers.js' const ASCENDING = 'asc' From 5a4c4a79b953aa39460ecf9be82ed7743606f457 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 29 Jan 2024 14:01:17 +0100 Subject: [PATCH 20/57] chore: padding styling on table cells --- src/components/datatable/UiDataTable.js | 5 +++-- .../datatable/styles/UiDataTable.module.css | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index f8559f47b..d1aea82aa 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -188,11 +188,12 @@ const Table = ({ height }) => { row.map(({ dataKey, value }) => ( {dataKey === 'color' ? value?.toLowerCase() : value} diff --git a/src/components/datatable/styles/UiDataTable.module.css b/src/components/datatable/styles/UiDataTable.module.css index 3e5e7b1e1..da05327ae 100644 --- a/src/components/datatable/styles/UiDataTable.module.css +++ b/src/components/datatable/styles/UiDataTable.module.css @@ -2,15 +2,13 @@ height: 1px; } -td.sizeClass { - padding: 7px 5px; +td.dataCell { + padding-top: var(--spacers-dp8); + padding-bottom: var(--spacers-dp8); + font-size: 12px; } -td.fontClass { - font-size: 11px; -} - -td.darkText { +td.lightText { color: var(--colors-white); } From a94523971dc3dce7aa2f0afa2ee03d8fd402003c Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 29 Jan 2024 15:46:40 +0100 Subject: [PATCH 21/57] fix: loading indicator for earthengine aggregations slow loading --- src/components/datatable/UiDataTable.js | 118 +++++++++++++---------- src/components/datatable/useTableData.js | 21 ++-- 2 files changed, 82 insertions(+), 57 deletions(-) diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js index d1aea82aa..9b274ff73 100644 --- a/src/components/datatable/UiDataTable.js +++ b/src/components/datatable/UiDataTable.js @@ -6,6 +6,9 @@ import { DataTableColumnHeader, DataTableHead, DataTableBody, + ComponentCover, + CenteredContent, + CircularLoader, } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' @@ -81,7 +84,11 @@ const Table = ({ height }) => { [sortDirection] ) - const { headers, rows } = useTableData({ layer, sortField, sortDirection }) + const { headers, rows, isLoading } = useTableData({ + layer, + sortField, + sortDirection, + }) const onTableRowClick = useCallback( (row) => { @@ -147,59 +154,68 @@ const Table = ({ height }) => { } return ( - ( - - {headers.map(({ name, dataKey, type }, index) => ( - + ( + + {headers.map(({ name, dataKey, type }, index) => ( + + ) + } + > + {name} + + ))} + + )} + itemContent={(_, row) => + row.map(({ dataKey, value }) => ( + - ) - } + backgroundColor={dataKey === 'color' ? value : null} + align="left" > - {name} - - ))} - + {dataKey === 'color' ? value?.toLowerCase() : value} + + )) + } + /> + {isLoading && ( + + + + + )} - itemContent={(_, row) => - row.map(({ dataKey, value }) => ( - - {dataKey === 'color' ? value?.toLowerCase() : value} - - )) - } - /> + ) } diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 023279b7b..67f3fe2ea 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -34,7 +34,7 @@ const OUNAME = 'ouname' const EVENTDATE = 'eventdate' const defaultFieldsMap = () => ({ - [INDEX]: { name: i18n.t('Index'), dataKey: INDEX }, + [INDEX]: { name: i18n.t('Index'), dataKey: INDEX, type: TYPE_NUMBER }, [NAME]: { name: i18n.t('Name'), dataKey: NAME, type: TYPE_STRING }, [ID]: { name: i18n.t('Id'), dataKey: ID, type: TYPE_STRING }, [LEVEL]: { name: i18n.t('Level'), dataKey: LEVEL, type: TYPE_NUMBER }, @@ -240,12 +240,21 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { }) return filteredData.map((item) => - headers.map(({ dataKey, roundFn }) => ({ - value: roundFn ? roundFn(item[dataKey]) : item[dataKey], - dataKey, - })) + headers.map(({ dataKey, roundFn, type }) => { + const value = roundFn ? roundFn(item[dataKey]) : item[dataKey] + + return { + value: type === TYPE_NUMBER && isNaN(value) ? null : value, + dataKey, + } + }) ) }, [dataWithAggregations, dataFilters, sortField, sortDirection, headers]) - return { headers, rows } + const isLoading = + layerType === EARTH_ENGINE_LAYER && + aggregationType?.length && + (!aggregations || aggregations === EMPTY_AGGREGATIONS) + + return { headers, rows, isLoading } } From 4019f3cee45f7b8abaa887ec27b3389271234d89 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 29 Jan 2024 16:34:43 +0100 Subject: [PATCH 22/57] fix: fetch extended data for event layers --- src/components/datatable/useTableData.js | 8 ++-- src/components/loaders/EventLoader.js | 6 ++- src/components/loaders/LayersLoader.js | 51 +++++++++++++++--------- src/loaders/eventLoader.js | 9 ++--- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 67f3fe2ea..9d037497c 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -162,6 +162,7 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { data, dataFilters, headers: layerHeaders, + serverCluster, } = layer || EMPTY_LAYER const dataWithAggregations = useMemo( @@ -252,9 +253,10 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { }, [dataWithAggregations, dataFilters, sortField, sortDirection, headers]) const isLoading = - layerType === EARTH_ENGINE_LAYER && - aggregationType?.length && - (!aggregations || aggregations === EMPTY_AGGREGATIONS) + (layerType === EARTH_ENGINE_LAYER && + aggregationType?.length && + (!aggregations || aggregations === EMPTY_AGGREGATIONS)) || + (layerType === EVENT_LAYER && !layer.isExtended && !serverCluster) return { headers, rows, isLoading } } diff --git a/src/components/loaders/EventLoader.js b/src/components/loaders/EventLoader.js index 2230658da..df45f6be2 100644 --- a/src/components/loaders/EventLoader.js +++ b/src/components/loaders/EventLoader.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types' import { useEffect } from 'react' +import { useSelector } from 'react-redux' import eventLoader from '../../loaders/eventLoader.js' const EventLoader = ({ config, onLoad }) => { + const dataTableOpen = useSelector((state) => !!state.dataTable) useEffect(() => { - eventLoader(config).then(onLoad) - }, [config, onLoad]) + eventLoader(config, dataTableOpen).then(onLoad) + }, [config, onLoad, dataTableOpen]) return null } diff --git a/src/components/loaders/LayersLoader.js b/src/components/loaders/LayersLoader.js index b703672a1..36396215c 100644 --- a/src/components/loaders/LayersLoader.js +++ b/src/components/loaders/LayersLoader.js @@ -1,29 +1,44 @@ import React, { useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { setLayerLoading, updateLayer } from '../../actions/layers.js' +import { EVENT_LAYER } from '../../constants/layers.js' import LayerLoader from './LayerLoader.js' const LayersLoader = () => { - const layers = useSelector((state) => - state.map.mapViews.filter((layer) => { - // The layer is currently being loaded - don't load again - if (layer.isLoading) { - return false - } else { - // The layer is not loaded - load it - if (!layer.isLoaded) { - return true + const layers = useSelector(({ map, dataTable }) => + map.mapViews.filter( + ({ + id, + isLoading, + isLoaded, + isExtended, + layer: layerType, + serverCluster, + }) => { + // The layer is currently being loaded - don't load again + if (isLoading) { + return false + } else { + // The layer is not loaded - load it + if (!isLoaded) { + return true + } + + // The layer is loaded but the data table is now displayed and + // event extended data hasn't been loaded yet - so load it + if ( + layerType === EVENT_LAYER && + id === dataTable && + !isExtended && + !serverCluster + ) { + return true + } + + return false } - - // The layer is loaded but the data table is now displayed and - // event extended data hasn't been loaded yet - so load it - if (layer.showDataTable && !layer.isExtended) { - return true - } - - return false } - }) + ) ) const dispatch = useDispatch() diff --git a/src/loaders/eventLoader.js b/src/loaders/eventLoader.js index c53eeb80c..8bb1bc115 100644 --- a/src/loaders/eventLoader.js +++ b/src/loaders/eventLoader.js @@ -37,10 +37,10 @@ const unknownErrorAlert = { // TODO: Refactor to share code with other loaders // Returns a promise -const eventLoader = async (layerConfig) => { +const eventLoader = async (layerConfig, loadExtended) => { const config = { ...layerConfig } try { - await loadEventLayer(config) + await loadEventLayer(config, loadExtended) } catch (e) { if (e.httpStatusCode === 403 || e.httpStatusCode === 409) { config.alerts = [ @@ -61,7 +61,7 @@ const eventLoader = async (layerConfig) => { return config } -const loadEventLayer = async (config) => { +const loadEventLayer = async (config, loadExtended) => { const { columns, endDate, @@ -74,14 +74,13 @@ const loadEventLayer = async (config) => { startDate, styleDataItem, areaRadius, - showDataTable, } = config const period = getPeriodFromFilters(filters) const dataFilters = getFiltersFromColumns(columns) const d2 = await getD2() - config.isExtended = showDataTable + config.isExtended = loadExtended const analyticsRequest = await getAnalyticsRequest(config, { d2, From 3118943fe58449019cd7493898eb61a26486496e Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 29 Jan 2024 16:42:38 +0100 Subject: [PATCH 23/57] chore: add cypress test for event layer data table --- cypress/elements/event_layer.js | 8 ++++++ cypress/integration/dataTable.cy.js | 44 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cypress/elements/event_layer.js b/cypress/elements/event_layer.js index f3a0fc47f..6f7db422c 100644 --- a/cypress/elements/event_layer.js +++ b/cypress/elements/event_layer.js @@ -3,6 +3,7 @@ import { Layer } from './layer.js' export class EventLayer extends Layer { selectProgram(program) { cy.get('[data-test="programselect"]').click() + cy.contains(program).scrollIntoView() cy.contains(program).click() return this @@ -22,4 +23,11 @@ export class EventLayer extends Layer { return this } + + selectPeriodType(periodType) { + cy.getByDataTest('relative-period-select-content').click() + cy.contains(periodType).click() + + return this + } } diff --git a/cypress/integration/dataTable.cy.js b/cypress/integration/dataTable.cy.js index 136798eaf..6ab3b09fc 100644 --- a/cypress/integration/dataTable.cy.js +++ b/cypress/integration/dataTable.cy.js @@ -1,4 +1,5 @@ -import { EXTENDED_TIMEOUT } from '../support/util.js' +import { EventLayer } from '../elements/event_layer.js' +import { CURRENT_YEAR, EXTENDED_TIMEOUT } from '../support/util.js' Cypress.on('uncaught:exception', (err) => { if ( @@ -125,4 +126,45 @@ describe('data table', () => { .eq(3) .should('contain', '76') }) + + it('opens the data table for an Event layer', () => { + cy.visit('/', EXTENDED_TIMEOUT) + + const Layer = new EventLayer() + + Layer.openDialog('Events') + .selectProgram('Malaria case registration') + .validateStage('Malaria case registration') + .selectTab('Period') + .selectPeriodType('Start/end dates') + .typeStartDate(`${CURRENT_YEAR - 1}-01-01`) + .typeEndDate(`${CURRENT_YEAR - 1}-01-15`) + .selectTab('Org Units') + .selectOu('Bo') + .addToMap() + + Layer.validateDialogClosed(true) + + Layer.validateCardTitle('Malaria case registration') + + cy.getByDataTest('moremenubutton').first().click() + + cy.getByDataTest('more-menu') + .find('li') + .contains('Show data table') + .not('disabled') + .click() + + cy.getByDataTest('bottom-panel').should('be.visible') + + // check number of columns + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-datatablecellhead') + .should('have.length', 9) + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-datatablecellhead') + .contains('gender', { matchCase: false }) + .should('be.visible') + }) }) From 1e40397c8f094c279892546dc6590c286a0790ac Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 29 Jan 2024 16:45:35 +0100 Subject: [PATCH 24/57] chore: rename file to DataTable --- src/components/datatable/BottomPanel.js | 2 +- src/components/datatable/DataTable.js | 560 +++++++----------- src/components/datatable/UiDataTable.js | 226 ------- .../datatable/styles/DataTable.module.css | 85 +-- .../datatable/styles/UiDataTable.module.css | 35 -- 5 files changed, 225 insertions(+), 683 deletions(-) delete mode 100644 src/components/datatable/UiDataTable.js delete mode 100644 src/components/datatable/styles/UiDataTable.module.css diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 4512614a8..1b7796124 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -8,7 +8,7 @@ import { LAYERS_PANEL_WIDTH, RIGHT_PANEL_WIDTH, } from '../../constants/layout.js' -import DataTable from '../datatable/UiDataTable.js' +import DataTable from '../datatable/DataTable.js' import { useWindowDimensions } from '../WindowDimensionsProvider.js' import ResizeHandle from './ResizeHandle.js' import styles from './styles/BottomPanel.module.css' diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 27c8f7a99..9a500dd2c 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -1,359 +1,150 @@ import i18n from '@dhis2/d2-i18n' -import { CenteredContent, CircularLoader } from '@dhis2/ui' -import { isValidUid } from 'd2/uid' -import { debounce } from 'lodash/fp' +import { + DataTable, + DataTableRow, + DataTableCell, + DataTableColumnHeader, + DataTableHead, + DataTableBody, + ComponentCover, + CenteredContent, + CircularLoader, +} from '@dhis2/ui' +import cx from 'classnames' import PropTypes from 'prop-types' -import React, { Component } from 'react' -import { connect } from 'react-redux' -import { Table, Column } from 'react-virtualized' -import { closeDataTable } from '../../actions/dataTable.js' +import React, { useReducer, useCallback, useMemo } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { TableVirtuoso } from 'react-virtuoso' import { highlightFeature } from '../../actions/feature.js' -import { updateLayer } from '../../actions/layers.js' import { setOrgUnitProfile } from '../../actions/orgUnits.js' -import { - EVENT_LAYER, - THEMATIC_LAYER, - ORG_UNIT_LAYER, - EARTH_ENGINE_LAYER, -} from '../../constants/layers.js' -import { numberValueTypes } from '../../constants/valueTypes.js' -import { filterData } from '../../util/filter.js' -import { formatTime } from '../../util/helpers.js' -import ColorCell from './ColorCell.js' -import ColumnHeader from './ColumnHeader.js' -import EarthEngineColumns from './EarthEngineColumns.js' +import { isDarkColor } from '../../util/colors.js' +import FilterInput from './FilterInput.js' import styles from './styles/DataTable.module.css' -import 'react-virtualized/styles.css' - -// Using react component to keep sorting state, which is only used within the data table. -class DataTable extends Component { - static propTypes = { - closeDataTable: PropTypes.func.isRequired, - height: PropTypes.number.isRequired, - highlightFeature: PropTypes.func.isRequired, - layer: PropTypes.object.isRequired, - setOrgUnitProfile: PropTypes.func.isRequired, - updateLayer: PropTypes.func.isRequired, - width: PropTypes.number.isRequired, - aggregations: PropTypes.object, - feature: PropTypes.object, - } - - static defaultProps = { - data: [], - } - - state = {} - - constructor(props, context) { - super(props, context) - - // Default sort - const sortBy = 'index' - const sortDirection = 'ASC' - - const data = this.sort(this.filter(), sortBy, sortDirection) - - this.state = { - sortBy, - sortDirection, - data, - } - } - - componentDidMount() { - this.loadExtendedData() - } +import { useTableData } from './useTableData.js' + +const ASCENDING = 'asc' +const DESCENDING = 'desc' + +const DataTableRowWithVirtuosoContext = ({ context, item, ...props }) => { + return ( + context.onClick(item)} + onMouseEnter={() => context.onMouseEnter(item)} + onMouseLeave={context.onMouseLeave} + {...props} + /> + ) +} - componentDidUpdate(prevProps) { - const { layer, aggregations, closeDataTable } = this.props - const { data, dataFilters } = layer - const prev = prevProps.layer +DataTableRowWithVirtuosoContext.propTypes = { + context: PropTypes.shape({ + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + }), + item: PropTypes.arrayOf( + PropTypes.shape({ + dataKey: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + }) + ), +} - if (!data) { - closeDataTable() - } else if ( - data !== prev.data || - dataFilters !== prev.dataFilters || - aggregations !== prevProps.aggregations - ) { - const { sortBy, sortDirection } = this.state +const TableComponents = { + Table: (props) => , + TableBody: DataTableBody, + TableHead: DataTableHead, + TableRow: DataTableRowWithVirtuosoContext, +} - this.setState({ - data: this.sort(this.filter(), sortBy, sortDirection), - }) +const Table = ({ height }) => { + const { mapViews } = useSelector((state) => state.map) + const activeLayerId = useSelector((state) => state.dataTable) + + const dispatch = useDispatch() + const feature = useSelector((state) => state.feature) + const [{ sortField, sortDirection }, setSorting] = useReducer( + (sorting, newSorting) => ({ ...sorting, ...newSorting }), + { + sortField: 'name', + sortDirection: ASCENDING, } - } + ) - loadExtendedData() { - const { layer, updateLayer } = this.props - const { layer: layerType, isExtended, serverCluster } = layer + const layer = mapViews.find((l) => l.id === activeLayerId) - if (layerType === EVENT_LAYER && !isExtended && !serverCluster) { - updateLayer({ - ...layer, - showDataTable: true, + const sortData = useCallback( + ({ name }) => { + setSorting({ + sortField: name, + sortDirection: + sortDirection === ASCENDING ? DESCENDING : ASCENDING, }) - } - } - - filter() { - const { layer, aggregations = {} } = this.props - const { dataFilters } = layer - const data = layer.data.filter( - (d) => !d.properties.hasAdditionalGeometry - ) - - return filterData( - data.map((d, index) => ({ - ...(d.properties || d), - ...aggregations[d.id], - index, - })), - dataFilters - ) - } - - // TODO: Make sure sorting works across different locales - use lib method - sort(data, sortBy, sortDirection) { - return data.sort((a, b) => { - a = a[sortBy] - b = b[sortBy] + }, + [sortDirection] + ) + + const { headers, rows, isLoading } = useTableData({ + layer, + sortField, + sortDirection, + }) - if (typeof a === 'number') { - return sortDirection === 'ASC' ? a - b : b - a + const onTableRowClick = useCallback( + (row) => { + const id = row.find((r) => r.dataKey === 'id')?.value + id && dispatch(setOrgUnitProfile(id)) + }, + [dispatch] + ) + + const highlightFeatureOnMouseEnter = useCallback( + (row) => { + const id = row.find((r) => r.dataKey === 'id')?.value + if (!id || !feature || id !== feature.id) { + dispatch( + highlightFeature( + id + ? { + id, + layerId: layer.id, + origin: 'table', + } + : null + ) + ) } - - if (a !== undefined) { - return sortDirection === 'ASC' - ? a.localeCompare(b) - : b.localeCompare(a) + }, + [feature, dispatch, layer.id] + ) + const clearFeatureHighlightOnMouseLeave = useCallback( + (event) => { + const nextElement = event.toElement ?? event.relatedTarget + // When hovering to the next row the next element is a `TD` + // If this is the case `highlightFeatureOnMouseEnter` will + // fire and the highlight does not need to be cleared + if (nextElement.tagName !== 'TD') { + dispatch(highlightFeature(null)) } - - return 0 - }) - } - - onSort(sortBy, sortDirection) { - const { data } = this.state - - this.setState({ - sortBy, - sortDirection, - data: this.sort(data, sortBy, sortDirection), - }) - } - - // Return event data items used for styling, filters or "display in reports" - getEventDataItems() { - const { headers = [] } = this.props.layer - - return headers - .filter(({ name }) => isValidUid(name)) - .map(({ name, column, valueType }) => ({ - key: name, - label: column, - type: numberValueTypes.includes(valueType) - ? 'number' - : 'string', - })) - } - - // Debounce needed as event is triggered multiple times for the same row - highlightFeature = debounce(50, (id) => { - const { feature, layer } = this.props - - // If not the same feature as already highlighted - if (!id || !feature || id !== feature.id) { - this.props.highlightFeature( - id - ? { - id, - layerId: layer.id, - origin: 'table', - } - : null - ) - } - }) - - onRowClick = (evt) => this.props.setOrgUnitProfile(evt.rowData.id) - onRowMouseOver = (evt) => this.highlightFeature(evt.rowData.id) - onRowMouseOut = () => this.highlightFeature() - - render() { - const { data, sortBy, sortDirection } = this.state - const { width, height, layer, aggregations } = this.props - - const { - layer: layerType, - styleDataItem, - serverCluster, - aggregationType, - legend, - } = layer - - const isThematic = layerType === THEMATIC_LAYER - const isOrgUnit = layerType === ORG_UNIT_LAYER - const isEvent = layerType === EVENT_LAYER - const isEarthEngine = layerType === EARTH_ENGINE_LAYER - const isLoading = - isEarthEngine && aggregationType?.length && !aggregations - - return !serverCluster ? ( - <> -
data[index]} - sort={({ sortBy, sortDirection }) => - this.onSort(sortBy, sortDirection) - } - sortBy={sortBy} - sortDirection={sortDirection} - useDynamicRowHeight={false} - hideIndexRow={false} - onRowClick={!isEvent ? this.onRowClick : undefined} - onRowMouseOver={this.onRowMouseOver} - onRowMouseOut={this.onRowMouseOut} - > - rowData.index} - dataKey="index" - label={i18n.t('Index')} - width={72} - className="right" - /> - ( - - )} - /> - ( - - )} - /> - {isEvent && ( - ( - - )} - cellRenderer={({ cellData }) => - cellData ? formatTime(cellData) : '' - } - /> - )} - {isEvent && - this.getEventDataItems().map(({ key, label, type }) => ( - ( - - )} - /> - ))} - {isThematic && ( - ( - - )} - /> - )} - {isThematic && ( - ( - - )} - /> - )} - {isThematic && ( - ( - - )} - /> - )} - {(isThematic || isOrgUnit) && ( - ( - - )} - /> - )} - {(isThematic || isOrgUnit) && ( - ( - - )} - /> - )} - ( - - )} - /> - {(isThematic || styleDataItem) && ( - ( - - )} - cellRenderer={ColorCell} - /> - )} - - {isEarthEngine && - EarthEngineColumns({ aggregationType, legend, data })} -
- {isLoading === true && ( -
- - - -
- )} - - ) : ( + }, + [dispatch] + ) + + const tableContext = useMemo( + () => ({ + onClick: onTableRowClick, + onMouseEnter: highlightFeatureOnMouseEnter, + onMouseLeave: clearFeatureHighlightOnMouseLeave, + }), + [ + onTableRowClick, + highlightFeatureOnMouseEnter, + clearFeatureHighlightOnMouseLeave, + ] + ) + + if (layer.serverCluster) { + return (
{i18n.t( 'Data table is not supported when events are grouped on the server.' @@ -361,24 +152,75 @@ class DataTable extends Component {
) } + + return ( + <> + ( + + {headers.map(({ name, dataKey, type }, index) => ( + + ) + } + > + {name} + + ))} + + )} + itemContent={(_, row) => + row.map(({ dataKey, value }) => ( + + {dataKey === 'color' ? value?.toLowerCase() : value} + + )) + } + /> + {isLoading && ( + + + + + + )} + + ) } -export default connect( - ({ dataTable, map, aggregations = {}, feature }) => { - const layer = map.mapViews.find((l) => l.id === dataTable) +Table.propTypes = { + height: PropTypes.number, +} - return layer - ? { - layer, - feature, - aggregations: aggregations[layer.id], - } - : {} - }, - { - closeDataTable, - updateLayer, - setOrgUnitProfile, - highlightFeature, - } -)(DataTable) +export default Table diff --git a/src/components/datatable/UiDataTable.js b/src/components/datatable/UiDataTable.js deleted file mode 100644 index 9b274ff73..000000000 --- a/src/components/datatable/UiDataTable.js +++ /dev/null @@ -1,226 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { - DataTable, - DataTableRow, - DataTableCell, - DataTableColumnHeader, - DataTableHead, - DataTableBody, - ComponentCover, - CenteredContent, - CircularLoader, -} from '@dhis2/ui' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { useReducer, useCallback, useMemo } from 'react' -import { useSelector, useDispatch } from 'react-redux' -import { TableVirtuoso } from 'react-virtuoso' -import { highlightFeature } from '../../actions/feature.js' -import { setOrgUnitProfile } from '../../actions/orgUnits.js' -import { isDarkColor } from '../../util/colors.js' -import FilterInput from './FilterInput.js' -import styles from './styles/UiDataTable.module.css' -import { useTableData } from './useTableData.js' - -const ASCENDING = 'asc' -const DESCENDING = 'desc' - -const DataTableRowWithVirtuosoContext = ({ context, item, ...props }) => { - return ( - context.onClick(item)} - onMouseEnter={() => context.onMouseEnter(item)} - onMouseLeave={context.onMouseLeave} - {...props} - /> - ) -} - -DataTableRowWithVirtuosoContext.propTypes = { - context: PropTypes.shape({ - onClick: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - }), - item: PropTypes.arrayOf( - PropTypes.shape({ - dataKey: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - }) - ), -} - -const TableComponents = { - Table: (props) => , - TableBody: DataTableBody, - TableHead: DataTableHead, - TableRow: DataTableRowWithVirtuosoContext, -} - -const Table = ({ height }) => { - const { mapViews } = useSelector((state) => state.map) - const activeLayerId = useSelector((state) => state.dataTable) - - const dispatch = useDispatch() - const feature = useSelector((state) => state.feature) - const [{ sortField, sortDirection }, setSorting] = useReducer( - (sorting, newSorting) => ({ ...sorting, ...newSorting }), - { - sortField: 'name', - sortDirection: ASCENDING, - } - ) - - const layer = mapViews.find((l) => l.id === activeLayerId) - - const sortData = useCallback( - ({ name }) => { - setSorting({ - sortField: name, - sortDirection: - sortDirection === ASCENDING ? DESCENDING : ASCENDING, - }) - }, - [sortDirection] - ) - - const { headers, rows, isLoading } = useTableData({ - layer, - sortField, - sortDirection, - }) - - const onTableRowClick = useCallback( - (row) => { - const id = row.find((r) => r.dataKey === 'id')?.value - id && dispatch(setOrgUnitProfile(id)) - }, - [dispatch] - ) - - const highlightFeatureOnMouseEnter = useCallback( - (row) => { - const id = row.find((r) => r.dataKey === 'id')?.value - if (!id || !feature || id !== feature.id) { - dispatch( - highlightFeature( - id - ? { - id, - layerId: layer.id, - origin: 'table', - } - : null - ) - ) - } - }, - [feature, dispatch, layer.id] - ) - const clearFeatureHighlightOnMouseLeave = useCallback( - (event) => { - const nextElement = event.toElement ?? event.relatedTarget - // When hovering to the next row the next element is a `TD` - // If this is the case `highlightFeatureOnMouseEnter` will - // fire and the highlight does not need to be cleared - if (nextElement.tagName !== 'TD') { - dispatch(highlightFeature(null)) - } - }, - [dispatch] - ) - - const tableContext = useMemo( - () => ({ - onClick: onTableRowClick, - onMouseEnter: highlightFeatureOnMouseEnter, - onMouseLeave: clearFeatureHighlightOnMouseLeave, - }), - [ - onTableRowClick, - highlightFeatureOnMouseEnter, - clearFeatureHighlightOnMouseLeave, - ] - ) - - if (layer.serverCluster) { - return ( -
- {i18n.t( - 'Data table is not supported when events are grouped on the server.' - )} -
- ) - } - - return ( - <> - ( - - {headers.map(({ name, dataKey, type }, index) => ( - - ) - } - > - {name} - - ))} - - )} - itemContent={(_, row) => - row.map(({ dataKey, value }) => ( - - {dataKey === 'color' ? value?.toLowerCase() : value} - - )) - } - /> - {isLoading && ( - - - - - - )} - - ) -} - -Table.propTypes = { - height: PropTypes.number, -} - -export default Table diff --git a/src/components/datatable/styles/DataTable.module.css b/src/components/datatable/styles/DataTable.module.css index b624f85ce..da05327ae 100644 --- a/src/components/datatable/styles/DataTable.module.css +++ b/src/components/datatable/styles/DataTable.module.css @@ -1,74 +1,35 @@ .dataTable { - font-size: 11px; - font-family: Roboto, sans-serif; - overflow: hidden; + height: 1px; } -.noSupport { - position: absolute; - top: 50%; - left: 50%; - transform: translateX(-50%) translateY(-50%); - color: #333; - font-style: italic; - line-height: 30px; +td.dataCell { + padding-top: var(--spacers-dp8); + padding-bottom: var(--spacers-dp8); + font-size: 12px; } -.loader { - position: absolute; - width: 100%; - height: 100%; - top: 0; +td.lightText { + color: var(--colors-white); } -:global(.ReactVirtualized__Table__headerRow) { - position: relative; - background-color: #eee; - box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 2px; - z-index: 10; +.columnHeader > :global(span.container) { + justify-content: space-between; } - -:global(.ReactVirtualized__Table__headerRow .ColumnHeader-label) { - white-space: nowrap; +/* Hide the filter icon */ +.columnHeader + > :global(span.container) + > :global(span.top) + > button:last-of-type { + visibility: hidden; } -:global(.ReactVirtualized__Table__headerTruncatedText) { - display: inline-block; - position: absolute; - top: 7px; - left: 10px; - right: 15px; - user-select: none; - overflow: hidden; -} - -:global(.ReactVirtualized__Table__sortableHeaderIcon) { +/* TODO */ +.noSupport { position: absolute; - top: 5px; - right: 0; -} - -:global(.ReactVirtualized__Table__headerColumn) { - height: 48px; - margin-bottom: 2px; - position: relative; -} - -:global(.ReactVirtualized__Table__headerColumn:focus) { - outline: none; -} - -:global(.ReactVirtualized__Table__row) { - position: relative; - border-bottom: 1px solid #e0e0e0; - z-index: 9; -} - -:global(.ReactVirtualized__Table__row:hover) { - background-color: #fafafa; - cursor: pointer; -} - -:global(.ReactVirtualized__Table__rowColumn.right) { - text-align: right; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + color: #333; + font-style: italic; + line-height: 30px; } diff --git a/src/components/datatable/styles/UiDataTable.module.css b/src/components/datatable/styles/UiDataTable.module.css deleted file mode 100644 index da05327ae..000000000 --- a/src/components/datatable/styles/UiDataTable.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.dataTable { - height: 1px; -} - -td.dataCell { - padding-top: var(--spacers-dp8); - padding-bottom: var(--spacers-dp8); - font-size: 12px; -} - -td.lightText { - color: var(--colors-white); -} - -.columnHeader > :global(span.container) { - justify-content: space-between; -} -/* Hide the filter icon */ -.columnHeader - > :global(span.container) - > :global(span.top) - > button:last-of-type { - visibility: hidden; -} - -/* TODO */ -.noSupport { - position: absolute; - top: 50%; - left: 50%; - transform: translateX(-50%) translateY(-50%); - color: #333; - font-style: italic; - line-height: 30px; -} From 1f75361f159f43df773ec19aa9666933554d62b3 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Mon, 29 Jan 2024 17:14:39 +0100 Subject: [PATCH 25/57] fix: show message when filter causes the array to be empty --- src/components/datatable/DataTable.js | 9 +++++++++ src/components/datatable/styles/DataTable.module.css | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 9a500dd2c..73dba344a 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -55,6 +55,15 @@ const TableComponents = { TableBody: DataTableBody, TableHead: DataTableHead, TableRow: DataTableRowWithVirtuosoContext, + EmptyPlaceholder: () => ( + + +
+ {i18n.t('No results found')} +
+ + + ), } const Table = ({ height }) => { diff --git a/src/components/datatable/styles/DataTable.module.css b/src/components/datatable/styles/DataTable.module.css index da05327ae..d7813d6a9 100644 --- a/src/components/datatable/styles/DataTable.module.css +++ b/src/components/datatable/styles/DataTable.module.css @@ -23,6 +23,15 @@ td.lightText { visibility: hidden; } +.noResults { + display: flex; + color: var(--colors-grey600); + align-items: center; + justify-content: center; + font-style: italic; + min-height: 40px; +} + /* TODO */ .noSupport { position: absolute; From 3e1dc42a04d25022da5b6287bfb1726879c4632a Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Tue, 30 Jan 2024 08:02:17 +0100 Subject: [PATCH 26/57] chore: use smaller text size in table --- src/components/datatable/styles/DataTable.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/datatable/styles/DataTable.module.css b/src/components/datatable/styles/DataTable.module.css index d7813d6a9..9e271890f 100644 --- a/src/components/datatable/styles/DataTable.module.css +++ b/src/components/datatable/styles/DataTable.module.css @@ -5,7 +5,7 @@ td.dataCell { padding-top: var(--spacers-dp8); padding-bottom: var(--spacers-dp8); - font-size: 12px; + font-size: 11px; } td.lightText { From baeb4d68fbe8b743a3502149a99181753ef57158 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Tue, 30 Jan 2024 09:18:50 +0100 Subject: [PATCH 27/57] fix: update snapshot --- .../__snapshots__/useTableData.spec.js.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap b/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap index f3c67d054..a9800bf96 100644 --- a/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap +++ b/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap @@ -6,7 +6,7 @@ exports[`useTableData gets headers and rows for EE population age groups layer 1 class="headers" >
  • - name=Index, dataKey=index, type=undefined and roundFn=undefined + name=Index, dataKey=index, type=number and roundFn=undefined
  • name=Name, dataKey=name, type=string and roundFn=undefined @@ -81,7 +81,7 @@ exports[`useTableData gets headers and rows for EE population layer 1`] = ` class="headers" >
  • - name=Index, dataKey=index, type=undefined and roundFn=undefined + name=Index, dataKey=index, type=number and roundFn=undefined
  • name=Name, dataKey=name, type=string and roundFn=undefined @@ -156,7 +156,7 @@ exports[`useTableData gets headers and rows for event layer 1`] = ` class="headers" >
  • - name=Index, dataKey=index, type=undefined and roundFn=undefined + name=Index, dataKey=index, type=number and roundFn=undefined
  • name=Org unit, dataKey=ouname, type=string and roundFn=undefined @@ -221,7 +221,7 @@ exports[`useTableData gets headers and rows for facility layer 1`] = ` class="headers" >
  • - name=Index, dataKey=index, type=undefined and roundFn=undefined + name=Index, dataKey=index, type=number and roundFn=undefined
  • name=Name, dataKey=name, type=string and roundFn=undefined @@ -262,7 +262,7 @@ exports[`useTableData gets headers and rows for orgUnit layer 1`] = ` class="headers" >
  • - name=Index, dataKey=index, type=undefined and roundFn=undefined + name=Index, dataKey=index, type=number and roundFn=undefined
  • name=Name, dataKey=name, type=string and roundFn=undefined @@ -315,7 +315,7 @@ exports[`useTableData gets headers and rows for thematic layer 1`] = ` class="headers" >
  • - name=Index, dataKey=index, type=undefined and roundFn=undefined + name=Index, dataKey=index, type=number and roundFn=undefined
  • name=Name, dataKey=name, type=string and roundFn=undefined From 80770c1118ba544aa2ed438d045ef78018856d29 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Tue, 30 Jan 2024 09:32:16 +0100 Subject: [PATCH 28/57] fix: fix LayersLoader jest tests --- .../loaders/__tests__/LayersLoader.spec.js | 32 +++++++++++++------ .../__snapshots__/LayersLoader.spec.js.snap | 24 ++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/components/loaders/__tests__/LayersLoader.spec.js b/src/components/loaders/__tests__/LayersLoader.spec.js index f9d933a89..f8a9fc4b9 100644 --- a/src/components/loaders/__tests__/LayersLoader.spec.js +++ b/src/components/loaders/__tests__/LayersLoader.spec.js @@ -2,10 +2,13 @@ import { render } from '@testing-library/react' import React from 'react' import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' +import { EVENT_LAYER } from '../../../constants/layers.js' import LayersLoader from '../LayersLoader.js' const mockStore = configureMockStore() +const id = 'thelayerid' + jest.mock( '../LayerLoader.js', () => @@ -16,24 +19,26 @@ jest.mock( const testCases = [ { - testTitle: 'renders 0 layers if no layers', + testTitle: 'renders 0 layerloaders if no layers', store: { map: { mapViews: [], }, + dataTable: null, }, }, { - testTitle: 'renders 0 layers if currently loading the layer', + testTitle: 'renders 0 layerloaders if currently loading the layer', store: { map: { mapViews: [{ isLoading: true, isLoaded: false }], }, + dataTable: null, }, }, { testTitle: - 'renders 0 layers if currently loading regardless of whether loaded already', + 'renders 0 layerloaders if currently loading regardless of whether loaded already', store: { map: { mapViews: [ @@ -43,10 +48,12 @@ const testCases = [ }, ], }, + dataTable: null, }, }, { - testTitle: 'renders 1 layer if not currently loading and not loaded', + testTitle: + 'renders 1 layerloader if not currently loading and not loaded', store: { map: { mapViews: [ @@ -56,45 +63,52 @@ const testCases = [ }, ], }, + dataTable: null, }, }, { testTitle: - 'renders 1 layer if not currently loading and is loaded but need extended data', + 'renders 1 layerloader if not currently loading and is loaded but need extended data', store: { map: { mapViews: [ { + id, + layer: EVENT_LAYER, isLoading: false, isLoaded: true, - showDataTable: true, isExtended: false, + serverCluster: false, }, ], }, + dataTable: id, }, }, { testTitle: - 'renders 0 layers if not currently loading and loaded and extended data also loaded', + 'renders 0 layerloaders if not currently loading and loaded and extended data also loaded', store: { map: { mapViews: [ { + id, + layer: EVENT_LAYER, isLoading: false, isLoaded: true, - showDataTable: true, isExtended: true, + serverCluster: false, }, ], }, + dataTable: id, }, }, ] describe('LayersLoader', () => { testCases.forEach(({ testTitle, store }) => { - test(testTitle, () => { + it(testTitle, () => { const { container } = render( diff --git a/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap b/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap index 90197f2b9..706d41761 100644 --- a/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap +++ b/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap @@ -1,5 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`LayersLoader renders 0 layerloaders if currently loading regardless of whether loaded already 1`] = `
    `; + +exports[`LayersLoader renders 0 layerloaders if currently loading the layer 1`] = `
    `; + +exports[`LayersLoader renders 0 layerloaders if no layers 1`] = `
    `; + +exports[`LayersLoader renders 0 layerloaders if not currently loading and loaded and extended data also loaded 1`] = `
    `; + exports[`LayersLoader renders 0 layers if currently loading regardless of whether loaded already 1`] = `
    `; exports[`LayersLoader renders 0 layers if currently loading the layer 1`] = `
    `; @@ -23,3 +31,19 @@ exports[`LayersLoader renders 1 layer if not currently loading and not loaded 1`
    `; + +exports[`LayersLoader renders 1 layerloader if not currently loading and is loaded but need extended data 1`] = ` +
    +
    + LayerLoader +
    +
    +`; + +exports[`LayersLoader renders 1 layerloader if not currently loading and not loaded 1`] = ` +
    +
    + LayerLoader +
    +
    +`; From caf8fb7b0aee705a9d8291b91de03346a61cc78f Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Tue, 30 Jan 2024 10:04:24 +0100 Subject: [PATCH 29/57] chore: snapshots --- .../__snapshots__/LayersLoader.spec.js.snap | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap b/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap index 706d41761..e8c7db59e 100644 --- a/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap +++ b/src/components/loaders/__tests__/__snapshots__/LayersLoader.spec.js.snap @@ -8,30 +8,6 @@ exports[`LayersLoader renders 0 layerloaders if no layers 1`] = `
    `; exports[`LayersLoader renders 0 layerloaders if not currently loading and loaded and extended data also loaded 1`] = `
    `; -exports[`LayersLoader renders 0 layers if currently loading regardless of whether loaded already 1`] = `
    `; - -exports[`LayersLoader renders 0 layers if currently loading the layer 1`] = `
    `; - -exports[`LayersLoader renders 0 layers if no layers 1`] = `
    `; - -exports[`LayersLoader renders 0 layers if not currently loading and loaded and extended data also loaded 1`] = `
    `; - -exports[`LayersLoader renders 1 layer if not currently loading and is loaded but need extended data 1`] = ` -
    -
    - LayerLoader -
    -
    -`; - -exports[`LayersLoader renders 1 layer if not currently loading and not loaded 1`] = ` -
    -
    - LayerLoader -
    -
    -`; - exports[`LayersLoader renders 1 layerloader if not currently loading and is loaded but need extended data 1`] = `
    From 35544e33b3f6c10c50d1effad3545d9b0e37192a Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Tue, 30 Jan 2024 16:30:01 +0100 Subject: [PATCH 30/57] chore: use renderHook to test useTableData --- i18n/en.pot | 43 +- package.json | 1 + .../__snapshots__/useTableData.spec.js.snap | 387 ------------------ .../datatable/__tests__/useTableData.spec.js | 312 +++++++++++--- yarn.lock | 15 + 5 files changed, 283 insertions(+), 475 deletions(-) delete mode 100644 src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap diff --git a/i18n/en.pot b/i18n/en.pot index 12b8caa48..852232a12 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-24T15:10:55.357Z\n" -"PO-Revision-Date: 2024-01-24T15:10:55.357Z\n" +"POT-Creation-Date: 2024-01-30T13:03:53.383Z\n" +"PO-Revision-Date: 2024-01-30T13:03:53.383Z\n" msgid "Untitled map, {{date}}" msgstr "Untitled map, {{date}}" @@ -131,23 +131,23 @@ msgstr "Date" msgid "Data set" msgstr "Data set" -msgid "Index" -msgstr "Index" +msgid "No results found" +msgstr "No results found" -msgid "Org unit" -msgstr "Org unit" +msgid "Data table is not supported when events are grouped on the server." +msgstr "Data table is not supported when events are grouped on the server." -msgid "Id" -msgstr "Id" +msgid "Sort by {{column}}" +msgstr "Sort by {{column}}" -msgid "Event time" -msgstr "Event time" +msgid "Search" +msgstr "Search" -msgid "Legend" -msgstr "Legend" +msgid "Index" +msgstr "Index" -msgid "Range" -msgstr "Range" +msgid "Id" +msgstr "Id" msgid "Level" msgstr "Level" @@ -158,14 +158,17 @@ msgstr "Parent" msgid "Type" msgstr "Type" -msgid "Data table is not supported when events are grouped on the server." -msgstr "Data table is not supported when events are grouped on the server." +msgid "Legend" +msgstr "Legend" -msgid "Search" -msgstr "Search" +msgid "Range" +msgstr "Range" -msgid "Sort by {{column}}" -msgstr "Sort by {{column}}" +msgid "Org unit" +msgstr "Org unit" + +msgid "Event time" +msgstr "Event time" msgid "Items" msgstr "Items" diff --git a/package.json b/package.json index 352aa783f..438188f05 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@krakenjs/post-robot": "^11.0.0", "@reportportal/agent-js-cypress": "git+https://github.com/dhis2/agent-js-cypress.git#develop", "@reportportal/agent-js-jest": "^5.0.7", + "@testing-library/react-hooks": "^8.0.1", "abortcontroller-polyfill": "^1.7.5", "array-move": "^4.0.0", "classnames": "^2.3.2", diff --git a/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap b/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap deleted file mode 100644 index a9800bf96..000000000 --- a/src/components/datatable/__tests__/__snapshots__/useTableData.spec.js.snap +++ /dev/null @@ -1,387 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`useTableData gets headers and rows for EE population age groups layer 1`] = ` -
    -
      -
    • - name=Index, dataKey=index, type=number and roundFn=undefined -
    • -
    • - name=Name, dataKey=name, type=string and roundFn=undefined -
    • -
    • - name=Id, dataKey=id, type=string and roundFn=undefined -
    • -
    • - name=Type, dataKey=type, type=string and roundFn=undefined -
    • -
    • - name=Sum Population Age Groups, dataKey=sum, type=number and roundFn=n => Math.round(n * m) / m -
    • -
    • - name=Mean Population Age Groups, dataKey=mean, type=number and roundFn=n => Math.round(n * m) / m -
    • -
    -
      -
        -
      • - value=0 and dataKey=index -
      • -
      • - value=Badija and dataKey=name -
      • -
      • - value=boOU and dataKey=id -
      • -
      • - value=Polygon and dataKey=type -
      • -
      • - value=2517 and dataKey=sum -
      • -
      • - value=3.976 and dataKey=mean -
      • -
      -
        -
      • - value=1 and dataKey=index -
      • -
      • - value=Baoma and dataKey=name -
      • -
      • - value=baomaOU and dataKey=id -
      • -
      • - value=Polygon and dataKey=type -
      • -
      • - value=9113 and dataKey=sum -
      • -
      • - value=6.003 and dataKey=mean -
      • -
      -
    -
    -`; - -exports[`useTableData gets headers and rows for EE population layer 1`] = ` -
    -
      -
    • - name=Index, dataKey=index, type=number and roundFn=undefined -
    • -
    • - name=Name, dataKey=name, type=string and roundFn=undefined -
    • -
    • - name=Id, dataKey=id, type=string and roundFn=undefined -
    • -
    • - name=Type, dataKey=type, type=string and roundFn=undefined -
    • -
    • - name=Sum Population, dataKey=sum, type=number and roundFn=n => Math.round(n * m) / m -
    • -
    • - name=Mean Population, dataKey=mean, type=number and roundFn=n => Math.round(n * m) / m -
    • -
    -
      -
        -
      • - value=0 and dataKey=index -
      • -
      • - value=Bo and dataKey=name -
      • -
      • - value=boOu and dataKey=id -
      • -
      • - value=Polygon and dataKey=type -
      • -
      • - value=851091 and dataKey=sum -
      • -
      • - value=47.35 and dataKey=mean -
      • -
      -
        -
      • - value=1 and dataKey=index -
      • -
      • - value=Bombali and dataKey=name -
      • -
      • - value=bombaliOU and dataKey=id -
      • -
      • - value=Polygon and dataKey=type -
      • -
      • - value=585872 and dataKey=sum -
      • -
      • - value=27.35 and dataKey=mean -
      • -
      -
    -
    -`; - -exports[`useTableData gets headers and rows for event layer 1`] = ` -
    -
      -
    • - name=Index, dataKey=index, type=number and roundFn=undefined -
    • -
    • - name=Org unit, dataKey=ouname, type=string and roundFn=undefined -
    • -
    • - name=Id, dataKey=id, type=string and roundFn=undefined -
    • -
    • - name=Event time, dataKey=eventdate, type=date and roundFn=undefined -
    • -
    • - name=Last updated on, dataKey=lastupdated, type=string and roundFn=undefined -
    • -
    • - name=Event status, dataKey=eventstatus, type=string and roundFn=undefined -
    • -
    • - name=Gender, dataKey=oZg33kd9taw, type=string and roundFn=undefined -
    • -
    • - name=Type, dataKey=type, type=string and roundFn=undefined -
    • -
    -
      -
        -
      • - value=0 and dataKey=index -
      • -
      • - value=Lumley Hospital and dataKey=ouname -
      • -
      • - value=a9712323629 and dataKey=id -
      • -
      • - value=2023-05-15 00:00:00.0 and dataKey=eventdate -
      • -
      • - value=2018-04-12 20:58:51.31 and dataKey=lastupdated -
      • -
      • - value=ACTIVE and dataKey=eventstatus -
      • -
      • - value=Female and dataKey=oZg33kd9taw -
      • -
      • - value=Point and dataKey=type -
      • -
      -
    -
    -`; - -exports[`useTableData gets headers and rows for facility layer 1`] = ` -
    -
      -
    • - name=Index, dataKey=index, type=number and roundFn=undefined -
    • -
    • - name=Name, dataKey=name, type=string and roundFn=undefined -
    • -
    • - name=Id, dataKey=id, type=string and roundFn=undefined -
    • -
    • - name=Type, dataKey=type, type=string and roundFn=undefined -
    • -
    -
      -
        -
      • - value=0 and dataKey=index -
      • -
      • - value=Facility 1 and dataKey=name -
      • -
      • - value=facility-1 and dataKey=id -
      • -
      • - value=Point and dataKey=type -
      • -
      -
    -
    -`; - -exports[`useTableData gets headers and rows for orgUnit layer 1`] = ` -
    -
      -
    • - name=Index, dataKey=index, type=number and roundFn=undefined -
    • -
    • - name=Name, dataKey=name, type=string and roundFn=undefined -
    • -
    • - name=Id, dataKey=id, type=string and roundFn=undefined -
    • -
    • - name=Level, dataKey=level, type=number and roundFn=undefined -
    • -
    • - name=Parent, dataKey=parentName, type=string and roundFn=undefined -
    • -
    • - name=Type, dataKey=type, type=string and roundFn=undefined -
    • -
    -
      -
        -
      • - value=0 and dataKey=index -
      • -
      • - value=OrgUnitName 1 and dataKey=name -
      • -
      • - value=orgunit-id-1 and dataKey=id -
      • -
      • - value=3 and dataKey=level -
      • -
      • - value=Bo and dataKey=parentName -
      • -
      • - value=MultiPolygon and dataKey=type -
      • -
      -
    -
    -`; - -exports[`useTableData gets headers and rows for thematic layer 1`] = ` -
    -
      -
    • - name=Index, dataKey=index, type=number and roundFn=undefined -
    • -
    • - name=Name, dataKey=name, type=string and roundFn=undefined -
    • -
    • - name=Id, dataKey=id, type=string and roundFn=undefined -
    • -
    • - name=Value, dataKey=value, type=number and roundFn=undefined -
    • -
    • - name=Legend, dataKey=legend, type=string and roundFn=undefined -
    • -
    • - name=Range, dataKey=range, type=string and roundFn=undefined -
    • -
    • - name=Level, dataKey=level, type=number and roundFn=undefined -
    • -
    • - name=Parent, dataKey=parentName, type=string and roundFn=undefined -
    • -
    • - name=Type, dataKey=type, type=string and roundFn=undefined -
    • -
    • - name=Color, dataKey=color, type=string and roundFn=undefined -
    • -
    -
      -
        -
      • - value=0 and dataKey=index -
      • -
      • - value=Ngelehun CHC and dataKey=name -
      • -
      • - value=thematicId-1 and dataKey=id -
      • -
      • - value=106.3 and dataKey=value -
      • -
      • - value=Great and dataKey=legend -
      • -
      • - value=90 - 120 and dataKey=range -
      • -
      • - value=4 and dataKey=level -
      • -
      • - value=Badjia and dataKey=parentName -
      • -
      • - value=Point and dataKey=type -
      • -
      • - value=#FFFFB2 and dataKey=color -
      • -
      -
    -
    -`; diff --git a/src/components/datatable/__tests__/useTableData.spec.js b/src/components/datatable/__tests__/useTableData.spec.js index 861535a2a..9976daadb 100644 --- a/src/components/datatable/__tests__/useTableData.spec.js +++ b/src/components/datatable/__tests__/useTableData.spec.js @@ -1,5 +1,4 @@ -import { render } from '@testing-library/react' -import PropTypes from 'prop-types' +import { renderHook } from '@testing-library/react-hooks' import React from 'react' import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' @@ -9,39 +8,6 @@ jest.mock('../../map/MapApi.js', () => ({ loadEarthEngineWorker: jest.fn(), })) -const Table = ({ layer, sortField, sortDirection }) => { - const { headers, rows } = useTableData({ layer, sortField, sortDirection }) - - return ( - <> -
      - {headers.map((header) => ( -
    • {`name=${header.name}, dataKey=${header.dataKey}, type=${header.type} and roundFn=${header.roundFn}`}
    • - ))} -
    -
      - {rows.map((row, index) => ( -
        - {row.map((r) => ( -
      • {`value=${r.value} and dataKey=${r.dataKey}`}
      • - ))} -
      - ))} -
    - - ) -} - -Table.propTypes = { - layer: PropTypes.object, - sortDirection: PropTypes.string, - sortField: PropTypes.string, -} - const mockStore = configureMockStore() describe('useTableData', () => { @@ -62,13 +28,38 @@ describe('useTableData', () => { }, ], } - const { container } = render( - - - + + const { result } = renderHook( + () => + useTableData({ + layer, + sortField: 'name', + sortDirection: 'asc', + }), + { + wrapper: ({ children }) => ( + {children} + ), + } ) - expect(container).toMatchSnapshot() + const { headers, rows, isLoading } = result.current + expect(headers).toHaveLength(4) + expect(headers).toMatchObject([ + { name: 'Index', dataKey: 'index', type: 'number' }, + { name: 'Name', dataKey: 'name', type: 'string' }, + { name: 'Id', dataKey: 'id', type: 'string' }, + { name: 'Type', dataKey: 'type', type: 'string' }, + ]) + expect(rows).toHaveLength(1) + expect(rows[0]).toHaveLength(4) + expect(rows[0]).toMatchObject([ + { value: 0, dataKey: 'index' }, + { value: 'Facility 1', dataKey: 'name' }, + { value: 'facility-1', dataKey: 'id' }, + { value: 'Point', dataKey: 'type' }, + ]) + expect(isLoading).toBe(false) }) test('gets headers and rows for orgUnit layer', () => { @@ -90,13 +81,41 @@ describe('useTableData', () => { }, ], } - const { container } = render( - -
    - - ) - expect(container).toMatchSnapshot() + const { result } = renderHook( + () => + useTableData({ + layer, + sortField: 'name', + sortDirection: 'asc', + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ) + const { headers, rows, isLoading } = result.current + expect(headers).toHaveLength(6) + expect(headers).toMatchObject([ + { name: 'Index', dataKey: 'index', type: 'number' }, + { name: 'Name', dataKey: 'name', type: 'string' }, + { name: 'Id', dataKey: 'id', type: 'string' }, + { name: 'Level', dataKey: 'level', type: 'number' }, + { name: 'Parent', dataKey: 'parentName', type: 'string' }, + { name: 'Type', dataKey: 'type', type: 'string' }, + ]) + expect(rows).toHaveLength(1) + expect(rows[0]).toHaveLength(6) + expect(rows[0]).toMatchObject([ + { value: 0, dataKey: 'index' }, + { value: 'OrgUnitName 1', dataKey: 'name' }, + { value: 'orgunit-id-1', dataKey: 'id' }, + { value: 3, dataKey: 'level' }, + { value: 'Bo', dataKey: 'parentName' }, + { value: 'MultiPolygon', dataKey: 'type' }, + ]) + expect(isLoading).toBe(false) }) test('gets headers and rows for thematic layer', () => { @@ -122,13 +141,53 @@ describe('useTableData', () => { }, ], } - const { container } = render( - -
    - + const { result } = renderHook( + () => + useTableData({ + layer, + sortField: 'name', + sortDirection: 'asc', + }), + { + wrapper: ({ children }) => ( + {children} + ), + } ) - - expect(container).toMatchSnapshot() + const { headers, rows, isLoading } = result.current + expect(headers).toHaveLength(10) + expect(headers).toMatchObject([ + { name: 'Index', dataKey: 'index', type: 'number' }, + { name: 'Name', dataKey: 'name', type: 'string' }, + { name: 'Id', dataKey: 'id', type: 'string' }, + { name: 'Value', dataKey: 'value', type: 'number' }, + { name: 'Legend', dataKey: 'legend', type: 'string' }, + { name: 'Range', dataKey: 'range', type: 'string' }, + { name: 'Level', dataKey: 'level', type: 'number' }, + { name: 'Parent', dataKey: 'parentName', type: 'string' }, + { name: 'Type', dataKey: 'type', type: 'string' }, + { + name: 'Color', + dataKey: 'color', + type: 'string', + renderer: 'rendercolor', + }, + ]) + expect(rows).toHaveLength(1) + expect(rows[0]).toHaveLength(10) + expect(rows[0]).toMatchObject([ + { value: 0, dataKey: 'index' }, + { value: 'Ngelehun CHC', dataKey: 'name' }, + { value: 'thematicId-1', dataKey: 'id' }, + { value: 106.3, dataKey: 'value' }, + { value: 'Great', dataKey: 'legend' }, + { value: '90 - 120', dataKey: 'range' }, + { value: 4, dataKey: 'level' }, + { value: 'Badjia', dataKey: 'parentName' }, + { value: 'Point', dataKey: 'type' }, + { value: '#FFFFB2', dataKey: 'color' }, + ]) + expect(isLoading).toBe(false) }) test('gets headers and rows for event layer', () => { @@ -138,6 +197,7 @@ describe('useTableData', () => { const layer = { layer: 'event', dataFilters: null, + isExtended: true, headers: [ { name: 'ps', @@ -183,13 +243,49 @@ describe('useTableData', () => { }, ], } - const { container } = render( - -
    - + const { result } = renderHook( + () => + useTableData({ + layer, + sortField: 'name', + sortDirection: 'asc', + }), + { + wrapper: ({ children }) => ( + {children} + ), + } ) - - expect(container).toMatchSnapshot() + const { headers, rows, isLoading } = result.current + expect(headers).toHaveLength(8) + expect(headers).toMatchObject([ + { name: 'Index', dataKey: 'index', type: 'number' }, + { name: 'Org unit', dataKey: 'ouname', type: 'string' }, + { name: 'Id', dataKey: 'id', type: 'string' }, + { + name: 'Event time', + dataKey: 'eventdate', + type: 'date', + renderer: 'formatTime...', + }, + { name: 'Last updated on', dataKey: 'lastupdated', type: 'string' }, + { name: 'Event status', dataKey: 'eventstatus', type: 'string' }, + { name: 'Gender', dataKey: 'oZg33kd9taw', type: 'string' }, + { name: 'Type', dataKey: 'type', type: 'string' }, + ]) + expect(rows).toHaveLength(1) + expect(rows[0]).toHaveLength(8) + expect(rows[0]).toMatchObject([ + { value: 0, dataKey: 'index' }, + { value: 'Lumley Hospital', dataKey: 'ouname' }, + { value: 'a9712323629', dataKey: 'id' }, + { value: '2023-05-15 00:00:00.0', dataKey: 'eventdate' }, + { value: '2018-04-12 20:58:51.31', dataKey: 'lastupdated' }, + { value: 'ACTIVE', dataKey: 'eventstatus' }, + { value: 'Female', dataKey: 'oZg33kd9taw' }, + { value: 'Point', dataKey: 'type' }, + ]) + expect(isLoading).toBe(false) }) test('gets headers and rows for EE population layer', () => { @@ -235,13 +331,53 @@ describe('useTableData', () => { }, ], } - const { container } = render( - -
    - + const { result } = renderHook( + () => + useTableData({ + layer, + sortField: 'name', + sortDirection: 'asc', + }), + { + wrapper: ({ children }) => ( + {children} + ), + } ) + const { headers, rows, isLoading } = result.current - expect(container).toMatchSnapshot() + expect(headers).toHaveLength(6) + expect(headers).toMatchObject([ + { name: 'Index', dataKey: 'index', type: 'number' }, + { name: 'Name', dataKey: 'name', type: 'string' }, + { name: 'Id', dataKey: 'id', type: 'string' }, + { name: 'Type', dataKey: 'type', type: 'string' }, + { + name: 'Sum Population', + dataKey: 'sum', + // roundFn: Function.prototype, + type: 'number', + }, + { + name: 'Mean Population', + dataKey: 'mean', + // roundFn: Function.prototype, + type: 'number', + }, + ]) + expect(headers[4].roundFn).toBeInstanceOf(Function) + expect(headers[5].roundFn).toBeInstanceOf(Function) + expect(rows).toHaveLength(2) + expect(rows[0]).toHaveLength(6) + expect(rows[0]).toMatchObject([ + { value: 0, dataKey: 'index' }, + { value: 'Bo', dataKey: 'name' }, + { value: 'boOu', dataKey: 'id' }, + { value: 'Polygon', dataKey: 'type' }, + { value: 851091, dataKey: 'sum' }, + { value: 47.35, dataKey: 'mean' }, + ]) + expect(isLoading).toBe(false) }) test('gets headers and rows for EE population age groups layer', () => { @@ -352,12 +488,52 @@ describe('useTableData', () => { ], } - const { container } = render( - -
    - + const { result } = renderHook( + () => + useTableData({ + layer, + sortField: 'name', + sortDirection: 'asc', + }), + { + wrapper: ({ children }) => ( + {children} + ), + } ) + const { headers, rows, isLoading } = result.current - expect(container).toMatchSnapshot() + expect(headers).toHaveLength(6) + expect(headers).toMatchObject([ + { name: 'Index', dataKey: 'index', type: 'number' }, + { name: 'Name', dataKey: 'name', type: 'string' }, + { name: 'Id', dataKey: 'id', type: 'string' }, + { name: 'Type', dataKey: 'type', type: 'string' }, + { + name: 'Sum Population Age Groups', + dataKey: 'sum', + // roundFn: Function.prototype, + type: 'number', + }, + { + name: 'Mean Population Age Groups', + dataKey: 'mean', + // roundFn: Function.prototype, + type: 'number', + }, + ]) + expect(headers[4].roundFn).toBeInstanceOf(Function) + expect(headers[5].roundFn).toBeInstanceOf(Function) + expect(rows).toHaveLength(2) + expect(rows[0]).toHaveLength(6) + expect(rows[0]).toMatchObject([ + { value: 0, dataKey: 'index' }, + { value: 'Badija', dataKey: 'name' }, + { value: 'boOU', dataKey: 'id' }, + { value: 'Polygon', dataKey: 'type' }, + { value: 2517, dataKey: 'sum' }, + { value: 3.976, dataKey: 'mean' }, + ]) + expect(isLoading).toBe(false) }) }) diff --git a/yarn.lock b/yarn.lock index 417da6d33..452847445 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3215,6 +3215,14 @@ lz-string "^1.4.4" pretty-format "^27.0.2" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react@^12.1.2", "@testing-library/react@^12.1.5": version "12.1.5" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" @@ -13312,6 +13320,13 @@ react-dom@^16.14.0, react-dom@^16.8.6: prop-types "^15.6.2" scheduler "^0.19.1" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" From 436445900c7e533963b1f6123a31cc43b7328a59 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 31 Jan 2024 14:30:04 +0100 Subject: [PATCH 31/57] fix: dont show orgunitprofile for EVENT layers --- src/components/datatable/DataTable.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 73dba344a..d0cf56387 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -17,6 +17,7 @@ import { useSelector, useDispatch } from 'react-redux' import { TableVirtuoso } from 'react-virtuoso' import { highlightFeature } from '../../actions/feature.js' import { setOrgUnitProfile } from '../../actions/orgUnits.js' +import { EVENT_LAYER } from '../../constants/layers.js' import { isDarkColor } from '../../util/colors.js' import FilterInput from './FilterInput.js' import styles from './styles/DataTable.module.css' @@ -99,7 +100,7 @@ const Table = ({ height }) => { sortDirection, }) - const onTableRowClick = useCallback( + const showOrgUnitProfile = useCallback( (row) => { const id = row.find((r) => r.dataKey === 'id')?.value id && dispatch(setOrgUnitProfile(id)) @@ -107,7 +108,7 @@ const Table = ({ height }) => { [dispatch] ) - const highlightFeatureOnMouseEnter = useCallback( + const setFeatureHighlight = useCallback( (row) => { const id = row.find((r) => r.dataKey === 'id')?.value if (!id || !feature || id !== feature.id) { @@ -126,11 +127,11 @@ const Table = ({ height }) => { }, [feature, dispatch, layer.id] ) - const clearFeatureHighlightOnMouseLeave = useCallback( + const clearFeatureHighlight = useCallback( (event) => { const nextElement = event.toElement ?? event.relatedTarget // When hovering to the next row the next element is a `TD` - // If this is the case `highlightFeatureOnMouseEnter` will + // If this is the case `setFeatureHighlight` will // fire and the highlight does not need to be cleared if (nextElement.tagName !== 'TD') { dispatch(highlightFeature(null)) @@ -141,14 +142,18 @@ const Table = ({ height }) => { const tableContext = useMemo( () => ({ - onClick: onTableRowClick, - onMouseEnter: highlightFeatureOnMouseEnter, - onMouseLeave: clearFeatureHighlightOnMouseLeave, + onClick: + layer.layer !== EVENT_LAYER + ? showOrgUnitProfile + : Function.prototype, + onMouseEnter: setFeatureHighlight, + onMouseLeave: clearFeatureHighlight, }), [ - onTableRowClick, - highlightFeatureOnMouseEnter, - clearFeatureHighlightOnMouseLeave, + layer.layer, + showOrgUnitProfile, + setFeatureHighlight, + clearFeatureHighlight, ] ) From 21318addc0e2964d9fa4290cf73959be1a5f5649 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 31 Jan 2024 14:30:37 +0100 Subject: [PATCH 32/57] fix: dont use text select cursor in table rows --- src/components/datatable/styles/DataTable.module.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/datatable/styles/DataTable.module.css b/src/components/datatable/styles/DataTable.module.css index 9e271890f..65f7382f9 100644 --- a/src/components/datatable/styles/DataTable.module.css +++ b/src/components/datatable/styles/DataTable.module.css @@ -6,6 +6,11 @@ td.dataCell { padding-top: var(--spacers-dp8); padding-bottom: var(--spacers-dp8); font-size: 11px; + +} + +td.dataCell:hover { + cursor: default; } td.lightText { From bd69a520b357790ca57e2db529ac037c611be03d Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 31 Jan 2024 14:52:37 +0100 Subject: [PATCH 33/57] fix: make sure numeric data items are filterable and sortable --- src/loaders/eventLoader.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/loaders/eventLoader.js b/src/loaders/eventLoader.js index 8bb1bc115..1761728b7 100644 --- a/src/loaders/eventLoader.js +++ b/src/loaders/eventLoader.js @@ -7,6 +7,7 @@ import { EVENT_COLOR, EVENT_RADIUS, } from '../constants/layers.js' +import { numberValueTypes } from '../constants/valueTypes.js' import { getFiltersFromColumns, getFiltersAsText, @@ -16,6 +17,7 @@ import { import { cssColor, getContrastColor } from '../util/colors.js' import { getAnalyticsRequest, loadData } from '../util/event.js' import { getBounds } from '../util/geojson.js' +import { isValidUid } from '../util/helpers.js' import { styleByDataItem } from '../util/styleByDataItem.js' import { formatStartEndDate, getDateArray } from '../util/time.js' @@ -159,6 +161,26 @@ const loadEventLayer = async (config, loadExtended) => { }) config.headers = response.headers + + const numericDataItemHeaders = config.headers.filter( + (header) => + isValidUid(header.name) && + numberValueTypes.includes(header.valueType) + ) + + if (numericDataItemHeaders.length) { + config.data = config.data.map((d) => { + const newD = { ...d } + + numericDataItemHeaders.forEach((header) => { + newD.properties[header.name] = parseFloat( + d.properties[header.name] + ) + }) + + return newD + }) + } } if (!styleDataItem) { From 14a31de4e8c42cad71f8f725e16dd155c0625c99 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 31 Jan 2024 16:30:58 +0100 Subject: [PATCH 34/57] chore: add additional assertions for filtering and sorting of event layer --- cypress/integration/dataTable.cy.js | 69 ++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/cypress/integration/dataTable.cy.js b/cypress/integration/dataTable.cy.js index 6ab3b09fc..01fb5f21e 100644 --- a/cypress/integration/dataTable.cy.js +++ b/cypress/integration/dataTable.cy.js @@ -125,6 +125,16 @@ describe('data table', () => { .find('td') .eq(3) .should('contain', '76') + + // click on a row + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .first() + .click() + + // check that the org unit profile drawer is opened + cy.getByDataTest('org-unit-profile').should('be.visible') }) it('opens the data table for an Event layer', () => { @@ -164,7 +174,64 @@ describe('data table', () => { cy.getByDataTest('bottom-panel') .findByDataTest('dhis2-uicore-datatablecellhead') - .contains('gender', { matchCase: false }) + .contains('Age in years', { matchCase: false }) .should('be.visible') + + // filter by Org unit + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-datatablecellhead') + .containsExact('Org unit') + .siblings('input') + .type('Kpetema') + + // filter by Gender + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-datatablecellhead') + .containsExact('Gender') + .siblings('input') + .type('Female') + + // filter by Age in years (numeric) + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-datatablecellhead') + .containsExact('Age in years') + .siblings('input') + .type('<11') + + // check that the filter returned the correct number of rows + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .should('have.length', 3) + + // Sort by Age in years + cy.get('button[title="Sort by Age in years"]').click() + + // confirm that the rows are sorted by Age in years descending + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .first() + .find('td') + .eq(7) + .should('contain', '8') + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .last() + .find('td') + .eq(7) + .should('contain', '2') + + // click on a row + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .first() + .click() + + // check that the org unit profile drawer is NOT opened + cy.getByDataTest('org-unit-profile').should('not.exist') }) }) From 9045cbc73c8649a7e16df99a00f3b95f5d363f19 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 1 Feb 2024 12:02:31 +0100 Subject: [PATCH 35/57] fix: error feedback to user about unsuppored layer --- cypress.config.js | 2 +- i18n/en.pot | 7 +++++-- src/components/datatable/DataTable.js | 10 +++++++++- src/components/datatable/useTableData.js | 1 - 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index 43a62ced8..0717507a8 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -67,7 +67,7 @@ module.exports = defineConfig({ // Enabled to reduce the risk of out-of-memory issues experimentalMemoryManagement: true, // Set to a low number to reduce the risk of out-of-memory issues - numTestsKeptInMemory: 5, + numTestsKeptInMemory: 4, /* When allowing 1 retry on CI, the test suite will pass if * it's flaky. And/but we also get to identify flaky tests on the * Cypress Dashboard. */ diff --git a/i18n/en.pot b/i18n/en.pot index 852232a12..0254b1efb 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-30T13:03:53.383Z\n" -"PO-Revision-Date: 2024-01-30T13:03:53.383Z\n" +"POT-Creation-Date: 2024-01-31T15:42:44.061Z\n" +"PO-Revision-Date: 2024-01-31T15:42:44.061Z\n" msgid "Untitled map, {{date}}" msgstr "Untitled map, {{date}}" @@ -134,6 +134,9 @@ msgstr "Data set" msgid "No results found" msgstr "No results found" +msgid "Data table is not supported for this layer." +msgstr "Data table is not supported for this layer." + msgid "Data table is not supported when events are grouped on the server." msgstr "Data table is not supported when events are grouped on the server." diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index d0cf56387..fea18eef2 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -94,7 +94,7 @@ const Table = ({ height }) => { [sortDirection] ) - const { headers, rows, isLoading } = useTableData({ + const { headers, rows, isLoading, isError } = useTableData({ layer, sortField, sortDirection, @@ -157,6 +157,14 @@ const Table = ({ height }) => { ] ) + if (!headers.length) { + return ( +
    + {i18n.t('Data table is not supported for this layer.')} +
    + ) + } + if (layer.serverCluster) { return (
    diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 9d037497c..2515a8855 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -203,7 +203,6 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { case FACILITY_LAYER: return getFacilityHeaders() default: - // TODO - throw error? return [] } }, [ From 41d3bd6a679c07ab75d686bcde74796f2932e43a Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 7 Feb 2024 15:57:01 +0100 Subject: [PATCH 36/57] fix: no crash if EE layer doesnt have orgunits --- i18n/en.pot | 13 ++-- src/components/datatable/DataTable.js | 20 +++---- src/components/datatable/useTableData.js | 60 ++++++++++++------- .../layers/toolbar/LayerToolbarMoreMenu.js | 5 +- 4 files changed, 58 insertions(+), 40 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0254b1efb..1203f17f1 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-31T15:42:44.061Z\n" -"PO-Revision-Date: 2024-01-31T15:42:44.061Z\n" +"POT-Creation-Date: 2024-02-07T14:40:25.874Z\n" +"PO-Revision-Date: 2024-02-07T14:40:25.874Z\n" msgid "Untitled map, {{date}}" msgstr "Untitled map, {{date}}" @@ -134,9 +134,6 @@ msgstr "Data set" msgid "No results found" msgstr "No results found" -msgid "Data table is not supported for this layer." -msgstr "Data table is not supported for this layer." - msgid "Data table is not supported when events are grouped on the server." msgstr "Data table is not supported when events are grouped on the server." @@ -173,6 +170,12 @@ msgstr "Org unit" msgid "Event time" msgstr "Event time" +msgid "No valid data was found for the current layer configuration." +msgstr "No valid data was found for the current layer configuration." + +msgid "Data table is not supported for this layer type." +msgstr "Data table is not supported for this layer type." + msgid "Items" msgstr "Items" diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index fea18eef2..5b485fb15 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -94,12 +94,6 @@ const Table = ({ height }) => { [sortDirection] ) - const { headers, rows, isLoading, isError } = useTableData({ - layer, - sortField, - sortDirection, - }) - const showOrgUnitProfile = useCallback( (row) => { const id = row.find((r) => r.dataKey === 'id')?.value @@ -157,12 +151,14 @@ const Table = ({ height }) => { ] ) - if (!headers.length) { - return ( -
    - {i18n.t('Data table is not supported for this layer.')} -
    - ) + const { headers, rows, isLoading, error } = useTableData({ + layer, + sortField, + sortDirection, + }) + + if (error) { + return
    {error}
    } if (layer.serverCluster) { diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 2515a8855..9978e66f9 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -165,26 +165,27 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { serverCluster, } = layer || EMPTY_LAYER - const dataWithAggregations = useMemo( - () => - data - .map((d, i) => ({ - index: i, - ...d, - })) - .filter((d) => !d.properties.hasAdditionalGeometry) - .map((d, i) => ({ - ...(d.properties || d), - ...aggregations[d.id], - index: d.index, - i, - })), - [data, aggregations] - ) + const dataWithAggregations = useMemo(() => { + if (!data) { + return null + } + return data + .map((d, i) => ({ + index: i, + ...d, + })) + .filter((d) => !d.properties.hasAdditionalGeometry) + .map((d, i) => ({ + ...(d.properties || d), + ...aggregations[d.id], + index: d.index, + i, + })) + }, [data, aggregations]) const headers = useMemo(() => { - if (!layerType || !dataWithAggregations.length) { - return [] + if (dataWithAggregations === null) { + return null } switch (layerType) { @@ -202,8 +203,9 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { }) case FACILITY_LAYER: return getFacilityHeaders() - default: - return [] + default: { + return null + } } }, [ layerType, @@ -215,6 +217,10 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { ]) const rows = useMemo(() => { + if (dataWithAggregations === null || headers === null) { + return null + } + if (!dataWithAggregations.length || !headers?.length) { return [] } @@ -249,13 +255,23 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { } }) ) - }, [dataWithAggregations, dataFilters, sortField, sortDirection, headers]) + }, [headers, dataWithAggregations, dataFilters, sortField, sortDirection]) + // EE layers and event layers may be loading additional data const isLoading = (layerType === EARTH_ENGINE_LAYER && aggregationType?.length && (!aggregations || aggregations === EMPTY_AGGREGATIONS)) || (layerType === EVENT_LAYER && !layer.isExtended && !serverCluster) - return { headers, rows, isLoading } + const error = + dataWithAggregations === null + ? i18n.t( + 'No valid data was found for the current layer configuration.' + ) + : headers === null + ? i18n.t('Data table is not supported for this layer type.') + : null + + return { headers, rows, isLoading, error } } diff --git a/src/components/layers/toolbar/LayerToolbarMoreMenu.js b/src/components/layers/toolbar/LayerToolbarMoreMenu.js index 619598f76..b2b3846bf 100644 --- a/src/components/layers/toolbar/LayerToolbarMoreMenu.js +++ b/src/components/layers/toolbar/LayerToolbarMoreMenu.js @@ -40,6 +40,9 @@ const LayerToolbarMoreMenu = ({ return null } + const showDataTableDisabled = + !hasOrgUnitData && (!dataTableOpen || dataTableOpen !== layer.id) + return ( <>
    )} {openAs && ( From 62a74f0825432dfa76ab23441c5b4bbf6bcfe800 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 7 Feb 2024 16:31:56 +0100 Subject: [PATCH 37/57] fix: align number columns right --- src/components/datatable/DataTable.js | 4 ++-- src/components/datatable/useTableData.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 5b485fb15..193ed4a40 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -211,7 +211,7 @@ const Table = ({ height }) => { )} itemContent={(_, row) => - row.map(({ dataKey, value }) => ( + row.map(({ dataKey, value, align }) => ( { dataKey === 'color' && isDarkColor(value), })} backgroundColor={dataKey === 'color' ? value : null} - align="left" + align={align} > {dataKey === 'color' ? value?.toLowerCase() : value} diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 9978e66f9..485ad7688 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -250,8 +250,9 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { const value = roundFn ? roundFn(item[dataKey]) : item[dataKey] return { - value: type === TYPE_NUMBER && isNaN(value) ? null : value, dataKey, + value: type === TYPE_NUMBER && isNaN(value) ? null : value, + align: type === TYPE_NUMBER ? 'right' : 'left', } }) ) From 40972c8421e0dead33b1f52683a31635c92ecc32 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 17:05:46 +0100 Subject: [PATCH 38/57] fix: prevent column width changes while scrolling --- src/components/datatable/BottomPanel.js | 5 +- src/components/datatable/DataTable.js | 97 +++++++++++++++++++++---- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 1b7796124..26c445f87 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -53,7 +53,10 @@ const BottomPanel = () => { onResize={onResize} onResizeEnd={(height) => dispatch(resizeDataTable(height))} /> - +
    ) } diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 193ed4a40..cc9893e1b 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -12,7 +12,14 @@ import { } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React, { useReducer, useCallback, useMemo } from 'react' +import React, { + useReducer, + useCallback, + useMemo, + useEffect, + useRef, + useState, +} from 'react' import { useSelector, useDispatch } from 'react-redux' import { TableVirtuoso } from 'react-virtuoso' import { highlightFeature } from '../../actions/feature.js' @@ -26,17 +33,29 @@ import { useTableData } from './useTableData.js' const ASCENDING = 'asc' const DESCENDING = 'desc' -const DataTableRowWithVirtuosoContext = ({ context, item, ...props }) => { - return ( - context.onClick(item)} - onMouseEnter={() => context.onMouseEnter(item)} - onMouseLeave={context.onMouseLeave} - {...props} - /> - ) +const DataTableWithVirtuosoContext = ({ context, ...props }) => ( + +) + +DataTableWithVirtuosoContext.propTypes = { + context: PropTypes.shape({ + layout: PropTypes.string, + }), } +const DataTableRowWithVirtuosoContext = ({ context, item, ...props }) => ( + context.onClick(item)} + onMouseEnter={() => context.onMouseEnter(item)} + onMouseLeave={context.onMouseLeave} + {...props} + /> +) + DataTableRowWithVirtuosoContext.propTypes = { context: PropTypes.shape({ onClick: PropTypes.func, @@ -52,7 +71,7 @@ DataTableRowWithVirtuosoContext.propTypes = { } const TableComponents = { - Table: (props) => , + Table: DataTableWithVirtuosoContext, TableBody: DataTableBody, TableHead: DataTableHead, TableRow: DataTableRowWithVirtuosoContext, @@ -67,7 +86,9 @@ const TableComponents = { ), } -const Table = ({ height }) => { +const Table = ({ availableHeight, availableWidth }) => { + const headerRowRef = useRef(null) + const [columnWidths, setColumnWidths] = useState([]) const { mapViews } = useSelector((state) => state.map) const activeLayerId = useSelector((state) => state.dataTable) @@ -142,12 +163,14 @@ const Table = ({ height }) => { : Function.prototype, onMouseEnter: setFeatureHighlight, onMouseLeave: clearFeatureHighlight, + layout: columnWidths.length > 0 ? 'fixed' : 'auto', }), [ layer.layer, showOrgUnitProfile, setFeatureHighlight, clearFeatureHighlight, + columnWidths, ] ) @@ -157,10 +180,43 @@ const Table = ({ height }) => { sortDirection, }) + useEffect(() => { + /* The combination of automtic table layout and virtual scrolling + * causes a content shift when scrolling and filtering because the + * cells in the DOM have a different content length which causes the + * columns to have a different width. To avoid that we measure the + * initial column widths and switch to a fixed layout based on these + * measured widths */ + if (columnWidths.length === 0) { + const measuredColumnWidths = [] + + for (const cell of headerRowRef.current.cells) { + measuredColumnWidths.push(cell.offsetWidth) + } + setColumnWidths(measuredColumnWidths) + } + }, [columnWidths]) + + useEffect(() => { + /* When the window is resized or the sidebar opens + * the table needs to switch back to its automatic layout + * so that the cells can subsequently can be measured again + * in the useEffect hook above */ + setColumnWidths([]) + }, [availableWidth]) + if (error) { return
    {error}
    } + if (!headers.length) { + return ( +
    + {i18n.t('Data table is not supported for this layer.')} +
    + ) + } + if (layer.serverCluster) { return (
    @@ -176,10 +232,13 @@ const Table = ({ height }) => { ( - +
    {headers.map(({ name, dataKey, type }, index) => ( { /> ) } + width={ + columnWidths.length > 0 + ? `columnWidths[index]px` + : 'auto' + } > {name} ))} - + )} itemContent={(_, row) => row.map(({ dataKey, value, align }) => ( @@ -238,7 +302,8 @@ const Table = ({ height }) => { } Table.propTypes = { - height: PropTypes.number, + availableHeight: PropTypes.number, + availableWidth: PropTypes.number, } export default Table From 93b88eaeccd67dbea2a83cc9d05f6c4e4d01b046 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 17:52:37 +0100 Subject: [PATCH 39/57] fix: ensure width is a valid value in px --- src/components/datatable/DataTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index cc9893e1b..accc34d0e 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -265,7 +265,7 @@ const Table = ({ availableHeight, availableWidth }) => { } width={ columnWidths.length > 0 - ? `columnWidths[index]px` + ? `${columnWidths[index]}px` : 'auto' } > From f4704ed6f7ea1bc85ab43d765190b4049997ed37 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 17:54:37 +0100 Subject: [PATCH 40/57] fix: allow the DOM to repaint before switching to fixed layout --- src/components/datatable/DataTable.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index accc34d0e..156ae2650 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -188,12 +188,15 @@ const Table = ({ availableHeight, availableWidth }) => { * initial column widths and switch to a fixed layout based on these * measured widths */ if (columnWidths.length === 0) { - const measuredColumnWidths = [] + requestAnimationFrame(() => { + const measuredColumnWidths = [] - for (const cell of headerRowRef.current.cells) { - measuredColumnWidths.push(cell.offsetWidth) - } - setColumnWidths(measuredColumnWidths) + for (const cell of headerRowRef.current.cells) { + measuredColumnWidths.push(cell.offsetWidth) + } + + setColumnWidths(measuredColumnWidths) + }) } }, [columnWidths]) From 1048506b7d87d589567c3284725590c59fed1537 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 17:55:38 +0100 Subject: [PATCH 41/57] fix: avoid rounding issue which can cause a scrollbar to appear --- src/components/datatable/DataTable.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 156ae2650..d15413ed2 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -192,7 +192,8 @@ const Table = ({ availableHeight, availableWidth }) => { const measuredColumnWidths = [] for (const cell of headerRowRef.current.cells) { - measuredColumnWidths.push(cell.offsetWidth) + const rect = cell.getBoundingClientRect() + measuredColumnWidths.push(Math.floor(rect.width)) } setColumnWidths(measuredColumnWidths) From fc156fd5ba26767500023a54efbeb4fdb9cebb84 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 17:57:21 +0100 Subject: [PATCH 42/57] fix: switch back to automatic table layout when the headers change --- src/components/datatable/DataTable.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index d15413ed2..13e5183d7 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -202,12 +202,12 @@ const Table = ({ availableHeight, availableWidth }) => { }, [columnWidths]) useEffect(() => { - /* When the window is resized or the sidebar opens - * the table needs to switch back to its automatic layout - * so that the cells can subsequently can be measured again - * in the useEffect hook above */ + /* When the window is resized or the sidebar opens the table + * headers change the table needs to switch back to its + * automatic layout so that the cells can subsequently can be + * measured again in the useEffect hook above */ setColumnWidths([]) - }, [availableWidth]) + }, [availableWidth, headers]) if (error) { return
    {error}
    From e3c527ef34687e6cc2d6ec4d6f59616fd5848ab4 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 17:59:56 +0100 Subject: [PATCH 43/57] fix: use DataTableRow instead of tr since it can also accept a ref --- src/components/datatable/DataTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 13e5183d7..78411cd59 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -242,7 +242,7 @@ const Table = ({ availableHeight, availableWidth }) => { }} data={rows} fixedHeaderContent={() => ( -
    + {headers.map(({ name, dataKey, type }, index) => ( { {name} ))} - + )} itemContent={(_, row) => row.map(({ dataKey, value, align }) => ( From 4c01a40f4d16a0c541186bff579c13c7ad473543 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 18:00:42 +0100 Subject: [PATCH 44/57] chore: fix spelling in a comment --- src/components/datatable/DataTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 78411cd59..1ed35c5b0 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -202,8 +202,8 @@ const Table = ({ availableHeight, availableWidth }) => { }, [columnWidths]) useEffect(() => { - /* When the window is resized or the sidebar opens the table - * headers change the table needs to switch back to its + /* When the window is resized, the sidebar opens, or the table + * headers change, the table needs to switch back to its * automatic layout so that the cells can subsequently can be * measured again in the useEffect hook above */ setColumnWidths([]) From 4d9f55821563c76886d2892dc347f116f346e8bd Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 7 Feb 2024 18:07:31 +0100 Subject: [PATCH 45/57] fix: disable text selection in thead --- src/components/datatable/styles/DataTable.module.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/datatable/styles/DataTable.module.css b/src/components/datatable/styles/DataTable.module.css index 65f7382f9..039e3839e 100644 --- a/src/components/datatable/styles/DataTable.module.css +++ b/src/components/datatable/styles/DataTable.module.css @@ -2,11 +2,14 @@ height: 1px; } +.dataTable > :global(thead) { + user-select: none; +} + td.dataCell { padding-top: var(--spacers-dp8); padding-bottom: var(--spacers-dp8); font-size: 11px; - } td.dataCell:hover { From 6fdbe68169ff3e9bf1e996b21735a66fb0ed5bfa Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 8 Feb 2024 10:21:39 +0100 Subject: [PATCH 46/57] fix: switch to ui Input for column filters, handle serverCluster error in hook --- i18n/en.pot | 10 ++++---- src/components/datatable/DataTable.js | 18 -------------- src/components/datatable/FilterInput.js | 24 ++++++------------- .../datatable/styles/DataTable.module.css | 10 +++++++- .../datatable/styles/FilterInput.module.css | 7 ------ src/components/datatable/useTableData.js | 22 ++++++++++------- 6 files changed, 34 insertions(+), 57 deletions(-) delete mode 100644 src/components/datatable/styles/FilterInput.module.css diff --git a/i18n/en.pot b/i18n/en.pot index 1203f17f1..86ee497e1 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-07T14:40:25.874Z\n" -"PO-Revision-Date: 2024-02-07T14:40:25.874Z\n" +"POT-Creation-Date: 2024-02-08T08:51:07.402Z\n" +"PO-Revision-Date: 2024-02-08T08:51:07.402Z\n" msgid "Untitled map, {{date}}" msgstr "Untitled map, {{date}}" @@ -134,9 +134,6 @@ msgstr "Data set" msgid "No results found" msgstr "No results found" -msgid "Data table is not supported when events are grouped on the server." -msgstr "Data table is not supported when events are grouped on the server." - msgid "Sort by {{column}}" msgstr "Sort by {{column}}" @@ -170,6 +167,9 @@ msgstr "Org unit" msgid "Event time" msgstr "Event time" +msgid "Data table is not supported when events are grouped on the server." +msgstr "Data table is not supported when events are grouped on the server." + msgid "No valid data was found for the current layer configuration." msgstr "No valid data was found for the current layer configuration." diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 1ed35c5b0..30e7d6d14 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -213,24 +213,6 @@ const Table = ({ availableHeight, availableWidth }) => { return
    {error}
    } - if (!headers.length) { - return ( -
    - {i18n.t('Data table is not supported for this layer.')} -
    - ) - } - - if (layer.serverCluster) { - return ( -
    - {i18n.t( - 'Data table is not supported when events are grouped on the server.' - )} -
    - ) - } - return ( <> { const dispatch = useDispatch() const dataTable = useSelector((state) => state.dataTable) @@ -24,24 +22,16 @@ const FilterInput = ({ type, dataKey }) => { const filterValue = filters[dataKey] || '' - // https://stackoverflow.com/questions/36683770/react-how-to-get-the-value-of-an-input-field - const onChange = (evt) => { - const value = evt.target.value + const onChange = (value) => + value !== '' + ? dispatch(setDataFilter(layerId, dataKey, value)) + : dispatch(clearDataFilter(layerId, dataKey, value)) - if (value !== '') { - dispatch(setDataFilter(layerId, dataKey, value)) - } else { - dispatch(clearDataFilter(layerId, dataKey, value)) - } - } - - // TODO: Support more field types return ( - 3&<8' : i18n.t('Search')} value={filterValue} - onClick={(evt) => evt.stopPropagation()} onChange={onChange} /> ) diff --git a/src/components/datatable/styles/DataTable.module.css b/src/components/datatable/styles/DataTable.module.css index 039e3839e..b5a3e6e4c 100644 --- a/src/components/datatable/styles/DataTable.module.css +++ b/src/components/datatable/styles/DataTable.module.css @@ -23,6 +23,15 @@ td.lightText { .columnHeader > :global(span.container) { justify-content: space-between; } + +.columnHeader :global(input.dense) { + padding: 4px 6px; +} + +.columnHeader :global(input::placeholder) { + color: var(--colors-grey400); +} + /* Hide the filter icon */ .columnHeader > :global(span.container) @@ -40,7 +49,6 @@ td.lightText { min-height: 40px; } -/* TODO */ .noSupport { position: absolute; top: 50%; diff --git a/src/components/datatable/styles/FilterInput.module.css b/src/components/datatable/styles/FilterInput.module.css deleted file mode 100644 index 773c179ef..000000000 --- a/src/components/datatable/styles/FilterInput.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.filterInput { - width: 100%; -} - -.filterInput::placeholder { - color: #ddd; -} diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index 485ad7688..e814451e3 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -166,7 +166,7 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { } = layer || EMPTY_LAYER const dataWithAggregations = useMemo(() => { - if (!data) { + if (!data || serverCluster) { return null } return data @@ -265,14 +265,18 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { (!aggregations || aggregations === EMPTY_AGGREGATIONS)) || (layerType === EVENT_LAYER && !layer.isExtended && !serverCluster) - const error = - dataWithAggregations === null - ? i18n.t( - 'No valid data was found for the current layer configuration.' - ) - : headers === null - ? i18n.t('Data table is not supported for this layer type.') - : null + let error = null + if (serverCluster) { + error = i18n.t( + 'Data table is not supported when events are grouped on the server.' + ) + } else if (dataWithAggregations === null) { + error = i18n.t( + 'No valid data was found for the current layer configuration.' + ) + } else if (headers === null) { + error = i18n.t('Data table is not supported for this layer type.') + } return { headers, rows, isLoading, error } } From 27471b6392485c756c5fdae7e98ae4bf8e98f816 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 8 Feb 2024 11:02:08 +0100 Subject: [PATCH 47/57] fix: close button styling and position --- src/components/datatable/BottomPanel.js | 4 ++-- .../datatable/styles/BottomPanel.module.css | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 26c445f87..8b9da4c62 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -42,12 +42,12 @@ const BottomPanel = () => { style={{ height: tableHeight, width: tableWidth }} data-test="bottom-panel" > - dispatch(closeDataTable())} > - + Date: Thu, 8 Feb 2024 11:28:08 +0100 Subject: [PATCH 48/57] fix: close icon additional positioning --- src/components/datatable/styles/BottomPanel.module.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/datatable/styles/BottomPanel.module.css b/src/components/datatable/styles/BottomPanel.module.css index 1d40a538e..b97cbc6b9 100644 --- a/src/components/datatable/styles/BottomPanel.module.css +++ b/src/components/datatable/styles/BottomPanel.module.css @@ -9,7 +9,8 @@ .closeIcon { position: absolute; - right: 8px; + top: var(--spacers-dp4); + right: var(--spacers-dp8); z-index: 100; cursor: pointer; color: var(--colors-grey800); From e6a7d404d766874edb2f9462bfa8c77ddf8fc81c Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 8 Feb 2024 11:48:50 +0100 Subject: [PATCH 49/57] fix: destructure value --- src/components/datatable/FilterInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/datatable/FilterInput.js b/src/components/datatable/FilterInput.js index f433cecbb..daae31541 100644 --- a/src/components/datatable/FilterInput.js +++ b/src/components/datatable/FilterInput.js @@ -22,7 +22,7 @@ const FilterInput = ({ type, dataKey }) => { const filterValue = filters[dataKey] || '' - const onChange = (value) => + const onChange = ({ value }) => value !== '' ? dispatch(setDataFilter(layerId, dataKey, value)) : dispatch(clearDataFilter(layerId, dataKey, value)) From 05afd5c20874ea47dbe1093ebb1a8350dbf78b1b Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 8 Feb 2024 15:36:16 +0100 Subject: [PATCH 50/57] fix: cypress test fix --- cypress/integration/dataTable.cy.js | 30 +++++++++---------------- cypress/support/commands.js | 2 +- src/components/datatable/DataTable.js | 1 + src/components/datatable/FilterInput.js | 3 ++- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/cypress/integration/dataTable.cy.js b/cypress/integration/dataTable.cy.js index 01fb5f21e..37ae95492 100644 --- a/cypress/integration/dataTable.cy.js +++ b/cypress/integration/dataTable.cy.js @@ -44,10 +44,8 @@ describe('data table', () => { .should('have.length', 10) // Filter by name - cy.getByDataTest('bottom-panel') - .findByDataTest('dhis2-uicore-datatablecellhead') - .containsExact('Name') - .siblings('input') + cy.getByDataTest('data-table-column-filter-input-Name') + .find('input') .type('bar') // check that the filter returned the correct number of rows @@ -94,10 +92,8 @@ describe('data table', () => { .should('contain', 'Bargbe') // filter by Value (numeric) - cy.getByDataTest('bottom-panel') - .findByDataTest('dhis2-uicore-datatablecellhead') - .containsExact('Value') - .siblings('input') + cy.getByDataTest('data-table-column-filter-input-Value') + .find('input') .type('>26') // check that the (combined) filter returned the correct number of rows @@ -178,24 +174,18 @@ describe('data table', () => { .should('be.visible') // filter by Org unit - cy.getByDataTest('bottom-panel') - .findByDataTest('dhis2-uicore-datatablecellhead') - .containsExact('Org unit') - .siblings('input') + cy.getByDataTest('data-table-column-filter-input-Org unit') + .find('input') .type('Kpetema') // filter by Gender - cy.getByDataTest('bottom-panel') - .findByDataTest('dhis2-uicore-datatablecellhead') - .containsExact('Gender') - .siblings('input') + cy.getByDataTest('data-table-column-filter-input-Gender') + .find('input') .type('Female') // filter by Age in years (numeric) - cy.getByDataTest('bottom-panel') - .findByDataTest('dhis2-uicore-datatablecellhead') - .containsExact('Age in years') - .siblings('input') + cy.getByDataTest('data-table-column-filter-input-Age in years') + .find('input') .type('<11') // check that the filter returned the correct number of rows diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 50b4f5a19..1243879df 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -2,7 +2,7 @@ import '@dhis2/cypress-commands' import 'cypress-wait-until' Cypress.Commands.add('getByDataTest', (selector, ...args) => - cy.get(`[data-test=${selector}]`, ...args) + cy.get(`[data-test="${selector}"]`, ...args) ) Cypress.Commands.add( 'findByDataTest', diff --git a/src/components/datatable/DataTable.js b/src/components/datatable/DataTable.js index 30e7d6d14..cf661531a 100644 --- a/src/components/datatable/DataTable.js +++ b/src/components/datatable/DataTable.js @@ -246,6 +246,7 @@ const Table = ({ availableHeight, availableWidth }) => { ) } diff --git a/src/components/datatable/FilterInput.js b/src/components/datatable/FilterInput.js index daae31541..94a991b57 100644 --- a/src/components/datatable/FilterInput.js +++ b/src/components/datatable/FilterInput.js @@ -5,7 +5,7 @@ import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { setDataFilter, clearDataFilter } from '../../actions/dataFilters.js' -const FilterInput = ({ type, dataKey }) => { +const FilterInput = ({ type, dataKey, name }) => { const dispatch = useDispatch() const dataTable = useSelector((state) => state.dataTable) const map = useSelector((state) => state.map) @@ -29,6 +29,7 @@ const FilterInput = ({ type, dataKey }) => { return ( 3&<8' : i18n.t('Search')} value={filterValue} From 3ac7a66e7f34afa3b90a9d05842efa234f4a9627 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 8 Feb 2024 15:43:12 +0100 Subject: [PATCH 51/57] fix: lint --- src/components/datatable/FilterInput.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/datatable/FilterInput.js b/src/components/datatable/FilterInput.js index 94a991b57..16bab4a9c 100644 --- a/src/components/datatable/FilterInput.js +++ b/src/components/datatable/FilterInput.js @@ -40,6 +40,7 @@ const FilterInput = ({ type, dataKey, name }) => { FilterInput.propTypes = { dataKey: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, type: PropTypes.string.isRequired, } From db8b57e27a566c8cf04c10713b5592b29289b315 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 8 Feb 2024 16:23:49 +0100 Subject: [PATCH 52/57] fix: virtualization means you cant check last row if they arent shown --- cypress/integration/dataTable.cy.js | 45 ++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/cypress/integration/dataTable.cy.js b/cypress/integration/dataTable.cy.js index 37ae95492..aa9c70e91 100644 --- a/cypress/integration/dataTable.cy.js +++ b/cypress/integration/dataTable.cy.js @@ -176,17 +176,54 @@ describe('data table', () => { // filter by Org unit cy.getByDataTest('data-table-column-filter-input-Org unit') .find('input') - .type('Kpetema') + .type('Yakaji') + + // check that all the rows have Org unit Yakaji + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .first() + .find('td') + .eq(1) + .should('contain', 'Yakaji') + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .find('tr') + .last() + .find('td') + .eq(1) + .should('contain', 'Yakaji') + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .should('have.length', 6) // filter by Gender cy.getByDataTest('data-table-column-filter-input-Gender') .find('input') .type('Female') + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .should('have.length', 2) + + cy.getByDataTest('data-table-column-filter-input-Gender') + .find('input') + .clear() + + cy.getByDataTest('bottom-panel') + .findByDataTest('dhis2-uicore-tablebody') + .findByDataTest('dhis2-uicore-datatablerow') + .should('have.length', 6) + // filter by Age in years (numeric) cy.getByDataTest('data-table-column-filter-input-Age in years') .find('input') - .type('<11') + .type('<51') // check that the filter returned the correct number of rows cy.getByDataTest('bottom-panel') @@ -204,7 +241,7 @@ describe('data table', () => { .first() .find('td') .eq(7) - .should('contain', '8') + .should('contain', '50') cy.getByDataTest('bottom-panel') .findByDataTest('dhis2-uicore-tablebody') @@ -212,7 +249,7 @@ describe('data table', () => { .last() .find('td') .eq(7) - .should('contain', '2') + .should('contain', '48') // click on a row cy.getByDataTest('bottom-panel') From 9fdf8aa757e901ddbe9ed6e078c1fa1e91a54b7b Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Fri, 9 Feb 2024 09:05:52 +0100 Subject: [PATCH 53/57] feat: data table control bar --- src/components/datatable/BottomPanel.js | 24 ++++++++++--------- .../datatable/styles/BottomPanel.module.css | 15 ++++++++---- .../datatable/styles/ResizeHandle.module.css | 17 ++++--------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 8b9da4c62..9a4b73d85 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -42,17 +42,19 @@ const BottomPanel = () => { style={{ height: tableHeight, width: tableWidth }} data-test="bottom-panel" > - - dispatch(resizeDataTable(height))} - /> +
    + dispatch(resizeDataTable(height))} + /> + +
    Date: Fri, 9 Feb 2024 09:49:00 +0100 Subject: [PATCH 54/57] chore: final style touches on datatable control bar --- src/components/datatable/styles/BottomPanel.module.css | 4 ++-- src/components/datatable/styles/ResizeHandle.module.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/datatable/styles/BottomPanel.module.css b/src/components/datatable/styles/BottomPanel.module.css index 452b271fa..85fc33e4f 100644 --- a/src/components/datatable/styles/BottomPanel.module.css +++ b/src/components/datatable/styles/BottomPanel.module.css @@ -20,7 +20,7 @@ z-index: 100; cursor: pointer; color: var(--colors-grey800); - background-color: transparent; + background-color: var(--colors-grey100); width: 20px; height: 20px; border: none; @@ -28,7 +28,7 @@ .closeIcon:hover { color: var(--colors-grey900); - background-color: var(--colors-grey100); + background-color: var(--colors-grey300); } .closeIcon svg { diff --git a/src/components/datatable/styles/ResizeHandle.module.css b/src/components/datatable/styles/ResizeHandle.module.css index edbcec7f0..27c0465ab 100644 --- a/src/components/datatable/styles/ResizeHandle.module.css +++ b/src/components/datatable/styles/ResizeHandle.module.css @@ -11,7 +11,7 @@ .resizeHandle:hover, .resizeHandle:active { background-color: var(--colors-grey300); - color: var(--colors-grey800); + color: var(--colors-grey900); } .resizeHandle:active { From 999566dc390c08d33990c2c761913447132dfe54 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Fri, 9 Feb 2024 10:11:00 +0100 Subject: [PATCH 55/57] fix: adjust cypress --- cypress/integration/dataTable.cy.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cypress/integration/dataTable.cy.js b/cypress/integration/dataTable.cy.js index aa9c70e91..7f5717f49 100644 --- a/cypress/integration/dataTable.cy.js +++ b/cypress/integration/dataTable.cy.js @@ -174,9 +174,10 @@ describe('data table', () => { .should('be.visible') // filter by Org unit + const ouName = 'Benduma' cy.getByDataTest('data-table-column-filter-input-Org unit') .find('input') - .type('Yakaji') + .type(ouName) // check that all the rows have Org unit Yakaji @@ -186,7 +187,7 @@ describe('data table', () => { .first() .find('td') .eq(1) - .should('contain', 'Yakaji') + .should('contain', ouName) cy.getByDataTest('bottom-panel') .findByDataTest('dhis2-uicore-tablebody') @@ -194,12 +195,12 @@ describe('data table', () => { .last() .find('td') .eq(1) - .should('contain', 'Yakaji') + .should('contain', ouName) cy.getByDataTest('bottom-panel') .findByDataTest('dhis2-uicore-tablebody') .findByDataTest('dhis2-uicore-datatablerow') - .should('have.length', 6) + .should('have.length', 5) // filter by Gender cy.getByDataTest('data-table-column-filter-input-Gender') @@ -209,7 +210,7 @@ describe('data table', () => { cy.getByDataTest('bottom-panel') .findByDataTest('dhis2-uicore-tablebody') .findByDataTest('dhis2-uicore-datatablerow') - .should('have.length', 2) + .should('have.length', 4) cy.getByDataTest('data-table-column-filter-input-Gender') .find('input') @@ -218,7 +219,7 @@ describe('data table', () => { cy.getByDataTest('bottom-panel') .findByDataTest('dhis2-uicore-tablebody') .findByDataTest('dhis2-uicore-datatablerow') - .should('have.length', 6) + .should('have.length', 5) // filter by Age in years (numeric) cy.getByDataTest('data-table-column-filter-input-Age in years') @@ -241,7 +242,7 @@ describe('data table', () => { .first() .find('td') .eq(7) - .should('contain', '50') + .should('contain', '44') cy.getByDataTest('bottom-panel') .findByDataTest('dhis2-uicore-tablebody') @@ -249,7 +250,7 @@ describe('data table', () => { .last() .find('td') .eq(7) - .should('contain', '48') + .should('contain', '6') // click on a row cy.getByDataTest('bottom-panel') From 0fed9be261f8625e9b9a86e01a5c96b444c90c03 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Fri, 9 Feb 2024 10:44:29 +0100 Subject: [PATCH 56/57] fix: effect dependencies --- src/components/datatable/useTableData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js index e814451e3..42e61bf0c 100644 --- a/src/components/datatable/useTableData.js +++ b/src/components/datatable/useTableData.js @@ -181,7 +181,7 @@ export const useTableData = ({ layer, sortField, sortDirection }) => { index: d.index, i, })) - }, [data, aggregations]) + }, [data, aggregations, serverCluster]) const headers = useMemo(() => { if (dataWithAggregations === null) { From 6ed255192a1994d7ca804429dc38e7d999f58714 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 12 Feb 2024 10:52:25 +0100 Subject: [PATCH 57/57] chore: add ErrorBoundary around the datatable --- i18n/en.pot | 7 ++-- src/components/datatable/BottomPanel.js | 13 +++++--- src/components/datatable/ErrorBoundary.js | 40 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 src/components/datatable/ErrorBoundary.js diff --git a/i18n/en.pot b/i18n/en.pot index 86ee497e1..71c7fd25c 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-08T08:51:07.402Z\n" -"PO-Revision-Date: 2024-02-08T08:51:07.402Z\n" +"POT-Creation-Date: 2024-02-12T09:30:18.362Z\n" +"PO-Revision-Date: 2024-02-12T09:30:18.362Z\n" msgid "Untitled map, {{date}}" msgstr "Untitled map, {{date}}" @@ -137,6 +137,9 @@ msgstr "No results found" msgid "Sort by {{column}}" msgstr "Sort by {{column}}" +msgid "Something went wrong" +msgstr "Something went wrong" + msgid "Search" msgstr "Search" diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 9a4b73d85..31b2be27c 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -8,8 +8,9 @@ import { LAYERS_PANEL_WIDTH, RIGHT_PANEL_WIDTH, } from '../../constants/layout.js' -import DataTable from '../datatable/DataTable.js' import { useWindowDimensions } from '../WindowDimensionsProvider.js' +import DataTable from './DataTable.js' +import ErrorBoundary from './ErrorBoundary.js' import ResizeHandle from './ResizeHandle.js' import styles from './styles/BottomPanel.module.css' @@ -55,10 +56,12 @@ const BottomPanel = () => { - + + + ) } diff --git a/src/components/datatable/ErrorBoundary.js b/src/components/datatable/ErrorBoundary.js new file mode 100644 index 000000000..d9032eab8 --- /dev/null +++ b/src/components/datatable/ErrorBoundary.js @@ -0,0 +1,40 @@ +import i18n from '@dhis2/d2-i18n' +import { CenteredContent } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React, { Component } from 'react' + +class ErrorBoundary extends Component { + constructor(props) { + super(props) + this.state = { + error: null, + errorInfo: null, + } + } + + componentDidCatch(error, errorInfo) { + this.setState({ + error, + errorInfo, + }) + } + + render() { + const { children } = this.props + if (this.state.error) { + return ( + +

    {i18n.t('Something went wrong')}

    +
    + ) + } + + return children + } +} + +ErrorBoundary.propTypes = { + children: PropTypes.node.isRequired, +} + +export default ErrorBoundary