@plone/volto 18.0.0-alpha.34 → 18.0.0-alpha.36

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 (78) hide show
  1. package/.release-it.json +1 -1
  2. package/CHANGELOG.md +43 -0
  3. package/locales/ca/LC_MESSAGES/volto.po +41 -4
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +41 -4
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +41 -4
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +41 -4
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +41 -4
  12. package/locales/eu.json +1 -1
  13. package/locales/fi/LC_MESSAGES/volto.po +44 -7
  14. package/locales/fi.json +1 -1
  15. package/locales/fr/LC_MESSAGES/volto.po +41 -4
  16. package/locales/fr.json +1 -1
  17. package/locales/hi/LC_MESSAGES/volto.po +41 -4
  18. package/locales/hi.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +41 -4
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +41 -4
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +41 -4
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +41 -4
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +41 -4
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +41 -4
  30. package/locales/ro.json +1 -1
  31. package/locales/volto.pot +42 -5
  32. package/locales/zh_CN/LC_MESSAGES/volto.po +41 -4
  33. package/locales/zh_CN.json +1 -1
  34. package/package.json +5 -5
  35. package/razzle.config.js +7 -1
  36. package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +78 -72
  37. package/src/components/manage/AnchorPlugin/useLinkEditor.js +79 -0
  38. package/src/components/manage/Blocks/Image/Edit.jsx +45 -347
  39. package/src/components/manage/Blocks/Teaser/Data.jsx +69 -1
  40. package/src/components/manage/Blocks/Teaser/schema.js +24 -3
  41. package/src/components/manage/Form/BlocksToolbar.jsx +8 -7
  42. package/src/components/manage/Form/Form.jsx +8 -1
  43. package/src/components/manage/Form/InlineForm.jsx +4 -0
  44. package/src/components/manage/Widgets/ImageWidget.jsx +311 -0
  45. package/src/components/manage/Widgets/ImageWidget.stories.jsx +46 -0
  46. package/src/config/NonContentRoutes.jsx +2 -1
  47. package/src/config/Widgets.jsx +2 -0
  48. package/src/config/index.js +0 -1
  49. package/src/helpers/Blocks/Blocks.js +20 -7
  50. package/src/helpers/Blocks/Blocks.test.js +55 -0
  51. package/src/helpers/Blocks/cloneBlocks.test.js +54 -0
  52. package/src/helpers/Blocks/cloneBlocks.ts +43 -0
  53. package/src/helpers/index.js +1 -0
  54. package/src/icons/external-link.svg +4 -0
  55. package/theme/themes/pastanaga/collections/form.overrides +4 -0
  56. package/theme/themes/pastanaga/elements/button.overrides +2 -1
  57. package/theme/themes/pastanaga/extras/blocks.less +40 -4
  58. package/theme/themes/pastanaga/extras/grid.less +3 -3
  59. package/theme/themes/pastanaga/extras/teaser.less +53 -0
  60. package/theme/themes/pastanaga/extras/widgets.less +108 -0
  61. package/types/components/manage/AnchorPlugin/useLinkEditor.d.ts +7 -0
  62. package/types/components/manage/Blocks/Teaser/schema.d.ts +9 -2
  63. package/types/components/manage/Widgets/ImageWidget.d.ts +10 -0
  64. package/types/components/manage/Widgets/ImageWidget.stories.d.ts +11 -0
  65. package/types/config/Widgets.d.ts +2 -0
  66. package/types/helpers/Blocks/Blocks.d.ts +6 -0
  67. package/types/helpers/Blocks/cloneBlocks.d.ts +1 -0
  68. package/types/helpers/Blocks/cloneBlocks.test.d.ts +1 -0
  69. package/types/helpers/index.d.ts +1 -1
  70. /package/src/components/manage/Blocks/Image/{schema.js → schema.jsx} +0 -0
  71. /package/src/components/theme/LanguageSelector/{LanguageSelector.js → LanguageSelector.jsx} +0 -0
  72. /package/src/helpers/Extensions/{withBlockExtensions.js → withBlockExtensions.jsx} +0 -0
  73. /package/src/helpers/Extensions/{withBlockSchemaEnhancer.js → withBlockSchemaEnhancer.jsx} +0 -0
  74. /package/src/helpers/FormValidation/{FormValidation.js → FormValidation.jsx} +0 -0
  75. /package/src/helpers/Helmet/{Helmet.js → Helmet.jsx} +0 -0
  76. /package/src/helpers/Loadable/{Loadable.js → Loadable.jsx} +0 -0
  77. /package/src/helpers/Loadable/__mocks__/{Loadable.js → Loadable.jsx} +0 -0
  78. /package/src/helpers/Utils/{Utils.js → Utils.jsx} +0 -0
