@plone/volto 18.35.0 → 18.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/locales/af/LC_MESSAGES/volto.po +42 -11
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +42 -11
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +42 -11
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +42 -11
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +42 -11
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +42 -11
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +42 -11
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +42 -11
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +42 -11
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +42 -11
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +42 -11
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +42 -11
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +42 -11
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +42 -11
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +42 -11
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +42 -11
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +42 -11
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +42 -11
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +42 -11
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +42 -11
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +42 -11
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +42 -11
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +42 -11
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +42 -11
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +42 -11
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +42 -11
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +42 -11
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +42 -11
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +42 -11
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +42 -11
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +42 -11
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +42 -11
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +42 -11
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +42 -11
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +42 -11
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +42 -11
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +42 -11
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +42 -11
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +42 -11
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +42 -11
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +42 -11
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +42 -11
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +42 -11
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +42 -11
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +42 -11
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +42 -11
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +42 -11
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +42 -11
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +42 -11
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +42 -11
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +42 -11
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +42 -11
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +42 -11
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +42 -11
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +42 -11
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +42 -11
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +42 -11
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +42 -11
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +42 -11
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +42 -11
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +42 -11
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +42 -11
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +40 -9
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +42 -11
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +42 -11
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +42 -11
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +18 -22
  134. package/src/actions/querystringsearch/querystringsearch.js +4 -1
  135. package/src/actions/querystringsearch/querystringsearch.test.js +77 -0
  136. package/src/components/manage/BlockChooser/BlockChooser.jsx +7 -10
  137. package/src/components/manage/Blocks/Block/Edit.jsx +9 -10
  138. package/src/components/manage/Blocks/Block/Order/Item.jsx +9 -4
  139. package/src/components/manage/Blocks/Image/View.test.jsx +8 -8
  140. package/src/components/manage/Contents/Contents.jsx +3 -3
  141. package/src/components/manage/Contents/ContentsBreadcrumbs.jsx +7 -6
  142. package/src/components/manage/Contents/ContentsItem.jsx +1 -3
  143. package/src/components/manage/Contents/DropZoneContent.jsx +1 -0
  144. package/src/components/manage/Sidebar/ObjectBrowser.jsx +7 -0
  145. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +30 -5
  146. package/src/components/manage/Sidebar/ObjectBrowserBody.test.jsx +52 -0
  147. package/src/components/manage/Toolbar/More.jsx +13 -0
  148. package/src/components/manage/Toolbar/Toolbar.jsx +91 -8
  149. package/src/components/manage/Widgets/ArrayWidget.jsx +3 -2
  150. package/src/components/manage/Widgets/FileWidget.jsx +71 -17
  151. package/src/components/manage/Widgets/FormFieldWrapper.jsx +16 -3
  152. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +1 -0
  153. package/src/components/manage/Widgets/QuerystringWidget.jsx +1 -18
  154. package/src/components/manage/Widgets/QuerystringWidget.test.jsx +45 -2
  155. package/src/components/manage/Widgets/RegistryImageWidget.jsx +4 -2
  156. package/src/components/manage/Widgets/SelectStyling.jsx +33 -1
  157. package/src/components/manage/Widgets/SelectWidget.jsx +3 -2
  158. package/src/components/manage/Widgets/TextWidget.test.jsx +44 -0
  159. package/src/components/theme/Sitemap/Sitemap.jsx +21 -28
  160. package/src/components/theme/Sitemap/Sitemap.test.jsx +18 -0
  161. package/src/components/theme/Unauthorized/Unauthorized.jsx +1 -2
  162. package/src/components/theme/View/EventView.stories.jsx +89 -0
  163. package/src/components/theme/View/FileView.stories.jsx +50 -0
  164. package/src/components/theme/View/LinkView.stories.jsx +57 -0
  165. package/src/components/theme/View/ListingView.stories.jsx +70 -0
  166. package/src/components/theme/View/NewsItemView.stories.jsx +58 -0
  167. package/src/components/theme/View/RenderBlocks.stories.jsx +112 -0
  168. package/src/components/theme/View/SummaryView.stories.jsx +71 -0
  169. package/src/components/theme/View/TabularView.stories.jsx +66 -0
  170. package/src/config/index.js +1 -0
  171. package/src/helpers/I18n/I18n.test.ts +44 -0
  172. package/src/helpers/I18n/I18n.ts +31 -0
  173. package/src/helpers/index.js +1 -0
  174. package/src/server.jsx +7 -1
  175. package/theme/themes/pastanaga/extras/focus.less +33 -0
  176. package/theme/themes/pastanaga/extras/main.less +2 -0
  177. package/theme/themes/pastanaga/extras/skiplinks.less +0 -2
  178. package/theme/themes/pastanaga/globals/site.variables +7 -0
  179. package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
  180. package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
  181. package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
  182. package/types/components/manage/Sidebar/ObjectBrowserBody.test.d.ts +1 -0
  183. package/types/components/manage/Widgets/ImageWidget.d.ts +1 -1
  184. package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
  185. package/types/components/manage/Widgets/QuerystringWidget.d.ts +0 -4
  186. package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
  187. package/types/components/manage/Widgets/index.d.ts +2 -2
  188. package/types/components/theme/View/EventView.stories.d.ts +19 -0
  189. package/types/components/theme/View/FileView.stories.d.ts +18 -0
  190. package/types/components/theme/View/LinkView.stories.d.ts +18 -0
  191. package/types/components/theme/View/ListingView.stories.d.ts +24 -0
  192. package/types/components/theme/View/NewsItemView.stories.d.ts +23 -0
  193. package/types/components/theme/View/RenderBlocks.stories.d.ts +23 -0
  194. package/types/components/theme/View/SummaryView.stories.d.ts +23 -0
  195. package/types/components/theme/View/TabularView.stories.d.ts +23 -0
  196. package/types/helpers/I18n/I18n.d.ts +20 -0
  197. package/types/helpers/index.d.ts +1 -0
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "18.35.0",
12
+ "version": "18.36.0",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -154,7 +154,6 @@
154
154
  "diff": "3.5.1",
