@eeacms/volto-eea-website-theme 0.5.2 → 0.6.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.
@@ -0,0 +1,478 @@
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 { Helmet } from '@plone/volto/helpers';
8
+ import { connect } from 'react-redux';
9
+ import { compose } from 'redux';
10
+ import { Link, withRouter } from 'react-router-dom';
11
+ import { find, isEqual, map } from 'lodash';
12
+ import { Portal } from 'react-portal';
13
+ import {
14
+ Button,
15
+ Checkbox,
16
+ Container,
17
+ Form,
18
+ Icon as IconOld,
19
+ Input,
20
+ Segment,
21
+ Table,
22
+ } from 'semantic-ui-react';
23
+ import jwtDecode from 'jwt-decode';
24
+ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
25
+
26
+ import { updateSharing, getSharing } from '@plone/volto/actions';
27
+ import { getBaseUrl } from '@plone/volto/helpers';
28
+ import { Icon, Toolbar, Toast } from '@plone/volto/components';
29
+ import { toast } from 'react-toastify';
30
+
31
+ import aheadSVG from '@plone/volto/icons/ahead.svg';
32
+ import clearSVG from '@plone/volto/icons/clear.svg';
33
+ import backSVG from '@plone/volto/icons/back.svg';
34
+
35
+ const messages = defineMessages({
36
+ searchForUserOrGroup: {
37
+ id: 'Search for user or group',
38
+ defaultMessage: 'Search for user or group',
39
+ },
40
+ inherit: {
41
+ id: 'Inherit permissions from higher levels',
42
+ defaultMessage: 'Inherit permissions from higher levels',
43
+ },
44
+ save: {
45
+ id: 'Save',
46
+ defaultMessage: 'Save',
47
+ },
48
+ cancel: {
49
+ id: 'Cancel',
50
+ defaultMessage: 'Cancel',
51
+ },
52
+ back: {
53
+ id: 'Back',
54
+ defaultMessage: 'Back',
55
+ },
56
+ sharing: {
57
+ id: 'Sharing',
58
+ defaultMessage: 'Sharing',
59
+ },
60
+ user: {
61
+ id: 'User',
62
+ defaultMessage: 'User',
63
+ },
64
+ group: {
65
+ id: 'Group',
66
+ defaultMessage: 'Group',
67
+ },
68
+ globalRole: {
69
+ id: 'Global role',
70
+ defaultMessage: 'Global role',
71
+ },
72
+ inheritedValue: {
73
+ id: 'Inherited value',
74
+ defaultMessage: 'Inherited value',
75
+ },
76
+ permissionsUpdated: {
77
+ id: 'Permissions updated',
78
+ defaultMessage: 'Permissions updated',
79
+ },
80
+ permissionsUpdatedSuccessfully: {
81
+ id: 'Permissions have been updated successfully',
82
+ defaultMessage: 'Permissions have been updated successfully',
83
+ },
84
+ });
85
+
86
+ /**
87
+ * SharingComponent class.
88
+ * @class SharingComponent
89
+ * @extends Component
90
+ */
91
+ class SharingComponent extends Component {
92
+ /**
93
+ * Property types.
94
+ * @property {Object} propTypes Property types.
95
+ * @static
96
+ */
97
+ static propTypes = {
98
+ updateSharing: PropTypes.func.isRequired,
99
+ getSharing: PropTypes.func.isRequired,
100
+ updateRequest: PropTypes.shape({
101
+ loading: PropTypes.bool,
102
+ loaded: PropTypes.bool,
103
+ }).isRequired,
104
+ pathname: PropTypes.string.isRequired,
105
+ entries: PropTypes.arrayOf(
106
+ PropTypes.shape({
107
+ id: PropTypes.string,
108
+ login: PropTypes.string,
109
+ roles: PropTypes.object,
110
+ title: PropTypes.string,
111
+ type: PropTypes.string,
112
+ }),
113
+ ).isRequired,
114
+ available_roles: PropTypes.arrayOf(PropTypes.object).isRequired,
115
+ inherit: PropTypes.bool,
116
+ title: PropTypes.string.isRequired,
117
+ login: PropTypes.string,
118
+ };
119
+
120
+ /**
121
+ * Default properties
122
+ * @property {Object} defaultProps Default properties.
123
+ * @static
124
+ */
125
+ static defaultProps = {
126
+ inherit: null,
127
+ login: '',
128
+ };
129
+
130
+ /**
131
+ * Constructor
132
+ * @method constructor
133
+ * @param {Object} props Component properties
134
+ * @constructs Sharing
135
+ */
136
+ constructor(props) {
137
+ super(props);
138
+ this.onCancel = this.onCancel.bind(this);
139
+ this.onChange = this.onChange.bind(this);
140
+ this.onChangeSearch = this.onChangeSearch.bind(this);
141
+ this.onSearch = this.onSearch.bind(this);
142
+ this.onSubmit = this.onSubmit.bind(this);
143
+ this.onToggleInherit = this.onToggleInherit.bind(this);
144
+ this.state = {
145
+ search: '',
146
+ inherit: props.inherit,
147
+ entries: props.entries,
148
+ isClient: false,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Component did mount
154
+ * @method componentDidMount
155
+ * @returns {undefined}
156
+ */
157
+ componentDidMount() {
158
+ this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
159
+ this.setState({ isClient: true });
160
+ }
161
+
162
+ /**
163
+ * Component will receive props
164
+ * @method componentWillReceiveProps
165
+ * @param {Object} nextProps Next properties
166
+ * @returns {undefined}
167
+ */
168
+ UNSAFE_componentWillReceiveProps(nextProps) {
169
+ if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) {
170
+ this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
171
+ toast.success(
172
+ <Toast
173
+ success
174
+ title={this.props.intl.formatMessage(messages.permissionsUpdated)}
175
+ content={this.props.intl.formatMessage(
176
+ messages.permissionsUpdatedSuccessfully,
177
+ )}
178
+ />,
179
+ );
180
+ }
181
+ this.setState({
182
+ inherit:
183
+ this.props.inherit === null ? nextProps.inherit : this.state.inherit,
184
+ entries: map(nextProps.entries, (entry) => {
185
+ const values = find(this.state.entries, { id: entry.id });
186
+ return {
187
+ ...entry,
188
+ roles: values ? values.roles : entry.roles,
189
+ };
190
+ }),
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Submit handler
196
+ * @method onSubmit
197
+ * @param {object} event Event object.
198
+ * @returns {undefined}
199
+ */
200
+ onSubmit(event) {
201
+ const data = { entries: [] };
202
+ event.preventDefault();
203
+ if (this.props.inherit !== this.state.inherit) {
204
+ data.inherit = this.state.inherit;
205
+ }
206
+ for (let i = 0; i < this.props.entries.length; i += 1) {
207
+ if (!isEqual(this.props.entries[i].roles, this.state.entries[i].roles)) {
208
+ data.entries.push({
209
+ id: this.state.entries[i].id,
210
+ type: this.state.entries[i].type,
211
+ roles: this.state.entries[i].roles,
212
+ });
213
+ }
214
+ }
215
+ this.props.updateSharing(getBaseUrl(this.props.pathname), data);
216
+ }
217
+
218
+ /**
219
+ * Search handler
220
+ * @method onSearch
221
+ * @param {object} event Event object.
222
+ * @returns {undefined}
223
+ */
224
+ onSearch(event) {
225
+ event.preventDefault();
226
+ this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
227
+ }
228
+
229
+ /**
230
+ * On change search handler
231
+ * @method onChangeSearch
232
+ * @param {object} event Event object.
233
+ * @returns {undefined}
234
+ */
235
+ onChangeSearch(event) {
236
+ this.setState({
237
+ search: event.target.value,
238
+ });
239
+ }
240
+
241
+ /**
242
+ * On toggle inherit handler
243
+ * @method onToggleInherit
244
+ * @returns {undefined}
245
+ */
246
+ onToggleInherit() {
247
+ this.setState({
248
+ inherit: !this.state.inherit,
249
+ });
250
+ }
251
+
252
+ /**
253
+ * On change handler
254
+ * @method onChange
255
+ * @param {object} event Event object
256
+ * @param {string} value Entry value
257
+ * @returns {undefined}
258
+ */
259
+ onChange(event, { value }) {
260
+ const [principal, role] = value.split(':');
261
+ this.setState({
262
+ entries: map(this.state.entries, (entry) => ({
263
+ ...entry,
264
+ roles:
265
+ entry.id === principal
266
+ ? {
267
+ ...entry.roles,
268
+ [role]: !entry.roles[role],
269
+ }
270
+ : entry.roles,
271
+ })),
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Cancel handler
277
+ * @method onCancel
278
+ * @returns {undefined}
279
+ */
280
+ onCancel() {
281
+ this.props.history.push(getBaseUrl(this.props.pathname));
282
+ }
283
+
284
+ /**
285
+ * Render method.
286
+ * @method render
287
+ * @returns {string} Markup for the component.
288
+ */
289
+ render() {
290
+ return (
291
+ <Container id="page-sharing">
292
+ <Helmet title={this.props.intl.formatMessage(messages.sharing)} />
293
+ <Segment.Group raised>
294
+ <Segment className="primary">
295
+ <FormattedMessage
296
+ id="Sharing for {title}"
297
+ defaultMessage="Sharing for {title}"
298
+ values={{ title: <q>{this.props.title}</q> }}
299
+ />
300
+ </Segment>
301
+ <Segment secondary>
302
+ <FormattedMessage
303
+ id="You can control who can view and edit your item using the list below."
304
+ defaultMessage="You can control who can view and edit your item using the list below."
305
+ />
306
+ </Segment>
307
+ <Segment>
308
+ <Form onSubmit={this.onSearch}>
309
+ <Form.Field>
310
+ <Input
311
+ name="SearchableText"
312
+ action={{ icon: 'search' }}
313
+ placeholder={this.props.intl.formatMessage(
314
+ messages.searchForUserOrGroup,
315
+ )}
316
+ onChange={this.onChangeSearch}
317
+ />
318
+ </Form.Field>
319
+ </Form>
320
+ </Segment>
321
+ <Form onSubmit={this.onSubmit}>
322
+ <Table celled padded striped attached>
323
+ <Table.Header>
324
+ <Table.Row>
325
+ <Table.HeaderCell>
326
+ <FormattedMessage id="Name" defaultMessage="Name" />
327
+ </Table.HeaderCell>
328
+ {this.props.available_roles?.map((role) => (
329
+ <Table.HeaderCell key={role.id}>
330
+ {role.title}
331
+ </Table.HeaderCell>
332
+ ))}
333
+ </Table.Row>
334
+ </Table.Header>
335
+ <Table.Body>
336
+ {this.state.entries?.map((entry) => (
337
+ <Table.Row key={entry.id}>
338
+ <Table.Cell>
339
+ <IconOld
340
+ name={entry.type === 'user' ? 'user' : 'users'}
341
+ title={
342
+ entry.type === 'user'
343
+ ? this.props.intl.formatMessage(messages.user)
344
+ : this.props.intl.formatMessage(messages.group)
345
+ }
346
+ />{' '}
347
+ {entry.title}
348
+ {entry.login && ` (${entry.login})`}
349
+ </Table.Cell>
350
+ {this.props.available_roles?.map((role) => (
351
+ <Table.Cell key={role.id}>
352
+ {entry.roles[role.id] === 'global' && (
353
+ <IconOld
354
+ name="check circle outline"
355
+ title={this.props.intl.formatMessage(
356
+ messages.globalRole,
357
+ )}
358
+ color="blue"
359
+ />
360
+ )}
361
+ {entry.roles[role.id] === 'acquired' && (
362
+ <IconOld
363
+ name="check circle outline"
364
+ color="green"
365
+ title={this.props.intl.formatMessage(
366
+ messages.inheritedValue,
367
+ )}
368
+ />
369
+ )}
370
+ {typeof entry.roles[role.id] === 'boolean' && (
371
+ <Checkbox
372
+ onChange={this.onChange}
373
+ value={`${entry.id}:${role.id}`}
374
+ checked={entry.roles[role.id]}
375
+ disabled={entry.login === this.props.login}
376
+ />
377
+ )}
378
+ </Table.Cell>
379
+ ))}
380
+ </Table.Row>
381
+ ))}
382
+ </Table.Body>
383
+ </Table>
384
+ <Segment attached>
385
+ <Form.Field>
386
+ <Checkbox
387
+ checked={this.state.inherit}
388
+ onChange={this.onToggleInherit}
389
+ label={this.props.intl.formatMessage(messages.inherit)}
390
+ />
391
+ </Form.Field>
392
+ <p className="help">
393
+ <FormattedMessage
394
+ 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."
395
+ 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, the symbol {inherited} indicates an inherited value. Similarly, the symbol {global} indicates a global role, which is managed by the site administrator."
396
+ values={{
397
+ inherited: (
398
+ <IconOld name="check circle outline" color="green" />
399
+ ),
400
+ global: (
401
+ <IconOld name="check circle outline" color="blue" />
402
+ ),
403
+ }}
404
+ />
405
+ </p>
406
+ </Segment>
407
+ <Segment className="actions" attached clearing>
408
+ <Button
409
+ basic
410
+ icon
411
+ primary
412
+ floated="right"
413
+ type="submit"
414
+ aria-label={this.props.intl.formatMessage(messages.save)}
415
+ title={this.props.intl.formatMessage(messages.save)}
416
+ loading={this.props.updateRequest.loading}
417
+ onClick={this.onSubmit}
418
+ >
419
+ <Icon className="circled" name={aheadSVG} size="30px" />
420
+ </Button>
421
+ <Button
422
+ basic
423
+ icon
424
+ secondary
425
+ aria-label={this.props.intl.formatMessage(messages.cancel)}
426
+ title={this.props.intl.formatMessage(messages.cancel)}
427
+ floated="right"
428
+ onClick={this.onCancel}
429
+ >
430
+ <Icon className="circled" name={clearSVG} size="30px" />
431
+ </Button>
432
+ </Segment>
433
+ </Form>
434
+ </Segment.Group>
435
+ {this.state.isClient && (
436
+ <Portal node={document.getElementById('toolbar')}>
437
+ <Toolbar
438
+ pathname={this.props.pathname}
439
+ hideDefaultViewButtons
440
+ inner={
441
+ <Link
442
+ to={`${getBaseUrl(this.props.pathname)}`}
443
+ className="item"
444
+ >
445
+ <Icon
446
+ name={backSVG}
447
+ className="contents circled"
448
+ size="30px"
449
+ title={this.props.intl.formatMessage(messages.back)}
450
+ />
451
+ </Link>
452
+ }
453
+ />
454
+ </Portal>
455
+ )}
456
+ </Container>
457
+ );
458
+ }
459
+ }
460
+
461
+ export default compose(
462
+ withRouter,
463
+ injectIntl,
464
+ connect(
465
+ (state, props) => ({
466
+ entries: state.sharing.data.entries,
467
+ inherit: state.sharing.data.inherit,
468
+ available_roles: state.sharing.data.available_roles,
469
+ updateRequest: state.sharing.update,
470
+ pathname: props.location.pathname,
471
+ title: state.content.data.title,
472
+ login: state.userSession.token
473
+ ? jwtDecode(state.userSession.token).sub
474
+ : '',
475
+ }),
476
+ { updateSharing, getSharing },
477
+ ),
478
+ )(SharingComponent);