@plone/volto 18.6.0 → 18.8.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/cypress/support/commands.js +22 -0
  3. package/locales/nl/LC_MESSAGES/volto.po +9 -9
  4. package/locales/nl.json +1 -1
  5. package/package.json +4 -4
  6. package/src/components/manage/Blocks/Block/BlocksForm.jsx +6 -4
  7. package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +1 -0
  8. package/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx +18 -14
  9. package/src/components/manage/Controlpanels/Relations/Relations.jsx +48 -46
  10. package/src/components/manage/Toolbar/More.jsx +113 -117
  11. package/src/components/manage/Toolbar/More.test.jsx +0 -24
  12. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +15 -2
  13. package/src/components/manage/Widgets/RegistryImageWidget.jsx +15 -16
  14. package/src/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory.jsx +53 -56
  15. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +23 -0
  16. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +135 -0
  17. package/src/components/theme/Image/Image.jsx +8 -1
  18. package/src/components/theme/View/View.jsx +2 -0
  19. package/src/config/ControlPanels.js +0 -1
  20. package/src/config/index.js +0 -1
  21. package/src/express-middleware/robotstxt.js +4 -5
  22. package/src/helpers/Api/Api.js +1 -1
  23. package/src/helpers/Blocks/Blocks.js +26 -4
  24. package/src/helpers/Blocks/Blocks.test.js +46 -0
  25. package/src/helpers/FormValidation/validators.ts +3 -1
  26. package/src/helpers/Robots/Robots.js +12 -31
  27. package/src/hooks/clipboard/useClipboard.js +7 -3
  28. package/types/components/manage/Widgets/ObjectBrowserWidget.d.ts +1 -0
  29. package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.d.ts +1 -0
  30. package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.d.ts +1 -0
  31. package/types/helpers/Api/Api.d.ts +7 -0
  32. package/types/helpers/Blocks/Blocks.d.ts +15 -5
  33. package/public/robots.txt +0 -2
@@ -60,22 +60,26 @@ const BrokenRelations = () => {
60
60
  }).map((el, index) => (
61
61
  <Table.Row key={index}>
62
62
  <Table.Cell>
63
- <ConditionalLink
64
- to={`${el[0]}/edit`}
65
- openLinkInNewTab={true}
66
- condition={el[0].includes('http')}
67
- >
68
- {flattenToAppURL(el[0])}
69
- </ConditionalLink>
63
+ {el[0] && (
64
+ <ConditionalLink
65
+ to={`${el[0]}/edit`}
66
+ openLinkInNewTab={true}
67
+ condition={el[0].includes('http')}
68
+ >
69
+ {flattenToAppURL(el[0])}
70
+ </ConditionalLink>
71
+ )}
70
72
  </Table.Cell>
71
73
  <Table.Cell>
72
- <ConditionalLink
73
- to={`${el[1]}/edit`}
74
- openLinkInNewTab={true}
75
- condition={el[1].includes('http')}
76
- >
77
- {flattenToAppURL(el[1])}
78
- </ConditionalLink>
74
+ {el[1] && (
75
+ <ConditionalLink
76
+ to={`${el[1]}/edit`}
77
+ openLinkInNewTab={true}
78
+ condition={el[1].includes('http')}
79
+ >
80
+ {flattenToAppURL(el[1])}
81
+ </ConditionalLink>
82
+ )}
79
83
  </Table.Cell>
80
84
  </Table.Row>
81
85
  ))}
