@plone/volto 19.0.0-alpha.1 → 19.0.0-alpha.10

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 (238) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc +37 -2
  3. package/CHANGELOG.md +228 -0
  4. package/README.md +3 -6
  5. package/cypress/docker/prefixed-rules.yml +26 -0
  6. package/cypress/docker/prefixed.yml +24 -0
  7. package/cypress/support/commands.js +12 -6
  8. package/cypress/support/guillotina.js +1 -0
  9. package/cypress.config.js +1 -0
  10. package/locales/af.json +1 -0
  11. package/locales/ar.json +1 -0
  12. package/locales/bg.json +1 -0
  13. package/locales/bn.json +1 -0
  14. package/locales/ca/LC_MESSAGES/volto.po +96 -17
  15. package/locales/ca.json +1 -1
  16. package/locales/cs.json +1 -0
  17. package/locales/cy.json +1 -0
  18. package/locales/da.json +1 -0
  19. package/locales/de/LC_MESSAGES/volto.po +104 -25
  20. package/locales/de.json +1 -1
  21. package/locales/el.json +1 -0
  22. package/locales/en/LC_MESSAGES/volto.po +97 -18
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU.json +1 -0
  25. package/locales/en_GB.json +1 -0
  26. package/locales/eo.json +1 -0
  27. package/locales/es/LC_MESSAGES/volto.po +97 -18
  28. package/locales/es.json +1 -1
  29. package/locales/et.json +1 -0
  30. package/locales/eu/LC_MESSAGES/volto.po +96 -17
  31. package/locales/eu.json +1 -1
  32. package/locales/fa.json +1 -0
  33. package/locales/fi/LC_MESSAGES/volto.po +96 -17
  34. package/locales/fi.json +1 -1
  35. package/locales/fr/LC_MESSAGES/volto.po +97 -18
  36. package/locales/fr.json +1 -1
  37. package/locales/fu.json +1 -0
  38. package/locales/gl.json +1 -0
  39. package/locales/he.json +1 -0
  40. package/locales/hi/LC_MESSAGES/volto.po +100 -21
  41. package/locales/hi.json +1 -1
  42. package/locales/hr.json +1 -0
  43. package/locales/hu.json +1 -0
  44. package/locales/hy.json +1 -0
  45. package/locales/id.json +1 -0
  46. package/locales/it/LC_MESSAGES/volto.po +101 -22
  47. package/locales/it.json +1 -1
  48. package/locales/ja/LC_MESSAGES/volto.po +96 -17
  49. package/locales/ja.json +1 -1
  50. package/locales/ka.json +1 -0
  51. package/locales/kn.json +1 -0
  52. package/locales/ko.json +1 -0
  53. package/locales/lt.json +1 -0
  54. package/locales/lv.json +1 -0
  55. package/locales/mi.json +1 -0
  56. package/locales/mk.json +1 -0
  57. package/locales/my.json +1 -0
  58. package/locales/nb_NO.json +1 -0
  59. package/locales/nl/LC_MESSAGES/volto.po +100 -21
  60. package/locales/nl.json +1 -1
  61. package/locales/nn.json +1 -0
  62. package/locales/pl.json +1 -0
  63. package/locales/pt/LC_MESSAGES/volto.po +96 -17
  64. package/locales/pt.json +1 -1
  65. package/locales/pt_BR/LC_MESSAGES/volto.po +116 -37
  66. package/locales/pt_BR.json +1 -1
  67. package/locales/rm.json +1 -0
  68. package/locales/ro/LC_MESSAGES/volto.po +100 -21
  69. package/locales/ro.json +1 -1
  70. package/locales/ru/LC_MESSAGES/volto.po +100 -21
  71. package/locales/ru.json +1 -1
  72. package/locales/sk.json +1 -0
  73. package/locales/sl.json +1 -0
  74. package/locales/sm.json +1 -0
  75. package/locales/sq.json +1 -0
  76. package/locales/sr.json +1 -0
  77. package/locales/sr@cyrl.json +1 -0
  78. package/locales/sr@latn.json +1 -0
  79. package/locales/sv.json +1 -1
  80. package/locales/ta.json +1 -0
  81. package/locales/te.json +1 -0
  82. package/locales/th.json +1 -0
  83. package/locales/to.json +1 -0
  84. package/locales/tr.json +1 -0
  85. package/locales/uk.json +1 -0
  86. package/locales/vi.json +1 -0
  87. package/locales/volto.pot +97 -18
  88. package/locales/zh_CN/LC_MESSAGES/volto.po +96 -17
  89. package/locales/zh_CN.json +1 -1
  90. package/locales/zh_Hant.json +1 -0
  91. package/locales/zh_Hant_HK.json +1 -0
  92. package/package.json +28 -26
  93. package/razzle.config.js +16 -0
  94. package/src/actions/content/content.js +0 -1
  95. package/src/actions/controlpanels/controlpanels.js +13 -7
  96. package/src/actions/controlpanels/controlpanels.test.js +11 -5
  97. package/src/actions/users/users.js +2 -2
  98. package/src/components/manage/Add/Add.jsx +5 -6
  99. package/src/components/manage/Blocks/Block/Edit.jsx +1 -0
  100. package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +9 -4
  101. package/src/components/manage/Blocks/Image/Edit.jsx +5 -1
  102. package/src/components/manage/Blocks/LeadImage/Edit.jsx +2 -2
  103. package/src/components/manage/Blocks/LeadImage/LeadImageSidebar.jsx +1 -1
  104. package/src/components/manage/Blocks/Listing/ImageGallery.jsx +6 -4
  105. package/src/components/manage/Blocks/Maps/Edit.jsx +2 -1
  106. package/src/components/manage/Blocks/Search/SearchBlockView.jsx +21 -4
  107. package/src/components/manage/Blocks/Teaser/Data.jsx +21 -7
  108. package/src/components/manage/Blocks/Teaser/DefaultBody.jsx +1 -1
  109. package/src/components/manage/Blocks/Teaser/schema.js +8 -3
  110. package/src/components/manage/Blocks/Title/Edit.jsx +8 -2
  111. package/src/components/manage/Blocks/Video/Edit.jsx +2 -1
  112. package/src/components/manage/Contents/Contents.jsx +23 -2
  113. package/src/components/manage/Contents/Contents.test.jsx +7 -0
  114. package/src/components/manage/Contents/ContentsBreadcrumbs.Multilingual.test.jsx +18 -5
  115. package/src/components/manage/Contents/ContentsBreadcrumbs.jsx +20 -26
  116. package/src/components/manage/Contents/ContentsBreadcrumbs.test.jsx +14 -0
  117. package/src/components/manage/Contents/ContentsDeleteModal.jsx +258 -206
  118. package/src/components/manage/Contents/ContentsDeleteModal.stories.jsx +26 -8
  119. package/src/components/manage/Contents/ContentsItem.jsx +10 -2
  120. package/src/components/manage/Contents/ContentsUploadModal.test.jsx +13 -22
  121. package/src/components/manage/Controlpanels/ContentType.jsx +1 -1
  122. package/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx +3 -2
  123. package/src/components/manage/Controlpanels/Users/RenderUsers.jsx +156 -175
  124. package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +575 -630
  125. package/src/components/manage/Controlpanels/Users/UsersControlpanel.test.jsx +4 -3
  126. package/src/components/manage/Edit/Edit.jsx +2 -3
  127. package/src/components/manage/Form/Form.jsx +32 -0
  128. package/src/components/manage/Form/Form.test.jsx +22 -18
  129. package/src/components/manage/Form/InlineForm.jsx +2 -2
  130. package/src/components/manage/Multilingual/CompareLanguages.jsx +2 -5
  131. package/src/components/manage/Multilingual/CreateTranslation.jsx +8 -8
  132. package/src/components/manage/Multilingual/ManageTranslations.jsx +4 -2
  133. package/src/components/manage/Multilingual/ManageTranslations.test.jsx +5 -1
  134. package/src/components/manage/Multilingual/TranslationObject.jsx +1 -1
  135. package/src/components/manage/Sidebar/ObjectBrowserNav.jsx +2 -1
  136. package/src/components/manage/Sidebar/SidebarPortal.test.tsx +42 -0
  137. package/src/components/manage/Sidebar/SidebarPortal.tsx +48 -0
  138. package/src/components/manage/TemplateChooser/TemplateChooser.jsx +2 -1
  139. package/src/components/manage/Toolbar/More.jsx +4 -1
  140. package/src/components/manage/Toolbar/More.test.jsx +3 -0
  141. package/src/components/manage/Toolbar/PersonalTools.jsx +2 -1
  142. package/src/components/manage/Toolbar/Toolbar.jsx +3 -4
  143. package/src/components/manage/Toolbar/Types.jsx +7 -7
  144. package/src/components/manage/UniversalLink/UniversalLink.tsx +1 -0
  145. package/src/components/manage/Widgets/DatetimeWidget.jsx +5 -0
  146. package/src/components/manage/Widgets/FileWidget.jsx +14 -8
  147. package/src/components/manage/Widgets/ImageWidget.jsx +171 -38
  148. package/src/components/manage/Widgets/InternalUrlWidget.jsx +2 -0
  149. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +3 -0
  150. package/src/components/manage/Widgets/RegistryImageWidget.test.jsx +3 -2
  151. package/src/components/manage/Widgets/SelectAutoComplete.jsx +29 -12
  152. package/src/components/manage/Widgets/SelectWidget.jsx +3 -1
  153. package/src/components/manage/Widgets/UrlWidget.jsx +2 -0
  154. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +1 -3
  155. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +0 -4
  156. package/src/components/theme/App/App.jsx +3 -1
  157. package/src/components/theme/App/App.test.jsx +1 -0
  158. package/src/components/theme/Avatar/Avatar.jsx +2 -1
  159. package/src/components/theme/FormattedDate/FormattedDate.stories.jsx +20 -2
  160. package/src/components/theme/Image/Image.jsx +11 -8
  161. package/src/components/theme/LanguageSelector/{LanguageSelector.test.jsx → LanguageSelector.test.tsx} +6 -6
  162. package/src/components/theme/LanguageSelector/LanguageSelector.tsx +89 -0
  163. package/src/components/theme/Logo/Logo.Multilingual.test.jsx +0 -5
  164. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +8 -12
  165. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.test.jsx +3 -5
  166. package/src/components/theme/Navigation/NavItem.jsx +1 -5
  167. package/src/components/theme/Navigation/Navigation.Multilingual.test.jsx +0 -5
  168. package/src/components/theme/NotFound/NotFound.jsx +5 -2
  169. package/src/components/theme/NotFound/NotFound.test.jsx +3 -0
  170. package/src/components/theme/PreviewImage/PreviewImage.jsx +1 -1
  171. package/src/components/theme/RequestTimeout/RequestTimeout.jsx +1 -1
  172. package/src/components/theme/Sitemap/Sitemap.jsx +6 -5
  173. package/src/components/theme/Sitemap/Sitemap.test.jsx +0 -1
  174. package/src/components/theme/View/FileView.jsx +9 -1
  175. package/src/components/theme/View/View.jsx +1 -1
  176. package/src/components/theme/Widgets/ImageWidget.jsx +2 -1
  177. package/src/config/ControlPanels.js +1 -0
  178. package/src/config/index.js +18 -25
  179. package/src/config/server.js +0 -2
  180. package/src/express-middleware/devproxy.js +20 -5
  181. package/src/helpers/Api/APIResourceWithAuth.js +8 -3
  182. package/src/helpers/Api/Api.js +7 -4
  183. package/src/helpers/AsyncConnect/ssr.js +4 -1
  184. package/src/helpers/Content/Content.js +23 -0
  185. package/src/helpers/Content/Content.test.js +39 -0
  186. package/src/helpers/Content/withClientSideContent.jsx +35 -0
  187. package/src/helpers/Extensions/withBlockSchemaEnhancer.jsx +4 -1
  188. package/src/helpers/Html/Html.jsx +13 -7
  189. package/src/helpers/LanguageMap/LanguageMap.js +115 -8
  190. package/src/helpers/MessageLabels/MessageLabels.js +5 -0
  191. package/src/helpers/Sitemap/Sitemap.js +4 -4
  192. package/src/helpers/Url/Url.js +32 -2
  193. package/src/helpers/Url/Url.test.js +62 -0
  194. package/src/helpers/Utils/withSaveAsDraft.jsx +241 -0
  195. package/src/hooks/user/useUser.js +1 -1
  196. package/src/middleware/Api.test.js +4 -0
  197. package/src/middleware/api.js +77 -28
  198. package/src/middleware/storeProtectLoadUtils.test.js +3 -3
  199. package/src/reducers/content/content.js +3 -18
  200. package/src/reducers/diff/diff.js +5 -1
  201. package/src/reducers/diff/diff.test.js +60 -4
  202. package/src/routes.js +4 -2
  203. package/src/server.jsx +45 -14
  204. package/src/start-client.jsx +9 -6
  205. package/src/start-server.js +9 -3
  206. package/test-setup-config.jsx +0 -2
  207. package/theme/themes/pastanaga/collections/table.overrides +9 -0
  208. package/theme/themes/pastanaga/extras/blocks.less +26 -0
  209. package/theme/themes/pastanaga/extras/contents.less +17 -5
  210. package/theme/themes/pastanaga/extras/main.less +32 -2
  211. package/tsconfig.json +3 -4
  212. package/types/components/manage/Blocks/Teaser/schema.d.ts +1 -0
  213. package/types/components/manage/Controlpanels/Users/RenderUsers.d.ts +18 -2
  214. package/types/components/manage/Controlpanels/Users/UsersControlpanel.d.ts +6 -2
  215. package/types/components/manage/Controlpanels/index.d.ts +2 -2
  216. package/types/components/manage/Sidebar/SidebarPortal.d.ts +7 -15
  217. package/types/components/manage/Widgets/ImageWidget.d.ts +41 -1
  218. package/types/components/manage/Widgets/RecurrenceWidget/Utils.d.ts +12 -18
  219. package/types/components/theme/FormattedDate/FormattedDate.stories.d.ts +1 -1
  220. package/types/components/theme/LanguageSelector/LanguageSelector.d.ts +3 -10
  221. package/types/helpers/Content/Content.d.ts +7 -0
  222. package/types/helpers/Content/withClientSideContent.d.ts +1 -0
  223. package/types/helpers/Extensions/withBlockSchemaEnhancer.d.ts +4 -5
  224. package/types/helpers/Helmet/Helmet.d.ts +1 -1
  225. package/types/helpers/LanguageMap/LanguageMap.d.ts +428 -4
  226. package/types/helpers/MessageLabels/MessageLabels.d.ts +68 -62
  227. package/types/helpers/Url/Url.d.ts +14 -0
  228. package/types/helpers/Url/bulkFlattenToAppURL.d.ts +5 -0
  229. package/types/helpers/Utils/withSaveAsDraft.d.ts +1 -0
  230. package/types/middleware/api.d.ts +6 -9
  231. package/types/reducers/index.d.ts +1 -0
  232. package/types/start-client.d.ts +0 -1
  233. package/package-why.json +0 -34
  234. package/src/actions/content/content.multilingual.test.js +0 -17
  235. package/src/components/manage/Sidebar/SidebarPortal.jsx +0 -47
  236. package/src/components/manage/Sidebar/SidebarPortal.test.jsx +0 -26
  237. package/src/components/theme/LanguageSelector/LanguageSelector.jsx +0 -77
  238. package/theme/themes/pastanaga/extras/utils.less +0 -63
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
2
  import { Button, Dimmer, Loader, Message } from 'semantic-ui-react';
