@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.
- package/.release-it.json +1 -1
- package/CHANGELOG.md +49 -0
- package/locales/ca/LC_MESSAGES/volto.po +58 -4
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +61 -7
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +58 -4
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +58 -4
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +58 -4
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +61 -7
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +58 -4
- package/locales/fr.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +58 -4
- package/locales/hi.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +58 -4
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +58 -4
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +58 -4
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +58 -4
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +58 -4
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +58 -4
- package/locales/ro.json +1 -1
- package/locales/volto.pot +59 -5
- package/locales/zh_CN/LC_MESSAGES/volto.po +58 -4
- package/locales/zh_CN.json +1 -1
- package/package.json +5 -5
- package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +78 -72
- package/src/components/manage/AnchorPlugin/useLinkEditor.jsx +79 -0
- package/src/components/manage/Blocks/Image/Edit.jsx +45 -347
- package/src/components/manage/Blocks/Teaser/Data.jsx +95 -2
- package/src/components/manage/Blocks/Teaser/schema.js +24 -3
- package/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx +34 -4
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +30 -4
- package/src/components/manage/Form/BlocksToolbar.jsx +8 -7
- package/src/components/manage/Form/Form.jsx +8 -1
- package/src/components/manage/Form/InlineForm.jsx +4 -0
- package/src/components/manage/Widgets/ImageWidget.jsx +311 -0
- package/src/components/manage/Widgets/ImageWidget.stories.jsx +46 -0
- package/src/config/NonContentRoutes.jsx +2 -1
- package/src/config/Widgets.jsx +2 -0
- package/src/helpers/Blocks/cloneBlocks.test.js +54 -0
- package/src/helpers/Blocks/cloneBlocks.ts +43 -0
- package/src/helpers/MessageLabels/MessageLabels.js +8 -0
- package/src/icons/external-link.svg +4 -0
- package/theme/themes/pastanaga/collections/form.overrides +4 -0
- package/theme/themes/pastanaga/elements/button.overrides +2 -1
- package/theme/themes/pastanaga/extras/blocks.less +40 -4
- package/theme/themes/pastanaga/extras/grid.less +3 -3
- package/theme/themes/pastanaga/extras/teaser.less +53 -0
- package/theme/themes/pastanaga/extras/widgets.less +108 -0
- package/types/components/manage/AnchorPlugin/useLinkEditor.d.ts +7 -0
- package/types/components/manage/Blocks/Teaser/schema.d.ts +9 -2
- package/types/components/manage/Widgets/ImageWidget.d.ts +10 -0
- package/types/components/manage/Widgets/ImageWidget.stories.d.ts +11 -0
- package/types/config/Widgets.d.ts +2 -0
- package/types/helpers/Blocks/cloneBlocks.d.ts +1 -0
- package/types/helpers/Blocks/cloneBlocks.test.d.ts +1 -0
- package/types/helpers/MessageLabels/MessageLabels.d.ts +552 -1
- /package/src/components/manage/Blocks/Image/{schema.js → schema.jsx} +0 -0
- /package/src/components/theme/LanguageSelector/{LanguageSelector.js → LanguageSelector.jsx} +0 -0
- /package/src/helpers/Extensions/{withBlockExtensions.js → withBlockExtensions.jsx} +0 -0
- /package/src/helpers/Extensions/{withBlockSchemaEnhancer.js → withBlockSchemaEnhancer.jsx} +0 -0
- /package/src/helpers/FormValidation/{FormValidation.js → FormValidation.jsx} +0 -0
- /package/src/helpers/Helmet/{Helmet.js → Helmet.jsx} +0 -0
- /package/src/helpers/Loadable/{Loadable.js → Loadable.jsx} +0 -0
- /package/src/helpers/Loadable/__mocks__/{Loadable.js → Loadable.jsx} +0 -0
- /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
|
|
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
|
-
: [
|
|
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={
|
|
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
|
+
};
|
package/src/config/Widgets.jsx
CHANGED
|
@@ -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>
|