@@ -47,53 +47,55 @@ const RelationsControlPanel = () => {
47
47
 
48
48
  return (
49
49
  <>
50
- <div className="relations-control-panel">
51
- <Helmet title={intl.formatMessage(messages.relations)} />
52
- {can_edit ? (
53
- <Segment.Group raised>
54
- <Segment className="primary">
55
- {brokenRelations && Object.keys(brokenRelations).length > 0 ? (
56
- <React.Fragment>
57
- <Message warning>
58
- <FormattedMessage
59
- id="Some relations are broken. Please fix."
60
- defaultMessage="Some relations are broken. Please fix."
61
- />
62
- </Message>
63
- <Divider hidden />
64
- </React.Fragment>
65
- ) : null}
66
- <h1>
50
+ <div className="ui container">
51
+ <div className="relations-control-panel">
52
+ <Helmet title={intl.formatMessage(messages.relations)} />
53
+ {can_edit ? (
54
+ <Segment.Group raised>
55
+ <Segment className="primary">
56
+ {brokenRelations && Object.keys(brokenRelations).length > 0 ? (
57
+ <React.Fragment>
58
+ <Message warning>
59
+ <FormattedMessage
60
+ id="Some relations are broken. Please fix."
61
+ defaultMessage="Some relations are broken. Please fix."
62
+ />
63
+ </Message>
64
+ <Divider hidden />
65
+ </React.Fragment>
66
+ ) : null}
67
+ <h1>
68
+ <FormattedMessage id="Relations" defaultMessage="Relations" />
69
+ </h1>
70
+ {relations_stats?.error ? (
71
+ <React.Fragment>
72
+ <Divider hidden />
73
+ <Message warning>
74
+ <FormattedMessage
75
+ id="Please upgrade to plone.restapi >= 8.39.0."
76
+ defaultMessage="Please upgrade to plone.restapi >= 8.39.0."
77
+ />
78
+ </Message>
79
+ </React.Fragment>
80
+ ) : null}
81
+ </Segment>
82
+ <Segment>
83
+ <RelationsMatrix />
84
+ </Segment>
85
+ </Segment.Group>
86
+ ) : (
87
+ <Segment.Group>
88
+ <Segment>
67
89
  <FormattedMessage id="Relations" defaultMessage="Relations" />
68
- </h1>
69
- {relations_stats?.error ? (
70
- <React.Fragment>
71
- <Divider hidden />
72
- <Message warning>
73
- <FormattedMessage
74
- id="Please upgrade to plone.restapi >= 8.39.0."
75
- defaultMessage="Please upgrade to plone.restapi >= 8.39.0."
76
- />
77
- </Message>
78
- </React.Fragment>
79
- ) : null}
80
- </Segment>
81
- <Segment>
82
- <RelationsMatrix />
83
- </Segment>
84
- </Segment.Group>
85
- ) : (
86
- <Segment.Group>
87
- <Segment>
88
- <FormattedMessage id="Relations" defaultMessage="Relations" />
89
- <Divider hidden />
90
- <FormattedMessage
91
- id="You have not the required permission for this control panel."
92
- defaultMessage="You have not the required permission for this control panel."
93
- />
94
- </Segment>
95
- </Segment.Group>
96
- )}
90
+ <Divider hidden />
91
+ <FormattedMessage
92
+ id="You have not the required permission for this control panel."
93
+ defaultMessage="You have not the required permission for this control panel."
94
+ />
95
+ </Segment>
96
+ </Segment.Group>
97
+ )}
98
+ </div>
97
99
  </div>
98
100
 
99
101
  {isClient &&
@@ -191,6 +191,13 @@ const More = (props) => {
191
191
  id: 'redirection',
192
192
  });
193
193
 
194
+ const workingCopyCheckoutAction = find(actions.object_buttons, {
195
+ id: 'iterate_checkout',
196
+ });
197
+ const workingCopyCheckinAction = find(actions.object_buttons, {
198
+ id: 'iterate_checkin',
199
+ });
200
+
194
201
  const dateOptions = {
195
202
  year: 'numeric',
196
203
  month: 'long',
@@ -320,125 +327,114 @@ const More = (props) => {
320
327
  </>
321
328
  )}
322
329
  </Pluggable>
323
- {config.settings.hasWorkingCopySupport &&
324
- content['@type'] !== 'Plone Site' && (
325
- <>
326
- {!content.working_copy && (
327
- <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
328
- <li>
329
- <button
330
- aria-label={intl.formatMessage(messages.CreateWorkingCopy)}
331
- onClick={() => {
332
- dispatch(createWorkingCopy(path)).then((response) => {
333
- history.push(flattenToAppURL(response['@id']));
334
- props.closeMenu();
335
- });
336
- }}
337
- >
338
- {intl.formatMessage(messages.CreateWorkingCopy)}
330
+ {workingCopyCheckoutAction && (
331
+ <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
332
+ <li>
333
+ <button
334
+ aria-label={intl.formatMessage(messages.CreateWorkingCopy)}
335
+ onClick={() => {
336
+ dispatch(createWorkingCopy(path)).then((response) => {
337
+ history.push(flattenToAppURL(response['@id']));
338
+ props.closeMenu();
339
+ });
340
+ }}
341
+ >
342
+ {intl.formatMessage(messages.CreateWorkingCopy)}
339
343
 
340
- <Icon name={rightArrowSVG} size="24px" />
341
- </button>
342
- </li>
343
- </Plug>
344
- )}
345
- {content.working_copy && content.working_copy_of && (
346
- <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
347
- <li>
348
- <button
349
- aria-label={intl.formatMessage(messages.applyWorkingCopy)}
350
- onClick={() => {
351
- dispatch(applyWorkingCopy(path)).then((response) => {
352
- history.push(
353
- flattenToAppURL(content.working_copy_of['@id']),
354
- );
355
- props.closeMenu();
356
- toast.info(
357
- <Toast
358
- info
359
- title={intl.formatMessage(
360
- messages.workingAppliedTitle,
361
- )}
362
- content={intl.formatMessage(
363
- messages.workingCopyAppliedBy,
364
- {
365
- creator: content.working_copy?.creator_name,
366
- date: (
367
- <FormattedDate
368
- date={content.working_copy?.created}
369
- format={dateOptions}
370
- />
371
- ),
372
- },
373
- )}
374
- />,
375
- {
376
- toastId: 'workingcopyapplyinfo',
377
- autoClose: 10000,
378
- },
379
- );
380
- });
381
- }}
382
- >
383
- {intl.formatMessage(messages.applyWorkingCopy)}
344
+ <Icon name={rightArrowSVG} size="24px" />
345
+ </button>
346
+ </li>
347
+ </Plug>
348
+ )}
349
+ {workingCopyCheckinAction && (
350
+ <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
351
+ <li>
352
+ <button
353
+ aria-label={intl.formatMessage(messages.applyWorkingCopy)}
354
+ onClick={() => {
355
+ dispatch(applyWorkingCopy(path)).then((response) => {
356
+ history.push(flattenToAppURL(content.working_copy_of['@id']));
357
+ props.closeMenu();
358
+ toast.info(
359
+ <Toast
360
+ info
361
+ title={intl.formatMessage(messages.workingAppliedTitle)}
362
+ content={intl.formatMessage(
363
+ messages.workingCopyAppliedBy,
364
+ {
365
+ creator: content.working_copy?.creator_name,
366
+ date: (
367
+ <FormattedDate
368
+ date={content.working_copy?.created}
369
+ format={dateOptions}
370
+ />
371
+ ),
372
+ },
373
+ )}
374
+ />,
375
+ {
376
+ toastId: 'workingcopyapplyinfo',
377
+ autoClose: 10000,
378
+ },
379
+ );
380
+ });
381
+ }}
382
+ >
383
+ {intl.formatMessage(messages.applyWorkingCopy)}
384
384
 
385
- <Icon
386
- name={applySVG}
387
- size="24px"
388
- title={intl.formatMessage(messages.applyWorkingCopy)}
389
- />
390
- </button>
391
- </li>
392
- <li>
393
- <button
394
- aria-label={intl.formatMessage(messages.removeWorkingCopy)}
395
- onClick={() => {
396
- dispatch(removeWorkingCopy(path)).then((response) => {
397
- history.push(
398
- flattenToAppURL(content.working_copy_of['@id']),
399
- );
400
- props.closeMenu();
401
- toast.info(
402
- <Toast
403
- info
404
- title={intl.formatMessage(
405
- messages.workingCopyRemovedTitle,
406
- )}
407
- />,
408
- {
409
- toastId: 'workingcopyremovednotice',
410
- autoClose: 10000,
411
- },
412
- );
413
- });
414
- }}
415
- >
416
- {intl.formatMessage(messages.removeWorkingCopy)}
417
- <Icon
418
- name={removeSVG}
419
- size="24px"
420
- color="#e40166"
421
- title={intl.formatMessage(messages.removeWorkingCopy)}
422
- />
423
- </button>
424
- </li>
425
- </Plug>
426
- )}
427
- {content.working_copy && !content.working_copy_of && (
428
- <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
429
- <li>
430
- <Link
431
- to={flattenToAppURL(content.working_copy['@id'])}
432
- onClick={() => props.closeMenu()}
433
- >
434
- {intl.formatMessage(messages.viewWorkingCopy)}
435
- <Icon name={rightArrowSVG} size="24px" />
436
- </Link>
437
- </li>
438
- </Plug>
439
- )}
440
- </>
441
- )}
385
+ <Icon
386
+ name={applySVG}
387
+ size="24px"
388
+ title={intl.formatMessage(messages.applyWorkingCopy)}
389
+ />
390
+ </button>
391
+ </li>
392
+ <li>
393
+ <button
394
+ aria-label={intl.formatMessage(messages.removeWorkingCopy)}
395
+ onClick={() => {
396
+ dispatch(removeWorkingCopy(path)).then((response) => {
397
+ history.push(flattenToAppURL(content.working_copy_of['@id']));
398
+ props.closeMenu();
399
+ toast.info(
400
+ <Toast
401
+ info
402
+ title={intl.formatMessage(
403
+ messages.workingCopyRemovedTitle,
404
+ )}
405
+ />,
406
+ {
407
+ toastId: 'workingcopyremovednotice',
408
+ autoClose: 10000,
409
+ },
410
+ );
411
+ });
412
+ }}
413
+ >
414
+ {intl.formatMessage(messages.removeWorkingCopy)}
415
+ <Icon
416
+ name={removeSVG}
417
+ size="24px"
418
+ color="#e40166"
419
+ title={intl.formatMessage(messages.removeWorkingCopy)}
420
+ />
421
+ </button>
422
+ </li>
423
+ </Plug>
424
+ )}
425
+ {content.working_copy && !content.working_copy_of && (
426
+ <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
427
+ <li>
428
+ <Link
429
+ to={flattenToAppURL(content.working_copy['@id'])}
430
+ onClick={() => props.closeMenu()}
431
+ >
432
+ {intl.formatMessage(messages.viewWorkingCopy)}
433
+ <Icon name={rightArrowSVG} size="24px" />
434
+ </Link>
435
+ </li>
436
+ </Plug>
437
+ )}
442
438
  {editAction && config.settings.isMultilingual && (
443
439
  <Plug pluggable="toolbar-more-manage-content" id="multilingual">
444
440
  <li>
@@ -1,10 +1,8 @@
1
- import React from 'react';
2
1
  import configureStore from 'redux-mock-store';
3
2
  import { Provider } from 'react-intl-redux';
4
3
  import { MemoryRouter } from 'react-router-dom';
5
4
  import { PluggablesProvider } from '@plone/volto/components/manage/Pluggable';
6
5
  import { waitFor, render } from '@testing-library/react';
7
- import config from '@plone/volto/registry';
8
6
 
9
7
  import More from './More';
10
8
 
@@ -162,26 +160,4 @@ describe('Toolbar More component', () => {
162
160
  await waitFor(() => {});
163
161
  expect(container).toMatchSnapshot();
164
162
  });
165
- it('renders a Toolbar More component with manage content (working copy)', async () => {
166
- config.settings.hasWorkingCopySupport = true;
167
-
168
- const { container } = render(
169
- <PluggablesProvider>
170
- <Provider store={store}>
171
- <MemoryRouter>
172
- <More
173
- pathname="/blah"
174
- loadComponent={() => {}}
175
- theToolbar={{
176
- current: { getBoundingClientRect: () => ({ width: '320' }) },
177
- }}
178
- closeMenu={() => {}}
179
- />
180
- </MemoryRouter>
181
- </Provider>
182
- </PluggablesProvider>,
183
- );
184
- await waitFor(() => {});
185
- expect(container).toMatchSnapshot();
186
- });
187
163
  });
@@ -16,10 +16,10 @@ import { Image, Label, Popup, Button } from 'semantic-ui-react';
16
16
  import {
17
17
  flattenToAppURL,
18
18
  isInternalURL,
19
- isUrl,
20
19
  normalizeUrl,
21
20
  removeProtocol,
22
21
  } from '@plone/volto/helpers/Url/Url';
22
+ import { urlValidator } from '@plone/volto/helpers/FormValidation/validators';
23
23
  import { searchContent } from '@plone/volto/actions/search/search';
24
24
  import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
25
25
  import { defineMessages, injectIntl } from 'react-intl';
@@ -102,6 +102,7 @@ export class ObjectBrowserWidgetComponent extends Component {
102
102
  state = {
103
103
  manualLinkInput: '',
104
104
  validURL: false,
105
+ errors: [],
105
106
  };
106
107
 
107
108
  constructor(props) {
@@ -230,7 +231,16 @@ export class ObjectBrowserWidgetComponent extends Component {
230
231
 
231
232
  validateManualLink = (url) => {
232
233
  if (this.props.allowExternals) {
233
- return isUrl(url);
234
+ const error = urlValidator({
235
+ value: url,
236
+ formatMessage: this.props.intl.formatMessage,
237
+ });
238
+ if (error && url !== '') {
239
+ this.setState({ errors: [error] });
240
+ } else {
241
+ this.setState({ errors: [] });
242
+ }
243
+ return !Boolean(error);
234
244
  } else {
235
245
  return isInternalURL(url);
236
246
  }
@@ -344,6 +354,8 @@ export class ObjectBrowserWidgetComponent extends Component {
344
354
  return (
345
355
  <FormFieldWrapper
346
356
  {...this.props}
357
+ // At the moment, OBW handles its own errors and validation
358
+ error={this.state.errors}
347
359
  className={description ? 'help text' : 'text'}
348
360
  >
349
361
  <div
@@ -372,6 +384,7 @@ export class ObjectBrowserWidgetComponent extends Component {
372
384
  items.length === 0 &&
373
385
  this.props.mode !== 'multiple' && (
374
386
  <input
387
+ onBlur={this.onSubmitManualLink}
375
388
  onKeyDown={this.onKeyDownManualLink}
376
389
  onChange={this.onManualLinkInput}
377
390
  value={this.state.manualLinkInput}
@@ -3,7 +3,7 @@
3
3
  * @module components/manage/Widgets/RegistryImageWidget
4
4
  */
5
5
 
6
- import React from 'react';
6
+ import React, { useState } from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { Button, Image, Dimmer } from 'semantic-ui-react';
9
9
  import { readAsDataURL } from 'promise-file-reader';
@@ -76,12 +76,15 @@ const RegistryImageWidget = (props) => {
76
76
  const { id, value, onChange, isDisabled } = props;
77
77
  const intl = useIntl();
78
78
 
79
- const fileName = value?.split(';')[0];
80
- const imgsrc = fileName
81
- ? `${toPublicURL('/')}@@site-logo/${atob(
82
- fileName.replace('filenameb64:', ''),
83
- )}`
84
- : '';
79
+ // State to manage the preview image source
80
+ const [previewSrc, setPreviewSrc] = useState(() => {
81
+ const fileName = value?.split(';')[0];
82
+ return fileName
83
+ ? `${toPublicURL('/')}@@site-logo/${atob(
84
+ fileName.replace('filenameb64:', ''),
85
+ )}`
86
+ : '';
87
+ });
85
88
 
86
89
  /**
87
90
  * Drop handler
@@ -102,8 +105,7 @@ const RegistryImageWidget = (props) => {
102
105
  reader.onload = function () {
103
106
  const fields = reader.result.match(/^data:(.*);(.*),(.*)$/);
104
107
  if (imageMimetypes.includes(fields[1])) {
105
- let imagePreview = document.getElementById(`field-${id}-image`);
106
- imagePreview.src = reader.result;
108
+ setPreviewSrc(reader.result);
107
109
  }
108
110
  };
109
111
  reader.readAsDataURL(files[0]);
@@ -115,12 +117,12 @@ const RegistryImageWidget = (props) => {
115
117
  {({ getRootProps, getInputProps, isDragActive }) => (
116
118
  <div className="file-widget-dropzone" {...getRootProps()}>
117
119
  {isDragActive && <Dimmer active></Dimmer>}
118
- {imgsrc ? (
120
+ {previewSrc ? (
119
121
  <Image
120
122
  className="image-preview"
121
123
  id={`field-${id}-image`}
122
124
  size="small"
123
- src={imgsrc}
125
+ src={previewSrc}
124
126
  />
125
127
  ) : (
126
128
  <div className="dropzone-placeholder">
@@ -139,7 +141,6 @@ const RegistryImageWidget = (props) => {
139
141
  )}
140
142
  </div>
141
143
  )}
142
-
143
144
  <label className="label-file-widget-input">
144
145
  {value
145
146
  ? intl.formatMessage(messages.replaceFile)
@@ -168,6 +169,7 @@ const RegistryImageWidget = (props) => {
168
169
  disabled={isDisabled}
169
170
  onClick={() => {
170
171
  onChange(id, '');
172
+ setPreviewSrc(''); // Clear the preview image
171
173
  }}
172
174
  >
173
175
  <Icon name={deleteSVG} size="20px" />
@@ -189,10 +191,7 @@ RegistryImageWidget.propTypes = {
189
191
  description: PropTypes.string,
190
192
  required: PropTypes.bool,
191
193
  error: PropTypes.arrayOf(PropTypes.string),
192
- value: PropTypes.shape({
193
- '@type': PropTypes.string,
194
- title: PropTypes.string,
195
- }),
194
+ value: PropTypes.string,
196
195
  onChange: PropTypes.func.isRequired,
197
196
  wrapped: PropTypes.bool,
198
197
  };