@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
@@ -0,0 +1,237 @@
1
+ import React from 'react';
2
+
3
+ import cx from 'classnames';
4
+
5
+ import {
6
+ Map,
7
+ Layer,
8
+ Layers,
9
+ Controls,
10
+ useMapContext,
11
+ } from '@eeacms/volto-openlayers-map/api';
12
+ import { withOpenLayers } from '@eeacms/volto-openlayers-map';
13
+
14
+ import InfoOverlay from './InfoOverlay';
15
+ import FeatureInteraction from './FeatureInteraction';
16
+ import CaseStudyList from './CaseStudyListing';
17
+
18
+ import { centerAndResetMapZoom, getFeatures, scrollToElement } from './utils';
19
+
20
+ const styleCache = {};
21
+ const MapContextGateway = ({ setMap }) => {
22
+ const { map } = useMapContext();
23
+ React.useEffect(() => {
24
+ setMap(map);
25
+ }, [map, setMap]);
26
+ return null;
27
+ };
28
+
29
+ function CaseStudyMap(props) {
30
+ const {
31
+ items,
32
+ activeItems,
33
+ hideFilters,
34
+ selectedCase,
35
+ onSelectedCase,
36
+ searchInput,
37
+ map,
38
+ setMap,
39
+ ol,
40
+ } = props;
41
+ const features = getFeatures({ cases: items, ol });
42
+ const [resetMapButtonClass, setResetMapButtonClass] =
43
+ React.useState('inactive');
44
+
45
+ const [tileWMSSources] = React.useState([
46
+ new ol.source.TileWMS({
47
+ url: 'https://gisco-services.ec.europa.eu/maps/service',
48
+ params: {
49
+ // LAYERS: 'OSMBlossomComposite', OSMCartoComposite, OSMPositronComposite
50
+ LAYERS: 'OSMPositronComposite',
51
+ TILED: true,
52
+ },
53
+ serverType: 'geoserver',
54
+ transition: 0,
55
+ }),
56
+ ]);
57
+ const [pointsSource] = React.useState(
58
+ new ol.source.Vector({
59
+ features,
60
+ }),
61
+ );
62
+
63
+ const [clusterSource] = React.useState(
64
+ new ol.source.Cluster({
65
+ distance: 19,
66
+ source: pointsSource,
67
+ }),
68
+ );
69
+
70
+ React.useEffect(() => {
71
+ if (activeItems) {
72
+ pointsSource.clear();
73
+ pointsSource.addFeatures(getFeatures({ cases: activeItems, ol }));
74
+ }
75
+ }, [activeItems, pointsSource, ol]);
76
+
77
+ React.useEffect(() => {
78
+ if (!map) return null;
79
+
80
+ const moveendListener = (e) => {
81
+ // console.log('map.getView()', map.getView());
82
+ // console.log('selectedCase', selectedCase);
83
+ const mapZoom = Math.round(map.getView().getZoom() * 10) / 10;
84
+ const mapCenter = map.getView().getCenter();
85
+
86
+ if (selectedCase) {
87
+ const coords = selectedCase.geometry.flatCoordinates;
88
+ const pixel = map.getPixelFromCoordinate(coords);
89
+ map.getInteractions().array_[9].getFeatures().clear();
90
+ map
91
+ .getInteractions()
92
+ .array_[9].getFeatures()
93
+ .push(map.getFeaturesAtPixel(pixel)[0]);
94
+ } else {
95
+ map.getInteractions().array_[9].getFeatures().clear();
96
+ }
97
+
98
+ if (
99
+ mapZoom === 4 &&
100
+ JSON.stringify(mapCenter) ===
101
+ JSON.stringify(ol.proj.transform([10, 49], 'EPSG:4326', 'EPSG:3857'))
102
+ ) {
103
+ setResetMapButtonClass('inactive');
104
+ } else {
105
+ setResetMapButtonClass('active');
106
+ }
107
+ };
108
+
109
+ map.on('moveend', moveendListener);
110
+
111
+ return () => {
112
+ map.un('moveend', moveendListener);
113
+ };
114
+ }, [map, selectedCase, resetMapButtonClass, setResetMapButtonClass, ol]);
115
+
116
+ const clusterStyle = React.useMemo(
117
+ () => selectedClusterStyle({ selectedCase, ol }),
118
+ [selectedCase, ol],
119
+ );
120
+
121
+ const MapWithSelection = React.useMemo(() => Map, []);
122
+ // console.log('render');
123
+
124
+ return features.length > 0 ? (
125
+ <div id="ol-map-container" className="nrr-case-study-map-container">
126
+ <MapWithSelection
127
+ view={{
128
+ center: ol.proj.fromLonLat([10, 49]),
129
+ showFullExtent: true,
130
+ zoom: 4,
131
+ }}
132
+ pixelRatio={1}
133
+ // controls={ol.control.defaults({ attribution: false })}
134
+ >
135
+ <Controls attribution={false} />
136
+ <Layers>
137
+ {hideFilters ? null : (
138
+ <button
139
+ className={cx(
140
+ 'reset-map-button ui button secondary',
141
+ String(resetMapButtonClass),
142
+ )}
143
+ onClick={() => {
144
+ scrollToElement('search-input');
145
+ onSelectedCase(null);
146
+ centerAndResetMapZoom({ map, ol });
147
+ map.getInteractions().array_[9].getFeatures().clear();
148
+ }}
149
+ >
150
+ <span className="result-info-title">Reset map</span>
151
+ <i className="icon ri-map-2-line"></i>
152
+ </button>
153
+ )}
154
+ <InfoOverlay
155
+ selectedFeature={selectedCase}
156
+ onFeatureSelect={onSelectedCase}
157
+ layerId={tileWMSSources[0]}
158
+ hideFilters={hideFilters}
159
+ />
160
+ <FeatureInteraction
161
+ onFeatureSelect={onSelectedCase}
162
+ hideFilters={hideFilters}
163
+ selectedCase={selectedCase}
164
+ />
165
+ <Layer.Tile source={tileWMSSources[0]} zIndex={0} />
166
+ <Layer.Vector
167
+ style={clusterStyle}
168
+ source={clusterSource}
169
+ zIndex={1}
170
+ />
171
+ <MapContextGateway setMap={setMap} />
172
+ </Layers>
173
+ </MapWithSelection>
174
+ {hideFilters ? null : (
175
+ <CaseStudyList
176
+ map={map}
177
+ activeItems={activeItems}
178
+ selectedCase={selectedCase}
179
+ onSelectedCase={onSelectedCase}
180
+ pointsSource={pointsSource}
181
+ searchInput={searchInput}
182
+ />
183
+ )}
184
+ </div>
185
+ ) : null;
186
+ }
187
+
188
+ const selectedClusterStyle = ({ selectedFeature, ol }) => {
189
+ function _clusterStyle(feature) {
190
+ const size = feature.get('features').length;
191
+ let style = styleCache[size];
192
+
193
+ if (!style) {
194
+ style = new ol.style.Style({
195
+ image: new ol.style.Circle({
196
+ radius: 12 + Math.min(Math.floor(size / 3), 10),
197
+ stroke: new ol.style.Stroke({
198
+ color: '#fff',
199
+ }),
200
+ fill: new ol.style.Fill({
201
+ color: '#007B6C', // #006BB8 #309ebc
202
+ }),
203
+ }),
204
+ text: new ol.style.Text({
205
+ text: size.toString(),
206
+ fill: new ol.style.Fill({
207
+ color: '#fff',
208
+ }),
209
+ }),
210
+ });
211
+ styleCache[size] = style;
212
+ }
213
+
214
+ if (size === 1) {
215
+ // let color = feature.values_.features[0].values_['color'];
216
+ let color = '#289588'; // #0083E0 #50B0A4
217
+
218
+ return new ol.style.Style({
219
+ image: new ol.style.Circle({
220
+ radius: 6,
221
+ fill: new ol.style.Fill({
222
+ color: '#fff',
223
+ }),
224
+ stroke: new ol.style.Stroke({
225
+ color: color,
226
+ width: 6,
227
+ }),
228
+ }),
229
+ });
230
+ } else {
231
+ return style;
232
+ }
233
+ }
234
+ return _clusterStyle;
235
+ };
236
+
237
+ export default withOpenLayers(CaseStudyMap);
@@ -0,0 +1,176 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import CaseStudyMap from './CaseStudyMap';
5
+ import { getFeatures, centerAndResetMapZoom, scrollToElement } from './utils';
6
+
7
+ jest.mock('@eeacms/volto-openlayers-map', () => ({
8
+ withOpenLayers: (Comp) => Comp,
9
+ }));
10
+
11
+ jest.mock('@eeacms/volto-openlayers-map/api', () => ({
12
+ Map: ({ children }) => <div data-testid="mock-map">{children}</div>,
13
+ Layer: {
14
+ Tile: ({ source }) => <div data-testid="mock-tile-layer" />,
15
+ Vector: ({ source }) => <div data-testid="mock-vector-layer" />,
16
+ },
17
+ Layers: ({ children }) => <div data-testid="mock-layers">{children}</div>,
18
+ Controls: () => <div data-testid="mock-controls" />,
19
+ useMapContext: () => ({ map: mockMap }),
20
+ }));
21
+
22
+ // Mock child components so we don't render full DOM
23
+ jest.mock('./InfoOverlay', () => () => <div data-testid="mock-info-overlay" />);
24
+ jest.mock('./FeatureInteraction', () => () => (
25
+ <div data-testid="mock-feature-interaction" />
26
+ ));
27
+ jest.mock('./CaseStudyListing', () => () => (
28
+ <div data-testid="mock-case-study-list" />
29
+ ));
30
+
31
+ jest.mock('./utils', () => ({
32
+ getFeatures: jest.fn(),
33
+ centerAndResetMapZoom: jest.fn(),
34
+ scrollToElement: jest.fn(),
35
+ }));
36
+
37
+ const mockOl = {
38
+ source: {
39
+ TileWMS: jest.fn(() => ({ type: 'TileWMS' })),
40
+ Vector: jest.fn(() => ({
41
+ clear: jest.fn(),
42
+ addFeatures: jest.fn(),
43
+ })),
44
+ Cluster: jest.fn(() => ({})),
45
+ },
46
+ style: {
47
+ Style: jest.fn(() => ({})),
48
+ Circle: jest.fn(() => ({})),
49
+ Stroke: jest.fn(() => ({})),
50
+ Fill: jest.fn(() => ({})),
51
+ Text: jest.fn(() => ({})),
52
+ },
53
+ proj: {
54
+ transform: jest.fn(() => [111, 222]),
55
+ fromLonLat: jest.fn(() => [333, 444]),
56
+ },
57
+ };
58
+
59
+ const mockMap = {
60
+ on: jest.fn(),
61
+ un: jest.fn(),
62
+ getView: jest.fn(() => ({
63
+ getZoom: () => 4,
64
+ getCenter: () => [111, 222],
65
+ })),
66
+ getInteractions: jest.fn(() => ({
67
+ array_: [
68
+ {},
69
+ {},
70
+ {},
71
+ {},
72
+ {},
73
+ {},
74
+ {},
75
+ {},
76
+ {},
77
+ {
78
+ getFeatures: jest.fn(() => ({
79
+ clear: jest.fn(),
80
+ push: jest.fn(),
81
+ })),
82
+ },
83
+ ],
84
+ })),
85
+ getPixelFromCoordinate: jest.fn(() => [10, 20]),
86
+ getFeaturesAtPixel: jest.fn(() => ['f']),
87
+ };
88
+
89
+ describe('CaseStudyMap', () => {
90
+ beforeEach(() => {
91
+ jest.clearAllMocks();
92
+ });
93
+
94
+ it('renders nothing when no features', () => {
95
+ getFeatures.mockReturnValueOnce([]);
96
+ const { container } = render(
97
+ <CaseStudyMap items={[]} ol={mockOl} setMap={jest.fn()} />,
98
+ );
99
+ expect(container.firstChild).toBeNull();
100
+ });
101
+
102
+ it('renders map and layers when features exist', () => {
103
+ getFeatures.mockReturnValueOnce([{ id: 1 }]);
104
+ render(
105
+ <CaseStudyMap
106
+ items={[{ id: 1 }]}
107
+ ol={mockOl}
108
+ setMap={jest.fn()}
109
+ map={mockMap}
110
+ onSelectedCase={jest.fn()}
111
+ />,
112
+ );
113
+ expect(screen.getByTestId('mock-map')).toBeInTheDocument();
114
+ expect(screen.getByTestId('mock-tile-layer')).toBeInTheDocument();
115
+ expect(screen.getByTestId('mock-vector-layer')).toBeInTheDocument();
116
+ expect(screen.getByTestId('mock-case-study-list')).toBeInTheDocument();
117
+ });
118
+
119
+ it('does not render CaseStudyList when hideFilters is true', () => {
120
+ getFeatures.mockReturnValueOnce([{ id: 1 }]);
121
+ render(
122
+ <CaseStudyMap
123
+ items={[{ id: 1 }]}
124
+ ol={mockOl}
125
+ hideFilters
126
+ setMap={jest.fn()}
127
+ map={mockMap}
128
+ onSelectedCase={jest.fn()}
129
+ />,
130
+ );
131
+ expect(
132
+ screen.queryByTestId('mock-case-study-list'),
133
+ ).not.toBeInTheDocument();
134
+ });
135
+
136
+ it('clicking Reset map calls utils and clears features', () => {
137
+ getFeatures.mockReturnValueOnce([{ id: 1 }]);
138
+ const onSelectedCase = jest.fn();
139
+ render(
140
+ <CaseStudyMap
141
+ items={[{ id: 1 }]}
142
+ ol={mockOl}
143
+ setMap={jest.fn()}
144
+ map={mockMap}
145
+ onSelectedCase={onSelectedCase}
146
+ />,
147
+ );
148
+
149
+ fireEvent.click(screen.getByText('Reset map'));
150
+ expect(scrollToElement).toHaveBeenCalledWith('search-input');
151
+ expect(onSelectedCase).toHaveBeenCalledWith(null);
152
+ expect(centerAndResetMapZoom).toHaveBeenCalledWith({
153
+ map: mockMap,
154
+ ol: mockOl,
155
+ });
156
+ });
157
+
158
+ it('map moveend listener toggles resetMapButtonClass', () => {
159
+ getFeatures.mockReturnValueOnce([{ id: 1 }]);
160
+ render(
161
+ <CaseStudyMap
162
+ items={[{ id: 1 }]}
163
+ ol={mockOl}
164
+ setMap={jest.fn()}
165
+ map={mockMap}
166
+ onSelectedCase={jest.fn()}
167
+ />,
168
+ );
169
+
170
+ const moveendCb = mockMap.on.mock.calls.find((c) => c[0] === 'moveend')[1];
171
+
172
+ moveendCb();
173
+
174
+ expect(mockMap.getView).toHaveBeenCalled();
175
+ });
176
+ });
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+
3
+ export default function FeatureDisplay({ feature }) {
4
+ return feature ? (
5
+ <div id="csepopup">
6
+ <h3>
7
+ <strong>
8
+ <a target="_blank" rel="noopener noreferrer" href={feature.path}>
9
+ {feature.title}
10
+ </a>
11
+ </strong>
12
+ </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
+ </div>
40
+ ) : null;
41
+ }
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+
5
+ import FeatureDisplay from './FeatureDisplay';
6
+
7
+ describe('FeatureDisplay Component', () => {
8
+ it('renders correctly with feature data', () => {
9
+ const feature = {
10
+ title: 'Sample Feature',
11
+ path: 'https://example.com/sample-feature',
12
+ measures_implemented: [
13
+ { path: 'https://example.com/nwrm-1', title: 'NWRM 1' },
14
+ { path: 'https://example.com/nwrm-2', title: 'NWRM 2' },
15
+ ],
16
+ typology_of_measures: ['Sector 1', 'Sector 2'],
17
+ };
18
+
19
+ const { getByRole } = render(<FeatureDisplay feature={feature} />);
20
+
21
+ const titleLink = getByRole('link', { name: 'Sample Feature' });
22
+ expect(titleLink).toHaveAttribute(
23
+ 'href',
24
+ 'https://example.com/sample-feature',
25
+ );
26
+ });
27
+
28
+ it('renders nothing when feature is null', () => {
29
+ const { container } = render(<FeatureDisplay feature={null} />);
30
+ expect(container.firstChild).toBeNull();
31
+ });
32
+ });
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import { withOpenLayers } from '@eeacms/volto-openlayers-map';
3
+ import { useMapContext } from '@eeacms/volto-openlayers-map/api';
4
+ import { scrollToElement, zoomMapToFeatures } from './utils';
5
+
6
+ export const useStyles = ({ ol }) => {
7
+ const selected = React.useMemo(
8
+ () =>
9
+ new ol.style.Style({
10
+ image: new ol.style.Circle({
11
+ radius: 12,
12
+ fill: new ol.style.Fill({
13
+ color: '#ccc',
14
+ }),
15
+ stroke: new ol.style.Stroke({
16
+ color: '#fff',
17
+ width: 0,
18
+ }),
19
+ }),
20
+ }),
21
+ [ol],
22
+ );
23
+
24
+ const selectStyle = React.useCallback(
25
+ (feature) => {
26
+ // const color = feature.values_.features[0].values_['color'] || '#ccc';
27
+ const color = '#0A99FF'; // #004B7F #309ebc #0A99FF
28
+ // console.log(color);
29
+ selected.image_.getFill().setColor(color);
30
+ return selected;
31
+ },
32
+ [selected],
33
+ );
34
+
35
+ return { selected, selectStyle };
36
+ };
37
+
38
+ function FeatureInteraction({
39
+ onFeatureSelect,
40
+ hideFilters,
41
+ selectedCase,
42
+ ol,
43
+ }) {
44
+ // console.log('featureinteraction', selectedCase);
45
+ const { map } = useMapContext();
46
+ const { selectStyle } = useStyles({ ol });
47
+
48
+ const select = new ol.interaction.Select({
49
+ condition: ol.condition.click,
50
+ style: hideFilters ? null : selectStyle,
51
+ });
52
+
53
+ React.useEffect(() => {
54
+ if (!map) return;
55
+
56
+ select.on('select', function (e) {
57
+ const features = e.target.getFeatures().getArray();
58
+
59
+ features.forEach((feature) => {
60
+ const subfeatures = feature.values_.features;
61
+ if (subfeatures.length === 1) {
62
+ const selectedFeature = subfeatures[0].values_;
63
+ if (hideFilters) {
64
+ const url = window.location.origin + selectedFeature.path;
65
+ // window.open(url);
66
+ window.location.href = url;
67
+ }
68
+ onFeatureSelect(selectedFeature);
69
+ scrollToElement('ol-map-container');
70
+ // map.getView().animate({
71
+ // duration: 10,
72
+ // center: selectedFeature.geometry.flatCoordinates,
73
+ // });
74
+ } else {
75
+ onFeatureSelect(null);
76
+ zoomMapToFeatures({ map, features: subfeatures, ol });
77
+ }
78
+ });
79
+
80
+ return null;
81
+ });
82
+
83
+ map.addInteraction(select);
84
+
85
+ map.on('pointermove', (e) => {
86
+ const pixel = map.getEventPixel(e.originalEvent);
87
+ const hit = map.hasFeatureAtPixel(pixel);
88
+ map.getViewport().style.cursor = hit ? 'pointer' : '';
89
+ });
90
+
91
+ return () => map.removeInteraction(select);
92
+ // eslint-disable-next-line react-hooks/exhaustive-deps
93
+ }, [map, selectStyle, onFeatureSelect, hideFilters]);
94
+
95
+ return null;
96
+ }
97
+
98
+ export default withOpenLayers(FeatureInteraction);