@defra/interactive-map 0.0.39-alpha → 0.0.41-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (387) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/control-definition.md +9 -1
  7. package/docs/api/slots.md +26 -5
  8. package/docs/api.md +4 -4
  9. package/docs/assets/images/slot-map.svg +61 -262
  10. package/docs/examples/draw-tools.mdx +3 -3
  11. package/docs/plugins/draw.md +80 -17
  12. package/docs/plugins/search.md +1 -1
  13. package/govuk-prototype-kit.config.json +2 -0
  14. package/jest.setup.js +5 -0
  15. package/package.json +9 -1
  16. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  19. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  21. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  22. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  23. package/plugins/datasets/dist/css/index.css +1 -1
  24. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  25. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  26. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  28. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  29. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  30. package/plugins/datasets/dist/umd/index.js +1 -1
  31. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +2 -3
  32. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  33. package/plugins/datasets/src/adapters/esri/registry/esriDataset.js +1 -1
  34. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  35. package/plugins/datasets/src/adapters/loadLayerAdapter.js +1 -1
  36. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  37. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +1 -3
  38. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +17 -3
  39. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  40. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  41. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  42. package/plugins/datasets/src/datasets.scss +0 -1
  43. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  44. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  45. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  46. package/plugins/datasets/src/index.test.js +36 -0
  47. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  48. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  49. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -5
  50. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  51. package/plugins/datasets/src/manifest.js +0 -38
  52. package/plugins/datasets/src/manifest.test.js +49 -0
  53. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  54. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  55. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  56. package/plugins/datasets/src/registry/dataset.js +15 -4
  57. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  58. package/plugins/datasets/src/registry/datasetRegistry.js +12 -9
  59. package/plugins/datasets/src/registry/datasetRegistry.test.js +17 -5
  60. package/plugins/datasets/src/registry/globalDataset.js +0 -3
  61. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  62. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  63. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  64. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  65. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  66. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  67. package/plugins/draw/dist/esm/index.js +1 -1
  68. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  69. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  70. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  72. package/plugins/draw/dist/umd/index.js +1 -1
  73. package/plugins/draw/src/DrawInit.jsx +12 -4
  74. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  75. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +92 -12
  76. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  77. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  78. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  79. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  81. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +1 -1
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +1 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +5 -28
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -54
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +11 -4
  110. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  112. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  114. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  116. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  117. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  130. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  132. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  133. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  135. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  137. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +1 -0
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +27 -1
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +24 -4
  140. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +47 -3
  141. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +1 -1
  142. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +32 -1
  143. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  144. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +34 -1
  145. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +12 -5
  148. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +26 -3
  149. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  150. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  151. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  152. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  153. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  154. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  155. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  156. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  162. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  163. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  164. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  165. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  166. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  167. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  168. package/plugins/draw/src/api/addFeature.js +16 -1
  169. package/plugins/draw/src/api/addFeature.test.js +62 -5
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newPoint.js +42 -0
  173. package/plugins/draw/src/api/newPoint.test.js +117 -0
  174. package/plugins/draw/src/api/setStyle.js +43 -0
  175. package/plugins/draw/src/api/setStyle.test.js +75 -0
  176. package/plugins/draw/src/events.js +21 -14
  177. package/plugins/draw/src/events.test.js +15 -1
  178. package/plugins/draw/src/manifest.js +18 -6
  179. package/plugins/draw/src/manifest.test.js +32 -1
  180. package/plugins/draw/src/reducer.test.js +7 -0
  181. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  182. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  183. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  184. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  185. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  186. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  187. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  188. package/plugins/interact/dist/umd/index.js +1 -1
  189. package/plugins/interact/src/hooks/useCrossHairVisibility.js +12 -4
  190. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +88 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/utils/featureQueries.js +18 -0
  194. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  195. package/plugins/interact/src/utils/spatial.js +9 -3
  196. package/plugins/interact/src/utils/spatial.test.js +12 -1
  197. package/plugins/map-key/dist/css/index.css +62 -0
  198. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  199. package/plugins/map-key/dist/esm/index.js +1 -0
  200. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  202. package/plugins/map-key/dist/umd/index.js +2 -0
  203. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  204. package/plugins/map-key/package.json +28 -0
  205. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  206. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  207. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  208. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  209. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  210. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  212. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  214. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  215. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  216. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  217. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  218. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  220. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  221. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  222. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  224. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  225. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  226. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  228. package/plugins/map-key/src/index.js +13 -0
  229. package/plugins/map-key/src/index.test.js +31 -0
  230. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  232. package/plugins/map-key/src/manifest.js +30 -0
  233. package/plugins/map-key/src/manifest.test.js +63 -0
  234. package/plugins/map-key/src/mapKey.scss +4 -0
  235. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  237. package/plugins/map-key/src/registry/index.js +3 -0
  238. package/plugins/map-key/src/registry/index.test.js +15 -0
  239. package/plugins/menu/dist/css/index.css +80 -0
  240. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  241. package/plugins/menu/dist/esm/index.js +1 -0
  242. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  243. package/plugins/menu/dist/umd/index.js +2 -0
  244. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  245. package/plugins/menu/package.json +28 -0
  246. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  247. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  249. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  250. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  251. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  252. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  254. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  255. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  257. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  258. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  259. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  260. package/plugins/menu/src/index.js +13 -0
  261. package/plugins/menu/src/index.test.js +31 -0
  262. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  263. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  264. package/plugins/menu/src/initialise/createMenu.js +10 -0
  265. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  266. package/plugins/menu/src/manifest.js +35 -0
  267. package/plugins/menu/src/manifest.test.js +63 -0
  268. package/plugins/menu/src/menu.scss +4 -0
  269. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  270. package/plugins/menu/src/reducers/pluginState.js +35 -0
  271. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  272. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  274. package/plugins/menu/src/registry/index.js +3 -0
  275. package/plugins/menu/src/registry/index.test.js +15 -0
  276. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  278. package/plugins/search/dist/css/index.css +1 -1
  279. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  280. package/plugins/search/dist/esm/index.js +1 -1
  281. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  282. package/plugins/search/dist/umd/index.js +1 -1
  283. package/plugins/search/src/Search.jsx +7 -5
  284. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  285. package/plugins/search/src/index.js +0 -1
  286. package/plugins/search/src/index.test.js +2 -4
  287. package/plugins/search/src/manifest.js +1 -1
  288. package/plugins/search/src/search.scss +17 -16
  289. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  290. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  292. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  294. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  296. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  298. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  299. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/index.js +1 -1
  301. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  302. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  303. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  304. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  305. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  306. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  307. package/providers/maplibre/src/utils/patternImages.js +1 -1
  308. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  309. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  310. package/rollup.esm.mjs +11 -1
  311. package/sonar-project.properties +7 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Attributions/Attributions.module.scss +10 -0
  314. package/src/App/components/Hints/Hints.jsx +7 -2
  315. package/src/App/components/Hints/Hints.test.jsx +13 -0
  316. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +2 -3
  317. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +3 -6
  318. package/src/App/components/{MoveControl/MoveControl.jsx → MoveControls/MoveControls.jsx} +10 -10
  319. package/src/App/components/{MoveControl/MoveControl.module.scss → MoveControls/MoveControls.module.scss} +60 -29
  320. package/src/App/components/{MoveControl/MoveControl.test.jsx → MoveControls/MoveControls.test.jsx} +48 -48
  321. package/src/App/components/Panel/Panel.jsx +18 -3
  322. package/src/App/components/Panel/Panel.module.scss +18 -8
  323. package/src/App/components/Panel/Panel.test.jsx +34 -0
  324. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  325. package/src/App/components/Viewport/Viewport.jsx +9 -1
  326. package/src/App/components/Viewport/Viewport.test.jsx +11 -0
  327. package/src/App/hooks/useFeatureItems.js +16 -7
  328. package/src/App/hooks/useFeatureItems.test.js +57 -0
  329. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  330. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  331. package/src/App/initialiseApp.js +11 -8
  332. package/src/App/initialiseApp.test.js +15 -3
  333. package/src/App/layout/Layout.jsx +12 -11
  334. package/src/App/layout/Layout.test.jsx +2 -0
  335. package/src/App/layout/layout.module.scss +31 -17
  336. package/src/App/registry/keyboardShortcutRegistry.js +25 -13
  337. package/src/App/registry/keyboardShortcutRegistry.test.js +49 -32
  338. package/src/App/registry/pluginRegistry.js +2 -3
  339. package/src/App/registry/pluginRegistry.test.js +16 -3
  340. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  341. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  342. package/src/App/renderer/mapButtons.js +28 -12
  343. package/src/App/renderer/mapButtons.test.js +17 -4
  344. package/src/App/renderer/mapControls.js +3 -1
  345. package/src/App/renderer/mapControls.test.js +16 -0
  346. package/src/App/renderer/mapPanels.js +44 -9
  347. package/src/App/renderer/mapPanels.test.js +51 -2
  348. package/src/App/renderer/orderItems.js +28 -0
  349. package/src/App/renderer/orderItems.test.js +50 -0
  350. package/src/App/renderer/slotAggregator.js +2 -18
  351. package/src/App/renderer/slotHelpers.js +3 -1
  352. package/src/App/renderer/slotHelpers.test.js +5 -0
  353. package/src/App/renderer/slots.js +3 -0
  354. package/src/App/store/AppProvider.jsx +4 -2
  355. package/src/App/store/appDispatchMiddleware.js +1 -1
  356. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  357. package/src/InteractiveMap/InteractiveMap.js +8 -2
  358. package/src/InteractiveMap/InteractiveMap.test.js +20 -3
  359. package/src/config/appConfig.js +17 -15
  360. package/src/config/appConfig.test.js +22 -22
  361. package/src/config/defaults.js +3 -1
  362. package/src/config/events.js +2 -0
  363. package/src/config/mergeConfig.js +7 -0
  364. package/src/config/mergeConfig.test.js +20 -0
  365. package/src/scss/main.scss +1 -1
  366. package/src/scss/settings/_colors.scss +1 -0
  367. package/src/scss/settings/_dimensions.scss +2 -3
  368. package/src/scss/settings/_transition.scss +1 -1
  369. package/src/services/eventBus.js +44 -0
  370. package/src/services/eventBus.test.js +83 -0
  371. package/src/services/symbolRegistry.js +1 -1
  372. package/src/test-utils.js +5 -0
  373. package/src/types.js +25 -7
  374. package/webpack.umd.mjs +2 -0
  375. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  376. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  377. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  378. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  379. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  380. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  381. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  382. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  383. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  384. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  385. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  386. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  387. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -0,0 +1,83 @@
