@eeacms/volto-cca-policy 1.0.2 → 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 +259 -3
- package/jest-addon.config.js +4 -2
- 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 +224 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +173 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueContentView.jsx +189 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.jsx +272 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.test.jsx +160 -0
- package/src/components/Search/NavigatorCatalogue/utils.js +157 -0
- package/src/components/Search/NavigatorCatalogue/utils.test.js +132 -0
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +409 -0
- package/src/components/Search/NavigatorGuide/NavigatorGuideLayout.jsx +8 -0
- package/src/components/index.js +2 -0
- package/src/components/theme/CompareTools/CompareToolsPanel.jsx +151 -0
- package/src/components/theme/CompareTools/CompareToolsPanel.test.jsx +96 -0
- package/src/components/theme/CompareTools/CompareToolsView.jsx +475 -0
- package/src/components/theme/CompareTools/CompareToolsView.test.jsx +110 -0
- package/src/components/theme/CompareTools/utils.js +111 -0
- package/src/components/theme/CompareTools/utils.test.jsx +217 -0
- package/src/components/theme/Views/ExtendedToolView.jsx +201 -201
- package/src/components/theme/Views/ExtendedToolView.test.jsx +420 -58
- package/src/constants.js +0 -48
- package/src/customizations/volto/reducers/breadcrumbs/breadcrumbs.js +1 -1
- package/src/helpers/Utils.jsx +29 -0
- package/src/helpers/index.js +3 -0
- package/src/index.js +28 -2
- package/src/search/index.js +6 -0
- package/src/search/navigator_catalogue/config.js +89 -0
- package/src/search/navigator_catalogue/facets.js +86 -0
- package/src/search/navigator_catalogue/views.js +28 -0
- package/src/search/navigator_guide/config.js +70 -0
- package/src/search/navigator_guide/facets.js +31 -0
- package/src/search/navigator_guide/guideSteps.js +93 -0
- package/src/slate-styles.less +4 -0
- package/src/state.js +1 -0
- package/theme/elements/button.overrides +0 -11
- package/theme/elements/label.overrides +15 -0
- package/theme/globals/blocks.less +3 -2
- package/theme/globals/navigator.less +1090 -0
- package/theme/globals/site.overrides +5 -0
- package/theme/tokens/colors.less +2 -0
- 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
|
@@ -0,0 +1,272 @@
|
|
|
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';
|
|
22
|
+
|
|
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>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
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
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
|
|
57
|
+
export const asArray = (value) => {
|
|
58
|
+
if (!value) return [];
|
|
59
|
+
const raw = value.raw !== undefined ? value.raw : value;
|
|
60
|
+
if (!raw) return [];
|
|
61
|
+
const values = Array.isArray(raw)
|
|
62
|
+
? raw.filter(Boolean)
|
|
63
|
+
: [raw].filter(Boolean);
|
|
64
|
+
|
|
65
|
+
return values.map((value) => value?.title || value?.token || value);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const arrayFieldToString = (value) => asArray(value).join(', ');
|
|
69
|
+
|
|
70
|
+
export const formatFunctionalityScore = (value) => {
|
|
71
|
+
const raw = value?.raw !== undefined ? value.raw : value;
|
|
72
|
+
const score = raw === null || raw === undefined ? Number.NaN : Number(raw);
|
|
73
|
+
|
|
74
|
+
return Number.isFinite(score) ? `${Math.min(6, Math.max(0, score))}/6` : '—';
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const escapeCsvValue = (value) => {
|
|
78
|
+
const stringValue = value === undefined || value === null ? '' : `${value}`;
|
|
79
|
+
return `"${stringValue.replace(/"/g, '""')}"`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const downloadCsv = (filename, rows) => {
|
|
83
|
+
const csv = rows.map((row) => row.map(escapeCsvValue).join(',')).join('\n');
|
|
84
|
+
|
|
85
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
86
|
+
const url = URL.createObjectURL(blob);
|
|
87
|
+
const link = document.createElement('a');
|
|
88
|
+
|
|
89
|
+
link.href = url;
|
|
90
|
+
link.setAttribute('download', filename);
|
|
91
|
+
document.body.appendChild(link);
|
|
92
|
+
link.click();
|
|
93
|
+
link.remove();
|
|
94
|
+
|
|
95
|
+
URL.revokeObjectURL(url);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const getLocalizedLandingPageURL = (appConfig, currentLang = 'en') => {
|
|
99
|
+
const lang = currentLang || 'en';
|
|
100
|
+
const landingPageURL =
|
|
101
|
+
appConfig?.landingPageURL || '/en/navigator/tool-catalogue';
|
|
102
|
+
|
|
103
|
+
return landingPageURL.replace(/^\/en(?=\/|$)/, `/${lang}`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const getComparePageURL = (appConfig, currentLang = 'en') => {
|
|
107
|
+
const lang = currentLang || 'en';
|
|
108
|
+
const comparePageURL = appConfig?.comparePageURL || '/en/navigator/compare';
|
|
109
|
+
|
|
110
|
+
return comparePageURL.replace(/^\/en(?=\/|$)/, `/${lang}`);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const exportComparisonTable = (tools, getToolField) => {
|
|
114
|
+
const rows = [
|
|
115
|
+
['Criteria', ...tools.map((tool) => tool.title)],
|
|
116
|
+
[
|
|
117
|
+
'Usability',
|
|
118
|
+
...tools.map((tool) =>
|
|
119
|
+
arrayFieldToString(getToolField(tool, 'accessibility_and_usability')),
|
|
120
|
+
),
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
'Functionality',
|
|
124
|
+
...tools.map((tool) =>
|
|
125
|
+
formatFunctionalityScore(getToolField(tool, 'functionality')),
|
|
126
|
+
),
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
'Spatial scale',
|
|
130
|
+
...tools.map((tool) =>
|
|
131
|
+
arrayFieldToString(getToolField(tool, 'cca_geographical_scale')),
|
|
132
|
+
),
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
'Output type',
|
|
136
|
+
...tools.map((tool) =>
|
|
137
|
+
arrayFieldToString(getToolField(tool, 'cca_type_of_outputs')),
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
'Adaptation support cycle step',
|
|
142
|
+
...tools.map((tool) =>
|
|
143
|
+
arrayFieldToString(
|
|
144
|
+
getToolField(tool, 'cca_adaptation_support_cycle_step'),
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
'Sector',
|
|
150
|
+
...tools.map((tool) =>
|
|
151
|
+
arrayFieldToString(getToolField(tool, 'cca_adaptation_sectors')),
|
|
152
|
+
),
|
|
153
|
+
],
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
downloadCsv('navigator-catalogue-comparison.csv', rows);
|
|
157
|
+
};
|