@eeacms/volto-bise-policy 1.3.4 → 1.3.6

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.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,45 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
- ### [1.3.4](https://github.com/eea/volto-bise-policy/compare/1.3.3...1.3.4) - 9 July 2026
7
+ ### [1.3.6](https://github.com/eea/volto-bise-policy/compare/1.3.5...1.3.6) - 19 August 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: add dompurify dependency [laszlocseh - [`8e2eda4`](https://github.com/eea/volto-bise-policy/commit/8e2eda4eba5084e231e8457da1a98a47036bccb7)]
12
+ - feat(CaseStudyExplorer): add path filter and edit form [laszlocseh - [`ef277dc`](https://github.com/eea/volto-bise-policy/commit/ef277dc8a60a8ae746e635e4da48dfefc67685dc)]
13
+ - feat(CaseStudyExplorer): add status filters and map legend [laszlocseh - [`f2c28d2`](https://github.com/eea/volto-bise-policy/commit/f2c28d25d259d3d661cfbb261419ee435f7470d5)]
14
+
15
+ #### :bug: Bug Fixes
16
+
17
+ - fix(CaseStudyMap): properly display the selected case study style on map [laszlocseh - [`0b374a7`](https://github.com/eea/volto-bise-policy/commit/0b374a70803be40421e11de9776155299d1c0e22)]
18
+ - fix(CaseStudyExplorer): sanitize HTML and escape regex input [laszlocseh - [`e6ab8e1`](https://github.com/eea/volto-bise-policy/commit/e6ab8e1bd7a2d4e6b9341b19eda440828715a4d9)]
19
+
20
+ #### :nail_care: Enhancements
21
+
22
+ - refactor(case-study-explorer): replace hardcoded interaction index with helper [laszlocseh - [`fb69ec0`](https://github.com/eea/volto-bise-policy/commit/fb69ec082ff5e468be0f6e872e6a45e9e7c3695f)]
23
+
24
+ #### :house: Internal changes
25
+
26
+ - style: Automated code fix [eea-jenkins - [`66ec7fa`](https://github.com/eea/volto-bise-policy/commit/66ec7faab819bb27a0a786d459b6389a86bde21d)]
27
+ - style(CaseStudyExplorer): update status colors and fix indentation [laszlocseh - [`54cb574`](https://github.com/eea/volto-bise-policy/commit/54cb574a4947580b19a01307484fcedbf23175a3)]
28
+ - style: Automated code fix [eea-jenkins - [`5c742ae`](https://github.com/eea/volto-bise-policy/commit/5c742ae2581275f9c3bd83a8376d848bf5d33ead)]
29
+ - style: Automated code fix [eea-jenkins - [`4441648`](https://github.com/eea/volto-bise-policy/commit/44416480a18e217db2ebaf86bc2aaa4f60d31787)]
30
+ - style: Automated code fix [eea-jenkins - [`5eb0e32`](https://github.com/eea/volto-bise-policy/commit/5eb0e3284d2ef540c2cdc51984839cf50aad8b75)]
31
+
32
+ #### :hammer_and_wrench: Others
33
+
34
+ - test: fix case study utils.test.js [laszlocseh - [`adcffba`](https://github.com/eea/volto-bise-policy/commit/adcffba55f0cab391eef6db2b88880acffcea918)]
35
+ ### [1.3.5](https://github.com/eea/volto-bise-policy/compare/1.3.4...1.3.5) - 5 August 2026
36
+
37
+ #### :rocket: New Features
38
+
39
+ - feat(widgets): add fallback url for EUNIS code [laszlocseh - [`2ad4400`](https://github.com/eea/volto-bise-policy/commit/2ad4400ac0dbb17ff7e207125d8b40c5b4a268cf)]
40
+
41
+ #### :bug: Bug Fixes
42
+
43
+ - fix(css): fix Marine EUNIS Habitat content type css [laszlocseh - [`e818c10`](https://github.com/eea/volto-bise-policy/commit/e818c10d5806f42b181f1ab3c51a565de5bb36e9)]
44
+
45
+ ### [1.3.4](https://github.com/eea/volto-bise-policy/compare/1.3.3...1.3.4) - 10 July 2026
8
46
 
9
47
  #### :hammer_and_wrench: Others
10
48
 
@@ -13,7 +51,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
13
51
 
14
52
  #### :house: Internal changes
15
53
 
16
- - chore: [JENKINSFILE] sanitize sonarqube command [valentinab25 - [`8e2dc1a`](https://github.com/eea/volto-bise-policy/commit/8e2dc1a14f3230f197f009a76bf87587724b6e3a)]
17
54
 
18
55
  #### :hammer_and_wrench: Others
19
56
 
@@ -117,8 +154,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
117
154
 
118
155
  #### :house: Internal changes
119
156
 
120
- - chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`4c2d75b`](https://github.com/eea/volto-bise-policy/commit/4c2d75bbf6dca47929c6613749169b40ed483985)]
121
- - chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`f99c622`](https://github.com/eea/volto-bise-policy/commit/f99c622726cfd711704f50218a2f1fe833974ec3)]
122
157
 
123
158
  #### :hammer_and_wrench: Others
124
159
 
@@ -381,7 +416,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
381
416
  - chore: customize root theme to use volto-spotlight - refs #259794 [ana-oprea - [`347f96e`](https://github.com/eea/volto-bise-policy/commit/347f96e335139f9656abd34c308c531b05de85e7)]
382
417
  - chore: change the theme and alias to volto-spotlight and add volto-spotlight to package.json - refs #259794 [ana-oprea - [`52732a1`](https://github.com/eea/volto-bise-policy/commit/52732a17b3247a04ffae0a3f5118e173afa98157)]
383
418
  - chore: change theme to eea-volto-theme, change config.alias - refs #259794 [ana-oprea - [`c2d08ea`](https://github.com/eea/volto-bise-policy/commit/c2d08ea08a7f0e5f6f7d32149e60ea3f3e901923)]
384
- - chore: [JENKINS] Refactor automated testing [valentinab25 - [`a3c9488`](https://github.com/eea/volto-bise-policy/commit/a3c9488c5d09ba97fe5976675524d436080a2460)]
385
419
  - chore: husky, lint-staged use fixed versions [valentinab25 - [`fddc30f`](https://github.com/eea/volto-bise-policy/commit/fddc30f11a1d8ec290edaca2d924282f5e113bc6)]
386
420
  - chore:volto 16 in tests, update docs, fix stylelint overrides [valentinab25 - [`005ce74`](https://github.com/eea/volto-bise-policy/commit/005ce742dbd014fde27e417059251233365766dd)]
387
421
 
@@ -394,17 +428,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
394
428
  - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`c1277ba`](https://github.com/eea/volto-bise-policy/commit/c1277bacf840ee9feccbfd6a2c85f0b25a9a9125)]
395
429
  - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`c944153`](https://github.com/eea/volto-bise-policy/commit/c944153710fd53c5d48863cf363464273e25f52e)]
396
430
  - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`d648d52`](https://github.com/eea/volto-bise-policy/commit/d648d525a2fc5076577fe0f75ff9782bf0d1d4e9)]
397
- - test: [JENKINS] fix jest config, fail with logs on coverage [valentinab25 - [`2a7affe`](https://github.com/eea/volto-bise-policy/commit/2a7affe33d737704bdba117c4a5c8c6748ef7a47)]
398
- - test: [JENKINS] fix jest config, fail with logs on coverage [valentinab25 - [`5929642`](https://github.com/eea/volto-bise-policy/commit/59296420105d0e8c1f1547aa0cd17a019e478c2b)]
399
- - test: [JENKINS] fix jest config, fail with logs on coverage [valentinab25 - [`b1142f1`](https://github.com/eea/volto-bise-policy/commit/b1142f1da3667a9f56720f65bebce897a5dd8005)]
400
431
  - test: cypress should not wait for navigation, actions, types - refs #259794 [ana-oprea - [`188637c`](https://github.com/eea/volto-bise-policy/commit/188637c78a0cae71b5ee48022507d559dfaceb3e)]
401
- - test: [JENKINS] Use java17 for sonarqube scanner [valentinab25 - [`fa4e473`](https://github.com/eea/volto-bise-policy/commit/fa4e473018386ec3f09d7300d655c8cccdc19a0e)]
402
- - test: [JENKINS] Run cypress in started frontend container [valentinab25 - [`1d9135b`](https://github.com/eea/volto-bise-policy/commit/1d9135bb878b3c7a1b828085b99b9d27d78ce60e)]
403
432
  - remove cypress tests as it doesn t work on bise [Dobricean Ioan Dorian - [`ba7e2de`](https://github.com/eea/volto-bise-policy/commit/ba7e2defea2dcfd01537c3abb41fa9515ff0a3e3)]
404
433
  - add mega menu layout settings as #259794 said [dobri1408 - [`b9a3683`](https://github.com/eea/volto-bise-policy/commit/b9a3683e7d85f8c88ad8feac975c47c434f62c4a)]
405
- - test: [JENKINS] Add cpu limit on cypress docker [valentinab25 - [`9181908`](https://github.com/eea/volto-bise-policy/commit/9181908c7f4b51bdf83a34f8c7f25827de9b1cc6)]
406
- - test: [JENKINS] Increase shm-size to cypress docker [valentinab25 - [`099d796`](https://github.com/eea/volto-bise-policy/commit/099d79669471e6924bf1e0bc51a79d54379c3a1a)]
407
- - test: [JENKINS] Improve cypress time [valentinab25 - [`6d5d8e2`](https://github.com/eea/volto-bise-policy/commit/6d5d8e29b04ddb92628245570055067ca3dcbb6c)]
408
434
  - test: fix comment [valentinab25 - [`4f27d5a`](https://github.com/eea/volto-bise-policy/commit/4f27d5af2e8895c7af4f1157521ca42682b96e23)]
409
435
  - test: EN locales, pre-commit fix, feature PRs checks Refs #257193 [valentinab25 - [`4c2fcb0`](https://github.com/eea/volto-bise-policy/commit/4c2fcb01065e1ef962317ba7be788fd1df079618)]
410
436
  - test: Update Makefile and docker-compose to align it with Jenkinsfile [valentinab25 - [`89afaa1`](https://github.com/eea/volto-bise-policy/commit/89afaa15892930452027ef8c8593fc6d193cda70)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-bise-policy",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "@eeacms/volto-bise-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -33,6 +33,7 @@
33
33
  "@eeacms/volto-openlayers-map": "2.0.1",
34
34
  "@eeacms/volto-resize-helper": "*",
35
35
  "@plone-collective/volto-authomatic": "2.0.1",
36
+ "dompurify": "^3.3.1",
36
37
  "esri-loader": "3.7.0",
37
38
  "plotly.js": "^2.28.0",
38
39
  "querystring": "0.2.1",
@@ -14,6 +14,7 @@ import deleteSVG from '@plone/volto/icons/delete.svg';
14
14
  import addSVG from '@plone/volto/icons/add.svg';
15
15
  import dragSVG from '@plone/volto/icons/drag.svg';
16
16
  import { v4 as uuid } from 'uuid';
17
+ import { useSelector } from 'react-redux';
17
18
 
18
19
  import { EuropeanRedListVocab, countryCodesVocab } from './vocabularies';
19
20
  import './styles.less';
@@ -254,16 +255,17 @@ export const EUNISMSFDView = ({ value }) => {
254
255
  );
255
256
  };
256
257
 
257
- export const EUNISCodeView = ({ value }) => {
258
+ export const EUNISCodeView = ({ value, content }) => {
259
+ const storeData = useSelector((state) => state.content?.data);
258
260
  if (!value) return value;
259
261
 
262
+ const contentData = content ?? storeData;
263
+ const altUrl = contentData?.['eunis_code_alt_url']?.trim();
264
+ const href = altUrl || `/habitats_eunis_revised/EUNISrev_${value}`;
265
+
260
266
  return (
261
267
  <span>
262
- <a
263
- href={`/habitats_eunis_revised/EUNISrev_${value}`}
264
- target="_blank"
265
- rel="noopener"
266
- >
268
+ <a href={href} target="_blank" rel="noopener">
267
269
  {value}
268
270
  </a>
269
271
  </span>
@@ -1,5 +1,26 @@
1
+ import React from 'react';
2
+ import SidebarPortal from '@plone/volto/components/manage/Sidebar/SidebarPortal';
3
+ import InlineForm from '@plone/volto/components/manage/Form/InlineForm';
1
4
  import CaseStudyExplorerView from './CaseStudyExplorerView';
5
+ import schema from './schema';
2
6
 
3
7
  export default function CaseStudyExplorerEdit(props) {
4
- return <CaseStudyExplorerView {...props} mode="edit" />;
8
+ return (
9
+ <>
10
+ <CaseStudyExplorerView {...props} mode="edit" />
11
+ <SidebarPortal selected={props.selected}>
12
+ <InlineForm
13
+ schema={schema}
14
+ title={schema.title}
15
+ onChangeField={(id, value) => {
16
+ props.onChangeBlock(props.block, {
17
+ ...props.data,
18
+ [id]: value,
19
+ });
20
+ }}
21
+ formData={props.data}
22
+ />
23
+ </SidebarPortal>
24
+ </>
25
+ );
5
26
  }
@@ -6,7 +6,7 @@ import CaseStudyMap from './CaseStudyMap';
6
6
  import { ActiveFilters, CaseStudyFilters, SearchBox } from './CaseStudyFilters';
7
7
  // import CaseStudyList from './CaseStudyListing';
8
8
 
9
- import { filterCases, getFilters } from './utils';
9
+ import { filterCases, filterCasesByPath, getFilters } from './utils';
10
10
  import { useCases } from './hooks';
11
11
 
12
12
  import './styles.less';
@@ -14,7 +14,8 @@ import './styles.less';
14
14
  export default function CaseStudyExplorerView(props) {
15
15
  const cases_url = '/@@case-studies-map.arcgis.json';
16
16
  let cases = useCases(addAppURL(cases_url));
17
- const { caseStudiesIds } = props; // case studies from measure view
17
+ const { caseStudiesIds, data } = props; // case studies from measure view
18
+ const { filter_path } = data || {};
18
19
  const [selectedCase, onSelectedCase] = React.useState(null);
19
20
  const [searchInput, setSearchInput] = React.useState('');
20
21
  const hideFilters = caseStudiesIds ? true : false;
@@ -22,21 +23,24 @@ export default function CaseStudyExplorerView(props) {
22
23
  const [activeFilters, setActiveFilters] = React.useState({
23
24
  measures_implemented: [],
24
25
  typology_of_measures: [],
26
+ current_status: [],
27
+ habitat_ecosystem_type: [],
28
+ nrr_article: [],
29
+ scale_of_planning: [],
25
30
  });
26
31
 
27
32
  const [activeItems, setActiveItems] = React.useState(cases);
28
33
  const [filters, setFilters] = React.useState([]);
29
34
  const [map, setMap] = React.useState();
35
+ const pathFilteredCases = React.useMemo(
36
+ () => filterCasesByPath(cases, filter_path),
37
+ [cases, filter_path],
38
+ );
30
39
 
31
40
  React.useEffect(() => {
32
- const _filters = getFilters(cases);
41
+ const _filters = getFilters(pathFilteredCases);
33
42
  setFilters(_filters);
34
- }, [
35
- cases,
36
- activeFilters.measures_implemented,
37
- activeFilters.typology_of_measures,
38
- activeItems.length,
39
- ]);
43
+ }, [pathFilteredCases, activeFilters, activeItems.length]);
40
44
 
41
45
  React.useEffect(() => {
42
46
  let activeItems = filterCases(
@@ -44,10 +48,11 @@ export default function CaseStudyExplorerView(props) {
44
48
  activeFilters,
45
49
  caseStudiesIds,
46
50
  searchInput,
51
+ filter_path,
47
52
  );
48
53
 
49
54
  setActiveItems(activeItems);
50
- }, [caseStudiesIds, activeFilters, cases, searchInput]);
55
+ }, [caseStudiesIds, activeFilters, cases, searchInput, filter_path]);
51
56
 
52
57
  if (__SERVER__) return '';
53
58
 
@@ -85,11 +90,11 @@ export default function CaseStudyExplorerView(props) {
85
90
  )}
86
91
  </Grid.Row>
87
92
  <Grid.Row>
88
- {cases.length ? (
93
+ {pathFilteredCases.length ? (
89
94
  <Grid columns={12}>
90
95
  <Grid.Column mobile={12} tablet={12} computer={12}>
91
96
  <CaseStudyMap
92
- items={cases}
97
+ items={pathFilteredCases}
93
98
  activeItems={activeItems}
94
99
  hideFilters={hideFilters}
95
100
  selectedCase={selectedCase}
@@ -31,6 +31,7 @@ jest.mock('./CaseStudyFilters', () => ({
31
31
 
32
32
  jest.mock('./utils', () => ({
33
33
  filterCases: jest.fn(() => [{ id: 1, title: 'Filtered Case' }]),
34
+ filterCasesByPath: jest.fn((cases) => cases),
34
35
  getFilters: jest.fn(() => ['filter1', 'filter2']),
35
36
  }));
36
37
 
@@ -3,16 +3,7 @@ import React from 'react';
3
3
  import { centerAndResetMapZoom, scrollToElement } from './utils';
4
4
  import { withOpenLayers } from '@eeacms/volto-openlayers-map';
5
5
 
6
- const normalizeSearchInput = (searchInput) => {
7
- let normInput = searchInput
8
- .toLowerCase()
9
- .replace('(', '')
10
- .replace(')', '')
11
- .replace('?', '.')
12
- .replace('*', '[^\\s]+');
13
-
14
- return '\\b' + normInput + '\\b';
15
- };
6
+ const normalizeSearchInput = (searchInput) => searchInput.trim().toLowerCase();
16
7
 
17
8
  export function CaseStudyFilter(props) {
18
9
  const {
@@ -24,6 +15,7 @@ export function CaseStudyFilter(props) {
24
15
  map,
25
16
  ol,
26
17
  } = props;
18
+ const filterEntries = Object.entries(filters?.[filterName] || {});
27
19
 
28
20
  const showInputs = (event) => {
29
21
  event.currentTarget.parentElement.classList.add('active');
@@ -33,6 +25,7 @@ export function CaseStudyFilter(props) {
33
25
  <div className="filter-wrapper">
34
26
  <button
35
27
  className="ui basic button facet-btn"
28
+ disabled={!filterEntries.length}
36
29
  onClick={(e) => showInputs(e)}
37
30
  >
38
31
  <span>
@@ -41,7 +34,9 @@ export function CaseStudyFilter(props) {
41
34
  </span>
42
35
  </button>
43
36
  <div className="filter-inputs-wrapper">
44
- {Object.entries(filters?.[filterName] || {}).length > 7 ? (
37
+ {!filterEntries.length ? (
38
+ <div className="filter-empty">No options available</div>
39
+ ) : filterEntries.length > 7 ? (
45
40
  <input
46
41
  type="text"
47
42
  className="filterInputText"
@@ -66,7 +61,7 @@ export function CaseStudyFilter(props) {
66
61
  )}
67
62
 
68
63
  <div className="filter-inputs">
69
- {Object.entries(filters?.[filterName] || {})
64
+ {filterEntries
70
65
  .sort((item1, item2) => item1[1].localeCompare(item2[1]))
71
66
  .map(([value, label], index) => (
72
67
  <label
@@ -138,6 +133,27 @@ function CaseStudyFiltersComponent(props) {
138
133
  map={map}
139
134
  ol={ol}
140
135
  />
136
+
137
+ <CaseStudyFilter
138
+ filterTitle="Current status"
139
+ filterName="current_status"
140
+ {...props}
141
+ />
142
+ <CaseStudyFilter
143
+ filterTitle="Habitat/ecosystem type"
144
+ filterName="habitat_ecosystem_type"
145
+ {...props}
146
+ />
147
+ <CaseStudyFilter
148
+ filterTitle="NRR Article"
149
+ filterName="nrr_article"
150
+ {...props}
151
+ />
152
+ <CaseStudyFilter
153
+ filterTitle="Scale of Planning"
154
+ filterName="scale_of_planning"
155
+ {...props}
156
+ />
141
157
  </>
142
158
  );
143
159
  }
@@ -266,7 +282,9 @@ function ActiveFiltersComponent(props) {
266
282
  for (let i = 0; i < filterInputs.length; i++) {
267
283
  filterInputs[i].checked = false;
268
284
  }
269
- setActiveFilters({ measures_implemented: [], typology_of_measures: [] });
285
+ setActiveFilters(
286
+ Object.fromEntries(Object.keys(activeFilters).map((key) => [key, []])),
287
+ );
270
288
  scrollToElement('search-input');
271
289
  };
272
290
 
@@ -314,6 +332,34 @@ function ActiveFiltersComponent(props) {
314
332
  filters={filters}
315
333
  onRemoveFilter={removeFilter}
316
334
  />
335
+ <FilterGroup
336
+ filterName="current_status"
337
+ filterLabel="Current status:"
338
+ activeFilterCodes={activeFilters.current_status}
339
+ filters={filters}
340
+ onRemoveFilter={removeFilter}
341
+ />
342
+ <FilterGroup
343
+ filterName="habitat_ecosystem_type"
344
+ filterLabel="Habitat/ecosystem type:"
345
+ activeFilterCodes={activeFilters.habitat_ecosystem_type}
346
+ filters={filters}
347
+ onRemoveFilter={removeFilter}
348
+ />
349
+ <FilterGroup
350
+ filterName="nrr_article"
351
+ filterLabel="NRR Article:"
352
+ activeFilterCodes={activeFilters.nrr_article}
353
+ filters={filters}
354
+ onRemoveFilter={removeFilter}
355
+ />
356
+ <FilterGroup
357
+ filterName="scale_of_planning"
358
+ filterLabel="Scale of Planning:"
359
+ activeFilterCodes={activeFilters.scale_of_planning}
360
+ filters={filters}
361
+ onRemoveFilter={removeFilter}
362
+ />
317
363
  </div>
318
364
  </div>
319
365
  </div>
@@ -1,8 +1,11 @@
1
1
  import React from 'react';
2
+ import DOMPurify from 'dompurify';
2
3
  import { withOpenLayers } from '@eeacms/volto-openlayers-map';
3
4
 
4
5
  import {
5
6
  centerAndResetMapZoom,
7
+ escapeRegExp,
8
+ getSelectInteraction,
6
9
  scrollToElement,
7
10
  zoomMapToFeatures,
8
11
  } from './utils';
@@ -33,7 +36,9 @@ const showPageNr = (pageNr, currentPage, numberOfPages) => {
33
36
  function CaseStudyList(props) {
34
37
  const { selectedCase, onSelectedCase, pointsSource, map, searchInput, ol } =
35
38
  props;
36
- const reSearch = new RegExp(`\\b(${searchInput})\\b`, 'gi');
39
+ const reSearch = searchInput
40
+ ? new RegExp(`\\b(${escapeRegExp(searchInput)})\\b`, 'gi')
41
+ : null;
37
42
  const [currentPage, setCurrentPage] = React.useState(1);
38
43
 
39
44
  const features = pointsSource
@@ -132,7 +137,7 @@ function CaseStudyList(props) {
132
137
  // reset map zoom
133
138
  onSelectedCase(null);
134
139
  centerAndResetMapZoom({ map, ol });
135
- map.getInteractions().array_[9].getFeatures().clear();
140
+ getSelectInteraction(map)?.getFeatures().clear();
136
141
  }}
137
142
  >
138
143
  <span
@@ -167,12 +172,17 @@ function CaseStudyList(props) {
167
172
  <p
168
173
  className="listing-description"
169
174
  dangerouslySetInnerHTML={{
170
- __html: searchInput
171
- ? item.values_.description.replaceAll(
172
- reSearch,
173
- '<b>$1</b>',
174
- )
175
- : item.values_.description,
175
+ // description is server data and may contain HTML;
176
+ // sanitize the highlighted result so no scripts or
177
+ // event handlers can reach the DOM
178
+ __html: DOMPurify.sanitize(
179
+ searchInput && reSearch && item.values_.description
180
+ ? item.values_.description.replaceAll(
181
+ reSearch,
182
+ '<b>$1</b>',
183
+ )
184
+ : item.values_.description || '',
185
+ ),
176
186
  }}
177
187
  ></p>
178
188
  <div className="slot-bottom">
@@ -212,10 +222,9 @@ function CaseStudyList(props) {
212
222
  role="button"
213
223
  onKeyDown={() => {}}
214
224
  onClick={() => {
215
- map
216
- .getInteractions()
217
- .array_[9].getFeatures()
218
- .clear();
225
+ const selectInteraction = getSelectInteraction(map);
226
+ selectInteraction?.getFeatures().clear();
227
+
219
228
  // scroll to the map
220
229
  scrollToElement('ol-map-container');
221
230
 
@@ -231,10 +240,13 @@ function CaseStudyList(props) {
231
240
  const coords =
232
241
  item.values_.geometry.flatCoordinates;
233
242
  const pixel = map.getPixelFromCoordinate(coords);
234
- map
235
- .getInteractions()
236
- .array_[9].getFeatures()
237
- .push(map.getFeaturesAtPixel(pixel)[0]);
243
+ const selectedFeature =
244
+ map.getFeaturesAtPixel(pixel)[0];
245
+ if (selectedFeature) {
246
+ selectInteraction
247
+ ?.getFeatures()
248
+ .push(selectedFeature);
249
+ }
238
250
  }, 1100);
239
251
  }}
240
252
  >
@@ -14,6 +14,7 @@ jest.mock('@eeacms/volto-openlayers-map', () => ({
14
14
  }));
15
15
 
16
16
  jest.mock('./utils', () => ({
17
+ ...jest.requireActual('./utils'),
17
18
  centerAndResetMapZoom: jest.fn(),
18
19
  scrollToElement: jest.fn(),
19
20
  zoomMapToFeatures: jest.fn(),
@@ -119,6 +120,59 @@ describe('CaseStudyList', () => {
119
120
  expect(getByText('Feature 1').getAttribute('href')).toBe('/feature1');
120
121
  });
121
122
 
123
+ it('does not crash on regex metacharacters in search input', () => {
124
+ const { getByText } = render(
125
+ <CaseStudyList
126
+ selectedCase={null}
127
+ onSelectedCase={jest.fn()}
128
+ pointsSource={mockPointsSource}
129
+ map={mockMap}
130
+ searchInput="("
131
+ ol={{}}
132
+ />,
133
+ );
134
+
135
+ // must not throw (previously: SyntaxError from invalid RegExp)
136
+ expect(getByText('Feature 1')).toBeInTheDocument();
137
+ expect(getByText('Description 1')).toBeInTheDocument();
138
+ });
139
+
140
+ it('sanitizes description HTML and keeps search highlights', () => {
141
+ const evilPointsSource = createMockPointsSource([
142
+ {
143
+ values_: {
144
+ title: 'Evil Case',
145
+ path: '/evil',
146
+ description:
147
+ '<img src=x onerror=alert(1)> <script>alert(2)</script> a river bed',
148
+ typology_of_measures: ['SectorA'],
149
+ measures_implemented: [{ title: 'M1', path: '/m1' }],
150
+ geometry: { flatCoordinates: [0, 0] },
151
+ },
152
+ },
153
+ ]);
154
+
155
+ const { container } = render(
156
+ <CaseStudyList
157
+ selectedCase={null}
158
+ onSelectedCase={jest.fn()}
159
+ pointsSource={evilPointsSource}
160
+ map={mockMap}
161
+ searchInput="river"
162
+ ol={{}}
163
+ />,
164
+ );
165
+
166
+ // XSS payloads must not survive into the DOM
167
+ expect(container.querySelector('img[onerror]')).toBeNull();
168
+ expect(container.querySelector('script')).toBeNull();
169
+ // harmless text still renders and the highlight survives sanitization
170
+ expect(container.textContent).toContain('a river bed');
171
+ const highlight = container.querySelector('b');
172
+ expect(highlight).not.toBeNull();
173
+ expect(highlight.textContent).toBe('river');
174
+ });
175
+
122
176
  it('calls zoomMapToFeatures and onSelectedCase when clicking Show on map', async () => {
123
177
  const onSelectedCase = jest.fn();
124
178