@eeacms/volto-bise-policy 1.3.11 → 1.3.12

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 (39) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/package.json +2 -1
  3. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.jsx +404 -0
  4. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.test.jsx +366 -0
  5. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.jsx +193 -93
  6. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +205 -58
  7. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +329 -178
  8. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +243 -123
  9. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.jsx +61 -0
  10. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.test.jsx +89 -0
  11. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.jsx +128 -0
  12. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.test.jsx +160 -0
  13. package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.jsx +180 -0
  14. package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.test.jsx +125 -0
  15. package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.jsx +171 -0
  16. package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.test.jsx +181 -0
  17. package/src/components/manage/Blocks/CaseStudyExplorer/Pagination.jsx +81 -0
  18. package/src/components/manage/Blocks/CaseStudyExplorer/filters.js +350 -0
  19. package/src/components/manage/Blocks/CaseStudyExplorer/filters.test.js +399 -0
  20. package/src/components/manage/Blocks/CaseStudyExplorer/hooks.js +28 -4
  21. package/src/components/manage/Blocks/CaseStudyExplorer/hooks.test.js +73 -0
  22. package/src/components/manage/Blocks/CaseStudyExplorer/messages.js +138 -0
  23. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +1157 -317
  24. package/src/components/manage/Blocks/CaseStudyExplorer/testUtils.js +44 -46
  25. package/src/components/manage/Blocks/CaseStudyExplorer/utils.js +68 -184
  26. package/src/components/manage/Blocks/CaseStudyExplorer/utils.test.js +91 -158
  27. package/src/index.js +39 -0
  28. package/src/openlayers.js +51 -0
  29. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.jsx +0 -383
  30. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.test.jsx +0 -90
  31. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.jsx +0 -342
  32. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.test.jsx +0 -196
  33. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +0 -24
  34. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +0 -49
  35. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +0 -100
  36. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.test.jsx +0 -189
  37. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +0 -111
  38. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.test.jsx +0 -159
  39. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less_old +0 -201
