@plone/volto 19.0.0-alpha.29 → 19.0.0-alpha.30

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 (165) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/locales/af/LC_MESSAGES/volto.po +45 -0
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +45 -0
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +45 -0
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +45 -0
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +45 -0
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +45 -0
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +45 -0
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +45 -0
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +45 -0
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +45 -0
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +45 -0
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +45 -0
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +45 -0
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +45 -0
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +45 -0
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +45 -0
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +45 -0
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +45 -0
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +45 -0
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +45 -0
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +45 -0
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +45 -0
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +45 -0
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +45 -0
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +45 -0
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +45 -0
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +45 -0
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +45 -0
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +45 -0
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +45 -0
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +45 -0
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +45 -0
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +45 -0
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +45 -0
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +45 -0
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +45 -0
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +45 -0
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +45 -0
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +45 -0
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +45 -0
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +45 -0
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +45 -0
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +45 -0
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +45 -0
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +45 -0
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +100 -55
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +45 -0
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +45 -0
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +45 -0
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +45 -0
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +45 -0
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +45 -0
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +45 -0
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +45 -0
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +45 -0
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +45 -0
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +45 -0
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +45 -0
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +45 -0
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +45 -0
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +45 -0
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +45 -0
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +46 -1
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +45 -0
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +45 -0
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +45 -0
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +26 -28
  134. package/src/components/manage/Blocks/Block/Order/Item.jsx +18 -10
  135. package/src/components/manage/Blocks/Block/Order/Item.test.jsx +90 -0
  136. package/src/components/manage/Contents/ContentsBreadcrumbs.jsx +4 -3
  137. package/src/components/manage/Controlpanels/Users/UserGroupMembershipControlPanel.test.jsx +3 -0
  138. package/src/components/manage/Toast/Toast.jsx +32 -0
  139. package/src/components/manage/Toast/Toast.test.jsx +9 -5
  140. package/src/components/manage/Widgets/UrlWidget.jsx +47 -18
  141. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +1 -0
  142. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +30 -0
  143. package/src/components/theme/Sitemap/Sitemap.stories.jsx +82 -0
  144. package/src/components/theme/Unauthorized/Unauthorized.jsx +30 -23
  145. package/src/components/theme/Unauthorized/Unauthorized.test.jsx +28 -1
  146. package/src/components/theme/View/EventView.stories.jsx +89 -0
  147. package/src/components/theme/View/FileView.stories.jsx +50 -0
  148. package/src/components/theme/View/LinkView.stories.jsx +57 -0
  149. package/src/components/theme/View/ListingView.stories.jsx +70 -0
  150. package/src/components/theme/View/NewsItemView.stories.jsx +58 -0
  151. package/src/components/theme/View/RenderBlocks.stories.jsx +112 -0
  152. package/src/components/theme/View/SummaryView.stories.jsx +71 -0
  153. package/src/components/theme/View/TabularView.stories.jsx +66 -0
  154. package/src/helpers/FormValidation/validators.ts +15 -2
  155. package/theme/themes/pastanaga/extras/main.less +1 -0
  156. package/types/components/manage/Blocks/Block/Order/Item.test.d.ts +1 -0
  157. package/types/components/theme/Sitemap/Sitemap.stories.d.ts +13 -0
  158. package/types/components/theme/View/EventView.stories.d.ts +19 -0
  159. package/types/components/theme/View/FileView.stories.d.ts +18 -0
  160. package/types/components/theme/View/LinkView.stories.d.ts +18 -0
  161. package/types/components/theme/View/ListingView.stories.d.ts +24 -0
  162. package/types/components/theme/View/NewsItemView.stories.d.ts +23 -0
  163. package/types/components/theme/View/RenderBlocks.stories.d.ts +23 -0
  164. package/types/components/theme/View/SummaryView.stories.d.ts +23 -0
  165. package/types/components/theme/View/TabularView.stories.d.ts +23 -0
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "19.0.0-alpha.29",
12
+ "version": "19.0.0-alpha.30",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -90,8 +90,8 @@
90
90
  "@dnd-kit/core": "6.0.8",
91
91
  "@dnd-kit/sortable": "7.0.2",
