@eeacms/volto-marine-policy 3.0.2 → 3.0.3
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,17 @@ 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.3](https://github.com/eea/volto-marine-policy/compare/3.0.2...3.0.3) - 11 June 2026
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- feat: added button to show NIS duplicates [laszlocseh - [`591927c`](https://github.com/eea/volto-marine-policy/commit/591927ca76c693d8d33d7b9ab8bdedc10368c0f2)]
|
|
12
|
+
- feat: added button to copy a NIS listing item [laszlocseh - [`588a5fe`](https://github.com/eea/volto-marine-policy/commit/588a5fe1409292a5df742590d615e230dc09ebd9)]
|
|
13
|
+
|
|
14
|
+
#### :hammer_and_wrench: Others
|
|
15
|
+
|
|
16
|
+
- test: fix NISListingView.test.jsx [laszlocseh - [`0ba486d`](https://github.com/eea/volto-marine-policy/commit/0ba486d1058bc71fe73ef53e628fd7c5423688ae)]
|
|
17
|
+
- test: added NISListingView.test.jsx [laszlocseh - [`4c54331`](https://github.com/eea/volto-marine-policy/commit/4c54331dd40f178dfd88dcc0d73d1eb96d2834e6)]
|
|
7
18
|
### [3.0.2](https://github.com/eea/volto-marine-policy/compare/3.0.1...3.0.2) - 10 June 2026
|
|
8
19
|
|
|
9
20
|
#### :rocket: Dependency updates
|
package/package.json
CHANGED
|
@@ -78,6 +78,9 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
78
78
|
const [isLoading, setIsLoading] = useState(false);
|
|
79
79
|
const [selectedItems, setSelectedItems] = useState([]);
|
|
80
80
|
const [itemsTotal, setItemsTotal] = useState(0);
|
|
81
|
+
const [duplicateIds, setDuplicateIds] = useState(null);
|
|
82
|
+
const [duplicateGroups, setDuplicateGroups] = useState([]);
|
|
83
|
+
|
|
81
84
|
const [users, setUsers] = useState([]);
|
|
82
85
|
const [assignee, setAssignee] = useState(null);
|
|
83
86
|
const actions = useSelector((state) => state.actions.actions);
|
|
@@ -128,6 +131,43 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
128
131
|
window.location.reload();
|
|
129
132
|
};
|
|
130
133
|
|
|
134
|
+
const handleCopy = async (item) => {
|
|
135
|
+
setIsLoading(true);
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch(
|
|
138
|
+
`${window.env.apiPath}${item['@id']}/@copy-nis-record`,
|
|
139
|
+
{
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
Accept: 'application/json',
|
|
144
|
+
},
|
|
145
|
+
credentials: 'include',
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
if (res.ok) {
|
|
149
|
+
window.location.reload();
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.error('Copy failed:', err);
|
|
154
|
+
setIsLoading(false);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const parsed = qs.parse(window.location.search);
|
|
160
|
+
if (parsed['check-duplicates']) {
|
|
161
|
+
const containerPath = window.location.pathname.replace('/marine', '');
|
|
162
|
+
fetch(`${window.env.apiPath}${containerPath}/@check-nis-duplicates`)
|
|
163
|
+
.then((res) => res.json())
|
|
164
|
+
.then((data) => {
|
|
165
|
+
setDuplicateIds(new Set(data.duplicate_ids));
|
|
166
|
+
setDuplicateGroups(data.groups || []);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
131
171
|
useEffect(() => {
|
|
132
172
|
const fetchUsers = async () => {
|
|
133
173
|
const res = await fetch(
|
|
@@ -183,9 +223,45 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
183
223
|
<i className="ri-file-download-line"></i>
|
|
184
224
|
Download search results
|
|
185
225
|
</a>
|
|
226
|
+
<Button
|
|
227
|
+
className="primary"
|
|
228
|
+
size="small"
|
|
229
|
+
onClick={() => {
|
|
230
|
+
const parsed = qs.parse(window.location.search);
|
|
231
|
+
parsed['check-duplicates'] = '1';
|
|
232
|
+
window.location.search = qs.stringify(parsed);
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
Check duplicates
|
|
236
|
+
</Button>
|
|
186
237
|
</div>
|
|
187
238
|
</div>
|
|
188
239
|
)}
|
|
240
|
+
{duplicateIds && (
|
|
241
|
+
<div
|
|
242
|
+
style={{
|
|
243
|
+
background: '#fff3cd',
|
|
244
|
+
border: '1px solid #ffc107',
|
|
245
|
+
padding: '10px 15px',
|
|
246
|
+
marginBottom: '15px',
|
|
247
|
+
borderRadius: '4px',
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
Showing {items.filter((i) => duplicateIds.has(i['@id'])).length}{' '}
|
|
251
|
+
duplicate records across {duplicateGroups.length} groups
|
|
252
|
+
<a
|
|
253
|
+
href={(() => {
|
|
254
|
+
const p = qs.parse(window.location.search);
|
|
255
|
+
delete p['check-duplicates'];
|
|
256
|
+
const q = qs.stringify(p);
|
|
257
|
+
return `${window.location.pathname}${q ? '?' + q : ''}`;
|
|
258
|
+
})()}
|
|
259
|
+
style={{ marginLeft: '10px', fontSize: '0.9em' }}
|
|
260
|
+
>
|
|
261
|
+
Clear
|
|
262
|
+
</a>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
189
265
|
<table className="ui table">
|
|
190
266
|
<thead>
|
|
191
267
|
<tr>
|
|
@@ -197,12 +273,16 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
197
273
|
<th>Country</th>
|
|
198
274
|
<th>Status</th>
|
|
199
275
|
<th>Group</th>
|
|
276
|
+
<th>Year</th>
|
|
200
277
|
<th>Assigned to</th>
|
|
201
278
|
<th></th>
|
|
202
279
|
</tr>
|
|
203
280
|
</thead>
|
|
204
281
|
<tbody>
|
|
205
|
-
{
|
|
282
|
+
{(duplicateIds
|
|
283
|
+
? items.filter((item) => duplicateIds.has(item['@id']))
|
|
284
|
+
: items
|
|
285
|
+
).map((item, index) => (
|
|
206
286
|
<tr key={item['@id']}>
|
|
207
287
|
<td>{item.nis_species_name_original}</td>
|
|
208
288
|
<td>{item.nis_species_name_accepted}</td>
|
|
@@ -212,6 +292,7 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
212
292
|
<td>{item.nis_country}</td>
|
|
213
293
|
<td>{item.nis_status}</td>
|
|
214
294
|
<td>{item.nis_group}</td>
|
|
295
|
+
<td>{item.nis_year}</td>
|
|
215
296
|
<td>
|
|
216
297
|
<div className="assigned-to-container">
|
|
217
298
|
<div>{formatAssignedTo(item.nis_assigned_to)}</div>
|
|
@@ -229,15 +310,27 @@ const NISListingView = ({ items, isEditMode }) => {
|
|
|
229
310
|
<UniversalLink
|
|
230
311
|
className="ui button secondary mini"
|
|
231
312
|
href={`${item['@id']}`}
|
|
313
|
+
target="_blank"
|
|
314
|
+
rel="noopener noreferrer"
|
|
232
315
|
>
|
|
233
316
|
View
|
|
234
317
|
</UniversalLink>
|
|
235
318
|
<UniversalLink
|
|
236
319
|
className="ui button primary mini"
|
|
237
320
|
href={`${item['@id']}/edit`}
|
|
321
|
+
target="_blank"
|
|
322
|
+
rel="noopener noreferrer"
|
|
238
323
|
>
|
|
239
324
|
Edit
|
|
240
325
|
</UniversalLink>
|
|
326
|
+
{canEditPage && (
|
|
327
|
+
<Button
|
|
328
|
+
className="tertiary mini"
|
|
329
|
+
onClick={() => handleCopy(item)}
|
|
330
|
+
>
|
|
331
|
+
Copy
|
|
332
|
+
</Button>
|
|
333
|
+
)}
|
|
241
334
|
</div>
|
|
242
335
|
<div className="workflow-progress">
|
|
243
336
|
<ProgressWorkflow
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import configureStore from 'redux-mock-store';
|
|
5
|
+
import { Provider } from 'react-redux';
|
|
6
|
+
import NISListingView from './NISListingView';
|
|
7
|
+
|
|
8
|
+
const mockUseSelector = jest.fn();
|
|
9
|
+
|
|
10
|
+
jest.mock('react-redux', () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
...jest.requireActual('react-redux'),
|
|
13
|
+
useSelector: (selector) => mockUseSelector(selector),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock(
|
|
17
|
+
'@plone/volto/components/manage/UniversalLink/UniversalLink',
|
|
18
|
+
() =>
|
|
19
|
+
function MockUniversalLink({ href, children, className }) {
|
|
20
|
+
return (
|
|
21
|
+
<a href={href} className={className}>
|
|
22
|
+
{children}
|
|
23
|
+
</a>
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
jest.mock(
|
|
29
|
+
'@eeacms/volto-marine-policy/components/theme/ProgressWorkflow/ProgressWorkflow',
|
|
30
|
+
() => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: () => <div data-testid="progress-workflow" />,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const mockStore = configureStore();
|
|
37
|
+
|
|
38
|
+
function buildItems(count = 3) {
|
|
39
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
40
|
+
'@id': `/marine/item-${i + 1}`,
|
|
41
|
+
nis_species_name_original: `Species ${i + 1}`,
|
|
42
|
+
nis_species_name_accepted: `Species ${i + 1} accepted`,
|
|
43
|
+
nis_scientificname_accepted: `Scientificus acceptus ${i + 1}`,
|
|
44
|
+
nis_region: 'Europe',
|
|
45
|
+
nis_subregion: 'Western Europe',
|
|
46
|
+
nis_country: 'France',
|
|
47
|
+
nis_status: 'Established',
|
|
48
|
+
nis_group: 'Fish',
|
|
49
|
+
nis_year: 2020 + i,
|
|
50
|
+
nis_assigned_to: `User ${i + 1} (user${i + 1})`,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderComponent(props = {}) {
|
|
55
|
+
const { items = buildItems(), ...rest } = props;
|
|
56
|
+
const store = mockStore({
|
|
57
|
+
intl: { locale: 'en', messages: {} },
|
|
58
|
+
});
|
|
59
|
+
return render(
|
|
60
|
+
<Provider store={store}>
|
|
61
|
+
<NISListingView items={items} {...rest} />
|
|
62
|
+
</Provider>,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('NISListingView', () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
jest.clearAllMocks();
|
|
69
|
+
|
|
70
|
+
global.fetch = jest.fn(() =>
|
|
71
|
+
Promise.resolve({
|
|
72
|
+
json: () => Promise.resolve({ items: [] }),
|
|
73
|
+
ok: true,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
delete window.location;
|
|
78
|
+
window.location = {
|
|
79
|
+
href: 'http://localhost:3000/marine/test-path',
|
|
80
|
+
pathname: '/marine/test-path',
|
|
81
|
+
search: '',
|
|
82
|
+
reload: jest.fn(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
window.env = { apiPath: 'http://localhost:8080/Plone' };
|
|
86
|
+
|
|
87
|
+
global.fetch.mockImplementation((url) => {
|
|
88
|
+
if (typeof url === 'string' && url.includes('nis_experts_vocabulary')) {
|
|
89
|
+
return Promise.resolve({
|
|
90
|
+
json: () =>
|
|
91
|
+
Promise.resolve({
|
|
92
|
+
items: [
|
|
93
|
+
{ token: 'jdoe', title: 'John Doe (jdoe)' },
|
|
94
|
+
{ token: 'asmith', title: 'Alice Smith (asmith)' },
|
|
95
|
+
],
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (typeof url === 'string' && url.includes('check-nis-duplicates')) {
|
|
100
|
+
return Promise.resolve({
|
|
101
|
+
json: () =>
|
|
102
|
+
Promise.resolve({
|
|
103
|
+
duplicate_ids: [],
|
|
104
|
+
groups: [],
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return Promise.resolve({
|
|
109
|
+
json: () => Promise.resolve({}),
|
|
110
|
+
ok: true,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
mockUseSelector.mockReturnValue({
|
|
115
|
+
object: [{ id: 'edit' }],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('table rendering', () => {
|
|
120
|
+
it('renders all column headers', async () => {
|
|
121
|
+
renderComponent();
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
expect(screen.getByText('Species name original')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
expect(screen.getByText('Species name accepted')).toBeInTheDocument();
|
|
126
|
+
expect(screen.getByText('Scientific name accepted')).toBeInTheDocument();
|
|
127
|
+
expect(screen.getByText('Region')).toBeInTheDocument();
|
|
128
|
+
expect(screen.getByText('Subregion')).toBeInTheDocument();
|
|
129
|
+
expect(screen.getByText('Country')).toBeInTheDocument();
|
|
130
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
131
|
+
expect(screen.getByText('Group')).toBeInTheDocument();
|
|
132
|
+
expect(screen.getByText('Year')).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByText('Assigned to')).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('renders one row per item', async () => {
|
|
137
|
+
renderComponent({ items: buildItems(3) });
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
const rows = screen.getAllByRole('row');
|
|
142
|
+
expect(rows).toHaveLength(4);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('renders item data in correct columns', async () => {
|
|
146
|
+
const items = [
|
|
147
|
+
{
|
|
148
|
+
'@id': '/marine/item-x',
|
|
149
|
+
nis_species_name_original: 'Alienus maximus original',
|
|
150
|
+
nis_species_name_accepted: 'Alienus maximus (Linnaeus)',
|
|
151
|
+
nis_scientificname_accepted: 'Alienus maximus scientific',
|
|
152
|
+
nis_region: 'Asia',
|
|
153
|
+
nis_subregion: 'South Asia',
|
|
154
|
+
nis_country: 'India',
|
|
155
|
+
nis_status: 'Invasive',
|
|
156
|
+
nis_group: 'Plant',
|
|
157
|
+
nis_year: 2023,
|
|
158
|
+
nis_assigned_to: 'Jane Expert (jexpert)',
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
renderComponent({ items });
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
expect(
|
|
164
|
+
screen.getByText('Alienus maximus original'),
|
|
165
|
+
).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
expect(
|
|
168
|
+
screen.getByText('Alienus maximus (Linnaeus)'),
|
|
169
|
+
).toBeInTheDocument();
|
|
170
|
+
expect(
|
|
171
|
+
screen.getByText('Alienus maximus scientific'),
|
|
172
|
+
).toBeInTheDocument();
|
|
173
|
+
expect(screen.getByText('Asia')).toBeInTheDocument();
|
|
174
|
+
expect(screen.getByText('South Asia')).toBeInTheDocument();
|
|
175
|
+
expect(screen.getByText('India')).toBeInTheDocument();
|
|
176
|
+
expect(screen.getByText('Invasive')).toBeInTheDocument();
|
|
177
|
+
expect(screen.getByText('Plant')).toBeInTheDocument();
|
|
178
|
+
expect(screen.getByText('2023')).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('renders an empty table body when items is empty', async () => {
|
|
182
|
+
renderComponent({ items: [] });
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(screen.getByText('Species name original')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
const rows = screen.getAllByRole('row');
|
|
187
|
+
expect(rows).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('formatAssignedTo — rendered output', () => {
|
|
192
|
+
it('strips the (userid) suffix from assigned_to', async () => {
|
|
193
|
+
const items = [
|
|
194
|
+
{
|
|
195
|
+
...buildItems(1)[0],
|
|
196
|
+
nis_assigned_to: 'Jane Expert (jexpert)',
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
renderComponent({ items });
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
expect(screen.getByText('Jane Expert')).toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
expect(screen.queryByText('Jane Expert (jexpert)')).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('decodes Python \\U escape sequences in assigned_to', async () => {
|
|
207
|
+
const items = [
|
|
208
|
+
{
|
|
209
|
+
...buildItems(1)[0],
|
|
210
|
+
nis_assigned_to:
|
|
211
|
+
'\\U000000d6sterreich User (\\U000000d6sterreichUser)',
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
renderComponent({ items });
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
expect(screen.getByText('Österreich User')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('shows empty string when assigned_to is null', async () => {
|
|
221
|
+
const items = [
|
|
222
|
+
{
|
|
223
|
+
...buildItems(1)[0],
|
|
224
|
+
nis_assigned_to: null,
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
renderComponent({ items });
|
|
228
|
+
await waitFor(() => {
|
|
229
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
const containers = document.querySelectorAll('.assigned-to-container');
|
|
232
|
+
expect(containers.length).toBeGreaterThan(0);
|
|
233
|
+
expect(containers[0].firstChild.textContent).toBe('');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('admin controls — canEditPage', () => {
|
|
238
|
+
it('shows assign, download, and check-duplicates buttons when user can edit', async () => {
|
|
239
|
+
mockUseSelector.mockReturnValue({
|
|
240
|
+
object: [{ id: 'edit' }],
|
|
241
|
+
});
|
|
242
|
+
renderComponent();
|
|
243
|
+
await waitFor(() => {
|
|
244
|
+
expect(screen.getByText('Assign search results')).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
expect(screen.getByText('Download search results')).toBeInTheDocument();
|
|
247
|
+
expect(screen.getByText('Check duplicates')).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('hides admin controls when user cannot edit', async () => {
|
|
251
|
+
mockUseSelector.mockReturnValue({
|
|
252
|
+
object: [],
|
|
253
|
+
});
|
|
254
|
+
renderComponent();
|
|
255
|
+
await waitFor(() => {
|
|
256
|
+
expect(screen.getByText('Species name original')).toBeInTheDocument();
|
|
257
|
+
});
|
|
258
|
+
expect(
|
|
259
|
+
screen.queryByText('Assign search results'),
|
|
260
|
+
).not.toBeInTheDocument();
|
|
261
|
+
expect(
|
|
262
|
+
screen.queryByText('Download search results'),
|
|
263
|
+
).not.toBeInTheDocument();
|
|
264
|
+
expect(screen.queryByText('Check duplicates')).not.toBeInTheDocument();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('hides checkboxes and Copy button when user cannot edit', async () => {
|
|
268
|
+
mockUseSelector.mockReturnValue({
|
|
269
|
+
object: [],
|
|
270
|
+
});
|
|
271
|
+
renderComponent();
|
|
272
|
+
await waitFor(() => {
|
|
273
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
274
|
+
});
|
|
275
|
+
expect(document.querySelector('.ui.checkbox')).toBeNull();
|
|
276
|
+
expect(screen.queryByText('Copy')).not.toBeInTheDocument();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('duplicate checking', () => {
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
window.location.search = '?check-duplicates=1';
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
function mockDuplicateFetch(duplicateIds, groups) {
|
|
286
|
+
global.fetch.mockImplementation((url) => {
|
|
287
|
+
if (typeof url === 'string' && url.includes('nis_experts_vocabulary')) {
|
|
288
|
+
return Promise.resolve({
|
|
289
|
+
json: () =>
|
|
290
|
+
Promise.resolve({
|
|
291
|
+
items: [{ token: 'jdoe', title: 'John Doe (jdoe)' }],
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (typeof url === 'string' && url.includes('check-nis-duplicates')) {
|
|
296
|
+
return Promise.resolve({
|
|
297
|
+
json: () =>
|
|
298
|
+
Promise.resolve({
|
|
299
|
+
duplicate_ids: duplicateIds,
|
|
300
|
+
groups: groups,
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return Promise.resolve({
|
|
305
|
+
json: () => Promise.resolve({}),
|
|
306
|
+
ok: true,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
it('shows the duplicate info banner when check-duplicates param is set', async () => {
|
|
312
|
+
mockDuplicateFetch(
|
|
313
|
+
['/marine/item-1', '/marine/item-3'],
|
|
314
|
+
[{ id: 1 }, { id: 2 }],
|
|
315
|
+
);
|
|
316
|
+
renderComponent();
|
|
317
|
+
await waitFor(() => {
|
|
318
|
+
expect(screen.getByText(/duplicate records/)).toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
expect(screen.getByText(/2 duplicate records/)).toBeInTheDocument();
|
|
321
|
+
expect(screen.getByText(/2 groups/)).toBeInTheDocument();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('filters table to show only duplicate items', async () => {
|
|
325
|
+
mockDuplicateFetch(
|
|
326
|
+
['/marine/item-1', '/marine/item-3'],
|
|
327
|
+
[{ id: 1 }, { id: 2 }],
|
|
328
|
+
);
|
|
329
|
+
const items = buildItems(5);
|
|
330
|
+
renderComponent({ items });
|
|
331
|
+
await waitFor(() => {
|
|
332
|
+
expect(screen.getByText(/duplicate records/)).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
335
|
+
expect(screen.getByText('Species 3')).toBeInTheDocument();
|
|
336
|
+
expect(screen.queryByText('Species 2')).toBeNull();
|
|
337
|
+
expect(screen.queryByText('Species 4')).toBeNull();
|
|
338
|
+
expect(screen.queryByText('Species 5')).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('renders a Clear link that removes check-duplicates from URL', async () => {
|
|
342
|
+
mockDuplicateFetch([], []);
|
|
343
|
+
renderComponent();
|
|
344
|
+
await waitFor(() => {
|
|
345
|
+
expect(screen.getByText('Clear')).toBeInTheDocument();
|
|
346
|
+
});
|
|
347
|
+
const clearLink = screen.getByText('Clear');
|
|
348
|
+
expect(clearLink.tagName).toBe('A');
|
|
349
|
+
expect(clearLink.getAttribute('href')).not.toContain('check-duplicates');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('item selection', () => {
|
|
354
|
+
it('toggles checkbox selection', async () => {
|
|
355
|
+
renderComponent();
|
|
356
|
+
await waitFor(() => {
|
|
357
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
const checkboxes = document.querySelectorAll('.ui.checkbox input');
|
|
360
|
+
expect(checkboxes.length).toBeGreaterThan(0);
|
|
361
|
+
fireEvent.click(checkboxes[0]);
|
|
362
|
+
// Selection panel should appear after clicking a checkbox
|
|
363
|
+
await waitFor(() => {
|
|
364
|
+
expect(screen.getByText(/Assign 1 selected item/)).toBeInTheDocument();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('removes selection when checkbox is unchecked', async () => {
|
|
369
|
+
renderComponent();
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
372
|
+
});
|
|
373
|
+
const checkboxes = document.querySelectorAll('.ui.checkbox input');
|
|
374
|
+
fireEvent.click(checkboxes[0]);
|
|
375
|
+
await waitFor(() => {
|
|
376
|
+
expect(screen.getByText(/Assign 1 selected item/)).toBeInTheDocument();
|
|
377
|
+
});
|
|
378
|
+
fireEvent.click(checkboxes[0]);
|
|
379
|
+
await waitFor(() => {
|
|
380
|
+
expect(
|
|
381
|
+
screen.queryByText(/Assign 1 selected item/),
|
|
382
|
+
).not.toBeInTheDocument();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('updates count when selecting multiple items', async () => {
|
|
387
|
+
renderComponent();
|
|
388
|
+
await waitFor(() => {
|
|
389
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
390
|
+
});
|
|
391
|
+
const checkboxes = document.querySelectorAll('.ui.checkbox input');
|
|
392
|
+
fireEvent.click(checkboxes[0]);
|
|
393
|
+
fireEvent.click(checkboxes[1]);
|
|
394
|
+
await waitFor(() => {
|
|
395
|
+
expect(screen.getByText(/Assign 2 selected items/)).toBeInTheDocument();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('bulk assign panel', () => {
|
|
401
|
+
it('shows Cancel and Assign buttons after selection', async () => {
|
|
402
|
+
renderComponent();
|
|
403
|
+
await waitFor(() => {
|
|
404
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
405
|
+
});
|
|
406
|
+
const checkboxes = document.querySelectorAll('.ui.checkbox input');
|
|
407
|
+
fireEvent.click(checkboxes[0]);
|
|
408
|
+
await waitFor(() => {
|
|
409
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
410
|
+
});
|
|
411
|
+
expect(screen.getByText('Assign')).toBeInTheDocument();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('Assign button is disabled when no assignee is selected', async () => {
|
|
415
|
+
renderComponent();
|
|
416
|
+
await waitFor(() => {
|
|
417
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
418
|
+
});
|
|
419
|
+
const checkboxes = document.querySelectorAll('.ui.checkbox input');
|
|
420
|
+
fireEvent.click(checkboxes[0]);
|
|
421
|
+
await waitFor(() => {
|
|
422
|
+
expect(screen.getByText('Assign')).toBeInTheDocument();
|
|
423
|
+
});
|
|
424
|
+
const assignButton = screen.getByText('Assign').closest('button');
|
|
425
|
+
expect(assignButton).toBeDisabled();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('Cancel button clears selection', async () => {
|
|
429
|
+
renderComponent();
|
|
430
|
+
await waitFor(() => {
|
|
431
|
+
expect(screen.getByText('Species 1')).toBeInTheDocument();
|
|
432
|
+
});
|
|
433
|
+
const checkboxes = document.querySelectorAll('.ui.checkbox input');
|
|
434
|
+
fireEvent.click(checkboxes[0]);
|
|
435
|
+
await waitFor(() => {
|
|
436
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
437
|
+
});
|
|
438
|
+
fireEvent.click(screen.getByText('Cancel'));
|
|
439
|
+
await waitFor(() => {
|
|
440
|
+
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|