155
155
  "express": "4.22.1",
156
156
  "filesize": "6",
157
- "full-icu": "1.4.0",
158
157
  "github-slugger": "1.4.0",
159
158
  "history": "4.10.1",
160
159
  "hoist-non-react-statics": "3.3.2",
@@ -173,7 +172,7 @@
173
172
  "prepend-http": "2",
174
173
  "pretty-bytes": "5.3.0",
175
174
  "prettier": "3.2.5",
176
- "prismjs": "1.27.0",
175
+ "prismjs": "1.30.0",
177
176
  "process": "^0.11.10",
178
177
  "promise-file-reader": "1.0.2",
179
178
  "prop-types": "15.7.2",
@@ -207,7 +206,6 @@
207
206
  "react-side-effect": "2.1.2",
208
207
  "react-simple-code-editor": "0.7.1",
209
208
  "react-sortable-hoc": "2.0.0",
210
- "react-test-renderer": "18.2.0",
211
209
  "react-toastify": "5.5.0",
212
210
  "react-transition-group": "4.4.5",
213
211
  "react-virtualized": "9.22.3",
@@ -215,7 +213,6 @@
215
213
  "redux-actions": "3.0.0",
216
214
  "redux-connect": "10.0.0",
217
215
  "redux-localstorage-simple": "2.5.1",
218
- "redux-mock-store": "1.5.4",
219
216
  "redux-thunk": "2.4.2",
220
217
  "rrule": "2.7.1",
221
218
  "semantic-ui-less": "2.4.1",
@@ -231,10 +228,10 @@
231
228
  "universal-cookie-express": "4.0.3",
232
229
  "url": "^0.11.3",
233
230
  "use-deep-compare-effect": "1.8.1",
234
- "uuid": "^8.3.2",
235
- "@plone/scripts": "3.10.6",
236
- "@plone/registry": "2.7.2",
237
- "@plone/volto-slate": "18.10.0"
231
+ "uuid": "^14.0.0",
232
+ "@plone/scripts": "3.10.7",
233
+ "@plone/registry": "2.7.3",
234
+ "@plone/volto-slate": "18.10.2"
238
235
  },