3
3
  import { useIntl, defineMessages } from 'react-intl';
4
- import { useDispatch } from 'react-redux';
4
+ import { useDispatch, useSelector } from 'react-redux';
5
5
  import { useLocation } from 'react-router-dom';
6
6
  import loadable from '@loadable/component';
7
7
  import { connect } from 'react-redux';
@@ -9,11 +9,15 @@ import { compose } from 'redux';
9
9
  import { toast } from 'react-toastify';
10
10
  import useLinkEditor from '@plone/volto/components/manage/AnchorPlugin/useLinkEditor';
11
11
  import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
12
+ import config from '@plone/volto/registry';
12
13
 
13
14
  import {
14
15
  flattenToAppURL,
15
16
  getBaseUrl,
17
+ getParentUrl,
16
18
  isInternalURL,
19
+ normalizeUrl,
20
+ removeProtocol,
17
21
  } from '@plone/volto/helpers/Url/Url';
18
22
  import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
19
23
  import { usePrevious } from '@plone/volto/helpers/Utils/usePrevious';
@@ -28,6 +32,9 @@ import clearSVG from '@plone/volto/icons/clear.svg';
28
32
  import navTreeSVG from '@plone/volto/icons/nav.svg';
29
33
  import linkSVG from '@plone/volto/icons/link.svg';
