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

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 +234 -0
  4. package/README.md +18 -14
  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
@@ -3,15 +3,9 @@ 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
- import config from '@plone/volto/registry';
7
6
 
8
7
  import LanguageSelector from './LanguageSelector';
9
8
 
10
- beforeAll(() => {
11
- config.settings.isMultilingual = true;
12
- config.settings.supportedLanguages = ['de', 'es'];
13
- });
14
-
15
9
  const mockStore = configureStore();
16
10
 
17
11
  describe('LanguageSelector', () => {
@@ -30,6 +24,12 @@ describe('LanguageSelector', () => {
30
24
  },
31
25
  },
32
26
  },
27
+ site: {
28
+ data: {
29
+ features: { multilingual: true },
30
+ 'plone.available_languages': ['es'],
31
+ },
32
+ },
33
33
  });
34
34
  const component = renderer.create(
35
35
  <Provider store={store}>
@@ -0,0 +1,89 @@
1
+ import { Link } from 'react-router-dom';
2
+
3
+ import { useSelector } from 'react-redux';
4
+ import cx from 'classnames';
5
+
6
+ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
7
+ import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
8
+ import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
9
+ import { toReactIntlLang } from '@plone/volto/helpers/Utils/Utils';
10
+
11
+ import { defineMessages, useIntl, type IntlShape } from 'react-intl';
12
+ import type {
13
+ Content,
14
+ GetSiteResponse,
15
+ GetTranslationResponse,
16
+ } from '@plone/types';
17
+
18
+ const messages = defineMessages({
19
+ switchLanguageTo: {
20
+ id: 'Switch to',
21
+ defaultMessage: 'Switch to',
22
+ },
23
+ });
24
+
25
+ type FormState = {
26
+ content: {
27
+ data: Content;
28
+ };
29
+ intl: IntlShape;
30
+ site: {
31
+ data: GetSiteResponse;
32
+ };
33
+ };
34
+
35
+ const LanguageSelector = ({
36
+ onClickAction = () => {},
37
+ }: {
38
+ onClickAction?: () => void;
39
+ }) => {
40
+ const intl = useIntl();
41
+ const currentLang = useSelector<FormState, IntlShape['locale']>(
42
+ (state) => state.intl.locale,
43
+ );
44
+ const translations = useSelector<
45
+ FormState,
46
+ GetTranslationResponse['items'] | undefined
47
+ >((state) => state.content.data?.['@components']?.translations?.items);
48
+ const isMultilingual = useSelector<
49
+ FormState,
50
+ GetSiteResponse['features']['multilingual']
51
+ >((state) => state.site.data.features?.multilingual);
52
+ const availableLanguages = useSelector<
53
+ FormState,
54
+ GetSiteResponse['plone.available_languages']
55
+ >((state) => state.site.data?.['plone.available_languages']);
56
+
57
+ return isMultilingual ? (
58
+ <div className="language-selector">
59
+ {availableLanguages?.map((lang) => {
60
+ const langKey = lang as keyof typeof langmap;
61
+ const translation = translations?.find(
62
+ (t: { language: string }) => t.language === lang,
63
+ );
64
+ return (
65
+ <Link
66
+ aria-label={`${intl.formatMessage(
67
+ messages.switchLanguageTo,
68
+ )} ${langmap[langKey].nativeName.toLowerCase()}`}
69
+ className={cx({ selected: toReactIntlLang(lang) === currentLang })}
70
+ to={translation ? flattenToAppURL(translation['@id']) : `/${lang}`}
71
+ title={langmap[langKey].nativeName}
72
+ onClick={() => {
73
+ onClickAction();
74
+ }}
75
+ key={`language-selector-${lang}`}
76
+ >
77
+ {langmap[langKey].nativeName}
78
+ </Link>
79
+ );
80
+ })}
81
+ </div>
82
+ ) : (
83
+ <Helmet>
84
+ <html lang={toReactIntlLang(currentLang)} />
85
+ </Helmet>
86
+ );
87
+ };
88
+
89
+ export default LanguageSelector;
@@ -3,14 +3,9 @@ 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
- import config from '@plone/volto/registry';
7
6
 
8
7
  import Logo from './Logo';
9
8
 
10
- beforeAll(() => {
11
- config.settings.isMultilingual = true;
12
- });
13
-
14
9
  const mockStore = configureStore();
15
10
 
16
11
  describe('Multilingual Logo', () => {
@@ -1,28 +1,24 @@
1
1
  import React from 'react';
2
2
  import { Redirect } from 'react-router-dom';
3
- import { useDispatch } from 'react-redux';
3
+ import { useDispatch, useSelector } from 'react-redux';
4
4
  import { useCookies } from 'react-cookie';
5
- import config from '@plone/volto/registry';
6
5
  import { changeLanguage } from '@plone/volto/actions/language/language';
7
6
  import { toGettextLang } from '@plone/volto/helpers/Utils/Utils';
8
7
 
9
8
  const MultilingualRedirector = (props) => {
10
- const { settings } = config;
11
9
  const { pathname, children } = props;
12
10
  const [cookies] = useCookies();
13
- const currentLanguage = cookies['I18N_LANGUAGE'] || settings.defaultLanguage;
14
- const redirectToLanguage = settings.supportedLanguages.includes(
15
- currentLanguage,
16
- )
17
- ? currentLanguage
18
- : settings.defaultLanguage;
11
+ const site = useSelector((state) => state.site.data);
12
+ const isMultilingual = site.features?.multilingual;
13
+ const redirectToLanguage =
14
+ cookies['I18N_LANGUAGE'] || site['plone.default_language'];
19
15
  const dispatch = useDispatch();
20
16
 
21
17
  React.useEffect(() => {
22
18
  // ToDo: Add means to support language negotiation (with config)
23
19
  // const detectedLang = (navigator.language || navigator.userLanguage).substring(0, 2);
24
20
  let mounted = true;
25
- if (settings.isMultilingual && pathname === '/') {
21
+ if (isMultilingual && pathname === '/') {
26
22
  const langFileName = toGettextLang(redirectToLanguage);
27
23
  import(
28
24
  /* @vite-ignore */ '@root/../locales/' + langFileName + '.json'
@@ -35,9 +31,9 @@ const MultilingualRedirector = (props) => {
35
31
  return () => {
36
32
  mounted = false;
37
33
  };
38
- }, [pathname, dispatch, redirectToLanguage, settings.isMultilingual]);
34
+ }, [pathname, dispatch, redirectToLanguage, isMultilingual]);
39
35
 
40
- return pathname === '/' && settings.isMultilingual ? (
36
+ return pathname === '/' && isMultilingual ? (
41
37
  <Redirect to={`/${redirectToLanguage}`} />
42
38
  ) : (
43
39
  <>{children}</>
@@ -3,19 +3,17 @@ 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
- import config from '@plone/volto/registry';
7
6
 
8
7
  import MultilingualRedirector from './MultilingualRedirector';
9
8
 
10
- beforeAll(() => {
11
- config.settings.isMultilingual = true;
12
- });
13
-
14
9
  const mockStore = configureStore();
15
10
 
16
11
  describe('MultilingualRedirector', () => {
17
12
  it('renders a MultilingualRedirector component', () => {
18
13
  const store = mockStore({
14
+ site: {
15
+ data: {},
16
+ },
19
17
  intl: {
20
18
  locale: 'en',
21
19
  messages: {},
@@ -15,11 +15,7 @@ const NavItem = ({ item, lang }) => {
15
15
  key={item.url}
16
16
  className="item"
17
17
  activeClassName="active"
18
- exact={
19
- settings.isMultilingual
20
- ? item.url === `/${toBackendLang(lang)}`
21
- : item.url === ''
22
- }
18
+ exact={item.url === '' || item.url === `/${toBackendLang(lang)}`}
23
19
  >
24
20
  {item.title}
25
21
  </NavLink>
@@ -3,14 +3,9 @@ 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
- import config from '@plone/volto/registry';
7
6
 
8
7
  import Navigation from './Navigation';
9
8
 
10
- beforeAll(() => {
11
- config.settings.isMultilingual = true;
12
- });
13
-
14
9
  const mockStore = configureStore();
15
10
 
16
11
  describe('Navigation Multilingual', () => {
@@ -19,15 +19,18 @@ import config from '@plone/volto/registry';
19
19
  const NotFound = () => {
20
20
  const dispatch = useDispatch();
21
21
  const lang = useSelector((state) => state.intl.locale);
22
+ const isMultilingual = useSelector(
23
+ (state) => state.site.data.features?.multilingual,
24
+ );
22
25
 
23
26
  useEffect(() => {
24
27
  dispatch(
25
28
  getNavigation(
26
- config.settings.isMultilingual ? `/${toBackendLang(lang)}` : '/',
29
+ isMultilingual ? `/${toBackendLang(lang)}` : '/',
27
30
  config.settings.navDepth,
28
31
  ),
29
32
  );
30
- }, [dispatch, lang]);
33
+ }, [dispatch, lang, isMultilingual]);
31
34
 
32
35
  return (
33
36
  <Container className="view-wrapper">
@@ -13,6 +13,9 @@ const mockStore = configureStore();
13
13
  describe('NotFound', () => {
14
14
  it('renders a not found component', () => {
15
15
  const store = mockStore({
16
+ site: {
17
+ data: {},
18
+ },
16
19
  intl: {
17
20
  locale: 'en',
18
21
  messages: {},
@@ -25,7 +25,7 @@ function PreviewImage({ item, alt, image_field, showDefault = true, ...rest }) {
25
25
  return image;
26
26
  } else {
27
27
  return (
28
- <img
28
+ <Image
29
29
  src={
30
30
  config.getComponent({
31
31
  name: 'DefaultImage',
@@ -29,7 +29,7 @@ const RequestTimeout = () => (
29
29
  <h1 style={{ textAlign: 'center', lineHeight: '40px' }}>
30
30
  <FormattedMessage
31
31
  id="No connection to the server"
32
- defaultMessage="There is no connection to the server, due to a timeout o no network connection."
32
+ defaultMessage="There is no connection to the server, due to a timeout or no network connection."
33
33
  />
34
34
  <br />
35
35
  <a href={config.settings.apiPath}>{config.settings.apiPath}</a>
@@ -36,14 +36,14 @@ function Sitemap(props) {
36
36
  location: { pathname },
37
37
  lang,
38
38
  getNavigation,
39
+ isMultilingual,
39
40
  } = props;
40
41
 
41
42
  useEffect(() => {
42
- const { settings } = config;
43
- const language = settings.isMultilingual ? `${toBackendLang(lang)}` : null;
43
+ const language = isMultilingual ? `${toBackendLang(lang)}` : null;
44
44
  const path = getSitemapPath(pathname, language);
45
45
  getNavigation(path, 4);
46
- }, [pathname, lang, getNavigation]);
46
+ }, [pathname, lang, getNavigation, isMultilingual]);
47
47
 
48
48
  const renderItems = (items) => {
49
49
  return (
@@ -100,6 +100,7 @@ export default compose(
100
100
  (state) => ({
101
101
  items: state.navigation.items,
102
102
  lang: state.intl.locale,
103
+ isMultilingual: state.site.data.features?.multilingual,
103
104
  }),
104
105
  { getNavigation },
105
106
  ),
@@ -108,10 +109,10 @@ export default compose(
108
109
  key: 'navigation',
109
110
  promise: ({ location, store: { dispatch, getState } }) => {
110
111
  if (!__SERVER__) return;
111
- const { settings } = config;
112
+ const state = getState();
112
113
  const path = getSitemapPath(
113
114
  location.pathname,
114
- settings.isMultilingual
115
+ state.site.data.features?.multilingual
115
116
  ? toBackendLang(getState().intl.locale)
116
117
  : null,
117
118
  );
@@ -58,7 +58,6 @@ describe('Sitemap', () => {
58
58
 
59
59
  describe('Sitemap in a multilingual site', () => {
60
60
  beforeEach(() => {
61
- config.settings.isMultilingual = true;
62
61
  config.settings.supportedLanguages = ['en', 'es'];
63
62
  });
64
63
  it('renders a sitemap component', () => {
@@ -7,6 +7,7 @@ import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { Container as SemanticContainer } from 'semantic-ui-react';
9
9
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
10
+ import { FormattedMessage } from 'react-intl';
10
11
  import config from '@plone/volto/registry';
11
12
 
12
13
  /**
@@ -30,7 +31,14 @@ const FileView = ({ content }) => {
30
31
  )}
31
32
  {content.file?.download && (
32
33
  <a href={flattenToAppURL(content.file.download)}>
33
- {content.file.filename}
34
+ {content.file.filename ? (
35
+ content.file.filename
36
+ ) : (
37
+ <FormattedMessage
38
+ id="Download file"
39
+ defaultMessage="Download file"
40
+ />
41
+ )}
34
42
  </a>
35
43
  )}
36
44
  </Container>
@@ -246,7 +246,7 @@ class View extends Component {
246
246
  />
247
247
  <SlotRenderer name="aboveContent" content={this.props.content} />
248
248
  <RenderedView
249
- key={this.props.content['@id']}
249
+ key={flattenToAppURL(this.props.content['@id'])}
250
250
  content={this.props.content}
251
251
  location={this.props.location}
252
252
  token={this.props.token}
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import cx from 'classnames';
3
3
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
4
+ import Image from '@plone/volto/components/theme/Image/Image';
4
5
 
5
6
  const niceBytes = (bytes) => {
6
7
  bytes = Number(bytes);
@@ -18,7 +19,7 @@ const niceBytes = (bytes) => {
18
19
  const ImageWidget = ({ value, className }) =>
19
20
  value ? (
20
21
  <span className={cx(className, 'image', 'widget')}>
21
- <img
22
+ <Image
22
23
  src={
23
24
  value.data
24
25
  ? `data:${value['content-type']};base64,${value.data}`
@@ -77,6 +77,7 @@ export const unwantedControlPanelsFields = {
77
77
  'exposeDCMetaTags',
78
78
  'enable_sitemap',
79
79
  'webstats_js',
80
+ 'webstats_head_js',
80
81
  ],
81
82
  editing: ['available_editors', 'default_editor', 'ext_editor'],
82
83
  imaging: [
@@ -25,13 +25,11 @@ import { installDefaultBlocks } from './Blocks';
25
25
  import { getSiteAsyncPropExtender } from '@plone/volto/helpers/Site';
26
26
  import { registerValidators } from './validation';
27
27
 
28
+ import languages from '@plone/volto/constants/Languages.cjs';
29
+
28
30
  const host = process.env.HOST || 'localhost';
29
31
  const port = process.env.PORT || '3000';
30
32
 
31
- const apiPath =
32
- process.env.RAZZLE_API_PATH ||
33
- (__DEVELOPMENT__ ? `http://${host}:${port}` : '');
34
-
35
33
  const getServerURL = (url) => {
36
34
  if (!url) return;
37
35
  const apiPathURL = parseUrl(url);
@@ -44,12 +42,14 @@ const getServerURL = (url) => {
44
42
  // if RAZZLE_PUBLIC_URL is present, use it
45
43
  // if in DEV, use the host/port combination by default
46
44
  // if in PROD, assume it's RAZZLE_API_PATH server name (no /api or alikes) or fallback
47
- // to DEV settings if RAZZLE_API_PATH is not present
45
+ // to DEV settings if RAZZLE_API_PATH is not present.
46
+ // Finally, add the subpath, if there is one.
48
47
  const publicURL =
49
- process.env.RAZZLE_PUBLIC_URL ||
50
- (__DEVELOPMENT__
51
- ? `http://${host}:${port}`
52
- : getServerURL(process.env.RAZZLE_API_PATH) || `http://${host}:${port}`);
48
+ (process.env.RAZZLE_PUBLIC_URL ||
49
+ (__DEVELOPMENT__
50
+ ? `http://${host}:${port}`
51
+ : getServerURL(process.env.RAZZLE_API_PATH) ||
52
+ `http://${host}:${port}`)) + (process.env.RAZZLE_SUBPATH_PREFIX || '');
53
53
 
54
54
  const serverConfig =
55
55
  typeof __SERVER__ !== 'undefined' && __SERVER__
@@ -63,7 +63,10 @@ let config = {
63
63
  // The URL Volto is going to be served (see sensible defaults above)
64
64
  publicURL,
65
65
  okRoute: '/ok',
66
- apiPath,
66
+ // Base URL for API requests from the browser.
67
+ // If not explicitly set, use the publicURL (seamless mode) --
68
+ // but in that case it will be updated in server.jsx for each request.
69
+ apiPath: process.env.RAZZLE_API_PATH || publicURL,
67
70
  apiExpanders: [
68
71
  // Added here for documentation purposes, added at the end because it
69
72
  // depends on a value of this object.
@@ -97,6 +100,7 @@ let config = {
97
100
  // apiPath: process.env.RAZZLE_API_PATH || 'http://localhost:8081/db/web', // for guillotina
98
101
  actions_raising_api_errors: ['GET_CONTENT', 'UPDATE_CONTENT'],
99
102
  internalApiPath: process.env.RAZZLE_INTERNAL_API_PATH || undefined,
103
+ subpathPrefix: process.env.RAZZLE_SUBPATH_PREFIX || '',
100
104
  websockets: process.env.RAZZLE_WEBSOCKETS || false,
101
105
  // TODO: legacyTraverse to be removed when the use of the legacy traverse is deprecated.
102
106
  legacyTraverse: process.env.RAZZLE_LEGACY_TRAVERSE || false,
@@ -111,9 +115,7 @@ let config = {
111
115
  openExternalLinkInNewTab: false,
112
116
  notSupportedBrowsers: ['ie'],
113
117
  defaultPageSize: 25,
114
- isMultilingual: false,
115
- supportedLanguages: ['en'],
116
- defaultLanguage: process.env.SITE_DEFAULT_LANGUAGE || 'en',
118
+ supportedLanguages: Object.keys(languages),
117
119
  navDepth: 1,
118
120
  expressMiddleware: serverConfig.expressMiddleware, // BBB
119
121
  defaultBlockType: 'slate',
@@ -141,7 +143,7 @@ let config = {
141
143
  serverConfig,
142
144
  storeExtenders: [],
143
145
  showTags: true,
144
- showRelatedItems: false,
146
+ showRelatedItems: true,
145
147
  controlpanels: [],
146
148
  controlPanelsIcons,
147
149
  filterControlPanels,
@@ -198,7 +200,8 @@ config.settings.apiExpanders = [
198
200
  ...config.settings.apiExpanders,
199
201
  {
200
202
  match: '',
201
- GET_CONTENT: ['breadcrumbs', 'actions', 'types', 'navroot'],
203
+ GET_CONTENT: ['breadcrumbs', 'actions', 'types', 'navroot', 'translations'],
204
+ // Note: translations is removed in the API middleware if the site is not multilingual.
202
205
  },
203
206
  {
204
207
  match: '',
@@ -232,16 +235,6 @@ Object.entries(slots).forEach(([slotName, components]) => {
232
235
  });
233
236
  });
234
237
 
235
- // Make sure that process.env.SITE_DEFAULT_LANGUAGE is set in availableLanguages
236
- if (
237
- process.env.SITE_DEFAULT_LANGUAGE &&
238
- !config.settings.supportedLanguages.includes(
239
- process.env.SITE_DEFAULT_LANGUAGE,
240
- )
241
- ) {
242
- config.settings.supportedLanguages.push(process.env.SITE_DEFAULT_LANGUAGE);
243
- }
244
-
245
238
  registerValidators(ConfigRegistry);
246
239
  installDefaultComponents(ConfigRegistry);
247
240
  installDefaultWidgets(ConfigRegistry);
@@ -1,7 +1,6 @@
1
1
  import imagesMiddleware from '@plone/volto/express-middleware/images';
2
2
  import filesMiddleware from '@plone/volto/express-middleware/files';
3
3
  import robotstxtMiddleware from '@plone/volto/express-middleware/robotstxt';
4
- import okMiddleware from '@plone/volto/express-middleware/ok';
5
4
  import sitemapMiddleware from '@plone/volto/express-middleware/sitemap';
6
5
  import staticsMiddleware from '@plone/volto/express-middleware/static';
7
6
  import devProxyMiddleware from '@plone/volto/express-middleware/devproxy';
@@ -12,7 +11,6 @@ const settings = {
12
11
  filesMiddleware(),
13
12
  imagesMiddleware(),
14
13
  robotstxtMiddleware(),
15
- okMiddleware(),
16
14
  sitemapMiddleware(),
17
15
  staticsMiddleware(),
18
16
  ],
@@ -10,8 +10,16 @@ import querystring from 'querystring';
10
10
  import { parse as parseUrl } from 'url';
11
11
 
12
12
  const filter = function (pathname, req) {
13
- // This is the proxy to the API in case the accept header is 'application/json'
14
- return config.settings.devProxyToApiPath && pathname.startsWith('/++api++');
13
+ // Check if pathname is defined, there are some corner cases that pathname is null
14
+ if (pathname) {
15
+ // This is the proxy to the API in case the accept header is 'application/json'
16
+ return (
17
+ config.settings.devProxyToApiPath &&
18
+ pathname.startsWith(`${config.settings.subpathPrefix}/++api++`)
19
+ );
20
+ } else {
21
+ return false;
22
+ }
15
23
  };
16
24
 
17
25
  let _env = null;
@@ -73,17 +81,24 @@ export default function devProxyMiddleware() {
73
81
  },
74
82
  pathRewrite: (path, req) => {
75
83
  const { apiPathURL, instancePath } = getEnv();
84
+ const vhSubpath = config.settings.subpathPrefix
85
+ ? config.settings.subpathPrefix
86
+ .split('/')
87
+ .filter(Boolean)
88
+ .map((part) => '/_vh_' + part)
89
+ .join('')
90
+ : '';
76
91
  const target =
77
92
  config.settings.proxyRewriteTarget ||
78
93
  `/VirtualHostBase/${apiPathURL.protocol.slice(0, -1)}/${
79
94
  apiPathURL.hostname
80
- }:${apiPathURL.port}${instancePath}/++api++/VirtualHostRoot`;
95
+ }:${apiPathURL.port}${instancePath}/++api++/VirtualHostRoot${vhSubpath}`;
81
96
 
82
- return `${target}${path.replace('/++api++', '')}`;
97
+ return `${target}${path.replace(`${config.settings.subpathPrefix}/++api++`, '')}`;
83
98
  },
99
+ changeOrigin: true,
84
100
  logLevel: process.env.DEBUG_HPM ? 'debug' : 'silent',
85
101
  ...(process.env.RAZZLE_DEV_PROXY_INSECURE && {
86
- changeOrigin: true,
87
102
  secure: false,
88
103
  }),
89
104
  });
@@ -6,6 +6,7 @@
6
6
  import superagent from 'superagent';
7
7
  import config from '@plone/volto/registry';
8
8
  import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
9
+ import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
9
10
 
10
11
  /**
11
12
  * Get a resource image/file with authenticated (if token exist) API headers
@@ -16,9 +17,9 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
16
17
  export const getAPIResourceWithAuth = (req) =>
17
18
  new Promise((resolve, reject) => {
18
19
  const { settings } = config;
19
- const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
20
-
20
+ const apiSuffix = settings.legacyTraverse ? '' : '/++api++';
21
21
  let apiPath = '';
22
+
22
23
  if (settings.internalApiPath && __SERVER__) {
23
24
  apiPath = settings.internalApiPath;
24
25
  } else if (__DEVELOPMENT__ && settings.devProxyToApiPath) {
@@ -26,8 +27,12 @@ export const getAPIResourceWithAuth = (req) =>
26
27
  } else {
27
28
  apiPath = settings.apiPath;
28
29
  }
30
+
31
+ //strip subpath if any
32
+ const contentPath = stripSubpathPrefix(req.path);
33
+
29
34
  const request = superagent
30
- .get(`${apiPath}${__DEVELOPMENT__ ? '' : APISUFIX}${req.path}`)
35
+ .get(`${apiPath}${__DEVELOPMENT__ ? '' : apiSuffix}${contentPath}`)
31
36
  .maxResponseSize(settings.maxResponseSize)
32
37
  .responseType('blob');
33
38
  const authToken = req.universalCookies.get('auth_token');
@@ -7,7 +7,10 @@ import superagent from 'superagent';
7
7
  import Cookies from 'universal-cookie';
8
8
  import config from '@plone/volto/registry';
9
9
  import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
10
- import { stripQuerystring } from '@plone/volto/helpers/Url/Url';
10
+ import {
11
+ stripQuerystring,
12
+ stripSubpathPrefix,
13
+ } from '@plone/volto/helpers/Url/Url';
11
14
 
12
15
  const methods = ['get', 'post', 'put', 'patch', 'del'];
13
16
 
@@ -19,11 +22,10 @@ const methods = ['get', 'post', 'put', 'patch', 'del'];
19
22
  */
20
23
  export function formatUrl(path) {
21
24
  const { settings } = config;
22
- const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
25
+ const apiSuffix = settings.legacyTraverse ? '' : '/++api++';
23
26
 
24
27
  if (path.startsWith('http://') || path.startsWith('https://')) return path;
25
28
 
26
- const adjustedPath = path[0] !== '/' ? `/${path}` : path;
27
29
  let apiPath = '';
28
30
  if (settings.internalApiPath && __SERVER__) {
29
31
  apiPath = settings.internalApiPath;
@@ -31,7 +33,8 @@ export function formatUrl(path) {
31
33
  apiPath = settings.apiPath;
32
34
  }
33
35
 
34
- return `${apiPath}${APISUFIX}${adjustedPath}`;
36
+ const contentPath = stripSubpathPrefix(path[0] !== '/' ? `/${path}` : path);
37
+ return `${apiPath}${apiSuffix}${contentPath}`;
35
38
  }
36
39
 
37
40
  /**
@@ -1,6 +1,7 @@
1
1
  import { matchRoutes } from 'react-router-config';
2
2
  import { mapSeries, isPromise } from './utils';
3
3
  import { endGlobalLoad } from '@plone/volto/actions/asyncConnect/asyncConnect';
4
+ import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
4
5
 
5
6
  export function filterComponents(branch) {
6
7
  return branch.reduce((result, { route, match }) => {
@@ -18,7 +19,9 @@ export function loadAsyncConnect({
18
19
  filter = () => true,
19
20
  ...rest
20
21
  }) {
21
- const layered = filterComponents(matchRoutes(routes, location.pathname));
22
+ const layered = filterComponents(
23
+ matchRoutes(routes, stripSubpathPrefix(location.pathname)),
24
+ );
22
25
 
23
26
  if (layered.length === 0) {
24
27
  return Promise.resolve();