@plone/volto 18.0.0-alpha.35 → 18.0.0-alpha.37

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 (75) hide show
  1. package/.release-it.json +1 -1
  2. package/CHANGELOG.md +49 -0
  3. package/locales/ca/LC_MESSAGES/volto.po +58 -4
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +61 -7
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +58 -4
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +58 -4
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +58 -4
  12. package/locales/eu.json +1 -1
  13. package/locales/fi/LC_MESSAGES/volto.po +61 -7
  14. package/locales/fi.json +1 -1
  15. package/locales/fr/LC_MESSAGES/volto.po +58 -4
  16. package/locales/fr.json +1 -1
  17. package/locales/hi/LC_MESSAGES/volto.po +58 -4
  18. package/locales/hi.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +58 -4
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +58 -4
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +58 -4
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +58 -4
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +58 -4
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +58 -4
  30. package/locales/ro.json +1 -1
  31. package/locales/volto.pot +59 -5
  32. package/locales/zh_CN/LC_MESSAGES/volto.po +58 -4
  33. package/locales/zh_CN.json +1 -1
  34. package/package.json +5 -5
  35. package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +78 -72
  36. package/src/components/manage/AnchorPlugin/useLinkEditor.jsx +79 -0
  37. package/src/components/manage/Blocks/Image/Edit.jsx +45 -347
  38. package/src/components/manage/Blocks/Teaser/Data.jsx +95 -2
  39. package/src/components/manage/Blocks/Teaser/schema.js +24 -3
  40. package/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx +34 -4
  41. package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +30 -4
  42. package/src/components/manage/Form/BlocksToolbar.jsx +8 -7
  43. package/src/components/manage/Form/Form.jsx +8 -1
  44. package/src/components/manage/Form/InlineForm.jsx +4 -0
  45. package/src/components/manage/Widgets/ImageWidget.jsx +311 -0
  46. package/src/components/manage/Widgets/ImageWidget.stories.jsx +46 -0
  47. package/src/config/NonContentRoutes.jsx +2 -1
  48. package/src/config/Widgets.jsx +2 -0
  49. package/src/helpers/Blocks/cloneBlocks.test.js +54 -0
  50. package/src/helpers/Blocks/cloneBlocks.ts +43 -0
  51. package/src/helpers/MessageLabels/MessageLabels.js +8 -0
  52. package/src/icons/external-link.svg +4 -0
  53. package/theme/themes/pastanaga/collections/form.overrides +4 -0
  54. package/theme/themes/pastanaga/elements/button.overrides +2 -1
  55. package/theme/themes/pastanaga/extras/blocks.less +40 -4
  56. package/theme/themes/pastanaga/extras/grid.less +3 -3
  57. package/theme/themes/pastanaga/extras/teaser.less +53 -0
  58. package/theme/themes/pastanaga/extras/widgets.less +108 -0
  59. package/types/components/manage/AnchorPlugin/useLinkEditor.d.ts +7 -0
  60. package/types/components/manage/Blocks/Teaser/schema.d.ts +9 -2
  61. package/types/components/manage/Widgets/ImageWidget.d.ts +10 -0
  62. package/types/components/manage/Widgets/ImageWidget.stories.d.ts +11 -0
  63. package/types/config/Widgets.d.ts +2 -0
  64. package/types/helpers/Blocks/cloneBlocks.d.ts +1 -0
  65. package/types/helpers/Blocks/cloneBlocks.test.d.ts +1 -0
  66. package/types/helpers/MessageLabels/MessageLabels.d.ts +552 -1
  67. /package/src/components/manage/Blocks/Image/{schema.js → schema.jsx} +0 -0
  68. /package/src/components/theme/LanguageSelector/{LanguageSelector.js → LanguageSelector.jsx} +0 -0
  69. /package/src/helpers/Extensions/{withBlockExtensions.js → withBlockExtensions.jsx} +0 -0
  70. /package/src/helpers/Extensions/{withBlockSchemaEnhancer.js → withBlockSchemaEnhancer.jsx} +0 -0
  71. /package/src/helpers/FormValidation/{FormValidation.js → FormValidation.jsx} +0 -0
  72. /package/src/helpers/Helmet/{Helmet.js → Helmet.jsx} +0 -0
  73. /package/src/helpers/Loadable/{Loadable.js → Loadable.jsx} +0 -0
  74. /package/src/helpers/Loadable/__mocks__/{Loadable.js → Loadable.jsx} +0 -0
  75. /package/src/helpers/Utils/{Utils.js → Utils.jsx} +0 -0