92
92
  "@dnd-kit/utilities": "3.2.2",
93
- "@loadable/component": "5.14.1",
94
- "@loadable/server": "5.14.0",
93
+ "@loadable/component": "5.16.7",
94
+ "@loadable/server": "5.16.7",
95
95
  "@redux-devtools/extension": "^3.3.0",
96
96
  "classnames": "2.5.1",
97
97
  "connected-react-router": "6.8.0",
@@ -115,14 +115,14 @@
115
115
  "jwt-decode": "2.2.0",
116
116
  "linkify-it": "3.0.2",
117
117
  "locale": "0.1.0",
118
- "lodash": "4.17.23",
118
+ "lodash": "4.18.1",
119
119
  "lodash-move": "1.1.1",
120
120
  "moment": "2.29.4",
121
121
  "object-assign": "4.1.1",
122
122
  "prepend-http": "2",
123
123
  "prettier": "3.2.5",
124
124
  "pretty-bytes": "5.3.0",
125
- "prismjs": "1.27.0",
125
+ "prismjs": "1.30.0",
126
126
  "process": "^0.11.10",
127
127
  "promise-file-reader": "1.0.2",
128
128
  "prop-types": "15.7.2",
@@ -180,11 +180,11 @@
180
180
  "universal-cookie-express": "4.0.3",
181
181
  "url": "^0.11.3",
182
182
  "use-deep-compare-effect": "1.8.1",
183
- "uuid": "^8.3.2",
184
- "@plone/registry": "3.0.0-alpha.10",
185
- "@plone/components": "4.0.0-alpha.6",
186
- "@plone/volto-slate": "19.0.0-alpha.14",
187
- "@plone/scripts": "4.0.0-alpha.6"
183
+ "uuid": "^14.0.0",
184
+ "@plone/registry": "3.0.0-alpha.11",
185
+ "@plone/scripts": "4.0.0-alpha.6",
186
+ "@plone/volto-slate": "19.0.0-alpha.15",
187
+ "@plone/components": "4.0.0-alpha.7"
188
188
  },
