@eeacms/volto-cca-policy 0.2.26 → 0.2.28

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.
@@ -69,7 +69,15 @@ const makeSearchBlockQuery = ({ base, query, field, value }) => {
69
69
  const makeEEASearchQuery = ({ base, field, value }) => {
70
70
  // TODO: don't hardcode the language
71
71
  const rest =
72
- 'filters[1][field]=issued.date&filters[1][values][0]=Last 5 years&filters[1][type]=any&filters[2][field]=language&filters[2][values][0]=en&filters[2][type]=any&sort-field=issued.date&sort-direction=desc';
72
+ 'filters[1][field]=issued.date' +
73
+ '&filters[1][values][0]=Last 5 years' +
74
+ '&filters[1][type]=any' +
75
+ '&filters[2][field]=language' +
76
+ '&filters[2][values][0]=en' +
77
+ '&filters[2][type]=any' +
78
+ '&sort-field=issued.date' +
79
+ '&sort-direction=desc';
80
+
73
81
  const filter = `filters[0][field]=${field}&filters[0][type]=any&filters[0][values][0]=${value}`;
74
82
 
75
83
  return `${base}?size=n_10_n&${filter}&${rest}`;
@@ -20,19 +20,19 @@ const healthImpactIcons = {
20
20
  };
21
21
 
22
22
  const portalTypeIcons = {
23
+ 'Adaptation option': 'cogs',
23
24
  'Case study': 'file text',
24
25
  Guidance: 'compass',
25
26
  Indicator: 'area chart',
26
27
  'Information portal': 'info circle',
27
- 'Publications and reports': 'newspaper',
28
+ Organisation: 'sitemap',
29
+ 'Publication and report': 'newspaper',
28
30
  'Research and knowledge project': 'university',
29
31
  Tool: 'wrench',
30
- 'Videos and podcasts': 'video play',
32
+ 'Video and podcast': 'video play',
31
33
  // 'eea.climateadapt.aceproject': '',
32
- // 'eea.climateadapt.adaptationoption': '',
33
34
  // 'eea.climateadapt.c3sindicator': '',
34
35
  // 'eea.climateadapt.mapgraphdataset': '',
35
- // 'eea.climateadapt.organisation': '',
36
36
  };
37
37
 
38
38
  const portalTypesToSearchTypes = {
@@ -42,15 +42,15 @@ const portalTypesToSearchTypes = {
42
42
  'eea.climateadapt.indicator': 'Indicator',
43
43
  'eea.climateadapt.c3sindicator': 'Indicator',
44
44
  'eea.climateadapt.informationportal': 'Information portal',
45
- 'eea.climateadapt.publicationreport': 'Publications and reports',
45
+ 'eea.climateadapt.organisation': 'Organisation',
46
+ 'eea.climateadapt.publicationreport': 'Publication and report',
46
47
  'eea.climateadapt.aceproject': 'Research and knowledge project',
47
48
  'eea.climateadapt.researchproject': 'Research and knowledge project',
48
49
  'eea.climateadapt.tool': 'Tool',
49
- 'eea.climateadapt.video': 'Videos and podcasts',
50
+ 'eea.climateadapt.video': 'Video and podcast',
50
51
  // TODO: what about these?
51
52
  // 'eea.climateadapt.aceproject': '',
52
53
  // 'eea.climateadapt.mapgraphdataset': '',
53
- // 'eea.climateadapt.organisation': '',
54
54
  };
55
55
 
56
56
  export default function installCollectionStatsBlock(config) {
@@ -0,0 +1,78 @@
1
+ import React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import configureStore from 'redux-mock-store';
4
+ import renderer from 'react-test-renderer';
5
+ import '@testing-library/jest-dom/extend-expect';
6
+ import { Provider } from 'react-intl-redux';
7
+ import DropdownListView from './DropdownListView';
8
+ import config from '@plone/volto/registry';
9
+
10
+ config.blocks = {
11
+ blocksConfig: {
12
+ contentLinks: {
13
+ variations: [
14
+ {
15
+ id: 'default',
16
+ title: 'Simple list (default)',
17
+ isDefault: true,
18
+ },
19
+ {
20
+ id: 'navigationList',
21
+ title: 'Navigation list',
22
+ isDefault: false,
23
+ },
24
+ ],
25
+ },
26
+ },
27
+ };
28
+
29
+ const mockStore = configureStore();
30
+
31
+ describe('DropdownListView', () => {
32
+ it('should render the component', () => {
33
+ const data = {
34
+ '@type': 'contentLinks',
35
+ items: [
36
+ {
37
+ item_title: 'Item 1',
38
+ source: [
39
+ {
40
+ '@id': '/item-1',
41
+ title: 'Item 1',
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ item_title: 'Item 2',
47
+ source: [
48
+ {
49
+ '@id': '/item-2',
50
+ title: 'Item 2',
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ show_share_btn: false,
56
+ title: 'Navigation',
57
+ variation: 'default',
58
+ };
59
+
60
+ const store = mockStore({
61
+ userSession: { token: '1234' },
62
+ intl: {
63
+ locale: 'en',
64
+ messages: {},
65
+ },
66
+ });
67
+
68
+ const component = renderer.create(
69
+ <Provider store={store}>
70
+ <MemoryRouter>
71
+ <DropdownListView {...data} />
72
+ </MemoryRouter>
73
+ </Provider>,
74
+ );
75
+ const json = component.toJSON();
76
+ expect(json).toMatchSnapshot();
77
+ });
78
+ });
@@ -5,6 +5,7 @@ import {
5
5
  setTooltipVisibility,
6
6
  getClosestFeatureToCoordinate,
7
7
  } from '@eeacms/volto-cca-policy/helpers/country_map/countryMap';
8
+ import { openlayers as ol } from '@eeacms/volto-openlayers-map';
8
9
  import { useMapContext } from '@eeacms/volto-openlayers-map/api';
9
10
 
10
11
  export const Interactions = ({
@@ -30,7 +31,12 @@ export const Interactions = ({
30
31
  const feature = getClosestFeatureToCoordinate(
31
32
  evt.coordinate,
32
33
  euCountryFeatures.current,
34
+ ol,
33
35
  );
36
+ if (!feature) {
37
+ const node = tooltipRef.current;
38
+ node.style.visibility = 'hidden';
39
+ }
34
40
  const domEvt = evt.originalEvent;
35
41
 
36
42
  if (
@@ -105,6 +111,7 @@ Information is available
105
111
  const feature = getClosestFeatureToCoordinate(
106
112
  evt.coordinate,
107
113
  euCountryFeatures.current,
114
+ ol,
108
115
  );
109
116
 
110
117
  highlight.current = feature && feature.get('na');
@@ -113,7 +113,12 @@ const View = (props) => {
113
113
  new ol.source.TileWMS({
114
114
  url: 'https://gisco-services.ec.europa.eu/maps/service',
115
115
  params: {
116
- // LAYERS: 'OSMBlossomComposite', OSMCartoComposite, OSMPositronComposite
116
+ // LAYERS: 'OSMBlossomComposite',
117
+ // LAYERS: 'OSMCartoComposite',
118
+ // LAYERS: 'OSMPositronComposite',
119
+ // LAYERS: 'GreyEarth',
120
+ // LAYERS: 'OSMCarto',
121
+ // LAYERS: 'NaturalEarth',
117
122
  LAYERS: 'OSMBrightBackground',
118
123
  TILED: true,
119
124
  },
@@ -137,7 +142,7 @@ const View = (props) => {
137
142
  // console.log('filtered', euCountriesSource?.getFeatures() || 'NOT SET YET');
138
143
 
139
144
  return (
140
- <div>
145
+ <div className="ol-country-map">
141
146
  <Grid columns="12">
142
147
  <Grid.Column mobile={12} tablet={12} computer={10} className="col-left">
143
148
  {tileWMSSources ? (
@@ -33,7 +33,7 @@ export const makeStyles = (highlight) => {
33
33
  const fill = new ol.style.Fill({ color: 'rgb(251,250,230, 0.8)' });
34
34
  const stroke = new ol.style.Stroke({
35
35
  // color: 'rgba(255,255,255,0.8)',
36
- color: '#d1d1d1',
36
+ color: '#333333',
37
37
  width: 1,
38
38
  });
39
39
 
@@ -43,13 +43,13 @@ export const makeStyles = (highlight) => {
43
43
 
44
44
  const getFillColor = (feature) => {
45
45
  if (feature.get('fillBlue') === 'blue1') {
46
- return new ol.style.Fill({ color: 'rgb(0, 75, 127, 0.8)' });
46
+ return new ol.style.Fill({ color: 'rgb(0, 75, 127, 1)' });
47
47
  }
48
48
  if (feature.get('fillBlue') === 'blue2') {
49
- return new ol.style.Fill({ color: 'rgb(10, 153, 255, 0.8)' });
49
+ return new ol.style.Fill({ color: 'rgb(10, 153, 255, 1)' });
50
50
  }
51
51
  if (feature.get('fillBlue') === 'blue3') {
52
- return new ol.style.Fill({ color: 'rgb(120, 217, 252, 0.8)' });
52
+ return new ol.style.Fill({ color: 'rgb(120, 217, 252, 1)' });
53
53
  }
54
54
  // console.log(feature.get('fillBlue'));
55
55
  return fill;
@@ -1,4 +1,14 @@
1
+ @height: 600px;
2
+
1
3
  .countryMapProfile {
4
+ height: @height;
5
+ min-height: @height;
6
+
7
+ .ol-map-wrapper,
8
+ .ol-map {
9
+ height: @height;
10
+ }
11
+
2
12
  &.sized-wrapper {
3
13
  width: 100%;
4
14
  height: 100%;
@@ -4,6 +4,7 @@ import ListingBody from '@plone/volto/components/manage/Blocks/Listing/ListingBo
4
4
  import { useSelector, useDispatch } from 'react-redux';
5
5
  import { getVocabulary } from '@plone/volto/actions';
6
6
  import { OTHER_REGIONS } from '@eeacms/volto-cca-policy/helpers';
7
+ import { Link } from 'react-router-dom';
7
8
  import {
8
9
  Option,
9
10
  DropdownIndicator,
@@ -19,6 +20,7 @@ const Select = loadable(() => import('react-select'));
19
20
 
20
21
  const IMPACTS = 'eea.climateadapt.aceitems_climateimpacts';
21
22
  const SECTORS = 'eea.climateadapt.aceitems_sectors';
23
+ const KEY_TYPE = 'eea.climateadapt.aceitems_key_type_measures_short';
22
24
 
23
25
  const FIELDS = [
24
26
  'bio_regions',
@@ -68,7 +70,28 @@ const sectors_no_value = [
68
70
  },
69
71
  ];
70
72
 
71
- const applyQuery = (id, data, currentLang, impacts, sectors) => {
73
+ const measures_no_value = [
74
+ {
75
+ label: 'All key type measures',
76
+ value: '',
77
+ },
78
+ ];
79
+
80
+ const _datatypes = {
81
+ DOCUMENT: 'Publications and reports',
82
+ INFORMATIONSOURCE: 'Information portal',
83
+ MAPGRAPHDATASET: 'Maps, graphs and datasets',
84
+ INDICATOR: 'Indicator',
85
+ GUIDANCE: 'Guidance',
86
+ TOOL: 'Tool',
87
+ RESEARCHPROJECT: 'Research and knowledge project',
88
+ MEASURE: 'Adaptation option',
89
+ ACTION: 'Case study',
90
+ ORGANISATION: 'Organisation',
91
+ VIDEOS: 'Videos',
92
+ };
93
+
94
+ const applyQuery = (id, data, currentLang, impacts, sectors, measures) => {
72
95
  const defaultQuery = Object.entries(data)
73
96
  .filter(
74
97
  ([field, value]) => FIELDS.includes(field) && value && value.length > 0,
@@ -95,20 +118,91 @@ const applyQuery = (id, data, currentLang, impacts, sectors) => {
95
118
  v: currentLang,
96
119
  });
97
120
 
121
+ defaultQuery.push({
122
+ i: 'review_state',
123
+ o: 'plone.app.querystring.operation.selection.any',
124
+ v: 'published',
125
+ });
126
+
98
127
  if (impacts) defaultQuery.push(impacts);
99
128
  if (sectors) defaultQuery.push(sectors);
129
+ if (measures) defaultQuery.push(measures);
100
130
 
131
+ const sort_on = data.sortBy || 'effective';
101
132
  return {
102
133
  block: id,
103
134
  limit: data.nr_items,
104
135
  query: defaultQuery,
105
- sort_on: data.sortBy || 'effective',
106
- sort_order: 'descending',
136
+ sort_on,
137
+ sort_order: sort_on === 'getId' ? 'ascending' : 'descending',
107
138
  template: 'summary',
108
139
  itemModel: { '@type': 'simpleItem' },
109
140
  };
110
141
  };
111
142
 
143
+ // mapping of "internal data stored fieldname" to name of EEA Search facet field
144
+ // TODO: this needs to be completed. As of today (21-05-2024), all the fields that are used in production blocks are present, but not all possible of them are properly declared here
145
+ const eeaSearchFieldMap = {
146
+ impacts: 'cca_climate_impacts.keyword',
147
+ sectors: 'cca_adaptation_sectors.keyword',
148
+ measures: 'cca_key_type_measure.keyword',
149
+ funding_programme: 'cca_funding_programme.keyword',
150
+ search_type: 'objectProvides',
151
+ language: 'language',
152
+ };
153
+
154
+ function toLabel(value, key, vocab) {
155
+ if (key === 'search_type') return _datatypes[value];
156
+ if (key === 'measures') return value;
157
+ if (!vocab) return value;
158
+
159
+ return vocab.find(({ value: v }) => value === v)?.label || value;
160
+ }
161
+
162
+ function buildQueryUrl({ vocabs, ...data }) {
163
+ let filters = Object.keys(data).reduce((acc, key) => {
164
+ const name = eeaSearchFieldMap[key];
165
+ const ploneFilter = data[key];
166
+ if (ploneFilter) {
167
+ const realValue = ploneFilter?.v || ploneFilter;
168
+ let filter = [
169
+ `filters[$index][field]=${name}`,
170
+ `filters[$index][type]=any`,
171
+ ];
172
+ if (Array.isArray(realValue)) {
173
+ const mv = realValue.map((rv) => toLabel(rv, key, vocabs[key]));
174
+ mv.forEach((v, i) => {
175
+ filter.push(`filters[$index][values][${i}]=${encodeURIComponent(v)}`);
176
+ });
177
+ } else {
178
+ const rv = toLabel(realValue, key, vocabs[key]);
179
+ filter.push(`filters[$index][values][0]=${encodeURIComponent(rv)}`);
180
+ }
181
+
182
+ filter = filter.join('&');
183
+ acc.push(filter);
184
+ }
185
+ return acc;
186
+ }, []);
187
+
188
+ filters = filters
189
+ .map((line, index) => line.replaceAll('$index', index))
190
+ .join('&');
191
+ const url = `/en/data-and-downloads?size=n_10&${filters}`;
192
+
193
+ return url;
194
+ }
195
+
196
+ const vocabImpactsAction = getVocabulary({
197
+ vocabNameOrURL: IMPACTS,
198
+ });
199
+ const vocabSectorsAction = getVocabulary({
200
+ vocabNameOrURL: SECTORS,
201
+ });
202
+ const vocabMeasuresAction = getVocabulary({
203
+ vocabNameOrURL: KEY_TYPE,
204
+ });
205
+
112
206
  const FilterAceContentView = (props) => {
113
207
  const { data, id, mode = 'view' } = props;
114
208
  const dispatch = useDispatch();
@@ -123,22 +217,20 @@ const FilterAceContentView = (props) => {
123
217
  ? state.vocabularies[SECTORS].items
124
218
  : [],
125
219
  );
220
+ const measuresVocabItems = useSelector((state) =>
221
+ state.vocabularies[KEY_TYPE]?.loaded
222
+ ? state.vocabularies[KEY_TYPE].items
223
+ : [],
224
+ );
126
225
 
127
226
  const [impactsQuery, setImpactsQueryQuery] = React.useState();
128
227
  const [sectorsQuery, setSectorsQuery] = React.useState();
228
+ const [measuresQuery, setMeasuresQuery] = React.useState();
129
229
 
130
230
  React.useEffect(() => {
131
- const action = getVocabulary({
132
- vocabNameOrURL: IMPACTS,
133
- });
134
- dispatch(action);
135
- }, [dispatch]);
136
-
137
- React.useEffect(() => {
138
- const action = getVocabulary({
139
- vocabNameOrURL: SECTORS,
140
- });
141
- dispatch(action);
231
+ dispatch(vocabImpactsAction);
232
+ dispatch(vocabSectorsAction);
233
+ dispatch(vocabMeasuresAction);
142
234
  }, [dispatch]);
143
235
 
144
236
  const listingBodyData = applyQuery(
@@ -147,41 +239,60 @@ const FilterAceContentView = (props) => {
147
239
  currentLang,
148
240
  impactsQuery,
149
241
  sectorsQuery,
242
+ measuresQuery,
150
243
  );
244
+ const viewAllUrl = buildQueryUrl({
245
+ // ...data,
246
+ impacts: impactsQuery,
247
+ sectors: sectorsQuery,
248
+ measures: measuresQuery,
249
+ search_type: data.search_type,
250
+ funding_programme: data.funding_programme,
251
+ language: currentLang,
252
+ vocabs: {
253
+ sectors: sectorsVocabItems,
254
+ measures: measuresVocabItems,
255
+ impacts: impactsVocabItems,
256
+ },
257
+ });
258
+
259
+ // console.log({ allData: data, viewAllUrl });
151
260
 
152
261
  return (
153
262
  <div className="block filter-acecontent-block">
154
263
  {data.title && <h4>{data.title}</h4>}
155
- <h5>
264
+ <span className="filter-title">
156
265
  <FormattedMessage id="Climate impact" defaultMessage="Climate impact" />
157
- </h5>
158
- <Select
159
- id="field-impacts"
160
- name="impacts"
161
- disabled={false}
162
- className="react-select-container"
163
- classNamePrefix="react-select"
164
- options={[impacts_no_value[0], ...(impactsVocabItems || [])]}
165
- styles={customSelectStyles}
166
- theme={selectTheme}
167
- components={{ DropdownIndicator, Option }}
168
- defaultValue={impacts_no_value}
169
- onChange={({ value }) => {
170
- if (value) {
171
- setImpactsQueryQuery({
172
- i: 'climate_impacts',
173
- o: 'plone.app.querystring.operation.selection.any',
174
- v: value,
175
- });
176
- } else {
177
- setImpactsQueryQuery(null);
178
- }
179
- }}
180
- />
266
+ </span>
267
+ <div>
268
+ <Select
269
+ id="field-impacts"
270
+ name="impacts"
271
+ disabled={false}
272
+ className="react-select-container"
273
+ classNamePrefix="react-select"
274
+ options={[impacts_no_value[0], ...(impactsVocabItems || [])]}
275
+ styles={customSelectStyles}
276
+ theme={selectTheme}
277
+ components={{ DropdownIndicator, Option }}
278
+ defaultValue={impacts_no_value}
279
+ onChange={({ value }) => {
280
+ if (value) {
281
+ setImpactsQueryQuery({
282
+ i: 'climate_impacts',
283
+ o: 'plone.app.querystring.operation.selection.any',
284
+ v: value,
285
+ });
286
+ } else {
287
+ setImpactsQueryQuery(null);
288
+ }
289
+ }}
290
+ />
291
+ </div>
181
292
 
182
- <h5>
293
+ <span className="filter-title">
183
294
  <FormattedMessage id="Sector" defaultMessage="Sector" />
184
- </h5>
295
+ </span>
185
296
  <Select
186
297
  id="field-sectors"
187
298
  name="sectors"
@@ -198,13 +309,45 @@ const FilterAceContentView = (props) => {
198
309
  setSectorsQuery({
199
310
  i: 'sectors',
200
311
  o: 'plone.app.querystring.operation.selection.any',
201
- v: value,
312
+ v: value.toUpperCase(),
202
313
  });
203
314
  } else {
204
315
  setSectorsQuery(null);
205
316
  }
206
317
  }}
207
318
  />
319
+
320
+ <div id="key-type-measure">
321
+ <span className="filter-title">
322
+ <FormattedMessage
323
+ id="Key Type Measure"
324
+ defaultMessage="Key Type Measure"
325
+ />
326
+ </span>
327
+ <Select
328
+ id="field-measure"
329
+ name="measure"
330
+ disabled={false}
331
+ className="react-select-container"
332
+ classNamePrefix="react-select"
333
+ options={[measures_no_value[0], ...(measuresVocabItems || [])]}
334
+ styles={customSelectStyles}
335
+ theme={selectTheme}
336
+ components={{ DropdownIndicator, Option }}
337
+ defaultValue={measures_no_value}
338
+ onChange={({ value }) => {
339
+ if (value) {
340
+ setMeasuresQuery({
341
+ i: 'key_type_measures',
342
+ o: 'plone.app.querystring.operation.selection.any',
343
+ v: value,
344
+ });
345
+ } else {
346
+ setMeasuresQuery(null);
347
+ }
348
+ }}
349
+ />
350
+ </div>
208
351
  <div className="listing-wrapper">
209
352
  <ListingBody
210
353
  id={id}
@@ -213,6 +356,9 @@ const FilterAceContentView = (props) => {
213
356
  isEditMode={mode === 'edit'}
214
357
  />
215
358
  </div>
359
+ <Link className="ui button secondary inverted" to={viewAllUrl}>
360
+ View all
361
+ </Link>
216
362
  </div>
217
363
  );
218
364
  };
@@ -18,6 +18,7 @@ const FilterSchema = (data, macro_regions) => {
18
18
  'bio_regions',
19
19
  'funding_programme',
20
20
  'nr_items',
21
+ 'sortBy',
21
22
  ],
22
23
  },
23
24
  ],
@@ -1,3 +1,19 @@
1
1
  .listing-wrapper {
2
2
  margin-top: 1.5em;
3
3
  }
4
+
5
+ .filter-title {
6
+ display: block;
7
+ margin: 0.7em 0 0.3em 0;
8
+ font-size: 16px;
9
+ font-weight: 600;
10
+ }
11
+
12
+ #key-type-measure {
13
+ display: none;
14
+ }
15
+
16
+ body.section-adaptation-options #key-type-measure,
17
+ body.section-adaptation-support-tool.section-step-3-1 #key-type-measure {
18
+ display: block !important;
19
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import configureStore from 'redux-mock-store';
4
+ import renderer from 'react-test-renderer';
5
+ import '@testing-library/jest-dom/extend-expect';
6
+ import { Provider } from 'react-intl-redux';
7
+ import DropdownListingView from './DropdownListingView';
8
+ import config from '@plone/volto/registry';
9
+
10
+ config.blocks = {
11
+ blocksConfig: {
12
+ contentLinks: {
13
+ variations: [
14
+ {
15
+ id: 'default',
16
+ title: 'Simple list (default)',
17
+ isDefault: true,
18
+ },
19
+ {
20
+ id: 'navigationList',
21
+ title: 'Navigation list',
22
+ isDefault: false,
23
+ },
24
+ ],
25
+ },
26
+ },
27
+ };
28
+
29
+ const mockStore = configureStore();
30
+
31
+ describe('DropdownListingView', () => {
32
+ it('should render the component', () => {
33
+ const data = {
34
+ '@type': 'listing',
35
+ items: [
36
+ {
37
+ '@id': '/item-1',
38
+ title: 'Item 1',
39
+ id: 'item-1',
40
+ },
41
+ {
42
+ '@id': '/item-2',
43
+ title: 'Item 2',
44
+ id: 'item-2',
45
+ },
46
+ ],
47
+ };
48
+
49
+ const store = mockStore({
50
+ userSession: { token: '1234' },
51
+ intl: {
52
+ locale: 'en',
53
+ messages: {},
54
+ },
55
+ });
56
+
57
+ const component = renderer.create(
58
+ <Provider store={store}>
59
+ <MemoryRouter>
60
+ <DropdownListingView {...data} />
61
+ </MemoryRouter>
62
+ </Provider>,
63
+ );
64
+ const json = component.toJSON();
65
+ expect(json).toMatchSnapshot();
66
+ });
67
+ });