@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/package.json +3 -1
  3. package/src/actions/index.js +1 -0
  4. package/src/actions/navigation.js +24 -0
  5. package/src/actions/print.js +9 -1
  6. package/src/components/manage/Blocks/ContextNavigation/variations/Accordion.jsx +42 -35
  7. package/src/components/manage/Blocks/LayoutSettings/LayoutSettingsEdit.test.jsx +383 -0
  8. package/src/components/manage/Blocks/Title/variations/WebReportPage.test.jsx +232 -0
  9. package/src/components/theme/Banner/View.jsx +11 -92
  10. package/src/components/theme/PrintLoader/PrintLoader.jsx +56 -0
  11. package/src/components/theme/PrintLoader/PrintLoader.test.jsx +91 -0
  12. package/src/components/theme/PrintLoader/style.less +12 -0
  13. package/src/components/theme/WebReport/WebReportSectionView.test.jsx +462 -0
  14. package/src/components/theme/Widgets/ImageViewWidget.test.jsx +26 -0
  15. package/src/components/theme/Widgets/NavigationBehaviorWidget.jsx +601 -0
  16. package/src/components/theme/Widgets/NavigationBehaviorWidget.test.jsx +507 -0
  17. package/src/components/theme/Widgets/SimpleArrayWidget.jsx +183 -0
  18. package/src/components/theme/Widgets/SimpleArrayWidget.test.jsx +283 -0
  19. package/src/constants/ActionTypes.js +2 -0
  20. package/src/customizations/volto/components/manage/History/History.diff +207 -0
  21. package/src/customizations/volto/components/manage/History/History.jsx +444 -0
  22. package/src/customizations/volto/components/theme/Comments/Comments.jsx +9 -2
  23. package/src/customizations/volto/components/theme/Comments/Comments.test.jsx +4 -4
  24. package/src/customizations/volto/components/theme/Comments/comments.less +16 -0
  25. package/src/customizations/volto/components/theme/Header/Header.jsx +60 -1
  26. package/src/customizations/volto/components/theme/View/DefaultView.jsx +42 -33
  27. package/src/customizations/volto/helpers/Html/Html.jsx +212 -0
  28. package/src/customizations/volto/helpers/Html/Readme.md +1 -0
  29. package/src/customizations/volto/server.jsx +375 -0
  30. package/src/helpers/loadLazyImages.js +11 -0
  31. package/src/helpers/loadLazyImages.test.js +22 -0
  32. package/src/helpers/setupPrintView.js +134 -0
  33. package/src/helpers/setupPrintView.test.js +49 -0
  34. package/src/index.js +11 -1
  35. package/src/index.test.js +6 -0
  36. package/src/middleware/voltoCustom.test.js +282 -0
  37. package/src/reducers/index.js +2 -1
  38. package/src/reducers/navigation/navigation.js +47 -0
  39. package/src/reducers/navigation/navigation.test.js +348 -0
  40. package/src/reducers/navigation.js +55 -0
  41. package/src/reducers/print.js +18 -8
  42. package/src/reducers/print.test.js +117 -0
