@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
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { render, act } from '@testing-library/react';
3
3
  import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
+ import { MemoryRouter } from 'react-router-dom';
5
6
  import jwt from 'jsonwebtoken';
6
7
 
7
8
  import UsersControlpanel from './UsersControlpanel';
@@ -41,10 +42,10 @@ describe('UsersControlpanel', () => {
41
42
  const { container } = await act(async () => {
42
43
  return render(
43
44
  <Provider store={store}>
44
- <>
45
- <UsersControlpanel location={{ pathname: '/blog' }} />
45
+ <MemoryRouter initialEntries={['/controlpanel/users']}>
46
+ <UsersControlpanel />
46
47
  <div id="toolbar"></div>
47
- </>
48
+ </MemoryRouter>
48
49
  </Provider>,
49
50
  );
50
51
  });
@@ -44,8 +44,6 @@ import { tryParseJSON } from '@plone/volto/helpers/FormValidation/FormValidation
44
44
  import saveSVG from '@plone/volto/icons/save.svg';
45
45
  import clearSVG from '@plone/volto/icons/clear.svg';
46
46
 
47
- import config from '@plone/volto/registry';
48
-
49
47
  const messages = defineMessages({
50
48
  edit: {
51
49
  id: 'Edit {title}',
@@ -460,7 +458,7 @@ class Edit extends Component {
460
458
  />
461
459
  </Button>
462
460
 
463
- {config.settings.isMultilingual && (
461
+ {this.props.isMultilingual && (
464
462
  <CompareLanguages
465
463
  content={this.props.content}
466
464
  visual={this.state.visual}
@@ -545,6 +543,7 @@ export default compose(
545
543
  updateRequest: state.content.update,
546
544
  pathname: props.location.pathname,
547
545
  returnUrl: qs.parse(props.location.search).return_url,
546
+ isMultilingual: state.site.data.features?.multilingual,
548
547
  }),
549
548
  {
550
549
  updateContent,
@@ -9,6 +9,7 @@ import { Field, BlocksForm } from '@plone/volto/components/manage/Form';
9
9
  import BlocksToolbar from '@plone/volto/components/manage/Form/BlocksToolbar';
10
10
  import UndoToolbar from '@plone/volto/components/manage/Form/UndoToolbar';
11
11
  import { difference } from '@plone/volto/helpers/Utils/Utils';
12
+ import withSaveAsDraft from '@plone/volto/helpers/Utils/withSaveAsDraft';
12
13
  import FormValidation from '@plone/volto/helpers/FormValidation/FormValidation';
13
14
  import {
14
15
  getBlocksFieldname,
@@ -265,6 +266,18 @@ class Form extends Component {
265
266
  this.onBlurField = this.onBlurField.bind(this);
266
267
  this.onClickInput = this.onClickInput.bind(this);
267
268
  this.onToggleMetadataFieldset = this.onToggleMetadataFieldset.bind(this);
269
+ this.updateFormDataWithSaved = this.updateFormDataWithSaved.bind(this);
270
+ }
271
+
272
+ /**
273
+ * Function sent as callback to saveAsDraft when user
274
+ * choses to load local data
275
+ * @param {Object} savedFormData
276
+ */
277
+ updateFormDataWithSaved(savedFormData) {
278
+ if (savedFormData) {
279
+ this.setState({ formData: savedFormData });
280
+ }
268
281
  }
269
282
 
270
283
  /**
@@ -278,6 +291,12 @@ class Form extends Component {
278
291
  let errors = {};
279
292
  let activeIndex = 0;
280
293
 
294
+ if (!prevProps.schema && this.props.schema) {
295
+ this.props.checkSavedDraft(
296
+ this.state.formData,
297
+ this.updateFormDataWithSaved,
298
+ );
299
+ }
281
300
  if (!this.props.isFormSelected && prevProps.isFormSelected) {
282
301
  this.props.setUIState({
283
302
  selected: null,
@@ -303,6 +322,10 @@ class Form extends Component {
303
322
  this.props.onChangeFormData(this.state.formData);
304
323
  }
305
324
  }
325
+ // on each formData update it will save the form to the localStorage
326
+ if (!isEqual(prevState?.formData, this.state.formData)) {
327
+ this.props.onSaveDraft(this.state.formData);
328
+ }
306
329
  if (
307
330
  this.props.global &&
308
331
  !isEqual(this.props.globalData, prevProps.globalData)
@@ -395,6 +418,13 @@ class Form extends Component {
395
418
  */
396
419
  componentDidMount() {
397
420
  this.setState({ isClient: true });
421
+ if (this.props.schema) {
422
+ this.props.checkSavedDraft(
423
+ this.state.formData,
424
+ this.updateFormDataWithSaved,
425
+ );
426
+ return;
427
+ }
398
428
  }
399
429
 
400
430
  /**
@@ -653,6 +683,7 @@ class Form extends Component {
653
683
  this.props.setFormData(this.props.formData);
654
684
  }
655
685
  }
686
+ this.props.onCancelDraft();
656
687
  }
657
688
  }
658
689
 
@@ -1136,4 +1167,5 @@ export default compose(
1136
1167
  null,
1137
1168
  { forwardRef: true },
1138
1169
  ),
1170
+ withSaveAsDraft({ forwardRef: true }),
1139
1171
  )(FormIntl);
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
3
  import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
-
5
+ import { MemoryRouter } from 'react-router-dom';
6
6
  import Form from './Form';
7
7
 
8
8
  const mockStore = configureStore();
@@ -30,26 +30,30 @@ describe('Form', () => {
30
30
  },
31
31
  },
32
32
  });
33
+ const route = '/some-route';
33
34
  const component = renderer.create(
34
35
  <Provider store={store}>
35
- <Form
36
- schema={{
37
- fieldsets: [
38
- {
39
- id: 'default',
40
- title: 'Default',
41
- fields: ['title'],
36
+ <MemoryRouter initialEntries={[route]}>
37
+ <Form
38
+ schema={{
39
+ fieldsets: [
40
+ {
41
+ id: 'default',
42
+ title: 'Default',
43
+ fields: ['title'],
44
+ },
45
+ ],
46
+ properties: {
47
+ title: {},
42
48
  },
43
- ],
44
- properties: {
45
- title: {},
46
- },
47
- required: [],
48
- }}
49
- requestError={errorMessage}
50
- onSubmit={() => {}}
51
- onCancel={() => {}}
52
- />
49
+ required: [],
50
+ }}
51
+ requestError={errorMessage}
52
+ onSubmit={() => {}}
53
+ onCancel={() => {}}
54
+ />
55
+ </MemoryRouter>
56
+ ,
53
57
  </Provider>,
54
58
  );
55
59
  const json = component.toJSON();
@@ -158,7 +158,7 @@ const InlineForm = (props) => {
158
158
  onChangeField(id, value, itemInfo);
159
159
  }}
160
160
  key={field}
161
- error={errors?.[block]?.[field] || {}}
161
+ error={errors?.[block]?.[field] || []}
162
162
  block={block}
163
163
  />
164
164
  ))}
@@ -199,7 +199,7 @@ const InlineForm = (props) => {
199
199
  onChangeField(id, value);
200
200
  }}
201
201
  key={field}
202
- error={errors?.[block]?.[field] || {}}
202
+ error={errors?.[block]?.[field] || []}
203
203
  block={block}
204
204
  />
205
205
  ))}
@@ -1,6 +1,5 @@
1
1
  import React, { useState } from 'react';
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
- import config from '@plone/volto/registry';
4
3
  import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
5
4
  import { useDetectClickOutside } from '@plone/volto/helpers/Utils/useDetectClickOutside';
6
5
 
@@ -106,16 +105,14 @@ const CompareLanguages = React.forwardRef((props, ref) => {
106
105
 
107
106
  const intl = useIntl();
108
107
  const [viewMenu, setViewMenu] = useState(false);
109
- const translations = config.settings.isMultilingual
110
- ? content?.['@components']?.translations?.items || []
111
- : [];
108
+ const translations = content?.['@components']?.translations?.items || [];
112
109
 
113
110
  const translationsObject = {};
114
111
  translations.forEach((t) => {
115
112
  translationsObject[t.language] = t['@id'];
116
113
  });
117
114
 
118
- if (config.settings.isMultilingual && translations.length > 0) {
115
+ if (translations.length > 0) {
119
116
  return (
120
117
  <div className="toolbar-compare-translations-wrapper">
121
118
  <div className="toolbar-button-spacer" />
@@ -12,9 +12,12 @@ const CreateTranslation = (props) => {
12
12
  const dispatch = useDispatch();
13
13
  const { language, translationOf } = props.location.state;
14
14
  const [translationLocation, setTranslationLocation] = React.useState(null);
15
- const [translationObject, setTranslationObject] = React.useState(null);
16
15
  const languageFrom = useSelector((state) => state.intl.locale);
17
16
 
17
+ const translationObject = useSelector(
18
+ (state) => state.content.subrequests['translationObject'],
19
+ );
20
+
18
21
  React.useEffect(() => {
19
22
  // Only on mount, we dispatch the locator query
20
23
  dispatch(getTranslationLocator(translationOf, language)).then((resp) => {
@@ -22,11 +25,7 @@ const CreateTranslation = (props) => {
22
25
  });
23
26
 
24
27
  //and we load the translationObject
25
- dispatch(getContent(translationOf, null, 'translationObject')).then(
26
- (resp) => {
27
- setTranslationObject(resp);
28
- },
29
- );
28
+ dispatch(getContent(translationOf, null, 'translationObject'));
30
29
 
31
30
  // On unmount we dispatch the language change
32
31
  return () => {
@@ -46,7 +45,8 @@ const CreateTranslation = (props) => {
46
45
 
47
46
  return (
48
47
  translationLocation &&
49
- translationObject && (
48
+ translationObject.data &&
49
+ translationObject.loaded > 0 && (
50
50
  <Redirect
51
51
  to={{
52
52
  pathname: `${flattenToAppURL(translationLocation)}/add`,
@@ -54,7 +54,7 @@ const CreateTranslation = (props) => {
54
54
  state: {
55
55
  translationOf: props.location.state.translationOf,
56
56
  language: props.location.state.language,
57
- translationObject: translationObject,
57
+ translationObject: translationObject.data,
58
58
  languageFrom,
59
59
  },
60
60
  }}
@@ -8,7 +8,6 @@ import { Link, useLocation } from 'react-router-dom';
8
8
  import Icon from '@plone/volto/components/theme/Icon/Icon';
9
9
  import Toast from '@plone/volto/components/manage/Toast/Toast';
10
10
  import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
11
- import config from '@plone/volto/registry';
12
11
 
13
12
  import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
14
13
  import {
@@ -66,6 +65,9 @@ const ManageTranslations = (props) => {
66
65
  const pathname = useLocation().pathname;
67
66
  const content = useSelector((state) => state.content.data);
68
67
  const dispatch = useDispatch();
68
+ const availableLanguages = useSelector(
69
+ (state) => state.site.data['plone.available_languages'],
70
+ );
69
71
 
70
72
  const [isClient, setIsClient] = useState(false);
71
73
 
@@ -189,7 +191,7 @@ const ManageTranslations = (props) => {
189
191
  </Table.Row>
190
192
  </Table.Header>
191
193
  <Table.Body>
192
- {config.settings.supportedLanguages.map((lang) => (
194
+ {availableLanguages.map((lang) => (
193
195
  <Table.Row key={lang}>
194
196
  <Table.Cell collapsing>
195
197
  {lang === content.language.token ? (
@@ -8,7 +8,6 @@ import config from '@plone/volto/registry';
8
8
  import ManageTranslations from './ManageTranslations';
9
9
 
10
10
  beforeAll(() => {
11
- config.settings.isMultilingual = true;
12
11
  config.settings.supportedLanguages = ['de', 'es'];
13
12
  });
14
13
  vi.mock('../Toolbar/Toolbar', () => ({
@@ -34,6 +33,11 @@ describe('ManageTranslations', () => {
34
33
  language: 'en',
35
34
  },
36
35
  },
36
+ site: {
37
+ data: {
38
+ 'plone.available_languages': ['de', 'es'],
39
+ },
40
+ },
37
41
  });
38
42
  const { container } = render(
39
43
  <Provider store={store}>
@@ -63,7 +63,7 @@ const TranslationObject = ({
63
63
  {
64
64
  ...window.__data,
65
65
  intl: {
66
- defaultLocale: config.settings.defaultLanguage,
66
+ defaultLocale: translationObject.language.token,
67
67
  locale: translationObject.language.token,
68
68
  messages: locales[translationObject.language.token],
69
69
  },
@@ -3,6 +3,7 @@ import { Button, Segment, Popup } from 'semantic-ui-react';
3
3
  import { useIntl, defineMessages } from 'react-intl';
4
4
  import cx from 'classnames';
5
5
  import Icon from '@plone/volto/components/theme/Icon/Icon';
6
+ import Image from '@plone/volto/components/theme/Image/Image';
6
7
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
7
8
  import { getContentIcon } from '@plone/volto/helpers/Content/Content';
8
9
  import config from '@plone/volto/registry';
@@ -69,7 +70,7 @@ const ObjectBrowserNav = ({
69
70
  }
70
71
  >
71
72
  {item['@type'] === 'Image' ? (
72
- <img
73
+ <Image
73
74
  src={`${item['@id']}/@@images/image/preview`}
74
75
  alt={item.title}
75
76
  style={{
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { cleanup, render, screen } from '@testing-library/react';
3
+
4
+ import SidebarPortal from './SidebarPortal';
5
+
6
+ describe('SidebarPortal', () => {
7
+ let portalRoot: HTMLDivElement | null;
8
+
9
+ beforeEach(() => {
10
+ portalRoot = document.createElement('div');
11
+ portalRoot.setAttribute('id', 'sidebar-properties');
12
+ document.body.appendChild(portalRoot);
13
+ });
14
+
15
+ afterEach(() => {
16
+ cleanup();
17
+ portalRoot?.remove();
18
+ portalRoot = null;
19
+ });
20
+
21
+ test('renders the sidebar portal when the block is selected', async () => {
22
+ render(
23
+ <SidebarPortal selected={true}>
24
+ <p>Tested!</p>
25
+ </SidebarPortal>,
26
+ );
27
+
28
+ expect(await screen.findByText('Tested!')).toBeInTheDocument();
29
+ });
30
+
31
+ test('does not render the sidebar portal when the block is not selected', () => {
32
+ render(
33
+ <SidebarPortal selected={false}>
34
+ <p>Tested, but you should not see this!</p>
35
+ </SidebarPortal>,
36
+ );
37
+
38
+ expect(
39
+ screen.queryByText('Tested, but you should not see this!'),
40
+ ).not.toBeInTheDocument();
41
+ });
42
+ });
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ type SidebarPortalProps = {
5
+ children?: React.ReactNode;
6
+ selected: boolean;
7
+ tab?: string;
8
+ };
9
+
10
+ const SidebarPortal = ({
11
+ children,
12
+ selected,
13
+ tab = 'sidebar-properties',
14
+ }: SidebarPortalProps) => {
15
+ const [isClient, setIsClient] = React.useState(false);
16
+
17
+ React.useEffect(() => setIsClient(true), []);
18
+
19
+ if (!isClient || !selected) {
20
+ return null;
21
+ }
22
+
23
+ const target = document.getElementById(tab);
24
+
25
+ if (!target) {
26
+ return null;
27
+ }
28
+
29
+ return createPortal(
30
+ <div role="form" style={{ height: '100%' }}>
31
+ <div
32
+ style={{ height: '100%' }}
33
+ role="presentation"
34
+ onClick={(e) => {
35
+ e.stopPropagation();
36
+ }}
37
+ onKeyDown={(e) => {
38
+ e.stopPropagation();
39
+ }}
40
+ >
41
+ {children}
42
+ </div>
43
+ </div>,
44
+ target,
45
+ );
46
+ };
47
+
48
+ export default SidebarPortal;
@@ -1,6 +1,7 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import { useIntl } from 'react-intl';
3
3
  import { Button, Grid, Message } from 'semantic-ui-react';
4
+ import Image from '@plone/volto/components/theme/Image/Image';
4
5
 
5
6
  const TemplateChooser = ({ templates, onSelectTemplate }) => {
6
7
  const intl = useIntl();
@@ -15,7 +16,7 @@ const TemplateChooser = ({ templates, onSelectTemplate }) => {
15
16
  className="template-chooser-item"
16
17
  onClick={() => onSelectTemplate(index)}
17
18
  >
18
- <img src={template.image} alt="" />
19
+ <Image src={template.image} alt="" />
19
20
  <div className="template-chooser-title">
20
21
  {intl.formatMessage({
21
22
  id: template.id,
@@ -114,6 +114,9 @@ const More = (props) => {
114
114
 
115
115
  const content = useSelector((state) => state.content?.data, shallowEqual);
116
116
  const workingCopy = useSelector((state) => state.workingCopy, shallowEqual);
117
+ const isMultilingual = useSelector(
118
+ (state) => state.site.data.features?.multilingual,
119
+ );
117
120
 
118
121
  const actions = useSelector((state) => state.actions.actions, shallowEqual);
119
122
 
@@ -435,7 +438,7 @@ const More = (props) => {
435
438
  </li>
436
439
  </Plug>
437
440
  )}
438
- {editAction && config.settings.isMultilingual && (
441
+ {editAction && isMultilingual && (
439
442
  <Plug pluggable="toolbar-more-manage-content" id="multilingual">
440
443
  <li>
441
444
  <Link to={`${path}/manage-translations`}>
@@ -135,6 +135,9 @@ const store = mockStore({
135
135
  is_folderish: true,
136
136
  },
137
137
  },
138
+ site: {
139
+ data: {},
140
+ },
138
141
  workflow: { history: [], transition: { loaded: true } },
139
142
  });
140
143
 
@@ -7,6 +7,7 @@ import cx from 'classnames';
7
7
  import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
8
8
 
9
9
  import Icon from '@plone/volto/components/theme/Icon/Icon';
10
+ import Image from '@plone/volto/components/theme/Image/Image';
10
11
  import { getUser } from '@plone/volto/actions/users/users';
11
12
  import { Pluggable } from '@plone/volto/components/manage/Pluggable';
12
13
  import { expandToBackendURL, getBaseUrl } from '@plone/volto/helpers/Url/Url';
@@ -96,7 +97,7 @@ const PersonalTools = (props) => {
96
97
  </header>
97
98
  <div className={cx('avatar', { default: !user.portrait })}>
98
99
  {user.portrait ? (
99
- <img
100
+ <Image
100
101
  src={expandToBackendURL(user.portrait)}
101
102
  alt={intl.formatMessage(messages.userAvatar)}
102
103
  />
@@ -537,10 +537,9 @@ class Toolbar extends Component {
537
537
  </Link>
538
538
  )}
539
539
  {this.props.content &&
540
- ((this.props.content.is_folderish &&
541
- this.props.types.length > 0) ||
542
- (config.settings.isMultilingual &&
543
- this.props.content['@components']?.translations)) && (
540
+ this.props.content.is_folderish &&
541
+ (this.props.types.length > 0 ||
542
+ this.props.content['@components']?.translations) && (
544
543
  <button
545
544
  className="add"
546
545
  aria-label={this.props.intl.formatMessage(
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { connect } from 'react-redux';
4
4
  import { Link } from 'react-router-dom';
5
+ import { useSelector } from 'react-redux';
5
6
  import filter from 'lodash/filter';
6
7
  import find from 'lodash/find';
7
8
  import isEmpty from 'lodash/isEmpty';
@@ -10,12 +11,12 @@ import { FormattedMessage } from 'react-intl';
10
11
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
11
12
  import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
12
13
  import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
13
- import config from '@plone/volto/registry';
14
14
 
15
15
  const Types = ({ types, pathname, content, currentLanguage }) => {
16
- const { settings } = config;
17
- return types.length > 0 ||
18
- (settings.isMultilingual && content['@components'].translations) ? (
16
+ const availableLanguages = useSelector(
17
+ (state) => state.site?.data?.['plone.available_languages'] || [],
18
+ );
19
+ return types.length > 0 || content?.['@components']?.translations ? (
19
20
  <div className="menu-more pastanaga-menu">
20
21
  {types.length > 0 && (
21
22
  <>
@@ -53,11 +54,10 @@ const Types = ({ types, pathname, content, currentLanguage }) => {
53
54
  </div>
54
55
  </>
55
56
  )}
56
- {settings.isMultilingual &&
57
- content['@components'].translations &&
57
+ {content['@components'].translations &&
58
58
  (() => {
59
59
  const translationsLeft = filter(
60
- settings.supportedLanguages,
60
+ availableLanguages,
61
61
  (lang) =>
62
62
  !Boolean(
63
63
  content['@components'].translations &&
@@ -148,6 +148,7 @@ const UniversalLink = React.memo(
148
148
  target={openLinkInNewTab ?? false ? '_blank' : undefined}
149
149
  title={title}
150
150
  className={className}
151
+ onClick={onClick}
151
152
  smooth={smooth ?? config.settings.hashLinkSmoothScroll}
152
153
  // @ts-ignore
153
154
  ref={ref}
@@ -27,6 +27,10 @@ const messages = defineMessages({
27
27
  id: 'Time',
28
28
  defaultMessage: 'Time',
29
29
  },
30
+ clearDateTime: {
31
+ id: 'Clear date/time',
32
+ defaultMessage: 'Clear date and time',
33
+ },
30
34
  });
31
35
 
32
36
  const PrevIcon = () => (
@@ -207,6 +211,7 @@ const DatetimeWidgetComponent = (props) => {
207
211
  disabled={isDisabled || !datetime}
208
212
  onClick={onResetDates}
209
213
  className="item ui noborder button"
214
+ aria-label={intl.formatMessage(messages.clearDateTime)}
210
215
  >
211
216
  <Icon name={clearSVG} size="24px" className="close" />
212
217
  </button>
@@ -15,7 +15,6 @@ import UniversalLink from '@plone/volto/components/manage/UniversalLink/Universa
15
15
  import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
16
16
  import Image from '@plone/volto/components/theme/Image/Image';
17
17
  import loadable from '@loadable/component';
18
- import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
19
18
  import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
20
19
  import { defineMessages, useIntl } from 'react-intl';
21
20
  import { toast } from 'react-toastify';
@@ -90,18 +89,25 @@ const FileWidget = (props) => {
90
89
  const [fileType, setFileType] = React.useState(false);
91
90
  const intl = useIntl();
92
91
 
92
+ const imgAttrs = React.useMemo(() => {
93
+ const data = {};
94
+ if (value?.download) {
95
+ data.item = {
96
+ '@id': value.download.substring(0, value.download.indexOf('/@@images')),
97
+ image: value,
98
+ };
99
+ } else if (value?.data) {
100
+ data.src = `data:${value['content-type']};${value.encoding},${value.data}`;
101
+ }
102
+ return data;
103
+ }, [value]);
104
+
93
105
  React.useEffect(() => {
94
106
  if (value && imageMimetypes.includes(value['content-type'])) {
95
107
  setFileType(true);
96
108
  }
97
109
  }, [value]);
98
110
 
99
- const imgsrc = value?.download
100
- ? `${flattenToAppURL(value?.download)}?id=${Date.now()}`
101
- : null || value?.data
102
- ? `data:${value['content-type']};${value.encoding},${value.data}`
103
- : null;
104
-
105
111
  /**
106
112
  * Drop handler
107
113
  * @method onDrop
@@ -175,7 +181,7 @@ const FileWidget = (props) => {
175
181
  <Image
176
182
  className="image-preview small ui image"
177
183
  id={`field-${id}-image`}
178
- src={imgsrc}
184
+ {...imgAttrs}
179
185
  />
180
186
  ) : (
181
187
  <div className="dropzone-placeholder">