@@ -3,256 +3,57 @@
3
3
  * @module components/manage/Blocks/Image/Edit
4
4
  */
5
5
 
6
- import React, { Component } from 'react';
7
- import PropTypes from 'prop-types';
6
+ import React from 'react';
8
7
  import { connect } from 'react-redux';
9
8
  import { compose } from 'redux';
10
- import { readAsDataURL } from 'promise-file-reader';
11
- import { Button, Dimmer, Input, Loader, Message } from 'semantic-ui-react';
12
- import { defineMessages, injectIntl } from 'react-intl';
13
- import loadable from '@loadable/component';
14
- import cx from 'classnames';
15
- import { isEqual } from 'lodash';
16
9
 
17
- import { Icon, ImageSidebar, SidebarPortal } from '@plone/volto/components';
10
+ import { injectIntl } from 'react-intl';
11
+ import cx from 'classnames';
12
+ import { ImageSidebar, SidebarPortal } from '@plone/volto/components';
18
13
  import { createContent } from '@plone/volto/actions';
14
+
19
15
  import {
20
16
  flattenToAppURL,
21
- getBaseUrl,
22
17
  isInternalURL,
23
18
  withBlockExtensions,
24
- validateFileUploadSize,
25
19
  } from '@plone/volto/helpers';
26
20
  import config from '@plone/volto/registry';
27
21
 
28
- import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
29
- import clearSVG from '@plone/volto/icons/clear.svg';
30
- import navTreeSVG from '@plone/volto/icons/nav.svg';
31
- import aheadSVG from '@plone/volto/icons/ahead.svg';
32
- import uploadSVG from '@plone/volto/icons/upload.svg';
33
-
34
- const Dropzone = loadable(() => import('react-dropzone'));
35
-
36
- const messages = defineMessages({
37
- ImageBlockInputPlaceholder: {
38
- id: 'Browse the site, drop an image, or type an URL',
39
- defaultMessage: 'Browse the site, drop an image, or type an URL',
40
- },
41
- uploadingImage: {
42
- id: 'Uploading image',
43
- defaultMessage: 'Uploading image',
44
- },
45
- });
22
+ import { ImageInput } from '@plone/volto/components/manage/Widgets/ImageWidget';
46
23
 
47
24
  /**
48
- * Edit image block class.
49
- * @class Edit
50
- * @extends Component
25
+ * Edit image block function.
26
+ * @function Edit
51
27
  */