239
236
  "devDependencies": {
240
237
  "@babel/core": "^7.0.0",
@@ -263,8 +260,8 @@
263
260
  "@storybook/test": "^8.0.4",
264
261
  "@storybook/theming": "^8.0.4",
265
262
  "@testing-library/cypress": "10.1.0",
266
- "@testing-library/jest-dom": "6.4.2",
267
- "@testing-library/react": "14.3.1",
263
+ "@testing-library/jest-dom": "6.9.1",
264
+ "@testing-library/react": "^16.3.2",
268
265
  "@testing-library/react-hooks": "8.0.1",
269
266
  "@types/history": "^4.7.11",
270
267
  "@types/jest": "^29.5.8",
@@ -277,11 +274,10 @@
277
274
  "@types/react-router-dom": "^5.3.3",
278
275
  "@types/react-test-renderer": "18.0.7",
279
276
  "@types/redux-mock-store": "^1.5.0",
280
- "@types/uuid": "^9.0.2",
281
277
  "@typescript-eslint/eslint-plugin": "^7.7.0",
282
278
  "@typescript-eslint/parser": "^7.7.0",
283
279
  "@vitejs/plugin-react": "^4.3.4",
284
- "@vitest/ui": "^2.1.8",
280
+ "@vitest/ui": "^3.2.4",
285
281
  "autoprefixer": "10.4.8",
286
282
  "axe-core": "4.4.2",
287
283
  "babel-loader": "9.1.0",
@@ -321,20 +317,20 @@
321
317
  "lodash-webpack-plugin": "0.11.6",
322
318
  "mini-css-extract-plugin": "2.10.2",
323
319
  "moment-locales-webpack-plugin": "1.2.0",
324
- "postcss": "8.4.31",
320
+ "postcss": "^8.5.15",
325
321
  "postcss-flexbugs-fixes": "5.0.2",
326
322
  "postcss-less": "6.0.0",
327
- "postcss-load-config": "3.1.4",
328
- "postcss-loader": "7.0.2",
329
- "postcss-overrides": "3.1.4",
330
- "postcss-scss": "4.0.6",
323
+ "postcss-load-config": "^6.0.1",
324
+ "postcss-loader": "^8.2.1",
325
+ "postcss-scss": "4.0.9",
331
326
  "razzle": "4.2.18",
332
327
  "razzle-dev-utils": "4.2.18",
333
328
  "razzle-plugin-scss": "4.2.18",
334
- "react-docgen-typescript-plugin": "^1.0.8",
335
329
  "react-error-overlay": "6.0.9",
336
330
  "react-is": "^18.2.0",
337
- "release-it": "^19.0.5",
331
+ "react-test-renderer": "18.2.0",
332
+ "redux-mock-store": "1.5.4",
333
+ "release-it": "^20.0.1",
338
334
  "semver": "^7.5.4",
339
335
  "start-server-and-test": "2.1.5",
340
336
  "storybook": "^8.6.15",
@@ -357,7 +353,7 @@
357
353
  "webpack-dev-server": "4.11.1",
358
354
  "webpack-node-externals": "3.0.0",
359
355
  "why": "0.6.2",
360
- "@plone/types": "1.6.2",
356
+ "@plone/types": "1.6.3",
361
357
  "@plone/volto-coresandbox": "1.0.0"
362
358
  },
