@eeacms/volto-bise-policy 1.3.11 → 1.3.13

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