30
34
  import uploadSVG from '@plone/volto/icons/upload.svg';
35
+ import Image from '../../theme/Image/Image';
36
+ import { urlValidator } from '@plone/volto/helpers/FormValidation/validators';
37
+ import { searchContent } from '@plone/volto/actions/search/search';
31
38
 
32
39
  const Dropzone = loadable(() => import('react-dropzone'));
33
40
 
@@ -70,6 +77,14 @@ const messages = defineMessages({
70
77
  id: 'imageUploadErrorMessage',
71
78
  defaultMessage: 'Please upload an image instead.',
72
79
  },
80
+ externalURLsNotAllowed: {
81
+ id: 'externalURLsNotAllowed',
82
+ defaultMessage: 'External URLs are not allowed in this field.',
83
+ },
84
+ internalImageNotFoundErrorMessage: {
85
+ id: 'internalImageNotFoundErrorMessage',
86
+ defaultMessage: 'No image was found in the internal path you provided.',
87
+ },
73
88
  });
74
89
 
75
90
  const UnconnectedImageInput = (props) => {
@@ -89,12 +104,15 @@ const UnconnectedImageInput = (props) => {
89
104
  placeholderLinkInput = '',
90
105
  onSelectItem,
91
106
  } = props;
92
- const imageValue = value?.[0]?.['@id'] || value;
107
+ const imageValue = value?.[0]?.['@id'] || value?.['@id'] || value;
93
108
 
94
109
  const intl = useIntl();
95
110
  const linkEditor = useLinkEditor();
96
111
  const location = useLocation();
97
112
  const dispatch = useDispatch();
113
+ const isFolderish = useSelector(
114
+ (state) => state?.content?.data?.is_folderish,
115
+ );
98
116
  const contextUrl = location.pathname;
99
117
 
100
118
  const [uploading, setUploading] = React.useState(false);
@@ -109,21 +127,41 @@ const UnconnectedImageInput = (props) => {
109
127
  const imageId = content?.['@id'];
110
128
  const image = content?.image;
111
129
  let loading = false;
130
+ const isRelationChoice = props.factory === 'Relation Choice';
112
131
 
113
132
  useEffect(() => {
114
133
  if (uploading && loading && loaded) {
115
134
  setUploading(false);
116
- onChange(id, imageId, {
117
- image_field: 'image',
118
- image_scales: { image: [image] },
119
- });
135
+ if (isRelationChoice) {
136
+ onChange(id, content, {
137
+ image_field: 'image',
138
+ image_scales: { image: [image] },
139
+ });
140
+ } else {
141
+ onChange(id, imageId, {
142
+ image_field: 'image',
143
+ image_scales: { image: [image] },
144
+ });
145
+ }
120
146
  }
121
- }, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
147
+ }, [
148
+ loading,
149
+ loaded,
150
+ uploading,
151
+ imageId,
152
+ image,
153
+ id,
154
+ content,
155
+ isRelationChoice,
156
+ onChange,
157
+ ]);
122
158
 
123
159
  loading = usePrevious(props.request?.loading);
124
160
 
125
161
  const handleUpload = React.useCallback(
126
162
  (eventOrFile) => {
163
+ let uploadUrl = getBaseUrl(contextUrl);
164
+ if (!isFolderish) uploadUrl = getParentUrl(uploadUrl);
127
165
  if (restrictFileUpload === true) return;
128
166
  eventOrFile.target && eventOrFile.stopPropagation();
129
167
 
@@ -139,7 +177,7 @@ const UnconnectedImageInput = (props) => {
139
177
  const fields = fileData.match(/^data:(.*);(.*),(.*)$/);
140
178
  dispatch(
141
179
  createContent(
142
- getBaseUrl(contextUrl),
180
+ uploadUrl,
143
181
  {
144
182
  '@type': 'Image',
145
183
  title: file.name,
@@ -156,11 +194,12 @@ const UnconnectedImageInput = (props) => {
156
194
  });
157
195
  },
158
196
  [
197
+ contextUrl,
198
+ isFolderish,
159
199
  restrictFileUpload,
160
200
  intl.formatMessage,
161
201
  dispatch,
162
- props,
163
- contextUrl,
202
+ props.block,
164
203
  requestId,
165
204
  ],
166
205
  );
@@ -170,6 +209,83 @@ const UnconnectedImageInput = (props) => {
170
209
  }, [restrictFileUpload]);
171
210
  const onDragLeave = React.useCallback(() => setDragging(false), []);
172
211
 
212
+ const validateManualLink = React.useCallback(
213
+ (url) => {
214
+ if (!url.startsWith('/')) {
215
+ const error = urlValidator({
216
+ value: url,
217
+ formatMessage: intl.formatMessage,
218
+ });
219
+ // if (error && url !== '') {
220
+ // this.setState({ errors: [error] });
221
+ // } else {
222
+ // this.setState({ errors: [] });
223
+ // }
224
+ return !Boolean(error);
225
+ } else {
226
+ return isInternalURL(url);
227
+ }
228
+ },
229
+ [intl.formatMessage],
230
+ );
231
+
232
+ const onSubmitURL = React.useCallback(
233
+ (url) => {
234
+ if (validateManualLink(url)) {
235
+ if (isInternalURL(url)) {
236
+ // convert it into an internal on if possible
237
+ props
238
+ .searchContent(
239
+ '/',
240
+ {
241
+ portal_type: config.settings.imageObjects,
242
+ 'path.query': flattenToAppURL(url),
243
+ 'path.depth': '0',
244
+ sort_on: 'getObjPositionInParent',
245
+ metadata_fields: '_all',
246
+ b_size: 1000,
247
+ },
248
+ `${props.block}-${props.mode}`,
249
+ )
250
+ .then((resp) => {
251
+ if (resp.items?.length > 0) {
252
+ onChange(props.id, resp.items[0], {});
253
+ } else {
254
+ toast.error(
255
+ <Toast
256
+ error
257
+ title={intl.formatMessage(messages.Error)}
258
+ content={intl.formatMessage(
259
+ messages.internalImageNotFoundErrorMessage,
260
+ )}
261
+ />,
262
+ );
263
+ }
264
+ });
265
+ } else {
266
+ if (isRelationChoice) {
267
+ toast.error(
268
+ <Toast
269
+ error
270
+ title={intl.formatMessage(messages.Error)}
271
+ content={intl.formatMessage(messages.imageUploadErrorMessage)}
272
+ />,
273
+ );
274
+ } else {
275
+ // if it's an external link, we save it as is
276
+ onChange(props.id, [
277
+ {
278
+ '@id': normalizeUrl(url),
279
+ title: removeProtocol(url),
280
+ },
281
+ ]);
282
+ }
283
+ }
284
+ }
285
+ },
286
+ [validateManualLink, props, intl, isRelationChoice, onChange],
287
+ );
288
+
173
289
  return imageValue ? (
174
290
  <div
175
291
  className="image-upload-widget-image"
@@ -178,15 +294,20 @@ const UnconnectedImageInput = (props) => {
178
294
  role="toolbar"
179
295
  >
180
296
  {selected && <ImageToolbar {...props} />}
181
- <img
182
- className={props.className}
183
- src={
184
- isInternalURL(imageValue)
185
- ? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
186
- : imageValue
187
- }
188
- alt=""
189
- />
297
+ {/* If it's relation choice (preview_image_link) */}
298
+ {isRelationChoice ? (
299
+ <Image item={value} width="fit-content" height="auto" loading="lazy" />
300
+ ) : (
301
+ <Image
302
+ className={props.className}
303
+ src={
304
+ isInternalURL(imageValue)
305
+ ? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
306
+ : imageValue
307
+ }
308
+ alt=""
309
+ />
310
+ )}
190
311
  </div>
191
312
  ) : (
192
313
  <div
@@ -227,7 +348,7 @@ const UnconnectedImageInput = (props) => {
227
348
  </Loader>
228
349
  </Dimmer>
229
350
  )}
230
- <img src={imageBlockSVG} alt="" className="placeholder" />
351
+ <Image src={imageBlockSVG} alt="" className="placeholder" />
231
352
  <p>{description || intl.formatMessage(messages.addImage)}</p>
232
353
  <div className="toolbar-wrapper">
233
354
  <div className="toolbar-inner" ref={linkEditor.anchorNode}>
@@ -242,15 +363,24 @@ const UnconnectedImageInput = (props) => {
242
363
  e.preventDefault();
243
364
  openObjectBrowser({
244
365
  mode: objectBrowserPickerType,
245
- onSelectItem: onSelectItem
246
- ? onSelectItem
247
- : (url, { title, image_field, image_scales }) => {
248
- onChange(props.id, flattenToAppURL(url), {
249
- title,
250
- image_field,
251
- image_scales,
252
- });
253
- },
366
+ onSelectItem: isRelationChoice
367
+ ? (url, item) => {
368
+ // we save the whole item if it's a relation choice
369
+ onChange(props.id, item);
370
+ }
371
+ : onSelectItem
372
+ ? onSelectItem
373
+ : // else we save the url along with the image field and scales
374
+ (
375
+ url,
376
+ { title, image_field, image_scales },
377
+ ) => {
378
+ onChange(props.id, flattenToAppURL(url), {
379
+ title,
380
+ image_field,
381
+ image_scales,
382
+ });
383
+ },
254
384
  currentPath: contextUrl,
255
385
  });
256
386
  }}
@@ -311,13 +441,14 @@ const UnconnectedImageInput = (props) => {
311
441
  intl.formatMessage(messages.linkAnImage)
312
442
  }
313
443
  objectBrowserPickerType={objectBrowserPickerType}
314
- onChange={(_, e) =>
315
- onChange(
316
- props.id,
317
- isInternalURL(e) ? flattenToAppURL(e) : e,
318
- {},
319
- )
320
- }
444
+ onChange={(_, e) => {
445
+ onSubmitURL(e);
446
+ // onChange(
447
+ // props.id,
448
+ // isInternalURL(e) ? flattenToAppURL(e) : e,
449
+ // {},
450
+ // );
451
+ }}
321
452
  id={id}
322
453
  />
323
454
  )}
@@ -331,6 +462,8 @@ const UnconnectedImageInput = (props) => {
331
462
  };
332
463
 
333
464
  export const ImageInput = compose(
465
+ // This HOC goes first because it injects block in case that it's not present (not a block, but a DX field)
466
+ withObjectBrowser,
334
467
  connect(
335
468
  (state, ownProps) => {
336
469
  const requestId = `image-upload-${ownProps.id}`;
@@ -339,9 +472,9 @@ export const ImageInput = compose(
339
472
  content: state.content.subrequests[ownProps.block || requestId]?.data,
340
473
  };
341
474
  },
342
- { createContent },
475
+ { createContent, searchContent },
343
476
  ),
344
- )(withObjectBrowser(UnconnectedImageInput));
477
+ )(UnconnectedImageInput);
345
478
 
