@plone/volto 16.12.0 → 16.14.0

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 (54) hide show
  1. package/.changelog.draft +2 -14
  2. package/.yarn/install-state.gz +0 -0
  3. package/CHANGELOG.md +29 -0
  4. package/locales/ca/LC_MESSAGES/volto.po +36 -1
  5. package/locales/ca.json +1 -1
  6. package/locales/de/LC_MESSAGES/volto.po +36 -1
  7. package/locales/de.json +1 -1
  8. package/locales/en/LC_MESSAGES/volto.po +36 -1
  9. package/locales/en.json +1 -1
  10. package/locales/es/LC_MESSAGES/volto.po +36 -1
  11. package/locales/es.json +1 -1
  12. package/locales/eu/LC_MESSAGES/volto.po +36 -1
  13. package/locales/eu.json +1 -1
  14. package/locales/fi/LC_MESSAGES/volto.po +36 -1
  15. package/locales/fi.json +1 -1
  16. package/locales/fr/LC_MESSAGES/volto.po +36 -1
  17. package/locales/fr.json +1 -1
  18. package/locales/it/LC_MESSAGES/volto.po +36 -1
  19. package/locales/it.json +1 -1
  20. package/locales/ja/LC_MESSAGES/volto.po +36 -1
  21. package/locales/ja.json +1 -1
  22. package/locales/nl/LC_MESSAGES/volto.po +36 -1
  23. package/locales/nl.json +1 -1
  24. package/locales/pt/LC_MESSAGES/volto.po +36 -1
  25. package/locales/pt.json +1 -1
  26. package/locales/pt_BR/LC_MESSAGES/volto.po +36 -1
  27. package/locales/pt_BR.json +1 -1
  28. package/locales/ro/LC_MESSAGES/volto.po +36 -1
  29. package/locales/ro.json +1 -1
  30. package/locales/volto.pot +37 -2
  31. package/locales/zh_CN/LC_MESSAGES/volto.po +36 -1
  32. package/locales/zh_CN.json +1 -1
  33. package/package.json +1 -1
  34. package/packages/volto-slate/package.json +1 -1
  35. package/pyvenv.cfg +1 -1
  36. package/share/man/man1/ttx.1 +225 -0
  37. package/src/components/manage/Blocks/Listing/DefaultNoResultsComponent.jsx +13 -0
  38. package/src/components/manage/Blocks/Listing/GalleryNoResultsComponent.jsx +10 -0
  39. package/src/components/manage/Blocks/Listing/ListingBody.jsx +8 -10
  40. package/src/components/manage/Blocks/Teaser/Body.jsx +30 -0
  41. package/src/components/manage/Blocks/Teaser/Data.jsx +71 -0
  42. package/src/components/manage/Blocks/Teaser/DefaultBody.jsx +89 -0
  43. package/src/components/manage/Blocks/Teaser/Edit.jsx +25 -0
  44. package/src/components/manage/Blocks/Teaser/View.jsx +9 -0
  45. package/src/components/manage/Blocks/Teaser/adapter.js +23 -0
  46. package/src/components/manage/Blocks/Teaser/schema.js +103 -0
  47. package/src/components/manage/Blocks/Teaser/utils.js +44 -0
  48. package/src/components/manage/Blocks/Teaser/utils.test.jsx +229 -0
  49. package/src/components/theme/App/App.jsx +71 -67
  50. package/src/config/Blocks.jsx +32 -0
  51. package/src/config/Components.jsx +2 -1
  52. package/src/server.jsx +12 -7
  53. package/theme/themes/pastanaga/extras/blocks.less +2 -0
  54. package/theme/themes/pastanaga/extras/teaser.less +166 -0
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Button } from 'semantic-ui-react';
4
+ import { BlockDataForm, Icon } from '@plone/volto/components';
5
+ import { isEmpty } from 'lodash';
6
+
7
+ import trashSVG from '@plone/volto/icons/delete.svg';
8
+
9
+ const messages = defineMessages({
10
+ resetTeaser: {
11
+ id: 'Reset the block',
12
+ defaultMessage: 'Reset the block',
13
+ },
14
+ });
15
+
16
+ const TeaserData = (props) => {
17
+ const { block, blocksConfig, data, onChangeBlock } = props;
18
+ const intl = useIntl();
19
+
20
+ const reset = () => {
21
+ onChangeBlock(block, {
22
+ ...data,
23
+ href: '',
24
+ title: '',
25
+ description: '',
26
+ head_title: '',
27
+ });
28
+ };
29
+
30
+ const isReseteable =
31
+ isEmpty(data.href) && !data.title && !data.description && !data.head_title;
32
+
33
+ const HeaderActions = (
34
+ <Button.Group>
35
+ <Button
36
+ aria-label={intl.formatMessage(messages.resetTeaser)}
37
+ basic
38
+ disabled={isReseteable}
39
+ onClick={() => reset()}
40
+ >
41
+ <Icon name={trashSVG} size="24px" color="red" />
42
+ </Button>
43
+ </Button.Group>
44
+ );
45
+
46
+ const schema = blocksConfig[data['@type']].blockSchema({ intl });
47
+ const dataAdapter = blocksConfig[data['@type']].dataAdapter;
48
+
49
+ return (
50
+ <BlockDataForm
51
+ schema={schema}
52
+ title={schema.title}
53
+ onChangeField={(id, value) => {
54
+ dataAdapter({
55
+ block,
56
+ data,
57
+ id,
58
+ onChangeBlock,
59
+ value,
60
+ });
61
+ }}
62
+ onChangeBlock={onChangeBlock}
63
+ formData={data}
64
+ block={block}
65
+ blocksConfig={blocksConfig}
66
+ headerActions={HeaderActions}
67
+ />
68
+ );
69
+ };
70
+
71
+ export default TeaserData;
@@ -0,0 +1,89 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Message } from 'semantic-ui-react';
4
+ import { defineMessages, useIntl } from 'react-intl';
5
+ import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
6
+ import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers';
7
+ import { getTeaserImageURL } from './utils';
8
+ import { MaybeWrap } from '@plone/volto/components';
9
+ import { UniversalLink } from '@plone/volto/components';
10
+ import cx from 'classnames';
11
+ import config from '@plone/volto/registry';
12
+
13
+ const messages = defineMessages({
14
+ PleaseChooseContent: {
15
+ id: 'Please choose an existing content as source for this element',
16
+ defaultMessage:
17
+ 'Please choose an existing content as source for this element',
18
+ },
19
+ });
20
+
21
+ const DefaultImage = (props) => <img {...props} alt={props.alt || ''} />;
22
+
23
+ const TeaserDefaultTemplate = (props) => {
24
+ const { className, data, isEditMode } = props;
25
+ const intl = useIntl();
26
+ const href = data.href?.[0];
27
+ const image = data.preview_image?.[0];
28
+ const align = data?.styles?.align;
29
+
30
+ const hasImageComponent = config.getComponent('Image').component;
31
+ const Image = config.getComponent('Image').component || DefaultImage;
32
+ const { openExternalLinkInNewTab } = config.settings;
33
+ const defaultImageSrc =
34
+ href && flattenToAppURL(getTeaserImageURL({ href, image, align }));
35
+
36
+ return (
37
+ <div className={cx('block teaser', className)}>
38
+ <>
39
+ {!href && isEditMode && (
40
+ <Message>
41
+ <div className="teaser-item placeholder">
42
+ <img src={imageBlockSVG} alt="" />
43
+ <p>{intl.formatMessage(messages.PleaseChooseContent)}</p>
44
+ </div>
45
+ </Message>
46
+ )}
47
+ {href && (
48
+ <MaybeWrap
49
+ condition={!isEditMode}
50
+ as={UniversalLink}
51
+ href={href['@id']}
52
+ target={
53
+ data.openLinkInNewTab ||
54
+ (openExternalLinkInNewTab && !isInternalURL(href['@id']))
55
+ ? '_blank'
56
+ : null
57
+ }
58
+ >
59
+ <div className="teaser-item default">
60
+ {(href.hasPreviewImage || href.image_field || image) && (
61
+ <div className="image-wrapper">
62
+ <Image
63
+ src={hasImageComponent ? href : defaultImageSrc}
64
+ alt=""
65
+ loading="lazy"
66
+ />
67
+ </div>
68
+ )}
69
+ <div className="content">
70
+ {data?.head_title && (
71
+ <div className="headline">{data.head_title}</div>
72
+ )}
73
+ <h2>{data?.title}</h2>
74
+ {!data.hide_description && <p>{data?.description}</p>}
75
+ </div>
76
+ </div>
77
+ </MaybeWrap>
78
+ )}
79
+ </>
80
+ </div>
81
+ );
82
+ };
83
+
84
+ TeaserDefaultTemplate.propTypes = {
85
+ data: PropTypes.objectOf(PropTypes.any).isRequired,
86
+ isEditMode: PropTypes.bool,
87
+ };
88
+
89
+ export default TeaserDefaultTemplate;
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import TeaserBody from './Body';
3
+ import { SidebarPortal } from '@plone/volto/components';
4
+ import TeaserData from './Data';
5
+ import { withBlockExtensions } from '@plone/volto/helpers';
6
+
7
+ const TeaserEdit = (props) => {
8
+ const { data, onChangeBlock, block, selected } = props;
9
+
10
+ return (
11
+ <>
12
+ <TeaserBody {...props} isEditMode />
13
+ <SidebarPortal selected={selected}>
14
+ <TeaserData
15
+ {...props}
16
+ data={data}
17
+ block={block}
18
+ onChangeBlock={onChangeBlock}
19
+ />
20
+ </SidebarPortal>
21
+ </>
22
+ );
23
+ };
24
+
25
+ export default withBlockExtensions(TeaserEdit);
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import TeaserBody from './Body';
3
+ import { withBlockExtensions } from '@plone/volto/helpers';
4
+
5
+ const TeaserView = (props) => {
6
+ return <TeaserBody {...props} />;
7
+ };
8
+
9
+ export default withBlockExtensions(TeaserView);
@@ -0,0 +1,23 @@
1
+ import { isEmpty } from 'lodash';
2
+
3
+ export const TeaserBlockDataAdapter = ({
4
+ block,
5
+ data,
6
+ id,
7
+ onChangeBlock,
8
+ value,
9
+ }) => {
10
+ let dataSaved = {
11
+ ...data,
12
+ [id]: value,
13
+ };
14
+ if (id === 'href' && !isEmpty(value) && !data.title && !data.description) {
15
+ dataSaved = {
16
+ ...dataSaved,
17
+ title: value[0].Title,
18
+ description: value[0].Description,
19
+ head_title: value[0].head_title,
20
+ };
21
+ }
22
+ onChangeBlock(block, dataSaved);
23
+ };
@@ -0,0 +1,103 @@
1
+ import { defineMessages } from 'react-intl';
2
+ import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
3
+
4
+ const messages = defineMessages({
5
+ Target: {
6
+ id: 'Target',
7
+ defaultMessage: 'Target',
8
+ },
9
+ imageOverride: {
10
+ id: 'Image override',
11
+ defaultMessage: 'Image override',
12
+ },
13
+ openLinkInNewTab: {
14
+ id: 'Open in a new tab',
15
+ defaultMessage: 'Open in a new tab',
16
+ },
17
+ title: {
18
+ id: 'Title',
19
+ defaultMessage: 'Title',
20
+ },
21
+ description: {
22
+ id: 'Description',
23
+ defaultMessage: 'Description',
24
+ },
25
+ head_title: {
26
+ id: 'head_title',
27
+ defaultMessage: 'Head title',
28
+ },
29
+ teaser: {
30
+ id: 'Teaser',
31
+ defaultMessage: 'Teaser',
32
+ },
33
+ align: {
34
+ id: 'Alignment',
35
+ defaultMessage: 'Alignment',
36
+ },
37
+ });
38
+
39
+ export const TeaserSchema = ({ intl }) => {
40
+ const schema = {
41
+ title: intl.formatMessage(messages.teaser),
42
+ fieldsets: [
43
+ {
44
+ id: 'default',
45
+ title: 'Default',
46
+ fields: ['href', 'title', 'head_title', 'description', 'preview_image'],
47
+ },
48
+ ],
49
+
50
+ properties: {
51
+ href: {
52
+ title: intl.formatMessage(messages.Target),
53
+ widget: 'object_browser',
54
+ mode: 'link',
55
+ selectedItemAttrs: [
56
+ 'Title',
57
+ 'head_title',
58
+ 'Description',
59
+ 'hasPreviewImage',
60
+ 'image_field',
61
+ 'image_scales',
62
+ '@type',
63
+ ],
64
+ allowExternals: true,
65
+ },
66
+ title: {
67
+ title: intl.formatMessage(messages.title),
68
+ },
69
+ head_title: {
70
+ title: intl.formatMessage(messages.head_title),
71
+ },
72
+ description: {
73
+ title: intl.formatMessage(messages.description),
74
+ widget: 'textarea',
75
+ },
76
+ preview_image: {
77
+ title: intl.formatMessage(messages.imageOverride),
78
+ widget: 'object_browser',
79
+ mode: 'image',
80
+ allowExternals: true,
81
+ selectedItemAttrs: ['image_field', 'image_scales'],
82
+ },
83
+ openLinkInNewTab: {
84
+ title: intl.formatMessage(messages.openLinkInNewTab),
85
+ type: 'boolean',
86
+ },
87
+ },
88
+ required: [],
89
+ };
90
+
91
+ addStyling({ schema, intl });
92
+
93
+ schema.properties.styles.schema.properties.align = {
94
+ widget: 'align',
95
+ title: intl.formatMessage(messages.align),
96
+ actions: ['left', 'right', 'center'],
97
+ default: 'left',
98
+ };
99
+
100
+ schema.properties.styles.schema.fieldsets[0].fields = ['align'];
101
+
102
+ return schema;
103
+ };
@@ -0,0 +1,44 @@
1
+ import { isInternalURL } from '@plone/volto/helpers';
2
+ import config from '@plone/volto/registry';
3
+
4
+ export function getTeaserImageURL({ href, image, align }) {
5
+ // The default scale used in teasers is the 'teaser' scale
6
+ // except if it's customized otherwise in the teaser block settings
7
+ // or if the teaser is center (top)
8
+ const imageScale =
9
+ align === 'center'
10
+ ? 'great'
11
+ : config.blocks.blocksConfig['teaser'].imageScale || 'teaser';
12
+
13
+ if (image) {
14
+ // If the image is overriden locally in the teaser block
15
+ if (isInternalURL(image['@id'])) {
16
+ // If it's internal check if image_scales catalog info is present
17
+ if (image?.image_scales?.[image?.image_field]) {
18
+ return `${image['@id']}/${
19
+ image.image_scales[image.image_field]?.[0].scales[imageScale]
20
+ ?.download || image.image_scales[image.image_field]?.[0].download
21
+ }`;
22
+ } else {
23
+ // If not, fallback to content scale URL shortcut
24
+ return `${image['@id']}/@@images/${image.image_field}/${imageScale}`;
25
+ }
26
+ } else {
27
+ // If it's external, return the plain URL
28
+ return image['@id'];
29
+ }
30
+ } else {
31
+ // If the image is not overriden
32
+ if (href?.image_scales?.[href?.image_field]) {
33
+ return `${href['@id']}/${
34
+ href.image_scales[href.image_field]?.[0].scales[imageScale]?.download ||
35
+ href.image_scales[href.image_field]?.[0].download
36
+ }`;
37
+ } else {
38
+ // If not, fallback to content scale URL shortcut
39
+ return `${href['@id']}/@@images/${
40
+ href.image_field || 'preview_image'
41
+ }/${imageScale}`;
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,229 @@
1
+ import { getTeaserImageURL } from './utils';
2
+ import config from '@plone/volto/registry';
3
+
4
+ beforeAll(() => {
5
+ config.blocks.blocksConfig.teaser = {};
6
+ config.blocks.blocksConfig.teaser.imageScale = 'teaser';
7
+ });
8
+
9
+ describe('getTeaserImageURL tests', () => {
10
+ it('getTeaserImageURL internal URL - no overriden', () => {
11
+ const align = 'left';
12
+ const href = {
13
+ '@id': '/document',
14
+ image_field: 'preview_image',
15
+ image_scales: {
16
+ preview_image: [
17
+ {
18
+ download: '@@images/default_original_URL',
19
+ scales: {
20
+ teaser: {
21
+ download: '@@images/teaser_scale_URL',
22
+ },
23
+ },
24
+ },
25
+ ],
26
+ },
27
+ };
28
+ const image = undefined;
29
+ expect(getTeaserImageURL({ href, image, align })).toBe(
30
+ '/document/@@images/teaser_scale_URL',
31
+ );
32
+ });
33
+
34
+ it('getTeaserImageURL internal URL - no overriden - no scale', () => {
35
+ const align = 'left';
36
+ const href = {
37
+ '@id': '/document',
38
+ image_field: 'preview_image',
39
+ image_scales: {
40
+ preview_image: [
41
+ {
42
+ download: '@@images/default_original_URL',
43
+ scales: {},
44
+ },
45
+ ],
46
+ },
47
+ };
48
+ const image = undefined;
49
+ expect(getTeaserImageURL({ href, image, align })).toBe(
50
+ '/document/@@images/default_original_URL',
51
+ );
52
+ });
53
+
54
+ it('getTeaserImageURL internal URL - no overriden - no catalog image info', () => {
55
+ const align = 'left';
56
+ const href = {
57
+ '@id': '/document',
58
+ image_field: 'image',
59
+ image_scales: {
60
+ preview_image: [
61
+ {
62
+ download: '@@images/default_original_URL',
63
+ scales: {},
64
+ },
65
+ ],
66
+ },
67
+ };
68
+ const image = undefined;
69
+ expect(getTeaserImageURL({ href, image, align })).toBe(
70
+ '/document/@@images/image/teaser',
71
+ );
72
+ });
73
+
74
+ it('getTeaserImageURL internal URL - no overriden - center', () => {
75
+ const align = 'center';
76
+ const href = {
77
+ '@id': '/document',
78
+ image_field: 'preview_image',
79
+ image_scales: {
80
+ preview_image: [
81
+ {
82
+ download: '@@images/default_original_URL',
83
+ scales: {
84
+ great: {
85
+ download: '@@images/great_scale_URL',
86
+ },
87
+ teaser: {
88
+ download: '@@images/teaser_scale_URL',
89
+ },
90
+ },
91
+ },
92
+ ],
93
+ },
94
+ };
95
+ const image = undefined;
96
+ expect(getTeaserImageURL({ href, image, align })).toBe(
97
+ '/document/@@images/great_scale_URL',
98
+ );
99
+ });
100
+
101
+ it('getTeaserImageURL internal URL - no overriden - center - no great scale', () => {
102
+ const align = 'center';
103
+ const href = {
104
+ '@id': '/document',
105
+ image_field: 'preview_image',
106
+ image_scales: {
107
+ preview_image: [
108
+ {
109
+ download: '@@images/default_original_URL',
110
+ scales: {
111
+ teaser: {
112
+ download: '@@images/teaser_scale_URL',
113
+ },
114
+ },
115
+ },
116
+ ],
117
+ },
118
+ };
119
+ const image = undefined;
120
+ expect(getTeaserImageURL({ href, image, align })).toBe(
121
+ '/document/@@images/default_original_URL',
122
+ );
123
+ });
124
+
125
+ it('getTeaserImageURL internal URL - image overriden', () => {
126
+ const align = 'left';
127
+ const href = {
128
+ '@id': '/document',
129
+ image_field: 'preview_image',
130
+ image_scales: {
131
+ preview_image: [
132
+ {
133
+ download: '@@images/default_original_URL',
134
+ scales: {
135
+ teaser: {
136
+ download: '@@images/teaser_scale_URL',
137
+ },
138
+ },
139
+ },
140
+ ],
141
+ },
142
+ };
143
+ const image = {
144
+ '@id': '/document/image',
145
+ image_field: 'image',
146
+ image_scales: {
147
+ image: [
148
+ {
149
+ download: '@@images/overriden_image_default_original_URL',
150
+ scales: {
151
+ teaser: {
152
+ download: '@@images/overriden_image_teaser_scale_URL',
153
+ },
154
+ },
155
+ },
156
+ ],
157
+ },
158
+ };
159
+ expect(getTeaserImageURL({ href, image, align })).toBe(
160
+ '/document/image/@@images/overriden_image_teaser_scale_URL',
161
+ );
162
+ });
163
+
164
+ it('getTeaserImageURL internal URL - image overriden - center - no great scale', () => {
165
+ const align = 'center';
166
+ const href = {
167
+ '@id': '/document',
168
+ image_field: 'preview_image',
169
+ image_scales: {
170
+ preview_image: [
171
+ {
172
+ download: '@@images/default_original_URL',
173
+ scales: {
174
+ teaser: {
175
+ download: '@@images/teaser_scale_URL',
176
+ },
177
+ },
178
+ },
179
+ ],
180
+ },
181
+ };
182
+ const image = {
183
+ '@id': '/document/image',
184
+ image_field: 'image',
185
+ image_scales: {
186
+ image: [
187
+ {
188
+ download: '@@images/overriden_image_default_original_URL',
189
+ scales: {
190
+ teaser: {
191
+ download: '@@images/overriden_image_teaser_scale_URL',
192
+ },
193
+ },
194
+ },
195
+ ],
196
+ },
197
+ };
198
+ expect(getTeaserImageURL({ href, image, align })).toBe(
199
+ '/document/image/@@images/overriden_image_default_original_URL',
200
+ );
201
+ });
202
+
203
+ it('getTeaserImageURL internal URL - image overriden - external', () => {
204
+ const align = 'left';
205
+ const href = {
206
+ '@id': '/document',
207
+ image_field: 'preview_image',
208
+ image_scales: {
209
+ preview_image: [
210
+ {
211
+ download: '@@images/default_original_URL',
212
+ scales: {
213
+ teaser: {
214
+ download: '@@images/teaser_scale_URL',
215
+ },
216
+ },
217
+ },
218
+ ],
219
+ },
220
+ };
221
+ const image = {
222
+ '@id': 'https://plone.org/document/image.png',
223
+ };
224
+
225
+ expect(getTeaserImageURL({ href, image, align })).toBe(
226
+ 'https://plone.org/document/image.png',
227
+ );
228
+ });
229
+ });