package/CHANGELOG.md CHANGED
@@ -4,7 +4,37 @@ 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.11](https://github.com/eea/volto-bise-policy/compare/1.3.10...1.3.11) - 28 October 2026
7
+ ### [1.3.12](https://github.com/eea/volto-bise-policy/compare/1.3.11...1.3.12) - 5 November 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(CaseStudyMap): add selection highlight and title popup [laszlocseh - [`52c3d89`](https://github.com/eea/volto-bise-policy/commit/52c3d894f3da4ecf741ddf0fd9333298cca23187)]
12
+ - feat(CaseStudyExplorer): jump to the page of a case study selected on the map [laszlocseh - [`9c2fd56`](https://github.com/eea/volto-bise-policy/commit/9c2fd5660348bcace5aa4150082a3184ec8c0363)]
13
+ - feat(CaseStudyExplorer): add dynamic ecosystem chip overflow popup [laszlocseh - [`feb69e2`](https://github.com/eea/volto-bise-policy/commit/feb69e24a5cd1330e6fc7f78aaa64d38ca94c47f)]
14
+
15
+ #### :bug: Bug Fixes
16
+
17
+ - fix(CaseStudyExplorer): replace useId with a custom hook [laszlocseh - [`45fbbd0`](https://github.com/eea/volto-bise-policy/commit/45fbbd08c9f7be705d86cc37eda505b4a5ca6667)]
18
+ - fix(CaseStudyExplorer): remove media placeholder and adjust map marker size [laszlocseh - [`59a037f`](https://github.com/eea/volto-bise-policy/commit/59a037f718efb17975acd2b7bd084411af17513c)]
19
+ - fix(CaseStudyExplorer): hide pagination when there are no results [laszlocseh - [`2f07403`](https://github.com/eea/volto-bise-policy/commit/2f074037c98a181f5a40f7dc7832f41104660191)]
20
+ - fix: es lint [laszlocseh - [`f226bb7`](https://github.com/eea/volto-bise-policy/commit/f226bb740cad4d83615cd8eca22c091fdeff112c)]
21
+
22
+ #### :nail_care: Enhancements
23
+
24
+ - change(CaseStudyExplorer): update filters, design [laszlocseh - [`470a068`](https://github.com/eea/volto-bise-policy/commit/470a068a352e4388d131eb58cfadcf42e94bde87)]
25
+
26
+ #### :house: Internal changes
27
+
28
+ - style: Automated code fix [eea-jenkins - [`30fc35a`](https://github.com/eea/volto-bise-policy/commit/30fc35a061321bc19679f98adb74030c16a85c41)]
29
+ - style(CaseStudyExplorer): remove duplicate attribute [laszlocseh - [`538b42e`](https://github.com/eea/volto-bise-policy/commit/538b42e3e1c01544a865f778c6cd60582a14a1c4)]
30
+ - style: Automated code fix [eea-jenkins - [`0e2b827`](https://github.com/eea/volto-bise-policy/commit/0e2b8274a79aa0e1acab7725b293a69a3a595a37)]
31
+ - style: Automated code fix [eea-jenkins - [`c2f8597`](https://github.com/eea/volto-bise-policy/commit/c2f8597a3d54d8be36a6e34c6e9a8cbbfae33064)]
32
+
33
+ #### :hammer_and_wrench: Others
34
+
35
+ - test(CaseStudyExplorer): fix hooks.test.js [laszlocseh - [`c6d1c03`](https://github.com/eea/volto-bise-policy/commit/c6d1c039593072c1eaef56cb9d642699d61b1b84)]
36
+ - test(CaseStudyExplorer): update test expectations for UI changes [laszlocseh - [`43e2f34`](https://github.com/eea/volto-bise-policy/commit/43e2f349f724a7d6957f01fe8863da1f57740660)]
37
+ ### [1.3.11](https://github.com/eea/volto-bise-policy/compare/1.3.10...1.3.11) - 29 October 2026
8
38
 
9
39
  #### :house: Internal changes
10
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-bise-policy",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
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",
@@ -35,6 +35,7 @@
35
35
  "@plone-collective/volto-authomatic": "2.0.1",
36
36
  "dompurify": "^3.3.1",
37
37
  "esri-loader": "3.7.0",
38
+ "ol": "6.6.x",
38
39
  "plotly.js": "^2.28.0",
39
40
  "querystring": "0.2.1",
40
41
  "react-plotly.js": "2.6.0"
@@ -0,0 +1,404 @@
1
+ import React from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import cx from 'classnames';
4
+ import Image from '@plone/volto/components/theme/Image/Image';
5
+
6
+ import messages from './messages';
7
+ import {
8
+ asValues,
9
+ // getArticleInfo,
10
+ getFacetValues,
11
+ valueLabel,
12
+ } from './filters';
13
+ import { getStatusColor, visibleChipCount } from './utils';
14
+
15
+ /** Rows of chips a card shows before collapsing the rest into "+N". */
16
+ const ECOSYSTEM_ROWS = 2;
17
+ const CHIP_GAP_FALLBACK = 8;
18
+ let nextPanelId = 0;
19
+
20
+ // Volto may run on React versions that do not yet provide useId. Keep IDs
21
+ // stable for the lifetime of a component instance without relying on that API.
22
+ function usePanelId() {
23
+ const id = React.useRef(null);
24
+ if (id.current === null) id.current = `cse-panel-${++nextPanelId}`;
25
+ return id.current;
26
+ }
27
+
28
+ // Measure before the browser paints where possible, without warning on the
29
+ // server-rendered first pass.
30
+ const useIsomorphicLayoutEffect =
31
+ typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect;
32
+
33
+ // const STATUS_LABELS = {
34
+ // planned: messages.statusPlanned,
35
+ // ongoing: messages.statusOngoing,
36
+ // completed: messages.statusCompleted,
37
+ // };
38
+
39
+ function StatusBadge({ status }) {
40
+ // const intl = useIntl();
41
+ // const label = STATUS_LABELS[status];
42
+
43
+ return (
44
+ <span className="cse-card-status">
45
+ <span
46
+ className="cse-card-status-dot"
47
+ style={{ backgroundColor: getStatusColor(status) }}
48
+ aria-hidden="true"
49
+ />
50
+ {/* <span className="cse-card-status-label">
51
+ {label ? intl.formatMessage(label) : status}
52
+ </span> */}
53
+ </span>
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Ecosystem chips, wrapped over at most two rows. How many fit depends on the
59
+ * card width, so the full list is mirrored into a hidden, absolutely
60
+ * positioned list that is measured with real layout; the chips that overflow
61
+ * two rows collapse into a "+N" trigger that opens a popup listing exactly the
62
+ * hidden values.
63
+ *
64
+ * Without a layout engine (jsdom, SSR) nothing can be measured, so every chip
65
+ * is rendered and no trigger appears.
66
+ */
67
+ function EcosystemChips({ ecosystems }) {
68
+ const intl = useIntl();
69
+ const [visibleCount, setVisibleCount] = React.useState(ecosystems.length);
70
+ const [open, setOpen] = React.useState(false);
71
+ const measureRef = React.useRef(null);
72
+ const triggerRef = React.useRef(null);
73
+ const panelRef = React.useRef(null);
74
+ const panelId = usePanelId();
75
+
76
+ const measure = React.useCallback(() => {
77
+ const list = measureRef.current;
78
+ if (!list) return;
79
+
80
+ const chips = Array.from(list.querySelectorAll('.cse-card-chip'));
81
+ const width = list.clientWidth;
82
+
83
+ // `visibility: hidden` boxes still measure, but jsdom reports 0 for all of
84
+ // them: keep every chip visible instead of guessing.
85
+ if (!width) {
86
+ setVisibleCount(ecosystems.length);
87
+ return;
88
+ }
89
+
90
+ const gap =
91
+ parseFloat(window.getComputedStyle(list).columnGap) || CHIP_GAP_FALLBACK;
92
+ const tops = chips.map((chip) => chip.offsetTop);
93
+ const rows = [...new Set(tops)].sort((a, b) => a - b);
94
+ const moreChip = list.querySelector('.cse-card-chip-more-measure');
95
+
96
+ setVisibleCount(
97
+ visibleChipCount(
98
+ chips.map((chip, index) => ({
99
+ row: rows.indexOf(tops[index]),
100
+ right: chip.offsetLeft + chip.offsetWidth,
101
+ })),
102
+ {
103
+ rows: ECOSYSTEM_ROWS,
104
+ gap,
105
+ moreWidth: moreChip?.offsetWidth || 0,
106
+ width,
107
+ },
108
+ ),
109
+ );
110
+ }, [ecosystems.length]);
111
+
112
+ useIsomorphicLayoutEffect(() => {
113
+ measure();
114
+
115
+ if (typeof ResizeObserver === 'undefined') return undefined;
116
+ const observer = new ResizeObserver(measure);
117
+ if (measureRef.current) observer.observe(measureRef.current);
118
+
119
+ return () => observer.disconnect();
120
+ }, [measure]);
121
+
122
+ React.useEffect(() => {
123
+ if (!open) return undefined;
124
+
125
+ const onDocumentClick = (event) => {
126
+ if (!panelRef.current?.parentElement?.contains(event.target)) {
127
+ setOpen(false);
128
+ }
129
+ };
130
+ const onKeyDown = (event) => {
131
+ if (event.key !== 'Escape') return;
132
+ setOpen(false);
133
+ triggerRef.current?.focus();
134
+ };
135
+
136
+ document.addEventListener('click', onDocumentClick);
137
+ document.addEventListener('keydown', onKeyDown);
138
+ panelRef.current?.focus();
139
+
140
+ return () => {
141
+ document.removeEventListener('click', onDocumentClick);
142
+ document.removeEventListener('keydown', onKeyDown);
143
+ };
144
+ }, [open]);
145
+
146
+ if (!ecosystems.length) return null;
147
+
148
+ const shown = Math.min(visibleCount, ecosystems.length);
149
+ const hidden = ecosystems.length - shown;
150
+
151
+ return (
152
+ <div className="cse-card-ecosystems-wrap">
153
+ <ul className="cse-card-ecosystems">
154
+ {ecosystems.slice(0, shown).map((ecosystem, index) => (
155
+ <li className="cse-card-chip" key={`${ecosystem}-${index}`}>
156
+ {ecosystem}
157
+ </li>
158
+ ))}
159
+ {hidden > 0 ? (
160
+ <li className="cse-card-chip cse-card-chip-more">
161
+ <button
162
+ type="button"
163
+ ref={triggerRef}
164
+ className="cse-card-chip-more-trigger"
165
+ aria-expanded={open}
166
+ aria-controls={panelId}
167
+ aria-haspopup="dialog"
168
+ onClick={() => setOpen((wasOpen) => !wasOpen)}
169
+ >
170
+ {intl.formatMessage(messages.moreEcosystems, { count: hidden })}
171
+ <span className="cse-visually-hidden">
172
+ {' '}
173
+ {intl.formatMessage(messages.hiddenEcosystems)}
174
+ </span>
175
+ </button>
176
+ </li>
177
+ ) : null}
178
+ </ul>
179
+
180
+ {hidden > 0 && open ? (
181
+ <div
182
+ ref={panelRef}
183
+ id={panelId}
184
+ className="cse-card-ecosystems-panel"
185
+ role="dialog"
186
+ aria-label={intl.formatMessage(messages.hiddenEcosystems)}
187
+ tabIndex={-1}
188
+ >
189
+ <ul className="cse-card-ecosystems cse-card-ecosystems-panel-list">
190
+ {ecosystems.slice(shown).map((ecosystem, index) => (
191
+ <li className="cse-card-chip" key={`${ecosystem}-${index}`}>
192
+ {ecosystem}
193
+ </li>
194
+ ))}
195
+ </ul>
196
+ </div>
197
+ ) : null}
198
+
199
+ <ul
200
+ ref={measureRef}
201
+ className="cse-card-ecosystems cse-card-ecosystems-measure"
202
+ aria-hidden="true"
203
+ >
204
+ {ecosystems.map((ecosystem, index) => (
205
+ <li className="cse-card-chip" key={`${ecosystem}-${index}`}>
206
+ {ecosystem}
207
+ </li>
208
+ ))}
209
+ <li className="cse-card-chip cse-card-chip-more-measure">
210
+ {intl.formatMessage(messages.moreEcosystems, {
211
+ count: ecosystems.length,
212
+ })}
213
+ </li>
214
+ </ul>
215
+ </div>
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Related measures, collapsed behind a full-width disclosure bar. The bar
221
+ * owns the count; the panel stays mounted so its height can animate, and is
222
+ * `inert` while collapsed so its links stay out of the tab order. Nothing is
223
+ * rendered at all for a case study without measures.
224
+ */
225
+ function RelatedMeasures({ measures }) {
226
+ const intl = useIntl();
227
+ const panelId = usePanelId();
228
+ const [open, setOpen] = React.useState(false);
229
+
230
+ if (!measures.length) return null;
231
+
232
+ return (
233
+ <div className="cse-card-measures">
234
+ <button
235
+ type="button"
236
+ className="cse-card-measures-toggle"
237
+ aria-expanded={open}
238
+ aria-controls={panelId}
239
+ onClick={() => setOpen((value) => !value)}
240
+ >
241
+ <span className="cse-card-measures-count">
242
+ {intl.formatNumber(measures.length)}
243
+ </span>
244
+ <span className="cse-card-measures-title">
245
+ {intl.formatMessage(messages.relatedMeasures)}
246
+ </span>
247
+ <span className="cse-card-measures-chevron">
248
+ <i className="icon ri-arrow-down-s-line" aria-hidden="true" />
249
+ </span>
250
+ </button>
251
+ <div
252
+ id={panelId}
253
+ className="cse-card-measures-panel"
254
+ data-open={open ? 'true' : 'false'}
255
+ inert={open ? undefined : ''}
256
+ >
257
+ <div className="cse-card-measures-panel-inner">
258
+ <ul className="cse-card-measures-list">
259
+ {measures.map((measure) => (
260
+ <li key={measure.id || measure.path || measure.title}>
261
+ <a
262
+ className="cse-card-measure-link"
263
+ href={measure.path}
264
+ title={measure.title}
265
+ >
266
+ <i className="icon ri-file-list-3-line" aria-hidden="true" />
267
+ <span className="cse-card-measure-title">
268
+ {measure.title}
269
+ </span>
270
+ <i
271
+ className="icon ri-arrow-right-s-line"
272
+ aria-hidden="true"
273
+ />
274
+ </a>
275
+ </li>
276
+ ))}
277
+ </ul>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ );
282
+ }
283
+
284
+ /**
285
+ * One result, laid out as image | main content, with a full-width related
286
+ * measures disclosure bar underneath. The title, the image and the footer's
287
+ * corner icon are all "open in a new tab" affordances; selecting a card (from
288
+ * a map pin) only highlights it, while "Show on map" is the reverse link to
289
+ * the map. Once the card is the selected one, that same button reads "Reset
290
+ * map" and clears the selection again.
291
+ */
292
+ function CaseStudyCard({ item, selected = false, onShowOnMap, onResetMap }) {
293
+ const intl = useIntl();
294
+ const properties = React.useMemo(() => item?.properties || {}, [item]);
295
+ // const { directives } = React.useMemo(
296
+ // () => getArticleInfo(properties),
297
+ // [properties],
298
+ // );
299
+ const ecosystems = React.useMemo(
300
+ () => getFacetValues(properties, 'ecosystem').map(valueLabel),
301
+ [properties],
302
+ );
303
+ const measures = React.useMemo(
304
+ () => asValues(properties.measures),
305
+ [properties],
306
+ );
307
+
308
+ return (
309
+ <article
310
+ className={cx('cse-card', {
311
+ selected,
312
+ 'cse-card-no-media': !properties.image,
313
+ })}
314
+ id={`cse-card-${encodeURIComponent(properties.path || '')}`}
315
+ data-selected={selected ? 'true' : undefined}
316
+ >
317
+ {properties.image ? (
318
+ <a
319
+ className="cse-card-media"
320
+ href={properties.path}
321
+ tabIndex={-1}
322
+ aria-hidden="true"
323
+ >
324
+ <Image src={properties.image} alt="" loading="lazy" />
325
+ </a>
326
+ ) : null}
327
+
328
+ <div className="cse-card-content">
329
+ <div className="cse-card-head">
330
+ <h3 className="cse-card-title">
331
+ <StatusBadge status={properties.current_status} />
332
+ <a
333
+ href={properties.path}
334
+ target="_blank"
335
+ rel="noopener noreferrer"
336
+ title={properties.title}
337
+ >
338
+ {properties.title}
339
+ <span className="cse-visually-hidden">
340
+ {' '}
341
+ {intl.formatMessage(messages.opensInNewTab)}
342
+ </span>
343
+ </a>
344
+ </h3>
345
+ <a
346
+ className="cse-card-open"
347
+ href={properties.path}
348
+ target="_blank"
349
+ rel="noopener noreferrer"
350
+ aria-label={intl.formatMessage(messages.openCaseStudy)}
351
+ title={intl.formatMessage(messages.openCaseStudy)}
352
+ >
353
+ <i className="icon ri-external-link-line" aria-hidden="true" />
354
+ <span className="cse-visually-hidden">
355
+ {intl.formatMessage(messages.opensInNewTab)}
356
+ </span>
357
+ </a>
358
+ </div>
359
+
360
+ <div className="cse-card-meta">
361
+ <StatusBadge status={properties.current_status} />
362
+ {/* {directives.length ? (
363
+ <>
364
+ <span className="cse-card-separator" aria-hidden="true">
365
+ ·
366
+ </span>
367
+ <span className="cse-card-directives">
368
+ {directives.map((directive) => (
369
+ <span className="cse-card-chip" key={directive}>
370
+ {directive}
371
+ </span>
372
+ ))}
373
+ </span>
374
+ </>
375
+ ) : null} */}
376
+ <button
377
+ type="button"
378
+ className="cse-card-show-on-map"
379
+ onClick={() =>
380
+ selected ? onResetMap?.(item) : onShowOnMap?.(item)
381
+ }
382
+ >
383
+ <i
384
+ className={cx(
385
+ 'icon',
386
+ selected ? 'ri-map-2-line' : 'ri-map-pin-line',
387
+ )}
388
+ aria-hidden="true"
389
+ />
390
+ {intl.formatMessage(
391
+ selected ? messages.resetMap : messages.showOnMap,
392
+ )}
393
+ </button>
394
+ </div>
395
+
396
+ <EcosystemChips ecosystems={ecosystems} />
397
+ </div>
398
+
399
+ <RelatedMeasures measures={measures} />
400
+ </article>
401
+ );
402
+ }
403
+
404
+ export default React.memo(CaseStudyCard);