346
479
  const ImageUploadWidget = (props) => {
347
480
  const { fieldSet, id, title } = props;
@@ -100,6 +100,7 @@ export const InternalUrlWidget = (props) => {
100
100
  {value?.length > 0 ? (
101
101
  <Button.Group>
102
102
  <Button
103
+ type="button"
103
104
  basic
104
105
  className="cancel"
105
106
  aria-label="clearUrlBrowser"
@@ -115,6 +116,7 @@ export const InternalUrlWidget = (props) => {
115
116
  ) : (
116
117
  <Button.Group>
117
118
  <Button
119
+ type="button"
118
120
  basic
119
121
  icon
120
122
  aria-label="openUrlBrowser"
@@ -399,6 +399,7 @@ export class ObjectBrowserWidgetComponent extends Component {
399
399
  {this.state.manualLinkInput && isEmpty(items) && (
400
400
  <Button.Group>
401
401
  <Button
402
+ type="button"
402
403
  basic
403
404
  className="cancel"
404
405
  onClick={(e) => {
@@ -409,6 +410,7 @@ export class ObjectBrowserWidgetComponent extends Component {
409
410
  <Icon name={clearSVG} size="18px" color="#e40166" />
410
411
  </Button>
411
412
  <Button
413
+ type="button"
412
414
  basic
413
415
  primary
414
416
  disabled={!this.state.validURL}
@@ -423,6 +425,7 @@ export class ObjectBrowserWidgetComponent extends Component {
423
425
  )}
424
426
  {!this.state.manualLinkInput && (
425
427
  <Button
428
+ type="button"
426
429
  aria-label={this.props.intl.formatMessage(
427
430
  messages.openObjectBrowser,
428
431
  )}
@@ -82,10 +82,11 @@ describe('RegistryImageWidget', () => {
82
82
  const dropzone = container.querySelector('.file-widget-dropzone');
83
83
  const preview = container.querySelector('.image-preview');
84
84
  const filename = container.querySelector('.field-file-name');
85
+ const img = container.querySelector('img[src*="logo"]');
85
86
 
86
- return dropzone && preview && filename;
87
+ return dropzone && preview && filename && img && img.complete;
87
88
  },
88
- { timeout: 1000 },
89
+ { timeout: 2000 },
89
90
  );
90
91
 
91
92
  expect(container).toMatchSnapshot();
@@ -105,6 +105,7 @@ class SelectAutoComplete extends Component {
105
105
  error: [],
106
106
  choices: [],
107
107
  value: null,
108
+ isMulti: true,
108
109
  };
109
110
 
110
111
  /**
@@ -127,10 +128,16 @@ class SelectAutoComplete extends Component {
127
128
  componentDidMount() {
128
129
  const { id, lang, value, choices } = this.props;
129
130
  if (value && value?.length > 0) {
130
- const tokensQuery = convertValueToVocabQuery(
131
- normalizeValue(choices, value, this.props.intl),
132
- );
133
-
131
+ let tokensQuery;
132
+ if (typeof value === 'string') {
133
+ tokensQuery = convertValueToVocabQuery(
134
+ normalizeValue(choices, [value], this.props.intl),
135
+ );
136
+ } else {
137
+ tokensQuery = convertValueToVocabQuery(
138
+ normalizeValue(choices, value, this.props.intl),
139
+ );
140
+ }
134
141
  this.props.getVocabularyTokenTitle({
135
142
  vocabNameOrURL: this.props.vocabBaseUrl,
136
143
  subrequest: `widget-${id}-${lang}`,
@@ -160,13 +167,22 @@ class SelectAutoComplete extends Component {
160
167
  * @returns {undefined}
161
168
  */
162
169
  handleChange(selectedOption) {
163
- this.props.onChange(
164
- this.props.id,
165
- selectedOption ? selectedOption.map((item) => item.value) : null,
166
- );
167
- this.setState((state) => ({
168
- termsPairsCache: [...state.termsPairsCache, ...selectedOption],
169
- }));
170
+ if (!Array.isArray(selectedOption)) {
171
+ this.props.onChange(this.props.id, selectedOption?.value || null);
172
+ if (selectedOption) {
173
+ this.setState((state) => ({
174
+ termsPairsCache: [...state.termsPairsCache, selectedOption],
175
+ }));
176
+ }
177
+ } else {
178
+ this.props.onChange(
179
+ this.props.id,
180
+ selectedOption ? selectedOption.map((item) => item.value) : null,
181
+ );
182
+ this.setState((state) => ({
183
+ termsPairsCache: [...state.termsPairsCache, ...selectedOption],
184
+ }));
185
+ }
170
186
  }
171
187
 
172
188
  timeoutRef = React.createRef();
@@ -251,7 +267,8 @@ class SelectAutoComplete extends Component {
251
267
  this.props.intl.formatMessage(messages.select)
252
268
  }
253
269
  onChange={this.handleChange}
254
- isMulti
270
+ isMulti={this.props.isMulti}
271
+ isClearable={!this.props.isMulti}
255
272
  />
256
273
  </FormFieldWrapper>
257
274
  );
@@ -128,6 +128,7 @@ class SelectWidget extends Component {
128
128
  isMulti: PropTypes.bool,
129
129
  placeholder: PropTypes.string,
130
130
  sort: PropTypes.bool,
131
+ isClearable: PropTypes.bool,
131
132
  };
132
133
 
133
134
  /**
@@ -156,6 +157,7 @@ class SelectWidget extends Component {
156
157
  noValueOption: true,
157
158
  customOptionStyling: null,
158
159
  sort: false,
160
+ isClearable: true,
159
161
  };
160
162
 
161
163
  /**
@@ -302,7 +304,7 @@ class SelectWidget extends Component {
302
304
  : undefined,
303
305
  );
304
306
  }}
305
- isClearable
307
+ isClearable={this.props.isClearable}
306
308
  />
307
309
  </FormFieldWrapper>
308
310
  );
@@ -105,6 +105,7 @@ export const UrlWidget = (props) => {
105
105
  {value?.length > 0 ? (
106
106
  <Button.Group>
107
107
  <Button
108
+ type="button"
108
109
  basic
109
110
  className="cancel"
110
111
  aria-label="clearUrlBrowser"
@@ -120,6 +121,7 @@ export const UrlWidget = (props) => {
120
121
  ) : (
121
122
  <Button.Group>
122
123
  <Button
124
+ type="button"
123
125
  basic
124
126
  icon
125
127
  aria-label="openUrlBrowser"
@@ -1,4 +1,3 @@
1
- import config from '@plone/volto/registry';
2
1
  import Helmet from '@plone/volto/helpers/Helmet/Helmet';
3
2
  import { flattenToAppURL, toPublicURL } from '@plone/volto/helpers/Url/Url';
4
3
 
@@ -6,8 +5,7 @@ const AlternateHrefLangs = (props) => {
6
5
  const { content } = props;
7
6
  return (
8
7
  <Helmet>
9
- {config.settings.isMultilingual &&
10
- content['@components']?.translations?.items &&
8
+ {content['@components']?.translations?.items &&
11
9
  [
12
10
  ...content['@components']?.translations?.items,
13
11
  { '@id': content['@id'], language: content.language.token },
@@ -13,7 +13,6 @@ const mockStore = configureStore();
13
13
  describe('AlternateHrefLangs', () => {
14
14
  beforeEach(() => {});
15
15
  it('non multilingual site, renders nothing', () => {
16
- config.settings.isMultilingual = false;
17
16
  const content = {
18
17
  '@id': '/',
19
18
  '@components': {},
@@ -38,7 +37,6 @@ describe('AlternateHrefLangs', () => {
38
37
 
39
38
  it('multilingual site, with some translations', () => {
40
39
  config.settings.publicURL = 'https://plone.org';
41
- config.settings.isMultilingual = true;
42
40
  config.settings.supportedLanguages = ['en', 'es', 'eu'];
43
41
 
44
42
  const content = {
@@ -85,7 +83,6 @@ describe('AlternateHrefLangs', () => {
85
83
 
86
84
  it('multilingual site, with all available translations', () => {
87
85
  config.settings.publicURL = 'https://plone.org';
88
- config.settings.isMultilingual = true;
89
86
  config.settings.supportedLanguages = ['en', 'es', 'eu'];
90
87
  const store = mockStore({
91
88
  intl: {
@@ -139,7 +136,6 @@ describe('AlternateHrefLangs', () => {
139
136
 
140
137
  it('multilingual site, with all available translations - with server URL', () => {
141
138
  config.settings.publicURL = 'https://plone.org';
142
- config.settings.isMultilingual = true;
143
139
  config.settings.supportedLanguages = ['en', 'es', 'eu'];
144
140
  const store = mockStore({
145
141
  intl: {
@@ -235,7 +235,9 @@ export const fetchContent = async ({ store, location }) => {
235
235
 
236
236
  const visitor = ([id, data]) => {
237
237
  const blockType = data['@type'];
238
- const { getAsyncData } = blocksConfig[blockType];
238
+ const block = blocksConfig[blockType];
239
+ if (!block) return;
240
+ const { getAsyncData } = block;
239
241
  if (getAsyncData) {
240
242
  const p = getAsyncData({
241
243
  store,
@@ -51,6 +51,7 @@ describe('App', () => {
51
51
  locale: 'en',
52
52
  messages: {},
53
53
  },
54
+ site: { data: {} },
54
55
  });
55
56
  const component = renderer.create(
56
57
  <Provider store={store}>
@@ -5,6 +5,7 @@
5
5
  import React from 'react';
6
6
  import PropTypes from 'prop-types';
7
7
  import { getInitials } from '@plone/volto/helpers/Utils/Utils';
8
+ import Image from '@plone/volto/components/theme/Image/Image';
8
9
 
9
10
  const defaultSize = 30;
10
11
  const defaultColor = 'Teal';
@@ -15,7 +16,7 @@ const Avatar = ({ src, title, text, size, color, className }) => {
15
16
  return (
16
17
  <div className={className} title={title}>
17
18
  {src ? (
18
- <img src={src} alt={title}></img>
19
+ <Image src={src} alt={title} />
19
20
  ) : (
20
21
  <svg width={size} height={size}>
21
22
  <circle cx={radius} cy={radius} r={radius} fill={color} />
@@ -82,9 +82,27 @@ export default {
82
82
  ],
83
83
  argTypes: {
84
84
  locale: {
85
+ options: [
86
+ 'ca',
87
+ 'de',
88
+ 'en',
89
+ 'es',
90
+ 'eu',
91
+ 'fi',
92
+ 'fr',
93
+ 'hi',
94
+ 'it',
95
+ 'ja',
96
+ 'nl',
97
+ 'pt',
98
+ 'pt-BR',
99
+ 'ro',
100
+ 'ru',
101
+ 'sv',
102
+ 'zh-CN',
103
+ ],
85
104
  control: {
86
- type: 'select',
87
- options: ['en', 'de', 'us'],
105
+ type: 'radio',
88
106
  },
89
107
  },
90
108
  },
@@ -1,6 +1,10 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import cx from 'classnames';
3
- import { flattenToAppURL, flattenScales } from '@plone/volto/helpers/Url/Url';
3
+ import {
4
+ flattenToAppURL,
5
+ flattenScales,
6
+ addSubpathPrefix,
7
+ } from '@plone/volto/helpers/Url/Url';
4
8
 
5
9
  /**
6
10
  * Image component
@@ -30,7 +34,7 @@ export default function Image({
30
34
  attrs.className = cx(className, { responsive }) || undefined;
31
35
 
32
36
  if (!item && src) {
33
- attrs.src = src;
37
+ attrs.src = addSubpathPrefix(src);
34
38
  } else {
35
39
  const isFromRealObject = !item.image_scales;
36
40
  const imageFieldWithDefault = imageField || item.image_field || 'image';
@@ -46,9 +50,11 @@ export default function Image({
46
50
 
47
51
  const isSvg = image['content-type'] === 'image/svg+xml';
48
52
  // In case `base_path` is present (`preview_image_link`) use it as base path
49
- const basePath = image.base_path || item['@id'];
53
+ const basePath = addSubpathPrefix(
54
+ flattenToAppURL(image.base_path || item['@id']),
55
+ );
56
+ attrs.src = `${basePath}/${image.download}`;
50
57
 
51
- attrs.src = `${flattenToAppURL(basePath)}/${image.download}`;
52
58
  attrs.width = image.width;
53
59
  attrs.height = image.height;
54
60
 
@@ -67,10 +73,7 @@ export default function Image({
67
73
  });
68
74
 
69
75
  attrs.srcSet = sortedScales
70
- .map(
71
- (scale) =>
72
- `${flattenToAppURL(basePath)}/${scale.download} ${scale.width}w`,
73
- )
76
+ .map((scale) => `${basePath}/${scale.download} ${scale.width}w`)
74
77
  .join(', ');
75
78
  }
76
79
  }