diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js
index c3c90202..1d6e2d83 100644
--- a/demo/js/esri-datasets.js
+++ b/demo/js/esri-datasets.js
@@ -7,102 +7,400 @@ import createDatasetsPlugin from '/plugins/beta/datasets/src/index.js'
import { vtsMapStyles27700 } from './mapStyles.js'
import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } from './auth.js'
+const nonFloodZoneLight = '#2b8cbe'
+const nonFloodZoneDark = '#7fcdbb'
+
+const COLOURS = {
+ // floodExtents: { default: nonFloodZoneLight, dark: nonFloodZoneDark },
+
+ depthOver2300: { default: '#7f2704', dark: '#238b45' },
+ depth2300: { default: '#a63603', dark: '#41ab5d' },
+ depth1200: { default: '#d94801', dark: '#74c476' },
+ depth900: { default: '#f16913', dark: '#a1d99b' },
+ depth600: { default: '#fd8d3c', dark: '#c7e9c0' },
+ depth300: { default: '#fdae6b', dark: '#e5f5e0' },
+ depth150: { default: '#fdd0a2', dark: '#f7fcf5' },
+
+ floodZone3: { default: '#003078', dark: '#e5f5e0' },
+ floodZone2: { default: '#1d70b8', dark: '#41ab5d' },
+ floodZoneClimateChange: { default: '#F4A582', dark: '#BF3D4A' },
+ // floodZoneClimateChangeNoData: { default: darkTeal, dark: white },
+
+ floodDefences: { default: '#f47738', dark: '#f47738' },
+ // waterStorageAreas: { default: darkTeal, dark: white },
+ // mainRivers: { default: darkTeal, dark: white }
+}
+
+// light tones > 2300 to < 150
+const nonFloodZoneDepthBandsLight = [COLOURS.depthOver2300.default, COLOURS.depth2300.default, COLOURS.depth1200.default, COLOURS.depth900.default, COLOURS.depth600.default, COLOURS.depth300.default, COLOURS.depth150.default]
+// GREENS dark tones > 2300 to < 150
+const nonFloodZoneDepthBandsDark = [COLOURS.depthOver2300.dark, COLOURS.depth2300.dark, COLOURS.depth1200.dark, COLOURS.depth900.dark, COLOURS.depth600.dark, COLOURS.depth300.dark, COLOURS.depth150.dark]
+
+
+
const datasetFloodZonesCC = {
- id: 'flood-zones-cc',
- label: 'Flood Zones Climate Change',
- groupLabel: 'Datasets',
- esriGroupId: 'flood-zones',
- tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`,
- showInKey: true,
- showInMenu: true,
- visible: true,
- sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1',
- sublayers: [
- {
- id: 'climate-change',
- label: 'Climate change (2070 to 2125)',
- esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1',
- showInKey: true,
- showInMenu: false,
- style: {
- fill: { outdoor: '#F4A582', dark: '#BF3D4A' },
- stroke: 'none'
- },
- },
- {
- id: 'data-unavailable',
- label: 'Climate change data unavailable',
- style: { // This is used just for the key - so that it renders the pattern correctly.
- fillPattern: 'dot',
- fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' },
- stroke: { outdoor: '#000000', dark: '#FFFFFF' },
- },
- showInKey: true,
- showInMenu: false,
- },
- {
- id: 'data-unavailable-outline',
- style: {
- stroke: { outdoor: '#000000', dark: '#FFFFFF' },
- },
- esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0',
- showInKey: false,
- showInMenu: false,
- },
- {
- id: 'data-unavailable-light',
- visibleWhen: { mapStyleId: ['outdoor', 'black-and-white'] },
- esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1',
- esriUseServerStyle: true,
- showInKey: false,
- showInMenu: false,
- },
- {
- id: 'data-unavailable-dark',
- visibleWhen: { mapStyleId: ['dark'] },
- esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2',
- esriUseServerStyle: true,
- showInKey: false,
- showInMenu: false,
+ id: 'floodzonescc',
+ label: 'Flood Zones Climate Change',
+ groupLabel: 'Datasets',
+ esriGroupId: 'floodzones-group',
+ tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`,
+ showInKey: true,
+ showInMenu: true,
+ visible: true,
+ sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1',
+ sublayers: [
+ {
+ id: 'climate-change',
+ label: 'Climate change (2070 to 2125)',
+ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1',
+ showInKey: true,
+ showInMenu: false,
+ visibleWhen: {
+ menu: {
+ dataset: ['floodzones'], timeframe: ['climatechange']
+ }
+ },
+ style: {
+ fill: { outdoor: '#F4A582', dark: '#BF3D4A' },
+ stroke: 'none'
+ },
+ },
+ {
+ id: 'data-unavailable',
+ label: 'Climate change data unavailable',
+ showInKey: true,
+ showInMenu: false,
+ visibleWhen: {
+ menu: {
+ dataset: ['floodzones'], timeframe: ['climatechange']
+ }
+ },
+ style: { // This is used just for the key - so that it renders the pattern correctly.
+ fillPattern: 'dot',
+ fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' },
+ stroke: { outdoor: '#000000', dark: '#FFFFFF' },
}
- ]
- }
+ },
+ {
+ id: 'data-unavailable-outline',
+ showInKey: false,
+ showInMenu: false,
+ visibleWhen: {
+ menu: { dataset: ['floodzones'], timeframe: ['climatechange'] }
+ },
+ style: {
+ stroke: { outdoor: '#000000', dark: '#FFFFFF' },
+ },
+ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0'
+ },
+ {
+ id: 'data-unavailable-light',
+ visibleWhen: {
+ mapStyleId: ['outdoor', 'black-and-white'],
+ menu: { dataset: ['floodzones'], timeframe: ['climatechange'] }
+ },
+ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1',
+ esriUseServerStyle: true,
+ showInKey: false,
+ showInMenu: false,
+ },
+ {
+ id: 'data-unavailable-dark',
+ visibleWhen: {
+ menu: { dataset: ['floodzones'], timeframe: ['climatechange'] },
+ mapStyleId: ['dark']
+ },
+ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2',
+ esriUseServerStyle: true,
+ showInKey: false,
+ showInMenu: false,
+ }
+ ]
+}
- const datasetFloodZones = {
- id: 'flood-zones',
- label: 'Flood Zones',
- groupLabel: 'Datasets',
- esriGroupId: 'flood-zones',
- tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`,
- showInKey: true,
- // showInMenu: true,
- sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea',
- sublayers: [
- {
- id: 'flood-zone-2',
- label: 'Flood Zone 2',
- esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1',
- showInMenu: true,
- style: {
- fill: { outdoor: '#1d70b8', dark: '#7fcdbb' },
- stroke: 'none'
- },
- },
- {
- id: 'flood-zone-3',
- label: 'Flood Zone 3',
- esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1',
- showInMenu: true,
- style: {
- fill: { outdoor: '#003078', dark: '#e5f5e0' },
- stroke: 'none'
- },
+const datasetFloodZones = {
+ id: 'floodzones',
+ label: 'Flood Zones',
+ groupLabel: 'Datasets',
+ esriGroupId: 'floodzones-group',
+ tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`,
+ showInKey: true,
+ // showInMenu: true,
+ sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea',
+ visibleWhen: {
+ menu: { dataset: ['floodzones'] }
+ },
+ sublayers: [
+ {
+ id: 'flood-zone-2',
+ label: 'Flood Zone 2',
+ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1',
+ style: {
+ fill: { outdoor: '#1d70b8', dark: '#7fcdbb' },
+ stroke: 'none'
+ },
+ },
+ {
+ id: 'flood-zone-3',
+ label: 'Flood Zone 3',
+ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1',
+ style: {
+ fill: { outdoor: '#003078', dark: '#e5f5e0' },
+ stroke: 'none'
+ },
+ }
+ ]
+}
+
+const surfaceWaterDataset = {
+ id: 'surfacewater',
+ label: 'Surface Water',
+ groupLabel: 'Datasets',
+ tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`,
+ showInKey: true,
+ sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ }
+ },
+ sublayers: [
+ {
+ id: 'depthOver2300',
+ label: 'Depth over 2300mm',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300', 'depthOver2300']
+ }
+ },
+ style: {
+ fill: { outdoor: nonFloodZoneDepthBandsLight[0], dark: nonFloodZoneDepthBandsDark[0] },
}
- ]
- }
+ },
+ {
+ id: 'depth2300',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300']
+ }
+ },
+ style: {
+ fill: { outdoor: nonFloodZoneDepthBandsLight[1], dark: nonFloodZoneDepthBandsDark[1] },
+ }
+ },
+ {
+ id: 'depth1200',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200']
+ }
+ },
+ style: {
+ fill: { outdoor: nonFloodZoneDepthBandsLight[2], dark: nonFloodZoneDepthBandsDark[2] },
+ }
+ },
+ {
+ id: 'depth900',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depth150', 'depth300', 'depth600', 'depth900']
+ }
+ },
+ style: {
+ fill: { outdoor: nonFloodZoneDepthBandsLight[3], dark: nonFloodZoneDepthBandsDark[3] },
+ }
+ },
+ {
+ id: 'depth600',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depth150', 'depth300', 'depth600']
+ }
+ },
+ style: {
+ fill: { outdoor: nonFloodZoneDepthBandsLight[4], dark: nonFloodZoneDepthBandsDark[4] },
+ }
+ },
+ {
+ id: 'depth300',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depth150', 'depth300']
+ }
+ },
+ style: {
+ fill: { outdoor: nonFloodZoneDepthBandsLight[5], dark: nonFloodZoneDepthBandsDark[5] },
+ }
+ },
+ {
+ id: 'depth150',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1',
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depth150']
+ }
+ },
+ style: {
+ fill: { outdoor: nonFloodZoneDepthBandsLight[6], dark: nonFloodZoneDepthBandsDark[6] },
+ }
+ },
+ ]
+}
+
+const surfaceWaterDepthAllDataset = {
+ id: 'surfacewaterDepthAll',
+ label: 'Surface Water Depth All',
+ groupLabel: 'Datasets',
+ tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`,
+ showInKey: true,
+ sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths',
+ style: {
+ fill: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark },
+ },
+ visibleWhen: {
+ menu: {
+ dataset: ['surfacewater'],
+ timeframe: ['presentday'],
+ aep: ['low'],
+ depth: ['depthAll']
+ },
+ },
+ sublayers: [
+ {
+ id: 'depthOver2300',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1',
+ },
+ {
+ id: 'depth2300',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1',
+ },
+ {
+ id: 'depth1200',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1',
+ },
+ {
+ id: 'depth900',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1',
+ },
+ {
+ id: 'depth600',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1',
+ },
+ {
+ id: 'depth300',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1',
+ },
+ {
+ id: 'depth150',
+ esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1',
+ }
+ ]
+}
const datasets = [
- datasetFloodZonesCC, datasetFloodZones
+ datasetFloodZonesCC, datasetFloodZones, surfaceWaterDataset, surfaceWaterDepthAllDataset
+]
+
+const menu = [
+ {
+ id: 'dataset',
+ label: 'Datasets',
+ urlKey: 'dataset',
+ visibleWhen: true,
+ type: 'radio', // 'checkbox' or 'radio'
+ value: 'floodzones', // this is the default value for the menu, it should be one of the items' id
+ items: [
+ { id: 'floodzones', label: 'Flood zones' },
+ { id: 'surfacewater', label: 'Surface water' },
+ { id: 'none', label: 'None', },
+ ],
+ },
+ {
+ id: 'timeframe',
+ label: 'Timeframe',
+ urlKey: 'dataset',
+ urlIndex: 1, // eg: surfacewater-presentday-high-depth or floodzones-climatechange
+ type: 'radio',
+ visibleWhen: { menu: { dataset: ['floodzones', 'surfacewater'] } },
+ value: 'presentday',
+ items: [
+ { id: 'presentday', label: 'Present day' },
+ { id: 'climatechange', label: '2070 to 2125', visibleWhen: { menu: { dataset: ['floodzones'] } } },
+ { id: 'climatechange', label: '2061 to 2125', visibleWhen: { menu: { dataset: ['surfacewater'] } } },
+ ]
+ }, {
+ id: 'aep',
+ label: 'Annual likelihood of flooding',
+ urlKey: 'dataset',
+ urlIndex: 2,
+ type: 'radio',
+ visibleWhen: { menu: { dataset: ['surfacewater'] } },
+ value: 'medium',
+ items: [
+ { id: 'high', label: '1 in 30' },
+ { id: 'medium', label: '1 in 100' },
+ { id: 'low', label: '1 in 1000' },
+ ]
+ }, {
+ id: 'depth',
+ label: 'Depth',
+ urlKey: 'dataset',
+ urlIndex: 3,
+ type: 'radio',
+ visibleWhen: { menu: { dataset: ['surfacewater'] } },
+ subMenu: true,
+ value: 'depthAll',
+ items: [
+ { id: 'depthAll', label: 'All depths', },
+ { id: 'depth150', label: 'Full extent of flooding', },
+ { id: 'depth300', label: 'Extent over 150mm', },
+ { id: 'depth600', label: 'Extent over 300mm', },
+ { id: 'depth900', label: 'Extent over 600mm', },
+ { id: 'depth1200', label: 'Extent over 900mm', },
+ { id: 'depth2300', label: 'Extent over 1200mm', },
+ { id: 'depthOver2300', label: 'Extent over 2300mm', },
+ ]
+ }, {
+ id: 'features',
+ label: 'Map features',
+ urlKey: 'features',
+ type: 'checkbox',
+ visibleWhen: true,
+ items: [
+ { id: 'water-storage', label: 'Water storage', checked: false },
+ { id: 'flood-defence', label: 'Flood defence', checked: false },
+ { id: 'main-rivers', label: 'Main rivers', checked: false },
+ ]
+ }
]
const datasetsPlugin = createDatasetsPlugin({
@@ -111,7 +409,8 @@ const datasetsPlugin = createDatasetsPlugin({
opacity: 0.75,
visible: true
},
- datasets
+ datasets,
+ menu
})
const interactiveMap = new InteractiveMap('map', {
@@ -132,8 +431,8 @@ const interactiveMap = new InteractiveMap('map', {
desktop: { slot: 'right-top', showLabel: true }
}],
panels: [
- {
- id: 'mapStyles',
+ {
+ id: 'mapStyles',
desktop: { slot: 'map-styles-button', width: '400px', modal: true }
}
]
@@ -147,9 +446,13 @@ const testGlobalVisibility = () => {
setTimeout(() => datasetsPlugin.setDatasetVisibility(true), 6000)
}
+const testAddRemoveDataset = () => {
+ setTimeout(() => datasetsPlugin.removeDataset('floodzonescc'), 1000)
+ setTimeout(() => datasetsPlugin.removeDataset('floodzones'), 3000)
+ setTimeout(() => datasetsPlugin.addDataset(datasetFloodZones), 5000)
+}
+
interactiveMap.on('datasets:ready', function () {
// testGlobalVisibility()
- setTimeout(() => datasetsPlugin.removeDataset('flood-zones-cc'), 1000)
- setTimeout(() => datasetsPlugin.removeDataset('flood-zones'), 3000)
- setTimeout(() => datasetsPlugin.addDataset(datasetFloodZones), 5000)
+ // testAddRemoveDataset()
})
\ No newline at end of file
diff --git a/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss b/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss
index ce035ba6..da6b1ba0 100644
--- a/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss
+++ b/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss
@@ -51,10 +51,11 @@
}
// GovUK style overide
-.im-c-datasets-layers__item .govuk-checkboxes__item {
+.im-c-datasets-layers__item .govuk-checkboxes__item,
+.im-c-datasets-layers__item .govuk-radios__item {
flex-wrap: nowrap;
- .govuk-checkboxes__input {
+ .govuk-checkboxes__input, .govuk-radios__input {
margin-left: -3px;
}
}
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx
index d9221cef..d3998299 100644
--- a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx
@@ -1,10 +1,12 @@
import React from 'react'
import { setDatasetVisibility } from '../../api/setDatasetVisibility.js'
import { LayersMenuCheckbox } from './LayersMenuCheckbox.jsx'
+import { LayersRadioGroupWrapper } from './LayersRadioGroupWrapper.jsx'
import { LayersMenuGroupWrapper } from './LayersMenuGroupWrapper.jsx'
export const LayersMenu = ({ pluginState }) => {
const { menu = [] } = pluginState
+
const handleDatasetChange = (e) => {
const { value, checked } = e.target
setDatasetVisibility({ pluginState }, checked, { datasetId: value })
@@ -15,18 +17,33 @@ export const LayersMenu = ({ pluginState }) => {
return (
{// Each menuGroup
- menu.map(menuGroup =>
-
- {// Each menuGroupItem
- menuGroup.items.map(menuGroupItem =>
-
- )
- }
- )
+ menu.map(menuGroup => {
+ const { type } = menuGroup
+ if (type === 'checkbox') {
+ return (
+ {
+ // Each menuGroupItem
+ menuGroup.items.map(menuGroupItem => (
+ )
+ )
+ }
+
+ )
+ } else {
+ return (
+
+ )
+ }
+ }
+ )
}
)
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx
new file mode 100644
index 00000000..0799c32b
--- /dev/null
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx
@@ -0,0 +1,175 @@
+import { render } from '@testing-library/react'
+import { LayersMenu } from './LayersMenu'
+import { setDatasetVisibility } from '../../api/setDatasetVisibility.js'
+
+jest.mock('../../api/setDatasetVisibility.js', () => ({
+ setDatasetVisibility: jest.fn()
+}))
+
+let capturedOnChange = null
+
+jest.mock('./LayersMenuCheckbox.jsx', () => ({
+ LayersMenuCheckbox: ({ menuGroupItem, onChange }) => {
+ capturedOnChange = onChange
+ return (
+
+ )
+ }
+}))
+
+jest.mock('./LayersRadioGroupWrapper.jsx', () => ({
+ LayersRadioGroupWrapper: ({ menuGroup }) => (
+
+ )
+}))
+
+jest.mock('./LayersMenuGroupWrapper.jsx', () => ({
+ LayersMenuGroupWrapper: ({ menuGroup, children }) => (
+
+ {children}
+
+ )
+}))
+
+const makePluginState = (menu = [], extra = {}) => ({
+ menu,
+ dispatch: jest.fn(),
+ ...extra
+})
+
+describe('LayersMenu', () => {
+ beforeEach(() => {
+ setDatasetVisibility.mockClear()
+ })
+
+ describe('container class', () => {
+ it('renders the base container class when no groups have a groupLabel', () => {
+ const pluginState = makePluginState([{ id: 'g1', type: 'checkbox', items: [] }])
+ const { container } = render()
+ const div = container.firstChild
+ expect(div.className).toBe('im-c-datasets-layers')
+ })
+
+ it('adds the --has-groups modifier when at least one group has a groupLabel', () => {
+ const pluginState = makePluginState([{ id: 'g1', type: 'checkbox', groupLabel: 'My Group', items: [] }])
+ const { container } = render()
+ const div = container.firstChild
+ expect(div.className).toBe('im-c-datasets-layers im-c-datasets-layers--has-groups')
+ })
+ })
+
+ describe('empty menu', () => {
+ it('renders the container with no children when menu is empty', () => {
+ const pluginState = makePluginState([])
+ const { container } = render()
+ expect(container.firstChild.children).toHaveLength(0)
+ })
+
+ it('uses an empty array when menu is not provided', () => {
+ const { container } = render()
+ expect(container.firstChild.children).toHaveLength(0)
+ })
+ })
+
+ describe('checkbox groups', () => {
+ it('renders a LayersMenuGroupWrapper for each checkbox group', () => {
+ const pluginState = makePluginState([
+ { id: 'g1', type: 'checkbox', items: [] },
+ { id: 'g2', type: 'checkbox', items: [] }
+ ])
+ const { getAllByTestId } = render()
+ expect(getAllByTestId('layers-menu-group-wrapper')).toHaveLength(2)
+ })
+
+ it('renders a LayersMenuCheckbox for each item in a checkbox group', () => {
+ const pluginState = makePluginState([
+ { id: 'g1', type: 'checkbox', items: [{ id: 'item-a' }, { id: 'item-b' }] }
+ ])
+ const { getAllByTestId } = render()
+ expect(getAllByTestId('layers-menu-checkbox')).toHaveLength(2)
+ })
+
+ it('passes the correct group to LayersMenuGroupWrapper', () => {
+ const group = { id: 'group-x', type: 'checkbox', items: [] }
+ const pluginState = makePluginState([group])
+ const { getByTestId } = render()
+ expect(getByTestId('layers-menu-group-wrapper').dataset.groupId).toBe('group-x')
+ })
+
+ it('passes the correct item to LayersMenuCheckbox', () => {
+ const pluginState = makePluginState([
+ { id: 'g1', type: 'checkbox', items: [{ id: 'item-1' }] }
+ ])
+ const { getByTestId } = render()
+ expect(getByTestId('layers-menu-checkbox').dataset.itemId).toBe('item-1')
+ })
+ })
+
+ describe('radio groups', () => {
+ it('renders a LayersRadioGroupWrapper for non-checkbox groups', () => {
+ const pluginState = makePluginState([
+ { id: 'r1', type: 'radio', items: [{ id: 'opt1' }, { id: 'opt2' }] }
+ ])
+ const { getAllByTestId } = render()
+ expect(getAllByTestId('layers-radio-group-wrapper')).toHaveLength(1)
+ })
+
+ it('passes the correct group to LayersRadioGroupWrapper', () => {
+ const pluginState = makePluginState([
+ { id: 'radio-group-1', type: 'radio', items: [] }
+ ])
+ const { getByTestId } = render()
+ expect(getByTestId('layers-radio-group-wrapper').dataset.groupId).toBe('radio-group-1')
+ })
+
+ it('does not render a LayersMenuGroupWrapper for radio groups', () => {
+ const pluginState = makePluginState([{ id: 'r1', type: 'radio', items: [] }])
+ const { queryByTestId } = render()
+ expect(queryByTestId('layers-menu-group-wrapper')).toBeNull()
+ })
+ })
+
+ describe('mixed groups', () => {
+ it('renders both checkbox and radio wrappers when both types are present', () => {
+ const pluginState = makePluginState([
+ { id: 'c1', type: 'checkbox', items: [] },
+ { id: 'r1', type: 'radio', items: [] }
+ ])
+ const { getByTestId } = render()
+ expect(getByTestId('layers-menu-group-wrapper')).toBeTruthy()
+ expect(getByTestId('layers-radio-group-wrapper')).toBeTruthy()
+ })
+ })
+
+ describe('handleDatasetChange', () => {
+ it('calls setDatasetVisibility with the correct arguments when a checkbox changes', () => {
+ const pluginState = makePluginState([
+ { id: 'g1', type: 'checkbox', items: [{ id: 'dataset-abc' }] }
+ ])
+ render()
+ capturedOnChange({ target: { value: 'dataset-abc', checked: true } })
+ expect(setDatasetVisibility).toHaveBeenCalledTimes(1)
+ expect(setDatasetVisibility).toHaveBeenCalledWith(
+ { pluginState },
+ true,
+ { datasetId: 'dataset-abc' }
+ )
+ })
+
+ it('passes checked: false to setDatasetVisibility when unchecking', () => {
+ const pluginState = makePluginState([
+ { id: 'g1', type: 'checkbox', items: [{ id: 'dataset-xyz' }] }
+ ])
+ render()
+ capturedOnChange({ target: { value: 'dataset-xyz', checked: false } })
+ expect(setDatasetVisibility).toHaveBeenCalledWith(
+ { pluginState },
+ false,
+ { datasetId: 'dataset-xyz' }
+ )
+ })
+ })
+})
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx
new file mode 100644
index 00000000..c0c7841e
--- /dev/null
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx
@@ -0,0 +1,117 @@
+import { render, screen, act } from '@testing-library/react'
+import { datasetRegistry } from '../../registry/datasetRegistry.js'
+import { LayersMenuCheckbox } from './LayersMenuCheckbox'
+
+jest.mock('../../registry/datasetRegistry.js', () => ({
+ datasetRegistry: {
+ getDataset: jest.fn()
+ }
+}))
+
+const onChange = jest.fn()
+const menuGroupItem = { id: 'dataset-1' }
+
+const baseDataset = {
+ id: 'dataset-1',
+ label: 'Dataset One',
+ visible: true,
+ isLocallyVisible: true,
+ isSublayer: false,
+ parentId: undefined
+}
+
+beforeEach(() => {
+ datasetRegistry.getDataset.mockReset()
+ onChange.mockReset()
+})
+
+describe('LayersMenuCheckbox', () => {
+ describe('when the dataset is not in the registry', () => {
+ it('returns null', () => {
+ datasetRegistry.getDataset.mockReturnValue(undefined)
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+ })
+
+ describe('when the dataset exists', () => {
+ it('renders a checkbox input', () => {
+ datasetRegistry.getDataset.mockReturnValue(baseDataset)
+ const { container } = render()
+ expect(container.querySelector('input[type="checkbox"]')).toBeTruthy()
+ })
+
+ it('renders the dataset label', () => {
+ datasetRegistry.getDataset.mockReturnValue(baseDataset)
+ render()
+ expect(screen.getByText('Dataset One')).toBeTruthy()
+ })
+
+ it('associates the label with the input via htmlFor', () => {
+ datasetRegistry.getDataset.mockReturnValue(baseDataset)
+ const { container } = render()
+ const label = container.querySelector('label')
+ const input = container.querySelector('input')
+ expect(label.htmlFor).toBe(input.id)
+ })
+
+ it('calls onChange when the checkbox changes', () => {
+ datasetRegistry.getDataset.mockReturnValue(baseDataset)
+ const { container } = render()
+ const input = container.querySelector('input')
+ const propsKey = Object.keys(input).find(k => k.startsWith('__reactProps'))
+ act(() => { input[propsKey].onChange({ target: { checked: true, value: input.value } }) })
+ expect(onChange).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('checked state', () => {
+ it('is checked when isLocallyVisible is true', () => {
+ datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isLocallyVisible: true })
+ const { container } = render()
+ expect(container.querySelector('input').checked).toBe(true)
+ })
+
+ it('is unchecked when isLocallyVisible is false', () => {
+ datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isLocallyVisible: false })
+ const { container } = render()
+ expect(container.querySelector('input').checked).toBe(false)
+ })
+ })
+
+ describe('item class', () => {
+ it('does not include the --checked modifier when visible is true', () => {
+ datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, visible: true })
+ const { container } = render()
+ expect(container.firstChild.className).not.toContain('im-c-datasets-layers__item--checked')
+ })
+
+ it('includes the --checked modifier when visible is false', () => {
+ datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, visible: false })
+ const { container } = render()
+ expect(container.firstChild.className).toContain('im-c-datasets-layers__item--checked')
+ })
+ })
+
+ describe('data attributes', () => {
+ it('uses the dataset id as data-dataset-id when not a sublayer', () => {
+ datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isSublayer: false })
+ const { container } = render()
+ const input = container.querySelector('input')
+ expect(input.dataset.datasetId).toBe('dataset-1')
+ expect(input.dataset.sublayerId).toBeUndefined()
+ })
+
+ it('uses parentId as data-dataset-id when a sublayer', () => {
+ datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, id: 'sub-1', isSublayer: true, parentId: 'parent-1' })
+ const { container } = render()
+ expect(container.querySelector('input').dataset.datasetId).toBe('parent-1')
+ })
+
+ it('uses the sublayer id as data-sublayer-id when a sublayer', () => {
+ datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, id: 'sub-1', isSublayer: true, parentId: 'parent-1' })
+ const { container } = render()
+ expect(container.querySelector('input').dataset.sublayerId).toBe('sub-1')
+ })
+ })
+})
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx
index 8467486a..912948ac 100644
--- a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx
@@ -2,9 +2,6 @@ export const LayersMenuGroupWrapper = ({ menuGroup, children }) => {
if (!menuGroup.groupLabel) {
return <>{children}>
}
- // TODO - is there any need for im-c-datasets-layers-group--items-checked - it isn't used anywhere
- // const anyItemsChecked = false
- // const wrapperClass = `govuk-form-group im-c-datasets-layers-group${anyItemsChecked ? ' im-c-datasets-layers-group--items-checked' : ''}`
const wrapperClass = 'govuk-form-group im-c-datasets-layers-group'
return (
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx
new file mode 100644
index 00000000..45865a3e
--- /dev/null
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx
@@ -0,0 +1,68 @@
+import { render, screen } from '@testing-library/react'
+import { LayersMenuGroupWrapper } from './LayersMenuGroupWrapper'
+
+const child =
Child content
+
+describe('LayersMenuGroupWrapper', () => {
+ describe('without a groupLabel', () => {
+ it('renders children directly without a wrapping div', () => {
+ const { getByTestId, container } = render(
+
{child}
+ )
+ expect(getByTestId('child')).toBeTruthy()
+ expect(container.querySelector('.govuk-form-group')).toBeNull()
+ })
+
+ it('does not render a fieldset when groupLabel is absent', () => {
+ const { container } = render(
+
{child}
+ )
+ expect(container.querySelector('fieldset')).toBeNull()
+ })
+
+ it('does not render a fieldset when groupLabel is an empty string', () => {
+ const { container } = render(
+
{child}
+ )
+ expect(container.querySelector('fieldset')).toBeNull()
+ })
+ })
+
+ describe('with a groupLabel', () => {
+ it('renders the outer wrapper with the correct classes', () => {
+ const { container } = render(
+
{child}
+ )
+ const wrapper = container.querySelector('.govuk-form-group.im-c-datasets-layers-group')
+ expect(wrapper).toBeTruthy()
+ })
+
+ it('renders a fieldset with the correct class', () => {
+ const { container } = render(
+
{child}
+ )
+ expect(container.querySelector('fieldset.im-c-datasets-layers-group__fieldset')).toBeTruthy()
+ })
+
+ it('renders the groupLabel as the legend text', () => {
+ render(
+
{child}
+ )
+ expect(screen.getByText('My Group')).toBeTruthy()
+ })
+
+ it('renders the legend with the correct class', () => {
+ const { container } = render(
+
{child}
+ )
+ expect(container.querySelector('legend.im-c-datasets-layers-group__legend')).toBeTruthy()
+ })
+
+ it('renders children inside the fieldset', () => {
+ const { getByTestId } = render(
+
{child}
+ )
+ expect(getByTestId('child')).toBeTruthy()
+ })
+ })
+})
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx
new file mode 100644
index 00000000..2b74090e
--- /dev/null
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx
@@ -0,0 +1,28 @@
+import { isVisibleWhen } from '../../registry/isVisibleWhen.js'
+
+export const LayersMenuRadio = ({ menuState, menuGroupItem, checked, name, onChange }) => {
+ const itemClass = 'im-c-datasets-layers__item govuk-radios govuk-radios--small"'
+ const { visibleWhen } = menuGroupItem
+ const visible = visibleWhen ? isVisibleWhen(visibleWhen) : true
+ if (!visible) {
+ return null
+ }
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx
new file mode 100644
index 00000000..55b2c15a
--- /dev/null
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx
@@ -0,0 +1,105 @@
+import { render, screen, act } from '@testing-library/react'
+import { isVisibleWhen } from '../../registry/isVisibleWhen.js'
+import { LayersMenuRadio } from './LayersMenuRadio'
+
+jest.mock('../../registry/isVisibleWhen.js', () => ({
+ isVisibleWhen: jest.fn()
+}))
+
+const onChange = jest.fn()
+const baseItem = { id: 'radio-1', label: 'Option One' }
+
+beforeEach(() => {
+ isVisibleWhen.mockReturnValue(true)
+ onChange.mockReset()
+})
+
+describe('LayersMenuRadio', () => {
+ describe('visibility', () => {
+ it('renders when visibleWhen is not set', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('input[type="radio"]')).toBeTruthy()
+ })
+
+ it('does not call isVisibleWhen when visibleWhen is not set', () => {
+ render(
)
+ expect(isVisibleWhen).not.toHaveBeenCalled()
+ })
+
+ it('calls isVisibleWhen with the visibleWhen value', () => {
+ const visibleWhen = { menu: ['someValue'] }
+ render(
)
+ expect(isVisibleWhen).toHaveBeenCalledWith(visibleWhen)
+ })
+
+ it('returns null when isVisibleWhen resolves to false', () => {
+ isVisibleWhen.mockReturnValue(false)
+ const { container } = render(
+
+ )
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders when isVisibleWhen resolves to true', () => {
+ isVisibleWhen.mockReturnValue(true)
+ const { container } = render(
+
+ )
+ expect(container.querySelector('input[type="radio"]')).toBeTruthy()
+ })
+ })
+
+ describe('rendered output', () => {
+ it('renders the item label text', () => {
+ render(
)
+ expect(screen.getByText('Option One')).toBeTruthy()
+ })
+
+ it('sets the name attribute on the input', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('input').name).toBe('my-radio-group')
+ })
+
+ it('sets checked to true when checked prop is true', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('input').checked).toBe(true)
+ })
+
+ it('sets checked to false when checked prop is false', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('input').checked).toBe(false)
+ })
+
+ it('sets the input value to the item id', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('input').value).toBe('radio-1')
+ })
+
+ it('associates the label with the input via htmlFor', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('label').htmlFor).toBe('radio-1')
+ })
+
+ it('calls onChange when the radio input changes', () => {
+ const { container } = render(
+
+ )
+ const input = container.querySelector('input')
+ const propsKey = Object.keys(input).find(k => k.startsWith('__reactProps'))
+ act(() => { input[propsKey].onChange({ target: { value: 'radio-1' } }) })
+ expect(onChange).toHaveBeenCalledTimes(1)
+ })
+ })
+})
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx
new file mode 100644
index 00000000..493d5143
--- /dev/null
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx
@@ -0,0 +1,40 @@
+import React, { useState } from 'react'
+import { isVisibleWhen } from '../../registry/isVisibleWhen.js'
+import { LayersMenuRadio } from './LayersMenuRadio.jsx'
+
+export const LayersRadioGroupWrapper = ({ pluginState, menuGroup }) => {
+ const { id, items, visibleWhen } = menuGroup
+ const visible = visibleWhen ? isVisibleWhen(visibleWhen) : true
+ if (!visible) {
+ return null
+ }
+
+ const { menuState, dispatch } = pluginState
+ const [value, setValue] = useState(menuState[id])
+ const handleChange = (event) => {
+ setValue(event.target.value)
+ dispatch({ type: 'UPDATE_MENU_STATE', payload: { [id]: event.target.value } })
+ }
+
+ const wrapperClass = 'govuk-form-group im-c-datasets-layers-group'
+ return (
+
+
+
+ )
+}
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx
new file mode 100644
index 00000000..24095d3e
--- /dev/null
+++ b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx
@@ -0,0 +1,136 @@
+import { render, screen, act } from '@testing-library/react'
+import { isVisibleWhen } from '../../registry/isVisibleWhen.js'
+import { LayersRadioGroupWrapper } from './LayersRadioGroupWrapper'
+
+jest.mock('../../registry/isVisibleWhen.js', () => ({
+ isVisibleWhen: jest.fn()
+}))
+
+let capturedOnChange = null
+
+jest.mock('./LayersMenuRadio.jsx', () => ({
+ LayersMenuRadio: ({ menuGroupItem, checked, name, onChange }) => {
+ capturedOnChange = onChange
+ return (
+
+ )
+ }
+}))
+
+const makePluginState = (menuState = {}) => ({
+ menuState,
+ dispatch: jest.fn()
+})
+
+const baseGroup = {
+ id: 'group-1',
+ label: 'My Group',
+ items: [{ id: 'opt-a' }, { id: 'opt-b' }]
+}
+
+beforeEach(() => {
+ isVisibleWhen.mockClear()
+ isVisibleWhen.mockReturnValue(true)
+ capturedOnChange = null
+})
+
+describe('LayersRadioGroupWrapper', () => {
+ describe('visibility', () => {
+ it('returns null when isVisibleWhen resolves to false', () => {
+ isVisibleWhen.mockReturnValue(false)
+ const { container } = render(
+
+ )
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders when visibleWhen is not set', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.govuk-form-group')).toBeTruthy()
+ })
+
+ it('does not call isVisibleWhen when visibleWhen is not set', () => {
+ render(
)
+ expect(isVisibleWhen).not.toHaveBeenCalled()
+ })
+
+ it('calls isVisibleWhen with the visibleWhen value', () => {
+ const visibleWhen = { menu: ['someValue'] }
+ render(
)
+ expect(isVisibleWhen).toHaveBeenCalledWith(visibleWhen)
+ })
+ })
+
+ describe('rendered output', () => {
+ it('renders the group label as the legend', () => {
+ render(
)
+ expect(screen.getByText('My Group')).toBeTruthy()
+ })
+
+ it('renders a LayersMenuRadio for each item', () => {
+ const { getAllByTestId } = render(
+
+ )
+ expect(getAllByTestId('layers-menu-radio')).toHaveLength(2)
+ })
+
+ it('passes the group id as the name to each radio item', () => {
+ const { getAllByTestId } = render(
+
+ )
+ getAllByTestId('layers-menu-radio').forEach(radio => {
+ expect(radio.dataset.name).toBe('group-1')
+ })
+ })
+ })
+
+ describe('checked state', () => {
+ it('marks the item matching menuState as checked', () => {
+ const { getAllByTestId } = render(
+
+ )
+ const radios = getAllByTestId('layers-menu-radio')
+ expect(radios[0].dataset.checked).toBe('true')
+ expect(radios[1].dataset.checked).toBe('false')
+ })
+
+ it('marks no item as checked when menuState has no value for the group', () => {
+ const { getAllByTestId } = render(
+
+ )
+ getAllByTestId('layers-menu-radio').forEach(radio => {
+ expect(radio.dataset.checked).toBe('false')
+ })
+ })
+ })
+
+ describe('handleChange', () => {
+ it('dispatches UPDATE_MENU_STATE with the selected item id', () => {
+ const pluginState = makePluginState({ 'group-1': 'opt-a' })
+ render(
)
+ act(() => { capturedOnChange({ target: { value: 'opt-b' } }) })
+ expect(pluginState.dispatch).toHaveBeenCalledWith({
+ type: 'UPDATE_MENU_STATE',
+ payload: { 'group-1': 'opt-b' }
+ })
+ })
+
+ it('dispatches using the group id as the payload key', () => {
+ const group = { ...baseGroup, id: 'another-group' }
+ const pluginState = makePluginState({ 'another-group': 'opt-a' })
+ render(
)
+ act(() => { capturedOnChange({ target: { value: 'opt-b' } }) })
+ expect(pluginState.dispatch).toHaveBeenCalledWith({
+ type: 'UPDATE_MENU_STATE',
+ payload: { 'another-group': 'opt-b' }
+ })
+ })
+ })
+})
diff --git a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx
index 36964be1..81f88be4 100755
--- a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx
+++ b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx
@@ -3,6 +3,7 @@ import { useEffect, useRef } from 'react'
import { EVENTS } from '../../../../../src/config/events.js'
import { initialiseDatasets } from './initialiseDatasets.js'
import { datasetRegistry } from '../registry/datasetRegistry.js'
+import { setMenuState } from '../registry/isVisibleWhen.js'
import { attachGlobalState } from '../registry/globalDataset.js'
import { loadLayerAdapter, layerAdapter } from '../adapters/loadLayerAdapter.js'
@@ -49,6 +50,8 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m
initDatasets()
}, [isBaseMapReady, appState.mode])
+ useEffect(() => setMenuState(pluginState.menuState), [pluginState.menuState])
+
useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets),
[pluginState.mappedDatasets, pluginState.orderedDatasets])
diff --git a/plugins/beta/datasets/src/initialise/defaults.js b/plugins/beta/datasets/src/initialise/defaults.js
index 7153f2dd..62b6e66e 100644
--- a/plugins/beta/datasets/src/initialise/defaults.js
+++ b/plugins/beta/datasets/src/initialise/defaults.js
@@ -7,7 +7,6 @@ const datasetDefaults = {
style: {
stroke: '#d4351c',
strokeWidth: 2,
- fill: 'transparent',
symbolDescription: 'red outline'
}
}
diff --git a/plugins/beta/datasets/src/initialise/defaults.test.js b/plugins/beta/datasets/src/initialise/defaults.test.js
index 626add11..6e25a271 100644
--- a/plugins/beta/datasets/src/initialise/defaults.test.js
+++ b/plugins/beta/datasets/src/initialise/defaults.test.js
@@ -15,7 +15,6 @@ describe('datasetDefaults', () => {
expect(datasetDefaults.style).toMatchObject({
stroke: '#d4351c',
strokeWidth: 2,
- fill: 'transparent',
symbolDescription: 'red outline'
})
})
diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.js
index ecd97f1f..cda72920 100644
--- a/plugins/beta/datasets/src/initialise/initialiseDatasets.js
+++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.js
@@ -3,6 +3,9 @@ import { createDynamicSource } from '../fetch/createDynamicSource.js'
import { applyDatasetDefaults, datasetDefaults } from './defaults.js'
import { mappedDatasetsReducer } from '../reducers/mappedDatasetsReducer.js'
import { datasetRegistry } from '../registry/datasetRegistry.js'
+import { setMenuState } from '../registry/isVisibleWhen.js'
+import { buildMenuState } from '../reducers/menuStateReducer.js'
+import { datasetsToMenu } from '../reducers/datasetsToMenu.js'
export const initialiseDatasets = ({
adapter,
@@ -28,7 +31,10 @@ export const initialiseDatasets = ({
datasetRegistry.attachCreateDataset(adapter.createDataset)
}
datasetRegistry.attach(mappedDatasets, orderedDatasets, mapStyle)
- adapter.init(mapStyle).then(() => {
+ const menu = pluginConfig.menu || datasetsToMenu({ datasets: processedDatasets })
+ setMenuState(buildMenuState(menu)) // Must be called before adapter.init so that menuState is set before any datasets are checked for visibility
+
+ adapter.init().then(() => {
datasetRegistry.forEachDataset(registryDataset => {
if (!registryDataset.hasDynamicGeoJSON) {
return
@@ -43,6 +49,7 @@ export const initialiseDatasets = ({
})
adapter.attachDynamicSources(dynamicSources)
// TODO - apply dynamic source defaults here, and include in mappedDatasets
+ dispatch({ type: 'SET_MENU', payload: { menu } })
dispatch({ type: 'SET_DATASETS', payload: { datasets: processedDatasets, mappedDatasets, orderedDatasets } })
eventBus.emit('datasets:ready')
})
diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js
index 78302805..9b1005dc 100644
--- a/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js
+++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js
@@ -61,7 +61,7 @@ describe('initialiseDatasets', () => {
it('calls adapter.init with mapStyle', () => {
const args = makeArgs()
initialiseDatasets(args)
- expect(args.adapter.init).toHaveBeenCalledWith(args.mapStyle)
+ expect(args.adapter.init).toHaveBeenCalledWith()
})
it('attaches mappedDatasets to datasetRegistry', () => {
diff --git a/plugins/beta/datasets/src/reducers/menuStateReducer.js b/plugins/beta/datasets/src/reducers/menuStateReducer.js
new file mode 100644
index 00000000..839e771b
--- /dev/null
+++ b/plugins/beta/datasets/src/reducers/menuStateReducer.js
@@ -0,0 +1,9 @@
+export const buildMenuState = (menu) => {
+ const menuState = {}
+ menu.forEach(menuGroup => {
+ if (menuGroup.type === 'radio') {
+ menuState[menuGroup.id] = menuGroup.value || menuGroup.items?.[0].value
+ }
+ })
+ return menuState
+}
diff --git a/plugins/beta/datasets/src/reducers/pluginState.js b/plugins/beta/datasets/src/reducers/pluginState.js
index cee81ca7..33f894a7 100755
--- a/plugins/beta/datasets/src/reducers/pluginState.js
+++ b/plugins/beta/datasets/src/reducers/pluginState.js
@@ -1,4 +1,5 @@
-import { datasetsToMenu, addDatasetToMenu, removeDatasetsFromMenu } from './datasetsToMenu.js'
+import { addDatasetToMenu, removeDatasetsFromMenu } from './datasetsToMenu.js'
+import { buildMenuState } from './menuStateReducer.js'
import { mappedDatasetsReducer } from './mappedDatasetsReducer.js'
import { logger } from '../../../../../src/services/logger.js'
@@ -15,7 +16,9 @@ const initialState = {
items: [],
hasGroups: false
},
- actionsArray: []
+ actionsArray: [],
+ menu: [],
+ menuState: {}
}
const validateDatasetExists = (state, datasetId, prefix, suffix = 'not found') => {
@@ -41,14 +44,31 @@ const setGlobalState = (state, payload) => {
})
}
+const updateMenuState = (state, payload) => {
+ return addAction('applyGlobalVisibility', [], {
+ ...state,
+ menuState: { ...state.menuState, ...payload }
+ })
+}
+
+const setMenu = (state, payload) => {
+ const { menu } = payload
+ // build the initial menuState for radios from the menu
+ const menuState = buildMenuState(menu)
+ return {
+ ...state,
+ menu,
+ menuState
+ }
+}
+
const setDatasets = (state, payload) => {
- const { datasets, mappedDatasets, orderedDatasets } = payload
- const menu = payload.menu || datasetsToMenu({ datasets })
+ const { mappedDatasets, orderedDatasets } = payload
+
return {
...state,
mappedDatasets,
- orderedDatasets,
- menu
+ orderedDatasets
}
}
@@ -93,6 +113,9 @@ const setDatasetVisibility = (state, payload) => {
if (!validateDatasetExists(state, datasetId, 'setDatasetVisibility')) {
return state
}
+ if (state.mappedDatasets[datasetId].visible === visible) {
+ return state
+ }
return addAction('applyDatasetVisibility', [datasetId, visible], {
...state,
@@ -197,7 +220,9 @@ const actions = {
HIDE_FEATURES: hideFeatures,
SHOW_FEATURES: showFeatures,
REMOVE_ADAPTER_ACTIONS: removeAdapterActions,
- SET_GLOBAL_STATE: setGlobalState
+ SET_GLOBAL_STATE: setGlobalState,
+ SET_MENU: setMenu,
+ UPDATE_MENU_STATE: updateMenuState
}
export {
diff --git a/plugins/beta/datasets/src/reducers/pluginState.test.js b/plugins/beta/datasets/src/reducers/pluginState.test.js
index 9a608ef8..c0d92753 100644
--- a/plugins/beta/datasets/src/reducers/pluginState.test.js
+++ b/plugins/beta/datasets/src/reducers/pluginState.test.js
@@ -54,13 +54,12 @@ describe('SET_DATASETS', () => {
const payload = {
datasets: [{ id: 'parks', label: 'Parks', showInMenu: true }],
mappedDatasets: { parks: { id: 'parks', label: 'Parks' } },
- orderedDatasets: ['parks'],
- menu: [{ id: 'parks', label: 'Parks', type: 'item' }]
+ orderedDatasets: ['parks']
}
const result = actions.SET_DATASETS(state, payload)
expect(result.mappedDatasets).toEqual(payload.mappedDatasets)
expect(result.orderedDatasets).toEqual(payload.orderedDatasets)
- expect(result.menu).toEqual(payload.menu)
+ expect(result.menu).toEqual([])
})
it('derives menu from datasets when menu is not in payload', () => {
diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js
index a8bd7423..36d1122d 100644
--- a/plugins/beta/datasets/src/registry/dataset.js
+++ b/plugins/beta/datasets/src/registry/dataset.js
@@ -1,4 +1,5 @@
import { datasetRegistry } from './datasetRegistry.js'
+import { isVisibleWhen } from './isVisibleWhen.js'
import { hasCustomVisualStyle } from '../initialise/defaults.js'
import { hasPattern } from '../../../../../src/utils/patternUtils.js'
import { DynamicGeoJson } from './dynamicGeoJson.js'
@@ -96,9 +97,9 @@ export class Dataset {
get esriStyleLayerId () { return this._datasetDefinition.esriStyleLayerId }
get visibility () {
- const visible = this.visible
- if (visible && this.visibleWhen?.mapStyleId) {
- return this.visibleWhen?.mapStyleId.includes(datasetRegistry.mapStyle.id) ? 'visible' : 'none'
+ const { visible, visibleWhen } = this
+ if (visible && visibleWhen) {
+ return isVisibleWhen(visibleWhen) ? 'visible' : 'none'
}
return visible ? 'visible' : 'none'
}
diff --git a/plugins/beta/datasets/src/registry/isVisibleWhen.js b/plugins/beta/datasets/src/registry/isVisibleWhen.js
new file mode 100644
index 00000000..b2fd1cb9
--- /dev/null
+++ b/plugins/beta/datasets/src/registry/isVisibleWhen.js
@@ -0,0 +1,55 @@
+import { datasetRegistry } from './datasetRegistry.js'
+
+let _menuState = {}
+export const setMenuState = (menuState) => (_menuState = menuState)
+
+const _isVisibleWhenMenuCheck = (menuVisibleWhen) => {
+ for (const [key, valueArray] of Object.entries(menuVisibleWhen)) {
+ const menuValue = _menuState[key]
+ if (!valueArray.includes(menuValue)) {
+ return false
+ }
+ }
+ return true
+}
+
+const _isVisibleWhenMapStyleCheck = (visibleWhenValue) => {
+ const mapStylesArray = Array.isArray(visibleWhenValue) ? visibleWhenValue : [visibleWhenValue]
+ return mapStylesArray.includes(_getMapStyleId())
+}
+
+// exported so it can be mocked and overridden when testing
+export const _getMapStyleId = () => datasetRegistry.mapStyle.id
+
+/**
+ * receives a visibleWhen boolean or object
+ * and returns a boolean indicating whether the dataset should be visible
+ * if visibleWhen is undefined, it returns true
+ * if visibleWhen is a boolean, it returns that boolean
+ * if visibleWhen is an object, it checks the properties of the object, against the relevant pluginState properties,
+ * and returns true if all properties are satisfied:
+ * @param {boolean|object} visibleWhen - the visibleWhen property of a dataset
+ * @returns {boolean} - true if the dataset should be visible, false otherwise
+ */
+export const isVisibleWhen = (visibleWhen) => {
+ if (visibleWhen === undefined || visibleWhen === null) {
+ return true
+ }
+ if (typeof visibleWhen === 'boolean') {
+ return visibleWhen
+ }
+ if (typeof visibleWhen === 'object') {
+ // check each property of the visibleWhen object against the relevant pluginState property
+ for (const [visibleWhenKey, visibleWhenValue] of Object.entries(visibleWhen)) {
+ if (visibleWhenKey === 'mapStyleId' && !_isVisibleWhenMapStyleCheck(visibleWhenValue)) {
+ return false
+ }
+ if (visibleWhenKey === 'menu' && !_isVisibleWhenMenuCheck(visibleWhenValue)) {
+ return false
+ }
+ }
+ return true
+ }
+ // Fallback to true if visibleWhen is incorrectly configured
+ return true
+}
diff --git a/plugins/beta/datasets/src/registry/isVisibleWhen.test.js b/plugins/beta/datasets/src/registry/isVisibleWhen.test.js
new file mode 100644
index 00000000..8d35bdd0
--- /dev/null
+++ b/plugins/beta/datasets/src/registry/isVisibleWhen.test.js
@@ -0,0 +1,67 @@
+import { isVisibleWhen, setMenuState } from './isVisibleWhen.js'
+import { datasetRegistry } from './datasetRegistry.js'
+
+describe('isVisibleWhen', () => {
+ beforeEach(() => {
+ // reset menu state before each test
+ setMenuState({})
+ })
+
+ it('returns true if visibleWhen is undefined', () => {
+ expect(isVisibleWhen(undefined)).toBe(true)
+ })
+
+ it('returns true if visibleWhen is null', () => {
+ expect(isVisibleWhen(null)).toBe(true)
+ })
+
+ it('returns the boolean value if visibleWhen is a boolean', () => {
+ expect(isVisibleWhen(true)).toBe(true)
+ expect(isVisibleWhen(false)).toBe(false)
+ })
+
+ it('returns true if all properties of visibleWhen object are satisfied', () => {
+ setMenuState({ datasets: 'floodZones', timeframe: 'climateChange' })
+ const visibleWhen = { menu: { datasets: ['floodZones'], timeframe: ['climateChange'] } }
+ expect(isVisibleWhen(visibleWhen)).toBe(true)
+ })
+
+ it('returns false if any property of visibleWhen object is not satisfied', () => {
+ setMenuState({ datasets: 'floodZones', timeframe: 'climateChange' })
+ const visibleWhen = { menu: { datasets: ['floodZones'], timeframe: ['presentDay'] } }
+ expect(isVisibleWhen(visibleWhen)).toBe(false)
+ })
+
+ it('returns true if mapStyleId is satisfied', () => {
+ const mapStyleId = 'outdoor'
+ datasetRegistry.attach([], [], { id: mapStyleId })
+ const visibleWhen = { mapStyleId }
+ expect(isVisibleWhen(visibleWhen)).toBe(true)
+ })
+
+ it('returns false if mapStyleId is not satisfied', () => {
+ const mapStyleId = 'outdoor'
+ datasetRegistry.attach([], [], { id: mapStyleId })
+ const visibleWhen = { mapStyleId: 'dark' }
+ expect(isVisibleWhen(visibleWhen)).toBe(false)
+ })
+
+ it('returns true if mapStyleId is an array and one of the values is satisfied', () => {
+ const mapStyleId = 'outdoor'
+ datasetRegistry.attach([], [], { id: mapStyleId })
+ const visibleWhen = { mapStyleId: ['dark', mapStyleId] }
+ expect(isVisibleWhen(visibleWhen)).toBe(true)
+ })
+
+ it('returns false if mapStyleId is an array and none of the values are satisfied', () => {
+ const mapStyleId = 'outdoor'
+ datasetRegistry.attach([], [], { id: mapStyleId })
+ const visibleWhen = { mapStyleId: ['dark', 'black-and-white'] }
+ expect(isVisibleWhen(visibleWhen)).toBe(false)
+ })
+
+ it('returns true if visibleWhen is incorrectly configured', () => {
+ const visibleWhen = 'incorrectly-configured'
+ expect(isVisibleWhen(visibleWhen)).toBe(true)
+ })
+})