@@ -49,8 +49,10 @@ import {
49
49
  Form,
50
50
  Input,
51
51
  Button,
52
+ Dimmer,
52
53
  Segment,
53
54
  Table,
55
+ Loader,
54
56
  } from 'semantic-ui-react';
55
57
 
56
58
  /**
@@ -177,6 +179,9 @@ class UsersControlpanel extends Component {
177
179
  search: this.state.search,
178
180
  });
179
181
  }
182
+ if (this.props.deleteRequest.loading && nextProps.deleteRequest.loaded) {
183
+ this.onDeleteUserSuccess();
184
+ }
180
185
  if (this.props.createRequest.loading && nextProps.createRequest.loaded) {
181
186
  this.onAddUserSuccess();
182
187
  }
@@ -256,10 +261,6 @@ class UsersControlpanel extends Component {
256
261
  onDeleteOk() {
257
262
  if (this.state.userToDelete) {
258
263
  this.props.deleteUser(this.state.userToDelete.id);
259
- this.setState({
260
- showDelete: false,
261
- userToDelete: undefined,
262
- });
263
264
  }
264
265
  }
265
266
 
@@ -272,6 +273,7 @@ class UsersControlpanel extends Component {
272
273
  this.setState({
273
274
  showDelete: false,
274
275
  itemsToDelete: [],
276
+ userToDelete: undefined,
275
277
  });
276
278
  }
277
279
 
@@ -344,6 +346,24 @@ class UsersControlpanel extends Component {
344
346
  );
345
347
  }
346
348
 
349
+ /**
350
+ * Handle Success after deleteUser()
351
+ *
352
+ * @returns {undefined}
353
+ */
354
+ onDeleteUserSuccess() {
355
+ this.setState({
356
+ userToDelete: undefined,
357
+ showDelete: false,
358
+ });
359
+ toast.success(
360
+ <Toast
361
+ success
362
+ title={this.props.intl.formatMessage(messages.success)}
363
+ content={this.props.intl.formatMessage(messages.userDeleted)}
364
+ />,
365
+ );
366
+ }
347
367
  /**
348
368
  *
349
369
  *
@@ -533,6 +553,12 @@ class UsersControlpanel extends Component {
533
553
  )}
534
554
  content={
535
555
  <div className="content">
556
+ <Dimmer active={this.props?.deleteRequest?.loading}>
557
+ <Loader>
558
+ <FormattedMessage id="Loading" defaultMessage="Loading." />
559
+ </Loader>
560
+ </Dimmer>
561
+
536
562
  <ul className="content">
537
563
  <FormattedMessage
538
564
  id="Do you really want to delete the user {username}?"
@@ -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
 
@@ -0,0 +1,311 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Button, Dimmer, Loader, Message } from 'semantic-ui-react';
3
+ import { useIntl, defineMessages } from 'react-intl';
4
+ import { useDispatch } from 'react-redux';
5
+ import { useLocation } from 'react-router-dom';
6
+ import loadable from '@loadable/component';
7
+ import { connect } from 'react-redux';
8
+ import { compose } from 'redux';
9
+ import useLinkEditor from '@plone/volto/components/manage/AnchorPlugin/useLinkEditor';
10
+ import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
11
+
12
+ import {
13
+ flattenToAppURL,
14
+ getBaseUrl,
15
+ isInternalURL,
16
+ validateFileUploadSize,
17
+ } from '@plone/volto/helpers';
18
+ import { createContent } from '@plone/volto/actions';
19
+ import { readAsDataURL } from 'promise-file-reader';
20
+ import { FormFieldWrapper, Icon } from '@plone/volto/components';
21
+
22
+ import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
23
+ import clearSVG from '@plone/volto/icons/clear.svg';
24
+ import navTreeSVG from '@plone/volto/icons/nav.svg';
25
+ import linkSVG from '@plone/volto/icons/link.svg';
26
+ import uploadSVG from '@plone/volto/icons/upload.svg';
27
+
28
+ const Dropzone = loadable(() => import('react-dropzone'));
29
+
30
+ export const ImageToolbar = ({ className, data, id, onChange, selected }) => (
31
+ <div className="image-upload-widget-toolbar">
32
+ <Button.Group>
33
+ <Button icon basic onClick={() => onChange(id, null)}>
34
+ <Icon className="circled" name={clearSVG} size="24px" color="#e40166" />
35
+ </Button>
36
+ </Button.Group>
37
+ </div>
38
+ );
39
+
40
+ const messages = defineMessages({
41
+ addImage: {
42
+ id: 'Browse the site, drop an image, or type a URL',
43
+ defaultMessage: 'Browse the site, drop an image, or use a URL',
44
+ },
45
+ pickAnImage: {
46
+ id: 'pickAnImage',
47
+ defaultMessage: 'Pick an existing image',
48
+ },
49
+ uploadAnImage: {
50
+ id: 'uploadAnImage',
51
+ defaultMessage: 'Upload an image from your computer',
52
+ },
53
+ linkAnImage: {
54
+ id: 'linkAnImage',
55
+ defaultMessage: 'Enter a URL to an image',
56
+ },
57
+ uploadingImage: {
58
+ id: 'Uploading image',
59
+ defaultMessage: 'Uploading image',
60
+ },
61
+ });
62
+
63
+ const UnconnectedImageInput = (props) => {
64
+ const {
65
+ id,
66
+ onChange,
67
+ onFocus,
68
+ openObjectBrowser,
69
+ value,
70
+ imageSize = 'teaser',
71
+ selected = true,
72
+ hideLinkPicker = false,
73
+ hideObjectBrowserPicker = false,
74
+ restrictFileUpload = false,
75
+ objectBrowserPickerType = 'image',
76
+ description,
77
+ placeholderLinkInput = '',
78
+ } = props;
79
+
80
+ const intl = useIntl();
81
+ const linkEditor = useLinkEditor();
82
+ const location = useLocation();
83
+ const dispatch = useDispatch();
84
+ const contextUrl = location.pathname;
85
+
86
+ const [uploading, setUploading] = React.useState(false);
87
+ const [dragging, setDragging] = React.useState(false);
88
+
89
+ const imageUploadInputRef = useRef(null);
90
+
91
+ const requestId = `image-upload-${id}`;
92
+
93
+ const { loading, loaded } = props.request;
94
+ const { content } = props;
95
+ const imageId = content?.['@id'];
96
+ const image = content?.image;
97
+
98
+ useEffect(() => {
99
+ if (uploading && !loading && loaded) {
100
+ setUploading(false);
101
+ onChange(id, imageId, {
102
+ image_field: 'image',
103
+ image_scales: { image: [image] },
104
+ });
105
+ }
106
+ }, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
107
+
108
+ const handleUpload = React.useCallback(
109
+ (eventOrFile) => {
110
+ if (restrictFileUpload === true) return;
111
+ eventOrFile.target && eventOrFile.stopPropagation();
112
+
113
+ setUploading(true);
114
+ const file = eventOrFile.target
115
+ ? eventOrFile.target.files[0]
116
+ : eventOrFile[0];
117
+ if (!validateFileUploadSize(file, intl.formatMessage)) return;
118
+ readAsDataURL(file).then((fileData) => {
119
+ const fields = fileData.match(/^data:(.*);(.*),(.*)$/);
120
+ dispatch(
121
+ createContent(
122
+ getBaseUrl(contextUrl),
123
+ {
124
+ '@type': 'Image',
125
+ title: file.name,
126
+ image: {
127
+ data: fields[3],
128
+ encoding: fields[2],
129
+ 'content-type': fields[1],
130
+ filename: file.name,
131
+ },
132
+ },
133
+ props.block || requestId,
134
+ ),
135
+ );
136
+ });
137
+ },
138
+ [
139
+ restrictFileUpload,
140
+ intl.formatMessage,
141
+ dispatch,
142
+ props,
143
+ contextUrl,
144
+ requestId,
145
+ ],
146
+ );
147
+
148
+ const onDragEnter = React.useCallback(() => {
149
+ if (restrictFileUpload === false) setDragging(true);
150
+ }, [restrictFileUpload]);
151
+ const onDragLeave = React.useCallback(() => setDragging(false), []);
152
+
153
+ return value ? (
154
+ <div
155
+ className="image-upload-widget-image"
156
+ onClick={onFocus}
157
+ onKeyDown={onFocus}
158
+ role="toolbar"
159
+ >
160
+ {selected && <ImageToolbar {...props} />}
161
+ <img
162
+ className={props.className}
163
+ src={`${flattenToAppURL(value)}/@@images/image/${imageSize}`}
164
+ alt=""
165
+ />
166
+ </div>
167
+ ) : (
168
+ <div
169
+ className="image-upload-widget"
170
+ onClick={onFocus}
171
+ onKeyDown={onFocus}
172
+ role="toolbar"
173
+ >
174
+ <Dropzone
175
+ noClick
176
+ onDrop={handleUpload}
177
+ onDragEnter={onDragEnter}
178
+ onDragLeave={onDragLeave}
179
+ className="dropzone"
180
+ >
181
+ {({ getRootProps, getInputProps }) => (
182
+ <div {...getRootProps()}>
183
+ <Message>
184
+ {dragging && <Dimmer active></Dimmer>}
185
+ {uploading && (
186
+ <Dimmer active>
187
+ <Loader indeterminate>
188
+ {intl.formatMessage(messages.uploadingImage)}
189
+ </Loader>
190
+ </Dimmer>
191
+ )}
192
+ <img src={imageBlockSVG} alt="" className="placeholder" />
193
+ <p>{description || intl.formatMessage(messages.addImage)}</p>
194
+ <div className="toolbar-wrapper">
195
+ <div className="toolbar-inner" ref={linkEditor.anchorNode}>
196
+ {hideObjectBrowserPicker === false && (
197
+ <Button.Group>
198
+ <Button
199
+ aria-label={intl.formatMessage(messages.pickAnImage)}
200
+ icon
201
+ basic
202
+ onClick={(e) => {
203
+ onFocus && onFocus();
204
+ e.preventDefault();
205
+ openObjectBrowser({
206
+ mode: objectBrowserPickerType,
207
+ onSelectItem: (
208
+ url,
209
+ { title, image_field, image_scales },
210
+ ) => {
211
+ onChange(props.id, flattenToAppURL(url), {
212
+ title,
213
+ image_field,
214
+ image_scales,
215
+ });
216
+ },
217
+ currentPath: contextUrl,
218
+ });
219
+ }}
220
+ >
221
+ <Icon name={navTreeSVG} size="24px" />
222
+ </Button>
223
+ </Button.Group>
224
+ )}
225
+ {restrictFileUpload === false && (
226
+ <Button.Group>
227
+ <Button
228
+ aria-label={intl.formatMessage(messages.uploadAnImage)}
229
+ icon
230
+ basic
231
+ compact
232
+ onClick={() => {
233
+ imageUploadInputRef.current.click();
234
+ }}
235
+ >
236
+ <Icon name={uploadSVG} size="24px" />
237
+ </Button>
238
+ <input
239
+ {...getInputProps({
240
+ type: 'file',
241
+ ref: imageUploadInputRef,
242
+ onChange: handleUpload,
243
+ style: { display: 'none' },
244
+ })}
245
+ />
246
+ </Button.Group>
247
+ )}
248
+
249
+ {hideLinkPicker === false && (
250
+ <Button.Group>
251
+ <Button
252
+ icon
253
+ basic
254
+ aria-label={intl.formatMessage(messages.linkAnImage)}
255
+ onClick={(e) => {
256
+ !props.selected && onFocus && onFocus();
257
+ linkEditor.show();
258
+ }}
259
+ >
260
+ <Icon name={linkSVG} circled size="24px" />
261
+ </Button>
262
+ </Button.Group>
263
+ )}
264
+ </div>
265
+ {linkEditor.anchorNode && (
266
+ <linkEditor.LinkEditor
267
+ value={value}
268
+ placeholder={
269
+ placeholderLinkInput ||
270
+ intl.formatMessage(messages.linkAnImage)
271
+ }
272
+ objectBrowserPickerType={objectBrowserPickerType}
273
+ onChange={(_, e) =>
274
+ onChange(
275
+ props.id,
276
+ isInternalURL(e) ? flattenToAppURL(e) : e,
277
+ {},
278
+ )
279
+ }
280
+ id={id}
281
+ />
282
+ )}
283
+ </div>
284
+ </Message>
285
+ </div>
286
+ )}
287
+ </Dropzone>
288
+ </div>
289
+ );
290
+ };
291
+
292
+ export const ImageInput = compose(
293
+ connect(
294
+ (state, ownProps) => {
295
+ const requestId = `image-upload-${ownProps.id}`;
296
+ return {
297
+ request: state.content.subrequests[ownProps.block || requestId] || {},
298
+ content: state.content.subrequests[ownProps.block || requestId]?.data,
299
+ };
300
+ },
301
+ { createContent },
302
+ ),
303
+ )(withObjectBrowser(UnconnectedImageInput));
304
+
305
+ const ImageUploadWidget = (props) => (
306
+ <FormFieldWrapper {...props} className="image-upload-widget">
307
+ <ImageInput {...props} />
308
+ </FormFieldWrapper>
309
+ );
310
+
311
+ export default ImageUploadWidget;
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import ImageWidget from './ImageWidget';
3
+ import WidgetStory from './story';
4
+
5
+ export const Default = WidgetStory.bind({
6
+ props: { id: 'image_widget', title: 'Image', data: {}, properties: {} },
7
+ widget: ImageWidget,
8
+ });
9
+
10
+ export const Filled = WidgetStory.bind({
11
+ props: {
12
+ id: 'image_widget',
13
+ title: 'Image',
14
+ data: {},
15
+ properties: {},
16
+ value: 'halfdome2022.jpeg',
17
+ },
18
+ widget: ImageWidget,
19
+ });
20
+ Filled.args = {
21
+ value: 'halfdome2022.jpeg',
22
+ };
23
+
24
+ export const Inline = WidgetStory.bind({
25
+ props: {
26
+ id: 'image_widget',
27
+ title: 'Image',
28
+ data: {},
29
+ properties: {},
30
+ inline: true,
31
+ },
32
+ widget: ImageWidget,
33
+ });
34
+
35
+ export default {
36
+ title: 'Edit Widgets/Image',
37
+ component: ImageWidget,
38
+ decorators: [
39
+ (Story) => (
40
+ <div className="ui segment form attached" style={{ width: '400px' }}>
41
+ <Story />
42
+ </div>
43
+ ),
44
+ ],
45
+ argTypes: {},
46
+ };
@@ -9,7 +9,8 @@ export const nonContentRoutes = [
9
9
  '/aliases',
10
10
  '/contents',
11
11
  '/delete',
12
- '/diff',
12
+ /\/diff$/,
13
+ /\/diff\?/,
13
14
  /\/edit$/,
14
15
  '/historyview',
15
16
  '/links-to-item',
@@ -50,6 +50,7 @@ import TextViewWidget from '@plone/volto/components/theme/Widgets/TextWidget';
50
50
  import TitleViewWidget from '@plone/volto/components/theme/Widgets/TitleWidget';
51
51
  import TokenViewWidget from '@plone/volto/components/theme/Widgets/TokenWidget';
52
52
  import UrlViewWidget from '@plone/volto/components/theme/Widgets/UrlWidget';
53
+ import ImageWidget from '@plone/volto/components/manage/Widgets/ImageWidget';
53
54
 
54
55
  // Widgets mapping
55
56
  export const widgetMapping = {
@@ -68,6 +69,7 @@ export const widgetMapping = {
68
69
  date: DatetimeWidget,
69
70
  password: PasswordWidget,
70
71
  file: FileWidget,
72
+ image: ImageWidget,
71
73
  align: AlignWidget,
72
74
  buttons: ButtonsWidget,
73
75
  url: UrlWidget,
@@ -0,0 +1,54 @@
1
+ import { cloneDeep } from 'lodash';
2
+ import { cloneBlocks } from './cloneBlocks';
3
+
4
+ describe('cloneBlocks', () => {
5
+ it('cloneBlocks - basic', () => {
6
+ const blocksData = {
7
+ blocks: {
8
+ a: { '@type': 'slate', value: 1 },
9
+ b: { '@type': 'slate', value: 2 },
10
+ },
11
+ blocks_layout: { items: ['a', 'b'] },
12
+ };
13
+ const result = cloneBlocks(blocksData);
14
+
15
+ expect(result.blocks_layout.items[0]).not.toBe('a');
16
+ expect(result.blocks[result.blocks_layout.items[0]]).toStrictEqual({
17
+ '@type': 'slate',
18
+ value: 1,
19
+ });
20
+ });
21
+
22
+ it('cloneBlocks - nested', () => {
23
+ const blocksData = {
24
+ blocks: {
25
+ a: { '@type': 'slate', value: 1 },
26
+ b: { '@type': 'slate', value: 2 },
27
+ },
28
+ blocks_layout: { items: ['a', 'b'] },
29
+ };
30
+ blocksData.blocks.c = cloneDeep(blocksData);
31
+ blocksData.blocks_layout.items.push('c');
32
+
33
+ const result = cloneBlocks(blocksData);
34
+
35
+ expect(result.blocks_layout.items[0]).not.toBe('a');
36
+ expect(result.blocks[result.blocks_layout.items[0]]).toStrictEqual({
37
+ '@type': 'slate',
38
+ value: 1,
39
+ });
40
+
41
+ expect(
42
+ result.blocks[result.blocks_layout.items[2]].blocks_layout.items[0],
43
+ ).not.toBe('c');
44
+
45
+ expect(
46
+ result.blocks[result.blocks_layout.items[2]].blocks[
47
+ result.blocks[result.blocks_layout.items[2]].blocks_layout.items[0]
48
+ ],
49
+ ).toStrictEqual({
50
+ '@type': 'slate',
51
+ value: 1,
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,43 @@
1
+ // @ts-nocheck
2
+ import { v4 as uuid } from 'uuid';
3
+ import {
4
+ getBlocksFieldname,
5
+ getBlocksLayoutFieldname,
6
+ hasBlocksData,
7
+ } from './Blocks';
8
+ import config from '@plone/registry';
9
+
10
+ export function cloneBlocks(blocksData) {
11
+ if (hasBlocksData(blocksData)) {
12
+ const blocksFieldname = getBlocksFieldname(blocksData);
13
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(blocksData);
14
+
15
+ const cloneWithIds = Object.keys(blocksData.blocks)
16
+ .map((key) => {
17
+ const block = blocksData.blocks[key];
18
+ const blockConfig = config.blocks.blocksConfig[blocksData['@type']];
19
+ return blockConfig?.cloneData
20
+ ? blockConfig.cloneData(block)
21
+ : [uuid(), cloneBlocks(block)];
22
+ })
23
+ .filter((info) => !!info); // some blocks may refuse to be copied
24
+
25
+ const newBlockData = {
26
+ ...blocksData,
27
+ [blocksFieldname]: {
28
+ ...Object.assign(
29
+ {},
30
+ ...cloneWithIds.map(([id, data]) => ({ [id]: data })),
31
+ ),
32
+ },
33
+ [blocksLayoutFieldname]: {
34
+ ...blocksData[blocksLayoutFieldname],
35
+ items: [...cloneWithIds.map(([id]) => id)],
36
+ },
37
+ };
38
+
39
+ return newBlockData;
40
+ }
41
+
42
+ return blocksData;
43
+ }
@@ -110,6 +110,10 @@ export const messages = defineMessages({
110
110
  id: 'Add group',
111
111
  defaultMessage: 'Add group',
112
112
  },
113
+ groupDeleted: {
114
+ id: 'Group deleted',
115
+ defaultMessage: 'Group deleted',
116
+ },
113
117
  addUserFormUsernameTitle: {
114
118
  id: 'Username',
115
119
  defaultMessage: 'Username',
@@ -213,6 +217,10 @@ export const messages = defineMessages({
213
217
  id: 'User created',
214
218
  defaultMessage: 'User created',
215
219
  },
220
+ userDeleted: {
221
+ id: 'User deleted',
222
+ defaultMessage: 'User deleted',
223
+ },
216
224
  groupCreated: {
217
225
  id: 'Group created',
218
226
  defaultMessage: 'Group created',
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">
2
+ <path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/>
3
+ <path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/>
4
+ </svg>
@@ -6,6 +6,10 @@
6
6
  margin: 0;
7
7
  }
8
8
 
9
+ .ui.form .inline.field.overwritten input {
10
+ background-color: rgb(#edc3c2);
11
+ }
12
+
9
13
  .ui.form .inline.field.help .wrapper {
10
14
  border-bottom: none;
11
15
  }
@@ -26,7 +26,8 @@
26
26
  }
27
27
  }
28
28
 
29
- #main {
29
+ #main,
30
+ .slate-inline-toolbar.slate-toolbar {
30
31
  .ui.basic.buttons .button,
31
32
  .ui.basic.button {
32
33
  -webkit-box-shadow: 0px 0px 0px @basicBorderSize transparent inset !important;