@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,330 @@
1
+ import React from 'react';
2
+ import { withOpenLayers } from '@eeacms/volto-openlayers-map';
3
+
4
+ import {
5
+ centerAndResetMapZoom,
6
+ scrollToElement,
7
+ zoomMapToFeatures,
8
+ } from './utils';
9
+
10
+ const showPageNr = (pageNr, currentPage, numberOfPages) => {
11
+ // show first 5 pages
12
+ if (currentPage < 4 && pageNr <= 5) {
13
+ return true;
14
+ }
15
+
16
+ // show last 5 pages
17
+ if (numberOfPages - currentPage < 4 && numberOfPages - pageNr < 5) {
18
+ return true;
19
+ }
20
+
21
+ if (
22
+ currentPage >= 4 &&
23
+ numberOfPages - currentPage >= 4 &&
24
+ pageNr >= currentPage - 2 &&
25
+ pageNr <= currentPage + 2
26
+ ) {
27
+ return true;
28
+ }
29
+
30
+ return false;
31
+ };
32
+
33
+ function CaseStudyList(props) {
34
+ const { selectedCase, onSelectedCase, pointsSource, map, searchInput, ol } =
35
+ props;
36
+ const reSearch = new RegExp(`\\b(${searchInput})\\b`, 'gi');
37
+ const [currentPage, setCurrentPage] = React.useState(1);
38
+
39
+ const features = pointsSource
40
+ .getFeatures(selectedCase)
41
+ .sort((item1, item2) =>
42
+ item1.values_.title.localeCompare(item2.values_.title),
43
+ );
44
+ const numberOfPages = Math.ceil(features.length / 10);
45
+
46
+ const displayFatures = features.slice(
47
+ 10 * (currentPage - 1),
48
+ 10 * currentPage,
49
+ );
50
+
51
+ return displayFatures.length === 0 ? (
52
+ <>
53
+ <h3 style={{ margin: 'calc(2rem - 0.1em) 0 1rem' }}>
54
+ We could not find any results for your search criteria
55
+ </h3>
56
+ <ul>
57
+ <li>check the selected filters</li>
58
+ </ul>
59
+ </>
60
+ ) : (
61
+ <>
62
+ <div className="listing">
63
+ {selectedCase ? (
64
+ <div
65
+ className="content-box u-item listing-item result-item"
66
+ style={{
67
+ marginTop: '2em',
68
+ padding: 'em',
69
+ // border: '3px solid #f2f2f2',
70
+ // borderTop: '1em solid #f2f2f2',
71
+ paddingTop: 0,
72
+ backgroundColor: '#f2f2f2',
73
+ border: 'none',
74
+ }}
75
+ >
76
+ <div className="slot-top">
77
+ <div className="listing-body">
78
+ <h3 className="listing-header">
79
+ <a
80
+ target="_blank"
81
+ rel="noopener noreferrer"
82
+ href={selectedCase.path}
83
+ title={selectedCase.title}
84
+ datatest-id="selected-case"
85
+ >
86
+ {selectedCase.title}
87
+ </a>
88
+ </h3>
89
+ <p className="listing-description">
90
+ {selectedCase.description}
91
+ </p>
92
+ <div className="slot-bottom">
93
+ <div className="result-bottom">
94
+ {/* <div className="result-info">
95
+ <span className="result-info-title">Typology of measures:</span>
96
+ <span>
97
+ {selectedCase.typology_of_measures
98
+ ? selectedCase.typology_of_measures.join(', ')
99
+ : ''}
100
+ </span>
101
+ </div> */}
102
+ {/* <div className="result-info">
103
+ <span className="result-info-title">
104
+ Measures implemented:
105
+ </span>
106
+ {selectedCase.measures_implemented.map((measure, index) => {
107
+ return (
108
+ <span>
109
+ <a
110
+ target="_blank"
111
+ rel="noopener noreferrer"
112
+ href={measure.path}
113
+ >
114
+ {measure.title}
115
+ {index !==
116
+ selectedCase.measures_implemented.length - 1
117
+ ? ', '
118
+ : ''}
119
+ </a>
120
+ </span>
121
+ );
122
+ })}
123
+ </div> */}
124
+ <div
125
+ className="result-info show-on-map"
126
+ tabIndex="0"
127
+ role="button"
128
+ onKeyDown={() => {}}
129
+ onClick={() => {
130
+ // scroll to the map
131
+ scrollToElement('search-input');
132
+ // reset map zoom
133
+ onSelectedCase(null);
134
+ centerAndResetMapZoom({ map, ol });
135
+ map.getInteractions().array_[9].getFeatures().clear();
136
+ }}
137
+ >
138
+ <span
139
+ className="result-info-title"
140
+ data-testid="reset-map"
141
+ >
142
+ Reset map
143
+ </span>
144
+ <i className="icon ri-map-2-line"></i>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ ) : (
152
+ displayFatures.map((item, index) => {
153
+ return (
154
+ <div className="u-item listing-item result-item" key={index}>
155
+ <div className="slot-top">
156
+ <div className="listing-body">
157
+ <h3 className="listing-header">
158
+ <a
159
+ target="_blank"
160
+ rel="noopener noreferrer"
161
+ href={item.values_.path}
162
+ title={item.values_.title}
163
+ >
164
+ {item.values_.title}
165
+ </a>
166
+ </h3>
167
+ <p
168
+ className="listing-description"
169
+ dangerouslySetInnerHTML={{
170
+ __html: searchInput
171
+ ? item.values_.description.replaceAll(
172
+ reSearch,
173
+ '<b>$1</b>',
174
+ )
175
+ : item.values_.description,
176
+ }}
177
+ ></p>
178
+ <div className="slot-bottom">
179
+ <div className="result-bottom">
180
+ {/* <div className="result-info">
181
+ <span className="result-info-title">Typology of measures:</span>
182
+ <span>{item.values_.typology_of_measures.join(', ')}</span>
183
+ </div> */}
184
+ {/* <div className="result-info">
185
+ <span className="result-info-title">
186
+ Measures implemented:
187
+ </span>
188
+
189
+ {item.values_.measures_implemented.map(
190
+ (measure, index) => {
191
+ return (
192
+ <span key={index}>
193
+ <a
194
+ target="_blank"
195
+ rel="noopener noreferrer"
196
+ href={measure.path}
197
+ >
198
+ {measure.title}
199
+ {index !==
200
+ item.values_.measures_implemented.length - 1
201
+ ? ', '
202
+ : ''}
203
+ </a>
204
+ </span>
205
+ );
206
+ },
207
+ )}
208
+ </div> */}
209
+ <div
210
+ className="result-info show-on-map"
211
+ tabIndex="0"
212
+ role="button"
213
+ onKeyDown={() => {}}
214
+ onClick={() => {
215
+ map
216
+ .getInteractions()
217
+ .array_[9].getFeatures()
218
+ .clear();
219
+ // scroll to the map
220
+ scrollToElement('ol-map-container');
221
+
222
+ zoomMapToFeatures({
223
+ map,
224
+ features: [item],
225
+ threshold: 5000,
226
+ ol,
227
+ });
228
+ onSelectedCase(item.values_);
229
+
230
+ setTimeout(() => {
231
+ const coords =
232
+ item.values_.geometry.flatCoordinates;
233
+ const pixel = map.getPixelFromCoordinate(coords);
234
+ map
235
+ .getInteractions()
236
+ .array_[9].getFeatures()
237
+ .push(map.getFeaturesAtPixel(pixel)[0]);
238
+ }, 1100);
239
+ }}
240
+ >
241
+ <span className="result-info-title">Show on map</span>
242
+ <i className="icon ri-road-map-line"></i>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ );
250
+ })
251
+ )}
252
+ </div>
253
+ {!selectedCase ? (
254
+ <div className="search-body-footer">
255
+ <div className="ui centered grid">
256
+ <div className="center aligned column">
257
+ <div className="prev-next-paging">
258
+ <div className="paging-wrapper">
259
+ {currentPage !== 1 ? (
260
+ <button
261
+ className="ui button prev double-angle"
262
+ onClick={() => {
263
+ setCurrentPage(1);
264
+ }}
265
+ ></button>
266
+ ) : (
267
+ ''
268
+ )}
269
+ {currentPage !== 1 ? (
270
+ <button
271
+ className="ui button prev single-angle"
272
+ onClick={() => {
273
+ setCurrentPage(currentPage - 1);
274
+ }}
275
+ ></button>
276
+ ) : (
277
+ ''
278
+ )}
279
+ {Array.from(Array(numberOfPages).keys()).map((index) => {
280
+ const pageNr = index + 1;
281
+ return showPageNr(pageNr, currentPage, numberOfPages) ? (
282
+ <button
283
+ className={
284
+ 'ui button pagination-item' +
285
+ (currentPage === pageNr ? ' active' : '')
286
+ }
287
+ onClick={() => {
288
+ setCurrentPage(pageNr);
289
+ }}
290
+ key={index}
291
+ >
292
+ {pageNr}
293
+ </button>
294
+ ) : (
295
+ ''
296
+ );
297
+ })}
298
+ {currentPage !== numberOfPages ? (
299
+ <button
300
+ className="ui button next single-angle"
301
+ onClick={() => {
302
+ setCurrentPage(currentPage + 1);
303
+ }}
304
+ ></button>
305
+ ) : (
306
+ ''
307
+ )}
308
+ {currentPage !== numberOfPages ? (
309
+ <button
310
+ className="ui button next double-angle"
311
+ onClick={() => {
312
+ setCurrentPage(numberOfPages);
313
+ }}
314
+ ></button>
315
+ ) : (
316
+ ''
317
+ )}{' '}
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ ) : (
324
+ ''
325
+ )}
326
+ </>
327
+ );
328
+ }
329
+
330
+ export default withOpenLayers(CaseStudyList);
@@ -0,0 +1,166 @@
1
+ import React from 'react';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import CaseStudyList from './CaseStudyListing';
5
+ import {
6
+ centerAndResetMapZoom,
7
+ scrollToElement,
8
+ zoomMapToFeatures,
9
+ } from './utils';
10
+
11
+ jest.mock('@eeacms/volto-openlayers-map', () => ({
12
+ withOpenLayers: (Component) => Component,
13
+ }));
14
+
15
+ jest.mock('./utils', () => ({
16
+ centerAndResetMapZoom: jest.fn(),
17
+ scrollToElement: jest.fn(),
18
+ zoomMapToFeatures: jest.fn(),
19
+ }));
20
+
21
+ const mockMap = {
22
+ getInteractions: () => ({
23
+ array_: [
24
+ {},
25
+ {},
26
+ {},
27
+ {},
28
+ {},
29
+ {},
30
+ {},
31
+ {},
32
+ {},
33
+ {
34
+ getFeatures: () => ({
35
+ clear: jest.fn(),
36
+ push: jest.fn(),
37
+ }),
38
+ },
39
+ ],
40
+ }),
41
+ getPixelFromCoordinate: jest.fn(() => [100, 200]),
42
+ getFeaturesAtPixel: jest.fn(() => ['mockFeature']),
43
+ };
44
+
45
+ const mockPointsSource = {
46
+ getFeatures: () => [
47
+ {
48
+ values_: {
49
+ title: 'Feature 1',
50
+ path: '/feature1',
51
+ description: 'Description 1',
52
+ typology_of_measures: ['SectorA'],
53
+ measures_implemented: [{ title: 'M1', path: '/m1' }],
54
+ geometry: { flatCoordinates: [0, 0] },
55
+ },
56
+ },
57
+ ],
58
+ };
59
+
60
+ const selectedCase = {
61
+ title: 'Selected Title',
62
+ path: '/selected',
63
+ description: 'Selected description',
64
+ typology_of_measures: ['S1', 'S2'],
65
+ measures_implemented: [{ title: 'M1', path: '/m1' }],
66
+ };
67
+
68
+ describe('CaseStudyList', () => {
69
+ beforeEach(() => {
70
+ jest.clearAllMocks();
71
+ });
72
+
73
+ it('renders empty state when no features', () => {
74
+ const emptyPointsSource = { getFeatures: () => [] };
75
+ const { getByText } = render(
76
+ <CaseStudyList
77
+ selectedCase={null}
78
+ onSelectedCase={jest.fn()}
79
+ pointsSource={emptyPointsSource}
80
+ searchInput=""
81
+ ol={{}}
82
+ />,
83
+ );
84
+
85
+ expect(
86
+ getByText('We could not find any results for your search criteria'),
87
+ ).toBeInTheDocument();
88
+ expect(getByText('check the selected filters')).toBeInTheDocument();
89
+ });
90
+
91
+ it('renders selectedCase details', () => {
92
+ const { getByText } = render(
93
+ <CaseStudyList
94
+ selectedCase={selectedCase}
95
+ onSelectedCase={jest.fn()}
96
+ pointsSource={mockPointsSource}
97
+ map={mockMap}
98
+ searchInput=""
99
+ ol={{}}
100
+ />,
101
+ );
102
+
103
+ expect(getByText('Selected Title')).toBeInTheDocument();
104
+ expect(getByText('Selected description')).toBeInTheDocument();
105
+ // Note: Sectors and NWRMs sections are commented out in the component
106
+ // So we only check the title and description render
107
+ });
108
+
109
+ it('calls reset map utils when clicking Reset map', () => {
110
+ const { getByTestId } = render(
111
+ <CaseStudyList
112
+ selectedCase={selectedCase}
113
+ onSelectedCase={jest.fn()}
114
+ pointsSource={mockPointsSource}
115
+ map={mockMap}
116
+ searchInput=""
117
+ ol={{}}
118
+ />,
119
+ );
120
+
121
+ fireEvent.click(getByTestId('reset-map'));
122
+
123
+ expect(scrollToElement).toHaveBeenCalledWith('search-input');
124
+ expect(centerAndResetMapZoom).toHaveBeenCalledWith({
125
+ map: mockMap,
126
+ ol: {},
127
+ });
128
+ });
129
+
130
+ it('renders features and highlights search term', () => {
131
+ const { getByText } = render(
132
+ <CaseStudyList
133
+ selectedCase={null}
134
+ onSelectedCase={jest.fn()}
135
+ pointsSource={mockPointsSource}
136
+ map={mockMap}
137
+ searchInput="match"
138
+ ol={{}}
139
+ />,
140
+ );
141
+
142
+ expect(getByText('Feature 1')).toBeInTheDocument();
143
+ expect(getByText('Feature 1').getAttribute('href')).toBe('/feature1');
144
+ });
145
+
146
+ it('calls zoomMapToFeatures and onSelectedCase when clicking Show on map', async () => {
147
+ const onSelectedCase = jest.fn();
148
+
149
+ const { getByText } = render(
150
+ <CaseStudyList
151
+ selectedCase={null}
152
+ onSelectedCase={onSelectedCase}
153
+ pointsSource={mockPointsSource}
154
+ map={mockMap}
155
+ searchInput=""
156
+ ol={{}}
157
+ />,
158
+ );
159
+
160
+ fireEvent.click(getByText('Show on map'));
161
+
162
+ expect(scrollToElement).toHaveBeenCalledWith('ol-map-container');
163
+ expect(zoomMapToFeatures).toHaveBeenCalled();
164
+ expect(onSelectedCase).toHaveBeenCalled();
165
+ });
166
+ });