setTableContextMenu(null)}
+ />
>
)
}
Table.propTypes = {
availableWidth: PropTypes.number,
+ onCountChange: PropTypes.func,
}
export default Table
diff --git a/src/components/datatable/TableContextMenu.jsx b/src/components/datatable/TableContextMenu.jsx
new file mode 100644
index 000000000..79692e01c
--- /dev/null
+++ b/src/components/datatable/TableContextMenu.jsx
@@ -0,0 +1,178 @@
+import i18n from '@dhis2/d2-i18n'
+import {
+ Popover,
+ Menu,
+ MenuItem,
+ IconArrowDown16,
+ IconArrowUp16,
+ IconInfo16,
+} from '@dhis2/ui'
+import PropTypes from 'prop-types'
+import React, { useRef } from 'react'
+import { useDispatch } from 'react-redux'
+import { highlightFeature, setFeatureProfile } from '../../actions/feature.js'
+import { updateLayer } from '../../actions/layers.js'
+import { setOrgUnitProfile } from '../../actions/orgUnits.js'
+import {
+ BOUNDARY_LAYER,
+ EVENT_LAYER,
+ FACILITY_LAYER,
+ GEOJSON_URL_LAYER,
+} from '../../constants/layers.js'
+import { getGeojsonFeatureProfile } from '../../util/geojson.js'
+import { drillUpDown } from '../../util/map.js'
+import { useCachedData } from '../cachedDataProvider/CachedDataProvider.jsx'
+import { IconZoomIn16 } from '../core/icons.jsx'
+
+const TableContextMenu = ({ contextMenu, layer, onClose }) => {
+ const anchorRef = useRef()
+ const dispatch = useDispatch()
+ const {
+ systemSettings: { keyAnalysisDigitGroupSeparator },
+ } = useCachedData()
+
+ if (!contextMenu) {
+ return null
+ }
+
+ const { x, y, featureProps } = contextMenu
+ const layerType = layer.layer
+
+ const {
+ id,
+ level,
+ hasCoordinatesUp,
+ hasCoordinatesDown,
+ grandParentId,
+ grandParentParentGraph,
+ parentGraph,
+ } = featureProps || {}
+
+ const canDrill =
+ layerType !== BOUNDARY_LAYER &&
+ layerType !== FACILITY_LAYER &&
+ layerType !== EVENT_LAYER &&
+ layerType !== GEOJSON_URL_LAYER
+
+ const canViewProfile = id && layerType !== EVENT_LAYER
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+TableContextMenu.propTypes = {
+ layer: PropTypes.object.isRequired,
+ onClose: PropTypes.func.isRequired,
+ contextMenu: PropTypes.shape({
+ featureProps: PropTypes.object,
+ x: PropTypes.number,
+ y: PropTypes.number,
+ }),
+}
+
+export default TableContextMenu
diff --git a/src/components/datatable/styles/BottomPanel.module.css b/src/components/datatable/styles/BottomPanel.module.css
index 68ce43eab..2d5dbaffb 100644
--- a/src/components/datatable/styles/BottomPanel.module.css
+++ b/src/components/datatable/styles/BottomPanel.module.css
@@ -18,32 +18,116 @@
.dataTableControls {
width: 100%;
- height: 20px;
+ height: 36px;
background-color: var(--colors-grey100);
position: relative;
+ display: flex;
+ align-items: center;
+ padding: 0 var(--spacers-dp4);
+ gap: var(--spacers-dp8);
+ flex-shrink: 0;
+ border-bottom: 1px solid var(--colors-grey300);
}
-.closeIcon {
+.layerName {
+ font-weight: 500;
+ font-size: 12px;
+ color: var(--colors-grey800);
+ flex: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+
+.rowCount {
+ font-size: 11px;
+ color: var(--colors-grey600);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+@keyframes tooltipExpandRight {
+ from {
+ clip-path: inset(0 100% 0 0);
+ }
+ to {
+ clip-path: inset(0 0% 0 0);
+ }
+}
+
+.nameTooltip {
+ animation: tooltipExpandRight 160ms ease-out;
+ background: var(--colors-white);
+ border-radius: 3px;
+ -webkit-mask-image: linear-gradient(to left, transparent, black 2em);
+ mask-image: linear-gradient(to left, transparent, black 2em);
+ padding: 0 2em 0 0;
+ pointer-events: none;
+ position: fixed;
+ white-space: nowrap;
+ z-index: 1000;
+}
+
+.filteredIcon {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+}
+
+.clearBadge {
position: absolute;
- top: 0;
- right: 2px;
- z-index: 100;
+ bottom: 0px;
+ right: 0px;
+ width: 8px;
+ height: 8px;
+ background: var(--colors-grey100);
+}
+
+.clearFiltersButton:hover .clearBadge {
+ background: var(--colors-grey300);
+}
+
+.clearBadge::before,
+.clearBadge::after {
+ content: '';
+ position: absolute;
+ width: 5px;
+ height: 1px;
+ background: currentColor;
+ top: 50%;
+ left: 50%;
+}
+
+.clearBadge::before {
+ transform: translate(-50%, -50%) rotate(45deg);
+}
+
+.clearBadge::after {
+ transform: translate(-50%, -50%) rotate(-45deg);
+}
+
+.clearFiltersButton,
+.closeIcon {
cursor: pointer;
color: var(--colors-grey800);
- background-color: var(--colors-grey100);
- width: 20px;
- height: 20px;
+ background-color: transparent;
+ width: 24px;
+ height: 24px;
border: none;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ padding: 0;
}
+.clearFiltersButton:hover,
.closeIcon:hover {
color: var(--colors-grey900);
background-color: var(--colors-grey300);
}
-
-.closeIcon svg {
- position: absolute;
- top: 50%;
- left: 50%;
- margin: -8px 0 0 -8px;
-}
diff --git a/src/components/datatable/styles/DataTable.module.css b/src/components/datatable/styles/DataTable.module.css
index c82cf0239..65a5b32ef 100644
--- a/src/components/datatable/styles/DataTable.module.css
+++ b/src/components/datatable/styles/DataTable.module.css
@@ -24,6 +24,36 @@ td.lightText {
justify-content: space-between;
}
+.headerContent {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ gap: 2px;
+}
+
+.sortButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ border-radius: 4px;
+ background: transparent;
+ cursor: pointer;
+}
+
+.sortButton:hover,
+.sortButton:focus-visible {
+ background: var(--colors-grey400);
+}
+
+.sortButton:focus {
+ outline: none;
+}
+
.columnHeader :global(input.dense) {
padding: 4px 6px;
}
diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js
index ccbc96ab8..12d8ebe2b 100644
--- a/src/components/datatable/useTableData.js
+++ b/src/components/datatable/useTableData.js
@@ -385,10 +385,15 @@ export const useTableData = ({ layer, sortField, sortDirection }) => {
(!aggregations || aggregations === EMPTY_AGGREGATIONS)) ||
(layerType === EVENT_LAYER && !layer.isExtended && !serverCluster)
+ const totalCount = dataWithAggregations?.length ?? 0
+ const filteredCount = rows?.length ?? 0
+
return {
headers,
rows,
isLoading,
error: getErrorCodeText(errorCode.current),
+ totalCount,
+ filteredCount,
}
}
diff --git a/src/components/map/ContextMenu.jsx b/src/components/map/ContextMenu.jsx
index f938129dd..a7a425529 100644
--- a/src/components/map/ContextMenu.jsx
+++ b/src/components/map/ContextMenu.jsx
@@ -11,6 +11,7 @@ import {
import PropTypes from 'prop-types'
import React, { Fragment, useRef } from 'react'
import { connect } from 'react-redux'
+import { highlightFeature, setFeatureProfile } from '../../actions/feature.js'
import { updateLayer } from '../../actions/layers.js'
import {
closeContextMenu,
@@ -20,14 +21,21 @@ import {
import { setOrgUnitProfile } from '../../actions/orgUnits.js'
import {
FACILITY_LAYER,
+ GEOJSON_URL_LAYER,
EARTH_ENGINE_LAYER,
RENDERING_STRATEGY_SPLIT_BY_PERIOD,
} from '../../constants/layers.js'
+import { getGeojsonFeatureProfile } from '../../util/geojson.js'
import { drillUpDown } from '../../util/map.js'
+import { useCachedData } from '../cachedDataProvider/CachedDataProvider.jsx'
+import { IconZoomIn16 } from '../core/icons.jsx'
import styles from './styles/ContextMenu.module.css'
const ContextMenu = (props) => {
const anchorRef = useRef()
+ const {
+ systemSettings: { keyAnalysisDigitGroupSeparator },
+ } = useCachedData()
const {
feature,
@@ -38,9 +46,11 @@ const ContextMenu = (props) => {
position,
offset,
closeContextMenu,
+ highlightFeature,
openCoordinatePopup,
showEarthEngineValue,
setOrgUnitProfile,
+ setFeatureProfile,
updateLayer,
} = props
@@ -81,7 +91,17 @@ const ContextMenu = (props) => {
)
break
case 'show_info':
- setOrgUnitProfile(attr.id)
+ if (layerType === GEOJSON_URL_LAYER) {
+ setFeatureProfile(
+ getGeojsonFeatureProfile(
+ { properties: attr },
+ layerConfig.name,
+ keyAnalysisDigitGroupSeparator
+ )
+ )
+ } else {
+ setOrgUnitProfile(attr.id)
+ }
break
case 'show_coordinate':
openCoordinatePopup(coordinates)
@@ -89,6 +109,14 @@ const ContextMenu = (props) => {
case 'show_ee_value':
showEarthEngineValue(id, coordinates)
break
+ case 'zoom_to_feature':
+ highlightFeature({
+ id: attr.id,
+ layerId: layerConfig.id,
+ origin: 'map',
+ zoom: true,
+ })
+ break
default:
}
@@ -109,25 +137,29 @@ const ContextMenu = (props) => {
>