189
189
  "devDependencies": {
190
190
  "@babel/core": "^7.28.5",
@@ -194,7 +194,7 @@
194
194
  "@babel/runtime": "^7.28.4",
195
195
  "@babel/types": "7.20.5",
196
196
  "@fiverr/afterbuild-webpack-plugin": "^1.0.0",
197
- "@loadable/babel-plugin": "5.13.2",
197
+ "@loadable/babel-plugin": "5.16.1",
198
198
  "@loadable/webpack-plugin": "5.15.2",
199
199
  "@sinonjs/fake-timers": "^6.0.1",
200
200
  "@storybook/addon-actions": "^8.0.4",
@@ -208,11 +208,11 @@
208
208
  "@storybook/test": "^8.0.4",
209
209
  "@storybook/theming": "^8.0.4",
210
210
  "@testing-library/cypress": "10.1.0",
211
- "@testing-library/jest-dom": "6.4.2",
211
+ "@testing-library/jest-dom": "^6.9.1",
212
212
  "@testing-library/react": "14.3.1",
213
213
  "@testing-library/react-hooks": "8.0.1",
214
214
  "@types/history": "^4.7.11",
215
- "@types/loadable__component": "^5.13.9",
215
+ "@types/loadable__component": "^5.13.10",
216
216
  "@types/lodash": "^4.14.201",
217
217
  "@types/node": "^24",
218
218
  "@types/react": "^18",
@@ -221,7 +221,6 @@
221
221
  "@types/react-router-dom": "^5.3.3",
222
222
  "@types/react-test-renderer": "18.0.7",
223
223
  "@types/redux-mock-store": "^1.5.0",
224
- "@types/uuid": "^9.0.2",
225
224
  "@typescript-eslint/eslint-plugin": "^7.7.0",
226
225
  "@typescript-eslint/parser": "^7.7.0",
227
226
  "@vitejs/plugin-react": "^4.3.4",
@@ -249,7 +248,7 @@
249
248
  "eslint-plugin-prettier": "^5.1.3",
250
249
  "eslint-plugin-react": "^7.34.1",
251
250
  "eslint-plugin-react-hooks": "^4.6.0",
252
- "html-webpack-plugin": "5.5.0",
251
+ "html-webpack-plugin": "5.6.7",
253
252
  "identity-obj-proxy": "3.0.0",
254
253
  "jiti": "^2.4.2",
255
254
  "jsdom": "^28.1.0",
@@ -257,19 +256,18 @@
257
256
  "less": "3.13.1",
258
257
  "less-loader": "11.1.0",
259
258
  "lodash-webpack-plugin": "0.11.6",
260
- "mini-css-extract-plugin": "2.7.2",
259
+ "mini-css-extract-plugin": "2.10.2",
261
260
  "moment-locales-webpack-plugin": "1.2.0",
262
- "postcss": "8.4.31",
261
+ "postcss": "^8.5.10",
263
262
  "postcss-flexbugs-fixes": "5.0.2",
264
263
  "postcss-less": "6.0.0",
265
- "postcss-load-config": "3.1.4",
266
- "postcss-loader": "7.0.2",
267
- "postcss-overrides": "3.1.4",
268
- "postcss-scss": "4.0.6",
269
- "react-docgen-typescript-plugin": "^1.0.5",
264
+ "postcss-load-config": "^6.0.1",
265
+ "postcss-loader": "^8.2.1",
266
+ "postcss-scss": "4.0.9",
267
+ "react-docgen-typescript-plugin": "^1.0.8",
270
268
  "react-error-overlay": "6.0.9",
271
269
  "react-is": "^18.2.0",
272
- "release-it": "^19.2.4",
270
+ "release-it": "^20.0.1",
273
271
  "resolve-url-loader": "^5.0.0",
274
272
  "sass": "1.58.0",
275
273
  "sass-loader": "^10.0.3",
@@ -283,7 +281,7 @@
283
281
  "svg-loader": "0.0.2",
284
282
  "svgo": "^3.0.0",
285
283
  "svgo-loader": "3.0.3",
286
- "terser-webpack-plugin": "5.3.6",
284
+ "terser-webpack-plugin": "5.4.0",
287
285
  "ts-loader": "9.4.4",
288
286
  "typescript": "^5.7.3",
289
287
  "use-trace-update": "1.3.2",
@@ -293,11 +291,11 @@
293
291
  "webpack-bundle-analyzer": "4.10.1",
294
292
  "webpack-dev-server": "^5.2.3",
295
293
  "webpack-node-externals": "3.0.0",
296
- "@plone/types": "2.0.0-alpha.16",
297
294
  "@plone/babel-preset-razzle": "^1.0.0-alpha.0",
298
- "@plone/razzle": "1.0.0-alpha.3",
299
- "@plone/razzle-dev-utils": "1.0.0-alpha.2",
300
- "@plone/volto-coresandbox": "1.0.0"
295
+ "@plone/razzle": "1.0.0-alpha.4",
296
+ "@plone/volto-coresandbox": "1.0.0",
297
+ "@plone/types": "2.0.0-alpha.17",
298
+ "@plone/razzle-dev-utils": "1.0.0-alpha.2"
301
299
  },
