@eeacms/volto-marine-policy 3.0.3 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,34 @@ 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
|
-
### [3.0.
|
|
7
|
+
### [3.0.4](https://github.com/eea/volto-marine-policy/compare/3.0.3...3.0.4) - 30 June 2026
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- feat(NISListingView): restrict edit/remove buttons to assigned items [laszlocseh - [`4844b47`](https://github.com/eea/volto-marine-policy/commit/4844b47335908c105551c7a2ca652af86e43a491)]
|
|
12
|
+
- feat(NISListingView): add remove functionality and error handling [laszlocseh - [`1121c97`](https://github.com/eea/volto-marine-policy/commit/1121c97f4a6de99994325624edf705a810933958)]
|
|
13
|
+
- feat: added button to NISListingView to add nis records [laszlocseh - [`709763c`](https://github.com/eea/volto-marine-policy/commit/709763c37e8add6cbfb22931005ac2fee5d32e18)]
|
|
14
|
+
|
|
15
|
+
#### :bug: Bug Fixes
|
|
16
|
+
|
|
17
|
+
- fix(theme/NISListingView): prevent download button from rendering when duplicates exist [laszlocseh - [`ae0b788`](https://github.com/eea/volto-marine-policy/commit/ae0b7886535e6e0bf0f36dc2a567e3319be94568)]
|
|
18
|
+
- fix(NISListingView): add currentUserId to useEffect dependency [laszlocseh - [`b9bf4d6`](https://github.com/eea/volto-marine-policy/commit/b9bf4d66cb0d1cbdbaa587ed28460154a2f792ac)]
|
|
19
|
+
- fix(theme/NISListingView): dynamically adjust API path based on current route [laszlocseh - [`6d1d1f5`](https://github.com/eea/volto-marine-policy/commit/6d1d1f58fbe4155833b753990f03940434cbc068)]
|
|
20
|
+
- fix: use POST method and handle errors in duplicate check API call [laszlocseh - [`7a0c62c`](https://github.com/eea/volto-marine-policy/commit/7a0c62c1ef4fda3152ed52a44243c8a4cd638fa7)]
|
|
21
|
+
- fix: NISListingView check for duplicates [laszlocseh - [`52ef865`](https://github.com/eea/volto-marine-policy/commit/52ef865978ce19eefa598b70bd1ccee05152169a)]
|
|
22
|
+
|
|
23
|
+
#### :house: Internal changes
|
|
24
|
+
|
|
25
|
+
- style: Automated code fix [eea-jenkins - [`a733b08`](https://github.com/eea/volto-marine-policy/commit/a733b081f6080ffaa6abddc8f343fba0cbe19ab6)]
|
|
26
|
+
- style: disable eslint no-alert rule [laszlocseh - [`44d0f90`](https://github.com/eea/volto-marine-policy/commit/44d0f901bd80d32c9448791cb4afce8bbb3ffde1)]
|
|
27
|
+
- style: Automated code fix [eea-jenkins - [`0ff6b42`](https://github.com/eea/volto-marine-policy/commit/0ff6b42b5e2d2997feaa67bf9ce25fc2d933074d)]
|
|
28
|
+
- style: Automated code fix [eea-jenkins - [`af3a4b6`](https://github.com/eea/volto-marine-policy/commit/af3a4b6f33c455089ac55198fa561a43747c07cc)]
|
|
29
|
+
- chore(deps): bump axios from 0.30.0 to 0.32.0 [dependabot[bot] - [`2b6a39b`](https://github.com/eea/volto-marine-policy/commit/2b6a39b4d9bddb01c57e5cb58efe5c08664aa6bc)]
|
|
30
|
+
|
|
31
|
+
#### :hammer_and_wrench: Others
|
|
32
|
+
|
|
33
|
+
- test: update NISListingView tests to use props instead of mock selectors [laszlocseh - [`42498c0`](https://github.com/eea/volto-marine-policy/commit/42498c006e5a1ad6dcb23ad2fd25c9c1de1280bc)]
|
|
34
|
+
### [3.0.3](https://github.com/eea/volto-marine-policy/compare/3.0.2...3.0.3) - 12 June 2026
|
|
8
35
|
|
|
9
36
|
#### :rocket: New Features
|
|
10
37
|
|
|
@@ -75,8 +102,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
75
102
|
#### :house: Internal changes
|
|
76
103
|
|
|
77
104
|
- style: Automated code fix [eea-jenkins - [`0421290`](https://github.com/eea/volto-marine-policy/commit/0421290d7fdd9fdd0b6d483b53c424a0747137b3)]
|
|
78
|
-
- chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`fb1d0f0`](https://github.com/eea/volto-marine-policy/commit/fb1d0f083474b3a14b0ec36a329c7efc7499dd3d)]
|
|
79
|
-
- chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`40d0b36`](https://github.com/eea/volto-marine-policy/commit/40d0b368836d32544b46ae7c7b070ffb71989b55)]
|
|
80
105
|
|
|
81
106
|
#### :hammer_and_wrench: Others
|
|
82
107
|
|
package/RELEASE.md
CHANGED
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
### Automatic release using Jenkins
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
- The automatic release is started by creating a [Pull Request](../../compare/master...develop) from `develop` to `master`. The pull request status checks correlated to the branch and PR Jenkins jobs need to be processed successfully. 1 review from a github user with rights is mandatory.
|
|
6
|
+
- It runs on every commit on `master` branch, which is protected from direct commits, only allowing pull request merge commits.
|
|
7
|
+
- The automatic release is done by [Jenkins](https://ci.eionet.europa.eu). The status of the release job can be seen both in the Readme.md badges and the green check/red cross/yellow circle near the last commit information. If you click on the icon, you will have the list of checks that were run. The `continuous-integration/jenkins/branch` link goes to the Jenkins job execution webpage.
|
|
8
|
+
- Automated release scripts are located in the `eeacms/gitflow` docker image, specifically [js-release.sh](https://github.com/eea/eea.docker.gitflow/blob/master/src/js-release.sh) script. It uses the `release-it` tool.
|
|
9
|
+
- As long as a PR request is open from develop to master, the PR Jenkins job will automatically re-create the CHANGELOG.md and package.json files to be production-ready.
|
|
10
|
+
- The version format must be MAJOR.MINOR.PATCH. By default, next release is set to next minor version (with patch 0).
|
|
11
|
+
- You can manually change the version in `package.json`. The new version must not be already present in the tags/releases of the repository, otherwise it will be automatically increased by the script. Any changes to the version will trigger a `CHANGELOG.md` re-generation.
|
|
12
|
+
- Automated commits and commits with [JENKINS] or [YARN] in the commit log are excluded from `CHANGELOG.md` file.
|
|
13
13
|
|
|
14
14
|
### Manual release from the develop branch ( beta release )
|
|
15
15
|
|
|
16
16
|
#### Installation and configuration of release-it
|
|
17
17
|
|
|
18
|
-
You need to first install the [release-it](https://github.com/release-it/release-it)
|
|
18
|
+
You need to first install the [release-it](https://github.com/release-it/release-it) client.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
```
|
|
21
|
+
npm install -g release-it
|
|
22
|
+
```
|
|
23
23
|
|
|
24
24
|
Release-it uses the configuration written in the [`.release-it.json`](./.release-it.json) file located in the root of the repository.
|
|
25
25
|
|
|
@@ -32,15 +32,15 @@ Release-it is a tool that automates 4 important steps in the release process:
|
|
|
32
32
|
|
|
33
33
|
To configure the authentification, you need to export GITHUB_TOKEN for [GitHub](https://github.com/settings/tokens)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
```
|
|
36
|
+
export GITHUB_TOKEN=XXX-XXXXXXXXXXXXXXXXXXXXXX
|
|
37
|
+
```
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
To configure npm, you can use the `npm login` command or use a configuration file with a TOKEN :
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
```
|
|
42
|
+
echo "//registry.npmjs.org/:_authToken=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" > .npmrc
|
|
43
|
+
```
|
|
44
44
|
|
|
45
45
|
#### Using release-it tool
|
|
46
46
|
|
|
@@ -71,4 +71,3 @@ Generic command, does not automatically add the `beta` to version, but you can s
|
|
|
71
71
|
> Do not use release-it tool on master branch, the commit on CHANGELOG.md file and the version increase in the package.json file can't be done without a PULL REQUEST.
|
|
72
72
|
|
|
73
73
|
> Do not keep Pull Requests from develop to master branches open when you are doing beta releases from the develop branch. As long as a PR to master is open, an automatic script will run on every commit and will update both the version and the changelog to a production-ready state - ( MAJOR.MINOR.PATCH mandatory format for version).
|
|
74
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eeacms/volto-marine-policy",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "@eeacms/volto-marine-policy: Volto add-on",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"author": "European Environment Agency: IDM2 A-Team",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@eeacms/volto-metadata-block": "*",
|
|
42
42
|
"@eeacms/volto-openlayers-map": "2.0.1",
|
|
43
43
|
"@eeacms/volto-workflow-progress": "*",
|
|
44
|
-
"axios": "0.
|
|
44
|
+
"axios": "0.32.0",
|
|
45
45
|
"d3-array": "^2.12.1",
|
|
46
46
|
"jquery": "3.6.0",
|
|
47
47
|
"razzle-plugin-scss": "^4.2.18",
|
|
@@ -3,11 +3,12 @@ import ProgressWorkflow from '@eeacms/volto-marine-policy/components/theme/Progr
|
|
|
3
3
|
import qs from 'query-string';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import './style.less';
|
|
6
|
-
import { useState, useEffect } from 'react';
|
|
6
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
7
7
|
import { useSelector } from 'react-redux';
|
|
8
8
|
import { Checkbox } from 'semantic-ui-react';
|
|
9
9
|
import { Button, Select, Dimmer, Loader } from 'semantic-ui-react';
|
|
10
10
|
import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
|
|
11
|
+
import jwtDecode from 'jwt-decode';
|
|
11
12
|
|
|
12
13
|
function normalizeQueryOperators(query) {
|
|
13
14
|
return query.map((q) => {
|
|
@@ -63,6 +64,21 @@ async function getCurrentSearchItems() {
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
function normalizeItemPath(itemId) {
|
|
68
|
+
if (!itemId) return '';
|
|
69
|
+
let path = itemId.replace(/^https?:\/\/[^/]+/, '');
|
|
70
|
+
try {
|
|
71
|
+
const apiURL = new URL(window.env.apiPath);
|
|
72
|
+
const prefix = apiURL.pathname;
|
|
73
|
+
if (prefix && prefix !== '/' && path.startsWith(prefix)) {
|
|
74
|
+
path = path.slice(prefix.length) || '/';
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// apiPath might be relative; ignore
|
|
78
|
+
}
|
|
79
|
+
return path;
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
function formatAssignedTo(assignedTo) {
|
|
67
83
|
if (!assignedTo) return '';
|
|
68
84
|
// Fix Python-style unicode escape sequences (\UXXXXXXXX -> actual char)
|
|
@@ -74,16 +90,27 @@ function formatAssignedTo(assignedTo) {
|
|
|
74
90
|
return result;
|
|
75
91
|
}
|
|
76
92
|
|
|
93
|
+
function isAssignedToMe(item, currentUserId) {
|
|
94
|
+
if (!currentUserId || !item.nis_assigned_to) return false;
|
|
95
|
+
const match = item.nis_assigned_to.match(/\(([^)]+)\)\s*$/);
|
|
96
|
+
return match ? match[1] === currentUserId : false;
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
const NISListingView = ({ items, isEditMode }) => {
|
|
78
100
|
const [isLoading, setIsLoading] = useState(false);
|
|
79
101
|
const [selectedItems, setSelectedItems] = useState([]);
|
|
80
102
|
const [itemsTotal, setItemsTotal] = useState(0);
|
|
81
103
|
const [duplicateIds, setDuplicateIds] = useState(null);
|
|
82
104
|
const [duplicateGroups, setDuplicateGroups] = useState([]);
|
|
105
|
+
const [duplicatesLoading, setDuplicatesLoading] = useState(false);
|
|
106
|
+
const [errorMessage, setErrorMessage] = useState(null);
|
|
107
|
+
const [locationTick, setLocationTick] = useState(0);
|
|
83
108
|
|
|
84
109
|
const [users, setUsers] = useState([]);
|
|
85
110
|
const [assignee, setAssignee] = useState(null);
|
|
86
111
|
const actions = useSelector((state) => state.actions.actions);
|
|
112
|
+
const token = useSelector((state) => state.userSession.token);
|
|
113
|
+
const currentUserId = token ? jwtDecode(token).sub : '';
|
|
87
114
|
const canEditPage = actions?.object?.some((action) => action.id === 'edit');
|
|
88
115
|
|
|
89
116
|
const toggleSelection = (id) => {
|
|
@@ -112,7 +139,10 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
112
139
|
const onBulkAssign = async (ids, assignee) => {
|
|
113
140
|
setIsLoading(true);
|
|
114
141
|
await fetch(
|
|
115
|
-
`${window.env.apiPath}/++api
|
|
142
|
+
`${window.env.apiPath}/++api++${window.location.pathname.replace(
|
|
143
|
+
'/marine',
|
|
144
|
+
'',
|
|
145
|
+
)}/@bulk-assign${window.location.search}`,
|
|
116
146
|
{
|
|
117
147
|
method: 'POST',
|
|
118
148
|
headers: {
|
|
@@ -132,10 +162,14 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
132
162
|
};
|
|
133
163
|
|
|
134
164
|
const handleCopy = async (item) => {
|
|
165
|
+
setErrorMessage(null);
|
|
135
166
|
setIsLoading(true);
|
|
136
167
|
try {
|
|
137
168
|
const res = await fetch(
|
|
138
|
-
`${window.env.apiPath}
|
|
169
|
+
`${window.env.apiPath}/++api++${item['@id'].replace(
|
|
170
|
+
/^\/marine/,
|
|
171
|
+
'',
|
|
172
|
+
)}/@copy-nis-record`,
|
|
139
173
|
{
|
|
140
174
|
method: 'POST',
|
|
141
175
|
headers: {
|
|
@@ -147,26 +181,96 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
147
181
|
);
|
|
148
182
|
if (res.ok) {
|
|
149
183
|
window.location.reload();
|
|
184
|
+
} else {
|
|
185
|
+
setErrorMessage('Something went wrong. The copy was not successful.');
|
|
186
|
+
setIsLoading(false);
|
|
150
187
|
}
|
|
151
188
|
} catch (err) {
|
|
152
189
|
// eslint-disable-next-line no-console
|
|
153
190
|
console.error('Copy failed:', err);
|
|
191
|
+
setErrorMessage('Something went wrong. The copy was not successful.');
|
|
154
192
|
setIsLoading(false);
|
|
155
193
|
}
|
|
156
194
|
};
|
|
157
195
|
|
|
196
|
+
const handleRemove = async (item) => {
|
|
197
|
+
// eslint-disable-next-line no-alert
|
|
198
|
+
if (!window.confirm('Are you sure you want to remove this item?')) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setErrorMessage(null);
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch(
|
|
204
|
+
`${window.env.apiPath}/++api++${item['@id'].replace(/^\/marine/, '')}`,
|
|
205
|
+
{
|
|
206
|
+
method: 'DELETE',
|
|
207
|
+
credentials: 'include',
|
|
208
|
+
headers: { Accept: 'application/json' },
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
if (res.ok) {
|
|
212
|
+
window.location.reload();
|
|
213
|
+
} else {
|
|
214
|
+
setErrorMessage('Something went wrong. The item could not be removed.');
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
// eslint-disable-next-line no-console
|
|
218
|
+
console.error('Remove failed:', err);
|
|
219
|
+
setErrorMessage('Something went wrong. The item could not be removed.');
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const trigger = () => setLocationTick((t) => t + 1);
|
|
225
|
+
window.addEventListener('popstate', trigger);
|
|
226
|
+
const origPush = window.history.pushState;
|
|
227
|
+
const origReplace = window.history.replaceState;
|
|
228
|
+
window.history.pushState = function (...a) {
|
|
229
|
+
origPush.apply(this, a);
|
|
230
|
+
trigger();
|
|
231
|
+
};
|
|
232
|
+
window.history.replaceState = function (...a) {
|
|
233
|
+
origReplace.apply(this, a);
|
|
234
|
+
trigger();
|
|
235
|
+
};
|
|
236
|
+
return () => {
|
|
237
|
+
window.removeEventListener('popstate', trigger);
|
|
238
|
+
window.history.pushState = origPush;
|
|
239
|
+
window.history.replaceState = origReplace;
|
|
240
|
+
};
|
|
241
|
+
}, []);
|
|
242
|
+
|
|
158
243
|
useEffect(() => {
|
|
159
244
|
const parsed = qs.parse(window.location.search);
|
|
160
245
|
if (parsed['check-duplicates']) {
|
|
161
|
-
|
|
162
|
-
fetch(
|
|
246
|
+
setDuplicatesLoading(true);
|
|
247
|
+
fetch(
|
|
248
|
+
`${window.env.apiPath}/++api++${window.location.pathname.replace(
|
|
249
|
+
'/marine',
|
|
250
|
+
'',
|
|
251
|
+
)}/@check-nis-duplicates`,
|
|
252
|
+
{
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
Accept: 'application/json',
|
|
257
|
+
},
|
|
258
|
+
credentials: 'include',
|
|
259
|
+
body: JSON.stringify({ search: window.location.search }),
|
|
260
|
+
},
|
|
261
|
+
)
|
|
163
262
|
.then((res) => res.json())
|
|
164
263
|
.then((data) => {
|
|
165
|
-
setDuplicateIds(new Set(data.duplicate_ids));
|
|
264
|
+
setDuplicateIds(new Set(data.duplicate_ids.map(normalizeItemPath)));
|
|
166
265
|
setDuplicateGroups(data.groups || []);
|
|
167
|
-
})
|
|
266
|
+
})
|
|
267
|
+
.catch((err) => {
|
|
268
|
+
// eslint-disable-next-line no-console
|
|
269
|
+
console.error('Check duplicates failed:', err);
|
|
270
|
+
})
|
|
271
|
+
.finally(() => setDuplicatesLoading(false));
|
|
168
272
|
}
|
|
169
|
-
}, []);
|
|
273
|
+
}, [locationTick]);
|
|
170
274
|
|
|
171
275
|
useEffect(() => {
|
|
172
276
|
const fetchUsers = async () => {
|
|
@@ -193,6 +297,108 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
193
297
|
fetchUsers();
|
|
194
298
|
}, []);
|
|
195
299
|
|
|
300
|
+
const duplicateTableRows = useMemo(() => {
|
|
301
|
+
if (!duplicateIds) return items;
|
|
302
|
+
|
|
303
|
+
const rows = [];
|
|
304
|
+
duplicateGroups.forEach((group, gIdx) => {
|
|
305
|
+
(group.items || []).forEach((item) => {
|
|
306
|
+
rows.push(
|
|
307
|
+
<tr key={item['@id']}>
|
|
308
|
+
<td>{item.nis_species_name_original}</td>
|
|
309
|
+
<td>{item.nis_species_name_accepted}</td>
|
|
310
|
+
<td>{item.nis_scientificname_accepted}</td>
|
|
311
|
+
<td>{item.nis_region}</td>
|
|
312
|
+
<td>{item.nis_subregion}</td>
|
|
313
|
+
<td>{item.nis_country}</td>
|
|
314
|
+
<td>{item.nis_status}</td>
|
|
315
|
+
<td>{item.nis_group}</td>
|
|
316
|
+
<td>{item.nis_year}</td>
|
|
317
|
+
<td>
|
|
318
|
+
<div className="assigned-to-container">
|
|
319
|
+
<div>{formatAssignedTo(item.nis_assigned_to)}</div>
|
|
320
|
+
{canEditPage && (
|
|
321
|
+
<Checkbox
|
|
322
|
+
checked={selectedItems.includes(item['@id'])}
|
|
323
|
+
onChange={() => toggleSelection(item['@id'])}
|
|
324
|
+
/>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
</td>
|
|
328
|
+
<td>
|
|
329
|
+
<div className="workflow-actions">
|
|
330
|
+
<div className="action-buttons">
|
|
331
|
+
<UniversalLink
|
|
332
|
+
className="ui button secondary mini"
|
|
333
|
+
href={item['@id']}
|
|
334
|
+
target="_blank"
|
|
335
|
+
rel="noopener noreferrer"
|
|
336
|
+
>
|
|
337
|
+
View
|
|
338
|
+
</UniversalLink>
|
|
339
|
+
{isAssignedToMe(item, currentUserId) && (
|
|
340
|
+
<UniversalLink
|
|
341
|
+
className="ui button primary mini"
|
|
342
|
+
href={`${item['@id']}/edit`}
|
|
343
|
+
target="_blank"
|
|
344
|
+
rel="noopener noreferrer"
|
|
345
|
+
>
|
|
346
|
+
Edit
|
|
347
|
+
</UniversalLink>
|
|
348
|
+
)}
|
|
349
|
+
{isAssignedToMe(item, currentUserId) && (
|
|
350
|
+
<Button
|
|
351
|
+
className="tertiary mini"
|
|
352
|
+
onClick={() => handleCopy(item)}
|
|
353
|
+
>
|
|
354
|
+
Copy
|
|
355
|
+
</Button>
|
|
356
|
+
)}
|
|
357
|
+
{isAssignedToMe(item, currentUserId) && (
|
|
358
|
+
<Button
|
|
359
|
+
className="negative mini"
|
|
360
|
+
onClick={() => handleRemove(item)}
|
|
361
|
+
>
|
|
362
|
+
Remove
|
|
363
|
+
</Button>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
<div className="workflow-progress">
|
|
367
|
+
<ProgressWorkflow
|
|
368
|
+
content={item}
|
|
369
|
+
pathname={item['@id']}
|
|
370
|
+
token={123}
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</td>
|
|
375
|
+
</tr>,
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
if (gIdx < duplicateGroups.length - 1) {
|
|
379
|
+
rows.push(
|
|
380
|
+
<tr key={`sep-${gIdx}`}>
|
|
381
|
+
<td
|
|
382
|
+
colSpan="11"
|
|
383
|
+
style={{
|
|
384
|
+
borderBottom: '2px solid #999',
|
|
385
|
+
padding: 0,
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
</tr>,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
return rows;
|
|
393
|
+
}, [
|
|
394
|
+
duplicateIds,
|
|
395
|
+
duplicateGroups,
|
|
396
|
+
items,
|
|
397
|
+
canEditPage,
|
|
398
|
+
selectedItems,
|
|
399
|
+
currentUserId,
|
|
400
|
+
]);
|
|
401
|
+
|
|
196
402
|
return (
|
|
197
403
|
<>
|
|
198
404
|
{isLoading && (
|
|
@@ -200,8 +406,28 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
200
406
|
<Loader>Assigning...</Loader>
|
|
201
407
|
</Dimmer>
|
|
202
408
|
)}
|
|
203
|
-
{canEditPage && (
|
|
409
|
+
{canEditPage && !duplicateIds && (
|
|
204
410
|
<div className="download-button-wrapper">
|
|
411
|
+
<UniversalLink
|
|
412
|
+
className="ui button primary download-as-xls"
|
|
413
|
+
href={`${window.location.pathname}/add?type=non_indigenous_species`}
|
|
414
|
+
target="_blank"
|
|
415
|
+
rel="noopener noreferrer"
|
|
416
|
+
>
|
|
417
|
+
<i className="ri-add-line"></i>Add NIS record
|
|
418
|
+
</UniversalLink>
|
|
419
|
+
<Button
|
|
420
|
+
className="primary"
|
|
421
|
+
size="small"
|
|
422
|
+
onClick={() => {
|
|
423
|
+
const parsed = qs.parse(window.location.search);
|
|
424
|
+
parsed['check-duplicates'] = '1';
|
|
425
|
+
window.location.search = qs.stringify(parsed);
|
|
426
|
+
}}
|
|
427
|
+
>
|
|
428
|
+
<i className="ri-file-copy-line"></i>
|
|
429
|
+
Check duplicates
|
|
430
|
+
</Button>
|
|
205
431
|
<Button
|
|
206
432
|
className="primary"
|
|
207
433
|
size="small"
|
|
@@ -209,32 +435,33 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
209
435
|
>
|
|
210
436
|
<i className="ri-user-add-line"></i>Assign search results
|
|
211
437
|
</Button>
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
438
|
+
<a
|
|
439
|
+
href={`/marine/++api++${window.location.pathname.replace(
|
|
440
|
+
'/marine',
|
|
441
|
+
'',
|
|
442
|
+
)}/nis-export${window.location.search}`}
|
|
443
|
+
title="Download"
|
|
444
|
+
target="_blank"
|
|
445
|
+
rel="noopener"
|
|
446
|
+
className="ui button primary download-as-xls"
|
|
447
|
+
>
|
|
448
|
+
<i className="ri-file-download-line"></i>
|
|
449
|
+
Download search results
|
|
450
|
+
</a>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
{errorMessage && (
|
|
454
|
+
<div
|
|
455
|
+
style={{
|
|
456
|
+
background: '#f8d7da',
|
|
457
|
+
border: '1px solid #f5c6cb',
|
|
458
|
+
color: '#721c24',
|
|
459
|
+
padding: '10px 15px',
|
|
460
|
+
marginBottom: '15px',
|
|
461
|
+
borderRadius: '4px',
|
|
462
|
+
}}
|
|
463
|
+
>
|
|
464
|
+
{errorMessage}
|
|
238
465
|
</div>
|
|
239
466
|
)}
|
|
240
467
|
{duplicateIds && (
|
|
@@ -247,7 +474,11 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
247
474
|
borderRadius: '4px',
|
|
248
475
|
}}
|
|
249
476
|
>
|
|
250
|
-
Showing
|
|
477
|
+
Showing{' '}
|
|
478
|
+
{duplicateGroups.reduce(
|
|
479
|
+
(sum, g) => sum + (g.items ? g.items.length : 0),
|
|
480
|
+
0,
|
|
481
|
+
)}{' '}
|
|
251
482
|
duplicate records across {duplicateGroups.length} groups
|
|
252
483
|
<a
|
|
253
484
|
href={(() => {
|
|
@@ -262,6 +493,13 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
262
493
|
</a>
|
|
263
494
|
</div>
|
|
264
495
|
)}
|
|
496
|
+
{duplicateIds && (
|
|
497
|
+
<style
|
|
498
|
+
dangerouslySetInnerHTML={{
|
|
499
|
+
__html: `.listing-pagination,.pagination-wrapper,nav[aria-label="Pagination Navigation"],.ui.pagination{display:none!important}`,
|
|
500
|
+
}}
|
|
501
|
+
/>
|
|
502
|
+
)}
|
|
265
503
|
<table className="ui table">
|
|
266
504
|
<thead>
|
|
267
505
|
<tr>
|
|
@@ -279,70 +517,87 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
279
517
|
</tr>
|
|
280
518
|
</thead>
|
|
281
519
|
<tbody>
|
|
282
|
-
{(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
<td>{item.nis_species_name_accepted}</td>
|
|
289
|
-
<td>{item.nis_scientificname_accepted}</td>
|
|
290
|
-
<td>{item.nis_region}</td>
|
|
291
|
-
<td>{item.nis_subregion}</td>
|
|
292
|
-
<td>{item.nis_country}</td>
|
|
293
|
-
<td>{item.nis_status}</td>
|
|
294
|
-
<td>{item.nis_group}</td>
|
|
295
|
-
<td>{item.nis_year}</td>
|
|
296
|
-
<td>
|
|
297
|
-
<div className="assigned-to-container">
|
|
298
|
-
<div>{formatAssignedTo(item.nis_assigned_to)}</div>
|
|
299
|
-
{canEditPage && (
|
|
300
|
-
<Checkbox
|
|
301
|
-
checked={selectedItems.includes(item['@id'])}
|
|
302
|
-
onChange={() => toggleSelection(item['@id'])}
|
|
303
|
-
/>
|
|
304
|
-
)}
|
|
305
|
-
</div>
|
|
520
|
+
{duplicateIds ? (
|
|
521
|
+
duplicateTableRows
|
|
522
|
+
) : duplicatesLoading ? (
|
|
523
|
+
<tr>
|
|
524
|
+
<td colSpan="11" style={{ textAlign: 'center', padding: '20px' }}>
|
|
525
|
+
<Loader active inline />
|
|
306
526
|
</td>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
>
|
|
324
|
-
Edit
|
|
325
|
-
</UniversalLink>
|
|
527
|
+
</tr>
|
|
528
|
+
) : (
|
|
529
|
+
items.map((item) => (
|
|
530
|
+
<tr key={item['@id']}>
|
|
531
|
+
<td>{item.nis_species_name_original}</td>
|
|
532
|
+
<td>{item.nis_species_name_accepted}</td>
|
|
533
|
+
<td>{item.nis_scientificname_accepted}</td>
|
|
534
|
+
<td>{item.nis_region}</td>
|
|
535
|
+
<td>{item.nis_subregion}</td>
|
|
536
|
+
<td>{item.nis_country}</td>
|
|
537
|
+
<td>{item.nis_status}</td>
|
|
538
|
+
<td>{item.nis_group}</td>
|
|
539
|
+
<td>{item.nis_year}</td>
|
|
540
|
+
<td>
|
|
541
|
+
<div className="assigned-to-container">
|
|
542
|
+
<div>{formatAssignedTo(item.nis_assigned_to)}</div>
|
|
326
543
|
{canEditPage && (
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Copy
|
|
332
|
-
</Button>
|
|
544
|
+
<Checkbox
|
|
545
|
+
checked={selectedItems.includes(item['@id'])}
|
|
546
|
+
onChange={() => toggleSelection(item['@id'])}
|
|
547
|
+
/>
|
|
333
548
|
)}
|
|
334
549
|
</div>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
550
|
+
</td>
|
|
551
|
+
<td>
|
|
552
|
+
<div className="workflow-actions">
|
|
553
|
+
<div className="action-buttons">
|
|
554
|
+
<UniversalLink
|
|
555
|
+
className="ui button secondary mini"
|
|
556
|
+
href={`${item['@id']}`}
|
|
557
|
+
target="_blank"
|
|
558
|
+
rel="noopener noreferrer"
|
|
559
|
+
>
|
|
560
|
+
View
|
|
561
|
+
</UniversalLink>
|
|
562
|
+
{isAssignedToMe(item, currentUserId) && (
|
|
563
|
+
<UniversalLink
|
|
564
|
+
className="ui button primary mini"
|
|
565
|
+
href={`${item['@id']}/edit`}
|
|
566
|
+
target="_blank"
|
|
567
|
+
rel="noopener noreferrer"
|
|
568
|
+
>
|
|
569
|
+
Edit
|
|
570
|
+
</UniversalLink>
|
|
571
|
+
)}
|
|
572
|
+
{isAssignedToMe(item, currentUserId) && (
|
|
573
|
+
<Button
|
|
574
|
+
className="tertiary mini"
|
|
575
|
+
onClick={() => handleCopy(item)}
|
|
576
|
+
>
|
|
577
|
+
Copy
|
|
578
|
+
</Button>
|
|
579
|
+
)}
|
|
580
|
+
{isAssignedToMe(item, currentUserId) && (
|
|
581
|
+
<Button
|
|
582
|
+
className="negative mini"
|
|
583
|
+
onClick={() => handleRemove(item)}
|
|
584
|
+
>
|
|
585
|
+
Remove
|
|
586
|
+
</Button>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
<div className="workflow-progress">
|
|
590
|
+
<ProgressWorkflow
|
|
591
|
+
content={item}
|
|
592
|
+
pathname={item['@id']}
|
|
593
|
+
token={123}
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
341
596
|
</div>
|
|
342
|
-
</
|
|
343
|
-
</
|
|
344
|
-
|
|
345
|
-
)
|
|
597
|
+
</td>
|
|
598
|
+
</tr>
|
|
599
|
+
))
|
|
600
|
+
)}
|
|
346
601
|
</tbody>
|
|
347
602
|
</table>
|
|
348
603
|
{selectedItems.length > 0 && (
|
|
@@ -5,12 +5,9 @@ import configureStore from 'redux-mock-store';
|
|
|
5
5
|
import { Provider } from 'react-redux';
|
|
6
6
|
import NISListingView from './NISListingView';
|
|
7
7
|
|
|
8
|
-
const mockUseSelector = jest.fn();
|
|
9
|
-
|
|
10
8
|
jest.mock('react-redux', () => ({
|
|
11
9
|
__esModule: true,
|
|
12
10
|
...jest.requireActual('react-redux'),
|
|
13
|
-
useSelector: (selector) => mockUseSelector(selector),
|
|
14
11
|
}));
|
|
15
12
|
|
|
16
13
|
jest.mock(
|
|
@@ -52,9 +49,16 @@ function buildItems(count = 3) {
|
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
function renderComponent(props = {}) {
|
|
55
|
-
const {
|
|
52
|
+
const {
|
|
53
|
+
items = buildItems(),
|
|
54
|
+
actions = { object: [{ id: 'edit' }] },
|
|
55
|
+
token = null,
|
|
56
|
+
...rest
|
|
57
|
+
} = props;
|
|
56
58
|
const store = mockStore({
|
|
57
59
|
intl: { locale: 'en', messages: {} },
|
|
60
|
+
actions: { actions },
|
|
61
|
+
userSession: { token },
|
|
58
62
|
});
|
|
59
63
|
return render(
|
|
60
64
|
<Provider store={store}>
|
|
@@ -110,10 +114,6 @@ describe('NISListingView', () => {
|
|
|
110
114
|
ok: true,
|
|
111
115
|
});
|
|
112
116
|
});
|
|
113
|
-
|
|
114
|
-
mockUseSelector.mockReturnValue({
|
|
115
|
-
object: [{ id: 'edit' }],
|
|
116
|
-
});
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
describe('table rendering', () => {
|
|
@@ -236,22 +236,17 @@ describe('NISListingView', () => {
|
|
|
236
236
|
|
|
237
237
|
describe('admin controls — canEditPage', () => {
|
|
238
238
|
it('shows assign, download, and check-duplicates buttons when user can edit', async () => {
|
|
239
|
-
mockUseSelector.mockReturnValue({
|
|
240
|
-
object: [{ id: 'edit' }],
|
|
241
|
-
});
|
|
242
239
|
renderComponent();
|
|
243
240
|
await waitFor(() => {
|
|
244
241
|
expect(screen.getByText('Assign search results')).toBeInTheDocument();
|
|
245
242
|
});
|
|
243
|
+
expect(screen.getByText('Add NIS record')).toBeInTheDocument();
|
|
246
244
|
expect(screen.getByText('Download search results')).toBeInTheDocument();
|
|
247
245
|
expect(screen.getByText('Check duplicates')).toBeInTheDocument();
|
|
248
246
|
});
|
|
249
247
|
|
|
250
248
|
it('hides admin controls when user cannot edit', async () => {
|
|
251
|
-
|
|
252
|
-
object: [],
|
|
253
|
-
});
|
|
254
|
-
renderComponent();
|
|
249
|
+
renderComponent({ actions: { object: [] } });
|
|
255
250
|
await waitFor(() => {
|
|
256
251
|
expect(screen.getByText('Species name original')).toBeInTheDocument();
|
|
257
252
|
});
|
|
@@ -265,10 +260,7 @@ describe('NISListingView', () => {
|
|
|
265
260
|
});
|
|
266
261
|
|
|
267
262
|
it('hides checkboxes and Copy button when user cannot edit', async () => {
|
|
268
|
-
|
|
269
|
-
object: [],
|
|
270
|
-
});
|
|
271
|
-
renderComponent();
|
|
263
|
+
renderComponent({ actions: { object: [] } });
|
|
272
264
|
await waitFor(() => {
|
|
273
265
|
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
274
266
|
});
|
|
@@ -308,10 +300,38 @@ describe('NISListingView', () => {
|
|
|
308
300
|
});
|
|
309
301
|
}
|
|
310
302
|
|
|
303
|
+
function makeGroup(itemNumber) {
|
|
304
|
+
const itemId = `/marine/item-${itemNumber}`;
|
|
305
|
+
const itemData = {
|
|
306
|
+
'@id': itemId,
|
|
307
|
+
review_state: 'draft',
|
|
308
|
+
nis_species_name_original: `Species ${itemNumber}`,
|
|
309
|
+
nis_species_name_accepted: `Species ${itemNumber} accepted`,
|
|
310
|
+
nis_scientificname_accepted: `Scientificus acceptus ${itemNumber}`,
|
|
311
|
+
nis_region: 'Europe',
|
|
312
|
+
nis_subregion: 'Western Europe',
|
|
313
|
+
nis_country: 'France',
|
|
314
|
+
nis_status: 'Established',
|
|
315
|
+
nis_group: 'Fish',
|
|
316
|
+
nis_year: 2020 + itemNumber,
|
|
317
|
+
nis_assigned_to: `User ${itemNumber} (user${itemNumber})`,
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
species_name_original: `Species ${itemNumber}`,
|
|
321
|
+
species_name_accepted: `Species ${itemNumber} accepted`,
|
|
322
|
+
scientificname_accepted: `Scientificus acceptus ${itemNumber}`,
|
|
323
|
+
region: 'Europe',
|
|
324
|
+
subregion: 'Western Europe',
|
|
325
|
+
country: 'France',
|
|
326
|
+
year: 2020 + itemNumber,
|
|
327
|
+
items: [itemData],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
311
331
|
it('shows the duplicate info banner when check-duplicates param is set', async () => {
|
|
312
332
|
mockDuplicateFetch(
|
|
313
333
|
['/marine/item-1', '/marine/item-3'],
|
|
314
|
-
[
|
|
334
|
+
[makeGroup(1), makeGroup(3)],
|
|
315
335
|
);
|
|
316
336
|
renderComponent();
|
|
317
337
|
await waitFor(() => {
|
|
@@ -324,7 +344,7 @@ describe('NISListingView', () => {
|
|
|
324
344
|
it('filters table to show only duplicate items', async () => {
|
|
325
345
|
mockDuplicateFetch(
|
|
326
346
|
['/marine/item-1', '/marine/item-3'],
|
|
327
|
-
[
|
|
347
|
+
[makeGroup(1), makeGroup(3)],
|
|
328
348
|
);
|
|
329
349
|
const items = buildItems(5);
|
|
330
350
|
renderComponent({ items });
|