@plone/volto 17.0.0-alpha.20 → 17.0.0-alpha.21

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 (74) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +19 -0
  3. package/cypress/support/commands.js +2 -1
  4. package/cypress/support/e2e.js +1 -2
  5. package/locales/ca/LC_MESSAGES/volto.po +10 -1
  6. package/locales/ca.json +1 -1
  7. package/locales/de/LC_MESSAGES/volto.po +10 -1
  8. package/locales/de.json +1 -1
  9. package/locales/en/LC_MESSAGES/volto.po +10 -1
  10. package/locales/en.json +1 -1
  11. package/locales/es/LC_MESSAGES/volto.po +10 -1
  12. package/locales/es.json +1 -1
  13. package/locales/eu/LC_MESSAGES/volto.po +10 -1
  14. package/locales/eu.json +1 -1
  15. package/locales/fi/LC_MESSAGES/volto.po +10 -1
  16. package/locales/fi.json +1 -1
  17. package/locales/fr/LC_MESSAGES/volto.po +10 -1
  18. package/locales/fr.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +11 -2
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +10 -1
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +10 -1
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +10 -1
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +10 -1
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +10 -1
  30. package/locales/ro.json +1 -1
  31. package/locales/volto.pot +10 -1
  32. package/locales/zh_CN/LC_MESSAGES/volto.po +10 -1
  33. package/locales/zh_CN.json +1 -1
  34. package/package.json +4 -4
  35. package/packages/volto-slate/package.json +1 -1
  36. package/src/components/index.js +1 -0
  37. package/src/components/manage/Blocks/Image/Edit.jsx +40 -5
  38. package/src/components/manage/Blocks/Image/Edit.test.jsx +2 -0
  39. package/src/components/manage/Blocks/Image/ImageSidebar.jsx +64 -15
  40. package/src/components/manage/Blocks/Image/View.jsx +25 -5
  41. package/src/components/manage/Blocks/Image/View.test.jsx +20 -0
  42. package/src/components/manage/Blocks/Image/schema.js +1 -9
  43. package/src/components/manage/Blocks/Image/utils.js +14 -0
  44. package/src/components/manage/Blocks/LeadImage/Edit.jsx +32 -10
  45. package/src/components/manage/Blocks/LeadImage/Edit.test.jsx +11 -1
  46. package/src/components/manage/Blocks/LeadImage/LeadImageSidebar.jsx +28 -9
  47. package/src/components/manage/Blocks/LeadImage/LeadImageSidebar.test.jsx +8 -2
  48. package/src/components/manage/Blocks/LeadImage/View.jsx +50 -38
  49. package/src/components/manage/Blocks/LeadImage/View.test.jsx +11 -1
  50. package/src/components/manage/Blocks/Listing/SummaryTemplate.jsx +1 -1
  51. package/src/components/manage/Blocks/Teaser/DefaultBody.jsx +13 -23
  52. package/src/components/manage/Contents/Contents.jsx +8 -6
  53. package/src/components/manage/Sidebar/AlignBlock.jsx +1 -1
  54. package/src/components/theme/Image/Image.jsx +96 -0
  55. package/src/components/theme/Image/Image.test.jsx +125 -0
  56. package/src/components/theme/Logo/Logo.jsx +2 -0
  57. package/src/components/theme/PreviewImage/PreviewImage.jsx +25 -14
  58. package/src/components/theme/PreviewImage/PreviewImage.test.js +39 -16
  59. package/src/components/theme/View/AlbumView.jsx +11 -15
  60. package/src/components/theme/View/EventView.jsx +30 -23
  61. package/src/components/theme/View/ImageView.jsx +5 -2
  62. package/src/components/theme/View/ImageView.test.jsx +4 -0
  63. package/src/components/theme/View/ListingView.jsx +5 -3
  64. package/src/components/theme/View/NewsItemView.jsx +7 -13
  65. package/src/components/theme/View/SummaryView.jsx +4 -3
  66. package/src/config/Blocks.jsx +2 -0
  67. package/src/config/Components.jsx +2 -1
  68. package/src/helpers/Url/Url.js +22 -1
  69. package/src/helpers/Url/Url.test.js +41 -0
  70. package/test-setup-config.js +9 -1
  71. package/theme/themes/pastanaga/extras/blocks.less +2 -0
  72. package/theme/themes/pastanaga/extras/main.less +5 -0
  73. package/src/components/manage/Blocks/Teaser/utils.js +0 -44
  74. package/src/components/manage/Blocks/Teaser/utils.test.jsx +0 -229
