@eeacms/volto-eea-website-theme 3.7.0 → 3.9.0
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 +19 -2
- package/package.json +3 -1
- package/src/actions/index.js +1 -0
- package/src/actions/navigation.js +24 -0
- package/src/actions/print.js +9 -1
- package/src/components/manage/Blocks/ContextNavigation/variations/Accordion.jsx +42 -35
- package/src/components/manage/Blocks/LayoutSettings/LayoutSettingsEdit.test.jsx +383 -0
- package/src/components/manage/Blocks/Title/variations/WebReportPage.test.jsx +232 -0
- package/src/components/theme/Banner/View.jsx +11 -92
- package/src/components/theme/PrintLoader/PrintLoader.jsx +56 -0
- package/src/components/theme/PrintLoader/PrintLoader.test.jsx +91 -0
- package/src/components/theme/PrintLoader/style.less +12 -0
- package/src/components/theme/WebReport/WebReportSectionView.test.jsx +462 -0
- package/src/components/theme/Widgets/ImageViewWidget.test.jsx +26 -0
- package/src/components/theme/Widgets/NavigationBehaviorWidget.jsx +601 -0
- package/src/components/theme/Widgets/NavigationBehaviorWidget.test.jsx +507 -0
- package/src/components/theme/Widgets/SimpleArrayWidget.jsx +183 -0
- package/src/components/theme/Widgets/SimpleArrayWidget.test.jsx +283 -0
- package/src/constants/ActionTypes.js +2 -0
- package/src/customizations/volto/components/manage/History/History.diff +207 -0
- package/src/customizations/volto/components/manage/History/History.jsx +444 -0
- package/src/customizations/volto/components/theme/Comments/Comments.jsx +9 -2
- package/src/customizations/volto/components/theme/Comments/Comments.test.jsx +4 -4
- package/src/customizations/volto/components/theme/Comments/comments.less +16 -0
- package/src/customizations/volto/components/theme/Header/Header.jsx +60 -1
- package/src/customizations/volto/components/theme/View/DefaultView.jsx +42 -33
- package/src/customizations/volto/helpers/Html/Html.jsx +212 -0
- package/src/customizations/volto/helpers/Html/Readme.md +1 -0
- package/src/customizations/volto/server.jsx +375 -0
- package/src/helpers/loadLazyImages.js +11 -0
- package/src/helpers/loadLazyImages.test.js +22 -0
- package/src/helpers/setupPrintView.js +134 -0
- package/src/helpers/setupPrintView.test.js +49 -0
- package/src/index.js +11 -1
- package/src/index.test.js +6 -0
- package/src/middleware/voltoCustom.test.js +282 -0
- package/src/reducers/index.js +2 -1
- package/src/reducers/navigation/navigation.js +47 -0
- package/src/reducers/navigation/navigation.test.js +348 -0
- package/src/reducers/navigation.js +55 -0
- package/src/reducers/print.js +18 -8
- package/src/reducers/print.test.js +117 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import { Provider } from 'react-intl-redux';
|
|
4
|
+
import configureStore from 'redux-mock-store';
|
|
5
|
+
import thunk from 'redux-thunk';
|
|
6
|
+
|
|
7
|
+
// Add jest-dom matchers
|
|
8
|
+
import '@testing-library/jest-dom';
|
|
9
|
+
|
|
10
|
+
const mockStore = configureStore([thunk]);
|
|
11
|
+
|
|
12
|
+
// Mock the getNavigation action
|
|
13
|
+
const mockGetNavigation = jest.fn(() => ({ type: 'GET_NAVIGATION' }));
|
|
14
|
+
jest.mock('@plone/volto/actions', () => ({
|
|
15
|
+
getNavigation: mockGetNavigation,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock the config
|
|
19
|
+
jest.mock('@plone/volto/registry', () => ({
|
|
20
|
+
default: {
|
|
21
|
+
settings: {
|
|
22
|
+
menuItemsLayouts: {
|
|
23
|
+
'/test-route-1': {
|
|
24
|
+
hideChildrenFromNavigation: false,
|
|
25
|
+
menuItemChildrenListColumns: [2, 3],
|
|
26
|
+
menuItemColumns: ['two wide column', 'three wide column'],
|
|
27
|
+
},
|
|
28
|
+
'/test-route-2': {
|
|
29
|
+
hideChildrenFromNavigation: true,
|
|
30
|
+
menuItemColumns: ['four wide column'],
|
|
31
|
+
},
|
|
32
|
+
'*': {
|
|
33
|
+
hideChildrenFromNavigation: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Mock uuid
|
|
41
|
+
jest.mock('uuid', () => ({
|
|
42
|
+
v4: () => 'test-uuid-123',
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Mock Volto components
|
|
46
|
+
jest.mock('@plone/volto/components', () => ({
|
|
47
|
+
Icon: ({ name, size }) => (
|
|
48
|
+
<div data-testid="icon" data-name={name} data-size={size} />
|
|
49
|
+
),
|
|
50
|
+
FormFieldWrapper: ({ children, ...props }) => (
|
|
51
|
+
<div data-testid="form-field-wrapper" {...props}>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
// Mock ObjectWidget
|
|
58
|
+
jest.mock('@plone/volto/components/manage/Widgets/ObjectWidget', () => {
|
|
59
|
+
return function MockObjectWidget({ id, schema, value, onChange }) {
|
|
60
|
+
return (
|
|
61
|
+
<div data-testid="object-widget" data-id={id}>
|
|
62
|
+
{schema.properties.hideChildrenFromNavigation && (
|
|
63
|
+
<div>
|
|
64
|
+
<label>Hide Children From Navigation</label>
|
|
65
|
+
<input
|
|
66
|
+
type="checkbox"
|
|
67
|
+
checked={value.hideChildrenFromNavigation || false}
|
|
68
|
+
onChange={(e) =>
|
|
69
|
+
onChange(id, {
|
|
70
|
+
...value,
|
|
71
|
+
hideChildrenFromNavigation: e.target.checked,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
{schema.properties.menuItemColumns && (
|
|
78
|
+
<div>
|
|
79
|
+
<label>Menu Item Columns</label>
|
|
80
|
+
<div data-testid="menu-item-columns">
|
|
81
|
+
{(value.menuItemColumns || []).map((col, index) => (
|
|
82
|
+
<span key={index}>{col}</span>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
{schema.properties.menuItemChildrenListColumns && (
|
|
88
|
+
<div>
|
|
89
|
+
<label>Menu Item Children List Columns</label>
|
|
90
|
+
<div data-testid="menu-item-children-columns">
|
|
91
|
+
{(value.menuItemChildrenListColumns || []).map((col, index) => (
|
|
92
|
+
<span key={index}>{col}</span>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Mock semantic-ui-react components
|
|
103
|
+
jest.mock('semantic-ui-react', () => {
|
|
104
|
+
const MockAccordion = ({ children, ...props }) => (
|
|
105
|
+
<div className="ui accordion" data-testid="accordion" {...props}>
|
|
106
|
+
{children}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
MockAccordion.Title = ({ children, onClick, active, index }) => (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className={`title ${active ? 'active' : ''}`}
|
|
114
|
+
onClick={(e) => onClick(e, { index })}
|
|
115
|
+
data-testid="accordion-title"
|
|
116
|
+
>
|
|
117
|
+
{children}
|
|
118
|
+
</button>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
MockAccordion.Content = ({ children, active }) => (
|
|
122
|
+
<div
|
|
123
|
+
className={`content ${active ? 'active' : ''}`}
|
|
124
|
+
data-testid="accordion-content"
|
|
125
|
+
>
|
|
126
|
+
{active && children}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
Accordion: MockAccordion,
|
|
132
|
+
Button: ({ children, ...props }) => <button {...props}>{children}</button>,
|
|
133
|
+
Segment: ({ children }) => <div data-testid="segment">{children}</div>,
|
|
134
|
+
Form: {
|
|
135
|
+
Field: ({ children }) => <div data-testid="form-field">{children}</div>,
|
|
136
|
+
},
|
|
137
|
+
Dropdown: ({ children }) => <div data-testid="dropdown">{children}</div>,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Mock SVG imports
|
|
142
|
+
jest.mock('@plone/volto/icons/up-key.svg', () => 'up-icon');
|
|
143
|
+
jest.mock('@plone/volto/icons/down-key.svg', () => 'down-icon');
|
|
144
|
+
|
|
145
|
+
describe('NavigationBehaviorWidget', () => {
|
|
146
|
+
let store;
|
|
147
|
+
let NavigationBehaviorWidget;
|
|
148
|
+
|
|
149
|
+
const mockOnChange = jest.fn();
|
|
150
|
+
|
|
151
|
+
const defaultNavigationItems = [
|
|
152
|
+
{
|
|
153
|
+
'@id': 'http://localhost:3000/test-route-1',
|
|
154
|
+
title: 'Test Route 1',
|
|
155
|
+
url: '/test-route-1',
|
|
156
|
+
id: 'test-route-1',
|
|
157
|
+
portal_type: 'Document',
|
|
158
|
+
items: [
|
|
159
|
+
{
|
|
160
|
+
'@id': 'http://localhost:3000/test-route-1/child',
|
|
161
|
+
title: 'Child Route',
|
|
162
|
+
url: '/test-route-1/child',
|
|
163
|
+
id: 'child',
|
|
164
|
+
portal_type: 'Document',
|
|
165
|
+
items: [],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
'@id': 'http://localhost:3000/test-route-2',
|
|
171
|
+
title: 'Test Route 2',
|
|
172
|
+
url: '/test-route-2',
|
|
173
|
+
id: 'test-route-2',
|
|
174
|
+
portal_type: 'Folder',
|
|
175
|
+
items: [],
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
jest.clearAllMocks();
|
|
181
|
+
store = mockStore({
|
|
182
|
+
intl: {
|
|
183
|
+
locale: 'en',
|
|
184
|
+
messages: {
|
|
185
|
+
'Load Main Navigation Routes': 'Load Main Navigation Routes',
|
|
186
|
+
'Hide Children From Navigation': 'Hide Children From Navigation',
|
|
187
|
+
'Menu Item Children List Columns': 'Menu Item Children List Columns',
|
|
188
|
+
'Menu Item Columns': 'Menu Item Columns',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
navigation: {
|
|
192
|
+
items: defaultNavigationItems,
|
|
193
|
+
loaded: true,
|
|
194
|
+
},
|
|
195
|
+
vocabularies: {},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
NavigationBehaviorWidget = require('./NavigationBehaviorWidget').default;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('renders with navigation data', () => {
|
|
202
|
+
render(
|
|
203
|
+
<Provider store={store}>
|
|
204
|
+
<NavigationBehaviorWidget
|
|
205
|
+
id="test"
|
|
206
|
+
value="{}"
|
|
207
|
+
onChange={mockOnChange}
|
|
208
|
+
/>
|
|
209
|
+
</Provider>,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
213
|
+
expect(screen.getByText('Test Route 2')).toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('dispatches getNavigation when not loaded', () => {
|
|
217
|
+
const storeNotLoaded = mockStore({
|
|
218
|
+
...store.getState(),
|
|
219
|
+
navigation: { items: [], loaded: false },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
render(
|
|
223
|
+
<Provider store={storeNotLoaded}>
|
|
224
|
+
<NavigationBehaviorWidget
|
|
225
|
+
id="test"
|
|
226
|
+
value="{}"
|
|
227
|
+
onChange={mockOnChange}
|
|
228
|
+
/>
|
|
229
|
+
</Provider>,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(mockGetNavigation).toHaveBeenCalledWith('', 1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('handles accordion expansion', async () => {
|
|
236
|
+
render(
|
|
237
|
+
<Provider store={store}>
|
|
238
|
+
<NavigationBehaviorWidget
|
|
239
|
+
id="test"
|
|
240
|
+
value="{}"
|
|
241
|
+
onChange={mockOnChange}
|
|
242
|
+
/>
|
|
243
|
+
</Provider>,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const accordionTitles = screen.getAllByTestId('accordion-title');
|
|
247
|
+
fireEvent.click(accordionTitles[0]);
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(
|
|
251
|
+
screen.getByText('Hide Children From Navigation'),
|
|
252
|
+
).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('handles JSON parsing correctly', () => {
|
|
257
|
+
render(
|
|
258
|
+
<Provider store={store}>
|
|
259
|
+
<NavigationBehaviorWidget
|
|
260
|
+
id="test"
|
|
261
|
+
value='{"test": {"hideChildrenFromNavigation": false}}'
|
|
262
|
+
onChange={mockOnChange}
|
|
263
|
+
/>
|
|
264
|
+
</Provider>,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('handles invalid JSON gracefully', () => {
|
|
271
|
+
render(
|
|
272
|
+
<Provider store={store}>
|
|
273
|
+
<NavigationBehaviorWidget
|
|
274
|
+
id="test"
|
|
275
|
+
value="invalid json"
|
|
276
|
+
onChange={mockOnChange}
|
|
277
|
+
/>
|
|
278
|
+
</Provider>,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('handles object values correctly', () => {
|
|
285
|
+
render(
|
|
286
|
+
<Provider store={store}>
|
|
287
|
+
<NavigationBehaviorWidget
|
|
288
|
+
id="test"
|
|
289
|
+
value={{ '/test': { hideChildrenFromNavigation: false } }}
|
|
290
|
+
onChange={mockOnChange}
|
|
291
|
+
/>
|
|
292
|
+
</Provider>,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('handles null values correctly', () => {
|
|
299
|
+
render(
|
|
300
|
+
<Provider store={store}>
|
|
301
|
+
<NavigationBehaviorWidget
|
|
302
|
+
id="test"
|
|
303
|
+
value={null}
|
|
304
|
+
onChange={mockOnChange}
|
|
305
|
+
/>
|
|
306
|
+
</Provider>,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('auto-populates settings from config when no settings exist', async () => {
|
|
313
|
+
render(
|
|
314
|
+
<Provider store={store}>
|
|
315
|
+
<NavigationBehaviorWidget
|
|
316
|
+
id="test"
|
|
317
|
+
value="{}"
|
|
318
|
+
onChange={mockOnChange}
|
|
319
|
+
/>
|
|
320
|
+
</Provider>,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
await waitFor(() => {
|
|
324
|
+
expect(mockOnChange).toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('handles empty navigation data', () => {
|
|
329
|
+
const emptyStore = mockStore({
|
|
330
|
+
...store.getState(),
|
|
331
|
+
navigation: { items: [], loaded: true },
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const { container } = render(
|
|
335
|
+
<Provider store={emptyStore}>
|
|
336
|
+
<NavigationBehaviorWidget
|
|
337
|
+
id="test"
|
|
338
|
+
value="{}"
|
|
339
|
+
onChange={mockOnChange}
|
|
340
|
+
/>
|
|
341
|
+
</Provider>,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(
|
|
345
|
+
container.querySelector('.navigation-behavior-widget'),
|
|
346
|
+
).toBeInTheDocument();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('toggles accordion active state correctly', async () => {
|
|
350
|
+
render(
|
|
351
|
+
<Provider store={store}>
|
|
352
|
+
<NavigationBehaviorWidget
|
|
353
|
+
id="test"
|
|
354
|
+
value="{}"
|
|
355
|
+
onChange={mockOnChange}
|
|
356
|
+
/>
|
|
357
|
+
</Provider>,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const accordionTitles = screen.getAllByTestId('accordion-title');
|
|
361
|
+
|
|
362
|
+
// Click to expand
|
|
363
|
+
fireEvent.click(accordionTitles[0]);
|
|
364
|
+
await waitFor(() => {
|
|
365
|
+
expect(accordionTitles[0]).toHaveClass('active');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Click again to collapse
|
|
369
|
+
fireEvent.click(accordionTitles[0]);
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(accordionTitles[0]).not.toHaveClass('active');
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('processes routes with config settings correctly', () => {
|
|
376
|
+
const storeWithConfig = mockStore({
|
|
377
|
+
...store.getState(),
|
|
378
|
+
navigation: {
|
|
379
|
+
items: [
|
|
380
|
+
{
|
|
381
|
+
'@id': 'http://localhost:3000/test-route-1',
|
|
382
|
+
title: 'Test Route 1',
|
|
383
|
+
url: '/test-route-1',
|
|
384
|
+
items: [],
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
loaded: true,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
render(
|
|
392
|
+
<Provider store={storeWithConfig}>
|
|
393
|
+
<NavigationBehaviorWidget
|
|
394
|
+
id="test"
|
|
395
|
+
value="{}"
|
|
396
|
+
onChange={mockOnChange}
|
|
397
|
+
/>
|
|
398
|
+
</Provider>,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('filters to show only level 0 routes', () => {
|
|
405
|
+
render(
|
|
406
|
+
<Provider store={store}>
|
|
407
|
+
<NavigationBehaviorWidget
|
|
408
|
+
id="test"
|
|
409
|
+
value="{}"
|
|
410
|
+
onChange={mockOnChange}
|
|
411
|
+
/>
|
|
412
|
+
</Provider>,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Should only show main routes, not child routes
|
|
416
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
417
|
+
expect(screen.getByText('Test Route 2')).toBeInTheDocument();
|
|
418
|
+
expect(screen.queryByText('Child Route')).not.toBeInTheDocument();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('handles routes without @id using fallback uuid', () => {
|
|
422
|
+
const storeWithoutIds = mockStore({
|
|
423
|
+
...store.getState(),
|
|
424
|
+
navigation: {
|
|
425
|
+
items: [
|
|
426
|
+
{
|
|
427
|
+
title: 'Route Without ID',
|
|
428
|
+
url: '/no-id-route',
|
|
429
|
+
items: [],
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
loaded: true,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
render(
|
|
437
|
+
<Provider store={storeWithoutIds}>
|
|
438
|
+
<NavigationBehaviorWidget
|
|
439
|
+
id="test"
|
|
440
|
+
value="{}"
|
|
441
|
+
onChange={mockOnChange}
|
|
442
|
+
/>
|
|
443
|
+
</Provider>,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(screen.getByText('Route Without ID')).toBeInTheDocument();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('merges config and saved settings correctly', async () => {
|
|
450
|
+
const existingSettings = {
|
|
451
|
+
'http://localhost:3000/test-route-1': {
|
|
452
|
+
hideChildrenFromNavigation: true,
|
|
453
|
+
menuItemColumns: [1, 2],
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
render(
|
|
458
|
+
<Provider store={store}>
|
|
459
|
+
<NavigationBehaviorWidget
|
|
460
|
+
id="test"
|
|
461
|
+
value={JSON.stringify(existingSettings)}
|
|
462
|
+
onChange={mockOnChange}
|
|
463
|
+
/>
|
|
464
|
+
</Provider>,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const accordionTitles = screen.getAllByTestId('accordion-title');
|
|
468
|
+
fireEvent.click(accordionTitles[0]);
|
|
469
|
+
|
|
470
|
+
await waitFor(() => {
|
|
471
|
+
expect(
|
|
472
|
+
screen.getByText('Hide Children From Navigation'),
|
|
473
|
+
).toBeInTheDocument();
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('handles routes with hasChildren property', () => {
|
|
478
|
+
render(
|
|
479
|
+
<Provider store={store}>
|
|
480
|
+
<NavigationBehaviorWidget
|
|
481
|
+
id="test"
|
|
482
|
+
value="{}"
|
|
483
|
+
onChange={mockOnChange}
|
|
484
|
+
/>
|
|
485
|
+
</Provider>,
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Test Route 1 has children, Test Route 2 doesn't
|
|
489
|
+
expect(screen.getByText('Test Route 1')).toBeInTheDocument();
|
|
490
|
+
expect(screen.getByText('Test Route 2')).toBeInTheDocument();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('displays route paths in accordion titles', () => {
|
|
494
|
+
render(
|
|
495
|
+
<Provider store={store}>
|
|
496
|
+
<NavigationBehaviorWidget
|
|
497
|
+
id="test"
|
|
498
|
+
value="{}"
|
|
499
|
+
onChange={mockOnChange}
|
|
500
|
+
/>
|
|
501
|
+
</Provider>,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(screen.getByText('(/test-route-1)')).toBeInTheDocument();
|
|
505
|
+
expect(screen.getByText('(/test-route-2)')).toBeInTheDocument();
|
|
506
|
+
});
|
|
507
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Array Widget that allows duplicates and direct typing
|
|
3
|
+
* @module components/manage/Widgets/SimpleArrayWidget
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { defineMessages, injectIntl } from 'react-intl';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
import { Button, Input, Label } from 'semantic-ui-react';
|
|
10
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
11
|
+
|
|
12
|
+
const messages = defineMessages({
|
|
13
|
+
add: {
|
|
14
|
+
id: 'Add',
|
|
15
|
+
defaultMessage: 'Add',
|
|
16
|
+
},
|
|
17
|
+
remove: {
|
|
18
|
+
id: 'Remove',
|
|
19
|
+
defaultMessage: 'Remove',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const SimpleArrayWidget = (props) => {
|
|
24
|
+
const { id, value: rawValue, onChange, items, intl } = props;
|
|
25
|
+
const [newValue, setNewValue] = useState('');
|
|
26
|
+
const [showInput, setShowInput] = useState(false);
|
|
27
|
+
|
|
28
|
+
// Get min/max from schema
|
|
29
|
+
const minValue = items?.minimum || 1;
|
|
30
|
+
const maxValue = items?.maximum || 10;
|
|
31
|
+
|
|
32
|
+
// Ensure value is always an array
|
|
33
|
+
const value = Array.isArray(rawValue) ? rawValue : [];
|
|
34
|
+
|
|
35
|
+
const handleAdd = () => {
|
|
36
|
+
if (newValue.trim() !== '') {
|
|
37
|
+
const numValue = parseInt(newValue.trim());
|
|
38
|
+
if (!isNaN(numValue) && numValue >= minValue && numValue <= maxValue) {
|
|
39
|
+
const newArray = [...value, numValue];
|
|
40
|
+
onChange(id, newArray);
|
|
41
|
+
setNewValue('');
|
|
42
|
+
setShowInput(false); // Hide input after adding
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleShowInput = () => {
|
|
48
|
+
setShowInput(true);
|
|
49
|
+
// Focus input after it appears
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
const input = document.querySelector(`#${id}-input`);
|
|
52
|
+
if (input) input.focus();
|
|
53
|
+
}, 100);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleCancel = () => {
|
|
57
|
+
setNewValue('');
|
|
58
|
+
setShowInput(false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleRemove = (index) => {
|
|
62
|
+
const newArray = value.filter((_, i) => i !== index);
|
|
63
|
+
// If array becomes empty, pass null to remove the field from data completely
|
|
64
|
+
onChange(id, newArray.length === 0 ? null : newArray);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleKeyPress = (e) => {
|
|
68
|
+
if (e.key === 'Enter') {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
handleAdd();
|
|
71
|
+
} else if (e.key === 'Escape') {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
handleCancel();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<FormFieldWrapper {...props}>
|
|
79
|
+
<div
|
|
80
|
+
style={{
|
|
81
|
+
display: 'flex',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
flexWrap: 'wrap',
|
|
84
|
+
gap: '0.5rem',
|
|
85
|
+
paddingTop: '0.5rem',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{/* Display current values as compact labels */}
|
|
89
|
+
{value.map((item, index) => {
|
|
90
|
+
// Force conversion to number for display (in case it comes as string)
|
|
91
|
+
const displayValue =
|
|
92
|
+
typeof item === 'string' ? parseInt(item) || item : item;
|
|
93
|
+
return (
|
|
94
|
+
<Label key={index} size="small" color="blue">
|
|
95
|
+
{displayValue}
|
|
96
|
+
<Label.Detail
|
|
97
|
+
as="a"
|
|
98
|
+
onClick={() => handleRemove(index)}
|
|
99
|
+
style={{ cursor: 'pointer' }}
|
|
100
|
+
title={intl.formatMessage(messages.remove)}
|
|
101
|
+
>
|
|
102
|
+
×
|
|
103
|
+
</Label.Detail>
|
|
104
|
+
</Label>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
|
|
108
|
+
{/* Input field (conditionally shown) */}
|
|
109
|
+
{showInput && (
|
|
110
|
+
<div
|
|
111
|
+
style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}
|
|
112
|
+
>
|
|
113
|
+
<Input
|
|
114
|
+
id={`${id}-input`}
|
|
115
|
+
type="number"
|
|
116
|
+
min={minValue}
|
|
117
|
+
max={maxValue}
|
|
118
|
+
value={newValue}
|
|
119
|
+
onChange={(e) => setNewValue(e.target.value)}
|
|
120
|
+
onKeyPress={handleKeyPress}
|
|
121
|
+
placeholder={`${minValue}-${maxValue}`}
|
|
122
|
+
size="mini"
|
|
123
|
+
style={{ width: '80px' }}
|
|
124
|
+
/>
|
|
125
|
+
<Button
|
|
126
|
+
type="button"
|
|
127
|
+
primary
|
|
128
|
+
size="mini"
|
|
129
|
+
icon="check"
|
|
130
|
+
onClick={handleAdd}
|
|
131
|
+
disabled={!newValue.trim()}
|
|
132
|
+
title={intl.formatMessage(messages.add)}
|
|
133
|
+
/>
|
|
134
|
+
<Button
|
|
135
|
+
type="button"
|
|
136
|
+
size="mini"
|
|
137
|
+
icon="close"
|
|
138
|
+
onClick={handleCancel}
|
|
139
|
+
title="Cancel"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Add button (shown when input is hidden) */}
|
|
145
|
+
{!showInput && (
|
|
146
|
+
<Button
|
|
147
|
+
type="button"
|
|
148
|
+
basic
|
|
149
|
+
size="mini"
|
|
150
|
+
icon="plus"
|
|
151
|
+
content={intl.formatMessage(messages.add)}
|
|
152
|
+
onClick={handleShowInput}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{props.description && (
|
|
158
|
+
<div
|
|
159
|
+
style={{
|
|
160
|
+
fontSize: '0.85em',
|
|
161
|
+
color: '#767676',
|
|
162
|
+
marginTop: '0.5rem',
|
|
163
|
+
fontStyle: 'italic',
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{props.description}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</FormFieldWrapper>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
SimpleArrayWidget.propTypes = {
|
|
174
|
+
id: PropTypes.string.isRequired,
|
|
175
|
+
title: PropTypes.string.isRequired,
|
|
176
|
+
description: PropTypes.string,
|
|
177
|
+
value: PropTypes.arrayOf(PropTypes.number),
|
|
178
|
+
onChange: PropTypes.func.isRequired,
|
|
179
|
+
items: PropTypes.object,
|
|
180
|
+
intl: PropTypes.object.isRequired,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default injectIntl(SimpleArrayWidget);
|