@eeacms/volto-bise-policy 1.3.6 → 1.3.8

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 CHANGED
@@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.3.8](https://github.com/eea/volto-bise-policy/compare/1.3.7...1.3.8) - 26 August 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: add NRR widget views and update HTML structure [laszlocseh - [`3ba2075`](https://github.com/eea/volto-bise-policy/commit/3ba207547d4f56790b5a10a23b6aefabb9bb3b70)]
12
+
13
+ #### :nail_care: Enhancements
14
+
15
+ - refactor(NRRWidgets): refactor shared components and parse helper [laszlocseh - [`8a0bbfa`](https://github.com/eea/volto-bise-policy/commit/8a0bbfaac42647a244b180b393ff3dea61a8a5c5)]
16
+
17
+ #### :house: Internal changes
18
+
19
+ - style: Automated code fix [eea-jenkins - [`0d9e222`](https://github.com/eea/volto-bise-policy/commit/0d9e222a45bad204e1d22ca7f1939ab6c1166549)]
20
+
21
+ #### :hammer_and_wrench: Others
22
+
23
+ - test(EUNISObjectListWidget): mock uuid module [laszlocseh - [`4053526`](https://github.com/eea/volto-bise-policy/commit/405352685a516f18e1c4f04d36f069dfcc584f36)]
24
+ - test(EUNISObjectListWidget): mock volto helpers to fix jest resolution [laszlocseh - [`92910f0`](https://github.com/eea/volto-bise-policy/commit/92910f09ebb1fa7ebed4caf1442db7935cb58684)]
25
+ - test: add unit tests for EUNIS and NRR widgets [laszlocseh - [`862db64`](https://github.com/eea/volto-bise-policy/commit/862db648d7a3546e3f7c7c5d812d35497116adb4)]
26
+ ### [1.3.7](https://github.com/eea/volto-bise-policy/compare/1.3.6...1.3.7) - 20 August 2026
27
+
28
+ #### :rocket: New Features
29
+
30
+ - feat: add Country Profile variation with CountryFlag banner [Claudia Ifrim - [`baff0b6`](https://github.com/eea/volto-bise-policy/commit/baff0b6d00b7c159b3520b89bcaaa3ba400d8d5b)]
31
+
32
+ #### :bug: Bug Fixes
33
+
34
+ - fix: expand siblings on GET_CONTENT [Claudia Ifrim - [`5e10b2d`](https://github.com/eea/volto-bise-policy/commit/5e10b2d05293f39e9f49673377fd72bb19791435)]
35
+ - fix: preserve extra item fields and store data on direct GET_NAVIGATION calls [Claudia Ifrim - [`d3352d8`](https://github.com/eea/volto-bise-policy/commit/d3352d897102c69155b0740c8f44096b89011630)]
36
+
37
+ #### :house: Internal changes
38
+
39
+ - style: Automated code fix [eea-jenkins - [`8a103af`](https://github.com/eea/volto-bise-policy/commit/8a103af9cf9905de5eeb3ea660a2b076cfa14002)]
40
+
7
41
  ### [1.3.6](https://github.com/eea/volto-bise-policy/compare/1.3.5...1.3.6) - 19 August 2026
8
42
 
9
43
  #### :rocket: New Features
@@ -51,6 +85,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
51
85
 
52
86
  #### :house: Internal changes
53
87
 
88
+ - chore: [JENKINSFILE] sanitize sonarqube command [valentinab25 - [`8e2dc1a`](https://github.com/eea/volto-bise-policy/commit/8e2dc1a14f3230f197f009a76bf87587724b6e3a)]
54
89
 
55
90
  #### :hammer_and_wrench: Others
56
91
 
@@ -154,6 +189,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
154
189
 
155
190
  #### :house: Internal changes
156
191
 
192
+ - chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`4c2d75b`](https://github.com/eea/volto-bise-policy/commit/4c2d75bbf6dca47929c6613749169b40ed483985)]
193
+ - chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`f99c622`](https://github.com/eea/volto-bise-policy/commit/f99c622726cfd711704f50218a2f1fe833974ec3)]
157
194
 
158
195
  #### :hammer_and_wrench: Others
159
196
 
@@ -416,6 +453,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
416
453
  - chore: customize root theme to use volto-spotlight - refs #259794 [ana-oprea - [`347f96e`](https://github.com/eea/volto-bise-policy/commit/347f96e335139f9656abd34c308c531b05de85e7)]
417
454
  - chore: change the theme and alias to volto-spotlight and add volto-spotlight to package.json - refs #259794 [ana-oprea - [`52732a1`](https://github.com/eea/volto-bise-policy/commit/52732a17b3247a04ffae0a3f5118e173afa98157)]
418
455
  - chore: change theme to eea-volto-theme, change config.alias - refs #259794 [ana-oprea - [`c2d08ea`](https://github.com/eea/volto-bise-policy/commit/c2d08ea08a7f0e5f6f7d32149e60ea3f3e901923)]
456
+ - chore: [JENKINS] Refactor automated testing [valentinab25 - [`a3c9488`](https://github.com/eea/volto-bise-policy/commit/a3c9488c5d09ba97fe5976675524d436080a2460)]
419
457
  - chore: husky, lint-staged use fixed versions [valentinab25 - [`fddc30f`](https://github.com/eea/volto-bise-policy/commit/fddc30f11a1d8ec290edaca2d924282f5e113bc6)]
420
458
  - chore:volto 16 in tests, update docs, fix stylelint overrides [valentinab25 - [`005ce74`](https://github.com/eea/volto-bise-policy/commit/005ce742dbd014fde27e417059251233365766dd)]
421
459
 
@@ -428,9 +466,17 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
428
466
  - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`c1277ba`](https://github.com/eea/volto-bise-policy/commit/c1277bacf840ee9feccbfd6a2c85f0b25a9a9125)]
429
467
  - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`c944153`](https://github.com/eea/volto-bise-policy/commit/c944153710fd53c5d48863cf363464273e25f52e)]
430
468
  - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`d648d52`](https://github.com/eea/volto-bise-policy/commit/d648d525a2fc5076577fe0f75ff9782bf0d1d4e9)]
469
+ - test: [JENKINS] fix jest config, fail with logs on coverage [valentinab25 - [`2a7affe`](https://github.com/eea/volto-bise-policy/commit/2a7affe33d737704bdba117c4a5c8c6748ef7a47)]
470
+ - test: [JENKINS] fix jest config, fail with logs on coverage [valentinab25 - [`5929642`](https://github.com/eea/volto-bise-policy/commit/59296420105d0e8c1f1547aa0cd17a019e478c2b)]
471
+ - test: [JENKINS] fix jest config, fail with logs on coverage [valentinab25 - [`b1142f1`](https://github.com/eea/volto-bise-policy/commit/b1142f1da3667a9f56720f65bebce897a5dd8005)]
431
472
  - test: cypress should not wait for navigation, actions, types - refs #259794 [ana-oprea - [`188637c`](https://github.com/eea/volto-bise-policy/commit/188637c78a0cae71b5ee48022507d559dfaceb3e)]
473
+ - test: [JENKINS] Use java17 for sonarqube scanner [valentinab25 - [`fa4e473`](https://github.com/eea/volto-bise-policy/commit/fa4e473018386ec3f09d7300d655c8cccdc19a0e)]
474
+ - test: [JENKINS] Run cypress in started frontend container [valentinab25 - [`1d9135b`](https://github.com/eea/volto-bise-policy/commit/1d9135bb878b3c7a1b828085b99b9d27d78ce60e)]
432
475
  - remove cypress tests as it doesn t work on bise [Dobricean Ioan Dorian - [`ba7e2de`](https://github.com/eea/volto-bise-policy/commit/ba7e2defea2dcfd01537c3abb41fa9515ff0a3e3)]
433
476
  - add mega menu layout settings as #259794 said [dobri1408 - [`b9a3683`](https://github.com/eea/volto-bise-policy/commit/b9a3683e7d85f8c88ad8feac975c47c434f62c4a)]
477
+ - test: [JENKINS] Add cpu limit on cypress docker [valentinab25 - [`9181908`](https://github.com/eea/volto-bise-policy/commit/9181908c7f4b51bdf83a34f8c7f25827de9b1cc6)]
478
+ - test: [JENKINS] Increase shm-size to cypress docker [valentinab25 - [`099d796`](https://github.com/eea/volto-bise-policy/commit/099d79669471e6924bf1e0bc51a79d54379c3a1a)]
479
+ - test: [JENKINS] Improve cypress time [valentinab25 - [`6d5d8e2`](https://github.com/eea/volto-bise-policy/commit/6d5d8e29b04ddb92628245570055067ca3dcbb6c)]
434
480
  - test: fix comment [valentinab25 - [`4f27d5a`](https://github.com/eea/volto-bise-policy/commit/4f27d5af2e8895c7af4f1157521ca42682b96e23)]
435
481
  - test: EN locales, pre-commit fix, feature PRs checks Refs #257193 [valentinab25 - [`4c2fcb0`](https://github.com/eea/volto-bise-policy/commit/4c2fcb01065e1ef962317ba7be788fd1df079618)]
436
482
  - test: Update Makefile and docker-compose to align it with Jenkinsfile [valentinab25 - [`89afaa1`](https://github.com/eea/volto-bise-policy/commit/89afaa15892930452027ef8c8593fc6d193cda70)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-bise-policy",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "@eeacms/volto-bise-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -0,0 +1,391 @@
1
+ import React from 'react';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { IntlProvider } from 'react-intl';
5
+
6
+ import {
7
+ EUNISMSFDView,
8
+ EUNISCodeView,
9
+ EUNISHDView,
10
+ EUNISEuropeanRedListView,
11
+ EUNISLinksToFinerEUNISHabitatsView,
12
+ EUNISCountryCodeView,
13
+ EUNISRegionalSeaConventionValueView,
14
+ EUNISEuropeanRedListWidget,
15
+ EUNISMSFDWidget,
16
+ EUNISHDWidget,
17
+ EUNISLinksToFinerEUNISHabitatsWidget,
18
+ EUNISRegionalSeaConventionValueWidget,
19
+ EUNISCountryCodeWidget,
20
+ } from './EUNISObjectListWidget';
21
+
22
+ // Mock the heavy @plone/volto building blocks used by BaseObjectListWidget so
23
+ // that we can render the widget without mounting react-beautiful-dnd, the
24
+ // ObjectWidget editor or the full FormFieldWrapper machinery.
25
+ jest.mock('@plone/volto/components/manage/DragDropList/DragDropList', () => ({
26
+ __esModule: true,
27
+ default: ({ childList, onMoveItem, children }) => (
28
+ <div data-testid="dragdrop-list">
29
+ {childList.map((childEntry, index) =>
30
+ children({
31
+ child: childEntry[1],
32
+ childId: childEntry[0],
33
+ index,
34
+ draginfo: {
35
+ innerRef: jest.fn(),
36
+ draggableProps: {},
37
+ dragHandleProps: {},
38
+ },
39
+ }),
40
+ )}
41
+ </div>
42
+ ),
43
+ }));
44
+
45
+ jest.mock('@plone/volto/components/manage/Widgets/FormFieldWrapper', () => ({
46
+ __esModule: true,
47
+ default: ({ children }) => <div data-testid="form-wrapper">{children}</div>,
48
+ }));
49
+
50
+ jest.mock('@plone/volto/components/manage/Widgets/ObjectWidget', () => ({
51
+ __esModule: true,
52
+ default: () => <div data-testid="object-widget" />,
53
+ }));
54
+
55
+ jest.mock('@plone/volto/components/theme/Icon/Icon', () => ({
56
+ __esModule: true,
57
+ default: () => <span data-testid="icon" />,
58
+ }));
59
+
60
+ // Mock the volto helpers too: the real `Blocks.js` requires `uuid`, which
61
+ // cannot always be resolved from within node_modules in the jest environment
62
+ // (and the repository already mocks @plone/volto/helpers in other suites).
63
+ // The schemas used here have no `default`s, so `applySchemaDefaults` is
64
+ // faithfully reproduced by returning the input data.
65
+ jest.mock('@plone/volto/helpers/Blocks/Blocks', () => ({
66
+ __esModule: true,
67
+ applySchemaDefaults: jest.fn(({ data }) => data),
68
+ }));
69
+
70
+ jest.mock('@plone/volto/helpers/Utils/Utils', () => ({
71
+ __esModule: true,
72
+ reorderArray: jest.fn((array) => array),
73
+ }));
74
+
75
+ // `uuid` is imported (and used) by the widget module directly. In some jest
76
+ // environments it cannot be resolved from the add-on source, so use a virtual
77
+ // mock to bypass module resolution entirely and return unique, deterministic ids
78
+ // (same pattern as volto-freshwater-policy's NWRMObjectListWidget test).
79
+ jest.mock(
80
+ 'uuid',
81
+ () => {
82
+ let value = 0;
83
+ return {
84
+ v4: jest.fn(() => `uuid-${value++}`),
85
+ };
86
+ },
87
+ { virtual: true },
88
+ );
89
+
90
+ // EUNISCodeView reads the current content data from the Redux store. Expose a
91
+ // mutable slice so each test can control what useSelector returns.
92
+ let mockContentData = {};
93
+ jest.mock('react-redux', () => ({
94
+ ...jest.requireActual('react-redux'),
95
+ useSelector: (selector) => selector({ content: { data: mockContentData } }),
96
+ }));
97
+
98
+ const renderView = (Component, props) => render(<Component {...props} />);
99
+
100
+ describe('EUNISMSFDView', () => {
101
+ it('renders nothing when value is empty', () => {
102
+ const { container } = renderView(EUNISMSFDView, { value: '' });
103
+ expect(container.firstChild).toBeNull();
104
+ });
105
+
106
+ it('renders nothing on invalid JSON', () => {
107
+ const { container } = renderView(EUNISMSFDView, { value: 'not json' });
108
+ expect(container.firstChild).toBeNull();
109
+ });
110
+
111
+ it('renders relation and value for each item', () => {
112
+ const value = JSON.stringify([
113
+ { '@id': '/1', relation: '=', value: 'V1' },
114
+ { '@id': '/2', relation: '#', value: 'V2' },
115
+ ]);
116
+
117
+ const { container } = renderView(EUNISMSFDView, { value });
118
+
119
+ const view = container.querySelector('.eunis-widget-view');
120
+ expect(view).toBeInTheDocument();
121
+
122
+ const items = view.querySelectorAll('.eunis-widget-item');
123
+ expect(items).toHaveLength(2);
124
+ expect(items[0].textContent).toContain('=');
125
+ expect(items[0].textContent).toContain('V1');
126
+ expect(items[1].textContent).toContain('#');
127
+ expect(items[1].textContent).toContain('V2');
128
+ });
129
+
130
+ it('supports an object value with a value array', () => {
131
+ const value = { value: [{ '@id': '/1', relation: '=', value: 'V' }] };
132
+ const { container } = renderView(EUNISMSFDView, { value });
133
+ expect(container.querySelector('.eunis-widget-view')).toBeInTheDocument();
134
+ });
135
+ });
136
+
137
+ describe('EUNISHDView', () => {
138
+ it('renders nothing when value is empty', () => {
139
+ const { container } = renderView(EUNISHDView, { value: [] });
140
+ expect(container.firstChild).toBeNull();
141
+ });
142
+
143
+ it('renders the item.link href when present', () => {
144
+ const value = JSON.stringify([
145
+ { '@id': '/1', relation: '=', value: 'V', link: 'http://x/1' },
146
+ ]);
147
+
148
+ const { container } = renderView(EUNISHDView, { value });
149
+
150
+ const a = container.querySelector('.eunis-widget-item a');
151
+ expect(a).toHaveAttribute('href', 'http://x/1');
152
+ expect(a).toHaveTextContent('V');
153
+ expect(
154
+ container.querySelector('.eunis-widget-item span'),
155
+ ).toHaveTextContent('=');
156
+ });
157
+
158
+ it('renders the Annex I URL when no link is present', () => {
159
+ const value = JSON.stringify([{ '@id': '/1', relation: '>', value: 'A1' }]);
160
+
161
+ const { container } = renderView(EUNISHDView, { value });
162
+
163
+ const a = container.querySelector('.eunis-widget-item a');
164
+ expect(a).toHaveAttribute('href', '/habitats/ANNEX1_A1');
165
+ expect(a).toHaveAttribute('target', '_blank');
166
+ expect(a).toHaveAttribute('rel', 'noopener');
167
+ expect(a).toHaveTextContent('A1');
168
+ });
169
+ });
170
+
171
+ describe('EUNISEuropeanRedListView', () => {
172
+ it('renders the item.link href when present', () => {
173
+ const value = JSON.stringify([
174
+ { '@id': '/1', relation: '=', value: 'V', link: 'http://x/2' },
175
+ ]);
176
+
177
+ const { container } = renderView(EUNISEuropeanRedListView, { value });
178
+
179
+ expect(container.querySelector('.eunis-widget-item a')).toHaveAttribute(
180
+ 'href',
181
+ 'http://x/2',
182
+ );
183
+ });
184
+
185
+ it('builds the Red List URL from the code part of the value', () => {
186
+ const value = JSON.stringify([
187
+ { '@id': '/1', relation: '=', value: 'Kelp communities - BAL1' },
188
+ ]);
189
+
190
+ const { container } = renderView(EUNISEuropeanRedListView, { value });
191
+
192
+ const a = container.querySelector('.eunis-widget-item a');
193
+ expect(a).toHaveAttribute('href', '/habitats_rl/REDLIST_BAL1');
194
+ expect(a).toHaveAttribute('target', '_blank');
195
+ expect(a).toHaveTextContent('Kelp communities - BAL1');
196
+ });
197
+ });
198
+
199
+ describe('EUNISLinksToFinerEUNISHabitatsView', () => {
200
+ it('renders the item.link href when present', () => {
201
+ const value = JSON.stringify([
202
+ { '@id': '/1', value: 'E1', link: 'http://x/3' },
203
+ ]);
204
+
205
+ const { container } = renderView(EUNISLinksToFinerEUNISHabitatsView, {
206
+ value,
207
+ });
208
+
209
+ expect(container.querySelector('.eunis-widget-item a')).toHaveAttribute(
210
+ 'href',
211
+ 'http://x/3',
212
+ );
213
+ });
214
+
215
+ it('builds the EUNIS revised URL when no link is present', () => {
216
+ const value = JSON.stringify([{ '@id': '/1', value: 'E1' }]);
217
+
218
+ const { container } = renderView(EUNISLinksToFinerEUNISHabitatsView, {
219
+ value,
220
+ });
221
+
222
+ const a = container.querySelector('.eunis-widget-item a');
223
+ expect(a).toHaveAttribute('href', '/habitats_eunis_revised/EUNISrev_E1');
224
+ expect(a).toHaveAttribute('target', '_blank');
225
+ expect(a).toHaveTextContent('E1');
226
+ });
227
+ });
228
+
229
+ describe('EUNISCountryCodeView', () => {
230
+ it('renders nothing when value is empty', () => {
231
+ const { container } = renderView(EUNISCountryCodeView, { value: '' });
232
+ expect(container.firstChild).toBeNull();
233
+ });
234
+
235
+ it('renders countries and their national items', () => {
236
+ const value = JSON.stringify([
237
+ {
238
+ '@id': '/cc1',
239
+ countryCode: ['BE', 'NL'],
240
+ national: [{ '@id': '/n1', relation: '=', value: 'V1' }],
241
+ },
242
+ ]);
243
+
244
+ const { container } = renderView(EUNISCountryCodeView, { value });
245
+
246
+ expect(container.querySelector('.country-item')).toHaveTextContent(
247
+ 'BE, NL',
248
+ );
249
+
250
+ const items = container.querySelectorAll('.eunis-widget-item');
251
+ expect(items).toHaveLength(1);
252
+ expect(items[0].textContent).toContain('=');
253
+ expect(items[0].textContent).toContain('V1');
254
+ });
255
+ });
256
+
257
+ describe('EUNISRegionalSeaConventionValueView', () => {
258
+ it('behaves like the MSFD view', () => {
259
+ const value = JSON.stringify([{ '@id': '/1', relation: '=', value: 'V' }]);
260
+
261
+ const { container } = renderView(EUNISRegionalSeaConventionValueView, {
262
+ value,
263
+ });
264
+
265
+ expect(container.querySelector('.eunis-widget-view')).toBeInTheDocument();
266
+ });
267
+ });
268
+
269
+ describe('EUNISCodeView', () => {
270
+ beforeEach(() => {
271
+ mockContentData = {};
272
+ });
273
+
274
+ it('builds the default EUNIS revised href', () => {
275
+ const { container } = renderView(EUNISCodeView, { value: 'E123' });
276
+
277
+ const a = container.querySelector('a');
278
+ expect(a).toHaveAttribute('href', '/habitats_eunis_revised/EUNISrev_E123');
279
+ expect(a).toHaveAttribute('target', '_blank');
280
+ expect(a).toHaveAttribute('rel', 'noopener');
281
+ expect(a).toHaveTextContent('E123');
282
+ });
283
+
284
+ it('uses the trimmed eunis_code_alt_url from the store when present', () => {
285
+ mockContentData = { eunis_code_alt_url: ' http://alt/1 ' };
286
+ const { container } = renderView(EUNISCodeView, { value: 'E123' });
287
+
288
+ expect(container.querySelector('a')).toHaveAttribute(
289
+ 'href',
290
+ 'http://alt/1',
291
+ );
292
+ });
293
+
294
+ it('prefers the content prop over the store', () => {
295
+ mockContentData = { eunis_code_alt_url: 'http://store/1' };
296
+ const { container } = renderView(EUNISCodeView, {
297
+ value: 'E123',
298
+ content: { eunis_code_alt_url: 'http://content/2' },
299
+ });
300
+
301
+ expect(container.querySelector('a')).toHaveAttribute(
302
+ 'href',
303
+ 'http://content/2',
304
+ );
305
+ });
306
+
307
+ it('returns the value unchanged when no value is provided', () => {
308
+ const { container } = renderView(EUNISCodeView, { value: null });
309
+ expect(container.firstChild).toBeNull();
310
+ });
311
+ });
312
+
313
+ const renderWidget = (Widget, props) =>
314
+ render(
315
+ <IntlProvider locale="en" messages={{}}>
316
+ <Widget
317
+ id="w"
318
+ value={{ value: [] }}
319
+ fieldSet="default"
320
+ block={{}}
321
+ {...props}
322
+ />
323
+ </IntlProvider>,
324
+ );
325
+
326
+ describe('BaseObjectListWidget (via the named widget wrappers)', () => {
327
+ it.each([
328
+ ['MSFD', EUNISMSFDWidget, 'Add MSFD'],
329
+ ['HD (Annex I)', EUNISHDWidget, 'Add HD (Annex I)'],
330
+ ['European Red List', EUNISEuropeanRedListWidget, 'Add European Red List'],
331
+ [
332
+ 'Links to finer EUNIS habitats',
333
+ EUNISLinksToFinerEUNISHabitatsWidget,
334
+ 'Add Links to finer EUNIS habitats',
335
+ ],
336
+ [
337
+ 'Regional Sea Convention Value',
338
+ EUNISRegionalSeaConventionValueWidget,
339
+ 'Add Regional Sea Convention Value',
340
+ ],
341
+ ['Country Code', EUNISCountryCodeWidget, 'Add Country Code'],
342
+ ])('renders the add button for %s', (title, Widget, label) => {
343
+ const { getByRole } = renderWidget(Widget, { onChange: jest.fn() });
344
+ expect(getByRole('button', { name: label })).toBeInTheDocument();
345
+ });
346
+
347
+ it('renders the empty-state input when there are no items', () => {
348
+ const { container } = renderWidget(EUNISMSFDWidget, {
349
+ onChange: jest.fn(),
350
+ });
351
+
352
+ const hidden = container.querySelector('input[type="hidden"]');
353
+ expect(hidden).toHaveValue('Empty object list');
354
+ });
355
+
356
+ it('adds a new item when the add button is clicked', () => {
357
+ const onChange = jest.fn();
358
+ const { getByRole } = renderWidget(EUNISMSFDWidget, { onChange });
359
+
360
+ fireEvent.click(getByRole('button', { name: 'Add MSFD' }));
361
+
362
+ expect(onChange).toHaveBeenCalledWith('w', {
363
+ value: expect.arrayContaining([
364
+ expect.objectContaining({ '@id': expect.any(String) }),
365
+ ]),
366
+ });
367
+ });
368
+
369
+ it('removes an item when the remove button is clicked', () => {
370
+ const onChange = jest.fn();
371
+ const value = {
372
+ value: [{ '@id': '/1', relation: '=', value: 'V' }],
373
+ };
374
+
375
+ const { getByRole } = render(
376
+ <IntlProvider locale="en" messages={{}}>
377
+ <EUNISMSFDWidget
378
+ id="w"
379
+ value={value}
380
+ fieldSet="default"
381
+ block={{}}
382
+ onChange={onChange}
383
+ />
384
+ </IntlProvider>,
385
+ );
386
+
387
+ fireEvent.click(getByRole('button', { name: 'Remove item #1' }));
388
+
389
+ expect(onChange).toHaveBeenCalledWith('w', { value: [] });
390
+ });
391
+ });
@@ -1,165 +1,185 @@
1
1
  import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
2
2
  import { Popup } from 'semantic-ui-react';
3
- export const NRRTypologyOfMeasuresView = ({ value }) => {
3
+
4
+ /**
5
+ * Normalize a relation/array field value into a plain array.
6
+ * Handles a JSON-encoded string, a raw array, or an object with a `value` array.
7
+ * Returns [] when the value can't be parsed or is empty.
8
+ */
9
+ const parseItems = (value) => {
4
10
  let parsedValue = value;
5
11
 
6
12
  if (typeof value === 'string') {
7
13
  try {
8
14
  parsedValue = JSON.parse(value);
9
15
  } catch (e) {
10
- return null;
16
+ return [];
11
17
  }
12
18
  }
13
19
 
14
- const items = Array.isArray(parsedValue)
15
- ? parsedValue
16
- : parsedValue?.value || [];
20
+ return Array.isArray(parsedValue) ? parsedValue : parsedValue?.value || [];
21
+ };
17
22
 
18
- if (!items || items.length === 0) return null;
23
+ const Label = ({ label, secondary = false }) =>
24
+ label ? (
25
+ <span className={secondary ? 'secondary' : undefined}>
26
+ <b>{label}: </b>
27
+ </span>
28
+ ) : null;
29
+
30
+ /** Renders a labeled list of items separated by commas. */
31
+ const InlineList = ({ label, secondary, items, render }) => (
32
+ <p>
33
+ <Label label={label} secondary={secondary} />
34
+ {items.map((item, index) => (
35
+ <span key={item['@id'] ?? index}>
36
+ {render(item)}
37
+ {index < items.length - 1 ? ', ' : ''}
38
+ </span>
39
+ ))}
40
+ </p>
41
+ );
42
+
43
+ /** Renders a labeled <ul> of items. */
44
+ const LinkedList = ({ label, secondary, items, render }) => (
45
+ <div className="nrr-linked-list">
46
+ <Label label={label} secondary={secondary} />
47
+ <ul>
48
+ {items.map((item, index) => (
49
+ <li key={item['@id'] ?? index}>{render(item)}</li>
50
+ ))}
51
+ </ul>
52
+ </div>
53
+ );
54
+
55
+ export const NRRTypologyOfMeasuresView = ({ value }) => {
56
+ const items = parseItems(value);
57
+ if (items.length === 0) return null;
19
58
 
20
59
  return (
21
- <div className="eunis-widget-view">
22
- <span>
23
- <b>Related typology of measures: </b>
24
- </span>
25
- <ul>
26
- {items.map((item) => {
27
- const description = item.title.split(' - ')?.pop();
28
- const code = item.title.split(' - ').slice(0, 2).join(' - ');
29
- return (
30
- <li key={item['@id']}>
31
- <Popup content={description} trigger={<b>{code}</b>} />
32
- </li>
33
- );
34
- })}
35
- </ul>
36
- </div>
60
+ <LinkedList
61
+ label="Related typology of measures"
62
+ secondary
63
+ items={items}
64
+ render={(item) => {
65
+ const parts = item.title.split(' - ');
66
+ const code = parts.slice(0, 2).join(' - ');
67
+ const description = parts[parts.length - 1];
68
+ return <Popup content={description} trigger={<b>{code}</b>} />;
69
+ }}
70
+ />
37
71
  );
38
72
  };
39
73
 
40
74
  export const NRRArticleView = ({ value }) => {
41
- let parsedValue = value;
42
-
43
- if (typeof value === 'string') {
44
- try {
45
- parsedValue = JSON.parse(value);
46
- } catch (e) {
47
- return null;
48
- }
49
- }
50
-
51
- const items = Array.isArray(parsedValue)
52
- ? parsedValue
53
- : parsedValue?.value || [];
54
-
55
- if (!items || items.length === 0) return null;
75
+ const items = parseItems(value);
76
+ if (items.length === 0) return null;
56
77
 
57
78
  return (
58
- <div className="eunis-widget-viewZ">
59
- <span>
60
- <b>NRR Article: </b>
61
- </span>
62
- {items.map((item) => (
63
- <span key={item['@id']}>
64
- {item.title.split(' - ')[0].split(' ')[1]}
65
- {items.indexOf(item) < items.length - 1 ? ', ' : ''}
66
- </span>
67
- ))}
68
- </div>
79
+ <InlineList
80
+ label="NRR Article"
81
+ secondary
82
+ items={items}
83
+ render={(item) => item.title.split(' - ')[0].split(' ')[1]}
84
+ />
69
85
  );
70
86
  };
71
87
 
72
88
  export const NRRrelatedCaseStudiesView = ({ value }) => {
73
- let parsedValue = value;
89
+ const items = parseItems(value);
90
+ if (items.length === 0) return null;
74
91
 
75
- if (typeof value === 'string') {
76
- try {
77
- parsedValue = JSON.parse(value);
78
- } catch (e) {
79
- return null;
80
- }
81
- }
82
-
83
- const items = Array.isArray(parsedValue)
84
- ? parsedValue
85
- : parsedValue?.value || [];
92
+ return (
93
+ <LinkedList
94
+ label="Related Case Studies"
95
+ items={items}
96
+ render={(item) => (
97
+ <UniversalLink href={item['@id']}>{item.title}</UniversalLink>
98
+ )}
99
+ />
100
+ );
101
+ };
86
102
 
87
- if (!items || items.length === 0) return null;
103
+ export const NRRMeasuresImplementedView = ({ value }) => {
104
+ const items = parseItems(value);
105
+ if (items.length === 0) return null;
88
106
 
89
107
  return (
90
- <div className="eunis-widget-viewZ">
91
- <span>
92
- <b>Related Case Studies: </b>
93
- </span>
94
- <ul>
95
- {items.map((item, index) => (
96
- <li key={item['@id'] ?? index}>
97
- <UniversalLink href={item['@id']}>{item.title}</UniversalLink>
98
- </li>
99
- ))}
100
- </ul>
101
- </div>
108
+ <LinkedList
109
+ items={items}
110
+ render={(item) => (
111
+ <UniversalLink href={item['@id']}>{item.title}</UniversalLink>
112
+ )}
113
+ />
102
114
  );
103
115
  };
104
116
 
105
- export const NRRMeasuresImplementedView = ({ value }) => {
106
- let parsedValue = value;
117
+ export const NRREcosystemTypologyView = ({ value }) => {
118
+ const items = parseItems(value);
119
+ if (items.length === 0) return null;
107
120
 
108
- if (typeof value === 'string') {
109
- try {
110
- parsedValue = JSON.parse(value);
111
- } catch (e) {
112
- return null;
113
- }
114
- }
121
+ return (
122
+ <InlineList
123
+ label="Ecosystem type"
124
+ items={items}
125
+ render={(item) => item.title}
126
+ />
127
+ );
128
+ };
115
129
 
116
- const items = Array.isArray(parsedValue)
117
- ? parsedValue
118
- : parsedValue?.value || [];
130
+ export const NRRHabitatEcosystemTypeView = ({ value }) => {
131
+ const items = parseItems(value);
132
+ if (items.length === 0) return null;
119
133
 
120
- if (!items || items.length === 0) return null;
134
+ return (
135
+ <InlineList
136
+ label="Habitat/Ecosystem type"
137
+ secondary
138
+ items={items}
139
+ render={(item) => item.title}
140
+ />
141
+ );
142
+ };
143
+
144
+ export const NRRScaleOfPlanningView = ({ value }) => {
145
+ const items = parseItems(value);
146
+ if (items.length === 0) return null;
121
147
 
122
148
  return (
123
- <div className="eunis-widget-viewZ">
124
- <ul>
125
- {items.map((item, index) => (
126
- <li key={item['@id'] ?? index}>
127
- <UniversalLink href={item['@id']}>{item.title}</UniversalLink>
128
- </li>
129
- ))}
130
- </ul>
131
- </div>
149
+ <InlineList
150
+ label="Scale of planning"
151
+ secondary
152
+ items={items}
153
+ render={(item) => item.title}
154
+ />
132
155
  );
133
156
  };
134
157
 
135
- export const NRREcosystemTypologyView = ({ value }) => {
158
+ export const NRRCurrentStatusView = ({ value }) => {
136
159
  let parsedValue = value;
137
160
 
138
161
  if (typeof value === 'string') {
139
162
  try {
140
163
  parsedValue = JSON.parse(value);
141
164
  } catch (e) {
142
- return null;
165
+ // Not JSON — keep the raw string value.
143
166
  }
144
167
  }
145
168
 
146
- const items = Array.isArray(parsedValue)
147
- ? parsedValue
148
- : parsedValue?.value || [];
169
+ // Choice field: value is a single token, not an array.
170
+ if (!parsedValue) return null;
149
171
 
150
- if (!items || items.length === 0) return null;
172
+ const text =
173
+ typeof parsedValue === 'string'
174
+ ? parsedValue
175
+ : parsedValue?.title || parsedValue?.token || parsedValue?.value;
176
+
177
+ if (!text) return null;
151
178
 
152
179
  return (
153
- <div className="eunis-widget-view">
154
- <span>
155
- <b>Ecosystem type: </b>
156
- </span>
157
- {items.map((item) => (
158
- <span key={item['@id']}>
159
- {item.title}
160
- {items.indexOf(item) < items.length - 1 ? ', ' : ''}
161
- </span>
162
- ))}
163
- </div>
180
+ <p>
181
+ <Label label="Current status" secondary />
182
+ {text}
183
+ </p>
164
184
  );
165
185
  };
@@ -0,0 +1,255 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+
5
+ import {
6
+ NRRTypologyOfMeasuresView,
7
+ NRRArticleView,
8
+ NRRrelatedCaseStudiesView,
9
+ NRRMeasuresImplementedView,
10
+ NRREcosystemTypologyView,
11
+ NRRHabitatEcosystemTypeView,
12
+ NRRScaleOfPlanningView,
13
+ NRRCurrentStatusView,
14
+ } from './NRRWidgets';
15
+
16
+ // Mock the heavy @plone/volto UniversalLink for the linked-list views so that
17
+ // we only assert on the rendered markup without having to mount a Router.
18
+ jest.mock('@plone/volto/components/manage/UniversalLink/UniversalLink', () => ({
19
+ __esModule: true,
20
+ default: ({ href, children }) => <a href={href}>{children}</a>,
21
+ }));
22
+
23
+ // Render the semantic-ui-react Popup inline (instead of via a portal) so the
24
+ // trigger and content are directly assertable. The mock factory uses
25
+ // jest.requireActual to avoid the TDZ hoisting issue of referencing a const.
26
+ jest.mock('semantic-ui-react', () => {
27
+ const actual = jest.requireActual('semantic-ui-react');
28
+ return {
29
+ ...actual,
30
+ Popup: ({ content, trigger }) => (
31
+ <div className="popup">
32
+ <div className="trigger">{trigger}</div>
33
+ <div className="content">{content}</div>
34
+ </div>
35
+ ),
36
+ };
37
+ });
38
+
39
+ describe('NRRTypologyOfMeasuresView', () => {
40
+ it('renders nothing when value is empty', () => {
41
+ const { container } = render(<NRRTypologyOfMeasuresView value="" />);
42
+ expect(container.firstChild).toBeNull();
43
+ });
44
+
45
+ it('renders nothing when value is invalid JSON', () => {
46
+ const { container } = render(
47
+ <NRRTypologyOfMeasuresView value="not json" />,
48
+ );
49
+ expect(container.firstChild).toBeNull();
50
+ });
51
+
52
+ it('renders a labeled linked list and splits title into code/description', () => {
53
+ const value = JSON.stringify([
54
+ { '@id': '/1', title: '001 - Sector A - Description one' },
55
+ { '@id': '/2', title: '002 - Sector B - Description two' },
56
+ ]);
57
+
58
+ const { container } = render(<NRRTypologyOfMeasuresView value={value} />);
59
+
60
+ const list = container.querySelector('.nrr-linked-list');
61
+ expect(list).toBeInTheDocument();
62
+ expect(list.textContent).toContain('Related typology of measures: ');
63
+
64
+ const popups = list.querySelectorAll('.popup');
65
+ expect(popups).toHaveLength(2);
66
+
67
+ // code = first two parts joined, description = last part
68
+ expect(popups[0].querySelector('.trigger')).toHaveTextContent(
69
+ '001 - Sector A',
70
+ );
71
+ expect(popups[0].querySelector('.content')).toHaveTextContent(
72
+ 'Description one',
73
+ );
74
+ expect(popups[1].querySelector('.trigger')).toHaveTextContent(
75
+ '002 - Sector B',
76
+ );
77
+ expect(popups[1].querySelector('.content')).toHaveTextContent(
78
+ 'Description two',
79
+ );
80
+ });
81
+ });
82
+
83
+ describe('NRRArticleView', () => {
84
+ it('renders nothing when value is empty', () => {
85
+ const { container } = render(<NRRArticleView value={[]} />);
86
+ expect(container.firstChild).toBeNull();
87
+ });
88
+
89
+ it('extracts the article number from the title and renders an inline list', () => {
90
+ const value = JSON.stringify([
91
+ { '@id': '/1', title: 'Article 5 - xxx' },
92
+ { '@id': '/2', title: 'Article 7 - yyy' },
93
+ ]);
94
+
95
+ const { container } = render(<NRRArticleView value={value} />);
96
+
97
+ const p = container.querySelector('p');
98
+ expect(p).toBeInTheDocument();
99
+ expect(p.textContent).toContain('NRR Article: ');
100
+ // render = title.split(' - ')[0].split(' ')[1]
101
+ expect(p.textContent).toContain('5');
102
+ expect(p.textContent).toContain('7');
103
+ // one label span plus one span per item
104
+ expect(p.querySelectorAll('span').length).toBe(3);
105
+ });
106
+ });
107
+
108
+ describe('NRRrelatedCaseStudiesView', () => {
109
+ it('renders nothing when value is empty', () => {
110
+ const { container } = render(<NRRrelatedCaseStudiesView value={null} />);
111
+ expect(container.firstChild).toBeNull();
112
+ });
113
+
114
+ it('renders links to each related case study', () => {
115
+ const value = JSON.stringify([
116
+ { '@id': '/c1', title: 'Case One' },
117
+ { '@id': '/c2', title: 'Case Two' },
118
+ ]);
119
+
120
+ const { container } = render(<NRRrelatedCaseStudiesView value={value} />);
121
+
122
+ const list = container.querySelector('.nrr-linked-list');
123
+ expect(list.textContent).toContain('Related Case Studies: ');
124
+
125
+ const links = list.querySelectorAll('a');
126
+ expect(links).toHaveLength(2);
127
+ expect(links[0]).toHaveAttribute('href', '/c1');
128
+ expect(links[0]).toHaveTextContent('Case One');
129
+ expect(links[1]).toHaveAttribute('href', '/c2');
130
+ expect(links[1]).toHaveTextContent('Case Two');
131
+ });
132
+ });
133
+
134
+ describe('NRRMeasuresImplementedView', () => {
135
+ it('renders nothing when value is empty', () => {
136
+ const { container } = render(<NRRMeasuresImplementedView value="" />);
137
+ expect(container.firstChild).toBeNull();
138
+ });
139
+
140
+ it('renders measures as links without a leading label', () => {
141
+ const value = [
142
+ { '@id': '/m1', title: 'Measure One' },
143
+ { '@id': '/m2', title: 'Measure Two' },
144
+ ];
145
+
146
+ const { container } = render(<NRRMeasuresImplementedView value={value} />);
147
+
148
+ const list = container.querySelector('.nrr-linked-list');
149
+ expect(list).toBeInTheDocument();
150
+
151
+ // no label rendered for this view
152
+ expect(list.querySelector('b')).toBeNull();
153
+
154
+ const links = list.querySelectorAll('a');
155
+ expect(links).toHaveLength(2);
156
+ expect(links[0]).toHaveAttribute('href', '/m1');
157
+ expect(links[0]).toHaveTextContent('Measure One');
158
+ expect(links[1]).toHaveAttribute('href', '/m2');
159
+ expect(links[1]).toHaveTextContent('Measure Two');
160
+ });
161
+ });
162
+
163
+ describe('NRREcosystemTypologyView', () => {
164
+ it('renders nothing when value is empty', () => {
165
+ const { container } = render(<NRREcosystemTypologyView value={[]} />);
166
+ expect(container.firstChild).toBeNull();
167
+ });
168
+
169
+ it('renders the ecosystem types as an inline list of titles', () => {
170
+ const value = JSON.stringify([
171
+ { '@id': '/1', title: 'Grassland' },
172
+ { '@id': '/2', title: 'Wetland' },
173
+ ]);
174
+
175
+ const { container } = render(<NRREcosystemTypologyView value={value} />);
176
+
177
+ const p = container.querySelector('p');
178
+ expect(p.textContent).toContain('Ecosystem type: ');
179
+ expect(p.textContent).toContain('Grassland');
180
+ expect(p.textContent).toContain('Wetland');
181
+ });
182
+ });
183
+
184
+ describe('NRRHabitatEcosystemTypeView', () => {
185
+ it('renders a labeled inline list of habitat/ecosystem types', () => {
186
+ const value = [
187
+ { '@id': '/1', title: 'Forest' },
188
+ { '@id': '/2', title: 'Heathland' },
189
+ ];
190
+
191
+ const { container } = render(<NRRHabitatEcosystemTypeView value={value} />);
192
+
193
+ const p = container.querySelector('p');
194
+ expect(p.textContent).toContain('Habitat/Ecosystem type: ');
195
+ expect(p.textContent).toContain('Forest');
196
+ expect(p.textContent).toContain('Heathland');
197
+ // the label span of a "secondary" view carries the secondary class
198
+ expect(container.querySelector('p span.secondary')).toBeInTheDocument();
199
+ });
200
+ });
201
+
202
+ describe('NRRScaleOfPlanningView', () => {
203
+ it('renders a labeled inline list of planning scales', () => {
204
+ const value = JSON.stringify([
205
+ { '@id': '/1', title: 'National' },
206
+ { '@id': '/2', title: 'Regional' },
207
+ ]);
208
+
209
+ const { container } = render(<NRRScaleOfPlanningView value={value} />);
210
+
211
+ const p = container.querySelector('p');
212
+ expect(p.textContent).toContain('Scale of planning: ');
213
+ expect(p.textContent).toContain('National');
214
+ expect(p.textContent).toContain('Regional');
215
+ expect(container.querySelector('p span.secondary')).toBeInTheDocument();
216
+ });
217
+ });
218
+
219
+ describe('NRRCurrentStatusView', () => {
220
+ it('renders nothing when value is empty', () => {
221
+ const { container } = render(<NRRCurrentStatusView value="" />);
222
+ expect(container.firstChild).toBeNull();
223
+ });
224
+
225
+ it('renders nothing for undefined or null', () => {
226
+ const { container } = render(<NRRCurrentStatusView value={undefined} />);
227
+ expect(container.firstChild).toBeNull();
228
+ });
229
+
230
+ it('renders the raw string value when it is not JSON', () => {
231
+ const { container } = render(<NRRCurrentStatusView value="Ongoing" />);
232
+ expect(container.textContent).toContain('Current status: ');
233
+ expect(container.textContent).toContain('Ongoing');
234
+ });
235
+
236
+ it('renders the title of an object value', () => {
237
+ const value = JSON.stringify({ title: 'Active' });
238
+ const { container } = render(<NRRCurrentStatusView value={value} />);
239
+ expect(container.textContent).toContain('Active');
240
+ });
241
+
242
+ it('falls back to token then value for object values', () => {
243
+ const { container } = render(
244
+ <NRRCurrentStatusView value={{ token: 't1', value: 'raw' }} />,
245
+ );
246
+ expect(container.textContent).toContain('t1');
247
+ });
248
+
249
+ it('renders nothing for an array value', () => {
250
+ const { container } = render(
251
+ <NRRCurrentStatusView value={JSON.stringify(['a', 'b'])} />,
252
+ );
253
+ expect(container.firstChild).toBeNull();
254
+ });
255
+ });
@@ -17,3 +17,7 @@
17
17
  margin-bottom: 0;
18
18
  }
19
19
  }
20
+
21
+ .nrr-linked-list {
22
+ margin-bottom: 1em;
23
+ }
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import config from '@plone/volto/registry';
3
+
4
+ const CountryProfileView = (props) => {
5
+ const defaultVariation = (
6
+ config.blocks.blocksConfig.title.variations || []
7
+ ).find((v) => v.isDefault);
8
+ const DefaultTemplate = defaultVariation?.view;
9
+ const CountryFlagView = config.blocks.blocksConfig?.countryFlag?.view;
10
+
11
+ if (!DefaultTemplate) return null;
12
+
13
+ const banner = CountryFlagView
14
+ ? {
15
+ ...(props.banner || {}),
16
+ title: {
17
+ view: (
18
+ <CountryFlagView {...props} path={props.path || props.pathname} />
19
+ ),
20
+ },
21
+ }
22
+ : props.banner;
23
+
24
+ return <DefaultTemplate {...props} banner={banner} />;
25
+ };
26
+
27
+ export default CountryProfileView;
@@ -0,0 +1,19 @@
1
+ import CountryProfileView from './CountryProfileView';
2
+ import { countryProfileSchemaEnhancer } from './schema';
3
+
4
+ export default function installTitleVariations(config) {
5
+ const titleBlock = config.blocks.blocksConfig.title;
6
+ if (!titleBlock) return config;
7
+
8
+ titleBlock.variations = [
9
+ ...(titleBlock.variations || []),
10
+ {
11
+ id: 'countryProfile',
12
+ title: 'Country Profile',
13
+ view: CountryProfileView,
14
+ schemaEnhancer: countryProfileSchemaEnhancer,
15
+ },
16
+ ];
17
+
18
+ return config;
19
+ }
@@ -0,0 +1,42 @@
1
+ import CountryFlagSchema from '@eeacms/volto-datablocks/components/manage/Blocks/CountryFlag/schema';
2
+ import countryNames from '@eeacms/volto-datablocks/components/manage/Blocks/CountryFlag/data/countries';
3
+
4
+ export const countryProfileSchemaEnhancer = ({ schema, intl }) => {
5
+ const cfSchema = CountryFlagSchema(intl);
6
+
7
+ cfSchema.properties.country_name.choices = Object.keys(countryNames).map(
8
+ (k) => [k, countryNames[k]],
9
+ );
10
+
11
+ schema.fieldsets.push(
12
+ {
13
+ id: 'countryFlag',
14
+ title: 'Country flag',
15
+ fields: [
16
+ 'country_name',
17
+ 'show_name',
18
+ 'show_flag',
19
+ 'show_dropdown',
20
+ 'render_as',
21
+ ],
22
+ },
23
+ {
24
+ id: 'dropdownItems',
25
+ title: 'Dropdown items',
26
+ fields: ['querystring', 'exclude'],
27
+ },
28
+ );
29
+
30
+ schema.properties = {
31
+ ...schema.properties,
32
+ country_name: cfSchema.properties.country_name,
33
+ show_name: cfSchema.properties.show_name,
34
+ show_flag: cfSchema.properties.show_flag,
35
+ show_dropdown: cfSchema.properties.show_dropdown,
36
+ render_as: cfSchema.properties.render_as,
37
+ querystring: cfSchema.properties.querystring,
38
+ exclude: cfSchema.properties.exclude,
39
+ };
40
+
41
+ return schema;
42
+ };
@@ -7,6 +7,7 @@ import installKeyFacts from './KeyFacts';
7
7
  import installMaesViewer from './MaesViewer';
8
8
  import installNavigation from './Navigation';
9
9
  import installCaseStudyExplorer from './CaseStudyExplorer';
10
+ import installTitleVariations from './Title';
10
11
 
11
12
  const config = (config) => {
12
13
  return [
@@ -19,6 +20,7 @@ const config = (config) => {
19
20
  installMaesViewer,
20
21
  installNavigation,
21
22
  installCaseStudyExplorer,
23
+ installTitleVariations,
22
24
  ].reduce((acc, apply) => apply(acc), config);
23
25
  };
24
26
 
@@ -28,9 +28,8 @@ const initialState = {
28
28
  */
29
29
  function getRecursiveItems(items) {
30
30
  return map(items, (item) => ({
31
- title: item.title,
32
- description: item.description,
33
31
  url: flattenToAppURL(item['@id']),
32
+ ...item,
34
33
  ...(item.items && { items: getRecursiveItems(item.items) }),
35
34
  }));
36
35
  }
@@ -71,11 +70,11 @@ export default function navigation(state = initialState, action = {}) {
71
70
  }
72
71
  return state;
73
72
  case `${GET_NAVIGATION}_SUCCESS`:
74
- hasExpander = hasApiExpander(
75
- 'navigation',
76
- getBaseUrl(flattenToAppURL(action.result['@id'])),
77
- );
78
- if (!hasExpander) {
73
+ // Even if the expander is set or not, if the GET_NAVIGATION is
74
+ // called, we want it to store the data if the actions data is
75
+ // not set in the expander data (['@components']) but in the "normal"
76
+ // action result (we look for the object property returned by the endpoint)
77
+ if (!action.result?.['@components'] && action.result?.items) {
79
78
  return {
80
79
  ...state,
81
80
  error: null,
package/src/index.js CHANGED
@@ -32,6 +32,9 @@ import {
32
32
  NRRArticleView,
33
33
  NRRrelatedCaseStudiesView,
34
34
  NRRMeasuresImplementedView,
35
+ NRRHabitatEcosystemTypeView,
36
+ NRRScaleOfPlanningView,
37
+ NRRCurrentStatusView,
35
38
  } from './components/Widgets/NRRWidgets';
36
39
  import RichTextWidget from './components/Widgets/RichTextWidget';
37
40
 
@@ -110,6 +113,17 @@ const applyConfig = (config) => {
110
113
  'nominatim.openstreetmap.org',
111
114
  ];
112
115
 
116
+ // Needed by the CountryFlag block to fall back to sibling items when it
117
+ // has no listing/query results (e.g. Title block's Country Profile
118
+ // variation).
119
+ config.settings.apiExpanders = [
120
+ ...(config.settings.apiExpanders || []),
121
+ {
122
+ match: '',
123
+ GET_CONTENT: ['siblings'],
124
+ },
125
+ ];
126
+
113
127
  // EEA customizations
114
128
  config.settings.eea = {
115
129
  ...(config.settings.eea || {}),
@@ -272,7 +286,11 @@ const applyConfig = (config) => {
272
286
  config.widgets.id.geolocation = GeolocationWidget;
273
287
  config.widgets.views.id.nrr_typology_of_measures = NRRTypologyOfMeasuresView;
274
288
  config.widgets.views.id.nrr_ecosystem_typology = NRREcosystemTypologyView;
289
+ config.widgets.views.id.nrr_habitat_ecosystem_type =
290
+ NRRHabitatEcosystemTypeView;
291
+ config.widgets.views.id.nrr_scale_of_planning = NRRScaleOfPlanningView;
275
292
  config.widgets.views.id.nrr_article = NRRArticleView;
293
+ config.widgets.views.id.nrr_current_status = NRRCurrentStatusView;
276
294
  // EUNIS Widgets
277
295
  config.widgets.id.eunis_national_json = EUNISCountryCodeWidget;
278
296
  config.widgets.views.id.eunis_national_json = EUNISCountryCodeView;