1
+ import { fetchGeoJSON } from './fetchGeoJSON.js'
2
+
3
+ const baseUrl = 'https://api.example.com/features'
4
+ const context = { bbox: [-1, -1, 1, 1], zoom: 10 }
5
+ const signal = { aborted: false }
6
+
7
+ const mockResponse = ({ ok = true, status = 200, statusText = 'OK', body = {} } = {}) => ({
8
+ ok,
9
+ status,
10
+ statusText,
11
+ json: async () => body
12
+ })
13
+
14
+ beforeEach(() => {
15
+ global.fetch = jest.fn()
16
+ })
17
+
18
+ describe('fetchGeoJSON', () => {
19
+ it('calls transformRequest with the base URL and context, then fetches the returned url/headers', async () => {
20
+ const featureCollection = { type: 'FeatureCollection', features: [] }
21
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: featureCollection }))
22
+ const transformRequest = jest.fn(() => ({ url: `${baseUrl}?bbox=${context.bbox.join(',')}`, headers: { Authorization: 'Bearer token' } }))
23
+
24
+ const result = await fetchGeoJSON(baseUrl, context, transformRequest, signal)
25
+
26
+ expect(transformRequest).toHaveBeenCalledWith(baseUrl, context)
27
+ expect(fetch).toHaveBeenCalledWith(
28
+ `${baseUrl}?bbox=-1,-1,1,1`,
29
+ { headers: { Authorization: 'Bearer token' }, signal }
30
+ )
31
+ expect(result).toBe(featureCollection)
32
+ })
33
+
34
+ it('supports transformRequest returning a plain URL string, defaulting headers to {}', async () => {
35
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: { type: 'FeatureCollection', features: [] } }))
36
+ const transformRequest = () => baseUrl
37
+
38
+ await fetchGeoJSON(baseUrl, context, transformRequest, signal)
39
+
40
+ expect(fetch).toHaveBeenCalledWith(baseUrl, { headers: {}, signal })
41
+ })
42
+
43
+ it('defaults headers to {} when transformRequest returns an object without headers', async () => {
44
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: { type: 'FeatureCollection', features: [] } }))
45
+ const transformRequest = () => ({ url: baseUrl })
46
+
47
+ await fetchGeoJSON(baseUrl, context, transformRequest, signal)
48
+
49
+ expect(fetch).toHaveBeenCalledWith(baseUrl, { headers: {}, signal })
50
+ })
51
+
52
+ it('throws when the response is not ok', async () => {
53
+ global.fetch.mockResolvedValueOnce(mockResponse({ ok: false, status: 500, statusText: 'Server Error' }))
54
+
55
+ await expect(fetchGeoJSON(baseUrl, context, () => baseUrl, signal))
56
+ .rejects.toThrow('Failed to fetch GeoJSON: 500 Server Error')
57
+ })
58
+
59
+ it('wraps a single Feature into a FeatureCollection', async () => {
60
+ const feature = { type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] } }
61
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: feature }))
62
+
63
+ const result = await fetchGeoJSON(baseUrl, context, () => baseUrl, signal)
64
+
65
+ expect(result).toEqual({ type: 'FeatureCollection', features: [feature] })
66
+ })
67
+
68
+ it('wraps a bare array of features into a FeatureCollection', async () => {
69
+ const features = [{ type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] } }]
70
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: features }))
71
+
72
+ const result = await fetchGeoJSON(baseUrl, context, () => baseUrl, signal)
73
+
74
+ expect(result).toEqual({ type: 'FeatureCollection', features })
75
+ })
76
+
77
+ it('throws for an unrecognized response shape', async () => {
78
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: { type: 'NotGeoJSON' } }))
79
+
80
+ await expect(fetchGeoJSON(baseUrl, context, () => baseUrl, signal))
81
+ .rejects.toThrow('Invalid GeoJSON response')
82
+ })
83
+ })
@@ -0,0 +1,36 @@
1
+ import createPlugin from './index.js'
2
+
3
+ jest.mock('./datasets.scss', () => {})
4
+ jest.mock('./manifest.js', () => ({
5
+ manifest: { InitComponent: jest.fn(), reducer: {} }
6
+ }))
7
+
8
+ describe('createPlugin', () => {
9
+ it('returns a plugin with id "datasets"', () => {
10
+ const plugin = createPlugin()
11
+ expect(plugin.id).toBe('datasets')
12
+ })
13
+
14
+ it('applies default noKeyItemText', () => {
15
+ const plugin = createPlugin()
16
+ expect(plugin.noKeyItemText).toBe('No features displayed')
17
+ })
18
+
19
+ it('merges provided options (options override defaults, id stays fixed)', () => {
20
+ const plugin = createPlugin({ noKeyItemText: 'Custom text', extra: true })
21
+ expect(plugin.noKeyItemText).toBe('Custom text')
22
+ expect(plugin.extra).toBe(true)
23
+ expect(plugin.id).toBe('datasets')
24
+ })
25
+
26
+ it('exposes a load function', () => {
27
+ const plugin = createPlugin()
28
+ expect(typeof plugin.load).toBe('function')
29
+ })
30
+
31
+ it('load() resolves to the manifest', async () => {
32
+ const plugin = createPlugin()
33
+ const result = await plugin.load()
34
+ expect(result).toBeDefined()
35
+ })
36
+ })
@@ -3,15 +3,27 @@ import { useEffect, useRef } from 'react'
3
3
  import { EVENTS } from '../../../../src/config/events.js'
