@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 +46 -0
- package/package.json +1 -1
- package/src/components/Widgets/EUNISObjectListWidget.test.jsx +391 -0
- package/src/components/Widgets/NRRWidgets.jsx +132 -112
- package/src/components/Widgets/NRRWidgets.test.jsx +255 -0
- package/src/components/Widgets/styles.less +4 -0
- package/src/components/manage/Blocks/Title/CountryProfileView.jsx +27 -0
- package/src/components/manage/Blocks/Title/index.js +19 -0
- package/src/components/manage/Blocks/Title/schema.js +42 -0
- package/src/components/manage/Blocks/index.js +2 -0
- package/src/customizations/volto/reducers/navigation/navigation.js +6 -7
- package/src/index.js +18 -0
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
|
@@ -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
|
-
|
|
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
|
|
16
|
+
return [];
|
|
11
17
|
}
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
: parsedValue?.value || [];
|
|
20
|
+
return Array.isArray(parsedValue) ? parsedValue : parsedValue?.value || [];
|
|
21
|
+
};
|
|
17
22
|
|
|
18
|
-
|
|
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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
|
|
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
|
-
|
|
89
|
+
const items = parseItems(value);
|
|
90
|
+
if (items.length === 0) return null;
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
103
|
+
export const NRRMeasuresImplementedView = ({ value }) => {
|
|
104
|
+
const items = parseItems(value);
|
|
105
|
+
if (items.length === 0) return null;
|
|
88
106
|
|
|
89
107
|
return (
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
106
|
-
|
|
117
|
+
export const NRREcosystemTypologyView = ({ value }) => {
|
|
118
|
+
const items = parseItems(value);
|
|
119
|
+
if (items.length === 0) return null;
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
return (
|
|
122
|
+
<InlineList
|
|
123
|
+
label="Ecosystem type"
|
|
124
|
+
items={items}
|
|
125
|
+
render={(item) => item.title}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
115
129
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
export const NRRHabitatEcosystemTypeView = ({ value }) => {
|
|
131
|
+
const items = parseItems(value);
|
|
132
|
+
if (items.length === 0) return null;
|
|
119
133
|
|
|
120
|
-
|
|
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
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
165
|
+
// Not JSON — keep the raw string value.
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: parsedValue?.value || [];
|
|
169
|
+
// Choice field: value is a single token, not an array.
|
|
170
|
+
if (!parsedValue) return null;
|
|
149
171
|
|
|
150
|
-
|
|
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
|
-
<
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
});
|
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
if (!
|
|
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;
|