@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.
- package/CHANGELOG.md +37 -1
- package/package.json +2 -1
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.jsx +404 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.test.jsx +366 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.jsx +192 -91
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +205 -58
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +329 -178
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +243 -123
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.jsx +61 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.test.jsx +89 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.jsx +128 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.test.jsx +160 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.jsx +180 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.test.jsx +125 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.jsx +171 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.test.jsx +181 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/Pagination.jsx +81 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/filters.js +350 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/filters.test.js +399 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/hooks.js +28 -4
- package/src/components/manage/Blocks/CaseStudyExplorer/hooks.test.js +73 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/messages.js +138 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +1157 -317
- package/src/components/manage/Blocks/CaseStudyExplorer/testUtils.js +44 -46
- package/src/components/manage/Blocks/CaseStudyExplorer/utils.js +68 -184
- package/src/components/manage/Blocks/CaseStudyExplorer/utils.test.js +91 -158
- package/src/index.js +39 -0
- package/src/openlayers.js +51 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.jsx +0 -383
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.test.jsx +0 -90
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.jsx +0 -342
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.test.jsx +0 -196
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +0 -24
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +0 -49
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +0 -100
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.test.jsx +0 -189
- package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +0 -111
- package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.test.jsx +0 -159
- package/src/components/manage/Blocks/CaseStudyExplorer/styles.less_old +0 -201
|
@@ -1,40 +1,48 @@
|
|
|
1
|
-
export const createMockMap = (additionalProps = {}) =>
|
|
2
|
-
|
|
3
|
-
un: jest.fn(),
|
|
4
|
-
addInteraction: jest.fn(),
|
|
5
|
-
removeInteraction: jest.fn(),
|
|
6
|
-
getView: jest.fn(() => ({
|
|
1
|
+
export const createMockMap = (additionalProps = {}) => {
|
|
2
|
+
const view = {
|
|
7
3
|
getZoom: () => 4,
|
|
8
4
|
getCenter: () => [111, 222],
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
5
|
+
fit: jest.fn(),
|
|
6
|
+
animate: jest.fn(),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
on: jest.fn(),
|
|
11
|
+
un: jest.fn(),
|
|
12
|
+
addInteraction: jest.fn(),
|
|
13
|
+
removeInteraction: jest.fn(),
|
|
14
|
+
getView: jest.fn(() => view),
|
|
15
|
+
view,
|
|
16
|
+
getInteractions: jest.fn(() => ({
|
|
17
|
+
array_: [
|
|
18
|
+
{},
|
|
19
|
+
{},
|
|
20
|
+
{},
|
|
21
|
+
{},
|
|
22
|
+
{},
|
|
23
|
+
{},
|
|
24
|
+
{},
|
|
25
|
+
{},
|
|
26
|
+
{},
|
|
27
|
+
{
|
|
28
|
+
getFeatures: jest.fn(() => ({
|
|
29
|
+
clear: jest.fn(),
|
|
30
|
+
push: jest.fn(),
|
|
31
|
+
})),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
})),
|
|
35
|
+
getEventPixel: jest.fn(() => [10, 20]),
|
|
36
|
+
hasFeatureAtPixel: jest.fn(() => true),
|
|
37
|
+
getViewport: jest.fn(() => ({ style: {} })),
|
|
38
|
+
getPixelFromCoordinate: jest.fn(() => [100, 200]),
|
|
39
|
+
getFeaturesAtPixel: jest.fn(() => ['mockFeature']),
|
|
40
|
+
updateSize: jest.fn(),
|
|
41
|
+
addOverlay: jest.fn(),
|
|
42
|
+
removeOverlay: jest.fn(),
|
|
43
|
+
...additionalProps,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
38
46
|
|
|
39
47
|
export const createMockOl = (additionalProps = {}) => ({
|
|
40
48
|
source: {
|
|
@@ -42,6 +50,7 @@ export const createMockOl = (additionalProps = {}) => ({
|
|
|
42
50
|
Vector: jest.fn(() => ({
|
|
43
51
|
clear: jest.fn(),
|
|
44
52
|
addFeatures: jest.fn(),
|
|
53
|
+
changed: jest.fn(),
|
|
45
54
|
})),
|
|
46
55
|
Cluster: jest.fn(() => ({})),
|
|
47
56
|
},
|
|
@@ -71,14 +80,3 @@ export const createMockOl = (additionalProps = {}) => ({
|
|
|
71
80
|
})),
|
|
72
81
|
...additionalProps,
|
|
73
82
|
});
|
|
74
|
-
|
|
75
|
-
export const createMockPointsSource = (features = []) => ({
|
|
76
|
-
getFeatures: () => features,
|
|
77
|
-
clear: jest.fn(),
|
|
78
|
-
addFeatures: jest.fn(),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
export const createMockFilters = () => ({
|
|
82
|
-
measures_implemented: { m1: 'Measure 1', m2: 'Measure 2' },
|
|
83
|
-
typology_of_measures: { sector1: 'Sector 1', sector2: 'Sector 2' },
|
|
84
|
-
});
|
|
@@ -1,224 +1,108 @@
|
|
|
1
|
+
// EEA design system tokens: link-default, warning, secondary.
|
|
1
2
|
export const STATUS_COLORS = {
|
|
2
|
-
planned: '#
|
|
3
|
-
ongoing: '#
|
|
4
|
-
completed: '#
|
|
3
|
+
planned: '#006BB8',
|
|
4
|
+
ongoing: '#FF9933',
|
|
5
|
+
completed: '#007B6C',
|
|
5
6
|
};
|
|
6
7
|
|
|
7
8
|
export const FALLBACK_STATUS_COLOR = '#6B7280';
|
|
8
9
|
export const CLUSTER_COLOR = '#007B6C';
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
habitat_ecosystem_type: ['habitat_ecosystem_type', 'ecosystem_typology'],
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function getCaseProperty(properties, fieldName) {
|
|
15
|
-
if (properties?.[fieldName] !== undefined) return properties[fieldName];
|
|
16
|
-
return (FIELD_ALIASES[fieldName] || [])
|
|
17
|
-
.map((field) => properties?.[field])
|
|
18
|
-
.find((value) => value !== undefined);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function asValues(value) {
|
|
22
|
-
if (value === undefined || value === null || value === '') return [];
|
|
23
|
-
return Array.isArray(value) ? value : [value];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function valueCode(value) {
|
|
27
|
-
if (value && typeof value === 'object') {
|
|
28
|
-
return String(
|
|
29
|
-
value.id ?? value.value ?? value.token ?? value.title ?? value,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return String(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function valueLabel(value) {
|
|
36
|
-
if (value && typeof value === 'object') {
|
|
37
|
-
return String(
|
|
38
|
-
value.title ?? value.name ?? value.label ?? value.value ?? value.id,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
return String(value);
|
|
42
|
-
}
|
|
10
|
+
// Design system `@cse-green-dark`: the pin of the selected case study.
|
|
11
|
+
export const SELECTED_COLOR = '#005248';
|
|
43
12
|
|
|
44
13
|
export function getStatusColor(status) {
|
|
45
14
|
return STATUS_COLORS[status] || FALLBACK_STATUS_COLOR;
|
|
46
15
|
}
|
|
47
16
|
|
|
17
|
+
export function prefersReducedMotion() {
|
|
18
|
+
return (
|
|
19
|
+
typeof window !== 'undefined' &&
|
|
20
|
+
typeof window.matchMedia === 'function' &&
|
|
21
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
48
25
|
export function centerAndResetMapZoom({ map, ol }) {
|
|
49
|
-
map
|
|
50
|
-
zoom: 4,
|
|
51
|
-
duration: 1000,
|
|
52
|
-
center: ol.proj.transform([
|
|
26
|
+
map?.getView().animate({
|
|
27
|
+
zoom: 4.5,
|
|
28
|
+
duration: prefersReducedMotion() ? 0 : 1000,
|
|
29
|
+
center: ol.proj.transform([8, 55], 'EPSG:4326', 'EPSG:3857'),
|
|
53
30
|
});
|
|
54
31
|
}
|
|
55
32
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
33
|
+
/**
|
|
34
|
+
* How many wrapping chips to keep visible before a trailing "+N" trigger has
|
|
35
|
+
* to stand in for the rest.
|
|
36
|
+
*
|
|
37
|
+
* `chips` are the measured boxes of every chip in render order: `right` is the
|
|
38
|
+
* chip's right edge measured from the left edge of the list and `row` its
|
|
39
|
+
* 0-based row index in the full, untruncated layout. The trigger takes the
|
|
40
|
+
* place right after the last visible chip, so dropping a chip may let the
|
|
41
|
+
* trigger fit on a row it did not fit on before; the scan goes from the full
|
|
42
|
+
* list downwards and stops at the first count whose last chip plus the trigger
|
|
43
|
+
* still fit in `rows` rows.
|
|
44
|
+
*
|
|
45
|
+
* Returns the number of chips to show (0 for an empty list).
|
|
46
|
+
*/
|
|
47
|
+
export function visibleChipCount(
|
|
48
|
+
chips,
|
|
49
|
+
{ rows = 2, gap = 0, moreWidth = 0, width = 0 } = {},
|
|
50
|
+
) {
|
|
51
|
+
const total = chips.length;
|
|
52
|
+
if (!total) return 0;
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
const interactions = map?.getInteractions?.();
|
|
63
|
-
const interactionArray =
|
|
64
|
-
interactions?.getArray?.() || interactions?.array_ || [];
|
|
54
|
+
const maxRow = rows - 1;
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
// Everything already fits: nothing is hidden, so no trigger is needed.
|
|
57
|
+
if (chips[total - 1].row <= maxRow) return total;
|
|
58
|
+
|
|
59
|
+
for (let count = total - 1; count > 0; count -= 1) {
|
|
60
|
+
const last = chips[count - 1];
|
|
61
|
+
const triggerRow =
|
|
62
|
+
last.right + gap + moreWidth <= width ? last.row : last.row + 1;
|
|
63
|
+
|
|
64
|
+
if (triggerRow <= maxRow) return count;
|
|
65
|
+
}
|
|
70
66
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return point.getExtent();
|
|
67
|
+
// Too narrow for any chip next to the trigger: keep one rather than hide
|
|
68
|
+
// everything behind a bare "+N".
|
|
69
|
+
return 1;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
export function
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
map.getView().fit(paddedExtent, { ...map.getSize(), duration: 1000 });
|
|
72
|
+
export function scrollToElement(elementId) {
|
|
73
|
+
const element = document.getElementById(elementId);
|
|
74
|
+
element?.scrollIntoView({
|
|
75
|
+
behavior: prefersReducedMotion() ? 'auto' : 'smooth',
|
|
76
|
+
});
|
|
83
77
|
}
|
|
84
78
|
|
|
79
|
+
/**
|
|
80
|
+
* GeoJSON features -> OpenLayers point features carrying the properties the
|
|
81
|
+
* map style and the map/list coupling need.
|
|
82
|
+
*/
|
|
85
83
|
export function getFeatures({ cases, ol }) {
|
|
86
84
|
const Feature = ol.ol.Feature;
|
|
87
85
|
|
|
88
|
-
return cases.map((
|
|
86
|
+
return cases.map((item, index) => {
|
|
89
87
|
const point = new Feature(
|
|
90
|
-
new ol.geom.Point(ol.proj.fromLonLat(
|
|
88
|
+
new ol.geom.Point(ol.proj.fromLonLat(item.geometry.coordinates)),
|
|
91
89
|
);
|
|
92
|
-
const properties =
|
|
90
|
+
const properties = item.properties || {};
|
|
93
91
|
const status = properties.current_status;
|
|
92
|
+
|
|
94
93
|
point.setId(index);
|
|
95
94
|
point.setProperties(
|
|
96
95
|
{
|
|
97
96
|
title: properties.title,
|
|
98
97
|
image: properties.image,
|
|
99
|
-
nwrm_type: properties.nwrm_type,
|
|
100
|
-
measures_implemented: properties.measures,
|
|
101
|
-
typology_of_measures: properties.typology_of_measures,
|
|
102
|
-
current_status: status,
|
|
103
|
-
habitat_ecosystem_type: getCaseProperty(
|
|
104
|
-
properties,
|
|
105
|
-
'habitat_ecosystem_type',
|
|
106
|
-
),
|
|
107
|
-
nrr_legislation: properties.nrr_legislation,
|
|
108
|
-
nrr_biogeographical_region: properties.nrr_biogeographical_region,
|
|
109
|
-
scale_of_planning: properties.scale_of_planning,
|
|
110
|
-
description: properties.description,
|
|
111
|
-
index,
|
|
112
98
|
path: properties.path,
|
|
99
|
+
current_status: status,
|
|
113
100
|
color: getStatusColor(status),
|
|
101
|
+
index,
|
|
114
102
|
},
|
|
115
103
|
false,
|
|
116
104
|
);
|
|
117
|
-
return point;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Escape a string so it can be embedded in a RegExp pattern as a literal.
|
|
122
|
-
// Prevents user-provided input from being interpreted as regex syntax
|
|
123
|
-
// (avoids ReDoS via crafted patterns and SyntaxError from malformed ones).
|
|
124
|
-
export function escapeRegExp(str) {
|
|
125
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const WORD_CHAR = /[A-Za-z0-9_]/;
|
|
129
|
-
|
|
130
|
-
// Case-insensitive whole-word substring match implemented without regular
|
|
131
|
-
// expressions, so the needle is always treated as literal text. Equivalent
|
|
132
|
-
// to /\bneedle\b/i but runs in linear time (indexOf) and is immune to
|
|
133
|
-
// catastrophic backtracking.
|
|
134
|
-
function hasWholeWord(text, needle) {
|
|
135
|
-
let index = text.indexOf(needle);
|
|
136
|
-
while (index !== -1) {
|
|
137
|
-
const before = index === 0 ? '' : text[index - 1];
|
|
138
|
-
const after =
|
|
139
|
-
index + needle.length >= text.length ? '' : text[index + needle.length];
|
|
140
|
-
if (!WORD_CHAR.test(before) && !WORD_CHAR.test(after)) return true;
|
|
141
|
-
index = text.indexOf(needle, index + 1);
|
|
142
|
-
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
105
|
|
|
146
|
-
|
|
147
|
-
'nrr_legislation',
|
|
148
|
-
'nrr_biogeographical_region',
|
|
149
|
-
'measures_implemented',
|
|
150
|
-
'typology_of_measures',
|
|
151
|
-
'current_status',
|
|
152
|
-
'habitat_ecosystem_type',
|
|
153
|
-
'scale_of_planning',
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
export function filterCasesByPath(cases, filterPath) {
|
|
157
|
-
const path = filterPath?.trim();
|
|
158
|
-
if (!path) return cases;
|
|
159
|
-
|
|
160
|
-
return cases.filter((_case) =>
|
|
161
|
-
String(_case.properties?.path || '').includes(path),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function filterCases(
|
|
166
|
-
cases,
|
|
167
|
-
activeFilters,
|
|
168
|
-
caseStudiesIds,
|
|
169
|
-
searchInput,
|
|
170
|
-
filterPath,
|
|
171
|
-
) {
|
|
172
|
-
return filterCasesByPath(cases, filterPath).filter((_case) => {
|
|
173
|
-
const properties = _case.properties || {};
|
|
174
|
-
const flagCase = caseStudiesIds
|
|
175
|
-
? caseStudiesIds.includes(properties.url?.split('/').pop())
|
|
176
|
-
: true;
|
|
177
|
-
const searchable = `${properties.title || ''} ${
|
|
178
|
-
properties.description || ''
|
|
179
|
-
}`;
|
|
180
|
-
const flagSearch =
|
|
181
|
-
!searchInput ||
|
|
182
|
-
hasWholeWord(searchable.toLowerCase(), searchInput.toLowerCase());
|
|
183
|
-
|
|
184
|
-
const matchesFilters = FILTER_FIELDS.every((filterName) => {
|
|
185
|
-
const selected = activeFilters[filterName] || [];
|
|
186
|
-
if (!selected.length) return true;
|
|
187
|
-
const values =
|
|
188
|
-
filterName === 'measures_implemented'
|
|
189
|
-
? asValues(properties.measures).map((item) => valueCode(item))
|
|
190
|
-
: filterName === 'typology_of_measures'
|
|
191
|
-
? asValues(properties.typology_of_measures).map((item) =>
|
|
192
|
-
valueCode(item),
|
|
193
|
-
)
|
|
194
|
-
: asValues(getCaseProperty(properties, filterName)).map((item) =>
|
|
195
|
-
valueCode(item),
|
|
196
|
-
);
|
|
197
|
-
return selected.some((filter) => values.includes(String(filter)));
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
return flagCase && flagSearch && matchesFilters;
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function getFilters(cases) {
|
|
205
|
-
const filters = Object.fromEntries(FILTER_FIELDS.map((field) => [field, {}]));
|
|
206
|
-
|
|
207
|
-
Object.values(cases || {}).forEach((_case) => {
|
|
208
|
-
const properties = _case.properties || {};
|
|
209
|
-
FILTER_FIELDS.forEach((field) => {
|
|
210
|
-
const source =
|
|
211
|
-
field === 'measures_implemented'
|
|
212
|
-
? properties.measures
|
|
213
|
-
: field === 'typology_of_measures'
|
|
214
|
-
? properties.typology_of_measures
|
|
215
|
-
: getCaseProperty(properties, field);
|
|
216
|
-
asValues(source).forEach((item) => {
|
|
217
|
-
const code = valueCode(item);
|
|
218
|
-
if (!filters[field][code]) filters[field][code] = valueLabel(item);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
106
|
+
return point;
|
|
221
107
|
});
|
|
222
|
-
|
|
223
|
-
return filters;
|
|
224
108
|
}
|
|
@@ -2,191 +2,124 @@ import './mockJsdom';
|
|
|
2
2
|
import '@testing-library/jest-dom';
|
|
3
3
|
import {
|
|
4
4
|
FALLBACK_STATUS_COLOR,
|
|
5
|
+
STATUS_COLORS,
|
|
6
|
+
centerAndResetMapZoom,
|
|
5
7
|
getFeatures,
|
|
6
8
|
getStatusColor,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
scrollToElement,
|
|
10
|
+
visibleChipCount,
|
|
9
11
|
} from './utils';
|
|
10
12
|
|
|
11
13
|
describe('utils.js', () => {
|
|
12
|
-
test('maps exact statuses
|
|
13
|
-
expect(getStatusColor('planned')).toBe('#
|
|
14
|
-
expect(getStatusColor('ongoing')).toBe('#
|
|
15
|
-
expect(getStatusColor('completed')).toBe('#
|
|
14
|
+
test('maps exact statuses to design system colours, unknown to fallback', () => {
|
|
15
|
+
expect(getStatusColor('planned')).toBe('#006BB8');
|
|
16
|
+
expect(getStatusColor('ongoing')).toBe('#FF9933');
|
|
17
|
+
expect(getStatusColor('completed')).toBe('#007B6C');
|
|
16
18
|
expect(getStatusColor('Planned')).toBe(FALLBACK_STATUS_COLOR);
|
|
17
19
|
expect(getStatusColor('future')).toBe(FALLBACK_STATUS_COLOR);
|
|
20
|
+
expect(STATUS_COLORS.completed).toBe('#007B6C');
|
|
18
21
|
});
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
geometry: { coordinates: [0, 0] },
|
|
23
|
-
properties: {
|
|
24
|
-
title: 'test case study',
|
|
25
|
-
image: '',
|
|
26
|
-
nwrm_type: 'light',
|
|
27
|
-
measures: [{ id: 'test-measure1', title: 'test measure 1' }],
|
|
28
|
-
description: 'test',
|
|
29
|
-
typology_of_measures: ['testsector'],
|
|
30
|
-
path: '/test-case-study',
|
|
31
|
-
url: 'localhost.com/test-case-study',
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
geometry: { coordinates: [0, 0] },
|
|
36
|
-
properties: {
|
|
37
|
-
title: 'case study 2',
|
|
38
|
-
image: '',
|
|
39
|
-
nwrm_type: 'light',
|
|
40
|
-
measures: [{ id: 'test-measure1', title: 'test measure 1' }],
|
|
41
|
-
description: 'test',
|
|
42
|
-
typology_of_measures: ['testsector'],
|
|
43
|
-
path: '/test-case-study',
|
|
44
|
-
url: 'localhost.com/test-case-study',
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
test('getFeatures', () => {
|
|
50
|
-
const mockFeature = {
|
|
23
|
+
test('getFeatures carries the properties needed by map and list', () => {
|
|
24
|
+
const feature = {
|
|
51
25
|
setId: jest.fn(),
|
|
52
26
|
setProperties: jest.fn(),
|
|
27
|
+
props: null,
|
|
53
28
|
};
|
|
29
|
+
feature.setProperties.mockImplementation((properties) => {
|
|
30
|
+
feature.props = properties;
|
|
31
|
+
});
|
|
54
32
|
|
|
55
33
|
const ol = {
|
|
56
|
-
ol: {
|
|
57
|
-
|
|
58
|
-
},
|
|
59
|
-
geom: {
|
|
60
|
-
Point: jest.fn().mockImplementation(() => ({})),
|
|
61
|
-
},
|
|
62
|
-
proj: {
|
|
63
|
-
fromLonLat: jest.fn().mockReturnValue([0, 0]),
|
|
64
|
-
},
|
|
34
|
+
ol: { Feature: jest.fn(() => feature) },
|
|
35
|
+
geom: { Point: jest.fn(() => ({})) },
|
|
36
|
+
proj: { fromLonLat: jest.fn(() => [1, 2]) },
|
|
65
37
|
};
|
|
66
38
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
const features = getFeatures({
|
|
40
|
+
cases: [
|
|
41
|
+
{
|
|
42
|
+
geometry: { coordinates: [14.4, 50.1] },
|
|
43
|
+
properties: {
|
|
44
|
+
title: 'Peatlands',
|
|
45
|
+
path: '/case-peatlands',
|
|
46
|
+
image: 'image.jpg',
|
|
47
|
+
current_status: 'ongoing',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
ol,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(features).toHaveLength(1);
|
|
55
|
+
expect(feature.setId).toHaveBeenCalledWith(0);
|
|
56
|
+
expect(feature.props).toMatchObject({
|
|
57
|
+
title: 'Peatlands',
|
|
58
|
+
path: '/case-peatlands',
|
|
59
|
+
current_status: 'ongoing',
|
|
60
|
+
color: '#FF9933',
|
|
61
|
+
});
|
|
70
62
|
});
|
|
71
63
|
|
|
72
|
-
test('
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
'
|
|
64
|
+
test('centerAndResetMapZoom returns to the European extent', () => {
|
|
65
|
+
const map = { getView: () => ({ animate: jest.fn() }) };
|
|
66
|
+
const ol = { proj: { transform: jest.fn(() => [111, 222]) } };
|
|
67
|
+
|
|
68
|
+
centerAndResetMapZoom({ map, ol });
|
|
69
|
+
|
|
70
|
+
expect(map.getView().animate).toBeDefined();
|
|
71
|
+
expect(ol.proj.transform).toHaveBeenCalledWith(
|
|
72
|
+
[8, 55],
|
|
73
|
+
'EPSG:4326',
|
|
74
|
+
'EPSG:3857',
|
|
83
75
|
);
|
|
84
|
-
expect(mockCasesFiltered).toStrictEqual([]);
|
|
85
76
|
});
|
|
86
77
|
|
|
87
|
-
test('
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
geometry: { coordinates: [0, 0] },
|
|
91
|
-
properties: {
|
|
92
|
-
title: 'the river project (R1)',
|
|
93
|
-
description: 'a aaa aaaa aaaaa b',
|
|
94
|
-
url: '/the-river-project-r1',
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
];
|
|
98
|
-
const noFilters = {
|
|
99
|
-
nrr_legislation: [],
|
|
100
|
-
nrr_biogeographical_region: [],
|
|
101
|
-
measures_implemented: [],
|
|
102
|
-
typology_of_measures: [],
|
|
103
|
-
current_status: [],
|
|
104
|
-
habitat_ecosystem_type: [],
|
|
105
|
-
scale_of_planning: [],
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Malformed regex input must not throw (previously: SyntaxError)
|
|
109
|
-
expect(() => filterCases(cases, noFilters, null, '(')).not.toThrow();
|
|
110
|
-
// Regex metacharacters are matched literally, not interpreted
|
|
111
|
-
expect(() => filterCases(cases, noFilters, null, '(a+)+$')).not.toThrow();
|
|
112
|
-
expect(filterCases(cases, noFilters, null, '(a+)+$')).toHaveLength(0);
|
|
113
|
-
expect(filterCases(cases, noFilters, null, '(r1')).toHaveLength(1);
|
|
114
|
-
// Whole-word semantics are preserved
|
|
115
|
-
expect(filterCases(cases, noFilters, null, 'rive')).toHaveLength(0);
|
|
116
|
-
expect(filterCases(cases, noFilters, null, 'river')).toHaveLength(1);
|
|
117
|
-
// Case-insensitive
|
|
118
|
-
expect(filterCases(cases, noFilters, null, 'R1')).toHaveLength(1);
|
|
78
|
+
test('scrollToElement tolerates missing elements', () => {
|
|
79
|
+
expect(() => scrollToElement('does-not-exist')).not.toThrow();
|
|
119
80
|
});
|
|
120
81
|
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
geometry: { coordinates: [0, 0] },
|
|
138
|
-
properties: {
|
|
139
|
-
title: 'future case',
|
|
140
|
-
description: '',
|
|
141
|
-
url: '/future-case',
|
|
142
|
-
current_status: 'Future',
|
|
143
|
-
ecosystem_typology: ['Forest'],
|
|
144
|
-
nrr_legislation: ['article-2'],
|
|
145
|
-
nrr_biogeographical_region: ['region-2'],
|
|
146
|
-
scale_of_planning: 'National',
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
];
|
|
82
|
+
describe('visibleChipCount', () => {
|
|
83
|
+
const CHIP = 90;
|
|
84
|
+
const GAP = 8;
|
|
85
|
+
const WIDTH = 300;
|
|
86
|
+
// Three chips per row: x = 0, 98, 196 and the fourth wraps.
|
|
87
|
+
const layout = (rows) =>
|
|
88
|
+
rows.flatMap((aRow) =>
|
|
89
|
+
[0, 1, 2].map((column) => ({
|
|
90
|
+
row: aRow,
|
|
91
|
+
right: column * (CHIP + GAP) + CHIP,
|
|
92
|
+
})),
|
|
93
|
+
);
|
|
94
|
+
const options = { rows: 2, gap: GAP, moreWidth: 40, width: WIDTH };
|
|
150
95
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
nrr_legislation: ['article-1'],
|
|
154
|
-
nrr_biogeographical_region: ['region-1'],
|
|
155
|
-
measures_implemented: [],
|
|
156
|
-
typology_of_measures: [],
|
|
157
|
-
current_status: ['planned'],
|
|
158
|
-
habitat_ecosystem_type: ['River'],
|
|
159
|
-
scale_of_planning: ['Regional'],
|
|
160
|
-
}),
|
|
161
|
-
).toHaveLength(1);
|
|
162
|
-
expect(getFilters(cases).nrr_legislation).toEqual({
|
|
163
|
-
'article-1': 'Article 1',
|
|
164
|
-
'article-2': 'article-2',
|
|
96
|
+
test('returns 0 for an empty list', () => {
|
|
97
|
+
expect(visibleChipCount([], options)).toBe(0);
|
|
165
98
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
99
|
+
|
|
100
|
+
test('keeps every chip when they all fit in two rows', () => {
|
|
101
|
+
expect(visibleChipCount(layout([0, 1]), options)).toBe(6);
|
|
102
|
+
expect(visibleChipCount(layout([0]), options)).toBe(3);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('drops a chip so the trigger fits on the second row', () => {
|
|
106
|
+
// 6 chips fill two rows, but the trigger no longer fits next to the 6th.
|
|
107
|
+
expect(visibleChipCount(layout([0, 1, 2]), options)).toBe(5);
|
|
169
108
|
});
|
|
170
|
-
});
|
|
171
109
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
current_status: {},
|
|
186
|
-
habitat_ecosystem_type: {},
|
|
187
|
-
nrr_legislation: {},
|
|
188
|
-
nrr_biogeographical_region: {},
|
|
189
|
-
scale_of_planning: {},
|
|
110
|
+
test('keeps one chip rather than hiding everything behind a trigger', () => {
|
|
111
|
+
const chips = [
|
|
112
|
+
{ row: 0, right: 300 },
|
|
113
|
+
{ row: 1, right: 50 },
|
|
114
|
+
];
|
|
115
|
+
expect(
|
|
116
|
+
visibleChipCount(chips, {
|
|
117
|
+
rows: 1,
|
|
118
|
+
gap: GAP,
|
|
119
|
+
moreWidth: 200,
|
|
120
|
+
width: 60,
|
|
121
|
+
}),
|
|
122
|
+
).toBe(1);
|
|
190
123
|
});
|
|
191
124
|
});
|
|
192
125
|
});
|