@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
|
@@ -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,18 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { toast } from 'react-toastify';
|
|
6
|
+
import { Icon, Toast } from '@plone/volto/components';
|
|
5
7
|
import { BlockDataForm } from '@plone/volto/components/manage/Form';
|
|
8
|
+
import {
|
|
9
|
+
flattenToAppURL,
|
|
10
|
+
messages as defaultMessages,
|
|
11
|
+
} from '@plone/volto/helpers';
|
|
12
|
+
import { getContent } from '@plone/volto/actions';
|
|
6
13
|
import { isEmpty } from 'lodash';
|
|
7
14
|
|
|
15
|
+
import reloadSVG from '@plone/volto/icons/reload.svg';
|
|
8
16
|
import trashSVG from '@plone/volto/icons/delete.svg';
|
|
9
17
|
|
|
10
18
|
const messages = defineMessages({
|
|
@@ -12,11 +20,20 @@ const messages = defineMessages({
|
|
|
12
20
|
id: 'Reset the block',
|
|
13
21
|
defaultMessage: 'Reset the block',
|
|
14
22
|
},
|
|
23
|
+
refreshTeaser: {
|
|
24
|
+
id: 'Refresh source content',
|
|
25
|
+
defaultMessage: 'Refresh source content',
|
|
26
|
+
},
|
|
27
|
+
invalidTeaser: {
|
|
28
|
+
id: 'Invalid teaser source',
|
|
29
|
+
defaultMessage: 'Invalid teaser source',
|
|
30
|
+
},
|
|
15
31
|
});
|
|
16
32
|
|
|
17
33
|
const TeaserData = (props) => {
|
|
18
34
|
const { block, blocksConfig, data, onChangeBlock, navRoot, contentType } =
|
|
19
35
|
props;
|
|
36
|
+
const dispatch = useDispatch();
|
|
20
37
|
const intl = useIntl();
|
|
21
38
|
|
|
22
39
|
const reset = () => {
|
|
@@ -29,6 +46,64 @@ const TeaserData = (props) => {
|
|
|
29
46
|
});
|
|
30
47
|
};
|
|
31
48
|
|
|
49
|
+
const dataTransformer = (resp, data) => {
|
|
50
|
+
let hrefData = {
|
|
51
|
+
'@id': flattenToAppURL(resp['@id']),
|
|
52
|
+
'@type': resp?.['@type'],
|
|
53
|
+
Description: resp?.description,
|
|
54
|
+
Title: resp.title,
|
|
55
|
+
hasPreviewImage: resp?.preview_image ? true : false,
|
|
56
|
+
head_title: resp.head_title ?? null,
|
|
57
|
+
image_field: resp?.preview_image
|
|
58
|
+
? 'preview_image'
|
|
59
|
+
: resp?.image
|
|
60
|
+
? 'image'
|
|
61
|
+
: null,
|
|
62
|
+
image_scales: {
|
|
63
|
+
preview_image: [resp?.preview_image],
|
|
64
|
+
image: [resp?.image],
|
|
65
|
+
},
|
|
66
|
+
title: resp.title,
|
|
67
|
+
};
|
|
68
|
+
let blockData = {
|
|
69
|
+
'@type': data['@type'],
|
|
70
|
+
description: resp?.description,
|
|
71
|
+
head_title: resp?.head_title,
|
|
72
|
+
overwrite: data.overwrite,
|
|
73
|
+
href: [hrefData],
|
|
74
|
+
styles: data.styles,
|
|
75
|
+
title: resp.title,
|
|
76
|
+
};
|
|
77
|
+
return blockData;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const refresh = () => {
|
|
81
|
+
if (data.href?.[0]?.['@id']) {
|
|
82
|
+
dispatch(
|
|
83
|
+
getContent(
|
|
84
|
+
flattenToAppURL(data.href[0]['@id']),
|
|
85
|
+
null,
|
|
86
|
+
`${block}-teaser`,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
.then((resp) => {
|
|
90
|
+
if (resp) {
|
|
91
|
+
let blockData = dataTransformer(resp, data);
|
|
92
|
+
onChangeBlock(block, blockData);
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
.catch((e) => {
|
|
96
|
+
toast.error(
|
|
97
|
+
<Toast
|
|
98
|
+
error
|
|
99
|
+
title={props.intl.formatMessage(defaultMessages.error)}
|
|
100
|
+
content={props.intl.formatMessage(messages.invalidTeaser)}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
32
107
|
const isReseteable =
|
|
33
108
|
isEmpty(data.href) && !data.title && !data.description && !data.head_title;
|
|
34
109
|
|
|
@@ -45,7 +120,24 @@ const TeaserData = (props) => {
|
|
|
45
120
|
</Button.Group>
|
|
46
121
|
);
|
|
47
122
|
|
|
48
|
-
const
|
|
123
|
+
const ActionButton = (
|
|
124
|
+
<Button.Group className="refresh teaser">
|
|
125
|
+
<Button
|
|
126
|
+
aria-label={intl.formatMessage(messages.refreshTeaser)}
|
|
127
|
+
basic
|
|
128
|
+
onClick={() => refresh()}
|
|
129
|
+
disabled={isEmpty(data.href)}
|
|
130
|
+
>
|
|
131
|
+
{intl.formatMessage(messages.refreshTeaser)}
|
|
132
|
+
<Icon name={reloadSVG} size="20px" color="#00000099" />
|
|
133
|
+
</Button>
|
|
134
|
+
</Button.Group>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const schema = blocksConfig[data['@type']].blockSchema({
|
|
138
|
+
data,
|
|
139
|
+
intl,
|
|
140
|
+
});
|
|
49
141
|
const dataAdapter = blocksConfig[data['@type']].dataAdapter;
|
|
50
142
|
|
|
51
143
|
return (
|
|
@@ -66,6 +158,7 @@ const TeaserData = (props) => {
|
|
|
66
158
|
block={block}
|
|
67
159
|
blocksConfig={blocksConfig}
|
|
68
160
|
headerActions={HeaderActions}
|
|
161
|
+
actionButton={data.overwrite && ActionButton}
|
|
69
162
|
navRoot={navRoot}
|
|
70
163
|
contentType={contentType}
|
|
71
164
|
/>
|
|
@@ -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 });
|
|
@@ -48,8 +48,10 @@ import {
|
|
|
48
48
|
Button,
|
|
49
49
|
Form,
|
|
50
50
|
Input,
|
|
51
|
+
Loader,
|
|
51
52
|
Segment,
|
|
52
53
|
Table,
|
|
54
|
+
Dimmer,
|
|
53
55
|
} from 'semantic-ui-react';
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -161,6 +163,12 @@ class GroupsControlpanel extends Component {
|
|
|
161
163
|
) {
|
|
162
164
|
this.props.listGroups(this.state.search);
|
|
163
165
|
}
|
|
166
|
+
if (
|
|
167
|
+
this.props.deleteGroupRequest.loading &&
|
|
168
|
+
nextProps.deleteGroupRequest.loaded
|
|
169
|
+
) {
|
|
170
|
+
this.onDeleteGroupSuccess();
|
|
171
|
+
}
|
|
164
172
|
if (
|
|
165
173
|
this.props.createGroupRequest.loading &&
|
|
166
174
|
nextProps.createGroupRequest.loaded
|
|
@@ -246,10 +254,6 @@ class GroupsControlpanel extends Component {
|
|
|
246
254
|
onDeleteOk() {
|
|
247
255
|
if (this.state.groupToDelete) {
|
|
248
256
|
this.props.deleteGroup(this.state.groupToDelete.id);
|
|
249
|
-
this.setState({
|
|
250
|
-
showDelete: false,
|
|
251
|
-
groupToDelete: undefined,
|
|
252
|
-
});
|
|
253
257
|
}
|
|
254
258
|
}
|
|
255
259
|
|
|
@@ -262,6 +266,7 @@ class GroupsControlpanel extends Component {
|
|
|
262
266
|
this.setState({
|
|
263
267
|
showDelete: false,
|
|
264
268
|
itemsToDelete: [],
|
|
269
|
+
groupToDelete: undefined,
|
|
265
270
|
});
|
|
266
271
|
}
|
|
267
272
|
|
|
@@ -367,6 +372,25 @@ class GroupsControlpanel extends Component {
|
|
|
367
372
|
);
|
|
368
373
|
}
|
|
369
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Handle Success after deleteGroup()
|
|
377
|
+
*
|
|
378
|
+
* @returns {undefined}
|
|
379
|
+
*/
|
|
380
|
+
onDeleteGroupSuccess() {
|
|
381
|
+
this.setState({
|
|
382
|
+
groupToDelete: undefined,
|
|
383
|
+
showDelete: false,
|
|
384
|
+
});
|
|
385
|
+
toast.success(
|
|
386
|
+
<Toast
|
|
387
|
+
success
|
|
388
|
+
title={this.props.intl.formatMessage(messages.success)}
|
|
389
|
+
content={this.props.intl.formatMessage(messages.groupDeleted)}
|
|
390
|
+
/>,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
370
394
|
/**
|
|
371
395
|
* On change page
|
|
372
396
|
* @method onChangePage
|
|
@@ -409,6 +433,12 @@ class GroupsControlpanel extends Component {
|
|
|
409
433
|
)}
|
|
410
434
|
content={
|
|
411
435
|
<div className="content">
|
|
436
|
+
<Dimmer active={this?.props?.deleteGroupRequest?.loading}>
|
|
437
|
+
<Loader>
|
|
438
|
+
<FormattedMessage id="Loading" defaultMessage="Loading." />
|
|
439
|
+
</Loader>
|
|
440
|
+
</Dimmer>
|
|
441
|
+
|
|
412
442
|
<ul className="content">
|
|
413
443
|
<FormattedMessage
|
|
414
444
|
id="Do you really want to delete the group {groupname}?"
|