302
300
  "scripts": {
303
301
  "analyze": "BUNDLE_ANALYZE=true razzle build",
@@ -44,6 +44,12 @@ export const Item = forwardRef(
44
44
  config.blocks.blocksConfig[data?.['@type']]?.icon ||
45
45
  config.blocks.blocksConfig.title?.icon;
46
46
 
47
+ const required =
48
+ typeof data?.required === 'boolean'
49
+ ? data.required
50
+ : includes(config.blocks.requiredBlocks, data?.['@type']);
51
+ const fixed = !!data?.fixed;
52
+
47
53
  return (
48
54
  <li
49
55
  className={classNames(
@@ -93,15 +99,17 @@ export const Item = forwardRef(
93
99
  ref={ref}
94
100
  style={style}
95
101
  >
96
- <button
97
- ref={ref}
98
- {...handleProps}
99
- className={classNames('action', 'drag')}
100
- tabIndex={0}
101
- data-cypress="draggable-handle"
102
- >
103
- <Icon name={dragSVG} size="16px" />
104
- </button>
102
+ {!fixed && (
103
+ <button
104
+ ref={ref}
105
+ {...handleProps}
106
+ className={classNames('action', 'drag')}
107
+ tabIndex={0}
108
+ data-cypress="draggable-handle"
109
+ >
110
+ <Icon name={dragSVG} size="16px" />
111
+ </button>
112
+ )}
105
113
  <span
106
114
  className={cx('text', {
107
115
  errored: errors && Object.keys(errors).length > 0,
@@ -118,7 +126,7 @@ export const Item = forwardRef(
118
126
  config.blocks.blocksConfig[data?.['@type']]?.title ||
119
127
  data?.title}
120
128
  </span>
121
- {!clone && onRemove && (
129
+ {!clone && onRemove && !required && (
122
130
  <button
123
131
  onClick={onRemove}
124
132
  className={classNames('action', 'delete')}
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import configureStore from 'redux-mock-store';
3
+ import { Provider } from 'react-intl-redux';
4
+ import { render } from '@testing-library/react';
5
+ import config from '@plone/volto/registry';
6
+
7
+ import { Item } from './Item';
8
+
9
+ const mockStore = configureStore();
10
+
11
+ const defaultStoreState = {
12
+ intl: {
13
+ locale: 'en',
14
+ messages: {},
15
+ },
16
+ form: {
17
+ ui: {
18
+ selected: null,
19
+ hovered: null,
20
+ multiSelected: [],
21
+ gridSelected: null,
22
+ },
23
+ },
24
+ };
25
+
26
+ const renderItem = (data = {}) => {
27
+ const store = mockStore(defaultStoreState);
28
+
29
+ return render(
30
+ <Provider store={store}>
31
+ <Item
32
+ id="title-block-id"
33
+ data={{ '@type': 'title', ...data }}
34
+ depth={0}
35
+ indentationWidth={25}
36
+ onRemove={() => {}}
37
+ onSelectBlock={() => {}}
38
+ handleProps={{}}
39
+ />
40
+ </Provider>,
41
+ );
42
+ };
43
+
44
+ describe('Order Item', () => {
45
+ let requiredBlocks;
46
+
47
+ beforeEach(() => {
48
+ requiredBlocks = [...(config.blocks.requiredBlocks || [])];
49
+ config.blocks.requiredBlocks = [];
50
+ });
51
+
52
+ afterEach(() => {
53
+ config.blocks.requiredBlocks = requiredBlocks;
54
+ });
55
+
56
+ test('renders drag and delete actions for movable and removable blocks', () => {
57
+ const { container } = renderItem();
58
+
59
+ expect(container.querySelector('.action.drag')).not.toBeNull();
60
+ expect(container.querySelector('.action.delete')).not.toBeNull();
61
+ });
62
+
63
+ test('hides delete action for required blocks', () => {
64
+ const { container } = renderItem({ required: true });
65
+
66
+ expect(container.querySelector('.action.delete')).toBeNull();
67
+ });
68
+
69
+ test('hides delete action for block types configured as required', () => {
70
+ config.blocks.requiredBlocks = ['title'];
71
+
72
+ const { container } = renderItem();
73
+
74
+ expect(container.querySelector('.action.delete')).toBeNull();
75
+ });
76
+
77
+ test('allows explicit required=false to override required block types', () => {
78
+ config.blocks.requiredBlocks = ['title'];
79
+
80
+ const { container } = renderItem({ required: false });
81
+
82
+ expect(container.querySelector('.action.delete')).not.toBeNull();
83
+ });
84
+
85
+ test('hides drag action for fixed blocks', () => {
86
+ const { container } = renderItem({ fixed: true });
87
+
88
+ expect(container.querySelector('.action.drag')).toBeNull();
89
+ });
90
+ });
@@ -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 } from 'react-router-dom';
4
5
  import { defineMessages, useIntl } from 'react-intl';
5
6
  import { useSelector } from 'react-redux';
@@ -43,13 +44,13 @@ const ContentsBreadcrumbs = (props) => {
43
44
  <ContentsBreadcrumbsRootItem />
44
45
  </Link>
45
46
  <Breadcrumb.Divider />
46
- <Link
47
- to={`${navroot?.['@id']}/contents`}
47
+ <UniversalLink
48
+ href={`${navroot?.['@id']}/contents`}
48
49
  className="section"
49
50
  title={navroot?.title}
50
51
  >
51
52
  {navroot?.title}
52
- </Link>
53
+ </UniversalLink>
53
54
  </>
54
55
  )}
55
56
  {items.map((breadcrumb, index, breadcrumbs) => [
@@ -14,6 +14,9 @@ vi.mock('../../Toolbar/Toolbar', () => ({
14
14
  describe('UserGroupMembershipControlPanel', () => {
15
15
  it('renders a user group membership control component', () => {
16
16
  const store = mockStore({
17
+ userSession: {
18
+ token: '1234',
19
+ },
17
20
  controlpanels: {
18
21
  controlpanel: {
19
22
  data: {
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import { defineMessages, useIntl } from 'react-intl';
3
4
  import Icon from '@plone/volto/components/theme/Icon/Icon';
4
5
 
5
6
  import successSVG from '@plone/volto/icons/ready.svg';
@@ -7,7 +8,28 @@ import infoSVG from '@plone/volto/icons/info.svg';
7
8
  import errorSVG from '@plone/volto/icons/error.svg';
8
9
  import warningSVG from '@plone/volto/icons/warning.svg';
9
10
 
11
+ const messages = defineMessages({
12
+ success: {
13
+ id: 'toast_type_success',
14
+ defaultMessage: 'Success',
15
+ },
16
+ error: {
17
+ id: 'toast_type_error',
18
+ defaultMessage: 'Error',
19
+ },
20
+ warning: {
21
+ id: 'toast_type_warning',
22
+ defaultMessage: 'Warning',
23
+ },
24
+ info: {
25
+ id: 'toast_type_info',
26
+ defaultMessage: 'Information',
27
+ },
28
+ });
29
+
10
30
  const Toast = (props) => {
31
+ const intl = useIntl();
32
+
11
33
  function getIcon(props) {
12
34
  if (props.info) {
13
35
  return infoSVG;
@@ -22,12 +44,22 @@ const Toast = (props) => {
22
44
  }
23
45
  }
24
46
 
47
+ function getTypeLabel(props) {
48
+ if (props.error) return intl.formatMessage(messages.error);
49
+ if (props.warning) return intl.formatMessage(messages.warning);
50
+ if (props.info) return intl.formatMessage(messages.info);
51
+ if (props.success) return intl.formatMessage(messages.success);
52
+ return null;
53
+ }
54
+
25
55
  const { title, content } = props;
56
+ const typeLabel = getTypeLabel(props);
26
57
 
27
58
  return (
28
59
  <>
29
60
  <Icon name={getIcon(props)} size="18px" />
30
61
  <div className="toast-inner-content">
62
+ {typeLabel && <span className="visually-hidden">{typeLabel}</span>}
31
63
  {title && <h4>{title}</h4>}
32
64
  <div>{content}</div>
33
65
  </div>
@@ -1,14 +1,18 @@
1
1
  import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
+ import { IntlProvider } from 'react-intl';
3
4
  import Toast from './Toast';
4
5
 
5
6
  test('renders a Toast info component', () => {
6
7
  const component = renderer.create(
7
- <Toast
8
- info
9
- title="I'm a title"
10
- content="This is the content, lorem ipsum"
11
- />,
8
+ <IntlProvider locale="en">
9
+ <Toast
10
+ info
11
+ title="I'm a title"
12
+ content="This is the content, lorem ipsum"
13
+ />
14
+ ,
15
+ </IntlProvider>,
12
16
  );
13
17
  const json = component.toJSON();
14
18
  expect(json).toMatchSnapshot();
@@ -2,7 +2,6 @@
2
2
  * UrlWidget component.
3
3
  * @module components/manage/Widgets/UrlWidget
4
4
  */
5
-
6
5
  import React, { useState } from 'react';
7
6
  import PropTypes from 'prop-types';
8
7
  import { Input, Button } from 'semantic-ui-react';
@@ -14,10 +13,10 @@ import {
14
13
  flattenToAppURL,
15
14
  URLUtils,
16
15
  } from '@plone/volto/helpers/Url/Url';
16
+ import { defineMessages, useIntl } from 'react-intl';
17
17
  import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
18
18
  import clearSVG from '@plone/volto/icons/clear.svg';
19
19
  import navTreeSVG from '@plone/volto/icons/nav.svg';
20
-
21
20
  /** Widget to edit urls
22
21
  *
23
22
  * This is the default widget used for the `remoteUrl` field. You can also use
@@ -30,6 +29,24 @@ import navTreeSVG from '@plone/volto/icons/nav.svg';
30
29
  * }
31
30
  * ```
32
31
  */
32
+ const messages = defineMessages({
33
+ urlMissing: {
34
+ id: 'URL is missing',
35
+ defaultMessage: 'URL is missing',
36
+ },
37
+ urlInvalid: {
38
+ id: 'URL is invalid',
39
+ defaultMessage: 'URL is invalid',
40
+ },
41
+ clearUrl: {
42
+ id: 'Clear URL',
43
+ defaultMessage: 'Clear URL',
44
+ },
45
+ openUrlBrowser: {
46
+ id: 'Open URL browser',
47
+ defaultMessage: 'Open URL browser',
48
+ },
49
+ });
33
50
  export const UrlWidget = (props) => {
34
51
  const {
35
52
  id,
@@ -40,9 +57,10 @@ export const UrlWidget = (props) => {
40
57
  maxLength,
41
58
  placeholder,
42
59
  isDisabled,
60
+ required,
43
61
  } = props;
44
62
  const inputId = `field-${id}`;
45
-
63
+ const intl = useIntl();
46
64
  const [value, setValue] = useState(flattenToAppURL(props.value));
47
65
  const [isInvalid, setIsInvalid] = useState(false);
48
66
  /**
@@ -54,24 +72,20 @@ export const UrlWidget = (props) => {
54
72
  const clear = () => {
55
73
  setValue('');
56
74
  onChange(id, undefined);
75
+ setIsInvalid(false);
57
76
  };
58
-
59
77
  const onChangeValue = (_value) => {
60
78
  let newValue = _value;
61
79
  if (newValue?.length > 0) {
62
80
  if (isInvalid && URLUtils.isUrl(URLUtils.normalizeUrl(newValue))) {
63
81
  setIsInvalid(false);
64
82
  }
65
-
66
83
  if (isInternalURL(newValue)) {
67
84
  newValue = flattenToAppURL(newValue);
68
85
  }
69
86
  }
70
-
71
87
  setValue(newValue);
72
-
73
88
  newValue = isInternalURL(newValue) ? addAppURL(newValue) : newValue;
74
-
75
89
  if (!isInternalURL(newValue) && newValue.length > 0) {
76
90
  const checkedURL = URLUtils.checkAndNormalizeUrl(newValue);
77
91
  newValue = checkedURL.url;
@@ -79,10 +93,15 @@ export const UrlWidget = (props) => {
79
93
  setIsInvalid(true);
80
94
  }
81
95
  }
82
-
83
96
  onChange(id, newValue === '' ? undefined : newValue);
84
97
  };
85
-
98
+ // A11y: if the field is required and the user leaves it empty, we mark it as missing
99
+ const handleBlur = ({ target }) => {
100
+ if (required && (!target.value || target.value === '')) {
101
+ setIsInvalid(true);
102
+ }
103
+ onBlur(id, target.value === '' ? undefined : target.value);
104
+ };
86
105
  return (
87
106
  <FormFieldWrapper {...props} className="url wide">
88
107
  <div className="wrapper">
@@ -90,25 +109,38 @@ export const UrlWidget = (props) => {
90
109
  id={inputId}
91
110
  name={id}
92
111
  type="url"
112
+ required={required}
113
+ aria-required={required}
114
+ aria-invalid={isInvalid}
115
+ aria-errormessage={isInvalid ? `${inputId}-error` : undefined}
116
+ onBlur={handleBlur}
93
117
  value={value || ''}
94
118
  disabled={isDisabled}
95
119
  placeholder={placeholder}
96
120
  onChange={({ target }) => onChangeValue(target.value)}
97
- onBlur={({ target }) =>
98
- onBlur(id, target.value === '' ? undefined : target.value)
99
- }
100
121
  onClick={() => onClick()}
101
122
  minLength={minLength || null}
102
123
  maxLength={maxLength || null}
103
124
  error={isInvalid}
104
125
  />
126
+ {isInvalid && (
127
+ <span
128
+ id={`${inputId}-error`}
129
+ role="alert"
130
+ className="visually-hidden"
131
+ >
132
+ {value?.length > 0
133
+ ? intl.formatMessage(messages.urlInvalid)
134
+ : intl.formatMessage(messages.urlMissing)}
135
+ </span>
136
+ )}
105
137
  {value?.length > 0 ? (
106
138
  <Button.Group>
107
139
  <Button
108
140
  type="button"
109
141
  basic
110
142
  className="cancel"
111
- aria-label="clearUrlBrowser"
143
+ aria-label={intl.formatMessage(messages.clearUrl)}
112
144
  onClick={(e) => {
113
145
  e.preventDefault();
114
146
  e.stopPropagation();
@@ -124,7 +156,7 @@ export const UrlWidget = (props) => {
124
156
  type="button"
125
157
  basic
126
158
  icon
127
- aria-label="openUrlBrowser"
159
+ aria-label={intl.formatMessage(messages.openUrlBrowser)}
128
160
  onClick={(e) => {
129
161
  e.preventDefault();
130
162
  e.stopPropagation();
@@ -145,7 +177,6 @@ export const UrlWidget = (props) => {
145
177
  </FormFieldWrapper>
146
178
  );
147
179
  };
148
-
149
180
  /**
150
181
  * Property types
151
182
  * @property {Object} propTypes Property types.
@@ -166,7 +197,6 @@ UrlWidget.propTypes = {
166
197
  openObjectBrowser: PropTypes.func.isRequired,
167
198
  placeholder: PropTypes.string,
168
199
  };
169
-
170
200
  /**
171
201
  * Default properties.
172
202
  * @property {Object} defaultProps Default properties.
@@ -183,5 +213,4 @@ UrlWidget.defaultProps = {
183
213
  minLength: null,
184
214
  maxLength: null,
185
215
  };
186
-
187
216
  export default withObjectBrowser(UrlWidget);
@@ -6,6 +6,7 @@ const AlternateHrefLangs = (props) => {
6
6
  return (
7
7
  <Helmet>
8
8
  {content['@components']?.translations?.items &&
9
+ content.language?.token &&
9
10
  [
10
11
  ...content['@components']?.translations?.items,
11
12
  { '@id': content['@id'], language: content.language.token },
@@ -35,6 +35,36 @@ describe('AlternateHrefLangs', () => {
35
35
  expect(helmetLinks.length).toBe(0);
36
36
  });
37
37
 
38
+ it('multilingual site, content without language field, renders nothing', () => {
39
+ config.settings.publicURL = 'https://plone.org';
40
+ config.settings.supportedLanguages = ['en', 'es'];
41
+
42
+ const content = {
43
+ '@id': 'http://localhost:8080/Plone/en/newsroom/news',
44
+ '@components': {
45
+ translations: {
46
+ items: [{ '@id': 'http://localhost:8080/Plone/es', language: 'es' }],
47
+ },
48
+ },
49
+ };
50
+
51
+ const store = mockStore({
52
+ intl: {
53
+ locale: 'en',
54
+ messages: {},
55
+ },
56
+ });
57
+
58
+ renderer.create(
59
+ <Provider store={store}>
60
+ <AlternateHrefLangs content={content} />
61
+ </Provider>,
62
+ );
63
+
64
+ const helmetLinks = Helmet.peek().linkTags;
65
+ expect(helmetLinks.length).toBe(0);
66
+ });
67
+
38
68
  it('multilingual site, with some translations', () => {
39
69
  config.settings.publicURL = 'https://plone.org';
40
70
  config.settings.supportedLanguages = ['en', 'es', 'eu'];