@@ -0,0 +1,283 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { Provider } from 'react-intl-redux';
4
+ import configureStore from 'redux-mock-store';
5
+ import SimpleArrayWidget from './SimpleArrayWidget';
6
+
7
+ // Add jest-dom matchers
8
+ import '@testing-library/jest-dom';
9
+
10
+ const mockStore = configureStore();
11
+
12
+ describe('SimpleArrayWidget', () => {
13
+ let store;
14
+ const mockOnChange = jest.fn();
15
+
16
+ beforeEach(() => {
17
+ store = mockStore({
18
+ intl: {
19
+ locale: 'en',
20
+ messages: {
21
+ Add: 'Add',
22
+ Remove: 'Remove',
23
+ },
24
+ },
25
+ });
26
+ mockOnChange.mockClear();
27
+ });
28
+
29
+ const defaultProps = {
30
+ id: 'test-field',
31
+ title: 'Test Field',
32
+ description: 'Test description',
33
+ value: [],
34
+ onChange: mockOnChange,
35
+ items: {
36
+ minimum: 1,
37
+ maximum: 5,
38
+ },
39
+ };
40
+
41
+ it('renders without crashing', () => {
42
+ const { container } = render(
43
+ <Provider store={store}>
44
+ <SimpleArrayWidget {...defaultProps} />
45
+ </Provider>,
46
+ );
47
+ expect(container).toBeTruthy();
48
+ });
49
+
50
+ it('displays the title', () => {
51
+ render(
52
+ <Provider store={store}>
53
+ <SimpleArrayWidget {...defaultProps} />
54
+ </Provider>,
55
+ );
56
+
57
+ expect(screen.getByText('Test Field')).toBeInTheDocument();
58
+ });
59
+
60
+ it('shows Add button when no input is shown', () => {
61
+ render(
62
+ <Provider store={store}>
63
+ <SimpleArrayWidget {...defaultProps} />
64
+ </Provider>,
65
+ );
66
+
67
+ expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
68
+ });
69
+
70
+ it('shows input field when Add button is clicked', () => {
71
+ render(
72
+ <Provider store={store}>
73
+ <SimpleArrayWidget {...defaultProps} />
74
+ </Provider>,
75
+ );
76
+
77
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
78
+
79
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
80
+ expect(screen.getByTitle('Add')).toBeInTheDocument();
81
+ expect(screen.getByTitle('Cancel')).toBeInTheDocument();
82
+ });
83
+
84
+ it('adds a valid number to the array', () => {
85
+ render(
86
+ <Provider store={store}>
87
+ <SimpleArrayWidget {...defaultProps} />
88
+ </Provider>,
89
+ );
90
+
91
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
92
+
93
+ const input = screen.getByRole('spinbutton');
94
+ fireEvent.change(input, { target: { value: '3' } });
95
+ fireEvent.click(screen.getByTitle('Add'));
96
+
97
+ expect(mockOnChange).toHaveBeenCalledWith('test-field', [3]);
98
+ });
99
+
100
+ it('does not add invalid numbers', () => {
101
+ render(
102
+ <Provider store={store}>
103
+ <SimpleArrayWidget {...defaultProps} />
104
+ </Provider>,
105
+ );
106
+
107
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
108
+
109
+ const input = screen.getByRole('spinbutton');
110
+ fireEvent.change(input, { target: { value: '10' } }); // Above maximum
111
+ fireEvent.click(screen.getByTitle('Add'));
112
+
113
+ expect(mockOnChange).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it('cancels input when cancel button is clicked', () => {
117
+ render(
118
+ <Provider store={store}>
119
+ <SimpleArrayWidget {...defaultProps} />
120
+ </Provider>,
121
+ );
122
+
123
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
124
+
125
+ const input = screen.getByRole('spinbutton');
126
+ fireEvent.change(input, { target: { value: '2' } });
127
+ fireEvent.click(screen.getByTitle('Cancel'));
128
+
129
+ // Input should be hidden after cancel
130
+ expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument();
131
+ expect(mockOnChange).not.toHaveBeenCalled();
132
+ });
133
+
134
+ it('displays existing values as labels', () => {
135
+ const props = {
136
+ ...defaultProps,
137
+ value: [1, 3, 5],
138
+ };
139
+
140
+ render(
141
+ <Provider store={store}>
142
+ <SimpleArrayWidget {...props} />
143
+ </Provider>,
144
+ );
145
+
146
+ expect(screen.getByText('1')).toBeInTheDocument();
147
+ expect(screen.getByText('3')).toBeInTheDocument();
148
+ expect(screen.getByText('5')).toBeInTheDocument();
149
+ });
150
+
151
+ it('removes value when × is clicked', () => {
152
+ const props = {
153
+ ...defaultProps,
154
+ value: [1, 3, 5],
155
+ };
156
+
157
+ render(
158
+ <Provider store={store}>
159
+ <SimpleArrayWidget {...props} />
160
+ </Provider>,
161
+ );
162
+
163
+ const removeButtons = screen.getAllByText('×');
164
+ fireEvent.click(removeButtons[1]); // Remove second item (value 3)
165
+
166
+ expect(mockOnChange).toHaveBeenCalledWith('test-field', [1, 5]);
167
+ });
168
+
169
+ it('passes null when removing last item', () => {
170
+ const props = {
171
+ ...defaultProps,
172
+ value: [1],
173
+ };
174
+
175
+ render(
176
+ <Provider store={store}>
177
+ <SimpleArrayWidget {...props} />
178
+ </Provider>,
179
+ );
180
+
181
+ const removeButton = screen.getByText('×');
182
+ fireEvent.click(removeButton);
183
+
184
+ expect(mockOnChange).toHaveBeenCalledWith('test-field', null);
185
+ });
186
+
187
+ it('handles non-array value gracefully', () => {
188
+ const props = {
189
+ ...defaultProps,
190
+ value: null,
191
+ };
192
+
193
+ const { container } = render(
194
+ <Provider store={store}>
195
+ <SimpleArrayWidget {...props} />
196
+ </Provider>,
197
+ );
198
+
199
+ expect(container).toBeTruthy();
200
+ expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
201
+ });
202
+
203
+ it('displays numbers correctly', () => {
204
+ const props = {
205
+ ...defaultProps,
206
+ value: [1, 2, 3],
207
+ };
208
+
209
+ render(
210
+ <Provider store={store}>
211
+ <SimpleArrayWidget {...props} />
212
+ </Provider>,
213
+ );
214
+
215
+ expect(screen.getByText('1')).toBeInTheDocument();
216
+ expect(screen.getByText('2')).toBeInTheDocument();
217
+ expect(screen.getByText('3')).toBeInTheDocument();
218
+ });
219
+
220
+ it('uses default min/max values when not provided', () => {
221
+ const props = {
222
+ ...defaultProps,
223
+ items: undefined,
224
+ };
225
+
226
+ render(
227
+ <Provider store={store}>
228
+ <SimpleArrayWidget {...props} />
229
+ </Provider>,
230
+ );
231
+
232
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
233
+
234
+ const input = screen.getByRole('spinbutton');
235
+ expect(input).toHaveAttribute('min', '1');
236
+ expect(input).toHaveAttribute('max', '10');
237
+ expect(input).toHaveAttribute('placeholder', '1-10');
238
+ });
239
+
240
+ it('uses custom min/max values when provided', () => {
241
+ render(
242
+ <Provider store={store}>
243
+ <SimpleArrayWidget {...defaultProps} />
244
+ </Provider>,
245
+ );
246
+
247
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
248
+
249
+ const input = screen.getByRole('spinbutton');
250
+ expect(input).toHaveAttribute('min', '1');
251
+ expect(input).toHaveAttribute('max', '5');
252
+ expect(input).toHaveAttribute('placeholder', '1-5');
253
+ });
254
+
255
+ it('disables add button when input is empty', () => {
256
+ render(
257
+ <Provider store={store}>
258
+ <SimpleArrayWidget {...defaultProps} />
259
+ </Provider>,
260
+ );
261
+
262
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
263
+
264
+ const addButton = screen.getByTitle('Add');
265
+ expect(addButton).toBeDisabled();
266
+ });
267
+
268
+ it('does not add empty or whitespace-only values', () => {
269
+ render(
270
+ <Provider store={store}>
271
+ <SimpleArrayWidget {...defaultProps} />
272
+ </Provider>,
273
+ );
274
+
275
+ fireEvent.click(screen.getByRole('button', { name: /add/i }));
276
+
277
+ const input = screen.getByRole('spinbutton');
278
+ fireEvent.change(input, { target: { value: ' ' } });
279
+ fireEvent.click(screen.getByTitle('Add'));
280
+
281
+ expect(mockOnChange).not.toHaveBeenCalled();
282
+ });
283
+ });
@@ -4,3 +4,5 @@
4
4
  */
5
5
 
6
6
  export const SET_ISPRINT = 'SET_ISPRINT';
7
+ export const SET_PRINT_LOADING = 'SET_PRINT_LOADING';
8
+ export const GET_NAVIGATION_SETTINGS = 'GET_NAVIGATION_SETTINGS';
@@ -0,0 +1,207 @@
1
+ --- History.jsx.original 2025-08-11 09:00:00.000000000 +0000
2
+ +++ History.jsx.modified 2025-08-11 09:30:00.000000000 +0000
3
+ @@ -1,6 +1,8 @@
4
+ /**
5
+ * History component.
6
+ * @module components/manage/History/History
7
+ + * Customized to enable complex versioning
8
+ + * https://taskman.eionet.europa.eu/issues/289335?issue_count=4&issue_position=1&next_issue_id=285635
9
+ */
10
+
11
+ import React, { Component } from 'react';
12
+ @@ -13,9 +15,11 @@
13
+ Container as SemanticContainer,
14
+ Dropdown,
15
+ Icon,
16
+ + Message,
17
+ Segment,
18
+ Table,
19
+ } from 'semantic-ui-react';
20
+ -import { concat, map, reverse, find } from 'lodash';
21
+ +import concat from 'lodash/concat';
22
+ +import map from 'lodash/map';
23
+ +import reverse from 'lodash/reverse';
24
+ +import find from 'lodash/find';
25
+ -import { Portal } from 'react-portal';
26
+ +import { createPortal } from 'react-dom';
27
+ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
28
+ -import { asyncConnect } from '@plone/volto/helpers';
29
+ +import { asyncConnect } from '@plone/volto/helpers/AsyncConnect';
30
+
31
+ -import {
32
+ - FormattedDate,
33
+ - Icon as IconNext,
34
+ - Toolbar,
35
+ - Forbidden,
36
+ - Unauthorized,
37
+ -} from '@plone/volto/components';
38
+ -import { getHistory, revertHistory, listActions } from '@plone/volto/actions';
39
+ +import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
40
+ +import IconNext from '@plone/volto/components/theme/Icon/Icon';
41
+ +import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
42
+ +import Forbidden from '@plone/volto/components/theme/Forbidden/Forbidden';
43
+ +import Unauthorized from '@plone/volto/components/theme/Unauthorized/Unauthorized';
44
+ +import {
45
+ + getHistory,
46
+ + revertHistory,
47
+ +} from '@plone/volto/actions/history/history';
48
+ +import { listActions } from '@plone/volto/actions/actions/actions';
49
+ import { getBaseUrl } from '@plone/volto/helpers';
50
+ import config from '@plone/volto/registry';
51
+
52
+ @@ -34,6 +38,22 @@
53
+ history: {
54
+ id: 'History',
55
+ defaultMessage: 'History',
56
+ },
57
+ + newerVersionAvailable: {
58
+ + id: 'Newer version available',
59
+ + defaultMessage: 'Newer version available',
60
+ + },
61
+ + thereIsNewerVersionAt: {
62
+ + id: 'There is a newer version at {link}',
63
+ + defaultMessage: 'There is a newer version at {link}',
64
+ + },
65
+ + olderVersionAvailable: {
66
+ + id: 'Older version available',
67
+ + defaultMessage: 'Older version available',
68
+ + },
69
+ + thereIsOlderVersionAt: {
70
+ + id: 'There is an older version at {link}',
71
+ + defaultMessage: 'There is an older version at {link}',
72
+ + },
73
+ });
74
+
75
+ /**
76
+ @@ -138,6 +158,25 @@
77
+ />
78
+ </Segment>
79
+ + {this.props.content?.copied_to && (
80
+ + <Message info icon attached="top">
81
+ + <Icon name="arrow right" />
82
+ + <Message.Content>
83
+ + <Message.Header>
84
+ + <FormattedMessage {...messages.newerVersionAvailable} />
85
+ + </Message.Header>
86
+ + <FormattedMessage
87
+ + {...messages.thereIsNewerVersionAt}
88
+ + values={{
89
+ + link: (
90
+ + <Link to={new URL(this.props.content.copied_to).pathname}>
91
+ + {new URL(this.props.content.copied_to).pathname
92
+ + .split('/')
93
+ + .pop() || 'newer version'}
94
+ + </Link>
95
+ + ),
96
+ + }}
97
+ + />
98
+ + </Message.Content>
99
+ + </Message>
100
+ + )}
101
+ - <Table selectable compact singleLine attached>
102
+ + <Table
103
+ + selectable
104
+ + compact
105
+ + attached
106
+ + style={{
107
+ + tableLayout: 'fixed',
108
+ + width: '100%',
109
+ + wordWrap: 'break-word',
110
+ + }}
111
+ + >
112
+ <Table.Header>
113
+ <Table.Row>
114
+ - <Table.HeaderCell width={1}>
115
+ + <Table.HeaderCell style={{ width: '5%' }}>
116
+ <FormattedMessage
117
+ id="History Version Number"
118
+ defaultMessage="#"
119
+ />
120
+ </Table.HeaderCell>
121
+ - <Table.HeaderCell width={4}>
122
+ + <Table.HeaderCell style={{ width: '25%' }}>
123
+ <FormattedMessage id="What" defaultMessage="What" />
124
+ </Table.HeaderCell>
125
+ - <Table.HeaderCell width={4}>
126
+ + <Table.HeaderCell style={{ width: '15%' }}>
127
+ <FormattedMessage id="Who" defaultMessage="Who" />
128
+ </Table.HeaderCell>
129
+ - <Table.HeaderCell width={4}>
130
+ + <Table.HeaderCell style={{ width: '15%' }}>
131
+ <FormattedMessage id="When" defaultMessage="When" />
132
+ </Table.HeaderCell>
133
+ - <Table.HeaderCell width={4}>
134
+ + <Table.HeaderCell style={{ width: '25%' }}>
135
+ <FormattedMessage
136
+ id="Change Note"
137
+ defaultMessage="Change Note"
138
+ />
139
+ </Table.HeaderCell>
140
+ - <Table.HeaderCell />
141
+ + <Table.HeaderCell style={{ width: '15%' }} />
142
+ </Table.Row>
143
+ </Table.Header>
144
+ @@ -280,14 +319,33 @@
145
+ </Table.Body>
146
+ </Table>
147
+ + {this.props.content?.copied_from && (
148
+ + <Message warning icon attached="bottom">
149
+ + <Icon name="arrow left" />
150
+ + <Message.Content>
151
+ + <Message.Header>
152
+ + <FormattedMessage {...messages.olderVersionAvailable} />
153
+ + </Message.Header>
154
+ + <FormattedMessage
155
+ + {...messages.thereIsOlderVersionAt}
156
+ + values={{
157
+ + link: (
158
+ + <Link
159
+ + to={new URL(this.props.content.copied_from).pathname}
160
+ + >
161
+ + {new URL(this.props.content.copied_from).pathname
162
+ + .split('/')
163
+ + .pop() || 'older version'}
164
+ + </Link>
165
+ + ),
166
+ + }}
167
+ + />
168
+ + </Message.Content>
169
+ + </Message>
170
+ + )}
171
+ </Segment.Group>
172
+ - {this.state.isClient && (
173
+ - <Portal node={document.getElementById('toolbar')}>
174
+ + {this.state.isClient &&
175
+ + createPortal(
176
+ <Toolbar
177
+ pathname={this.props.pathname}
178
+ hideDefaultViewButtons
179
+ @@ -302,8 +360,9 @@
180
+ </Link>
181
+ }
182
+ - />
183
+ - </Portal>
184
+ - )}
185
+ + />,
186
+ + document.getElementById('toolbar'),
187
+ + )}
188
+ </Container>
189
+ );
190
+ }
191
+ @@ -314,7 +373,7 @@
192
+ asyncConnect([
193
+ {
194
+ key: 'actions',
195
+ - // Dispatch async/await to make the operation syncronous, otherwise it returns
196
+ + // Dispatch async/await to make the operation synchronous, otherwise it returns
197
+ // before the promise is resolved
198
+ promise: async ({ location, store: { dispatch } }) =>
199
+ await dispatch(listActions(getBaseUrl(location.pathname))),
200
+ @@ -327,6 +386,7 @@
201
+ entries: state.history.entries,
202
+ pathname: props.location.pathname,
203
+ title: state.content.data?.title,
204
+ + content: state.content.data,
205
+ revertRequest: state.history.revert,
206
+ }),
207
+ { getHistory, revertHistory },