@plone/volto 17.0.0-alpha.25 → 17.0.0-alpha.26

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 (95) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +52 -5
  3. package/README.md +8 -7
  4. package/cypress/support/commands.js +12 -9
  5. package/cypress.config.js +1 -0
  6. package/locales/ca/LC_MESSAGES/volto.po +36 -15
  7. package/locales/ca.json +1 -1
  8. package/locales/de/LC_MESSAGES/volto.po +36 -15
  9. package/locales/de.json +1 -1
  10. package/locales/en/LC_MESSAGES/volto.po +35 -14
  11. package/locales/en.json +1 -1
  12. package/locales/es/LC_MESSAGES/volto.po +65 -44
  13. package/locales/es.json +1 -1
  14. package/locales/eu/LC_MESSAGES/volto.po +35 -14
  15. package/locales/eu.json +1 -1
  16. package/locales/fi/LC_MESSAGES/volto.po +35 -14
  17. package/locales/fi.json +1 -1
  18. package/locales/fr/LC_MESSAGES/volto.po +36 -15
  19. package/locales/fr.json +1 -1
  20. package/locales/it/LC_MESSAGES/volto.po +35 -14
  21. package/locales/it.json +1 -1
  22. package/locales/ja/LC_MESSAGES/volto.po +35 -14
  23. package/locales/ja.json +1 -1
  24. package/locales/nl/LC_MESSAGES/volto.po +36 -15
  25. package/locales/nl.json +1 -1
  26. package/locales/pt/LC_MESSAGES/volto.po +36 -15
  27. package/locales/pt.json +1 -1
  28. package/locales/pt_BR/LC_MESSAGES/volto.po +35 -14
  29. package/locales/pt_BR.json +1 -1
  30. package/locales/ro/LC_MESSAGES/volto.po +36 -15
  31. package/locales/ro.json +1 -1
  32. package/locales/volto.pot +35 -14
  33. package/locales/zh_CN/LC_MESSAGES/volto.po +36 -15
  34. package/locales/zh_CN.json +1 -1
  35. package/package.json +4 -4
  36. package/packages/volto-slate/package.json +1 -1
  37. package/packages/volto-slate/src/editor/render.jsx +2 -3
  38. package/src/actions/index.js +3 -0
  39. package/src/actions/navroot/navroot.js +16 -0
  40. package/src/actions/navroot/navroot.test.js +15 -0
  41. package/src/actions/site/site.js +16 -0
  42. package/src/actions/site/site.test.js +15 -0
  43. package/src/actions/userSession/userSession.js +17 -1
  44. package/src/components/manage/Blocks/Block/Settings.jsx +2 -0
  45. package/src/components/manage/Blocks/Block/Settings.test.jsx +90 -0
  46. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +1 -1
  47. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +42 -25
  48. package/src/components/manage/Blocks/ToC/View.jsx +75 -13
  49. package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +2 -12
  50. package/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx +65 -38
  51. package/src/components/manage/Controlpanels/Rules/AddRule.jsx +1 -1
  52. package/src/components/manage/Controlpanels/Rules/EditRule.jsx +1 -1
  53. package/src/components/manage/Controlpanels/Users/RenderUsers.jsx +95 -5
  54. package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +116 -103
  55. package/src/components/manage/Form/BlockDataForm.jsx +3 -2
  56. package/src/components/manage/Form/BlockDataForm.test.jsx +34 -2
  57. package/src/components/manage/LinksToItem/LinksToItem.test.jsx +4 -1
  58. package/src/components/manage/Messages/Messages.jsx +32 -99
  59. package/src/components/manage/Messages/Messages.test.jsx +0 -1
  60. package/src/components/manage/Sharing/Sharing.jsx +39 -16
  61. package/src/components/manage/UniversalLink/UniversalLink.jsx +4 -6
  62. package/src/components/manage/Widgets/ArrayWidget.jsx +3 -1
  63. package/src/components/manage/Widgets/ArrayWidget.test.jsx +45 -1
  64. package/src/components/manage/Widgets/RegistryImageWidget.jsx +210 -0
  65. package/src/components/manage/Widgets/RegistryImageWidget.test.jsx +91 -0
  66. package/src/components/manage/Widgets/SelectWidget.jsx +15 -1
  67. package/src/components/manage/Widgets/SelectWidget.test.jsx +45 -1
  68. package/src/components/theme/ContentMetadataTags/ContentMetadataTags.jsx +37 -3
  69. package/src/components/theme/Login/Login.jsx +159 -241
  70. package/src/components/theme/Logo/Logo.Multilingual.test.jsx +131 -1
  71. package/src/components/theme/Logo/Logo.jsx +35 -29
  72. package/src/components/theme/Logo/Logo.test.jsx +135 -1
  73. package/src/components/theme/Logout/Logout.jsx +1 -1
  74. package/src/components/theme/Navigation/Navigation.jsx +86 -171
  75. package/src/components/theme/SearchWidget/SearchWidget.jsx +15 -3
  76. package/src/components/theme/SearchWidget/SearchWidget.test.jsx +8 -0
  77. package/src/components/theme/View/View.jsx +2 -0
  78. package/src/config/ControlPanels.js +0 -1
  79. package/src/config/Widgets.jsx +2 -0
  80. package/src/config/index.js +15 -3
  81. package/src/constants/ActionTypes.js +3 -0
  82. package/src/express-middleware/images.js +1 -0
  83. package/src/helpers/MessageLabels/MessageLabels.js +26 -4
  84. package/src/helpers/Site/index.js +21 -0
  85. package/src/helpers/index.js +1 -0
  86. package/src/reducers/index.js +4 -0
  87. package/src/reducers/navroot/navroot.js +79 -0
  88. package/src/reducers/navroot/navroot.test.js +110 -0
  89. package/src/reducers/site/site.js +51 -0
  90. package/src/reducers/site/site.test.js +67 -0
  91. package/src/reducers/userSession/userSession.js +15 -1
  92. package/test-setup-config.js +1 -0
  93. package/theme/themes/pastanaga/elements/input.overrides +5 -1
  94. package/theme/themes/pastanaga/extras/login.less +3 -0
  95. package/webpack-plugins/webpack-less-plugin.js +19 -0