4
4
  import { initialiseDatasets } from './initialiseDatasets.js'
5
5
  import { datasetRegistry } from '../registry/datasetRegistry.js'
6
- import { setMenuState } from '../registry/isVisibleWhen.js'
7
6
  import { attachGlobalState } from '../registry/globalDataset.js'
8
7
  import { loadLayerAdapter, layerAdapter } from '../adapters/loadLayerAdapter.js'
9
8
 
9
+ const pluginConfigHasMenuItems = (pluginConfig) =>
10
+ pluginConfig.datasets
11
+ .some(dataset => dataset.showInMenu || dataset.sublayers?.some(sublayer => sublayer.showInMenu))
12
+
10
13
  export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, mapProvider, services }) {
11
14
  const { dispatch } = pluginState
12
15
  const { eventBus, symbolRegistry, patternRegistry } = services
13
16
  const isBaseMapReady = Boolean(mapProvider?.isBaseMapReady())
14
17
 
18
+ useEffect(() => {
19
+ const hasMenu = pluginConfig.hasMenu !== false && pluginConfigHasMenuItems(pluginConfig)
20
+
21
+ if (!hasMenu) {
22
+ eventBus.emit(EVENTS.APP_REMOVE_PANEL, 'datasetsLayers')
23
+ eventBus.emit(EVENTS.APP_TOGGLE_BUTTON_STATE, { id: 'datasetsLayers', prop: 'hidden', value: true })
24
+ }
25
+ }, [pluginConfig.hasMenu])
26
+
15
27
  // Keep a ref to the latest pluginState so event handlers can access current data
16
28
  const pluginStateRef = useRef(pluginState)
17
29
  pluginStateRef.current = pluginState
@@ -51,12 +63,10 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m
51
63
  }, [isBaseMapReady, appState.mode])
