@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
|
@@ -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.
|