@@ -11,6 +11,7 @@ import {
11
11
  getControlpanel,
12
12
  updateUser,
13
13
  updateGroup,
14
+ getUserSchema,
14
15
  } from '@plone/volto/actions';
15
16
  import {
16
17
  Icon,
@@ -98,6 +99,7 @@ class UsersControlpanel extends Component {
98
99
  this.updateUserRole = this.updateUserRole.bind(this);
99
100
  this.state = {
100
101
  search: '',
102
+ isLoading: false,
101
103
  showAddUser: false,
102
104
  showAddUserErrorConfirm: false,
103
105
  addUserError: '',
@@ -121,6 +123,7 @@ class UsersControlpanel extends Component {
121
123
  entries: this.props.users,
122
124
  });
123
125
  }
126
+ await this.props.getUserSchema();
124
127
  };
125
128
 
126
129
  // Because username field needs to be disabled if email login is enabled!
@@ -181,9 +184,19 @@ class UsersControlpanel extends Component {
181
184
  */
182
185
  onSearch(event) {
183
186
  event.preventDefault();
184
- this.props.listUsers({
185
- search: this.state.search,
186
- });
187
+ this.setState({ isLoading: true });
188
+ this.props
189
+ .listUsers({
190
+ search: this.state.search,
191
+ })
192
+ .then(() => {
193
+ this.setState({ isLoading: false });
194
+ })
195
+ .catch((error) => {
196
+ this.setState({ isLoading: false });
197
+ // eslint-disable-next-line no-console
198
+ console.error('Error searching users', error);
199
+ });
187
200
  }
188
201
 
189
202
  /**
@@ -265,12 +278,28 @@ class UsersControlpanel extends Component {
265
278
  * @returns {undefined}
266
279
  */
267
280
  onAddUserSubmit(data, callback) {
268
- const { groups, sendPasswordReset } = data;
269
- if (groups && groups.length > 0) this.addUserToGroup(data);
270
- this.props.createUser(data, sendPasswordReset);
271
- this.setState({
272
- addUserSetFormDataCallback: callback,
273
- });
281
+ const { groups, sendPasswordReset, password } = data;
282
+ if (
283
+ sendPasswordReset !== undefined &&
284
+ sendPasswordReset === true &&
285
+ password !== undefined
286
+ ) {
287
+ toast.error(
288
+ <Toast
289
+ error
290
+ title={this.props.intl.formatMessage(messages.error)}
291
+ content={this.props.intl.formatMessage(
292
+ messages.addUserFormPasswordAndSendPasswordTogetherNotAllowed,
293
+ )}
294
+ />,
295
+ );
296
+ } else {
297
+ if (groups && groups.length > 0) this.addUserToGroup(data);
298
+ this.props.createUser(data, sendPasswordReset);
299
+ this.setState({
300
+ addUserSetFormDataCallback: callback,
301
+ });
302
+ }
274
303
  }
275
304
 
276
305
  /**
@@ -392,6 +421,65 @@ class UsersControlpanel extends Component {
392
421
  let usernameToDelete = this.state.userToDelete
393
422
  ? this.state.userToDelete.username
394
423
  : '';
424
+ // Copy the userschema using JSON serialization/deserialization
425
+ // this is really ugly, but if we don't do this the original value
426
+ // of the userschema is changed and it is used like that through
427
+ // the lifecycle of the application
428
+ let adduserschema = {};
429
+ if (this.props?.userschema?.loaded) {
430
+ adduserschema = JSON.parse(
431
+ JSON.stringify(this.props?.userschema?.userschema),
432
+ );
433
+ adduserschema.properties['username'] = {
434
+ title: this.props.intl.formatMessage(messages.addUserFormUsernameTitle),
435
+ type: 'string',
436
+ description: this.props.intl.formatMessage(
437
+ messages.addUserFormUsernameDescription,
438
+ ),
439
+ };
440
+ adduserschema.properties['password'] = {
441
+ title: this.props.intl.formatMessage(messages.addUserFormPasswordTitle),
442
+ type: 'password',
443
+ description: this.props.intl.formatMessage(
444
+ messages.addUserFormPasswordDescription,
445
+ ),
446
+ widget: 'password',
447
+ };
448
+ adduserschema.properties['sendPasswordReset'] = {
449
+ title: this.props.intl.formatMessage(
450
+ messages.addUserFormSendPasswordResetTitle,
451
+ ),
452
+ type: 'boolean',
453
+ };
454
+ adduserschema.properties['roles'] = {
455
+ title: this.props.intl.formatMessage(messages.addUserFormRolesTitle),
456
+ type: 'array',
457
+ choices: this.props.roles.map((role) => [role.id, role.title]),
458
+ noValueOption: false,
459
+ };
460
+ adduserschema.properties['groups'] = {
461
+ title: this.props.intl.formatMessage(messages.addUserGroupNameTitle),
462
+ type: 'array',
463
+ choices: this.props.groups.map((group) => [group.id, group.id]),
464
+ noValueOption: false,
465
+ };
466
+ if (
467
+ adduserschema.fieldsets &&
468
+ adduserschema.fieldsets.length > 0 &&
469
+ !adduserschema.fieldsets[0]['fields'].includes('username')
470
+ ) {
471
+ adduserschema.fieldsets[0]['fields'] = adduserschema.fieldsets[0][
472
+ 'fields'
473
+ ].concat([
474
+ 'username',
475
+ 'password',
476
+ 'sendPasswordReset',
477
+ 'roles',
478
+ 'groups',
479
+ ]);
480
+ }
481
+ }
482
+
395
483
  return (
396
484
  <Container className="users-control-panel">
397
485
  <Helmet title={this.props.intl.formatMessage(messages.users)} />
@@ -418,7 +506,7 @@ class UsersControlpanel extends Component {
418
506
  onConfirm={this.onDeleteOk}
419
507
  size={null}
420
508
  />
421
- {this.state.showAddUser ? (
509
+ {this.props?.userschema?.loaded && this.state.showAddUser ? (
422
510
  <ModalForm
423
511
  open={this.state.showAddUser}
424
512
  className="modal"
@@ -429,96 +517,7 @@ class UsersControlpanel extends Component {
429
517
  }
430
518
  title={this.props.intl.formatMessage(messages.addUserFormTitle)}
431
519
  loading={this.props.createRequest.loading}
432
- schema={{
433
- fieldsets: [
434
- {
435
- id: 'default',
436
- title: 'FIXME: User Data',
437
- fields: [
438
- ...(!this.state.loginUsingEmail ? ['username'] : []),
439
- 'fullname',
440
- 'email',
441
- 'password',
442
- 'sendPasswordReset',
443
- 'roles',
444
- 'groups',
445
- ],
446
- },
447
- ],
448
- properties: {
449
- ...(!this.state.loginUsingEmail
450
- ? {
451
- username: {
452
- title: this.props.intl.formatMessage(
453
- messages.addUserFormUsernameTitle,
454
- ),
455
- type: 'string',
456
- description: this.props.intl.formatMessage(
457
- messages.addUserFormUsernameDescription,
458
- ),
459
- },
460
- }
461
- : {}),
462
- fullname: {
463
- title: this.props.intl.formatMessage(
464
- messages.addUserFormFullnameTitle,
465
- ),
466
- type: 'string',
467
- description: this.props.intl.formatMessage(
468
- messages.addUserFormFullnameDescription,
469
- ),
470
- },
471
- email: {
472
- title: this.props.intl.formatMessage(
473
- messages.addUserFormEmailTitle,
474
- ),
475
- type: 'string',
476
- description: this.props.intl.formatMessage(
477
- messages.addUserFormEmailDescription,
478
- ),
479
- widget: 'email',
480
- },
481
- password: {
482
- title: this.props.intl.formatMessage(
483
- messages.addUserFormPasswordTitle,
484
- ),
485
- type: 'password',
486
- description: this.props.intl.formatMessage(
487
- messages.addUserFormPasswordDescription,
488
- ),
489
- widget: 'password',
490
- },
491
- sendPasswordReset: {
492
- title: this.props.intl.formatMessage(
493
- messages.addUserFormSendPasswordResetTitle,
494
- ),
495
- type: 'boolean',
496
- },
497
- roles: {
498
- title: this.props.intl.formatMessage(
499
- messages.addUserFormRolesTitle,
500
- ),
501
- type: 'array',
502
- choices: this.props.roles.map((role) => [
503
- role.id,
504
- role.title,
505
- ]),
506
- noValueOption: false,
507
- },
508
- groups: {
509
- title: this.props.intl.formatMessage(
510
- messages.addUserGroupNameTitle,
511
- ),
512
- type: 'array',
513
- choices: this.props.groups.map((group) => [
514
- group.id,
515
- group.id,
516
- ]),
517
- noValueOption: false,
518
- },
519
- },
520
- required: ['username', 'email'],
521
- }}
520
+ schema={adduserschema}
522
521
  />
523
522
  ) : null}
524
523
  </div>
@@ -547,7 +546,11 @@ class UsersControlpanel extends Component {
547
546
  <Form.Field>
548
547
  <Input
549
548
  name="SearchableText"
550
- action={{ icon: 'search' }}
549
+ action={{
550
+ icon: 'search',
551
+ loading: this.state.isLoading,
552
+ disabled: this.state.isLoading,
553
+ }}
551
554
  placeholder={this.props.intl.formatMessage(
552
555
  messages.searchUsers,
553
556
  )}
@@ -558,7 +561,8 @@ class UsersControlpanel extends Component {
558
561
  </Form>
559
562
  </Segment>
560
563
  <Form>
561
- <div className="table">
564
+ {((this.props.many_users && this.state.entries.length > 0) ||
565
+ !this.props.many_users) && (
562
566
  <Table padded striped attached unstackable>
563
567
  <Table.Header>
564
568
  <Table.Row>
@@ -592,11 +596,18 @@ class UsersControlpanel extends Component {
592
596
  user={user}
593
597
  updateUser={this.updateUserRole}
594
598
  inheritedRole={this.props.inheritedRole}
599
+ userschema={this.props.userschema}
600
+ listUsers={this.props.listUsers}
595
601
  />
596
602
  ))}
597
603
  </Table.Body>
598
604
  </Table>
599
- </div>
605
+ )}
606
+ {this.state.entries.length === 0 && this.state.search && (
607
+ <Segment>
608
+ {this.props.intl.formatMessage(messages.userSearchNoResults)}
609
+ </Segment>
610
+ )}
600
611
  <div className="contents-pagination">
601
612
  <Pagination
602
613
  current={this.state.currentPage}
@@ -684,6 +695,7 @@ export default compose(
684
695
  createRequest: state.users.create,
685
696
  loadRolesRequest: state.roles,
686
697
  inheritedRole: state.authRole.authenticatedRole,
698
+ userschema: state.userschema,
687
699
  controlPanelData: state.controlpanels?.controlpanel,
688
700
  }),
689
701
  (dispatch) =>
@@ -697,6 +709,7 @@ export default compose(
697
709
  createUser,
698
710
  updateUser,
699
711
  updateGroup,
712
+ getUserSchema,
700
713
  },
701
714
  dispatch,
702
715
  ),
@@ -5,7 +5,7 @@ import { withVariationSchemaEnhancer } from '@plone/volto/helpers';
5
5
  const EnhancedBlockDataForm = withVariationSchemaEnhancer(InlineForm);
6
6
 
7
7
  export default function BlockDataForm(props) {
8
- const { onChangeBlock, block } = props;
8
+ const { onChangeBlock, block, applySchemaEnhancers = true } = props;
9
9
 
10
10
  if (!onChangeBlock) {
11
11
  // eslint-disable-next-line no-console
@@ -19,8 +19,9 @@ export default function BlockDataForm(props) {
19
19
  [block, onChangeBlock],
20
20
  );
21
21
 
22
+ const Form = applySchemaEnhancers ? EnhancedBlockDataForm : InlineForm;
22
23
  return (
23
- <EnhancedBlockDataForm
24
+ <Form
24
25
  {...props}
25
26
  onChangeFormData={onChangeBlock ? onChangeFormData : undefined}
26
27
  />
@@ -72,7 +72,7 @@ beforeAll(() => {
72
72
  });
73
73
 
74
74
  describe('BlockDataForm', () => {
75
- it('should does not add variations to schema when unneeded', () => {
75
+ it('should not add variations to schema when unneeded', () => {
76
76
  const WrappedBlockDataForm = withStateManagement(BlockDataForm);
77
77
  const store = mockStore({
78
78
  intl: {
@@ -103,7 +103,7 @@ describe('BlockDataForm', () => {
103
103
  expect(testSchema.fieldsets[0].fields).toStrictEqual([]);
104
104
  });
105
105
 
106
- it('should does not add variations when only one variation', () => {
106
+ it('should not add variations when only one variation', () => {
107
107
  const WrappedBlockDataForm = withStateManagement(BlockDataForm);
108
108
  const store = mockStore({
109
109
  intl: {
@@ -164,4 +164,36 @@ describe('BlockDataForm', () => {
164
164
  // schema is cloned, not mutated in place
165
165
  expect(testSchema.fieldsets[0].fields).toStrictEqual([]);
166
166
  });
167
+
168
+ it('should not add variations to schema when explicitly disabled', () => {
169
+ const WrappedBlockDataForm = withStateManagement(BlockDataForm);
170
+ const store = mockStore({
171
+ intl: {
172
+ locale: 'en',
173
+ messages: {},
174
+ },
175
+ });
176
+ const testSchema = {
177
+ fieldsets: [{ title: 'Default', id: 'default', fields: [] }],
178
+ properties: {},
179
+ required: [],
180
+ };
181
+ const formData = {
182
+ '@type': 'testBlock',
183
+ };
184
+ const { container } = render(
185
+ <Provider store={store}>
186
+ <WrappedBlockDataForm
187
+ formData={formData}
188
+ schema={testSchema}
189
+ onChangeField={(id, value) => {}}
190
+ applySchemaEnhancers={false}
191
+ />
192
+ </Provider>,
193
+ );
194
+ expect(container).toMatchSnapshot();
195
+
196
+ // schema is cloned, not mutated in place
197
+ expect(testSchema.fieldsets[0].fields).toStrictEqual([]);
198
+ });
167
199
  });
@@ -3,6 +3,7 @@ import renderer from 'react-test-renderer';
3
3
  import { Provider } from 'react-intl-redux';
4
4
  import configureMockStore from 'redux-mock-store';
5
5
  import thunk from 'redux-thunk';
6
+ import { MemoryRouter } from 'react-router-dom';
6
7
 
7
8
  import LinksToItem from './LinksToItem';
8
9
 
@@ -88,7 +89,9 @@ describe('LinksToItem', () => {
88
89
  });
89
90
  const component = renderer.create(
90
91
  <Provider store={store}>
91
- <LinksToItem location={{ pathname: '/page-1/links-to-item' }} />
92
+ <MemoryRouter>
93
+ <LinksToItem location={{ pathname: '/page-1/links-to-item' }} />
94
+ </MemoryRouter>
92
95
  </Provider>,
93
96
  );
94
97
  const json = component.toJSON();
@@ -1,107 +1,40 @@
1
- /**
2
- * Messages component.
3
- * @module components/manage/Messages/Messages
4
- */
5
-
6
- import React, { Component } from 'react';
7
- import PropTypes from 'prop-types';
8
- import { connect } from 'react-redux';
1
+ import { useDispatch, useSelector, shallowEqual } from 'react-redux';
9
2
  import { Message, Container } from 'semantic-ui-react';
10
3
  import { map } from 'lodash';
11
4
 
12
5
  import { removeMessage } from '@plone/volto/actions';
13
6
 
14
- /**
15
- * Messages container class.
16
- * @class Messages
17
- * @extends Component
18
- */
19
- class Messages extends Component {
20
- /**
21
- * Property types.
22
- * @property {Object} propTypes Property types.
23
- * @static
24
- */
25
- static propTypes = {
26
- removeMessage: PropTypes.func.isRequired,
27
- messages: PropTypes.arrayOf(
28
- PropTypes.shape({
29
- title: PropTypes.string,
30
- body: PropTypes.string,
31
- level: PropTypes.string,
32
- }),
33
- ).isRequired,
34
- };
35
-
36
- /**
37
- * Constructor
38
- * @method constructor
39
- * @param {Object} props Component properties
40
- * @constructs Messages
41
- */
42
- constructor(props) {
43
- super(props);
44
- this.onDismiss = this.onDismiss.bind(this);
45
- }
7
+ const Messages = () => {
8
+ const dispatch = useDispatch();
46
9
 
47
- // /**
48
- // * Component will receive props
49
- // * @method componentWillReceiveProps
50
- // * @param {Object} nextProps Next properties
51
- // * @returns {undefined}
52
- // */
53
- // componentWillReceiveProps(nextProps) {
54
- // if (nextProps.messages.length > this.props.messages.length) {
55
- // window.setTimeout(() => {
56
- // if (this.props.messages.length > 0) {
57
- // this.props.removeMessage(-1);
58
- // }
59
- // }, 6000);
60
- // }
61
- // }
10
+ const messages = useSelector(
11
+ (state) => state.messages.messages,
12
+ shallowEqual,
13
+ );
62
14
 
63
- /**
64
- * On dismiss
65
- * @method onDismiss
66
- * @param {Object} event Event object
67
- * @param {number} value Index of message
68
- * @returns {undefined}
69
- */
70
- onDismiss(event, { value }) {
71
- this.props.removeMessage(value);
72
- }
73
-
74
- /**
75
- * Render method.
76
- * @method render
77
- * @returns {string} Markup for the component.
78
- */
79
- render() {
80
- return (
81
- this.props.messages && (
82
- <Container className="messages">
83
- {map(this.props.messages, (message, index) => (
84
- <Message
85
- key={message.id}
86
- value={index}
87
- onDismiss={this.onDismiss}
88
- error={message.level === 'error'}
89
- success={message.level === 'success'}
90
- warning={message.level === 'warning'}
91
- info={message.level === 'info'}
92
- header={message.title}
93
- content={message.body}
94
- />
95
- ))}
96
- </Container>
97
- )
98
- );
99
- }
100
- }
15
+ const onDismiss = (event, { value }) => {
16
+ dispatch(removeMessage(value));
17
+ };
101
18
 
102
- export default connect(
103
- (state) => ({
104
- messages: state.messages.messages,
105
- }),
106
- { removeMessage },
107
- )(Messages);
19
+ return (
20
+ messages && (
21
+ <Container className="messages">
22
+ {map(messages, (message, index) => (
23
+ <Message
24
+ key={message.id}
25
+ value={index}
26
+ onDismiss={onDismiss}
27
+ error={message.level === 'error'}
28
+ success={message.level === 'success'}
29
+ warning={message.level === 'warning'}
30
+ info={message.level === 'info'}
31
+ header={message.title}
32
+ content={message.body}
33
+ />
34
+ ))}
35
+ </Container>
36
+ )
37
+ );
38
+ };
39
+
40
+ export default Messages;
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import renderer from 'react-test-renderer';
3
2
  import configureStore from 'redux-mock-store';
4
3
  import { Provider } from 'react-redux';
@@ -145,6 +145,7 @@ class SharingComponent extends Component {
145
145
  this.onToggleInherit = this.onToggleInherit.bind(this);
146
146
  this.state = {
147
147
  search: '',
148
+ isLoading: false,
148
149
  inherit: props.inherit,
149
150
  entries: props.entries,
150
151
  isClient: false,
@@ -225,7 +226,17 @@ class SharingComponent extends Component {
225
226
  */
226
227
  onSearch(event) {
227
228
  event.preventDefault();
228
- this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
229
+ this.setState({ isLoading: true });
230
+ this.props
231
+ .getSharing(getBaseUrl(this.props.pathname), this.state.search)
232
+ .then(() => {
233
+ this.setState({ isLoading: false });
234
+ })
235
+ .catch((error) => {
236
+ this.setState({ isLoading: false });
237
+ // eslint-disable-next-line no-console
238
+ console.error('Error searching users or groups', error);
239
+ });
229
240
  }
230
241
 
231
242
  /**
@@ -296,7 +307,10 @@ class SharingComponent extends Component {
296
307
  <Container id="page-sharing">
297
308
  <Helmet title={this.props.intl.formatMessage(messages.sharing)} />
298
309
  <Segment.Group raised>
299
- <Pluggable name="sharing-component" />
310
+ <Pluggable
311
+ name="sharing-component"
312
+ params={{ isLoading: this.state.isLoading }}
313
+ />
300
314
  <Plug pluggable="sharing-component" id="sharing-component-title">
301
315
  <Segment className="primary">
302
316
  <FormattedMessage
@@ -318,20 +332,29 @@ class SharingComponent extends Component {
318
332
  </Segment>
319
333
  </Plug>
320
334
  <Plug pluggable="sharing-component" id="sharing-component-search">
321
- <Segment>
322
- <Form onSubmit={this.onSearch}>
323
- <Form.Field>
324
- <Input
325
- name="SearchableText"
326
- action={{ icon: 'search' }}
327
- placeholder={this.props.intl.formatMessage(
328
- messages.searchForUserOrGroup,
329
- )}
330
- onChange={this.onChangeSearch}
331
- />
332
- </Form.Field>
333
- </Form>
334
- </Segment>
335
+ {({ isLoading }) => {
336
+ return (
337
+ <Segment>
338
+ <Form onSubmit={this.onSearch}>
339
+ <Form.Field>
340
+ <Input
341
+ name="SearchableText"
342
+ action={{
343
+ icon: 'search',
344
+ loading: isLoading,
345
+ disabled: isLoading,
346
+ }}
347
+ placeholder={this.props.intl.formatMessage(
348
+ messages.searchForUserOrGroup,
349
+ )}
350
+ onChange={this.onChangeSearch}
351
+ id="sharing-component-search"
352
+ />
353
+ </Form.Field>
354
+ </Form>
355
+ </Segment>
356
+ );
357
+ }}
335
358
  </Plug>
336
359
  <Plug
337
360
  pluggable="sharing-component"
@@ -14,6 +14,7 @@ import {
14
14
  } from '@plone/volto/helpers/Url/Url';
15
15
 
16
16
  import config from '@plone/volto/registry';
17
+ import cx from 'classnames';
17
18
 
18
19
  const UniversalLink = ({
19
20
  href,
@@ -88,19 +89,16 @@ const UniversalLink = ({
88
89
  );
89
90
 
90
91
  if (isExternal) {
92
+ const isTelephoneOrMail = checkedURL.isMail || checkedURL.isTelephone;
91
93
  tag = (
92
94
  <a
93
95
  href={url}
94
96
  title={title}
95
97
  target={
96
- !checkedURL.isMail &&
97
- !checkedURL.isTelephone &&
98
- !(openLinkInNewTab === false)
99
- ? '_blank'
100
- : null
98
+ !isTelephoneOrMail && !(openLinkInNewTab === false) ? '_blank' : null
101
99
  }
102
100
  rel="noopener noreferrer"
103
- className={className}
101
+ className={cx({ external: !isTelephoneOrMail }, className)}
104
102
  {...props}
105
103
  >
106
104
  {children}
@@ -325,7 +325,9 @@ class ArrayWidget extends Component {
325
325
  : this.props.choices
326
326
  ? [
327
327
  ...choices,
328
- ...(this.props.noValueOption && !this.props.default
328
+ ...(this.props.noValueOption &&
329
+ (this.props.default === undefined ||
330
+ this.props.default === null)
329
331
  ? [
330
332
  {
331
333
  label: this.props.intl.formatMessage(