@eeacms/volto-cca-policy 1.0.3 → 1.0.4
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 +41 -0
- package/locales/bg/LC_MESSAGES/volto.po +576 -0
- package/locales/cs/LC_MESSAGES/volto.po +576 -0
- package/locales/da/LC_MESSAGES/volto.po +576 -0
- package/locales/de/LC_MESSAGES/volto.po +576 -0
- package/locales/el/LC_MESSAGES/volto.po +576 -0
- package/locales/en/LC_MESSAGES/volto.po +576 -0
- package/locales/es/LC_MESSAGES/volto.po +576 -0
- package/locales/et/LC_MESSAGES/volto.po +576 -0
- package/locales/fi/LC_MESSAGES/volto.po +576 -0
- package/locales/fr/LC_MESSAGES/volto.po +576 -0
- package/locales/ga/LC_MESSAGES/volto.po +576 -0
- package/locales/hr/LC_MESSAGES/volto.po +576 -0
- package/locales/hu/LC_MESSAGES/volto.po +576 -0
- package/locales/is/LC_MESSAGES/volto.po +576 -0
- package/locales/it/LC_MESSAGES/volto.po +576 -0
- package/locales/lt/LC_MESSAGES/volto.po +576 -0
- package/locales/lv/LC_MESSAGES/volto.po +576 -0
- package/locales/mt/LC_MESSAGES/volto.po +576 -0
- package/locales/nl/LC_MESSAGES/volto.po +576 -0
- package/locales/nn/LC_MESSAGES/volto.po +576 -0
- package/locales/pl/LC_MESSAGES/volto.po +576 -0
- package/locales/pt/LC_MESSAGES/volto.po +576 -0
- package/locales/ro/LC_MESSAGES/volto.po +576 -0
- package/locales/sk/LC_MESSAGES/volto.po +576 -0
- package/locales/sl/LC_MESSAGES/volto.po +576 -0
- package/locales/sv/LC_MESSAGES/volto.po +576 -0
- package/locales/tr/LC_MESSAGES/volto.po +576 -0
- package/locales/volto.pot +577 -1
- package/package.json +1 -1
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +10 -14
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +6 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueContentView.jsx +1 -1
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.jsx +268 -7
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.test.jsx +160 -0
- package/src/components/Search/NavigatorCatalogue/utils.js +56 -0
- package/src/components/theme/CompareTools/CompareToolsView.jsx +15 -8
- package/src/components/theme/CompareTools/CompareToolsView.test.jsx +110 -0
- package/src/constants.js +0 -48
- package/theme/globals/navigator.less +178 -55
- package/artifacts/BLOCKS.md +0 -167
- package/artifacts/link-integrity-workflow/README.md +0 -76
- package/artifacts/link-integrity-workflow/index.md +0 -33
- package/artifacts/link-integrity-workflow/link-integrity-block-fields-report.md +0 -132
- package/artifacts/link-integrity-workflow/link-integrity-blocks-report.md +0 -52
- package/artifacts/link-integrity-workflow/understanding-link-integrity.md +0 -143
- package/artifacts/link-integrity-workflow/volto-block-link-analysis.md +0 -63
- package/artifacts/link-integrity-workflow/volto-block-link-discovery.md +0 -60
- package/artifacts/test-fixes/test-fixes-specification.md +0 -267
package/package.json
CHANGED
|
@@ -54,7 +54,7 @@ const publicationDateFormatter = new Intl.DateTimeFormat('en-GB', {
|
|
|
54
54
|
timeZone: 'UTC',
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
const TagGroup = ({
|
|
57
|
+
const TagGroup = ({ typeLabel, values, type }) => {
|
|
58
58
|
const visible = values.slice(0, 3);
|
|
59
59
|
const hidden = values.slice(3);
|
|
60
60
|
const remaining = values.length - visible.length;
|
|
@@ -80,7 +80,13 @@ const TagGroup = ({ intl, typeLabel, values, type }) => {
|
|
|
80
80
|
}
|
|
81
81
|
position="bottom left"
|
|
82
82
|
trigger={
|
|
83
|
-
<
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
className={`navigator-tag ${type} more`}
|
|
86
|
+
aria-label={`${typeLabel}: ${hidden.join(', ')}`}
|
|
87
|
+
>
|
|
88
|
+
+ {remaining}
|
|
89
|
+
</button>
|
|
84
90
|
}
|
|
85
91
|
/>
|
|
86
92
|
)}
|
|
@@ -172,18 +178,8 @@ const NavigatorCatalogueCardItem = (props) => {
|
|
|
172
178
|
</p>
|
|
173
179
|
|
|
174
180
|
<div className="catalogue-taxonomy">
|
|
175
|
-
<TagGroup
|
|
176
|
-
|
|
177
|
-
typeLabel={sectorLabel}
|
|
178
|
-
values={sectors}
|
|
179
|
-
type="sector"
|
|
180
|
-
/>
|
|
181
|
-
<TagGroup
|
|
182
|
-
intl={intl}
|
|
183
|
-
typeLabel={hazardLabel}
|
|
184
|
-
values={hazards}
|
|
185
|
-
type="hazard"
|
|
186
|
-
/>
|
|
181
|
+
<TagGroup typeLabel={sectorLabel} values={sectors} type="sector" />
|
|
182
|
+
<TagGroup typeLabel={hazardLabel} values={hazards} type="hazard" />
|
|
187
183
|
</div>
|
|
188
184
|
|
|
189
185
|
<div className="catalogue-item-footer">
|
|
@@ -115,6 +115,12 @@ describe('NavigatorCatalogueCardItem', () => {
|
|
|
115
115
|
expect(screen.getByText('Agriculture')).toBeInTheDocument();
|
|
116
116
|
expect(screen.getByText('Energy')).toBeInTheDocument();
|
|
117
117
|
expect(screen.getByText('Wildfires')).toBeInTheDocument();
|
|
118
|
+
expect(
|
|
119
|
+
screen.getByRole('button', { name: 'Sector: Energy' }),
|
|
120
|
+
).toBeInTheDocument();
|
|
121
|
+
expect(
|
|
122
|
+
screen.getByRole('button', { name: 'Hazard: Wildfires' }),
|
|
123
|
+
).toBeInTheDocument();
|
|
118
124
|
expect(screen.getByText('Step 1')).toBeInTheDocument();
|
|
119
125
|
expect(screen.queryByText('Step 4')).not.toBeInTheDocument();
|
|
120
126
|
expect(
|
|
@@ -160,7 +160,7 @@ const NavigatorCatalogueContentView = (props) => {
|
|
|
160
160
|
<ResultViewComponent>{children}</ResultViewComponent>
|
|
161
161
|
<CompareToolsPanel appConfig={appConfig} />
|
|
162
162
|
|
|
163
|
-
{children.length > 0 && (
|
|
163
|
+
{children.length > 0 && activeNavigatorViewId !== 'map' && (
|
|
164
164
|
<div className="search-body-footer">
|
|
165
165
|
<Grid columns={2}>
|
|
166
166
|
<Grid.Column>
|
|
@@ -1,11 +1,272 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { compose } from 'redux';
|
|
3
|
+
import { Icon, Button } from 'semantic-ui-react';
|
|
4
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
5
|
+
import { useSearchContext, useViews } from '@eeacms/search/lib/hocs';
|
|
6
|
+
import { withOpenLayers } from '@eeacms/volto-openlayers-map';
|
|
7
|
+
import { useMapContext } from '@eeacms/volto-openlayers-map/api';
|
|
8
|
+
import { Map, Layer, Layers, Controls } from '@eeacms/volto-openlayers-map/api';
|
|
9
|
+
import { withGeoJsonData } from '@eeacms/volto-cca-policy/helpers/country_map/hocs';
|
|
10
|
+
import { euCountryNames } from '@eeacms/volto-cca-policy/helpers/country_map/countryMap';
|
|
11
|
+
import { clientOnly } from '@eeacms/volto-cca-policy/helpers';
|
|
12
|
+
import {
|
|
13
|
+
withResponsiveContainer,
|
|
14
|
+
withVisibilitySensor,
|
|
15
|
+
} from '@eeacms/volto-cca-policy/hocs';
|
|
16
|
+
import {
|
|
17
|
+
getColorForCount,
|
|
18
|
+
normalizeCountryName,
|
|
19
|
+
buildCountryCounts,
|
|
20
|
+
mapLegendItems,
|
|
21
|
+
} from './utils';
|
|
2
22
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
23
|
+
const messages = defineMessages({
|
|
24
|
+
toolAvailability: {
|
|
25
|
+
id: '{count, plural, =0 {No tools available} one {# tool available} other {# tools available}}',
|
|
26
|
+
defaultMessage:
|
|
27
|
+
'{count, plural, =0 {No tools available} one {# tool available} other {# tools available}}',
|
|
28
|
+
},
|
|
29
|
+
exploreTools: {
|
|
30
|
+
id: 'Explore tools',
|
|
31
|
+
defaultMessage: 'Explore tools',
|
|
32
|
+
},
|
|
33
|
+
toolsPerCountry: {
|
|
34
|
+
id: 'Tools per country',
|
|
35
|
+
defaultMessage: 'Tools per country',
|
|
36
|
+
},
|
|
37
|
+
close: {
|
|
38
|
+
id: 'Close',
|
|
39
|
+
defaultMessage: 'Close',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/* istanbul ignore next */
|
|
44
|
+
const CountryClickInteractions = ({ ol, countryCounts, onExploreTools }) => {
|
|
45
|
+
const { map } = useMapContext();
|
|
46
|
+
const intl = useIntl();
|
|
47
|
+
const [tooltipData, setTooltipData] = React.useState(null);
|
|
48
|
+
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
if (!map) return;
|
|
51
|
+
|
|
52
|
+
const handleClick = (evt) => {
|
|
53
|
+
if (evt.dragging) return;
|
|
54
|
+
const pixel = map.getEventPixel(evt.originalEvent);
|
|
55
|
+
const feature = map.forEachFeatureAtPixel(pixel, (f) => f);
|
|
56
|
+
if (!feature) {
|
|
57
|
+
setTooltipData(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const name = feature.get('na');
|
|
62
|
+
const normalized = normalizeCountryName(name);
|
|
63
|
+
const count = countryCounts[normalized] || 0;
|
|
64
|
+
|
|
65
|
+
setTooltipData({
|
|
66
|
+
name: normalized,
|
|
67
|
+
count,
|
|
68
|
+
pixel: [evt.originalEvent.clientX, evt.originalEvent.clientY],
|
|
69
|
+
mapRect: map.getTargetElement().getBoundingClientRect(),
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handlePointerMove = (evt) => {
|
|
74
|
+
if (evt.dragging) return;
|
|
75
|
+
const pixel = map.getEventPixel(evt.originalEvent);
|
|
76
|
+
const feature = map.forEachFeatureAtPixel(pixel, (f) => f);
|
|
77
|
+
map.getTargetElement().style.cursor = feature ? 'pointer' : '';
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
map.on('singleclick', handleClick);
|
|
81
|
+
map.on('pointermove', handlePointerMove);
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
map.un('singleclick', handleClick);
|
|
85
|
+
map.un('pointermove', handlePointerMove);
|
|
86
|
+
};
|
|
87
|
+
}, [map, countryCounts]);
|
|
88
|
+
|
|
89
|
+
// Render tooltip as a portal-like overlay
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
{tooltipData && (
|
|
93
|
+
<div
|
|
94
|
+
className="navigator-catalogue-map-tooltip"
|
|
95
|
+
style={{
|
|
96
|
+
left: `${tooltipData.pixel[0] - tooltipData.mapRect.left + 15}px`,
|
|
97
|
+
top: `${tooltipData.pixel[1] - tooltipData.mapRect.top - 10}px`,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<div className="tooltip-header">
|
|
101
|
+
<strong>{tooltipData.name}</strong>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="tooltip-body">
|
|
104
|
+
<span className="tooltip-count">
|
|
105
|
+
{intl.formatMessage(messages.toolAvailability, {
|
|
106
|
+
count: tooltipData.count,
|
|
107
|
+
})}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
{tooltipData.count > 0 && (
|
|
111
|
+
<div className="tooltip-actions">
|
|
112
|
+
<Button
|
|
113
|
+
labelPosition="right"
|
|
114
|
+
className="primary small icon"
|
|
115
|
+
onClick={() => onExploreTools(tooltipData.name)}
|
|
116
|
+
>
|
|
117
|
+
{intl.formatMessage(messages.exploreTools)}
|
|
118
|
+
<Icon className="ri-arrow-right-line" />
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
<Button
|
|
123
|
+
className="tooltip-close"
|
|
124
|
+
onClick={() => setTooltipData(null)}
|
|
125
|
+
aria-label={intl.formatMessage(messages.close)}
|
|
126
|
+
>
|
|
127
|
+
<Icon className="ri-close-line" aria-hidden="true" />
|
|
128
|
+
</Button>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/* istanbul ignore next */
|
|
136
|
+
const NavigatorCatalogueMapViewInner = (props) => {
|
|
137
|
+
const { geofeatures, projection, ol } = props;
|
|
138
|
+
const intl = useIntl();
|
|
139
|
+
const searchContext = useSearchContext();
|
|
140
|
+
const views = useViews();
|
|
141
|
+
|
|
142
|
+
// Handler: replace country filter + switch to list view
|
|
143
|
+
const handleExploreTools = React.useCallback(
|
|
144
|
+
(countryName) => {
|
|
145
|
+
const field = 'cca_geographic_countries.keyword';
|
|
146
|
+
// Clear existing country filter, then set the new one
|
|
147
|
+
searchContext?.removeFilter(field);
|
|
148
|
+
searchContext?.addFilter(field, countryName, 'any');
|
|
149
|
+
// Switch to listing view
|
|
150
|
+
views.setActiveViewId('listing');
|
|
151
|
+
},
|
|
152
|
+
[searchContext, views],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Build lookup: countryName -> count
|
|
156
|
+
const countryCounts = React.useMemo(
|
|
157
|
+
() => buildCountryCounts(searchContext?.facets),
|
|
158
|
+
[searchContext?.facets],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const [tileWMSSources, setTileWMSSources] = React.useState(null);
|
|
162
|
+
const [euCountriesSource, setEuCountriesSource] = React.useState(null);
|
|
163
|
+
|
|
164
|
+
React.useEffect(() => {
|
|
165
|
+
// Tile layer
|
|
166
|
+
setTileWMSSources([
|
|
167
|
+
new ol.source.TileWMS({
|
|
168
|
+
url: 'https://gisco-services.ec.europa.eu/maps/service',
|
|
169
|
+
params: {
|
|
170
|
+
LAYERS: 'OSMBrightBackground',
|
|
171
|
+
TILED: true,
|
|
172
|
+
},
|
|
173
|
+
serverType: 'geoserver',
|
|
174
|
+
transition: 0,
|
|
175
|
+
}),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
// Country vector layer with per-feature styles
|
|
179
|
+
const features = new ol.format.GeoJSON().readFeatures(geofeatures, {
|
|
180
|
+
dataProjection: 'EPSG:4326',
|
|
181
|
+
featureProjection: 'EPSG:3857',
|
|
182
|
+
});
|
|
183
|
+
const filtered = features.filter((f) =>
|
|
184
|
+
euCountryNames.includes(f.get('na')),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Apply style to each feature
|
|
188
|
+
filtered.forEach((feature) => {
|
|
189
|
+
const name = feature.get('na');
|
|
190
|
+
const normalized = normalizeCountryName(name);
|
|
191
|
+
const count = countryCounts[normalized] || 0;
|
|
192
|
+
const fillColor = getColorForCount(count);
|
|
193
|
+
|
|
194
|
+
feature.setStyle(
|
|
195
|
+
new ol.style.Style({
|
|
196
|
+
stroke: new ol.style.Stroke({
|
|
197
|
+
color: '#333333',
|
|
198
|
+
width: 1,
|
|
199
|
+
}),
|
|
200
|
+
fill: new ol.style.Fill({
|
|
201
|
+
color: fillColor,
|
|
202
|
+
}),
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
setEuCountriesSource(new ol.source.Vector({ features: filtered }));
|
|
208
|
+
}, [geofeatures, ol, countryCounts]);
|
|
209
|
+
|
|
210
|
+
if (!tileWMSSources || !euCountriesSource) {
|
|
211
|
+
return (
|
|
212
|
+
<div className="navigator-catalogue-map-loading">
|
|
213
|
+
<i className="icon loading" />
|
|
214
|
+
Loading map...
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className="navigator-catalogue-map">
|
|
221
|
+
<div className="navigator-catalogue-map-canvas">
|
|
222
|
+
<Map
|
|
223
|
+
view={{
|
|
224
|
+
center: ol.proj.fromLonLat([14.5, 57], projection),
|
|
225
|
+
projection,
|
|
226
|
+
showFullExtent: true,
|
|
227
|
+
zoom: 3.6,
|
|
228
|
+
}}
|
|
229
|
+
pixelRatio={1}
|
|
230
|
+
>
|
|
231
|
+
<Controls attribution={false} />
|
|
232
|
+
<Layers>
|
|
233
|
+
<CountryClickInteractions
|
|
234
|
+
ol={ol}
|
|
235
|
+
countryCounts={countryCounts}
|
|
236
|
+
onExploreTools={handleExploreTools}
|
|
237
|
+
/>
|
|
238
|
+
<Layer.Vector source={euCountriesSource} zIndex={2} />
|
|
239
|
+
<Layer.Tile source={tileWMSSources[0]} zIndex={0} />
|
|
240
|
+
</Layers>
|
|
241
|
+
</Map>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<aside
|
|
245
|
+
className="navigator-catalogue-map-legend"
|
|
246
|
+
aria-label={intl.formatMessage(messages.toolsPerCountry)}
|
|
247
|
+
>
|
|
248
|
+
<h4 className="legend-title">
|
|
249
|
+
{intl.formatMessage(messages.toolsPerCountry)}
|
|
250
|
+
</h4>
|
|
251
|
+
{mapLegendItems.map((item) => (
|
|
252
|
+
<div key={item.label} className="legend-item">
|
|
253
|
+
<span
|
|
254
|
+
className="legend-color"
|
|
255
|
+
style={{ backgroundColor: item.color }}
|
|
256
|
+
/>
|
|
257
|
+
<span>{item.label}</span>
|
|
258
|
+
</div>
|
|
259
|
+
))}
|
|
260
|
+
</aside>
|
|
7
261
|
</div>
|
|
8
|
-
|
|
9
|
-
|
|
262
|
+
);
|
|
263
|
+
};
|
|
10
264
|
|
|
11
|
-
|
|
265
|
+
/* istanbul ignore next */
|
|
266
|
+
export default compose(
|
|
267
|
+
clientOnly,
|
|
268
|
+
withGeoJsonData(true),
|
|
269
|
+
withResponsiveContainer('navigatorCatalogueMap'),
|
|
270
|
+
withVisibilitySensor(),
|
|
271
|
+
withOpenLayers,
|
|
272
|
+
)(NavigatorCatalogueMapViewInner);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import {
|
|
3
|
+
getColorForCount,
|
|
4
|
+
normalizeCountryName,
|
|
5
|
+
buildCountryCounts,
|
|
6
|
+
mapLegendItems,
|
|
7
|
+
} from './utils';
|
|
8
|
+
|
|
9
|
+
describe('getColorForCount', () => {
|
|
10
|
+
it('returns darkest green for 10+ tools', () => {
|
|
11
|
+
expect(getColorForCount(10)).toBe('#0a5c4e');
|
|
12
|
+
expect(getColorForCount(15)).toBe('#0a5c4e');
|
|
13
|
+
expect(getColorForCount(100)).toBe('#0a5c4e');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns dark green for 7-9 tools', () => {
|
|
17
|
+
expect(getColorForCount(7)).toBe('#0d7a68');
|
|
18
|
+
expect(getColorForCount(8)).toBe('#0d7a68');
|
|
19
|
+
expect(getColorForCount(9)).toBe('#0d7a68');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns base accent green for 4-6 tools', () => {
|
|
23
|
+
expect(getColorForCount(4)).toBe('#289588');
|
|
24
|
+
expect(getColorForCount(5)).toBe('#289588');
|
|
25
|
+
expect(getColorForCount(6)).toBe('#289588');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns light green for 1-3 tools', () => {
|
|
29
|
+
expect(getColorForCount(1)).toBe('#6fc4b8');
|
|
30
|
+
expect(getColorForCount(2)).toBe('#6fc4b8');
|
|
31
|
+
expect(getColorForCount(3)).toBe('#6fc4b8');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns lightest gray-green for 0 or falsy counts', () => {
|
|
35
|
+
expect(getColorForCount(0)).toBe('#e8eded');
|
|
36
|
+
expect(getColorForCount(null)).toBe('#e8eded');
|
|
37
|
+
expect(getColorForCount(undefined)).toBe('#e8eded');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('normalizeCountryName', () => {
|
|
42
|
+
it('normalizes Türkiye to Turkey', () => {
|
|
43
|
+
expect(normalizeCountryName('Türkiye')).toBe('Turkey');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('normalizes Czech Rep. to Czechia', () => {
|
|
47
|
+
expect(normalizeCountryName('Czech Rep.')).toBe('Czechia');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('normalizes Bosnia and Herz. to Bosnia and Herzegovina', () => {
|
|
51
|
+
expect(normalizeCountryName('Bosnia and Herz.')).toBe(
|
|
52
|
+
'Bosnia and Herzegovina',
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns unchanged name for non-special countries', () => {
|
|
57
|
+
expect(normalizeCountryName('Germany')).toBe('Germany');
|
|
58
|
+
expect(normalizeCountryName('Italy')).toBe('Italy');
|
|
59
|
+
expect(normalizeCountryName('France')).toBe('France');
|
|
60
|
+
expect(normalizeCountryName('Spain')).toBe('Spain');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles null and undefined', () => {
|
|
64
|
+
expect(normalizeCountryName(null)).toBeNull();
|
|
65
|
+
expect(normalizeCountryName(undefined)).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('buildCountryCounts', () => {
|
|
70
|
+
it('builds correct lookup from facet data array', () => {
|
|
71
|
+
const facets = {
|
|
72
|
+
'cca_geographic_countries.keyword': [
|
|
73
|
+
{
|
|
74
|
+
field: 'cca_geographic_countries.keyword',
|
|
75
|
+
type: 'value',
|
|
76
|
+
data: [
|
|
77
|
+
{ value: 'Germany', count: 5 },
|
|
78
|
+
{ value: 'Italy', count: 3 },
|
|
79
|
+
{ value: 'France', count: 1 },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const counts = buildCountryCounts(facets);
|
|
86
|
+
expect(counts).toEqual({
|
|
87
|
+
Germany: 5,
|
|
88
|
+
Italy: 3,
|
|
89
|
+
France: 1,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles empty facet data', () => {
|
|
94
|
+
const facets = {
|
|
95
|
+
'cca_geographic_countries.keyword': [
|
|
96
|
+
{ field: 'cca_geographic_countries.keyword', data: [] },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
const counts = buildCountryCounts(facets);
|
|
100
|
+
expect(Object.keys(counts)).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('handles missing facet gracefully', () => {
|
|
104
|
+
const facets = {};
|
|
105
|
+
const counts = buildCountryCounts(facets);
|
|
106
|
+
expect(counts).toEqual({});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('handles undefined facets gracefully', () => {
|
|
110
|
+
const counts = buildCountryCounts(undefined);
|
|
111
|
+
expect(counts).toEqual({});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('handles null facets gracefully', () => {
|
|
115
|
+
const counts = buildCountryCounts(null);
|
|
116
|
+
expect(counts).toEqual({});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('handles non-array facet value', () => {
|
|
120
|
+
const facets = {
|
|
121
|
+
'cca_geographic_countries.keyword': {
|
|
122
|
+
data: [{ value: 'Germany', count: 5 }],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const counts = buildCountryCounts(facets);
|
|
126
|
+
expect(counts).toEqual({ Germany: 5 });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('handles missing count values', () => {
|
|
130
|
+
const facets = {
|
|
131
|
+
'cca_geographic_countries.keyword': [{ data: [{ value: 'Germany' }] }],
|
|
132
|
+
};
|
|
133
|
+
const counts = buildCountryCounts(facets);
|
|
134
|
+
expect(counts.Germany).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('mapLegendItems', () => {
|
|
139
|
+
it('has 5 legend items', () => {
|
|
140
|
+
expect(mapLegendItems).toHaveLength(5);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('colors match getColorForCount scale', () => {
|
|
144
|
+
expect(mapLegendItems[0].color).toBe(getColorForCount(10));
|
|
145
|
+
expect(mapLegendItems[1].color).toBe(getColorForCount(7));
|
|
146
|
+
expect(mapLegendItems[2].color).toBe(getColorForCount(4));
|
|
147
|
+
expect(mapLegendItems[3].color).toBe(getColorForCount(1));
|
|
148
|
+
expect(mapLegendItems[4].color).toBe(getColorForCount(0));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('has correct labels', () => {
|
|
152
|
+
expect(mapLegendItems.map((item) => item.label)).toEqual([
|
|
153
|
+
'10+',
|
|
154
|
+
'7–9',
|
|
155
|
+
'4–6',
|
|
156
|
+
'1–3',
|
|
157
|
+
'None',
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color scale for tool counts (EEA green tones)
|
|
3
|
+
* Used by the map view to color-code countries
|
|
4
|
+
*/
|
|
5
|
+
export const getColorForCount = (count) => {
|
|
6
|
+
if (count >= 10) return '#0a5c4e'; // darkest
|
|
7
|
+
if (count >= 7) return '#0d7a68';
|
|
8
|
+
if (count >= 4) return '#289588'; // base accent
|
|
9
|
+
if (count >= 1) return '#6fc4b8'; // light
|
|
10
|
+
return '#e8eded'; // none (very light gray-green)
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize country names between GeoJSON and facet data
|
|
15
|
+
*/
|
|
16
|
+
export const normalizeCountryName = (name) => {
|
|
17
|
+
if (!name) return name;
|
|
18
|
+
const normalizer = {
|
|
19
|
+
Türkiye: 'Turkey',
|
|
20
|
+
'United Kingdom': 'United Kingdom',
|
|
21
|
+
'Czech Rep.': 'Czechia',
|
|
22
|
+
'Bosnia and Herz.': 'Bosnia and Herzegovina',
|
|
23
|
+
};
|
|
24
|
+
return normalizer[name] || name;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a country -> count lookup from facet data
|
|
29
|
+
* @param {Object} facets - searchContext.facets
|
|
30
|
+
* @returns {Object} countryName -> count
|
|
31
|
+
*/
|
|
32
|
+
export const buildCountryCounts = (facets) => {
|
|
33
|
+
const countriesFacetArray = facets?.['cca_geographic_countries.keyword'];
|
|
34
|
+
const countriesFacet = Array.isArray(countriesFacetArray)
|
|
35
|
+
? countriesFacetArray[0]
|
|
36
|
+
: countriesFacetArray;
|
|
37
|
+
const countryData = countriesFacet?.data || [];
|
|
38
|
+
|
|
39
|
+
const counts = {};
|
|
40
|
+
countryData.forEach((entry) => {
|
|
41
|
+
counts[entry.value] = entry.count;
|
|
42
|
+
});
|
|
43
|
+
return counts;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Legend items for the map view
|
|
48
|
+
*/
|
|
49
|
+
export const mapLegendItems = [
|
|
50
|
+
{ label: '10+', color: '#0a5c4e' },
|
|
51
|
+
{ label: '7–9', color: '#0d7a68' },
|
|
52
|
+
{ label: '4–6', color: '#289588' },
|
|
53
|
+
{ label: '1–3', color: '#6fc4b8' },
|
|
54
|
+
{ label: 'None', color: '#e8eded' },
|
|
55
|
+
];
|
|
56
|
+
|
|
1
57
|
export const asArray = (value) => {
|
|
2
58
|
if (!value) return [];
|
|
3
59
|
const raw = value.raw !== undefined ? value.raw : value;
|
|
@@ -67,6 +67,10 @@ const messages = defineMessages({
|
|
|
67
67
|
id: 'Open tool',
|
|
68
68
|
defaultMessage: 'Open tool',
|
|
69
69
|
},
|
|
70
|
+
removeTool: {
|
|
71
|
+
id: 'Remove {title} from comparison',
|
|
72
|
+
defaultMessage: 'Remove {title} from comparison',
|
|
73
|
+
},
|
|
70
74
|
backToResults: {
|
|
71
75
|
id: 'Back to results',
|
|
72
76
|
defaultMessage: 'Back to results',
|
|
@@ -319,7 +323,7 @@ const CompareToolsView = () => {
|
|
|
319
323
|
)}
|
|
320
324
|
|
|
321
325
|
{!isLoading && hasEnoughTools && (
|
|
322
|
-
<Table celled>
|
|
326
|
+
<Table celled aria-label={compareToolsTitle} unstackable>
|
|
323
327
|
<Table.Header>
|
|
324
328
|
<Table.Row>
|
|
325
329
|
<Table.HeaderCell>
|
|
@@ -353,9 +357,12 @@ const CompareToolsView = () => {
|
|
|
353
357
|
)}
|
|
354
358
|
<Button
|
|
355
359
|
className="icon compare-tool-clear"
|
|
360
|
+
aria-label={intl.formatMessage(messages.removeTool, {
|
|
361
|
+
title: tool.title,
|
|
362
|
+
})}
|
|
356
363
|
onClick={() => removeTool(tool.id)}
|
|
357
364
|
>
|
|
358
|
-
<Icon className="ri-close-line" />
|
|
365
|
+
<Icon className="ri-close-line" aria-hidden="true" />
|
|
359
366
|
</Button>
|
|
360
367
|
</div>
|
|
361
368
|
</div>
|
|
@@ -365,7 +372,7 @@ const CompareToolsView = () => {
|
|
|
365
372
|
</Table.Header>
|
|
366
373
|
<Table.Body>
|
|
367
374
|
<Table.Row>
|
|
368
|
-
<Table.Cell>
|
|
375
|
+
<Table.Cell as="th" scope="row">
|
|
369
376
|
<div className="compare-criteria">
|
|
370
377
|
<div className="compare-criteria-title">
|
|
371
378
|
{intl.formatMessage(messages.usability)}
|
|
@@ -381,7 +388,7 @@ const CompareToolsView = () => {
|
|
|
381
388
|
))}
|
|
382
389
|
</Table.Row>
|
|
383
390
|
<Table.Row>
|
|
384
|
-
<Table.Cell>
|
|
391
|
+
<Table.Cell as="th" scope="row">
|
|
385
392
|
<div className="compare-criteria">
|
|
386
393
|
<div className="compare-criteria-title">
|
|
387
394
|
{intl.formatMessage(messages.functionality)}
|
|
@@ -399,7 +406,7 @@ const CompareToolsView = () => {
|
|
|
399
406
|
))}
|
|
400
407
|
</Table.Row>
|
|
401
408
|
<Table.Row>
|
|
402
|
-
<Table.Cell>
|
|
409
|
+
<Table.Cell as="th" scope="row">
|
|
403
410
|
<div className="compare-criteria">
|
|
404
411
|
<div className="compare-criteria-title">
|
|
405
412
|
{intl.formatMessage(messages.spatialScale)}
|
|
@@ -413,7 +420,7 @@ const CompareToolsView = () => {
|
|
|
413
420
|
))}
|
|
414
421
|
</Table.Row>
|
|
415
422
|
<Table.Row>
|
|
416
|
-
<Table.Cell>
|
|
423
|
+
<Table.Cell as="th" scope="row">
|
|
417
424
|
<div className="compare-criteria">
|
|
418
425
|
<div className="compare-criteria-title">
|
|
419
426
|
{intl.formatMessage(messages.outputType)}
|
|
@@ -427,7 +434,7 @@ const CompareToolsView = () => {
|
|
|
427
434
|
))}
|
|
428
435
|
</Table.Row>
|
|
429
436
|
<Table.Row>
|
|
430
|
-
<Table.Cell>
|
|
437
|
+
<Table.Cell as="th" scope="row">
|
|
431
438
|
<div className="compare-criteria">
|
|
432
439
|
<div className="compare-criteria-title">
|
|
433
440
|
{intl.formatMessage(messages.adaptationSupportCycleStep)}
|
|
@@ -444,7 +451,7 @@ const CompareToolsView = () => {
|
|
|
444
451
|
))}
|
|
445
452
|
</Table.Row>
|
|
446
453
|
<Table.Row>
|
|
447
|
-
<Table.Cell>
|
|
454
|
+
<Table.Cell as="th" scope="row">
|
|
448
455
|
<div className="compare-criteria">
|
|
449
456
|
<div className="compare-criteria-title">
|
|
450
457
|
{intl.formatMessage(messages.sector)}
|