363
359
  "scripts": {
@@ -369,7 +365,7 @@
369
365
  "vitest": "vitest",
370
366
  "coverage": "vitest run --coverage",
371
367
  "test": "razzle test --maxWorkers=${MAX_WORKERS:-50%}",
372
- "test:ci": "CI=true NODE_ICU_DATA=node_modules/full-icu razzle test",
368
+ "test:ci": "CI=true razzle test",
373
369
  "test:husky": "CI=true yarn test --bail --findRelatedTests",
374
370
  "start:prod": "NODE_ENV=production node build/server.js",
375
371
  "prettier": "prettier --single-quote --check '{src,cypress}/**/*.{js,jsx,ts,tsx}' --check '*.js'",
@@ -17,7 +17,10 @@ export function getQueryStringResults(path, data, subrequest, page) {
17
17
  delete requestData.depth;
18
18
  requestData.query.forEach((q) => {
19
19
  if (q.i === 'path') {
20
- q.v += '::' + data.depth;
20
+ // fixes https://github.com/plone/volto/issues/8349
21
+ // Skip if depth is already embedded in the path value (e.g. set via object browser)
22
+ const hasEmbeddedDepth = /::\d+$/.test(q.v);
23
+ if (!hasEmbeddedDepth) q.v += '::' + data.depth;
21
24
  }
22
25
  });
23
26
  }
@@ -73,4 +73,81 @@ describe('querystringsearch action', () => {
73
73
  expect(action.request.path).toEqual('/@querystring-search');
74
74
  expect(action.request.data.sort_order).toEqual('descending');
75
75
  });
76
+
77
+ describe('depth handling', () => {
78
+ it('appends depth to path value when depth is set and path has no embedded depth', () => {
79
+ const data = {
80
+ query: [
81
+ {
82
+ i: 'path',
83
+ o: 'plone.app.querystring.operation.string.path',
84
+ v: '/folder',
85
+ },
86
+ ],
87
+ depth: 2,
88
+ };
89
+ const action = getQueryStringResults('', data);
90
+
91
+ const pathQuery = action.request.data.query.find((q) => q.i === 'path');
92
+ expect(pathQuery.v).toEqual('/folder::2');
93
+ });
94
+
95
+ it('does not double-append depth when path value already contains an embedded depth', () => {
96
+ const data = {
97
+ query: [
98
+ {
99
+ i: 'path',
100
+ o: 'plone.app.querystring.operation.string.path',
101
+ v: '/folder::1',
102
+ },
103
+ ],
104
+ depth: 3,
105
+ };
106
+ const action = getQueryStringResults('', data);
107
+
108
+ const pathQuery = action.request.data.query.find((q) => q.i === 'path');
109
+ expect(pathQuery.v).toEqual('/folder::1');
110
+ });
111
+
112
+ it('removes depth from the top-level request data', () => {
113
+ const data = {
114
+ query: [
115
+ {
116
+ i: 'path',
117
+ o: 'plone.app.querystring.operation.string.path',
118
+ v: '/folder',
119
+ },
120
+ ],
121
+ depth: 2,
122
+ };
123
+ const action = getQueryStringResults('', data);
124
+
125
+ expect(action.request.data.depth).toBeUndefined();
126
+ });
127
+
128
+ it('handles mixed path entries: appends depth only to those without embedded depth', () => {
129
+ const data = {
130
+ query: [
131
+ {
132
+ i: 'path',
133
+ o: 'plone.app.querystring.operation.string.path',
134
+ v: '/folder-a',
135
+ },
136
+ {
137
+ i: 'path',
138
+ o: 'plone.app.querystring.operation.string.path',
139
+ v: '/folder-b::5',
140
+ },
141
+ ],
142
+ depth: 2,
143
+ };
144
+ const action = getQueryStringResults('', data);
145
+
146
+ const [pathA, pathB] = action.request.data.query.filter(
147
+ (q) => q.i === 'path',
148
+ );
149
+ expect(pathA.v).toEqual('/folder-a::2');
150
+ expect(pathB.v).toEqual('/folder-b::5');
151
+ });
152
+ });
76
153
  });
@@ -10,6 +10,7 @@ import { useIntl, defineMessages } from 'react-intl';
10
10
  import Icon from '@plone/volto/components/theme/Icon/Icon';
11
11
  import AnimateHeight from 'react-animate-height';
12
12
  import config from '@plone/volto/registry';
13
+ import { formatMessageWithFallback } from '@plone/volto/helpers/I18n/I18n';
13
14
  import upSVG from '@plone/volto/icons/up-key.svg';
14
15
  import downSVG from '@plone/volto/icons/down-key.svg';
15
16
  import BlockChooserSearch from './BlockChooserSearch';