@@ -4,6 +4,8 @@ import { render } from '@testing-library/react';
4
4
  import { MemoryRouter } from 'react-router-dom';
5
5
  import { Provider } from 'react-intl-redux';
6
6
  import configureStore from 'redux-mock-store';
7
+ import { getImageBlockSizes } from './utils';
8
+ import config from '@plone/volto/registry';
7
9
  import { View } from './View';
8
10
 
9
11
  const mockStore = configureStore();
@@ -15,6 +17,24 @@ const store = mockStore({
15
17
  },
16
18
  });
17
19
 
20
+ config.blocks.blocksConfig = {
21
+ image: {
22
+ id: 'image',
23
+ title: 'Image',
24
+ group: 'media',
25
+ extensions: {},
26
+ variations: [],
27
+ restricted: false,
28
+ mostUsed: true,
29
+ sidebarTab: 1,
30
+ security: {
31
+ addPermission: [],
32
+ view: [],
33
+ },
34
+ getSizes: getImageBlockSizes,
35
+ },
36
+ };
37
+
18
38
  describe('Image View Component', () => {
19
39
  test('renders a view image component with a local image', () => {
20
40
  const { getByRole } = render(<View data={{ url: '/image.jpg' }} />);
@@ -1,10 +1,6 @@
1
1
  import { defineMessages } from 'react-intl';
2
2
 
3
3
  const messages = defineMessages({
4
- Source: {
5
- id: 'Source',
6
- defaultMessage: 'Source',
7
- },
8
4
  Image: {
9
5
  id: 'Image',
10
6
  defaultMessage: 'Image',
@@ -45,7 +41,7 @@ export function ImageSchema({ formData, intl }) {
45
41
  {
46
42
  id: 'default',
47
43
  title: 'Default',
48
- fields: [...(formData.url ? ['url', 'alt', 'align', 'size'] : [])],
44
+ fields: [...(formData.url ? ['alt', 'align', 'size'] : [])],
49
45
  },
50
46
  ...(formData.url
51
47
  ? [
@@ -58,10 +54,6 @@ export function ImageSchema({ formData, intl }) {
58
54
  : []),
59
55
  ],
60
56
  properties: {
61
- url: {
62
- title: intl.formatMessage(messages.Source),
63
- widget: 'url',
64
- },
65
57
  alt: {
66
58
  title: intl.formatMessage(messages.AltText),
67
59
  description: (
@@ -0,0 +1,14 @@
1
+ export function getImageBlockSizes(data) {
2
+ if (data.align === 'full') return '100vw';
3
+ if (data.align === 'center') {
4
+ if (data.size === 'l') return '100vw';
5
+ if (data.size === 'm') return '50vw';
6
+ if (data.size === 's') return '25vw';
7
+ }
8
+ if (data.align === 'left' || data.align === 'right') {
9
+ if (data.size === 'l') return '50vw';
10
+ if (data.size === 'm') return '25vw';
11
+ if (data.size === 's') return '15vw';
12
+ }
13
+ return undefined;
14
+ }
@@ -12,7 +12,7 @@ import { Message } from 'semantic-ui-react';
12
12
  import { isEqual } from 'lodash';
13
13
 
14
14
  import { LeadImageSidebar, SidebarPortal } from '@plone/volto/components';
15
- import { flattenToAppURL } from '@plone/volto/helpers';
15
+ import config from '@plone/volto/registry';
16
16
 
17
17
  import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
18
18
 
@@ -79,11 +79,17 @@ class Edit extends Component {
79
79
  * @returns {string} Markup for the component.
80
80
  */
81
81
  render() {
82
+ const Image = config.getComponent({ name: 'Image' }).component;
82
83
  const { data, properties } = this.props;
83
84
  const placeholder =
84
85
  this.props.data.placeholder ||
85
86
  this.props.intl.formatMessage(messages.ImageBlockInputPlaceholder);
86
87
 
88
+ const hasImage = !!properties.image;
89
+ const hasImageData = hasImage && !!properties.image.data;
90
+ const className = cx('responsive', { 'full-image': data.align === 'full' });
91
+ const altText = data.image_caption || properties.image_caption || '';
92
+
87
93
  return (
88
94
  <div
89
95
  className={cx(
@@ -94,7 +100,7 @@ class Edit extends Component {
94
100
  data.align,
95
101
  )}
96
102
  >
97
- {!properties.image && (
103
+ {!hasImage && (
98
104
  <Message>
99
105
  <center>
100
106
  <img src={imageBlockSVG} alt="" />
@@ -102,15 +108,31 @@ class Edit extends Component {
102
108
  </center>
103
109
  </Message>
104
110
  )}
105
- {properties.image && (
111
+ {hasImage && hasImageData && (
106
112
  <img
107
- className={cx({ 'full-width': data.align === 'full' })}
108
- src={
109
- properties.image.data
110
- ? `data:${properties.image['content-type']};base64,${properties.image.data}`
111
- : flattenToAppURL(properties.image.download)
112
- }
113
- alt={data.image_caption || ''}
113
+ className={className}
114
+ src={`data:${properties.image['content-type']};base64,${properties.image.data}`}
115
+ width={properties.image.width}
116
+ height={properties.image.height}
117
+ alt={altText}
118
+ style={{
119
+ aspectRatio: `${properties.image.width}/${properties.image.height}`,
120
+ }}
121
+ />
122
+ )}
123
+ {hasImage && !hasImageData && (
124
+ <Image
125
+ className={className}
126
+ item={properties}
127
+ imageField="image"
128
+ sizes={(() => {
129
+ if (data.align === 'full' || data.align === 'center')
130
+ return '100vw';
131
+ if (data.align === 'left' || data.align === 'right')
132
+ return '50vw';
133
+ return undefined;
134
+ })()}
135
+ alt={altText}
114
136
  />
115
137
  )}
116
138
  <SidebarPortal selected={this.props.selected}>
@@ -20,7 +20,17 @@ test('renders an edit Lead Image block component', () => {
20
20
  data={{}}
21
21
  properties={{
22
22
  image: {
23
- download: 'image.png',
23
+ download: 'http://localhost:3000/image.png',
24
+ width: 400,
25
+ height: 400,
26
+ scales: {
27
+ preview: {
28
+ download:
29
+ 'http://localhost:3000/image.png/@@images/image/image-400.png',
30
+ width: 400,
31
+ height: 400,
32
+ },
33
+ },
24
34
  },
25
35
  }}
26
36
  selected={false}
@@ -3,7 +3,12 @@ import PropTypes from 'prop-types';
3
3
  import { Form } from 'semantic-ui-react';
4
4
  import { Accordion, Grid, Segment } from 'semantic-ui-react';
5
5
  import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
6
- import { CheckboxWidget, Icon, TextWidget } from '@plone/volto/components';
6
+ import {
7
+ CheckboxWidget,
8
+ Icon,
9
+ Image,
10
+ TextWidget,
11
+ } from '@plone/volto/components';
7
12
  import { flattenToAppURL } from '@plone/volto/helpers';
8
13
  import AlignBlock from '@plone/volto/components/manage/Sidebar/AlignBlock';
9
14
 
@@ -90,14 +95,28 @@ const LeadImageSidebar = ({
90
95
  <>
91
96
  <Segment className="sidebar-metadata-container" secondary>
92
97
  {properties.image.filename}
93
- <img
94
- src={
95
- properties.image.data
96
- ? `data:${properties.image['content-type']};base64,${properties.image.data}`
97
- : flattenToAppURL(properties.image.scales.mini.download)
98
- }
99
- alt={properties.image_caption || ''}
100
- />
98
+ {properties.image.data && (
99
+ <img
100
+ // TODO understand when this actually happens
101
+ src={`data:${properties.image['content-type']};base64,${properties.image.data}`}
102
+ width={properties.image.width}
103
+ height={properties.image.height}
104
+ alt={data.image_caption || properties.image_caption || ''}
105
+ className="responsive"
106
+ style={{
107
+ aspectRatio: `${properties.image.width} / ${properties.image.height}`,
108
+ }}
109
+ />
110
+ )}
111
+ {!properties.image.data && (
112
+ <Image
113
+ item={properties}
114
+ imageField="image"
115
+ alt={data.image_caption || properties.image_caption || ''}
116
+ responsive={true}
117
+ sizes="188px"
118
+ />
119
+ )}
101
120
  </Segment>
102
121
  <Segment className="form sidebar-image-data">
103
122
  <TextWidget
@@ -20,9 +20,15 @@ test('renders a Lead Image block Sidebar component', () => {
20
20
  data={{}}
21
21
  properties={{
22
22
  image: {
23
+ download: 'http://localhost:3000/image.png',
24
+ width: 400,
25
+ height: 400,
23
26
  scales: {
24
- mini: {
25
- download: 'image.png',
27
+ preview: {
28
+ download:
29
+ 'http://localhost:3000/image.png/@@images/image/image-400.png',
30
+ width: 400,
31
+ height: 400,
26
32
  },
27
33
  },
28
34
  },
@@ -7,51 +7,63 @@ import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { UniversalLink } from '@plone/volto/components';
9
9
  import cx from 'classnames';
10
-
11
- import { flattenToAppURL } from '@plone/volto/helpers';
10
+ import config from '@plone/volto/registry';
12
11
 
13
12
  /**
14
13
  * View image block class.
15
14
  * @class View
16
15
  * @extends Component
17
16
  */
18
- const View = ({ data, properties }) => (
19
- <p
20
- className={cx(
21
- 'block image align',
22
- {
23
- center: !Boolean(data.align),
24
- },
25
- data.align,
26
- )}
27
- >
28
- {properties.image && (
29
- <>
30
- {(() => {
31
- const image = (
32
- <img
33
- className={cx({ 'full-width': data.align === 'full' })}
34
- src={flattenToAppURL(properties.image.download)}
35
- alt={properties.image_caption || ''}
36
- />
37
- );
38
- if (data.href) {
39
- return (
40
- <UniversalLink
41
- href={data.href}
42
- openLinkInNewTab={data.openLinkInNewTab}
43
- >
44
- {image}
45
- </UniversalLink>
17
+ const View = ({ data, properties }) => {
18
+ const Image = config.getComponent({ name: 'Image' }).component;
19
+
20
+ return (
21
+ <p
22
+ className={cx(
23
+ 'block image align',
24
+ {
25
+ center: !Boolean(data.align),
26
+ },
27
+ data.align,
28
+ )}
29
+ >
30
+ {properties.image && (
31
+ <>
32
+ {(() => {
33
+ const image = (
34
+ <Image
35
+ className={cx({ 'full-width': data.align === 'full' })}
36
+ item={properties}
37
+ imageField="image"
38
+ sizes={(() => {
39
+ if (data.align === 'full' || data.align === 'center')
40
+ return '100vw';
41
+ if (data.align === 'left' || data.align === 'right')
42
+ return '50vw';
43
+ return undefined;
44
+ })()}
45
+ alt={properties.image_caption || ''}
46
+ responsive={true}
47
+ />
46
48
  );
47
- } else {
48
- return image;
49
- }
50
- })()}
51
- </>
52
- )}
53
- </p>
54
- );
49
+ if (data.href) {
50
+ return (
51
+ <UniversalLink
52
+ href={data.href}
53
+ openLinkInNewTab={data.openLinkInNewTab}
54
+ >
55
+ {image}
56
+ </UniversalLink>
57
+ );
58
+ } else {
59
+ return image;
60
+ }
61
+ })()}
62
+ </>
63
+ )}
64
+ </p>
65
+ );
66
+ };
55
67
 
56
68
  /**
57
69
  * Property types.
@@ -8,7 +8,17 @@ test('renders a view image component', () => {
8
8
  data={{}}
9
9
  properties={{
10
10
  image: {
11
- download: 'image.png',
11
+ download: 'http://localhost:3000/image.png',
12
+ width: 400,
13
+ height: 400,
14
+ scales: {
15
+ preview: {
16
+ download:
17
+ 'http://localhost:3000/image.png/@@images/image/image-400.png',
18
+ width: 400,
19
+ height: 400,
20
+ },
21
+ },
12
22
  },
13
23
  }}
14
24
  />,
@@ -27,7 +27,7 @@ const SummaryTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
27
27
  <ConditionalLink item={item} condition={!isEditMode}>
28
28
  <Component componentName="PreviewImage" item={item} alt="" />
29
29
  <div className="listing-body">
30
- <h3>{item.title ? item.title : item.id}</h3>
30
+ <h3>{item.title || item.id}</h3>
31
31
  <p>{item.description}</p>
32
32
  </div>
33
33
  </ConditionalLink>
@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
3
3
  import { Message } from 'semantic-ui-react';
4
4
  import { defineMessages, useIntl } from 'react-intl';
5
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';
6
+ import { isInternalURL } from '@plone/volto/helpers';
8
7
  import { MaybeWrap } from '@plone/volto/components';
9
8
  import { UniversalLink } from '@plone/volto/components';
10
9
  import cx from 'classnames';
@@ -22,28 +21,11 @@ const TeaserDefaultTemplate = (props) => {
22
21
  const { className, data, isEditMode } = props;
23
22
  const intl = useIntl();
24
23
  const href = data.href?.[0];
25
- const imageOverride = data.preview_image?.[0];
26
- const align = data?.styles?.align;
24
+ const image = data.preview_image?.[0];
27
25
 
26
+ const Image = config.getComponent('Image').component;
28
27
  const { openExternalLinkInNewTab } = config.settings;
29
28
 
30
- let renderedImage = null;
31
- if (href && (imageOverride || href.hasPreviewImage || href.image_field)) {
32
- let Image = config.getComponent('Image').component;
33
- if (Image) {
34
- // custom image component expects item summary as src
35
- renderedImage = (
36
- <Image src={imageOverride || href} alt="" loading="lazy" />
37
- );
38
- } else {
39
- // default img expects string src
40
- const src = flattenToAppURL(
41
- getTeaserImageURL({ href, imageOverride, align }),
42
- );
43
- renderedImage = <img src={src} alt="" loading="lazy" />;
44
- }
45
- }
46
-
47
29
  return (
48
30
  <div className={cx('block teaser', className)}>
49
31
  <>
@@ -68,8 +50,16 @@ const TeaserDefaultTemplate = (props) => {
68
50
  }
69
51
  >
70
52
  <div className="teaser-item default">
71
- {renderedImage && (
72
- <div className="image-wrapper">{renderedImage}</div>
53
+ {(href.hasPreviewImage || href.image_field || image) && (
54
+ <div className="image-wrapper">
55
+ <Image
56
+ item={image || href}
57
+ imageField={image ? image.image_field : href.image_field}
58
+ alt=""
59
+ loading="lazy"
60
+ responsive={true}
61
+ />
62
+ </div>
73
63
  )}
74
64
  <div className="content">
75
65
  {data?.head_title && (
@@ -450,7 +450,7 @@ class Contents extends Component {
450
450
  sort_on: this.props.sort?.on || 'getObjPositionInParent',
451
451
  sort_order: this.props.sort?.order || 'ascending',
452
452
  isClient: false,
453
- linkIntegrityBreakages: '',
453
+ linkIntegrityBreakages: [],
454
454
  };
455
455
  this.filterTimeout = null;
456
456
  }
@@ -1236,7 +1236,10 @@ class Contents extends Component {
1236
1236
  Show all items
1237
1237
  </Button>
1238
1238
  )}
1239
- {this.state.linkIntegrityBreakages.length > 0 ? (
1239
+ {this.state.linkIntegrityBreakages.reduce(
1240
+ (a, b) => a + b.breaches.length,
1241
+ 0,
1242
+ ) ? (
1240
1243
  <div>
1241
1244
  <h3>
1242
1245
  {this.props.intl.formatMessage(
@@ -1249,9 +1252,8 @@ class Contents extends Component {
1249
1252
  )}
1250
1253
  </p>
1251
1254
  <ul className="content">
1252
- {map(
1253
- this.state.linkIntegrityBreakages,
1254
- (item) => (
1255
+ {map(this.state.linkIntegrityBreakages, (item) =>
1256
+ item.breaches.length ? (
1255
1257
  <li key={item['@id']}>
1256
1258
  <a href={item['@id']}>{item.title}</a>
1257
1259
  <p>
@@ -1269,7 +1271,7 @@ class Contents extends Component {
1269
1271
  ))}
1270
1272
  </ul>
1271
1273
  </li>
1272
- ),
1274
+ ) : null,
1273
1275
  )}
1274
1276
  </ul>
1275
1277
  </div>
@@ -57,7 +57,7 @@ const AlignBlock = ({
57
57
  return (
58
58
  <div className="align-buttons">
59
59
  {actions.map((action) => (
60
- <Button.Group>
60
+ <Button.Group key={action}>
61
61
  <Button
62
62
  icon
63
63
  basic
@@ -0,0 +1,96 @@
1
+ import PropTypes from 'prop-types';
2
+ import cx from 'classnames';
3
+ import { flattenToAppURL } from '@plone/volto/helpers';
4
+
5
+ /**
6
+ * Image component
7
+ * @param {object} item - Context item that has the image field (can also be a catalog brain or summary)
8
+ * @param {string} imageField - Key of the image field inside the item, or inside the image_scales object of the item if it is a catalog brain or summary
9
+ * @param {string} src - URL of the image to be used if the item field is not available
10
+ * @param {string} alt - Alternative text for the image
11
+ * @param {boolean} loading - (default: eager) set to `lazy` to lazy load the image
12
+ * @param {boolean} responsive - (default: false) set to `true` to add the `responsive` class to the image
13
+ * @param {string} className - Additional classes to add to the image
14
+ */
15
+ export default function Image({
16
+ item,
17
+ imageField,
18
+ src,
19
+ alt = '',
20
+ loading = 'eager',
21
+ responsive = false,
22
+ className = '',
23
+ ...imageProps
24
+ }) {
25
+ if (!item && !src) return null;
26
+
27
+ // TypeScript hints for editor autocomplete :)
28
+ /** @type {React.ImgHTMLAttributes<HTMLImageElement>} */
29
+ const attrs = {};
30
+
31
+ if (!item && src) {
32
+ attrs.src = src;
33
+ attrs.className = cx(className, { responsive });
34
+ } else {
35
+ const isFromRealObject = !item.image_scales;
36
+ const imageFieldWithDefault = imageField || item.image_field || 'image';
37
+
38
+ const image = isFromRealObject
39
+ ? item[imageFieldWithDefault]
40
+ : item.image_scales[imageFieldWithDefault]?.[0];
41
+
42
+ if (!image) return null;
43
+
44
+ const isSvg = image['content-type'] === 'image/svg+xml';
45
+
46
+ const baseUrl = isFromRealObject ? '' : flattenToAppURL(item['@id'] + '/');
47
+
48
+ attrs.src = `${baseUrl}${flattenToAppURL(image.download)}`;
49
+ attrs.width = image.width;
50
+ attrs.height = image.height;
51
+ attrs.style = {
52
+ aspectRatio: `${image.width} / ${image.height}`,
53
+ ...imageProps.style,
54
+ };
55
+ attrs.className = cx(className, { responsive });
56
+
57
+ if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
58
+ const sortedScales = Object.values(image.scales).sort((a, b) => {
59
+ if (a.width > b.width) return 1;
60
+ else if (a.width < b.width) return -1;
61
+ else return 0;
62
+ });
63
+
64
+ attrs.srcSet = sortedScales
65
+ .map(
66
+ (scale) =>
67
+ `${baseUrl}${flattenToAppURL(scale.download)} ${scale.width}w`,
68
+ )
69
+ .join(', ');
70
+ }
71
+ }
72
+
73
+ if (loading === 'lazy') {
74
+ attrs.loading = 'lazy';
75
+ attrs.decoding = 'async';
76
+ } else {
77
+ attrs.fetchpriority = 'high';
78
+ }
79
+
80
+ return <img {...attrs} alt={alt} {...imageProps} />;
81
+ }
82
+
83
+ Image.propTypes = {
84
+ item: PropTypes.shape({
85
+ '@id': PropTypes.string,
86
+ image_field: PropTypes.string,
87
+ image_scales: PropTypes.object,
88
+ image: PropTypes.object,
89
+ }),
90
+ imageField: PropTypes.string,
91
+ src: PropTypes.string,
92
+ alt: PropTypes.string.isRequired,
93
+ loading: PropTypes.string,
94
+ responsive: PropTypes.bool,
95
+ className: PropTypes.string,
96
+ };