@eeacms/volto-eea-website-theme 3.18.1 → 3.19.1

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,26 @@ 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.19.1](https://github.com/eea/volto-eea-website-theme/compare/3.19.0...3.19.1) - 24 February 2026
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix: History in indicators when copied_to or copied_from are not valid URLs - refs #297683 [Teodor Voicu - [`ccd5dad`](https://github.com/eea/volto-eea-website-theme/commit/ccd5dad4c4700d43d0697a1fb49df83d4ee96c83)]
12
+
13
+ #### :house: Internal changes
14
+
15
+ - chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`b15b302`](https://github.com/eea/volto-eea-website-theme/commit/b15b302ff88ec6afa6901ee7e1726cc4e04a8739)]
16
+ - chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`36f2e1e`](https://github.com/eea/volto-eea-website-theme/commit/36f2e1e176471b3753d77ab8aaf0cba577b43b36)]
17
+
18
+ ### [3.19.0](https://github.com/eea/volto-eea-website-theme/compare/3.18.1...3.19.0) - 11 February 2026
19
+
20
+ #### :house: Internal changes
21
+
22
+ - style: Automated code fix [eea-jenkins - [`26e2fec`](https://github.com/eea/volto-eea-website-theme/commit/26e2fec974c85b5221730215650d049e3deefbad)]
23
+
24
+ #### :hammer_and_wrench: Others
25
+
26
+ - fix formating [Teodor - [`7131bcf`](https://github.com/eea/volto-eea-website-theme/commit/7131bcf73f9b561c87599e590941419bf6c23c67)]
7
27
  ### [3.18.1](https://github.com/eea/volto-eea-website-theme/compare/3.18.0...3.18.1) - 20 January 2026
8
28
 
9
29
  #### :bug: Bug Fixes
package/README.md CHANGED
@@ -3,16 +3,16 @@
3
3
  [![Releases](https://img.shields.io/github/v/release/eea/volto-eea-website-theme)](https://github.com/eea/volto-eea-website-theme/releases)
4
4
 
5
5
  [![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-eea-website-theme%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-website-theme/job/master/display/redirect)
6
- [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-master&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-master)
7
- [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-master&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-master)
8
- [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-master&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-master)
9
- [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-master&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-master)
6
+ [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme)
7
+ [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme)
8
+ [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme)
9
+ [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme)
10
10
 
11
11
  [![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-eea-website-theme%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-website-theme/job/develop/display/redirect)
12
- [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-develop)
13
- [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-develop)
14
- [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-develop)
15
- [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme-develop)
12
+ [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&branch=develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme&branch=develop)
13
+ [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&branch=develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme&branch=develop)
14
+ [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&branch=develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme&branch=develop)
15
+ [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-website-theme&branch=develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-website-theme&branch=develop)
16
16
 
17
17
  EEA Website [Volto](https://github.com/plone/volto) Theme
18
18
 
@@ -1,4 +1,3 @@
1
- version: "3"
2
1
  services:
3
2
  backend:
4
3
  image: eeacms/plone-backend
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "3.18.1",
3
+ "version": "3.19.1",
4
4
  "description": "@eeacms/volto-eea-website-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -183,7 +183,7 @@ const View = (props) => {
183
183
  />
184
184
  )}
185
185
  {rssLinks?.map((rssLink, index) => (
186
- <>
186
+ <React.Fragment key={rssLink.href || index}>
187
187
  <Helmet
188
188
  link={[
189
189
  {
@@ -205,7 +205,7 @@ const View = (props) => {
205
205
  href={rssLink.href}
206
206
  target="_blank"
207
207
  />
208
- </>
208
+ </React.Fragment>
209
209
  ))}
210
210
  </>
211
211
  }
@@ -4,16 +4,16 @@ const ContributorsViewWidget = ({ value, content, children, className }) => {
4
4
  const resolvedValue = content?.contributors_fullname || value || [];
5
5
  return resolvedValue ? (
6
6
  <span className={cx(className, 'array', 'widget')}>
7
- {resolvedValue.map((item, key) => (
8
- <>
9
- {key ? ', ' : ''}
10
- <span key={item.token || item.title || item}>
11
- {children
12
- ? children(item.title || item.token || item)
13
- : item.title || item.token || item}
7
+ {resolvedValue.map((item, index) => {
8
+ const label = item?.title || item?.token || item;
9
+ const key = `${label}-${index}`;
10
+ return (
11
+ <span key={key}>
12
+ {index ? ', ' : ''}
13
+ {children ? children(label) : label}
14
14
  </span>
15
- </>
16
- ))}
15
+ );
16
+ })}
17
17
  </span>
18
18
  ) : (
19
19
  ''
@@ -4,16 +4,16 @@ const CreatorsViewWidget = ({ value, content, children, className }) => {
4
4
  const resolvedValue = content?.creators_fullname || value || [];
5
5
  return resolvedValue ? (
6
6
  <span className={cx(className, 'array', 'widget')}>
7
- {resolvedValue.map((item, key) => (
8
- <>
9
- {key ? ', ' : ''}
10
- <span key={item.token || item.title || item}>
11
- {children
12
- ? children(item.title || item.token || item)
13
- : item.title || item.token || item}
7
+ {resolvedValue.map((item, index) => {
8
+ const label = item?.title || item?.token || item;
9
+ const key = `${label}-${index}`;
10
+ return (
11
+ <span key={key}>
12
+ {index ? ', ' : ''}
13
+ {children ? children(label) : label}
14
14
  </span>
15
- </>
16
- ))}
15
+ );
16
+ })}
17
17
  </span>
18
18
  ) : (
19
19
  ''
@@ -101,7 +101,7 @@ jest.mock('@plone/volto/components/manage/Widgets/ObjectWidget', () => {
101
101
 
102
102
  // Mock semantic-ui-react components
103
103
  jest.mock('semantic-ui-react', () => {
104
- const MockAccordion = ({ children, ...props }) => (
104
+ const MockAccordion = ({ children, fluid, styled, ...props }) => (
105
105
  <div className="ui accordion" data-testid="accordion" {...props}>
106
106
  {children}
107
107
  </div>
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Slate Table block's View component.
3
+ * @module volto-slate/blocks/Table/View
4
+ */
5
+
6
+ import React, { useState, useMemo } from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import { Table } from 'semantic-ui-react';
9
+ import { map } from 'lodash';
10
+ import {
11
+ serializeNodes,
12
+ serializeNodesToText,
13
+ } from '@plone/volto-slate/editor/render';
14
+ import { Node } from 'slate';
15
+
16
+ // TODO: loading LESS files with `volto-slate/...` paths does not work currently
17
+ import '../../editor/plugins/Table/less/public.less';
18
+
19
+ /**
20
+ * Slate Table block's View class.
21
+ * @class View
22
+ * @extends Component
23
+ * @param {object} data The table data to render as a table.
24
+ */
25
+ const View = ({ data }) => {
26
+ const [state, setState] = useState({
27
+ column: null,
28
+ direction: null,
29
+ });
30
+
31
+ const headers = useMemo(() => {
32
+ return data.table.rows?.[0]?.cells;
33
+ }, [data.table.rows]);
34
+
35
+ const rows = useMemo(() => {
36
+ const items = [];
37
+ if (!data.table.rows) return [];
38
+ data.table.rows.forEach((row, index) => {
39
+ if (index > 0) {
40
+ items[index] = [];
41
+ row.cells.forEach((cell, cellIndex) => {
42
+ items[index][cellIndex] = {
43
+ ...cell,
44
+ value:
45
+ cell.value && Node.string({ children: cell.value }).length > 0
46
+ ? serializeNodes(cell.value)
47
+ : '\u00A0',
48
+ valueText:
49
+ cell.value && Node.string({ children: cell.value }).length > 0
50
+ ? serializeNodesToText(cell.value)
51
+ : '\u00A0',
52
+ align: cell.value?.[0]?.align || 'left',
53
+ };
54
+ });
55
+ }
56
+ });
57
+ return items;
58
+ }, [data.table.rows]);
59
+
60
+ const sortedRows = useMemo(() => {
61
+ if (state.column === null) return Object.keys(rows);
62
+ return Object.keys(rows).sort((a, b) => {
63
+ const a_text = rows[a][state.column].valueText;
64
+ const b_text = rows[b][state.column].valueText;
65
+ if (state.direction === 'ascending' ? a_text < b_text : a_text > b_text) {
66
+ return -1;
67
+ }
68
+ if (state.direction === 'ascending' ? a_text > b_text : a_text < b_text) {
69
+ return 1;
70
+ }
71
+ return 0;
72
+ });
73
+ }, [state, rows]);
74
+
75
+ const handleSort = (index) => {
76
+ if (!data.table.sortable) return;
77
+ setState({
78
+ column: index,
79
+ direction:
80
+ state.column !== index
81
+ ? 'ascending'
82
+ : state.direction === 'ascending'
83
+ ? 'descending'
84
+ : 'ascending',
85
+ });
86
+ };
87
+
88
+ return (
89
+ <>
90
+ {data && data.table && (
91
+ <Table
92
+ fixed={data.table.fixed}
93
+ compact={data.table.compact}
94
+ basic={data.table.basic ? 'very' : false}
95
+ celled={data.table.celled}
96
+ inverted={data.table.inverted}
97
+ striped={data.table.striped}
98
+ sortable={data.table.sortable}
99
+ className="slate-table-block"
100
+ >
101
+ {!data.table.hideHeaders ? (
102
+ <Table.Header>
103
+ <Table.Row>
104
+ {headers.map((cell, index) => (
105
+ <Table.HeaderCell
106
+ key={cell.key}
107
+ textAlign={cell.value?.[0]?.align || 'left'}
108
+ verticalAlign="middle"
109
+ tabIndex={data.table.sortable ? '0' : '-1'}
110
+ sorted={state.column === index ? state.direction : null}
111
+ onClick={() => {
112
+ handleSort(index);
113
+ }}
114
+ onKeyDown={(e) => {
115
+ if (e.key === 'Enter' || e.key === ' ') {
116
+ e.preventDefault();
117
+ handleSort(index);
118
+ }
119
+ }}
120
+ aria-sort={
121
+ state.column === index ? state.direction : 'none'
122
+ }
123
+ >
124
+ {cell.value &&
125
+ Node.string({ children: cell.value }).length > 0
126
+ ? serializeNodes(cell.value)
127
+ : '\u00A0'}
128
+ </Table.HeaderCell>
129
+ ))}
130
+ </Table.Row>
131
+ </Table.Header>
132
+ ) : (
133
+ ''
134
+ )}
135
+ <Table.Body>
136
+ {map(sortedRows, (row) => (
137
+ <Table.Row key={row}>
138
+ {map(rows[row], (cell) => (
139
+ <Table.Cell
140
+ key={cell.key}
141
+ textAlign={cell.align}
142
+ verticalAlign="middle"
143
+ >
144
+ {cell.value}
145
+ </Table.Cell>
146
+ ))}
147
+ </Table.Row>
148
+ ))}
149
+ </Table.Body>
150
+ </Table>
151
+ )}
152
+ </>
153
+ );
154
+ };
155
+
156
+ /**
157
+ * Property types.
158
+ * @property {Object} propTypes Property types.
159
+ * @static
160
+ */
161
+ View.propTypes = {
162
+ data: PropTypes.objectOf(PropTypes.any).isRequired,
163
+ };
164
+
165
+ export default View;
@@ -0,0 +1,43 @@
1
+ @brown: #826a6a;
2
+
3
+ table.slate-table {
4
+ width: 100%;
5
+ border: 0.025rem solid @brown;
6
+ margin-top: 1rem;
7
+ margin-bottom: 1rem;
8
+ border-collapse: collapse;
9
+ border-spacing: 30px;
10
+
11
+ th,
12
+ td {
13
+ padding: 0.5rem;
14
+ border: 0.05rem solid @brown;
15
+ vertical-align: middle;
16
+ }
17
+
18
+ th {
19
+ border-bottom: 0.15rem solid @brown;
20
+ }
21
+ }
22
+
23
+ table.slate-table-block {
24
+ th p,
25
+ td p {
26
+ margin-top: 0;
27
+ margin-bottom: 0;
28
+ }
29
+ }
30
+
31
+ table.slate-table-block.sortable {
32
+ tr th {
33
+ position: relative;
34
+ padding-right: 1.5em;
35
+
36
+ &::after {
37
+ position: absolute !important;
38
+ top: 50%;
39
+ right: 0.5em;
40
+ transform: translateY(-50%);
41
+ }
42
+ }
43
+ }
@@ -42,6 +42,14 @@ import config from '@plone/volto/registry';
42
42
 
43
43
  import backSVG from '@plone/volto/icons/back.svg';
44
44
 
45
+ const getPathname = (url) => {
46
+ try {
47
+ return new URL(url).pathname;
48
+ } catch {
49
+ return typeof url === 'string' && url.startsWith('/') ? url : null;
50
+ }
51
+ };
52
+
45
53
  const messages = defineMessages({
46
54
  back: {
47
55
  id: 'Back',
@@ -213,32 +221,31 @@ class History extends Component {
213
221
  defaultMessage="You can view the history of your item below."
214
222
  />
215
223
  </Segment>
216
- {this.props.content?.copied_to && (
217
- <Message info icon attached="top">
218
- <Icon name="arrow right" />
219
- <Message.Content>
220
- <Message.Header>
221
- <FormattedMessage {...messages.newerVersionAvailable} />
222
- </Message.Header>
223
- <FormattedMessage
224
- {...messages.thereIsNewerVersionAt}
225
- values={{
226
- link: (
227
- <a
228
- href={`${
229
- new URL(this.props.content.copied_to).pathname
230
- }/historyview`}
231
- >
232
- {new URL(this.props.content.copied_to).pathname
233
- .split('/')
234
- .pop() || 'newer version'}
235
- </a>
236
- ),
237
- }}
238
- />
239
- </Message.Content>
240
- </Message>
241
- )}
224
+ {(() => {
225
+ const copiedToPath = getPathname(this.props.content?.copied_to);
226
+ return (
227
+ copiedToPath && (
228
+ <Message info icon attached="top">
229
+ <Icon name="arrow right" />
230
+ <Message.Content>
231
+ <Message.Header>
232
+ <FormattedMessage {...messages.newerVersionAvailable} />
233
+ </Message.Header>
234
+ <FormattedMessage
235
+ {...messages.thereIsNewerVersionAt}
236
+ values={{
237
+ link: (
238
+ <a href={`${copiedToPath}/historyview`}>
239
+ {copiedToPath.split('/').pop() || 'newer version'}
240
+ </a>
241
+ ),
242
+ }}
243
+ />
244
+ </Message.Content>
245
+ </Message>
246
+ )
247
+ );
248
+ })()}
242
249
  <Table
243
250
  selectable
244
251
  compact
@@ -371,32 +378,31 @@ class History extends Component {
371
378
  ))}
372
379
  </Table.Body>
373
380
  </Table>
374
- {this.props.content?.copied_from && (
375
- <Message warning icon attached="bottom">
376
- <Icon name="arrow left" />
377
- <Message.Content>
378
- <Message.Header>
379
- <FormattedMessage {...messages.olderVersionAvailable} />
380
- </Message.Header>
381
- <FormattedMessage
382
- {...messages.thereIsOlderVersionAt}
383
- values={{
384
- link: (
385
- <a
386
- href={`${
387
- new URL(this.props.content.copied_from).pathname
388
- }/historyview`}
389
- >
390
- {new URL(this.props.content.copied_from).pathname
391
- .split('/')
392
- .pop() || 'older version'}
393
- </a>
394
- ),
395
- }}
396
- />
397
- </Message.Content>
398
- </Message>
399
- )}
381
+ {(() => {
382
+ const copiedFromPath = getPathname(this.props.content?.copied_from);
383
+ return (
384
+ copiedFromPath && (
385
+ <Message warning icon attached="bottom">
386
+ <Icon name="arrow left" />
387
+ <Message.Content>
388
+ <Message.Header>
389
+ <FormattedMessage {...messages.olderVersionAvailable} />
390
+ </Message.Header>
391
+ <FormattedMessage
392
+ {...messages.thereIsOlderVersionAt}
393
+ values={{
394
+ link: (
395
+ <a href={`${copiedFromPath}/historyview`}>
396
+ {copiedFromPath.split('/').pop() || 'older version'}
397
+ </a>
398
+ ),
399
+ }}
400
+ />
401
+ </Message.Content>
402
+ </Message>
403
+ )
404
+ );
405
+ })()}
400
406
  </Segment.Group>
401
407
  {this.state.isClient &&
402
408
  createPortal(
@@ -438,8 +438,8 @@ class Comments extends Component {
438
438
  open={this.state.showEdit}
439
439
  onCancel={this.onEditCancel}
440
440
  onOk={this.onEditOk}
441
- id={this.state.editId}
442
- text={this.state.editText}
441
+ id={this.state.editId ?? ''}
442
+ text={this.state.editText ?? ''}
443
443
  />
444
444
  {permissions.can_reply && (
445
445
  <div id="comment-add-id">
@@ -26,6 +26,21 @@ beforeAll(
26
26
  await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
27
27
  );
28
28
 
29
+ jest.mock('semantic-ui-react', () => {
30
+ const React = require('react');
31
+ const actual = jest.requireActual('semantic-ui-react');
32
+ const Dropdown = React.forwardRef(({ text, trigger, ...props }, ref) => {
33
+ const resolvedTrigger =
34
+ trigger || (typeof text === 'function' ? text() : text);
35
+ return <actual.Dropdown {...props} ref={ref} trigger={resolvedTrigger} />;
36
+ });
37
+ Dropdown.Menu = actual.Dropdown.Menu;
38
+ Dropdown.Item = actual.Dropdown.Item;
39
+ Dropdown.Header = actual.Dropdown.Header;
40
+ Dropdown.Divider = actual.Dropdown.Divider;
41
+ return { ...actual, Dropdown };
42
+ });
43
+
29
44
  describe('Header', () => {
30
45
  it('renders a header component with homepage_inverse_view layout', () => {
31
46
  const store = mockStore({
@@ -383,24 +383,11 @@ describe('setupPrintView', () => {
383
383
  });
384
384
  document.body.appendChild(iframe);
385
385
 
386
- // Mock the iframe to never fire load event, causing timeout
387
- const originalSetTimeout = global.setTimeout;
388
- let timeoutCallback;
389
- global.setTimeout = jest.fn((callback, delay) => {
390
- if (delay === 5000) {
391
- // This is the iframe timeout
392
- timeoutCallback = callback;
393
- }
394
- return originalSetTimeout(callback, delay);
395
- });
396
-
397
386
  await act(async () => {
398
387
  setupPrintView(dispatch);
399
388
 
400
389
  // Trigger the iframe timeout to simulate error condition
401
- if (timeoutCallback) {
402
- timeoutCallback();
403
- }
390
+ jest.advanceTimersByTime(5000);
404
391
 
405
392
  for (let i = 0; i < 10; i++) {
406
393
  jest.runAllTimers();
@@ -410,9 +397,6 @@ describe('setupPrintView', () => {
410
397
 
411
398
  // Should still call print even with timeout
412
399
  expect(window.print).toHaveBeenCalled();
413
-
414
- // Restore original setTimeout
415
- global.setTimeout = originalSetTimeout;
416
400
  });
417
401
 
418
402
  it('prevents multiple resets of print state', async () => {