52
64
 
53
65
  useEffect(() => {
54
- setMenuState(pluginState.menuState)
55
- datasetRegistry.invalidateKeyItems()
56
- }, [pluginState.menuState])
57
-
58
- useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets),
59
- [pluginState.mappedDatasets, pluginState.orderedDatasets])
66
+ datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets)
67
+ eventBus.emit('datasets:changed')
68
+ },
69
+ [pluginState.mappedDatasets, pluginState.orderedDatasets])
60
70
 
61
71
  useEffect(() => {
62
72
  datasetRegistry.attachMapStyle(mapState.mapStyle)
@@ -0,0 +1,262 @@
1
+ import { render, act } from '@testing-library/react'
2
+ import { DatasetsInit } from './DatasetsInit.jsx'
3
+ import { datasetRegistry } from '../registry/datasetRegistry.js'
4
+ import { attachGlobalState } from '../registry/globalDataset.js'
5
+ import { loadLayerAdapter, layerAdapter } from '../adapters/loadLayerAdapter.js'
6
+ import { initialiseDatasets } from './initialiseDatasets.js'
7
+ import { EVENTS } from '../../../../src/config/events.js'
8
+
9
+ jest.mock('../registry/datasetRegistry.js', () => ({
10
+ datasetRegistry: {
11
+ attach: jest.fn(),
12
+ attachMapStyle: jest.fn(),
13
+ attachCreateDataset: jest.fn()
14
+ }
15
+ }))
16
+ jest.mock('../registry/globalDataset.js', () => ({
17
+ attachGlobalState: jest.fn()
18
+ }))
19
+ jest.mock('../adapters/loadLayerAdapter.js', () => ({
20
+ loadLayerAdapter: jest.fn(),
21
+ layerAdapter: {}
22
+ }))
23
+ jest.mock('./initialiseDatasets.js')
24
+
25
+ const makeEventBus = () => ({
26
+ emit: jest.fn(),
27
+ on: jest.fn(),
28
+ off: jest.fn()
29
+ })
30
+
31
+ const makeAdapter = () => ({
32
+ init: jest.fn().mockResolvedValue(undefined)
33
+ })
34
+
35
+ const makePluginState = (overrides = {}) => ({
36
+ dispatch: jest.fn(),
37
+ mappedDatasets: {},
38
+ orderedDatasets: [],
39
+ globals: { visible: true, opacity: 1 },
40
+ actionsArray: [],
41
+ ...overrides
42
+ })
43
+
44
+ const makeProps = (overrides = {}) => ({
45
+ pluginConfig: {
46
+ datasets: [{ id: 'roads', label: 'Roads', showInMenu: true }]
47
+ },
48
+ pluginState: makePluginState(),
49
+ appState: { mode: 'default' },
50
+ mapState: { mapStyle: { id: 'outdoor' } },
51
+ mapProvider: { isBaseMapReady: jest.fn().mockReturnValue(true) },
52
+ services: {
53
+ eventBus: makeEventBus(),
54
+ symbolRegistry: {},
55
+ patternRegistry: {}
56
+ },
57
+ ...overrides
58
+ })
59
+
60
+ describe('DatasetsInit', () => {
61
+ let adapter
62
+
63
+ beforeEach(() => {
64
+ jest.clearAllMocks()
65
+ adapter = makeAdapter()
66
+ loadLayerAdapter.mockResolvedValue(adapter)
67
+ initialiseDatasets.mockReturnValue({ remove: jest.fn() })
68
+ })
69
+
70
+ it('renders null', () => {
71
+ const { container } = render(<DatasetsInit {...makeProps()} />)
72
+ expect(container.firstChild).toBeNull()
73
+ })
74
+
75
+ // ─── hasMenu / panel removal ─────────────────────────────────────────────────
76
+
77
+ describe('menu panel management', () => {
78
+ it('removes the datasetsLayers panel when no datasets have showInMenu', async () => {
79
+ const props = makeProps({
80
+ pluginConfig: { datasets: [{ id: 'roads', label: 'Roads' }] }
81
+ })
82
+ await render(<DatasetsInit {...props} />)
83
+ expect(props.services.eventBus.emit).toHaveBeenCalledWith(
84
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
85
+ )
86
+ expect(props.services.eventBus.emit).toHaveBeenCalledWith(
87
+ EVENTS.APP_TOGGLE_BUTTON_STATE, { id: 'datasetsLayers', prop: 'hidden', value: true }
88
+ )
89
+ })
90
+
91
+ it('does not remove the panel when at least one dataset has showInMenu', async () => {
92
+ const props = makeProps()
93
+ await render(<DatasetsInit {...props} />)
94
+ expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(
95
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
96
+ )
97
+ })
98
+
99
+ it('removes the panel when hasMenu is explicitly false', async () => {
100
+ const props = makeProps({
101
+ pluginConfig: { hasMenu: false, datasets: [{ id: 'roads', label: 'Roads', showInMenu: true }] }
102
+ })
103
+ await render(<DatasetsInit {...props} />)
104
+ expect(props.services.eventBus.emit).toHaveBeenCalledWith(
105
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
106
+ )
107
+ })
108
+
109
+ it('includes datasets with sublayer showInMenu when checking for menu items', async () => {
110
+ const props = makeProps({
111
+ pluginConfig: {
112
+ datasets: [{
113
+ id: 'parent',
114
+ label: 'Parent',
115
+ sublayers: [{ id: 'child', showInMenu: true }]
116
+ }]
117
+ }
118
+ })
119
+ await render(<DatasetsInit {...props} />)
120
+ expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(
121
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
122
+ )
123
+ })
124
+ })
125
+
126
+ // ─── initialisation ──────────────────────────────────────────────────────────
127
+
128
+ describe('initialisation', () => {
129
+ it('calls loadLayerAdapter and initialiseDatasets when base map is ready', async () => {
130
+ const props = makeProps()
131
+ await render(<DatasetsInit {...props} />)
132
+ expect(loadLayerAdapter).toHaveBeenCalledWith(
133
+ props.mapProvider, props.services.symbolRegistry, props.services.patternRegistry
134
+ )
135
+ expect(initialiseDatasets).toHaveBeenCalled()
136
+ })
137
+
138
+ it('does not initialise when base map is not ready', async () => {
139
+ const props = makeProps({
140
+ mapProvider: { isBaseMapReady: jest.fn().mockReturnValue(false) }
141
+ })
142
+ await render(<DatasetsInit {...props} />)
143
+ expect(loadLayerAdapter).not.toHaveBeenCalled()
144
+ })
145
+
146
+ it('does not initialise when mode is not in includeModes', async () => {
147
+ const props = makeProps({
148
+ pluginConfig: { datasets: [], includeModes: ['edit'] },
149
+ appState: { mode: 'default' }
150
+ })
151
+ await render(<DatasetsInit {...props} />)
152
+ expect(loadLayerAdapter).not.toHaveBeenCalled()
153
+ })
154
+
155
+ it('does not initialise when mode is in excludeModes', async () => {
156
+ const props = makeProps({
157
+ pluginConfig: { datasets: [], excludeModes: ['default'] },
158
+ appState: { mode: 'default' }
159
+ })
160
+ await render(<DatasetsInit {...props} />)
161
+ expect(loadLayerAdapter).not.toHaveBeenCalled()
162
+ })
163
+
164
+ it('does not initialise twice when re-rendered', async () => {
165
+ const props = makeProps()
166
+ const { rerender } = render(<DatasetsInit {...props} />)
167
+ await act(async () => {})
168
+ rerender(<DatasetsInit {...props} mapState={{ mapStyle: { id: 'dark' } }} />)
169
+ await act(async () => {})
170
+ expect(loadLayerAdapter).toHaveBeenCalledTimes(1)
171
+ })
172
+
173
+ it('skips init when datasetsInstanceRef is already populated (mode change)', async () => {
174
+ const props = makeProps()
175
+ const { rerender } = render(<DatasetsInit {...props} />)
176
+ await act(async () => {})
177
+ // Change appState.mode — that's in the effect deps, so the effect re-runs
178
+ await act(async () => {
179
+ rerender(<DatasetsInit {...props} appState={{ mode: 'edit' }} />)
180
+ })
181
+ // loadLayerAdapter should only be called once despite the re-run
182
+ expect(loadLayerAdapter).toHaveBeenCalledTimes(1)
183
+ })
184
+
185
+ it('calls remove on cleanup when unmounted after init', async () => {
186
+ const removeFn = jest.fn()
187
+ initialiseDatasets.mockReturnValue({ remove: removeFn })
188
+ const props = makeProps()
189
+ const { unmount } = render(<DatasetsInit {...props} />)
190
+ await act(async () => {})
191
+ unmount()
192
+ expect(removeFn).toHaveBeenCalled()
193
+ })
194
+ })
195
+
196
+ // ─── registry / map style effects ────────────────────────────────────────────
197
+
198
+ describe('datasetRegistry attach effects', () => {
199
+ it('attaches mappedDatasets and orderedDatasets to the registry', async () => {
200
+ const mappedDatasets = { roads: { id: 'roads' } }
201
+ const orderedDatasets = ['roads']
202
+ const props = makeProps({
203
+ pluginState: makePluginState({ mappedDatasets, orderedDatasets })
204
+ })
205
+ await render(<DatasetsInit {...props} />)
206
+ expect(datasetRegistry.attach).toHaveBeenCalledWith(mappedDatasets, orderedDatasets)
207
+ })
208
+
209
+ it('attaches the map style to the registry', async () => {
210
+ const props = makeProps()
211
+ await render(<DatasetsInit {...props} />)
212
+ expect(datasetRegistry.attachMapStyle).toHaveBeenCalledWith(props.mapState.mapStyle)
213
+ })
214
+
215
+ it('calls layerAdapter.onMapStyleChange when available', async () => {
216
+ const onMapStyleChange = jest.fn()
217
+ Object.assign(layerAdapter, { onMapStyleChange })
218
+ const props = makeProps()
219
+ await render(<DatasetsInit {...props} />)
220
+ expect(onMapStyleChange).toHaveBeenCalled()
221
+ delete layerAdapter.onMapStyleChange
222
+ })
223
+ })
224
+
225
+ // ─── globals effect ──────────────────────────────────────────────────────────
226
+
227
+ describe('globals effect', () => {
228
+ it('calls attachGlobalState with the current globals', async () => {
229
+ const globals = { visible: false, opacity: 0.5 }
230
+ const props = makeProps({ pluginState: makePluginState({ globals }) })
231
+ await render(<DatasetsInit {...props} />)
232
+ expect(attachGlobalState).toHaveBeenCalledWith(globals)
233
+ })
234
+ })
235
+
236
+ // ─── actionsArray effect ─────────────────────────────────────────────────────
237
+
238
+ describe('actionsArray effect', () => {
239
+ it('calls each queued adapter method and dispatches REMOVE_ADAPTER_ACTIONS', async () => {
240
+ const applyStyle = jest.fn()
241
+ Object.assign(layerAdapter, { applyStyle })
242
+ const actions = [{ method: 'applyStyle', parameters: ['roads', {}] }]
243
+ const dispatch = jest.fn()
244
+ const props = makeProps({
245
+ pluginState: makePluginState({ actionsArray: actions, dispatch })
246
+ })
247
+ await render(<DatasetsInit {...props} />)
248
+ expect(applyStyle).toHaveBeenCalledWith('roads', {})
249
+ expect(dispatch).toHaveBeenCalledWith({ type: 'REMOVE_ADAPTER_ACTIONS', payload: actions })
250
+ delete layerAdapter.applyStyle
251
+ })
252
+
253
+ it('skips dispatching when actionsArray is empty', async () => {
254
+ const dispatch = jest.fn()
255
+ const props = makeProps({ pluginState: makePluginState({ actionsArray: [], dispatch }) })
256
+ await render(<DatasetsInit {...props} />)
257
+ expect(dispatch).not.toHaveBeenCalledWith(
258
+ expect.objectContaining({ type: 'REMOVE_ADAPTER_ACTIONS' })
259
+ )
260
+ })
261
+ })
262
+ })
@@ -3,8 +3,7 @@ import { createDynamicSource } from '../fetch/createDynamicSource.js'
3
3
  import { applyDatasetDefaults, datasetDefaults } from './defaults.js'
4
4
  import { mappedDatasetsReducer } from '../reducers/mappedDatasetsReducer.js'
5
5
  import { datasetRegistry } from '../registry/datasetRegistry.js'
6
- import { setMenuState } from '../registry/isVisibleWhen.js'
7
- import { buildMenuState } from '../reducers/menuStateReducer.js'
6
+ import { attachMenuStateRef } from '../registry/isVisibleWhen.js'
8
7
  import { datasetsToMenu } from '../reducers/datasetsToMenu.js'
9
8
 
10
9
  export const initialiseDatasets = ({
@@ -30,8 +29,10 @@ export const initialiseDatasets = ({
30
29
  datasetRegistry.attachCreateDataset(adapter.createDataset)
31
30
  }
32
31
  datasetRegistry.attach(mappedDatasets, orderedDatasets, mapStyle)
33
- const menu = pluginConfig.menu || datasetsToMenu({ datasets: processedDatasets })
34
- setMenuState(buildMenuState(menu)) // Must be called before adapter.init so that menuState is set before any datasets are checked for visibility
32
+ const menu = datasetsToMenu({ datasets: processedDatasets })
33
+
34
+ eventBus.requestOnce('menu:state', attachMenuStateRef) // Request the menu state from the menu plugin
35
+
35
36
  dispatch({ type: 'SET_MENU', payload: { menu } })
36
37
  dispatch({ type: 'SET_DATASETS', payload: { mappedDatasets, orderedDatasets } })
37
38
 
@@ -49,7 +50,6 @@ export const initialiseDatasets = ({
49
50
  dynamicSources.set(registryDataset.id, dynamicSource)
50
51
  })
51
52
  adapter.attachDynamicSources(dynamicSources)
52
- // TODO - apply dynamic source defaults here, and include in mappedDatasets
53
53
  eventBus.emit('datasets:ready')
54
54
  })
55
55
 
@@ -59,9 +59,20 @@ export const initialiseDatasets = ({
59
59
 
60
60
  eventBus.on(events.MAP_SIZE_CHANGE, onMapSizeChange)
61
61
 
62
+ // Add a listener for the datasets:registry event, which will be emitted
63
+ // whenever requested by other plugins, e.g. the menu and the mapKey plugin,
64
+ // so they can access the dataset registry.
65
+ const removeRegistryListener = eventBus.emitWhenRequested('datasets:registry', datasetRegistry)
66
+
67
+ eventBus.on('menu:changed', (menuState) => {
68
+ datasetRegistry.invalidateKeyItemsOnMenuStateChange(menuState)
69
+ adapter.applyGlobalVisibility()
70
+ })
71
+
62
72
  return {
63
73
  remove () {
64
74
  eventBus.off(events.MAP_SIZE_CHANGE, onMapSizeChange)
75
+ removeRegistryListener()
65
76
 
66
77
  // Clean up dynamic sources
67
78
  dynamicSources.forEach(source => source.destroy())
@@ -8,6 +8,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({
8
8
  attach: jest.fn(),
9
9
  attachCreateDataset: jest.fn(),
10
10
  forEachDataset: jest.fn(),
11
+ invalidateKeyItemsOnMenuStateChange: jest.fn(),
11
12
  _invalidateCache: jest.fn()
12
13
  }
13
14
  }))
@@ -20,6 +21,7 @@ const makeAdapter = (overrides = {}) => ({
20
21
  onMapStyleChange: jest.fn(),
21
22
  onMapSizeChange: jest.fn(),
22
23
  setData: jest.fn(),
24
+ applyGlobalVisibility: jest.fn(),
23
25
  destroy: jest.fn(),
24
26
  createDataset: jest.fn(),
25
27
  ...overrides
@@ -29,6 +31,8 @@ const makeEventBus = () => {
29
31
  const handlers = {}
30
32
  return {
31
33
  on: jest.fn((event, handler) => { handlers[event] = handler }),
34
+ requestOnce: jest.fn((event, handler) => { handlers[event] = handler }),
35
+ emitWhenRequested: jest.fn(() => () => {}),
32
36
  off: jest.fn(),
33
37
  emit: jest.fn(),
34
38
  _handlers: handlers
@@ -253,3 +257,41 @@ describe('returned API', () => {
253
257
  expect(() => instance.clearDatasetCache('unknown')).not.toThrow()
254
258
  })
255
259
  })
260
+
261
+ // ─── onUpdate callback ────────────────────────────────────────────────────────
262
+
263
+ describe('onUpdate callback', () => {
264
+ it('calls adapter.setData when the onUpdate callback fires', async () => {
265
+ const mockSource = { destroy: jest.fn(), refresh: jest.fn(), clear: jest.fn(), getFeatureCount: jest.fn() }
266
+ let capturedOnUpdate
267
+ createDynamicSource.mockImplementation(({ onUpdate }) => {
268
+ capturedOnUpdate = onUpdate
269
+ return mockSource
270
+ })
271
+ const mockDataset = { id: 'parcels', hasDynamicGeoJSON: true, dynamicGeoJSON: {} }
272
+ datasetRegistry.forEachDataset.mockImplementation(cb => cb(mockDataset))
273
+
274
+ const args = makeArgs()
275
+ initialiseDatasets(args)
276
+ await Promise.resolve()
277
+
278
+ const geojson = { type: 'FeatureCollection', features: [] }
279
+ capturedOnUpdate('parcels', geojson)
280
+ expect(args.adapter.setData).toHaveBeenCalledWith('parcels', geojson)
281
+ })
282
+ })
283
+
284
+ // ─── menu:changed handler ─────────────────────────────────────────────────────
285
+
286
+ describe('menu:changed handler', () => {
287
+ it('calls invalidateKeyItemsOnMenuStateChange and applyGlobalVisibility', () => {
288
+ const args = makeArgs()
289
+ initialiseDatasets(args)
290
+
291
+ const menuState = { items: [{ id: 'roads', visible: true }] }
292
+ args.eventBus._handlers['menu:changed'](menuState)
293
+
294
+ expect(datasetRegistry.invalidateKeyItemsOnMenuStateChange).toHaveBeenCalledWith(menuState)
295
+ expect(args.adapter.applyGlobalVisibility).toHaveBeenCalled()
296
+ })
297
+ })
@@ -2,7 +2,6 @@
2
2
  import { initialState, actions } from './reducers/pluginState.js'
3
3
  import { DatasetsInit } from './initialise/DatasetsInit.jsx'
4
4
  import { LayersMenu } from './components/LayersMenu/LayersMenu.jsx'
5
- import { Key } from './components/Key/Key.jsx'
6
5
  import { addDataset } from './api/addDataset.js'
7
6
  import { removeDataset } from './api/removeDataset.js'
8
7
  import { setDatasetVisibility } from './api/setDatasetVisibility.js'
@@ -44,22 +43,6 @@ export const manifest = {
44
43
  width: '280px'
45
44
  },
46
45
  render: LayersMenu
47
- }, {
48
- id: 'datasetsKey',
49
- label: 'Key',
50
- mobile: {
51
- slot: 'drawer',
52
- modal: true
53
- },
54
- tablet: {
55
- slot: 'left-top',
56
- width: '260px'
57
- },
58
- desktop: {
59
- slot: 'left-top',
60
- width: '280px'
61
- },
62
- render: Key
63
46
  }],
64
47
 
65
48
  buttons: [{
@@ -67,9 +50,6 @@ export const manifest = {
67
50
  label: 'Layers',
68
51
  panelId: 'datasetsLayers',
69
52
  iconId: 'layers',
70
- excludeWhen: ({ pluginConfig }) => !pluginConfig.menu && !pluginConfig.datasets.some(l =>
71
- l.showInMenu || l.sublayers?.some(r => r.showInMenu)
72
- ),
73
53
  mobile: {
74
54
  slot: 'top-left',
75
55
  showLabel: true
@@ -82,24 +62,6 @@ export const manifest = {
82
62
  slot: 'top-left',
83
63
  showLabel: true
84
64
  }
85
- }, {
86
- id: 'datasetsKey',
87
- label: 'Key',
88
- panelId: 'datasetsKey',
89
- iconId: 'key',
90
- excludeWhen: ({ pluginConfig }) => !pluginConfig.datasets.some(l => l.showInKey),
91
- mobile: {
92
- slot: 'top-left',
93
- showLabel: false
94
- },
95
- tablet: {
96
- slot: 'top-left',
97
- showLabel: true
98
- },
99
- desktop: {
100
- slot: 'top-left',
101
- showLabel: true
102
- }
103
65
  }],
104
66
 
105
67
  icons: [{
@@ -0,0 +1,49 @@
1
+ import { manifest } from './manifest.js'
2
+
3
+ jest.mock('./reducers/pluginState.js', () => ({ initialState: {}, actions: {} }))
4
+ jest.mock('./initialise/DatasetsInit.jsx', () => ({ DatasetsInit: jest.fn() }))
5
+ jest.mock('./components/LayersMenu/LayersMenu.jsx', () => ({ LayersMenu: jest.fn() }))
6
+ jest.mock('./api/addDataset.js', () => ({ addDataset: jest.fn() }))
7
+ jest.mock('./api/removeDataset.js', () => ({ removeDataset: jest.fn() }))
8
+ jest.mock('./api/setDatasetVisibility.js', () => ({ setDatasetVisibility: jest.fn() }))
9
+ jest.mock('./api/setFeatureVisibility.js', () => ({ setFeatureVisibility: jest.fn() }))
10
+ jest.mock('./api/setStyle.js', () => ({ setStyle: jest.fn() }))
11
+ jest.mock('./api/getStyle.js', () => ({ getStyle: jest.fn() }))
12
+ jest.mock('./api/setOpacity.js', () => ({ setOpacity: jest.fn() }))
13
+ jest.mock('./api/getOpacity.js', () => ({ getOpacity: jest.fn() }))
14
+ jest.mock('./api/setData.js', () => ({ setData: jest.fn() }))
15
+ jest.mock('./api/setGlobals.js', () => ({ setGlobals: jest.fn() }))
16
+
17
+ describe('manifest', () => {
18
+ it('exports a manifest object', () => {
19
+ expect(manifest).toBeDefined()
20
+ expect(typeof manifest).toBe('object')
21
+ })
22
+
23
+ it('has an InitComponent', () => {
24
+ expect(manifest.InitComponent).toBeDefined()
25
+ })
26
+
27
+ it('has a reducer with initialState and actions', () => {
28
+ expect(manifest.reducer).toBeDefined()
29
+ expect(manifest.reducer.initialState).toBeDefined()
30
+ expect(manifest.reducer.actions).toBeDefined()
31
+ })
32
+
33
+ it('has panels with a datasetsLayers panel', () => {
34
+ expect(Array.isArray(manifest.panels)).toBe(true)
35
+ expect(manifest.panels.find(p => p.id === 'datasetsLayers')).toBeDefined()
36
+ })
37
+
38
+ it('has a buttons array', () => {
39
+ expect(Array.isArray(manifest.buttons)).toBe(true)
40
+ })
41
+
42
+ it('has an api with all expected methods', () => {
43
+ expect(manifest.api.addDataset).toBeDefined()
44
+ expect(manifest.api.removeDataset).toBeDefined()
45
+ expect(manifest.api.setDatasetVisibility).toBeDefined()
46
+ expect(manifest.api.setOpacity).toBeDefined()
47
+ expect(manifest.api.getOpacity).toBeDefined()
48
+ })
49
+ })