@eeacms/volto-bise-policy 1.2.32 → 1.2.34

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 (34) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +3 -1
  3. package/src/components/Widgets/GeolocationWidget.jsx +143 -0
  4. package/src/components/Widgets/GeolocationWidgetMapContainer.jsx +131 -0
  5. package/src/components/Widgets/NRRWidgets.jsx +95 -0
  6. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerEdit.jsx +5 -0
  7. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.jsx +107 -0
  8. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +89 -0
  9. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.jsx +339 -0
  10. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.test.jsx +111 -0
  11. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.jsx +330 -0
  12. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.test.jsx +166 -0
  13. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +237 -0
  14. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +176 -0
  15. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +41 -0
  16. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +32 -0
  17. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +98 -0
  18. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.test.jsx +160 -0
  19. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +82 -0
  20. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.test.jsx +153 -0
  21. package/src/components/manage/Blocks/CaseStudyExplorer/hooks.js +20 -0
  22. package/src/components/manage/Blocks/CaseStudyExplorer/images/icon-depth.png +0 -0
  23. package/src/components/manage/Blocks/CaseStudyExplorer/images/icon-light.png +0 -0
  24. package/src/components/manage/Blocks/CaseStudyExplorer/images/search.svg +3 -0
  25. package/src/components/manage/Blocks/CaseStudyExplorer/index.js +16 -0
  26. package/src/components/manage/Blocks/CaseStudyExplorer/mockJsdom.js +8 -0
  27. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +359 -0
  28. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less_old +201 -0
  29. package/src/components/manage/Blocks/CaseStudyExplorer/utils.js +144 -0
  30. package/src/components/manage/Blocks/CaseStudyExplorer/utils.test.js +88 -0
  31. package/src/components/manage/Blocks/index.js +2 -0
  32. package/src/express-middleware.js +37 -0
  33. package/src/index.js +29 -0
  34. package/theme/globals/site.overrides +12 -4
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ 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.2.34](https://github.com/eea/volto-bise-policy/compare/1.2.33...1.2.34) - 23 January 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: added NRR measure view widgets [laszlocseh - [`9468505`](https://github.com/eea/volto-bise-policy/commit/9468505f30d0755de0e2c3386e63229244137777)]
12
+ - feat: small color changes to case study map [laszlocseh - [`4cba979`](https://github.com/eea/volto-bise-policy/commit/4cba979d7292210659ca60f96e438a646aa94fab)]
13
+ - feat: small color changes to case study map [laszlocseh - [`ca6f43c`](https://github.com/eea/volto-bise-policy/commit/ca6f43c5cc81f16568650f2dfdca2591348bc75a)]
14
+ - feat: added GeolocationWidget for case studies [laszlocseh - [`17b6c1c`](https://github.com/eea/volto-bise-policy/commit/17b6c1c72c336c8c87291dc4303f02bcd82e7956)]
15
+ - feat: NRR case study map [laszlocseh - [`daafb50`](https://github.com/eea/volto-bise-policy/commit/daafb50825d893da08d64fdd48a9b287ec9ff916)]
16
+
17
+ #### :nail_care: Enhancements
18
+
19
+ - change: added typology_of_measures filter to case study map [laszlocseh - [`cd9d50e`](https://github.com/eea/volto-bise-policy/commit/cd9d50e9fefab49ddbf80c4b61113ec0beaf9b73)]
20
+ - change: added measures filter to case study map [laszlocseh - [`0a6bcdd`](https://github.com/eea/volto-bise-policy/commit/0a6bcdde399d2354e112a25ef3abe5cbd8f099cb)]
21
+
22
+ #### :house: Internal changes
23
+
24
+ - style: Automated code fix [eea-jenkins - [`e10917b`](https://github.com/eea/volto-bise-policy/commit/e10917b4a14a0897707fd50d33913e969d46ee89)]
25
+ - style: Automated code fix [eea-jenkins - [`702f53d`](https://github.com/eea/volto-bise-policy/commit/702f53d74ff560b58a48bf519fc9a40cb9bddb56)]
26
+ - style: Automated code fix [eea-jenkins - [`1b2648b`](https://github.com/eea/volto-bise-policy/commit/1b2648bedc44c4ca955499cb3267104e40e0a9e9)]
27
+ - style: Automated code fix [eea-jenkins - [`87e5dff`](https://github.com/eea/volto-bise-policy/commit/87e5dffcf2e7f362e095fc20b54e932f186bbcf8)]
28
+
29
+ #### :hammer_and_wrench: Others
30
+
31
+ - test: fix estlint and tests [laszlocseh - [`8867193`](https://github.com/eea/volto-bise-policy/commit/88671932c76859406c397eb9cb108ce4073f7952)]
32
+ - test: fix case study map tests [laszlocseh - [`8459b83`](https://github.com/eea/volto-bise-policy/commit/8459b839766a8fda4c729c558cb6ce940f1c8536)]
33
+ - fix eslint [laszlocseh - [`8cd8dfa`](https://github.com/eea/volto-bise-policy/commit/8cd8dfabd3ffaf49eb0b8fe5dec076f61e48a6ac)]
34
+ - added volto-openlayers-map to package.json [laszlocseh - [`2d9eb10`](https://github.com/eea/volto-bise-policy/commit/2d9eb101c4ce5fd490560e2a91c025a85a8474cb)]
35
+ ### [1.2.33](https://github.com/eea/volto-bise-policy/compare/1.2.32...1.2.33) - 9 December 2025
36
+
37
+ #### :bug: Bug Fixes
38
+
39
+ - fix: wrong z-index for the menu in some edge cases [laszlocseh - [`dc23db7`](https://github.com/eea/volto-bise-policy/commit/dc23db7a2f10ae3c64cb25835dbdda30b6f1e3e4)]
40
+
7
41
  ### [1.2.32](https://github.com/eea/volto-bise-policy/compare/1.2.31...1.2.32) - 3 December 2025
8
42
 
9
43
  #### :nail_care: Enhancements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-bise-policy",
3
- "version": "1.2.32",
3
+ "version": "1.2.34",
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",
@@ -13,6 +13,7 @@
13
13
  "react"
14
14
  ],
15
15
  "addons": [
16
+ "@eeacms/volto-openlayers-map",
16
17
  "@eeacms/volto-datablocks",
17
18
  "@eeacms/volto-block-toc",
18
19
  "@eeacms/volto-eea-design-system",
@@ -29,6 +30,7 @@
29
30
  "@eeacms/volto-datablocks": "*",
30
31
  "@eeacms/volto-eea-design-system": "*",
31
32
  "@eeacms/volto-eea-website-theme": "*",
33
+ "@eeacms/volto-openlayers-map": "1.0.1",
32
34
  "@eeacms/volto-resize-helper": "*",
33
35
  "@plone-collective/volto-authomatic": "2.0.1",
34
36
  "esri-loader": "3.7.0",
@@ -0,0 +1,143 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Input, Label, Button } from 'semantic-ui-react';
4
+ import config from '@plone/volto/registry';
5
+
6
+ import { injectIntl } from 'react-intl';
7
+ import { FormFieldWrapper } from '@plone/volto/components';
8
+ import MapContainer from './GeolocationWidgetMapContainer';
9
+
10
+ const defaultValue = {
11
+ latitude: 55.6761,
12
+ longitude: 12.5683,
13
+ };
14
+
15
+ const GeolocationWidget = (props) => {
16
+ const { id, value, onChange } = props;
17
+
18
+ const [address, setAddress] = useState('');
19
+ const [isFetching, setIsFetching] = useState();
20
+
21
+ const handleAddressChange = (event) => {
22
+ setAddress(event.target.value);
23
+ };
24
+
25
+ const handleSearch = async (e) => {
26
+ e.preventDefault();
27
+
28
+ const url = `https://nominatim.openstreetmap.org/search?q=${address}&format=json`;
29
+
30
+ const { corsProxyPath = '/cors-proxy', host, port } = config.settings;
31
+ const base = __SERVER__
32
+ ? `http://${host}:${port}`
33
+ : `${window.location.protocol}//${window.location.host}`;
34
+
35
+ const path = `${base}${corsProxyPath}/${url}`;
36
+
37
+ let locations;
38
+ setIsFetching(true);
39
+ try {
40
+ const response = await fetch(path);
41
+ locations = await response.json();
42
+ setIsFetching(false);
43
+ } catch (e) {
44
+ // eslint-disable-next-line no-console
45
+ console.log('error in fetching location', e);
46
+ }
47
+
48
+ if (locations?.length) {
49
+ const { lat, lon } = locations[0];
50
+ onChange(id, { latitude: lat, longitude: lon });
51
+ }
52
+ };
53
+
54
+ if (__SERVER__) return '';
55
+
56
+ const lat = value?.latitude ?? defaultValue.latitude;
57
+ const long = value?.longitude ?? defaultValue.longitude;
58
+ const mapKey = `${lat}_${long}`;
59
+
60
+ return (
61
+ <FormFieldWrapper {...props} className="geolocation-field">
62
+ <div className="ui form">
63
+ <div className="inline fields">
64
+ <div className="field">
65
+ <Input
66
+ type="text"
67
+ value={address}
68
+ onChange={handleAddressChange}
69
+ onKeyDown={(e) => {
70
+ if (e.key === 'Enter') handleSearch(e);
71
+ }}
72
+ />
73
+ </div>
74
+ <div className="field">
75
+ <Button onClick={handleSearch}>
76
+ {isFetching ? 'Loading' : 'Search'}
77
+ </Button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ <MapContainer
82
+ key={mapKey}
83
+ longitude={value?.longitude || defaultValue.longitude}
84
+ latitude={value?.latitude || defaultValue.latitude}
85
+ onChange={({ latitude, longitude }) =>
86
+ onChange(id, { ...value, longitude, latitude })
87
+ }
88
+ />
89
+ <div className="ui form">
90
+ <div className="inline fields">
91
+ <div className="field">
92
+ <Label>Latitude</Label>
93
+ <Input
94
+ type="number"
95
+ placeholder="latitude"
96
+ value={value?.latitude || defaultValue.latitude}
97
+ onChange={(e) =>
98
+ onChange(id, { ...value, latitude: e.target.value })
99
+ }
100
+ />
101
+ </div>
102
+ <div className="field">
103
+ <Label>Longitude</Label>
104
+ <Input
105
+ type="number"
106
+ placeholder="longitude"
107
+ value={value?.longitude || defaultValue.longitude}
108
+ onChange={(e) =>
109
+ onChange(id, { ...value, longitude: e.target.value })
110
+ }
111
+ />
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </FormFieldWrapper>
116
+ );
117
+ };
118
+
119
+ GeolocationWidget.propTypes = {
120
+ id: PropTypes.string.isRequired,
121
+ title: PropTypes.string.isRequired,
122
+ description: PropTypes.string,
123
+ required: PropTypes.bool,
124
+ error: PropTypes.arrayOf(PropTypes.string),
125
+ value: PropTypes.object,
126
+ onChange: PropTypes.func,
127
+ onEdit: PropTypes.func,
128
+ onDelete: PropTypes.func,
129
+ wrapped: PropTypes.bool,
130
+ placeholder: PropTypes.string,
131
+ };
132
+
133
+ GeolocationWidget.defaultProps = {
134
+ description: null,
135
+ required: false,
136
+ error: [],
137
+ value: null,
138
+ onChange: null,
139
+ onEdit: null,
140
+ onDelete: null,
141
+ };
142
+
143
+ export default injectIntl(GeolocationWidget);
@@ -0,0 +1,131 @@
1
+ import { withOpenLayers } from '@eeacms/volto-openlayers-map';
2
+ // import { openlayers as ol } from '@eeacms/volto-openlayers-map';
3
+ import {
4
+ Controls,
5
+ Interactions,
6
+ Layer,
7
+ Layers,
8
+ Map,
9
+ } from '@eeacms/volto-openlayers-map/api';
10
+ import React, { useState } from 'react';
11
+ import { useMapContext } from '@eeacms/volto-openlayers-map/hocs';
12
+
13
+ function PinInteraction({ longitude, latitude, onChange, ol }) {
14
+ const mapContext = useMapContext();
15
+ const { addLayer, addInteraction, map } = mapContext;
16
+
17
+ React.useEffect(() => {
18
+ if (
19
+ !map ||
20
+ typeof latitude === 'undefined' ||
21
+ typeof longitude === 'undefined'
22
+ )
23
+ return;
24
+
25
+ // Create a feature (the pin) and set it to be draggable
26
+ const pin = new ol.ol.Feature({
27
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([longitude, latitude])), // Initial location
28
+ });
29
+
30
+ // Style for the pin
31
+ pin.setStyle(
32
+ new ol.style.Style({
33
+ image: new ol.style.Icon({
34
+ anchor: [0.5, 1],
35
+ src: 'https://openlayers.org/en/latest/examples/data/icon.png',
36
+ }),
37
+ }),
38
+ );
39
+
40
+ // Create a vector layer to hold the pin
41
+ const vectorSource = new ol.source.Vector({
42
+ features: [pin],
43
+ });
44
+
45
+ const vectorLayer = new ol.layer.Vector({
46
+ source: vectorSource,
47
+ });
48
+
49
+ map.addLayer(vectorLayer);
50
+
51
+ // Add drag interaction
52
+ const dragInteraction = new ol.interaction.Modify({
53
+ source: vectorSource,
54
+ pixelTolerance: 20,
55
+ });
56
+
57
+ map.addInteraction(dragInteraction);
58
+
59
+ // Log the new position when the pin is dragged
60
+ dragInteraction.on('modifyend', function (event) {
61
+ const feature = event.features.getArray()[0];
62
+ const coordinates = feature.getGeometry().getCoordinates();
63
+ const lonLat = ol.proj.toLonLat(coordinates);
64
+ const [longitude, latitude] = lonLat;
65
+ onChange({ latitude, longitude });
66
+ });
67
+ }, [addInteraction, addLayer, map, onChange, latitude, longitude, ol]);
68
+
69
+ return null;
70
+ }
71
+
72
+ const TileSetLoader = (props) => {
73
+ const [tileWMSSources, setTileWMSSources] = useState([]);
74
+ const { ol } = props;
75
+
76
+ React.useEffect(() => {
77
+ setTileWMSSources([
78
+ new ol.source.TileWMS({
79
+ url: 'https://gisco-services.ec.europa.eu/maps/service',
80
+ params: {
81
+ LAYERS: 'OSMBlossomComposite',
82
+ TILED: true,
83
+ },
84
+ serverType: 'geoserver',
85
+ transition: 0,
86
+ }),
87
+ ]);
88
+ }, [ol]);
89
+
90
+ return tileWMSSources ? (
91
+ <MapContainer {...props} source={tileWMSSources[0]} />
92
+ ) : null;
93
+ };
94
+
95
+ const MapContainer = (props) => {
96
+ const { longitude, latitude, source, onChange, ol } = props;
97
+ return (
98
+ <Map
99
+ view={{
100
+ center: ol.proj.fromLonLat([longitude, latitude]),
101
+ showFullExtent: true,
102
+ zoom: 15,
103
+ }}
104
+ pixelRatio={1}
105
+ controls={ol.control.defaults({ attribution: false })}
106
+ >
107
+ <Layers>
108
+ <Controls attribution={false} zoom={false} />
109
+ <PinInteraction
110
+ ol={ol}
111
+ latitude={latitude}
112
+ longitude={longitude}
113
+ onChange={onChange}
114
+ />
115
+ <Interactions
116
+ doubleClickZoom={true}
117
+ dragAndDrop={true}
118
+ dragPan={true}
119
+ keyboardPan={true}
120
+ keyboardZoom={true}
121
+ mouseWheelZoom={true}
122
+ pointer={true}
123
+ select={true}
124
+ />
125
+ <Layer.Tile source={source} zIndex={0} />
126
+ </Layers>
127
+ </Map>
128
+ );
129
+ };
130
+
131
+ export default withOpenLayers(TileSetLoader);
@@ -0,0 +1,95 @@
1
+ export const NRRTypologyOfMeasuresView = ({ value }) => {
2
+ let parsedValue = value;
3
+
4
+ if (typeof value === 'string') {
5
+ try {
6
+ parsedValue = JSON.parse(value);
7
+ } catch (e) {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ const items = Array.isArray(parsedValue)
13
+ ? parsedValue
14
+ : parsedValue?.value || [];
15
+
16
+ if (!items || items.length === 0) return null;
17
+
18
+ return (
19
+ <div className="eunis-widget-view">
20
+ <span>
21
+ <b>Related typology of measures: </b>
22
+ </span>
23
+ {items.map((item) => (
24
+ <span key={item['@id']}>
25
+ <b>{item.title.split(' - ')[0]}</b> {item.title.split(' - ')[1]}
26
+ {items.indexOf(item) < items.length - 1 ? ' | ' : ''}
27
+ </span>
28
+ ))}
29
+ </div>
30
+ );
31
+ };
32
+
33
+ export const NRRArticleView = ({ value }) => {
34
+ let parsedValue = value;
35
+
36
+ if (typeof value === 'string') {
37
+ try {
38
+ parsedValue = JSON.parse(value);
39
+ } catch (e) {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ const items = Array.isArray(parsedValue)
45
+ ? parsedValue
46
+ : parsedValue?.value || [];
47
+
48
+ if (!items || items.length === 0) return null;
49
+
50
+ return (
51
+ <div className="eunis-widget-viewZ">
52
+ <span>
53
+ <b>NRR Article: </b>
54
+ </span>
55
+ {items.map((item) => (
56
+ <span key={item['@id']}>
57
+ {item.title.split(' - ')[0].split(' ')[1]}
58
+ {items.indexOf(item) < items.length - 1 ? ', ' : ''}
59
+ </span>
60
+ ))}
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export const NRREcosystemTypologyView = ({ value }) => {
66
+ let parsedValue = value;
67
+
68
+ if (typeof value === 'string') {
69
+ try {
70
+ parsedValue = JSON.parse(value);
71
+ } catch (e) {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ const items = Array.isArray(parsedValue)
77
+ ? parsedValue
78
+ : parsedValue?.value || [];
79
+
80
+ if (!items || items.length === 0) return null;
81
+
82
+ return (
83
+ <div className="eunis-widget-view">
84
+ <span>
85
+ <b>Ecosystem type: </b>
86
+ </span>
87
+ {items.map((item) => (
88
+ <span key={item['@id']}>
89
+ {item.title}
90
+ {items.indexOf(item) < items.length - 1 ? ', ' : ''}
91
+ </span>
92
+ ))}
93
+ </div>
94
+ );
95
+ };
@@ -0,0 +1,5 @@
1
+ import CaseStudyExplorerView from './CaseStudyExplorerView';
2
+
3
+ export default function CaseStudyExplorerEdit(props) {
4
+ return <CaseStudyExplorerView {...props} mode="edit" />;
5
+ }
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import { Grid } from 'semantic-ui-react'; // Dropdown,
3
+ import { addAppURL } from '@plone/volto/helpers';
4
+ // import config from '@plone/volto/registry';
5
+ import CaseStudyMap from './CaseStudyMap';
6
+ import { ActiveFilters, CaseStudyFilters, SearchBox } from './CaseStudyFilters';
7
+ // import CaseStudyList from './CaseStudyListing';
8
+
9
+ import { filterCases, getFilters } from './utils';
10
+ import { useCases } from './hooks';
11
+
12
+ import './styles.less';
13
+
14
+ export default function CaseStudyExplorerView(props) {
15
+ const cases_url = '/@@case-studies-map.arcgis.json';
16
+ let cases = useCases(addAppURL(cases_url));
17
+ const { caseStudiesIds } = props; // case studies from measure view
18
+ const [selectedCase, onSelectedCase] = React.useState(null);
19
+ const [searchInput, setSearchInput] = React.useState('');
20
+ const hideFilters = caseStudiesIds ? true : false;
21
+
22
+ const [activeFilters, setActiveFilters] = React.useState({
23
+ measures_implemented: [],
24
+ typology_of_measures: [],
25
+ });
26
+
27
+ const [activeItems, setActiveItems] = React.useState(cases);
28
+ const [filters, setFilters] = React.useState([]);
29
+ const [map, setMap] = React.useState();
30
+
31
+ React.useEffect(() => {
32
+ const _filters = getFilters(cases);
33
+ setFilters(_filters);
34
+ }, [
35
+ cases,
36
+ activeFilters.measures_implemented,
37
+ activeFilters.typology_of_measures,
38
+ activeItems.length,
39
+ ]);
40
+
41
+ React.useEffect(() => {
42
+ let activeItems = filterCases(
43
+ cases,
44
+ activeFilters,
45
+ caseStudiesIds,
46
+ searchInput,
47
+ );
48
+
49
+ setActiveItems(activeItems);
50
+ }, [caseStudiesIds, activeFilters, cases, searchInput]);
51
+
52
+ if (__SERVER__) return '';
53
+
54
+ return (
55
+ <div className="searchlib-block">
56
+ <Grid.Row>
57
+ {hideFilters ? null : (
58
+ <SearchBox
59
+ filters={filters}
60
+ activeFilters={activeFilters}
61
+ setActiveFilters={setActiveFilters}
62
+ searchInput={searchInput}
63
+ setSearchInput={setSearchInput}
64
+ map={map}
65
+ />
66
+ )}
67
+ </Grid.Row>
68
+ <Grid.Row>
69
+ {hideFilters ? null : (
70
+ <ActiveFilters
71
+ filters={filters}
72
+ activeFilters={activeFilters}
73
+ setActiveFilters={setActiveFilters}
74
+ />
75
+ )}
76
+ </Grid.Row>
77
+ <Grid.Row stretched={true} id="cse-filter">
78
+ {hideFilters ? null : (
79
+ <CaseStudyFilters
80
+ filters={filters}
81
+ activeFilters={activeFilters}
82
+ setActiveFilters={setActiveFilters}
83
+ map={map}
84
+ />
85
+ )}
86
+ </Grid.Row>
87
+ <Grid.Row>
88
+ {cases.length ? (
89
+ <Grid columns={12}>
90
+ <Grid.Column mobile={12} tablet={12} computer={12}>
91
+ <CaseStudyMap
92
+ items={cases}
93
+ activeItems={activeItems}
94
+ hideFilters={hideFilters}
95
+ selectedCase={selectedCase}
96
+ onSelectedCase={onSelectedCase}
97
+ searchInput={searchInput}
98
+ map={map}
99
+ setMap={setMap}
100
+ />
101
+ </Grid.Column>
102
+ </Grid>
103
+ ) : null}
104
+ </Grid.Row>
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,89 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { useCases } from './hooks';
5
+ import CaseStudyExplorerView from './CaseStudyExplorerView';
6
+
7
+ jest.mock('@plone/volto/helpers', () => ({
8
+ addAppURL: jest.fn((url) => url),
9
+ }));
10
+
11
+ jest.mock('@plone/volto/registry', () => ({
12
+ __esModule: true,
13
+ default: {
14
+ settings: { prefixPath: '' },
15
+ },
16
+ }));
17
+
18
+ jest.mock('./CaseStudyMap', () =>
19
+ jest.fn(() => <div id="case-study-map-mock">CaseStudyMapMock</div>),
20
+ );
21
+
22
+ jest.mock('./CaseStudyFilters', () => ({
23
+ ActiveFilters: jest.fn(() => (
24
+ <div id="active-filters-mock">ActiveFiltersMock</div>
25
+ )),
26
+ CaseStudyFilters: jest.fn(() => (
27
+ <div id="case-study-fiters-mock">CaseStudyFiltersMock</div>
28
+ )),
29
+ SearchBox: jest.fn(() => <div id="search-box-mock">SearchBoxMock</div>),
30
+ }));
31
+
32
+ jest.mock('./utils', () => ({
33
+ filterCases: jest.fn(() => [{ id: 1, title: 'Filtered Case' }]),
34
+ getFilters: jest.fn(() => ['filter1', 'filter2']),
35
+ }));
36
+
37
+ jest.mock('./hooks', () => ({
38
+ useCases: jest.fn(),
39
+ }));
40
+
41
+ beforeAll(() => {
42
+ global.__SERVER__ = false;
43
+ });
44
+
45
+ describe('CaseStudyExplorerView', () => {
46
+ beforeEach(() => {
47
+ jest.clearAllMocks();
48
+ });
49
+
50
+ it('renders filters and map when there are cases', () => {
51
+ useCases.mockReturnValue([{ id: 1, title: 'Test Case' }]);
52
+
53
+ const { container } = render(<CaseStudyExplorerView />);
54
+
55
+ expect(container.querySelector('#search-box-mock')).toBeInTheDocument();
56
+ expect(container.querySelector('#active-filters-mock')).toBeInTheDocument();
57
+ expect(
58
+ container.querySelector('#case-study-fiters-mock'),
59
+ ).toBeInTheDocument();
60
+ expect(container.querySelector('#case-study-map-mock')).toBeInTheDocument();
61
+ });
62
+
63
+ it('hides filters when caseStudiesIds is provided', () => {
64
+ useCases.mockReturnValue([{ id: 1, title: 'Test Case' }]);
65
+
66
+ const { container } = render(
67
+ <CaseStudyExplorerView caseStudiesIds={[1, 2]} />,
68
+ );
69
+
70
+ expect(container.querySelector('#search-box-mock')).not.toBeInTheDocument();
71
+ expect(
72
+ container.querySelector('#active-filters-mock'),
73
+ ).not.toBeInTheDocument();
74
+ expect(
75
+ container.querySelector('#case-study-fiters-mock'),
76
+ ).not.toBeInTheDocument();
77
+ expect(container.querySelector('#case-study-map-mock')).toBeInTheDocument();
78
+ });
79
+
80
+ it('does not render map when there are no cases', () => {
81
+ useCases.mockReturnValue([]);
82
+
83
+ const { container } = render(<CaseStudyExplorerView />);
84
+
85
+ expect(
86
+ container.querySelector('#case-study-map-mock'),
87
+ ).not.toBeInTheDocument();
88
+ });
89
+ });