@eeacms/volto-eea-website-theme 4.3.4 → 4.3.5

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,16 @@ 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
+ ### [4.3.5](https://github.com/eea/volto-eea-website-theme/compare/4.3.4...4.3.5) - 28 May 2026
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix: Cannot read properties of null title - refs #304017 [Alin Voinea - [`7bb0a9f`](https://github.com/eea/volto-eea-website-theme/commit/7bb0a9fa97bfb993f8a35514175d372860206900)]
12
+
13
+ #### :house: Internal changes
14
+
15
+ - style: Automated code fix [eea-jenkins - [`dc7ea42`](https://github.com/eea/volto-eea-website-theme/commit/dc7ea426f540957ffa08bd45ddac3a78b31f278c)]
16
+
7
17
  ### [4.3.4](https://github.com/eea/volto-eea-website-theme/compare/4.3.3...4.3.4) - 28 May 2026
8
18
 
9
19
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "4.3.4",
3
+ "version": "4.3.5",
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",
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Sharing container.
3
+ * @module components/manage/Sharing/Sharing
4
+ */
5
+ import React, { Component } from 'react';
6
+ import PropTypes from 'prop-types';
7
+ import { Plug, Pluggable } from '@plone/volto/components/manage/Pluggable';
8
+ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
9
+ import { connect } from 'react-redux';
10
+ import { compose } from 'redux';
11
+ import { Link, withRouter } from 'react-router-dom';
12
+ import find from 'lodash/find';
13
+ import isEqual from 'lodash/isEqual';
14
+ import map from 'lodash/map';
15
+ import { createPortal } from 'react-dom';
16
+ import {
17
+ Button,
18
+ Checkbox,
19
+ Container as SemanticContainer,
20
+ Form,
21
+ Icon as IconOld,
22
+ Input,
23
+ Segment,
24
+ Table,
25
+ } from 'semantic-ui-react';
26
+ import jwtDecode from 'jwt-decode';
27
+ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
28
+
29
+ import {
30
+ updateSharing,
31
+ getSharing,
32
+ } from '@plone/volto/actions/sharing/sharing';
33
+ import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
34
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
35
+ import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
36
+ import Toast from '@plone/volto/components/manage/Toast/Toast';
37
+ import { toast } from 'react-toastify';
38
+ import config from '@plone/volto/registry';
39
+
40
+ import aheadSVG from '@plone/volto/icons/ahead.svg';
41
+ import clearSVG from '@plone/volto/icons/clear.svg';
42
+ import backSVG from '@plone/volto/icons/back.svg';
43
+
44
+ const messages = defineMessages({
45
+ searchForUserOrGroup: {
46
+ id: 'Search for user or group',
47
+ defaultMessage: 'Search for user or group',
48
+ },
49
+ search: {
50
+ id: 'Search',
51
+ defaultMessage: 'Search',
52
+ },
53
+ inherit: {
54
+ id: 'Inherit permissions from higher levels',
55
+ defaultMessage: 'Inherit permissions from higher levels',
56
+ },
57
+ save: {
58
+ id: 'Save',
59
+ defaultMessage: 'Save',
60
+ },
61
+ cancel: {
62
+ id: 'Cancel',
63
+ defaultMessage: 'Cancel',
64
+ },
65
+ back: {
66
+ id: 'Back',
67
+ defaultMessage: 'Back',
68
+ },
69
+ sharing: {
70
+ id: 'Sharing',
71
+ defaultMessage: 'Sharing',
72
+ },
73
+ user: {
74
+ id: 'User',
75
+ defaultMessage: 'User',
76
+ },
77
+ group: {
78
+ id: 'Group',
79
+ defaultMessage: 'Group',
80
+ },
81
+ globalRole: {
82
+ id: 'Global role',
83
+ defaultMessage: 'Global role',
84
+ },
85
+ inheritedValue: {
86
+ id: 'Inherited value',
87
+ defaultMessage: 'Inherited value',
88
+ },
89
+ permissionsUpdated: {
90
+ id: 'Permissions updated',
91
+ defaultMessage: 'Permissions updated',
92
+ },
93
+ permissionsUpdatedSuccessfully: {
94
+ id: 'Permissions have been updated successfully',
95
+ defaultMessage: 'Permissions have been updated successfully',
96
+ },
97
+ assignNewRoles: {
98
+ id: 'Assign the {role} role to {entry}',
99
+ defaultMessage: 'Assign the {role} role to {entry}',
100
+ },
101
+ });
102
+
103
+ /**
104
+ * SharingComponent class.
105
+ * @class SharingComponent
106
+ * @extends Component
107
+ */
108
+ class SharingComponent extends Component {
109
+ /**
110
+ * Property types.
111
+ * @property {Object} propTypes Property types.
112
+ * @static
113
+ */
114
+ static propTypes = {
115
+ updateSharing: PropTypes.func.isRequired,
116
+ getSharing: PropTypes.func.isRequired,
117
+ updateRequest: PropTypes.shape({
118
+ loading: PropTypes.bool,
119
+ loaded: PropTypes.bool,
120
+ }).isRequired,
121
+ pathname: PropTypes.string.isRequired,
122
+ entries: PropTypes.arrayOf(
123
+ PropTypes.shape({
124
+ id: PropTypes.string,
125
+ login: PropTypes.string,
126
+ roles: PropTypes.object,
127
+ title: PropTypes.string,
128
+ type: PropTypes.string,
129
+ }),
130
+ ).isRequired,
131
+ available_roles: PropTypes.arrayOf(PropTypes.object).isRequired,
132
+ inherit: PropTypes.bool,
133
+ title: PropTypes.string.isRequired,
134
+ login: PropTypes.string,
135
+ };
136
+
137
+ /**
138
+ * Default properties
139
+ * @property {Object} defaultProps Default properties.
140
+ * @static
141
+ */
142
+ static defaultProps = {
143
+ inherit: null,
144
+ login: '',
145
+ };
146
+
147
+ /**
148
+ * Constructor
149
+ * @method constructor
150
+ * @param {Object} props Component properties
151
+ * @constructs Sharing
152
+ */
153
+ constructor(props) {
154
+ super(props);
155
+ this.onCancel = this.onCancel.bind(this);
156
+ this.onChange = this.onChange.bind(this);
157
+ this.onChangeSearch = this.onChangeSearch.bind(this);
158
+ this.onSearch = this.onSearch.bind(this);
159
+ this.onSubmit = this.onSubmit.bind(this);
160
+ this.onToggleInherit = this.onToggleInherit.bind(this);
161
+ this.state = {
162
+ search: '',
163
+ isLoading: false,
164
+ inherit: props.inherit,
165
+ entries: props.entries,
166
+ isClient: false,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Component did mount
172
+ * @method componentDidMount
173
+ * @returns {undefined}
174
+ */
175
+ componentDidMount() {
176
+ this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
177
+ this.setState({ isClient: true });
178
+ }
179
+
180
+ /**
181
+ * Component will receive props
182
+ * @method componentWillReceiveProps
183
+ * @param {Object} nextProps Next properties
184
+ * @returns {undefined}
185
+ */
186
+ UNSAFE_componentWillReceiveProps(nextProps) {
187
+ if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) {
188
+ this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
189
+ toast.success(
190
+ <Toast
191
+ success
192
+ title={this.props.intl.formatMessage(messages.permissionsUpdated)}
193
+ content={this.props.intl.formatMessage(
194
+ messages.permissionsUpdatedSuccessfully,
195
+ )}
196
+ />,
197
+ );
198
+ }
199
+ this.setState({
200
+ inherit:
201
+ this.props.inherit === null ? nextProps.inherit : this.state.inherit,
202
+ entries: map(nextProps.entries, (entry) => {
203
+ const values = find(this.state.entries, { id: entry.id });
204
+ return {
205
+ ...entry,
206
+ roles: values ? values.roles : entry.roles,
207
+ };
208
+ }),
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Submit handler
214
+ * @method onSubmit
215
+ * @param {object} event Event object.
216
+ * @returns {undefined}
217
+ */
218
+ onSubmit(event) {
219
+ const data = { entries: [] };
220
+ event.preventDefault();
221
+ if (this.props.inherit !== this.state.inherit) {
222
+ data.inherit = this.state.inherit;
223
+ }
224
+ for (let i = 0; i < this.props.entries.length; i += 1) {
225
+ if (!isEqual(this.props.entries[i].roles, this.state.entries[i].roles)) {
226
+ data.entries.push({
227
+ id: this.state.entries[i].id,
228
+ type: this.state.entries[i].type,
229
+ roles: this.state.entries[i].roles,
230
+ });
231
+ }
232
+ }
233
+ this.props.updateSharing(getBaseUrl(this.props.pathname), data);
234
+ }
235
+
236
+ /**
237
+ * Search handler
238
+ * @method onSearch
239
+ * @param {object} event Event object.
240
+ * @returns {undefined}
241
+ */
242
+ onSearch(event) {
243
+ event.preventDefault();
244
+ this.setState({ isLoading: true });
245
+ this.props
246
+ .getSharing(getBaseUrl(this.props.pathname), this.state.search)
247
+ .then(() => {
248
+ this.setState({ isLoading: false });
249
+ })
250
+ .catch((error) => {
251
+ this.setState({ isLoading: false });
252
+ // eslint-disable-next-line no-console
253
+ console.error('Error searching users or groups', error);
254
+ });
255
+ }
256
+
257
+ /**
258
+ * On change search handler
259
+ * @method onChangeSearch
260
+ * @param {object} event Event object.
261
+ * @returns {undefined}
262
+ */
263
+ onChangeSearch(event) {
264
+ this.setState({
265
+ search: event.target.value,
266
+ });
267
+ }
268
+
269
+ /**
270
+ * On toggle inherit handler
271
+ * @method onToggleInherit
272
+ * @returns {undefined}
273
+ */
274
+ onToggleInherit() {
275
+ this.setState((state) => ({
276
+ inherit: !state.inherit,
277
+ }));
278
+ }
279
+
280
+ /**
281
+ * On change handler
282
+ * @method onChange
283
+ * @param {object} event Event object
284
+ * @param {string} value Entry value
285
+ * @returns {undefined}
286
+ */
287
+ onChange(event, { value }) {
288
+ const [principal, role] = value.split(':');
289
+ this.setState({
290
+ entries: map(this.state.entries, (entry) => ({
291
+ ...entry,
292
+ roles:
293
+ entry.id === principal
294
+ ? {
295
+ ...entry.roles,
296
+ [role]: !entry.roles[role],
297
+ }
298
+ : entry.roles,
299
+ })),
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Cancel handler
305
+ * @method onCancel
306
+ * @returns {undefined}
307
+ */
308
+ onCancel() {
309
+ this.props.history.push(getBaseUrl(this.props.pathname));
310
+ }
311
+
312
+ /**
313
+ * Render method.
314
+ * @method render
315
+ * @returns {string} Markup for the component.
316
+ */
317
+ render() {
318
+ const Container =
319
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
320
+
321
+ return (
322
+ <Container id="page-sharing">
323
+ <Helmet title={this.props.intl.formatMessage(messages.sharing)} />
324
+ <Segment.Group raised>
325
+ <Pluggable
326
+ name="sharing-component"
327
+ params={{ isLoading: this.state.isLoading }}
328
+ />
329
+ <Plug pluggable="sharing-component" id="sharing-component-title">
330
+ <Segment className="primary">
331
+ <FormattedMessage
332
+ id="Sharing for {title}"
333
+ defaultMessage="Sharing for {title}"
334
+ values={{ title: <q>{this.props.title}</q> }}
335
+ />
336
+ </Segment>
337
+ </Plug>
338
+ <Plug
339
+ pluggable="sharing-component"
340
+ id="sharing-component-description"
341
+ >
342
+ <Segment secondary>
343
+ <FormattedMessage
344
+ id="You can control who can view and edit your item using the list below."
345
+ defaultMessage="You can control who can view and edit your item using the list below."
346
+ />
347
+ </Segment>
348
+ </Plug>
349
+ <Plug pluggable="sharing-component" id="sharing-component-search">
350
+ {({ isLoading }) => {
351
+ return (
352
+ <Segment>
353
+ <Form onSubmit={this.onSearch}>
354
+ <Form.Field>
355
+ <Input
356
+ name="SearchableText"
357
+ action={{
358
+ icon: 'search',
359
+ loading: isLoading,
360
+ disabled: isLoading,
361
+ 'aria-label': this.props.intl.formatMessage(
362
+ messages.search,
363
+ ),
364
+ }}
365
+ placeholder={this.props.intl.formatMessage(
366
+ messages.searchForUserOrGroup,
367
+ )}
368
+ onChange={this.onChangeSearch}
369
+ id="sharing-component-search"
370
+ />
371
+ </Form.Field>
372
+ </Form>
373
+ </Segment>
374
+ );
375
+ }}
376
+ </Plug>
377
+ <Plug
378
+ pluggable="sharing-component"
379
+ id="sharing-component-form"
380
+ dependencies={[this.state.entries, this.props.available_roles]}
381
+ >
382
+ <Form onSubmit={this.onSubmit}>
383
+ <Table celled padded striped attached>
384
+ <Table.Header>
385
+ <Table.Row>
386
+ <Table.HeaderCell>
387
+ <FormattedMessage id="Name" defaultMessage="Name" />
388
+ </Table.HeaderCell>
389
+ {this.props.available_roles?.map((role) => (
390
+ <Table.HeaderCell key={role.id}>
391
+ {role.title}
392
+ </Table.HeaderCell>
393
+ ))}
394
+ </Table.Row>
395
+ </Table.Header>
396
+ <Table.Body>
397
+ {this.state.entries?.map((entry) => (
398
+ <Table.Row key={entry.id}>
399
+ <Table.Cell>
400
+ <IconOld
401
+ name={entry.type === 'user' ? 'user' : 'users'}
402
+ title={
403
+ entry.type === 'user'
404
+ ? this.props.intl.formatMessage(messages.user)
405
+ : this.props.intl.formatMessage(messages.group)
406
+ }
407
+ />{' '}
408
+ {entry.title}
409
+ {entry.login && ` (${entry.login})`}
410
+ </Table.Cell>
411
+ {this.props.available_roles?.map((role) => (
412
+ <Table.Cell key={role.id}>
413
+ {entry.roles[role.id] === 'global' && (
414
+ <IconOld
415
+ name="check circle outline"
416
+ title={this.props.intl.formatMessage(
417
+ messages.globalRole,
418
+ )}
419
+ color="blue"
420
+ />
421
+ )}
422
+ {entry.roles[role.id] === 'acquired' && (
423
+ <IconOld
424
+ name="check circle outline"
425
+ color="green"
426
+ title={this.props.intl.formatMessage(
427
+ messages.inheritedValue,
428
+ )}
429
+ />
430
+ )}
431
+ {typeof entry.roles[role.id] === 'boolean' && (
432
+ <Checkbox
433
+ name={this.props.intl.formatMessage(
434
+ messages.assignNewRoles,
435
+ {
436
+ entry: entry.title,
437
+ role: role.id,
438
+ },
439
+ )}
440
+ aria-label={this.props.intl.formatMessage(
441
+ messages.assignNewRoles,
442
+ {
443
+ entry: entry.title,
444
+ role: role.id,
445
+ },
446
+ )}
447
+ onChange={this.onChange}
448
+ value={`${entry.id}:${role.id}`}
449
+ checked={entry.roles[role.id]}
450
+ disabled={entry.login === this.props.login}
451
+ />
452
+ )}
453
+ </Table.Cell>
454
+ ))}
455
+ </Table.Row>
456
+ ))}
457
+ </Table.Body>
458
+ </Table>
459
+ <Segment attached>
460
+ <Form.Field>
461
+ <Checkbox
462
+ id="inherit-permissions-checkbox"
463
+ name="inherit-permissions-checkbox"
464
+ defaultChecked={this.state.inherit}
465
+ onChange={this.onToggleInherit}
466
+ label={
467
+ <label htmlFor="inherit-permissions-checkbox">
468
+ {this.props.intl.formatMessage(messages.inherit)}
469
+ </label>
470
+ }
471
+ />
472
+ </Form.Field>
473
+ <p className="help">
474
+ <FormattedMessage
475
+ id="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, the symbol {inherited} indicates an inherited value. Similarly, the symbol {global} indicates a global role, which is managed by the site administrator."
476
+ defaultMessage="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, inherited values are explicitly labeled as 'Inherited value' and receive a green check mark {inherited}. Similarly, roles managed by the site administrator are labeled as 'Global role' and receive a blue check mark {global}."
477
+ values={{
478
+ inherited: (
479
+ <IconOld
480
+ aria-hidden="true"
481
+ name="check circle outline"
482
+ color="green"
483
+ />
484
+ ),
485
+ global: (
486
+ <IconOld
487
+ aria-hidden="true"
488
+ name="check circle outline"
489
+ color="blue"
490
+ />
491
+ ),
492
+ }}
493
+ />
494
+ </p>
495
+ </Segment>
496
+ <Segment className="right aligned actions" attached clearing>
497
+ <Button
498
+ basic
499
+ secondary
500
+ aria-label={this.props.intl.formatMessage(messages.cancel)}
501
+ title={this.props.intl.formatMessage(messages.cancel)}
502
+ onClick={this.onCancel}
503
+ >
504
+ <Icon className="circled" name={clearSVG} size="30px" />
505
+ </Button>
506
+ <Button
507
+ basic
508
+ primary
509
+ type="submit"
510
+ aria-label={this.props.intl.formatMessage(messages.save)}
511
+ title={this.props.intl.formatMessage(messages.save)}
512
+ loading={this.props.updateRequest.loading}
513
+ onClick={this.onSubmit}
514
+ >
515
+ <Icon className="circled" name={aheadSVG} size="30px" />
516
+ </Button>
517
+ </Segment>
518
+ </Form>
519
+ </Plug>
520
+ </Segment.Group>
521
+ {this.state.isClient &&
522
+ createPortal(
523
+ <Toolbar
524
+ pathname={this.props.pathname}
525
+ hideDefaultViewButtons
526
+ inner={
527
+ <Link
528
+ to={`${getBaseUrl(this.props.pathname)}`}
529
+ className="item"
530
+ >
531
+ <Icon
532
+ name={backSVG}
533
+ className="contents circled"
534
+ size="30px"
535
+ title={this.props.intl.formatMessage(messages.back)}
536
+ />
537
+ </Link>
538
+ }
539
+ />,
540
+ document.getElementById('toolbar'),
541
+ )}
542
+ </Container>
543
+ );
544
+ }
545
+ }
546
+
547
+ export default compose(
548
+ withRouter,
549
+ injectIntl,
550
+ connect(
551
+ (state, props) => ({
552
+ entries: state.sharing.data.entries,
553
+ inherit: state.sharing.data.inherit,
554
+ available_roles: state.sharing.data.available_roles,
555
+ updateRequest: state.sharing.update,
556
+ pathname: props.location.pathname,
557
+ title: state.content.data?.title || '',
558
+ login: state.userSession.token
559
+ ? jwtDecode(state.userSession.token).sub
560
+ : '',
561
+ }),
562
+ { updateSharing, getSharing },
563
+ ),
564
+ )(SharingComponent);
@@ -0,0 +1,11 @@
1
+ --- node_modules/@plone/volto/src/components/manage/Sharing/Sharing.jsx 2026-05-27 09:41:17
2
+ +++ src/addons/volto-eea-website-theme/src/customizations/volto/components/manage/Sharing/Sharing.jsx 2026-05-28 15:16:25
3
+ @@ -554,7 +554,7 @@
4
+ available_roles: state.sharing.data.available_roles,
5
+ updateRequest: state.sharing.update,
6
+ pathname: props.location.pathname,
7
+ - title: state.content.data.title,
8
+ + title: state.content.data?.title || "",
9
+ login: state.userSession.token
10
+ ? jwtDecode(state.userSession.token).sub
11
+ : '',
@@ -0,0 +1,25 @@
1
+ # Sharing.jsx customization
2
+
3
+ This customization shadows Volto core's
4
+ `src/components/manage/Sharing/Sharing.jsx` from `@plone/volto`.
5
+
6
+ ## Changes
7
+
8
+ - **Null-safe access to `state.content.data.title`**: The original code
9
+ accesses `state.content.data.title` directly, which throws a `TypeError:
10
+ Cannot read properties of null (reading 'title')` when `state.content.data`
11
+ is `null` (e.g., when the content hasn't loaded yet, or when SSR renders a
12
+ page where the content API returned an error/null).
13
+
14
+ Fix: `state.content.data.title` → `state.content.data?.title || ""`
15
+
16
+ ## Sentry issue
17
+
18
+ - [440198](https://sentry.eea.europa.eu/organizations/eea/issues/440198/) —
19
+ `TypeError: Cannot read properties of null (reading 'title')` — 1,253 events
20
+
21
+ ## When upgrading Volto
22
+
23
+ Compare the new core `Sharing.jsx` `mapStateToProps` with this override.
24
+ If upstream adds more `state.content.data.*` references, apply optional
25
+ chaining to those as well.