@eeacms/volto-bise-policy 1.3.9 → 1.3.10

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,6 +4,21 @@ 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.10](https://github.com/eea/volto-bise-policy/compare/1.3.9...1.3.10) - 21 September 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(CaseStudyExplorer): add image and link icon to feature popup [laszlocseh - [`ceba7db`](https://github.com/eea/volto-bise-policy/commit/ceba7db816f00fd430edab13812aac348a3042c2)]
12
+ - feat: replace NRR article with legislation and biogeographical region filters [laszlocseh - [`6f6c0b4`](https://github.com/eea/volto-bise-policy/commit/6f6c0b479f046977a8ff63524f7db32ebdb1ae03)]
13
+
14
+ #### :nail_care: Enhancements
15
+
16
+ - refactor(CaseStudyExplorer): use Image component in FeatureDisplay [laszlocseh - [`19e4a0f`](https://github.com/eea/volto-bise-policy/commit/19e4a0fa357391ab96a12c676ae79c3501f0a2ba)]
17
+
18
+ #### :house: Internal changes
19
+
20
+ - style: Automated code fix [eea-jenkins - [`9df6965`](https://github.com/eea/volto-bise-policy/commit/9df696548aac424140d0db0b1f1181a19c8ba2a0)]
21
+
7
22
  ### [1.3.9](https://github.com/eea/volto-bise-policy/compare/1.3.8...1.3.9) - 27 August 2026
8
23
 
9
24
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-bise-policy",
3
- "version": "1.3.9",
3
+ "version": "1.3.10",
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",
@@ -85,6 +85,34 @@ export const NRRArticleView = ({ value }) => {
85
85
  );
86
86
  };
87
87
 
88
+ export const NRRLegislationView = ({ value }) => {
89
+ const items = parseItems(value);
90
+ if (items.length === 0) return null;
91
+
92
+ return (
93
+ <InlineList
94
+ label="NRR Legislation"
95
+ secondary
96
+ items={items}
97
+ render={(item) => item.title}
98
+ />
99
+ );
100
+ };
101
+
102
+ export const NRRBiogeographicalRegionView = ({ value }) => {
103
+ const items = parseItems(value);
104
+ if (items.length === 0) return null;
105
+
106
+ return (
107
+ <InlineList
108
+ label="Biogeographical region"
109
+ secondary
110
+ items={items}
111
+ render={(item) => item.title}
112
+ />
113
+ );
114
+ };
115
+
88
116
  export const NRRrelatedCaseStudiesView = ({ value }) => {
89
117
  const items = parseItems(value);
90
118
  if (items.length === 0) return null;
@@ -21,11 +21,12 @@ export default function CaseStudyExplorerView(props) {
21
21
  const hideFilters = caseStudiesIds ? true : false;
22
22
 
23
23
  const [activeFilters, setActiveFilters] = React.useState({
24
+ nrr_legislation: [],
25
+ nrr_biogeographical_region: [],
24
26
  measures_implemented: [],
25
27
  typology_of_measures: [],
26
28
  current_status: [],
27
29
  habitat_ecosystem_type: [],
28
- nrr_article: [],
29
30
  scale_of_planning: [],
30
31
  });
31
32
 
@@ -114,6 +114,16 @@ function CaseStudyFiltersComponent(props) {
114
114
 
115
115
  return (
116
116
  <>
117
+ <CaseStudyFilter
118
+ filterTitle="Legislation"
119
+ filterName="nrr_legislation"
120
+ {...props}
121
+ />
122
+ <CaseStudyFilter
123
+ filterTitle="Biogeographical region"
124
+ filterName="nrr_biogeographical_region"
125
+ {...props}
126
+ />
117
127
  <CaseStudyFilter
118
128
  filterTitle="Measures"
119
129
  filterName="measures_implemented"
@@ -144,11 +154,6 @@ function CaseStudyFiltersComponent(props) {
144
154
  filterName="habitat_ecosystem_type"
145
155
  {...props}
146
156
  />
147
- <CaseStudyFilter
148
- filterTitle="NRR Article"
149
- filterName="nrr_article"
150
- {...props}
151
- />
152
157
  <CaseStudyFilter
153
158
  filterTitle="Scale of Planning"
154
159
  filterName="scale_of_planning"
@@ -347,9 +352,16 @@ function ActiveFiltersComponent(props) {
347
352
  onRemoveFilter={removeFilter}
348
353
  />
349
354
  <FilterGroup
350
- filterName="nrr_article"
351
- filterLabel="NRR Article:"
352
- activeFilterCodes={activeFilters.nrr_article}
355
+ filterName="nrr_legislation"
356
+ filterLabel="Legislation:"
357
+ activeFilterCodes={activeFilters.nrr_legislation}
358
+ filters={filters}
359
+ onRemoveFilter={removeFilter}
360
+ />
361
+ <FilterGroup
362
+ filterName="nrr_biogeographical_region"
363
+ filterLabel="Biogeographical region:"
364
+ activeFilterCodes={activeFilters.nrr_biogeographical_region}
353
365
  filters={filters}
354
366
  onRemoveFilter={removeFilter}
355
367
  />
@@ -1,41 +1,24 @@
1
1
  import React from 'react';
2
+ import Image from '@plone/volto/components/theme/Image/Image';
2
3
 
3
4
  export default function FeatureDisplay({ feature }) {
4
5
  return feature ? (
5
6
  <div id="csepopup">
7
+ {feature.image ? (
8
+ <Image
9
+ className="csepopup-image"
10
+ src={feature.image}
11
+ alt={feature.title || ''}
12
+ />
13
+ ) : null}
6
14
  <h3>
7
15
  <strong>
8
16
  <a target="_blank" rel="noopener noreferrer" href={feature.path}>
9
17
  {feature.title}
18
+ <i class="ri-external-link-line"></i>
10
19
  </a>
11
20
  </strong>
12
21
  </h3>
13
- {/* <div>
14
- <h4>Measures implemented</h4>
15
- <ul>
16
- {feature.measures_implemented.map((item, index) => {
17
- return (
18
- <li key={index}>
19
- <a
20
- target="_blank"
21
- rel="noopener noreferrer"
22
- href={item['path']}
23
- >
24
- {item['title']}
25
- </a>
26
- </li>
27
- );
28
- })}
29
- </ul>
30
- </div> */}
31
- {/* <div>
32
- <h4>typology_of_measures </h4>
33
- <ul>
34
- {feature.typology_of_measures.map((item, index) => {
35
- return <li key={index}>{item}</li>;
36
- })}
37
- </ul>
38
- </div> */}
39
22
  </div>
40
23
  ) : null;
41
24
  }
@@ -9,6 +9,7 @@ describe('FeatureDisplay Component', () => {
9
9
  const feature = {
10
10
  title: 'Sample Feature',
11
11
  path: 'https://example.com/sample-feature',
12
+ image: 'https://example.com/preview.jpg',
12
13
  measures_implemented: [
13
14
  { path: 'https://example.com/nwrm-1', title: 'NWRM 1' },
14
15
  { path: 'https://example.com/nwrm-2', title: 'NWRM 2' },
@@ -23,6 +24,22 @@ describe('FeatureDisplay Component', () => {
23
24
  'href',
24
25
  'https://example.com/sample-feature',
25
26
  );
27
+ expect(getByRole('img')).toHaveAttribute(
28
+ 'src',
29
+ 'https://example.com/preview.jpg',
30
+ );
31
+ });
32
+
33
+ it('does not render an image when the feature has none', () => {
34
+ const { queryByRole, getByRole } = render(
35
+ <FeatureDisplay feature={{ title: 'No image', path: '/no-image' }} />,
36
+ );
37
+
38
+ expect(queryByRole('img')).not.toBeInTheDocument();
39
+ expect(getByRole('link', { name: 'No image' })).toHaveAttribute(
40
+ 'href',
41
+ '/no-image',
42
+ );
26
43
  });
27
44
 
28
45
  it('renders nothing when feature is null', () => {
@@ -14,6 +14,17 @@ function InfoOverlay({
14
14
  const { map } = useMapContext();
15
15
  const [tooltip, setTooltipRef] = React.useState();
16
16
  const [showTooltip, setShowTooltip] = React.useState();
17
+ const overlayRef = React.useRef(null);
18
+
19
+ // `ol` is a fresh object literal on every render (see withOpenLayers) and
20
+ // `onFeatureSelect`/`hideFilters` can change identity; keep them in refs so
21
+ // the overlay and its map handler are created only once per mount.
22
+ const olRef = React.useRef(ol);
23
+ olRef.current = ol;
24
+ const onFeatureSelectRef = React.useRef(onFeatureSelect);
25
+ onFeatureSelectRef.current = onFeatureSelect;
26
+ const hideFiltersRef = React.useRef(hideFilters);
27
+ hideFiltersRef.current = hideFilters;
17
28
 
18
29
  const prevLayerId = usePrevious(layerId);
19
30
 
@@ -26,12 +37,13 @@ function InfoOverlay({
26
37
  React.useEffect(() => {
27
38
  if (!(map && tooltip)) return;
28
39
 
29
- const overlay = new ol.Overlay({
30
- element: document.getElementById('popup-overlay'),
40
+ const overlay = new olRef.current.Overlay({
41
+ element: tooltip,
31
42
  positioning: 'bottom-center',
32
43
  offset: [0, -10],
33
44
  stopEvent: false,
34
45
  });
46
+ overlayRef.current = overlay;
35
47
  map.addOverlay(overlay);
36
48
 
37
49
  function handler(evt) {
@@ -41,7 +53,7 @@ function InfoOverlay({
41
53
 
42
54
  if (
43
55
  features.length &&
44
- !hideFilters // && !isCluster(features)
56
+ !hideFiltersRef.current // && !isCluster(features)
45
57
  ) {
46
58
  setShowTooltip(true);
47
59
  } else {
@@ -49,7 +61,7 @@ function InfoOverlay({
49
61
  if (evt.originalEvent.target.tagName === 'A') return;
50
62
  setShowTooltip(false);
51
63
  popupOverlay.style.display = 'none';
52
- onFeatureSelect(null);
64
+ onFeatureSelectRef.current(null);
53
65
  }
54
66
  }
55
67
 
@@ -58,8 +70,25 @@ function InfoOverlay({
58
70
  return () => {
59
71
  map.un('click', handler);
60
72
  map.removeOverlay(overlay);
73
+ overlayRef.current = null;
61
74
  };
62
- }, [map, tooltip, onFeatureSelect, hideFilters, ol]); //
75
+ }, [map, tooltip]);
76
+
77
+ // Anchor the popup to the selected case study and show it above the marker.
78
+ // Selecting a cluster (null feature) or clicking the map clears the popup.
79
+ React.useEffect(() => {
80
+ const overlay = overlayRef.current;
81
+ if (!overlay) return;
82
+
83
+ const coordinates = selectedFeature?.geometry?.flatCoordinates;
84
+ if (coordinates) {
85
+ overlay.setPosition(coordinates);
86
+ setShowTooltip(true);
87
+ } else {
88
+ overlay.setPosition(undefined);
89
+ setShowTooltip(false);
90
+ }
91
+ }, [selectedFeature, tooltip]);
63
92
 
64
93
  const [isClient, setIsClient] = React.useState(false);
65
94
  React.useEffect(() => setIsClient(true), []);
@@ -31,6 +31,7 @@ jest.mock('ol/Overlay', () => {
31
31
  const mockOl = {
32
32
  Overlay: jest.fn((config) => ({
33
33
  element: config.element,
34
+ setPosition: jest.fn(),
34
35
  })),
35
36
  };
36
37
 
@@ -111,7 +112,8 @@ describe('InfoOverlay', () => {
111
112
  handler(evt);
112
113
 
113
114
  expect(onFeatureSelect).toHaveBeenCalledWith(null);
114
- expect(document.getElementById('popup-overlay').style.display).toBe('none');
115
+ const overlay = mockOl.Overlay.mock.results[0].value;
116
+ expect(overlay.element.style.display).toBe('none');
115
117
  });
116
118
 
117
119
  it('ignores click when target is a link (A tag)', () => {
@@ -135,4 +137,23 @@ describe('InfoOverlay', () => {
135
137
 
136
138
  expect(onFeatureSelect).not.toHaveBeenCalled();
137
139
  });
140
+
141
+ it('positions the overlay over the selected feature', () => {
142
+ render(
143
+ <InfoOverlay
144
+ {...defaultProps}
145
+ selectedFeature={{ geometry: { flatCoordinates: [10, 20] } }}
146
+ />,
147
+ );
148
+
149
+ const overlay = mockOl.Overlay.mock.results[0].value;
150
+ expect(overlay.setPosition).toHaveBeenCalledWith([10, 20]);
151
+ });
152
+
153
+ it('clears the overlay position when no feature is selected', () => {
154
+ render(<InfoOverlay {...defaultProps} selectedFeature={null} />);
155
+
156
+ const overlay = mockOl.Overlay.mock.results[0].value;
157
+ expect(overlay.setPosition).toHaveBeenCalledWith(undefined);
158
+ });
138
159
  });
@@ -35,6 +35,14 @@
35
35
  opacity: 0.55;
36
36
  }
37
37
 
38
+ .ol-overlaycontainer {
39
+ z-index: 1 !important;
40
+ }
41
+
42
+ .ol-viewport {
43
+ overflow: visible !important;
44
+ }
45
+
38
46
  #csepopup > span {
39
47
  display: 'block';
40
48
  margin-bottom: '10px';
@@ -42,21 +50,30 @@
42
50
  }
43
51
 
44
52
  #csepopup {
45
- // width: 300px;
46
- width: 100%;
53
+ width: 300px;
47
54
  // height: 100%;
48
55
  padding: 1em;
56
+ background-color: #f9f9f9;
49
57
  // border: 1px solid black;
50
- // font-size: 14px;
51
58
  // line-height: initial;
52
59
  font-size: 1rem;
53
60
 
61
+ .csepopup-image {
62
+ display: block;
63
+ width: 100%;
64
+ max-width: 260px;
65
+ max-height: 160px;
66
+ margin-bottom: 10px;
67
+ object-fit: cover;
68
+ }
69
+
54
70
  a:hover {
55
71
  color: #289588;
56
72
  }
57
73
 
58
74
  h3 {
59
- font-size: 1.4rem;
75
+ margin: 0px;
76
+ font-size: 1rem;
60
77
  }
61
78
 
62
79
  h4 {
@@ -96,6 +113,10 @@
96
113
  max-height: 133px;
97
114
  }
98
115
  }
116
+
117
+ .ri-external-link-line {
118
+ margin-left: 0.25rem;
119
+ }
99
120
  }
100
121
 
101
122
  #external-popup-overlay {
@@ -104,7 +104,8 @@ export function getFeatures({ cases, ol }) {
104
104
  properties,
105
105
  'habitat_ecosystem_type',
106
106
  ),
107
- nrr_article: properties.nrr_article,
107
+ nrr_legislation: properties.nrr_legislation,
108
+ nrr_biogeographical_region: properties.nrr_biogeographical_region,
108
109
  scale_of_planning: properties.scale_of_planning,
109
110
  description: properties.description,
110
111
  index,
@@ -143,11 +144,12 @@ function hasWholeWord(text, needle) {
143
144
  }
144
145
 
145
146
  const FILTER_FIELDS = [
147
+ 'nrr_legislation',
148
+ 'nrr_biogeographical_region',
146
149
  'measures_implemented',
147
150
  'typology_of_measures',
148
151
  'current_status',
149
152
  'habitat_ecosystem_type',
150
- 'nrr_article',
151
153
  'scale_of_planning',
152
154
  ];
153
155
 
@@ -96,11 +96,12 @@ describe('utils.js', () => {
96
96
  },
97
97
  ];
98
98
  const noFilters = {
99
+ nrr_legislation: [],
100
+ nrr_biogeographical_region: [],
99
101
  measures_implemented: [],
100
102
  typology_of_measures: [],
101
103
  current_status: [],
102
104
  habitat_ecosystem_type: [],
103
- nrr_article: [],
104
105
  scale_of_planning: [],
105
106
  };
106
107
 
@@ -127,7 +128,8 @@ describe('utils.js', () => {
127
128
  url: '/planned-river-case',
128
129
  current_status: 'planned',
129
130
  ecosystem_typology: ['River', 'Wetland'],
130
- nrr_article: [{ id: 'article-1', title: 'Article 1' }],
131
+ nrr_legislation: [{ id: 'article-1', title: 'Article 1' }],
132
+ nrr_biogeographical_region: [{ id: 'region-1', title: 'Alpine' }],
131
133
  scale_of_planning: 'Regional',
132
134
  },
133
135
  },
@@ -139,7 +141,8 @@ describe('utils.js', () => {
139
141
  url: '/future-case',
140
142
  current_status: 'Future',
141
143
  ecosystem_typology: ['Forest'],
142
- nrr_article: ['article-2'],
144
+ nrr_legislation: ['article-2'],
145
+ nrr_biogeographical_region: ['region-2'],
143
146
  scale_of_planning: 'National',
144
147
  },
145
148
  },
@@ -147,18 +150,23 @@ describe('utils.js', () => {
147
150
 
148
151
  expect(
149
152
  filterCases(cases, {
153
+ nrr_legislation: ['article-1'],
154
+ nrr_biogeographical_region: ['region-1'],
150
155
  measures_implemented: [],
151
156
  typology_of_measures: [],
152
157
  current_status: ['planned'],
153
158
  habitat_ecosystem_type: ['River'],
154
- nrr_article: ['article-1'],
155
159
  scale_of_planning: ['Regional'],
156
160
  }),
157
161
  ).toHaveLength(1);
158
- expect(getFilters(cases).nrr_article).toEqual({
162
+ expect(getFilters(cases).nrr_legislation).toEqual({
159
163
  'article-1': 'Article 1',
160
164
  'article-2': 'article-2',
161
165
  });
166
+ expect(getFilters(cases).nrr_biogeographical_region).toEqual({
167
+ 'region-1': 'Alpine',
168
+ 'region-2': 'region-2',
169
+ });
162
170
  });
163
171
 
164
172
  test('getFilters', () => {
@@ -176,7 +184,8 @@ describe('utils.js', () => {
176
184
  },
177
185
  current_status: {},
178
186
  habitat_ecosystem_type: {},
179
- nrr_article: {},
187
+ nrr_legislation: {},
188
+ nrr_biogeographical_region: {},
180
189
  scale_of_planning: {},
181
190
  });
182
191
  });
package/src/index.js CHANGED
@@ -30,6 +30,8 @@ import {
30
30
  NRRTypologyOfMeasuresView,
31
31
  NRREcosystemTypologyView,
32
32
  NRRArticleView,
33
+ NRRLegislationView,
34
+ NRRBiogeographicalRegionView,
33
35
  NRRrelatedCaseStudiesView,
34
36
  NRRMeasuresImplementedView,
35
37
  NRRHabitatEcosystemTypeView,
@@ -284,14 +286,23 @@ const applyConfig = (config) => {
284
286
  }
285
287
 
286
288
  config.widgets.id.geolocation = GeolocationWidget;
289
+ // NRR Measure widgets
287
290
  config.widgets.views.id.nrr_typology_of_measures = NRRTypologyOfMeasuresView;
288
291
  config.widgets.views.id.nrr_ecosystem_typology = NRREcosystemTypologyView;
292
+ config.widgets.views.id.nrr_article = NRRArticleView;
293
+ config.widgets.views.id.related_case_studies = NRRrelatedCaseStudiesView;
294
+ // NRR Case study widgets
295
+ config.widgets.views.id.nrr_measures_implemented = NRRMeasuresImplementedView;
289
296
  config.widgets.views.id.nrr_habitat_ecosystem_type =
290
297
  NRRHabitatEcosystemTypeView;
291
298
  config.widgets.views.id.nrr_scale_of_planning = NRRScaleOfPlanningView;
292
- config.widgets.views.id.nrr_article = NRRArticleView;
299
+ config.widgets.views.id.nrr_legislation = NRRLegislationView;
300
+ config.widgets.views.id.nrr_biogeographical_region =
301
+ NRRBiogeographicalRegionView;
293
302
  config.widgets.views.id.nrr_current_status = NRRCurrentStatusView;
294
- // EUNIS Widgets
303
+ config.widgets.id.nrr_habitat_directive = EUNISHDWidget;
304
+ config.widgets.views.id.nrr_habitat_directive = EUNISHDView;
305
+ // Marine EUNIS habitat Widgets
295
306
  config.widgets.id.eunis_national_json = EUNISCountryCodeWidget;
296
307
  config.widgets.views.id.eunis_national_json = EUNISCountryCodeView;
297
308
  config.widgets.id.eunis_regional_sea_convention_value_json =
@@ -311,8 +322,7 @@ const applyConfig = (config) => {
311
322
  config.widgets.views.id.eunis_msfd_relevant_classification_json =
312
323
  EUNISMSFDView;
313
324
  config.widgets.views.id.eunis_code = EUNISCodeView;
314
- config.widgets.views.id.related_case_studies = NRRrelatedCaseStudiesView;
315
- config.widgets.views.id.nrr_measures_implemented = NRRMeasuresImplementedView;
325
+
316
326
  config.widgets.views.widget.richtext = RichTextWidget;
317
327
 
318
328
  return config;