@@ -98,16 +99,10 @@ const BlockChooser = ({
98
99
  }
99
100
  const [filterValue, setFilterValue] = React.useState('');
100
101
 
101
- const getFormatMessage = (message) =>
102
- intl.formatMessage({
103
- id: message,
104
- defaultMessage: message,
105
- });
106
-
107
102
  function blocksAvailableFilter(blocks) {
108
103
  return blocks.filter(
109
104
  (block) =>
110
- getFormatMessage(block.title)
105
+ formatMessageWithFallback(intl, block.title)
111
106
  .toLowerCase()
112
107
  .includes(filterValue.toLowerCase()) ||
113
108
  block.title.toLowerCase().includes(filterValue.toLowerCase()) ||
@@ -117,7 +112,7 @@ const BlockChooser = ({
117
112
  function filterVariations(block) {
118
113
  return block.variations?.filter(
119
114
  (variation) =>
120
- getFormatMessage(variation.title)
115
+ formatMessageWithFallback(intl, variation.title)
121
116
  .toLowerCase()
122
117
  .includes(filterValue.toLowerCase()) &&
123
118
  !variation.title.toLowerCase().includes('default'),
@@ -145,9 +140,11 @@ const BlockChooser = ({
145
140
  }}
146
141
  >
147
142
  <Icon name={block.icon} size="36px" />
148
- {getFormatMessage(block.title)}
143
+ {formatMessageWithFallback(intl, block.title)}
149
144
  {filterValue && variations?.[0]?.title && (
150
- <small>{getFormatMessage(variations[0].title)}</small>
145
+ <small>
146
+ {formatMessageWithFallback(intl, variations[0].title)}
147
+ </small>
151
148
  )}
152
149
  </Button>
153
150
  </Button.Group>
@@ -149,15 +149,6 @@ export class Edit extends Component {
149
149
  this.props.setUIState({ hovered: this.props.id });
150
150
  }
151
151
  }}
152
- onFocus={(e) => {
153
- // TODO: This `onFocus` steals somehow the focus from the slate block
154
- // we have to investigate why this is happening
155
- // Apparently, I can't see any difference in the behavior
156
- // If any, we can fix it in successive iterations
157
- // if (this.props.hovered !== this.props.id) {
158
- // this.props.setUIState({ hovered: this.props.id });
159
- // }
160
- }}
161
152
  onMouseLeave={(e) => {
162
153
  e.preventDefault();
163
154
  e.stopPropagation();
@@ -172,6 +163,15 @@ export class Edit extends Component {
172
163
  e,
173
164
  );
174
165
  }}
166
+ onFocus={(e) => {
167
+ const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey;
168
+ !this.props.selected &&
169
+ this.props.onSelectBlock(
170
+ this.props.id,
171
+ this.props.selected ? false : isMultipleSelection,
172
+ e,
173
+ );
174
+ }}
175
175
  onKeyDown={
176
176
  !(blockHasOwnFocusManagement || disableNewBlocks)
177
177
  ? (e) =>
@@ -245,7 +245,6 @@ export class Edit extends Component {
245
245
  className={cx(`block ${type}`, { selected: this.props.selected })}
246
246
  style={{ outline: 'none' }}
247
247
  ref={this.blockNode}
248
- // The tabIndex is required for the keyboard navigation
249
248
  tabIndex={-1}
250
249
  >
251
250
  {this.props.intl.formatMessage(messages.unknownBlock, {
@@ -5,7 +5,9 @@ import includes from 'lodash/includes';
5
5
  import cx from 'classnames';
6
6
  import Icon from '@plone/volto/components/theme/Icon/Icon';
7
7
  import { setUIState } from '@plone/volto/actions/form/form';
8
+ import { formatMessageWithFallback } from '@plone/volto/helpers/I18n/I18n';
8
9
  import config from '@plone/volto/registry';
10
+ import { useIntl } from 'react-intl';
9
11
 
10
12
  import deleteSVG from '@plone/volto/icons/delete.svg';
11
13
  import dragSVG from '@plone/volto/icons/drag.svg';
@@ -34,6 +36,7 @@ export const Item = forwardRef(
34
36
  },
35
37
  ref,
36
38
  ) => {
39
+ const intl = useIntl();
37
40
  const selected = useSelector((state) => state.form.ui.selected);
38
41
  const hovered = useSelector((state) => state.form.ui.hovered);
39
42
  const multiSelected = useSelector((state) => state.form.ui.multiSelected);
@@ -49,7 +52,11 @@ export const Item = forwardRef(
49
52
  ? data.required
50
53
  : includes(config.blocks.requiredBlocks, data?.['@type']);
51
54
  const fixed = !!data?.fixed;
52
-
55
+ const configTitle = config.blocks.blocksConfig[data?.['@type']]?.title;
56
+ const blockTitle =
57
+ data?.plaintext ||
58
+ formatMessageWithFallback(intl, configTitle) ||
59
+ data?.title;
53
60
  return (
54
61
  <li
55
62
  className={classNames(
@@ -122,9 +129,7 @@ export const Item = forwardRef(
122
129
  style={{ verticalAlign: 'middle' }}
123
130
  />
124
131
  )}{' '}
125
- {data?.plaintext ||
126
- config.blocks.blocksConfig[data?.['@type']]?.title ||
127
- data?.title}
132
+ {blockTitle}
128
133
  </span>
129
134
  {!clone && onRemove && !required && (
130
135
  <button
@@ -36,13 +36,13 @@ config.blocks.blocksConfig = {
36
36
 
37
37
  describe('Image View Component', () => {
38
38
  test('renders a view image component with a local image', () => {
39
- const { getByRole } = render(<View data={{ url: '/image.jpg' }} />);
40
- const img = getByRole('img');
39
+ const { container } = render(<View data={{ url: '/image.jpg' }} />);
40
+ const img = container.querySelector('img');
41
41
  expect(img).toHaveAttribute('src', '/image.jpg/@@images/image');
42
42
  expect(img).toHaveAttribute('loading', 'lazy');
43
43
  });
44
44
  test('renders a view image component with a local image with a link', () => {
45
- const { container, getByRole } = render(
45
+ const { container } = render(
46
46
  <Provider store={store}>
47
47
  <MemoryRouter>
48
48
  <View
@@ -51,22 +51,22 @@ describe('Image View Component', () => {
51
51
  </MemoryRouter>
52
52
  </Provider>,
53
53
  );
54
- const img = getByRole('img');
54
+ const img = container.querySelector('img');
55
55
  const a = container.querySelector('a');
56
56
  expect(img).toHaveAttribute('src', '/image.jpg/@@images/image');
57
57
  expect(a).toHaveAttribute('href', '/front-page');
58
58
  });
59
59
  test('renders a view image component with an external image', () => {
60
- const { getByRole } = render(
60
+ const { container } = render(
61
61
  <Provider store={store}>
62
62
  <View data={{ url: 'https://plone.org/logo.jpg' }} />
63
63
  </Provider>,
64
64
  );
65
- const img = getByRole('img');
65
+ const img = container.querySelector('img');
66
66
  expect(img).toHaveAttribute('src', 'https://plone.org/logo.jpg');
67
67
  });
68
68
  test('renders a view image component with an external image with a link', () => {
69
- const { container, getByRole } = render(
69
+ const { container } = render(
70
70
  <Provider store={store}>
71
71
  <View
72
72
  data={{
@@ -76,7 +76,7 @@ describe('Image View Component', () => {
76
76
  />
77
77
  </Provider>,
78
78
  );
79
- const img = getByRole('img');
79
+ const img = container.querySelector('img');
80
80
  const a = container.querySelector('a');
81
81
  expect(img).toHaveAttribute('src', 'https://plone.org/logo.jpg');
82
82
  expect(a).toHaveAttribute('href', 'http://front-page');
@@ -724,9 +724,9 @@ class Contents extends Component {
724
724
  delta,
725
725
  );
726
726
  } else {
727
- this.setState({
728
- items: move(this.state.items, itemIndex, itemIndex + delta),
729
- });
727
+ this.setState((prevState) => ({
728
+ items: move(prevState.items, itemIndex, itemIndex + delta),
729
+ }));
730
730
  }
731
731
  }
732
732
 
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Breadcrumb } from 'semantic-ui-react';
3
+ import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
3
4
  import { Link, useLocation } from 'react-router-dom';
4
5
  import { defineMessages, useIntl } from 'react-intl';
5
6
  import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
@@ -41,22 +42,22 @@ const ContentsBreadcrumbs = (props) => {
41
42
  </>
42
43
  )}
43
44
  {settings.isMultilingual && pathname?.split('/')?.length > 2 && (
44
- <Link
45
- to={`/${lang}/contents`}
45
+ <UniversalLink
46
+ href={`/${lang}/contents`}
46
47
  className="section"
47
48
  title={intl.formatMessage(messages.home)}
48
49
  >
49
50
  {langmap?.[lang]?.nativeName ?? lang}
50
- </Link>
51
+ </UniversalLink>
51
52
  )}
52
53
  {!settings.isMultilingual && (
53
- <Link
54
- to="/contents"
54
+ <UniversalLink
55
+ href="/contents"
55
56
  className="section"
56
57
  title={intl.formatMessage(messages.home)}
57
58
  >
58
59
  <ContentsBreadcrumbsHomeItem />
59
- </Link>
60
+ </UniversalLink>
60
61
  )}
61
62
  {items.map((breadcrumb, index, breadcrumbs) => [
62
63
  <Breadcrumb.Divider key={`divider-${breadcrumb.url}`} />,
@@ -364,15 +364,13 @@ const DragDropConnector = (props) => {
364
364
  drop(props, monitor) {
365
365
  const id = monitor.getItem().id;
366
366
  const dragOrder = monitor.getItem().startOrder;
367
- const dropOrder = props.order;
367
+ const dropOrder = monitor.getItem().order;
368
368
 
369
369
  if (dragOrder === dropOrder) {
370
370
  return;
371
371
  }
372
372
 
373
373
  props.onOrderItem(id, dragOrder, dropOrder - dragOrder, true);
374
-
375
- monitor.getItem().order = dropOrder;
376
374
  },
377
375
  },
378
376
  (connect) => ({
@@ -113,6 +113,7 @@ const DropZoneContent = (props) => {
113
113
  };
114
114
 
115
115
  const onDrop = async (e) => {
116
+ e.preventDefault();
116
117
  if (!hasFiles(e)) {
117
118
  return;
118
119
  }
@@ -58,6 +58,7 @@ const withObjectBrowser = (WrappedComponent) =>
58
58
  selectableTypes,
59
59
  maximumSelectionSize,
60
60
  currentPath,
61
+ initialPath,
61
62
  onlyFolderishSelectable,
62
63
  } = {}) =>
63
64
  this.setState(() => ({
@@ -71,6 +72,7 @@ const withObjectBrowser = (WrappedComponent) =>
71
72
  selectableTypes,
72
73
  maximumSelectionSize,
73
74
  currentPath,
75
+ initialPath,
74
76
  onlyFolderishSelectable,
75
77
  }));
76
78
 
@@ -82,6 +84,10 @@ const withObjectBrowser = (WrappedComponent) =>
82
84
  this.props.pathname ||
83
85
  this.props.location?.pathname;
84
86
 
87
+ let initialPath = this.state?.initialPath
88
+ ? getBaseUrl(this.state.initialPath)
89
+ : null;
90
+
85
91
  return (
86
92
  <>
87
93
  <WrappedComponent
@@ -105,6 +111,7 @@ const withObjectBrowser = (WrappedComponent) =>
105
111
  : this.props.data
106
112
  }
107
113
  contextURL={getBaseUrl(contextURL)}
114
+ initialPath={initialPath}
108
115
  closeObjectBrowser={this.closeObjectBrowser}
109
116
  mode={this.state.mode}
110
117
  onSelectItem={this.state.onSelectItem}
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
5
5
  import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
6
6
  import { Input, Segment, Breadcrumb } from 'semantic-ui-react';
7
7
 
8
+ import debounce from 'lodash/debounce';
8
9
  import join from 'lodash/join';
9
10
 
10
11
  // These absolute imports (without using the corresponding centralized index.js) are required
@@ -84,6 +85,7 @@ class ObjectBrowserBody extends Component {
84
85
  onSelectItem: PropTypes.func,
85
86
  dataName: PropTypes.string,
86
87
  maximumSelectionSize: PropTypes.number,
88
+ initialPath: PropTypes.string,
87
89
  contextURL: PropTypes.string,
88
90
  searchableTypes: PropTypes.arrayOf(PropTypes.string),
89
91
  onlyFolderishSelectable: PropTypes.bool,
@@ -113,18 +115,21 @@ class ObjectBrowserBody extends Component {
113
115
  */
114
116
  constructor(props) {
115
117
  super(props);
118
+ const defaultMultiplePath = props.initialPath || '/';
116
119
  this.state = {
117
120
  currentFolder:
118
- this.props.mode === 'multiple' ? '/' : this.props.contextURL || '/',
121
+ this.props.mode === 'multiple'
122
+ ? defaultMultiplePath
123
+ : this.props.contextURL || '/',
119
124
  currentImageFolder:
120
125
  this.props.mode === 'multiple'
121
- ? '/'
126
+ ? defaultMultiplePath
122
127
  : this.props.mode === 'image' && this.props.data?.url
123
128
  ? getParentURL(this.props.data.url)
124
129
  : '/',
125
130
  currentLinkFolder:
126
131
  this.props.mode === 'multiple'
127
- ? '/'
132
+ ? defaultMultiplePath
128
133
  : this.props.mode === 'link' && this.props.data?.href
129
134
  ? getParentURL(this.props.data.href)
130
135
  : '/',
@@ -155,6 +160,22 @@ class ObjectBrowserBody extends Component {
155
160
  view: this.props.mode === 'image' ? 'icons' : 'list',
156
161
  };
157
162
  this.searchInputRef = React.createRef();
163
+ // Debounce the live-search dispatch so rapid keystrokes do not produce a
164
+ // burst of overlapping requests whose responses may resolve out of order
165
+ // (an older, less-filtered response arriving after the latest one would
166
+ // visually "reset" the list to look unfiltered).
167
+ this.debouncedSearch = debounce(this.doSearch, 300);
168
+ }
169
+
170
+ /**
171
+ * Component will unmount
172
+ * @method componentWillUnmount
173
+ * @returns {undefined}
174
+ */
175
+ componentWillUnmount() {
176
+ if (this.debouncedSearch) {
177
+ this.debouncedSearch.cancel();
178
+ }
158
179
  }
159
180
 
160
181
  /**
@@ -244,8 +265,7 @@ class ObjectBrowserBody extends Component {
244
265
  view: prevState.view === 'icons' ? 'list' : 'icons',
245
266
  }));
246
267
 
247
- onSearch = (e) => {
248
- const text = flattenToAppURL(e.target.value);
268
+ doSearch = (text) => {
249
269
  if (text.startsWith('/')) {
250
270
  this.setState({ currentFolder: text });
251
271
  this.props.searchContent(
@@ -282,6 +302,11 @@ class ObjectBrowserBody extends Component {
282
302
  }
283
303
  };
284
304
 
305
+ onSearch = (e) => {
306
+ const text = flattenToAppURL(e.target.value);
307
+ this.debouncedSearch(text);
308
+ };
309
+
285
310
  onSelectItem = (item) => {
286
311
  const url = item['@id'];
287
312
  const { block, data, mode, dataName, onChangeBlock } = this.props;
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import configureStore from 'redux-mock-store';
4
+ import { Provider } from 'react-intl-redux';
5
+ import ObjectBrowserBody from './ObjectBrowserBody';
6
+
7
+ const mockStore = configureStore();
8
+
9
+ const baseState = {
10
+ search: { subrequests: {} },
11
+ intl: { locale: 'en', messages: {} },
12
+ };
13
+
14
+ const baseProps = {
15
+ block: 'test-block',
16
+ data: {},
17
+ closeObjectBrowser: () => {},
18
+ onChangeBlock: () => {},
19
+ };
20
+
21
+ const getInitialSearchPath = (actions) => {
22
+ const action = actions.find((a) => a.type === 'SEARCH_CONTENT');
23
+ return action?.request?.path?.split('/@search')[0];
24
+ };
25
+
26
+ describe('ObjectBrowserBody', () => {
27
+ it('uses initialPath as the default folder when mode=multiple', () => {
28
+ const store = mockStore(baseState);
29
+ render(
30
+ <Provider store={store}>
31
+ <ObjectBrowserBody
32
+ {...baseProps}
33
+ mode="multiple"
34
+ initialPath="/company/team"
35
+ />
36
+ </Provider>,
37
+ );
38
+
39
+ expect(getInitialSearchPath(store.getActions())).toBe('/company/team');
40
+ });
41
+
42
+ it('defaults to root when mode=multiple and initialPath is not provided', () => {
43
+ const store = mockStore(baseState);
44
+ render(
45
+ <Provider store={store}>
46
+ <ObjectBrowserBody {...baseProps} mode="multiple" />
47
+ </Provider>,
48
+ );
49
+
50
+ expect(getInitialSearchPath(store.getActions())).toBe('/');
51
+ });
52
+ });