52
- class Edit extends Component {
53
- /**
54
- * Property types.
55
- * @property {Object} propTypes Property types.
56
- * @static
57
- */
58
- static propTypes = {
59
- selected: PropTypes.bool.isRequired,
60
- block: PropTypes.string.isRequired,
61
- index: PropTypes.number.isRequired,
62
- data: PropTypes.objectOf(PropTypes.any).isRequired,
63
- content: PropTypes.objectOf(PropTypes.any),
64
- request: PropTypes.shape({
65
- loading: PropTypes.bool,
66
- loaded: PropTypes.bool,
67
- }).isRequired,
68
- pathname: PropTypes.string.isRequired,
69
- onChangeBlock: PropTypes.func.isRequired,
70
- onSelectBlock: PropTypes.func.isRequired,
71
- onDeleteBlock: PropTypes.func.isRequired,
72
- onFocusPreviousBlock: PropTypes.func.isRequired,
73
- onFocusNextBlock: PropTypes.func.isRequired,
74
- handleKeyDown: PropTypes.func.isRequired,
75
- createContent: PropTypes.func.isRequired,
76
- openObjectBrowser: PropTypes.func.isRequired,
77
- };
78
28
 
79
- state = {
80
- uploading: false,
81
- url: '',
82
- dragging: false,
83
- };
84
-
85
- /**
86
- * Component will receive props
87
- * @method componentWillReceiveProps
88
- * @param {Object} nextProps Next properties
89
- * @returns {undefined}
90
- */
91
- UNSAFE_componentWillReceiveProps(nextProps) {
92
- // Update block data after upload finished
93
- if (
94
- this.props.request.loading &&
95
- nextProps.request.loaded &&
96
- this.state.uploading
97
- ) {
98
- this.setState({
99
- uploading: false,
100
- });
101
- this.props.onChangeBlock(this.props.block, {
102
- ...this.props.data,
103
- url: nextProps.content['@id'],
104
- image_field: 'image',
105
- image_scales: { image: [nextProps.content.image] },
106
- alt: '',
29
+ // const messages = defineMessages({
30
+ // notImage: {
31
+ // id: 'The provided link does not lead to an image.',
32
+ // defaultMessage: 'The provided link does not lead to an image.',
33
+ // },
34
+ // });
35
+
36
+ function Edit(props) {
37
+ const { data } = props;
38
+ const Image = config.getComponent({ name: 'Image' }).component;
39
+
40
+ const handleChange = React.useCallback(
41
+ async (id, image, { title, image_field, image_scales } = {}) => {
42
+ const url = image ? image['@id'] || image : '';
43
+
44
+ props.onChangeBlock(props.block, {
45
+ ...props.data,
46
+ url: flattenToAppURL(url),
47
+ image_field,
48
+ image_scales,
49
+ alt: props.data.alt || title || '',
107
50
  });
108
- }
109
- }
110
-
111
- /**
112
- * @param {*} nextProps
113
- * @returns {boolean}
114
- * @memberof Edit
115
- */
116
- shouldComponentUpdate(nextProps) {
117
- return (
118
- this.props.selected ||
119
- nextProps.selected ||
120
- !isEqual(this.props.data, nextProps.data)
121
- );
122
- }
123
-
124
- /**
125
- * Upload image handler (not used), but useful in case that we want a button
126
- * not powered by react-dropzone
127
- * @method onUploadImage
128
- * @returns {undefined}
129
- */
130
- onUploadImage = (e) => {
131
- e.stopPropagation();
132
- const file = e.target.files[0];
133
- if (!validateFileUploadSize(file, this.props.intl.formatMessage)) return;
134
- this.setState({
135
- uploading: true,
136
- });
137
- readAsDataURL(file).then((data) => {
138
- const fields = data.match(/^data:(.*);(.*),(.*)$/);
139
- this.props.createContent(
140
- getBaseUrl(this.props.pathname),
141
- {
142
- '@type': 'Image',
143
- title: file.name,
144
- image: {
145
- data: fields[3],
146
- encoding: fields[2],
147
- 'content-type': fields[1],
148
- filename: file.name,
149
- },
150
- },
151
- this.props.block,
152
- );
153
- });
154
- };
51
+ },
52
+ [props],
53
+ );
155
54
 
156
- /**
157
- * Change url handler
158
- * @method onChangeUrl
159
- * @param {Object} target Target object
160
- * @returns {undefined}
161
- */
162
- onChangeUrl = ({ target }) => {
163
- this.setState({
164
- url: target.value,
165
- });
166
- };
167
-
168
- /**
169
- * Submit url handler
170
- * @method onSubmitUrl
171
- * @param {object} e Event
172
- * @returns {undefined}
173
- */
174
- onSubmitUrl = () => {
175
- this.props.onChangeBlock(this.props.block, {
176
- ...this.props.data,
177
- url: flattenToAppURL(this.state.url),
178
- image_field: undefined,
179
- image_scales: undefined,
180
- });
181
- };
182
-
183
- /**
184
- * Drop handler
185
- * @method onDrop
186
- * @param {array} files File objects
187
- * @returns {undefined}
188
- */
189
- onDrop = (files) => {
190
- if (!validateFileUploadSize(files[0], this.props.intl.formatMessage)) {
191
- this.setState({ dragging: false });
192
- return;
193
- }
194
- this.setState({ uploading: true });
195
-
196
- readAsDataURL(files[0]).then((data) => {
197
- const fields = data.match(/^data:(.*);(.*),(.*)$/);
198
- this.props.createContent(
199
- getBaseUrl(this.props.pathname),
200
- {
201
- '@type': 'Image',
202
- title: files[0].name,
203
- image: {
204
- data: fields[3],
205
- encoding: fields[2],
206
- 'content-type': fields[1],
207
- filename: files[0].name,
208
- },
209
- },
210
- this.props.block,
211
- );
212
- });
213
- };
214
-
215
- /**
216
- * Keydown handler on Variant Menu Form
217
- * This is required since the ENTER key is already mapped to a onKeyDown
218
- * event and needs to be overriden with a child onKeyDown.
219
- * @method onKeyDownVariantMenuForm
220
- * @param {Object} e Event object
221
- * @returns {undefined}
222
- */
223
- onKeyDownVariantMenuForm = (e) => {
224
- if (e.key === 'Enter') {
225
- e.preventDefault();
226
- e.stopPropagation();
227
- this.onSubmitUrl();
228
- } else if (e.key === 'Escape') {
229
- e.preventDefault();
230
- e.stopPropagation();
231
- // TODO: Do something on ESC key
232
- }
233
- };
234
- onDragEnter = () => {
235
- this.setState({ dragging: true });
236
- };
237
- onDragLeave = () => {
238
- this.setState({ dragging: false });
239
- };
240
-
241
- node = React.createRef();
242
-
243
- /**
244
- * Render method.
245
- * @method render
246
- * @returns {string} Markup for the component.
247
- */
248
- render() {
249
- const Image = config.getComponent({ name: 'Image' }).component;
250
- const { data } = this.props;
251
- const placeholder =
252
- this.props.data.placeholder ||
253
- this.props.intl.formatMessage(messages.ImageBlockInputPlaceholder);
254
-
255
- return (
55
+ return (
56
+ <>
256
57
  <div
257
58
  className={cx(
258
59
  'block image align',
@@ -303,123 +104,20 @@ class Edit extends Component {
303
104
  responsive={true}
304
105
  />
305
106
  ) : (
306
- <div>
307
- {this.props.editable && (
308
- <Dropzone
309
- noClick
310
- onDrop={this.onDrop}
311
- onDragEnter={this.onDragEnter}
312
- onDragLeave={this.onDragLeave}
313
- className="dropzone"
314
- >
315
- {({ getRootProps, getInputProps }) => (
316
- <div {...getRootProps()}>
317
- <Message>
318
- {this.state.dragging && <Dimmer active></Dimmer>}
319
- {this.state.uploading && (
320
- <Dimmer active>
321
- <Loader indeterminate>
322
- {this.props.intl.formatMessage(
323
- messages.uploadingImage,
324
- )}
325
- </Loader>
326
- </Dimmer>
327
- )}
328
- <div className="no-image-wrapper">
329
- <img src={imageBlockSVG} alt="" />
330
- <div className="toolbar-inner">
331
- <Button.Group>
332
- <Button
333
- basic
334
- icon
335
- onClick={(e) => {
336
- e.stopPropagation();
337
- e.preventDefault();
338
- this.props.openObjectBrowser({
339
- onSelectItem: (
340
- url,
341
- { title, image_field, image_scales },
342
- ) => {
343
- this.props.onChangeBlock(this.props.block, {
344
- ...this.props.data,
345
- url,
346
- image_field,
347
- image_scales,
348
- alt: this.props.data.alt || title || '',
349
- });
350
- },
351
- });
352
- }}
353
- >
354
- <Icon name={navTreeSVG} size="24px" />
355
- </Button>
356
- </Button.Group>
357
- <Button.Group>
358
- <label className="ui button basic icon">
359
- <Icon name={uploadSVG} size="24px" />
360
- <input
361
- {...getInputProps({
362
- type: 'file',
363
- onChange: this.onUploadImage,
364
- style: { display: 'none' },
365
- })}
366
- />
367
- </label>
368
- </Button.Group>
369
- <Input
370
- onKeyDown={this.onKeyDownVariantMenuForm}
371
- onChange={this.onChangeUrl}
372
- placeholder={placeholder}
373
- value={this.state.url}
374
- onClick={(e) => {
375
- e.target.focus();
376
- }}
377
- onFocus={(e) => {
378
- this.props.onSelectBlock(this.props.id);
379
- }}
380
- />
381
- {this.state.url && (
382
- <Button.Group>
383
- <Button
384
- basic
385
- className="cancel"
386
- onClick={(e) => {
387
- e.stopPropagation();
388
- this.setState({ url: '' });
389
- }}
390
- >
391
- <Icon name={clearSVG} size="30px" />
392
- </Button>
393
- </Button.Group>
394
- )}
395
- <Button.Group>
396
- <Button
397
- basic
398
- primary
399
- disabled={!this.state.url}
400
- onClick={(e) => {
401
- e.stopPropagation();
402
- this.onSubmitUrl();
403
- }}
404
- >
405
- <Icon name={aheadSVG} size="30px" />
406
- </Button>
407
- </Button.Group>
408
- </div>
409
- </div>
410
- </Message>
411
- </div>
412
- )}
413
- </Dropzone>
414
- )}
415
- </div>
107
+ <ImageInput
108
+ onChange={handleChange}
109
+ placeholderLinkInput={data.placeholder}
110
+ block={props.block}
111
+ id={props.block}
112
+ objectBrowserPickerType={'image'}
113
+ />
416
114
  )}
417
- <SidebarPortal selected={this.props.selected}>
418
- <ImageSidebar {...this.props} />
115
+ <SidebarPortal selected={props.selected}>
116
+ <ImageSidebar {...props} />
419
117
  </SidebarPortal>
420
118
  </div>
421
- );
422
- }
119
+ </>
120
+ );
423
121
  }
424
122
 
425
123
  export default compose(
@@ -1,10 +1,14 @@
1
1
  import React from 'react';
2
+ import { useDispatch } from 'react-redux';
2
3
  import { defineMessages, useIntl } from 'react-intl';
3
4
  import { Button } from 'semantic-ui-react';
4
5
  import { Icon } from '@plone/volto/components';
5
6
  import { BlockDataForm } from '@plone/volto/components/manage/Form';
7
+ import { flattenToAppURL } from '@plone/volto/helpers';
8
+ import { getContent } from '@plone/volto/actions';
6
9
  import { isEmpty } from 'lodash';
7
10
 
11
+ import reloadSVG from '@plone/volto/icons/reload.svg';
8
12
  import trashSVG from '@plone/volto/icons/delete.svg';
9
13
 
10
14
  const messages = defineMessages({
@@ -12,11 +16,16 @@ const messages = defineMessages({
12
16
  id: 'Reset the block',
13
17
  defaultMessage: 'Reset the block',
14
18
  },
19
+ refreshTeaser: {
20
+ id: 'Refresh source content',
21
+ defaultMessage: 'Refresh source content',
22
+ },
15
23
  });
16
24
 
17
25
  const TeaserData = (props) => {
18
26
  const { block, blocksConfig, data, onChangeBlock, navRoot, contentType } =
19
27
  props;
28
+ const dispatch = useDispatch();
20
29
  const intl = useIntl();
21
30
 
22
31
  const reset = () => {
@@ -29,6 +38,48 @@ const TeaserData = (props) => {
29
38
  });
30
39
  };
31
40
 
41
+ const dataTransformer = (resp, data) => {
42
+ let hrefData = {
43
+ '@id': flattenToAppURL(resp['@id']),
44
+ '@type': resp?.['@type'],
45
+ Description: resp?.description,
46
+ Title: resp.title,
47
+ hasPreviewImage: resp?.preview_image ? true : false,
48
+ head_title: resp.head_title ?? null,
49
+ image_field: resp?.preview_image
50
+ ? 'preview_image'
51
+ : resp?.image
52
+ ? 'image'
53
+ : null,
54
+ image_scales: {
55
+ preview_image: [resp?.preview_image],
56
+ image: [resp?.image],
57
+ },
58
+ title: resp.title,
59
+ };
60
+ let blockData = {
61
+ '@type': data['@type'],
62
+ description: resp?.description,
63
+ head_title: resp?.head_title,
64
+ overwrite: data.overwrite,
65
+ href: [hrefData],
66
+ styles: data.styles,
67
+ title: resp.title,
68
+ };
69
+ return blockData;
70
+ };
71
+
72
+ const refresh = () => {
73
+ dispatch(
74
+ getContent(flattenToAppURL(data.href[0]['@id']), null, `${block}-teaser`),
75
+ ).then((resp) => {
76
+ if (resp) {
77
+ let blockData = dataTransformer(resp, data);
78
+ onChangeBlock(block, blockData);
79
+ }
80
+ });
81
+ };
82
+
32
83
  const isReseteable =
33
84
  isEmpty(data.href) && !data.title && !data.description && !data.head_title;
34
85
 
@@ -45,7 +96,23 @@ const TeaserData = (props) => {
45
96
  </Button.Group>
46
97
  );
47
98
 
48
- const schema = blocksConfig[data['@type']].blockSchema({ intl });
99
+ const ActionButton = (
100
+ <Button.Group className="refresh teaser">
101
+ <Button
102
+ aria-label={intl.formatMessage(messages.refreshTeaser)}
103
+ basic
104
+ onClick={() => refresh()}
105
+ >
106
+ {intl.formatMessage(messages.refreshTeaser)}
107
+ <Icon name={reloadSVG} size="20px" color="#00000099" />
108
+ </Button>
109
+ </Button.Group>
110
+ );
111
+
112
+ const schema = blocksConfig[data['@type']].blockSchema({
113
+ data,
114
+ intl,
115
+ });
49
116
  const dataAdapter = blocksConfig[data['@type']].dataAdapter;
50
117
 
51
118
  return (
@@ -66,6 +133,7 @@ const TeaserData = (props) => {
66
133
  block={block}
67
134
  blocksConfig={blocksConfig}
68
135
  headerActions={HeaderActions}
136
+ actionButton={data.overwrite && ActionButton}
69
137
  navRoot={navRoot}
70
138
  contentType={contentType}
71
139
  />
@@ -34,16 +34,31 @@ const messages = defineMessages({
34
34
  id: 'Alignment',
35
35
  defaultMessage: 'Alignment',
36
36
  },
37
+ overwrite: {
38
+ id: 'Customize teaser content',
39
+ defaultMessage: 'Customize teaser content',
40
+ },
41
+ overwriteDescription: {
42
+ id: 'Check this box to customize the title, description, or image of the target content item for this teaser. Leave it unchecked to show updates to the target content item if it is edited later.',
43
+ defaultMessage:
44
+ 'Check this box to customize the title, description, or image of the target content item for this teaser. Leave it unchecked to show updates to the target content item if it is edited later.',
45
+ },
37
46
  });
38
47
 
39
- export const TeaserSchema = ({ intl }) => {
48
+ export const TeaserSchema = ({ data, intl }) => {
40
49
  const schema = {
41
50
  title: intl.formatMessage(messages.teaser),
42
51
  fieldsets: [
43
52
  {
44
53
  id: 'default',
45
54
  title: 'Default',
46
- fields: ['href', 'title', 'head_title', 'description', 'preview_image'],
55
+ fields: [
56
+ 'href',
57
+ 'overwrite',
58
+ ...(data?.overwrite
59
+ ? ['title', 'head_title', 'description', 'preview_image']
60
+ : []),
61
+ ],
47
62
  },
48
63
  ],
49
64
 
@@ -63,6 +78,12 @@ export const TeaserSchema = ({ intl }) => {
63
78
  ],
64
79
  allowExternals: true,
65
80
  },
81
+ overwrite: {
82
+ title: intl.formatMessage(messages.overwrite),
83
+ description: intl.formatMessage(messages.overwriteDescription),
84
+ type: 'boolean',
85
+ default: false,
86
+ },
66
87
  title: {
67
88
  title: intl.formatMessage(messages.title),
68
89
  },
@@ -85,7 +106,7 @@ export const TeaserSchema = ({ intl }) => {
85
106
  type: 'boolean',
86
107
  },
87
108
  },
88
- required: [],
109
+ required: ['href'],
89
110
  };
90
111
 
91
112
  addStyling({ schema, intl });
@@ -20,6 +20,7 @@ import copySVG from '@plone/volto/icons/copy.svg';
20
20
  import cutSVG from '@plone/volto/icons/cut.svg';
21
21
  import pasteSVG from '@plone/volto/icons/paste.svg';
22
22
  import trashSVG from '@plone/volto/icons/delete.svg';
23
+ import { cloneBlocks } from '@plone/volto/helpers/Blocks/cloneBlocks';
23
24
 
24
25
  export class BlocksToolbarComponent extends React.Component {
25
26
  constructor(props) {
@@ -79,9 +80,9 @@ export class BlocksToolbarComponent extends React.Component {
79
80
  const { formData } = this.props;
80
81
  const blocksFieldname = getBlocksFieldname(formData);
81
82
  const blocks = formData[blocksFieldname];
82
- const blocksData = this.props.selectedBlocks.map(
83
- (blockId) => blocks[blockId],
84
- );
83
+ const blocksData = this.props.selectedBlocks
84
+ .map((blockId) => [blockId, blocks[blockId]])
85
+ .filter(([blockId]) => !!blockId); // Removes null blocks
85
86
  this.props.setBlocksClipboard({ [actionType]: blocksData });
86
87
  this.props.onSetSelectedBlocks([]);
87
88
  }
@@ -91,14 +92,14 @@ export class BlocksToolbarComponent extends React.Component {
91
92
  const mode = Object.keys(blocksClipboard).includes('cut') ? 'cut' : 'copy';
92
93
  const blocksData = blocksClipboard[mode] || [];
93
94
  const cloneWithIds = blocksData
94
- .filter((blockData) => !!blockData['@type'])
95
- .map((blockData) => {
95
+ .filter(([blockId, blockData]) => blockId && !!blockData['@type']) // Removes null blocks
96
+ .map(([blockId, blockData]) => {
96
97
  const blockConfig = config.blocks.blocksConfig[blockData['@type']];
97
98
  return mode === 'copy'
98
99
  ? blockConfig.cloneData
99
100
  ? blockConfig.cloneData(blockData)
100
- : [uuid(), blockData]
101
- : [uuid(), blockData]; // if cut/pasting blocks, we don't clone
101
+ : [uuid(), cloneBlocks(blockData)]
102
+ : [blockId, blockData]; // if cut/pasting blocks, we don't clone
102
103
  })
103
104
  .filter((info) => !!info); // some blocks may refuse to be copied
104
105
  const blocksFieldname = getBlocksFieldname(formData);
@@ -774,7 +774,14 @@ class Form extends Component {
774
774
  id={field}
775
775
  fieldSet={fieldset.title.toLowerCase()}
776
776
  formData={formData}
777
- focus={this.state.inFocus[field]}
777
+ focus={
778
+ this.state.isClient &&
779
+ document
780
+ .getElementById('sidebar-metadata')
781
+ ?.contains(document.activeElement)
782
+ ? this.state.inFocus[field]
783
+ : false
784
+ }
778
785
  value={formData?.[field]}
779
786
  required={
780
787
  schema.required.indexOf(field) !== -1
@@ -48,6 +48,7 @@ const InlineForm = (props) => {
48
48
  title,
49
49
  icon,
50
50
  headerActions,
51
+ actionButton,
51
52
  footer,
52
53
  focusIndex,
53
54
  intl,
@@ -160,6 +161,9 @@ const InlineForm = (props) => {
160
161
  block={block}
161
162
  />
162
163
  ))}
164
+ {actionButton && (
165
+ <Segment className="attached actions">{actionButton}</Segment>
166
+ )}
163
167
  </Segment>
164
168
  </div>
165
169