@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.
- package/.release-it.json +1 -1
- package/CHANGELOG.md +43 -0
- package/locales/ca/LC_MESSAGES/volto.po +41 -4
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +41 -4
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +41 -4
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +41 -4
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +41 -4
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +44 -7
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +41 -4
- package/locales/fr.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +41 -4
- package/locales/hi.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +41 -4
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +41 -4
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +41 -4
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +41 -4
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +41 -4
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +41 -4
- package/locales/ro.json +1 -1
- package/locales/volto.pot +42 -5
- package/locales/zh_CN/LC_MESSAGES/volto.po +41 -4
- package/locales/zh_CN.json +1 -1
- package/package.json +5 -5
- package/razzle.config.js +7 -1
- package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +78 -72
- package/src/components/manage/AnchorPlugin/useLinkEditor.js +79 -0
- package/src/components/manage/Blocks/Image/Edit.jsx +45 -347
- package/src/components/manage/Blocks/Teaser/Data.jsx +69 -1
- package/src/components/manage/Blocks/Teaser/schema.js +24 -3
- 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/config/index.js +0 -1
- package/src/helpers/Blocks/Blocks.js +20 -7
- package/src/helpers/Blocks/Blocks.test.js +55 -0
- package/src/helpers/Blocks/cloneBlocks.test.js +54 -0
- package/src/helpers/Blocks/cloneBlocks.ts +43 -0
- package/src/helpers/index.js +1 -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/Blocks.d.ts +6 -0
- package/types/helpers/Blocks/cloneBlocks.d.ts +1 -0
- package/types/helpers/Blocks/cloneBlocks.test.d.ts +1 -0
- package/types/helpers/index.d.ts +1 -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
|
@@ -3,256 +3,57 @@
|
|
|
3
3
|
* @module components/manage/Blocks/Image/Edit
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import 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 {
|
|
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
|
|
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
|
|
49
|
-
* @
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
307
|
-
{
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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={
|
|
418
|
-
<ImageSidebar {...
|
|
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
|
|
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: [
|
|
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
|
|
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
|
|