@plone/volto 18.32.1 → 18.32.3

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 (167) hide show
  1. package/.eslintignore +1 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +0 -4
  4. package/locales/af/LC_MESSAGES/volto.po +20 -0
  5. package/locales/af.json +1 -1
  6. package/locales/ar/LC_MESSAGES/volto.po +20 -0
  7. package/locales/ar.json +1 -1
  8. package/locales/bg/LC_MESSAGES/volto.po +20 -0
  9. package/locales/bg.json +1 -1
  10. package/locales/bn/LC_MESSAGES/volto.po +20 -0
  11. package/locales/bn.json +1 -1
  12. package/locales/ca/LC_MESSAGES/volto.po +20 -0
  13. package/locales/ca.json +1 -1
  14. package/locales/cs/LC_MESSAGES/volto.po +20 -0
  15. package/locales/cs.json +1 -1
  16. package/locales/cy/LC_MESSAGES/volto.po +20 -0
  17. package/locales/cy.json +1 -1
  18. package/locales/da/LC_MESSAGES/volto.po +20 -0
  19. package/locales/da.json +1 -1
  20. package/locales/de/LC_MESSAGES/volto.po +20 -0
  21. package/locales/de.json +1 -1
  22. package/locales/el/LC_MESSAGES/volto.po +20 -0
  23. package/locales/el.json +1 -1
  24. package/locales/en/LC_MESSAGES/volto.po +20 -0
  25. package/locales/en.json +1 -1
  26. package/locales/en_AU/LC_MESSAGES/volto.po +20 -0
  27. package/locales/en_AU.json +1 -1
  28. package/locales/en_GB/LC_MESSAGES/volto.po +20 -0
  29. package/locales/en_GB.json +1 -1
  30. package/locales/eo/LC_MESSAGES/volto.po +20 -0
  31. package/locales/eo.json +1 -1
  32. package/locales/es/LC_MESSAGES/volto.po +20 -0
  33. package/locales/es.json +1 -1
  34. package/locales/et/LC_MESSAGES/volto.po +20 -0
  35. package/locales/et.json +1 -1
  36. package/locales/eu/LC_MESSAGES/volto.po +20 -0
  37. package/locales/eu.json +1 -1
  38. package/locales/fa/LC_MESSAGES/volto.po +20 -0
  39. package/locales/fa.json +1 -1
  40. package/locales/fi/LC_MESSAGES/volto.po +20 -0
  41. package/locales/fi.json +1 -1
  42. package/locales/fr/LC_MESSAGES/volto.po +20 -0
  43. package/locales/fr.json +1 -1
  44. package/locales/fu/LC_MESSAGES/volto.po +20 -0
  45. package/locales/fu.json +1 -1
  46. package/locales/gl/LC_MESSAGES/volto.po +20 -0
  47. package/locales/gl.json +1 -1
  48. package/locales/he/LC_MESSAGES/volto.po +20 -0
  49. package/locales/he.json +1 -1
  50. package/locales/hi/LC_MESSAGES/volto.po +20 -0
  51. package/locales/hi.json +1 -1
  52. package/locales/hr/LC_MESSAGES/volto.po +20 -0
  53. package/locales/hr.json +1 -1
  54. package/locales/hu/LC_MESSAGES/volto.po +20 -0
  55. package/locales/hu.json +1 -1
  56. package/locales/hy/LC_MESSAGES/volto.po +20 -0
  57. package/locales/hy.json +1 -1
  58. package/locales/id/LC_MESSAGES/volto.po +20 -0
  59. package/locales/id.json +1 -1
  60. package/locales/it/LC_MESSAGES/volto.po +31 -11
  61. package/locales/it.json +1 -1
  62. package/locales/ja/LC_MESSAGES/volto.po +20 -0
  63. package/locales/ja.json +1 -1
  64. package/locales/ka/LC_MESSAGES/volto.po +20 -0
  65. package/locales/ka.json +1 -1
  66. package/locales/kn/LC_MESSAGES/volto.po +20 -0
  67. package/locales/kn.json +1 -1
  68. package/locales/ko/LC_MESSAGES/volto.po +20 -0
  69. package/locales/ko.json +1 -1
  70. package/locales/lt/LC_MESSAGES/volto.po +20 -0
  71. package/locales/lt.json +1 -1
  72. package/locales/lv/LC_MESSAGES/volto.po +20 -0
  73. package/locales/lv.json +1 -1
  74. package/locales/mi/LC_MESSAGES/volto.po +20 -0
  75. package/locales/mi.json +1 -1
  76. package/locales/mk/LC_MESSAGES/volto.po +20 -0
  77. package/locales/mk.json +1 -1
  78. package/locales/my/LC_MESSAGES/volto.po +20 -0
  79. package/locales/my.json +1 -1
  80. package/locales/nb_NO/LC_MESSAGES/volto.po +20 -0
  81. package/locales/nb_NO.json +1 -1
  82. package/locales/nl/LC_MESSAGES/volto.po +20 -0
  83. package/locales/nl.json +1 -1
  84. package/locales/nn/LC_MESSAGES/volto.po +20 -0
  85. package/locales/nn.json +1 -1
  86. package/locales/pl/LC_MESSAGES/volto.po +20 -0
  87. package/locales/pl.json +1 -1
  88. package/locales/pt/LC_MESSAGES/volto.po +20 -0
  89. package/locales/pt.json +1 -1
  90. package/locales/pt_BR/LC_MESSAGES/volto.po +20 -0
  91. package/locales/pt_BR.json +1 -1
  92. package/locales/rm/LC_MESSAGES/volto.po +20 -0
  93. package/locales/rm.json +1 -1
  94. package/locales/ro/LC_MESSAGES/volto.po +20 -0
  95. package/locales/ro.json +1 -1
  96. package/locales/ru/LC_MESSAGES/volto.po +20 -0
  97. package/locales/ru.json +1 -1
  98. package/locales/sk/LC_MESSAGES/volto.po +20 -0
  99. package/locales/sk.json +1 -1
  100. package/locales/sl/LC_MESSAGES/volto.po +20 -0
  101. package/locales/sl.json +1 -1
  102. package/locales/sm/LC_MESSAGES/volto.po +20 -0
  103. package/locales/sm.json +1 -1
  104. package/locales/sq/LC_MESSAGES/volto.po +20 -0
  105. package/locales/sq.json +1 -1
  106. package/locales/sr/LC_MESSAGES/volto.po +20 -0
  107. package/locales/sr.json +1 -1
  108. package/locales/sr@cyrl/LC_MESSAGES/volto.po +20 -0
  109. package/locales/sr@cyrl.json +1 -1
  110. package/locales/sr@latn/LC_MESSAGES/volto.po +20 -0
  111. package/locales/sr@latn.json +1 -1
  112. package/locales/sv/LC_MESSAGES/volto.po +20 -0
  113. package/locales/sv.json +1 -1
  114. package/locales/ta/LC_MESSAGES/volto.po +20 -0
  115. package/locales/ta.json +1 -1
  116. package/locales/te/LC_MESSAGES/volto.po +20 -0
  117. package/locales/te.json +1 -1
  118. package/locales/th/LC_MESSAGES/volto.po +20 -0
  119. package/locales/th.json +1 -1
  120. package/locales/to/LC_MESSAGES/volto.po +20 -0
  121. package/locales/to.json +1 -1
  122. package/locales/tr/LC_MESSAGES/volto.po +20 -0
  123. package/locales/tr.json +1 -1
  124. package/locales/uk/LC_MESSAGES/volto.po +20 -0
  125. package/locales/uk.json +1 -1
  126. package/locales/vi/LC_MESSAGES/volto.po +20 -0
  127. package/locales/vi.json +1 -1
  128. package/locales/volto.pot +21 -1
  129. package/locales/zh_CN/LC_MESSAGES/volto.po +20 -0
  130. package/locales/zh_CN.json +1 -1
  131. package/locales/zh_Hant/LC_MESSAGES/volto.po +20 -0
  132. package/locales/zh_Hant.json +1 -1
  133. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +20 -0
  134. package/locales/zh_Hant_HK.json +1 -1
  135. package/package.json +6 -5
  136. package/src/components/manage/Blocks/Block/BlocksForm.jsx +10 -7
  137. package/src/components/manage/Blocks/Block/BlocksForm.test.jsx +2 -9
  138. package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +10 -1
  139. package/src/components/manage/Blocks/Block/Order/Item.jsx +9 -3
  140. package/src/components/manage/Blocks/Block/Order/Order.jsx +116 -67
  141. package/src/components/manage/Blocks/Block/Order/utilities.js +28 -11
  142. package/src/components/manage/Blocks/Listing/Edit.jsx +1 -0
  143. package/src/components/manage/Controlpanels/ContentTypeSchema.jsx +1 -1
  144. package/src/components/manage/Sharing/Sharing.jsx +10 -12
  145. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +5 -1
  146. package/src/components/manage/UniversalLink/UniversalLink.test.jsx +16 -0
  147. package/src/components/manage/UniversalLink/UniversalLink.tsx +1 -0
  148. package/src/components/theme/App/App.jsx +3 -1
  149. package/src/components/theme/ConnectionRefused/ConnectionRefused.jsx +3 -2
  150. package/src/components/theme/PasswordReset/PasswordReset.jsx +108 -191
  151. package/src/components/theme/View/RenderBlocks.jsx +8 -10
  152. package/src/components/theme/View/RenderBlocks.test.jsx +14 -4
  153. package/src/config/index.js +1 -1
  154. package/src/config/validation.ts +8 -0
  155. package/src/helpers/Blocks/Blocks.js +109 -24
  156. package/src/helpers/Blocks/Blocks.test.js +100 -0
  157. package/src/helpers/FormValidation/FormValidation.test.js +47 -0
  158. package/src/helpers/FormValidation/validators.ts +37 -4
  159. package/src/helpers/MessageLabels/MessageLabels.js +5 -0
  160. package/types/components/manage/Blocks/Block/Order/utilities.d.ts +2 -1
  161. package/types/components/theme/ConnectionRefused/ConnectionRefused.d.ts +2 -2
  162. package/types/components/theme/PasswordReset/PasswordReset.d.ts +6 -2
  163. package/types/config/Views.d.ts +1 -1
  164. package/types/helpers/Blocks/Blocks.d.ts +4 -0
  165. package/types/helpers/FormValidation/validators.d.ts +18 -1
  166. package/types/helpers/MessageLabels/MessageLabels.d.ts +100 -94
  167. package/types/routes.d.ts +7 -5
@@ -3,14 +3,12 @@
3
3
  * @module components/theme/PasswordReset/PasswordReset
4
4
  */
5
5
 
6
- import React, { Component } from 'react';
7
- import PropTypes from 'prop-types';
8
- import { connect } from 'react-redux';
9
- import { compose } from 'redux';
10
- import { Link, withRouter } from 'react-router-dom';
6
+ import { useState, useEffect } from 'react';
7
+ import { useSelector, useDispatch } from 'react-redux';
8
+ import { Link, useHistory, useParams } from 'react-router-dom';
11
9
  import Helmet from '@plone/volto/helpers/Helmet/Helmet';
12
10
  import { Container } from 'semantic-ui-react';
13
- import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
11
+ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
14
12
 
15
13
  import { Form } from '@plone/volto/components/manage/Form';
16
14
  import { setInitialPassword } from '@plone/volto/actions/users/users';
@@ -90,220 +88,139 @@ const messages = defineMessages({
90
88
  });
91
89
 
92
90
  /**
93
- * PasswordReset class.
94
- * @class PasswordReset
95
- * @extends Component
91
+ * @function PasswordReset
92
+ * @returns {JSX.Element}
96
93
  */
97
- class PasswordReset extends Component {
98
- /**
99
- * Property types.
100
- * @property {Object} propTypes Property types.
101
- * @static
102
- */
103
- static propTypes = {
104
- loading: PropTypes.bool.isRequired,
105
- loaded: PropTypes.bool.isRequired,
106
- error: PropTypes.string,
107
- token: PropTypes.string.isRequired,
108
- setInitialPassword: PropTypes.func.isRequired,
109
- };
94
+ function PasswordReset() {
95
+ const dispatch = useDispatch();
96
+ const history = useHistory();
97
+ const { token } = useParams();
110
98
 
111
- /**
112
- * Default properties.
113
- * @property {Object} defaultProps Default properties.
114
- * @static
115
- */
116
- static defaultProps = {
117
- error: null,
118
- };
99
+ const loading = useSelector((state) => state.users.initial.loading);
100
+ const loaded = useSelector((state) => state.users.initial.loaded);
101
+ const error = useSelector((state) => state.users.initial.error);
119
102
 
120
- /**
121
- * Constructor
122
- * @method constructor
123
- * @param {Object} props Component properties
124
- * @constructs Controlpanel
125
- */
126
- constructor(props) {
127
- super(props);
128
- this.onCancel = this.onCancel.bind(this);
129
- this.onSubmit = this.onSubmit.bind(this);
130
- this.state = {
131
- error: null,
132
- isSuccessful: false,
133
- };
103
+ const [localError, setLocalError] = useState(null);
104
+ const [isSuccessful, setIsSuccessful] = useState(false);
105
+ const intl = useIntl();
134
106
 
135
- this.identifierField = config.settings.useEmailAsLogin
136
- ? 'email'
137
- : 'username';
107
+ const identifierField = config.settings.useEmailAsLogin
108
+ ? 'email'
109
+ : 'username';
138
110
 
139
- this.identifierTitle =
140
- this.identifierField === 'email'
141
- ? this.props.intl.formatMessage(messages.emailTitle)
142
- : this.props.intl.formatMessage(messages.usernameTitle);
111
+ const identifierTitle =
112
+ identifierField === 'email'
113
+ ? intl.formatMessage(messages.emailTitle)
114
+ : intl.formatMessage(messages.usernameTitle);
143
115
 
144
- this.identifierDescription =
145
- this.identifierField === 'email'
146
- ? this.props.intl.formatMessage(messages.emailDescription)
147
- : this.props.intl.formatMessage(messages.usernameDescription);
148
- }
116
+ const identifierDescription =
117
+ identifierField === 'email'
118
+ ? intl.formatMessage(messages.emailDescription)
119
+ : intl.formatMessage(messages.usernameDescription);
149
120
 
150
- /**
151
- * Component will receive props
152
- * @method componentWillReceiveProps
153
- * @param {Object} nextProps Next properties
154
- * @returns {undefined}
155
- */
156
- UNSAFE_componentWillReceiveProps(nextProps) {
157
- if (this.props.loading && nextProps.loaded) {
158
- this.setState({ isSuccessful: true });
121
+ useEffect(() => {
122
+ if (!loading && loaded) {
123
+ setIsSuccessful(true);
159
124
  }
160
- }
125
+ }, [loading, loaded]);
161
126
 
162
127
  /**
163
128
  * Submit handler
164
129
  * @method onSubmit
165
130
  * @param {object} data Form data.
166
- * @param {object} event Form data.
167
131
  * @returns {undefined}
168
132
  */
169
- onSubmit(data) {
133
+ const onSubmit = (data) => {
170
134
  if (data.password === data.passwordRepeat) {
171
- this.props.setInitialPassword(
172
- data[this.identifierField],
173
- this.props.token,
174
- data.password,
175
- );
176
- this.setState({
177
- error: null,
178
- });
135
+ dispatch(setInitialPassword(data[identifierField], token, data.password));
136
+ setLocalError(null);
179
137
  } else {
180
- this.setState({
181
- error: {
182
- message: this.props.intl.formatMessage(messages.passwordsDoNotMatch),
183
- },
138
+ setLocalError({
139
+ message: intl.formatMessage(messages.passwordsDoNotMatch),
184
140
  });
185
141
  }
186
- }
142
+ };
187
143
 
188
144
  /**
189
145
  * Cancel handler
190
146
  * @method onCancel
191
147
  * @returns {undefined}
192
148
  */
193
- onCancel() {
194
- this.props.history.goBack();
149
+ const onCancel = () => {
150
+ history.goBack();
151
+ };
152
+
153
+ if (isSuccessful) {
154
+ return (
155
+ <Container>
156
+ <h1 className="documentFirstHeading">
157
+ <FormattedMessage {...messages.successRedirectToLoginTitle} />
158
+ </h1>
159
+ <p className="description">
160
+ <FormattedMessage
161
+ {...messages.successRedirectToLoginBody}
162
+ values={{
163
+ link: (
164
+ <Link to="/login">{intl.formatMessage({ id: 'Log In' })}</Link>
165
+ ),
166
+ }}
167
+ />
168
+ </p>
169
+ </Container>
170
+ );
195
171
  }
196
172
 
197
- /**
198
- * Render method.
199
- * @method render
200
- * @returns {string} Markup for the component.
201
- */
202
- render() {
203
- if (this.state.isSuccessful) {
204
- return (
173
+ if (token) {
174
+ const errmsg = error ? error.response?.body?.error || error : null;
175
+ return (
176
+ <div id="page-password-reset">
177
+ <Helmet title={intl.formatMessage(messages.passwordReset)} />
205
178
  <Container>
206
- <h1 className="documentFirstHeading">
207
- <FormattedMessage
208
- id="Account activation completed"
209
- defaultMessage="Account activation completed"
210
- />
211
- </h1>
212
- <p className="description">
213
- <FormattedMessage
214
- id="Your password has been set successfully. You may now {link} with your new password."
215
- defaultMessage="Your password has been set successfully. You may now {link} with your new password."
216
- values={{
217
- link: (
218
- <Link to="/login">
219
- {this.props.intl.formatMessage({ id: 'Log In' })}
220
- </Link>
221
- ),
222
- }}
223
- />
224
- </p>
225
- </Container>
226
- );
227
- }
228
- if (this.props.token) {
229
- const errmsg = this.props.error
230
- ? this.props.error.response.body.error
231
- : null;
232
- return (
233
- <div id="page-password-reset">
234
- <Helmet
235
- title={this.props.intl.formatMessage(messages.passwordReset)}
236
- />
237
- <Container>
238
- <Form
239
- title={this.props.intl.formatMessage(messages.title)}
240
- description={this.props.intl.formatMessage(messages.description)}
241
- onSubmit={this.onSubmit}
242
- onCancel={this.onCancel}
243
- error={this.state.error || errmsg}
244
- schema={{
245
- fieldsets: [
246
- {
247
- id: 'default',
248
- title: this.props.intl.formatMessage(messages.default),
249
- fields: [
250
- this.identifierField,
251
- 'password',
252
- 'passwordRepeat',
253
- ],
254
- },
255
- ],
256
- properties: {
257
- [this.identifierField]: {
258
- type: 'string',
259
- title: this.identifierTitle,
260
- description: this.identifierDescription,
261
- },
262
- password: {
263
- description: this.props.intl.formatMessage(
264
- messages.passwordDescription,
265
- ),
266
- title: this.props.intl.formatMessage(
267
- messages.passwordTitle,
268
- ),
269
- type: 'string',
270
- widget: 'password',
271
- },
272
- passwordRepeat: {
273
- description: this.props.intl.formatMessage(
274
- messages.passwordRepeatDescription,
275
- ),
276
- title: this.props.intl.formatMessage(
277
- messages.passwordRepeatTitle,
278
- ),
279
- type: 'string',
280
- widget: 'password',
281
- },
179
+ <Form
180
+ title={intl.formatMessage(messages.title)}
181
+ description={intl.formatMessage(messages.description)}
182
+ onSubmit={onSubmit}
183
+ onCancel={onCancel}
184
+ error={localError || errmsg}
185
+ schema={{
186
+ fieldsets: [
187
+ {
188
+ id: 'default',
189
+ title: intl.formatMessage(messages.default),
190
+ fields: [identifierField, 'password', 'passwordRepeat'],
282
191
  },
283
- submitLabel: this.props.intl.formatMessage(
284
- messages.setMyPassword,
285
- ),
286
- required: [this.identifierField, 'password', 'passwordRepeat'],
287
- }}
288
- />
289
- </Container>
290
- </div>
291
- );
292
- }
293
- return <div />;
192
+ ],
193
+ properties: {
194
+ [identifierField]: {
195
+ type: 'string',
196
+ title: identifierTitle,
197
+ description: identifierDescription,
198
+ },
199
+ password: {
200
+ description: intl.formatMessage(messages.passwordDescription),
201
+ title: intl.formatMessage(messages.passwordTitle),
202
+ type: 'string',
203
+ widget: 'password',
204
+ },
205
+ passwordRepeat: {
206
+ description: intl.formatMessage(
207
+ messages.passwordRepeatDescription,
208
+ ),
209
+ title: intl.formatMessage(messages.passwordRepeatTitle),
210
+ type: 'string',
211
+ widget: 'password',
212
+ },
213
+ },
214
+ submitLabel: intl.formatMessage(messages.setMyPassword),
215
+ required: [identifierField, 'password', 'passwordRepeat'],
216
+ }}
217
+ />
218
+ </Container>
219
+ </div>
220
+ );
294
221
  }
222
+
223
+ return <div />;
295
224
  }
296
225
 
297
- export default compose(
298
- withRouter,
299
- injectIntl,
300
- connect(
301
- (state, props) => ({
302
- loading: state.users.initial.loading,
303
- loaded: state.users.initial.loaded,
304
- error: state.users.initial.error,
305
- token: props.match.params.token,
306
- }),
307
- { setInitialPassword },
308
- ),
309
- )(PasswordReset);
226
+ export default PasswordReset;
@@ -5,8 +5,7 @@ import map from 'lodash/map';
5
5
  import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
6
6
  import {
7
7
  applyBlockDefaults,
8
- getBlocksFieldname,
9
- getBlocksLayoutFieldname,
8
+ getBlocks,
10
9
  hasBlocksData,
11
10
  } from '@plone/volto/helpers/Blocks/Blocks';
12
11
  import StyleWrapper from '@plone/volto/components/manage/Blocks/Block/StyleWrapper';
@@ -28,26 +27,25 @@ const messages = defineMessages({
28
27
  const RenderBlocks = (props) => {
29
28
  const { blockWrapperTag, content, location, isContainer, metadata } = props;
30
29
  const intl = useIntl();
31
- const blocksFieldname = getBlocksFieldname(content);
32
- const blocksLayoutFieldname = getBlocksLayoutFieldname(content);
33
30
  const blocksConfig = props.blocksConfig || config.blocks.blocksConfig;
34
31
  const CustomTag = props.as || React.Fragment;
35
32
 
33
+ const blockList = getBlocks(content);
34
+
36
35
  return hasBlocksData(content) ? (
37
36
  <CustomTag>
38
- {map(content[blocksLayoutFieldname].items, (block) => {
37
+ {map(blockList, ([block, rawBlockData]) => {
39
38
  const Block =
40
- blocksConfig[content[blocksFieldname]?.[block]?.['@type']]?.view ||
41
- ViewDefaultBlock;
39
+ blocksConfig[rawBlockData?.['@type']]?.view || ViewDefaultBlock;
42
40
 
43
41
  const blockData = applyBlockDefaults({
44
- data: content[blocksFieldname][block],
42
+ data: rawBlockData,
45
43
  intl,
46
44
  metadata,
47
45
  properties: content,
48
46
  });
49
47
 
50
- if (content[blocksFieldname]?.[block]?.['@type'] === 'empty') {
48
+ if (rawBlockData?.['@type'] === 'empty') {
51
49
  return (
52
50
  <MaybeWrap
53
51
  key={block}
@@ -91,7 +89,7 @@ const RenderBlocks = (props) => {
91
89
  return (
92
90
  <div key={block}>
93
91
  {intl.formatMessage(messages.unknownBlock, {
94
- block: content[blocksFieldname]?.[block]?.['@type'],
92
+ block: rawBlockData?.['@type'],
95
93
  })}
96
94
  </div>
97
95
  );
@@ -90,14 +90,14 @@ test('Provides path to blocks', () => {
90
90
  expect(container).toMatchSnapshot();
91
91
  });
92
92
 
93
- test('Renders invalid blocks', () => {
93
+ test('Filters out invalid blocks', () => {
94
94
  const store = mockStore({
95
95
  intl: {
96
96
  locale: 'en',
97
97
  messages: {},
98
98
  },
99
99
  });
100
- const { queryAllByText } = render(
100
+ const { queryAllByText, queryByText } = render(
101
101
  <Provider store={store}>
102
102
  <RenderBlocks
103
103
  blocksConfig={{
@@ -113,7 +113,14 @@ test('Renders invalid blocks', () => {
113
113
  }}
114
114
  content={{
115
115
  blocks_layout: {
116
- items: ['MISSING-YOU-1', 'a', 'MISSING-YOU-2'],
116
+ items: [
117
+ 'MISSING-YOU-1',
118
+ 'a',
119
+ 'MISSING-YOU-2',
120
+ null,
121
+ undefined,
122
+ 'undefined',
123
+ ],
117
124
  },
118
125
  blocks: {
119
126
  a: {
@@ -126,7 +133,10 @@ test('Renders invalid blocks', () => {
126
133
  />
127
134
  </Provider>,
128
135
  );
136
+ // Invalid blocks (missing from blocks object or invalid IDs) are filtered out and not rendered
129
137
  expect(
130
138
  queryAllByText('Invalid block - Will be removed on saving'),
131
- ).toHaveLength(2);
139
+ ).toHaveLength(0);
140
+ // Only valid blocks are rendered
141
+ expect(queryByText('id: a - text: bar - path: /foo')).not.toBeNull();
132
142
  });
@@ -119,7 +119,7 @@ let config = {
119
119
  defaultBlockType: 'slate',
120
120
  verticalFormTabs: false,
121
121
  useEmailAsLogin: false,
122
- persistentReducers: ['blocksClipboard'],
122
+ persistentReducers: ['blocksClipboard.cut', 'blocksClipboard.copy'],
123
123
  initialReducersBlacklist: [], // reducers in this list won't be hydrated in windows.__data
124
124
  asyncPropsExtenders: [getSiteAsyncPropExtender], // per route asyncConnect customizers
125
125
  contentIcons: contentIcons,
@@ -16,6 +16,7 @@ import {
16
16
  endEventDateRangeValidator,
17
17
  patternValidator,
18
18
  defaultLanguageControlPanelValidator,
19
+ sizeValidator,
19
20
  } from '@plone/volto/helpers/FormValidation/validators';
20
21
 
21
22
  const registerValidators = (config: ConfigType) => {
@@ -33,6 +34,13 @@ const registerValidators = (config: ConfigType) => {
33
34
  method: maxLengthValidator,
34
35
  });
35
36
 
37
+ config.registerUtility({
38
+ name: 'size',
39
+ type: 'validator',
40
+ dependencies: { fieldType: 'object' },
41
+ method: sizeValidator,
42
+ });
43
+
36
44
  config.registerUtility({
37
45
  name: 'pattern',
38
46
  type: 'validator',
@@ -80,6 +80,14 @@ export function blockHasValue(data) {
80
80
  return check(data);
81
81
  }
82
82
 
83
+ /**
84
+ * Block id is valid (not undefined/null or the string "undefined" from object[undefined])
85
+ * @param {*} id Block id
86
+ * @return {boolean}
87
+ */
88
+ const isValidBlockId = (id) =>
89
+ id != null && id !== 'undefined' && (typeof id !== 'string' || id.length > 0);
90
+
83
91
  /**
84
92
  * Get block pairs of [id, block] from content properties
85
93
  * @function getBlocks
@@ -89,12 +97,26 @@ export function blockHasValue(data) {
89
97
  export const getBlocks = (properties) => {
90
98
  const blocksFieldName = getBlocksFieldname(properties);
91
99
  const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
92
- return (
93
- properties[blocksLayoutFieldname]?.items?.map((n) => [
94
- n,
95
- properties[blocksFieldName][n],
96
- ]) || []
97
- );
100
+ const blocks = properties?.[blocksFieldName];
101
+ const items = properties?.[blocksLayoutFieldname]?.items;
102
+ if (!items) return [];
103
+ return items
104
+ .filter((n) => isValidBlockId(n))
105
+ .map((n) => [n, blocks?.[n]])
106
+ .filter(([, block]) => block != null);
107
+ };
108
+
109
+ /**
110
+ * Get layout item IDs that are valid but have no block data (orphaned refs).
111
+ * @param {Object} properties Content form properties
112
+ * @return {string[]} IDs that should be removed from layout
113
+ */
114
+ export const getInvalidBlockLayoutIds = (properties) => {
115
+ const blocksFieldName = getBlocksFieldname(properties);
116
+ const blocksLayoutFieldName = getBlocksLayoutFieldname(properties);
117
+ const blocks = properties?.[blocksFieldName] ?? {};
118
+ const layoutItems = properties?.[blocksLayoutFieldName]?.items ?? [];
119
+ return layoutItems.filter((id) => isValidBlockId(id) && blocks[id] == null);
98
120
  };
99
121
 
100
122
  /**
@@ -129,12 +151,28 @@ export function deleteBlock(formData, blockId, intl) {
129
151
 
130
152
  let newFormData = {
131
153
  ...formData,
132
- [blocksLayoutFieldname]: {
133
- items: without(formData[blocksLayoutFieldname].items, blockId),
134
- },
135
- [blocksFieldname]: omit(formData[blocksFieldname], [blockId]),
136
154
  };
137
155
 
156
+ let container = findParent(newFormData, {
157
+ blockId,
158
+ });
159
+
160
+ if (container) {
161
+ container[blocksLayoutFieldname].items = without(
162
+ container[blocksLayoutFieldname].items,
163
+ blockId,
164
+ );
165
+ container[blocksFieldname] = omit(container[blocksFieldname], [blockId]);
166
+ } else {
167
+ newFormData[blocksLayoutFieldname].items = without(
168
+ newFormData[blocksLayoutFieldname].items,
169
+ blockId,
170
+ );
171
+ newFormData[blocksFieldname] = omit(newFormData[blocksFieldname], [
172
+ blockId,
173
+ ]);
174
+ }
175
+
138
176
  if (newFormData[blocksLayoutFieldname].items.length === 0) {
139
177
  newFormData = addBlock(
140
178
  newFormData,
@@ -817,7 +855,9 @@ export const getBlocksHierarchy = (properties) => {
817
855
  title: properties[blocksFieldName][n]?.['@type'],
818
856
  data: properties[blocksFieldName][n],
819
857
  children: isBlockContainer(properties[blocksFieldName][n])
820
- ? getBlocksHierarchy(properties[blocksFieldName][n])
858
+ ? properties[blocksFieldName][n].data
859
+ ? getBlocksHierarchy(properties[blocksFieldName][n].data)
860
+ : getBlocksHierarchy(properties[blocksFieldName][n])
821
861
  : [],
822
862
  }));
823
863
  };
@@ -911,8 +951,13 @@ export function moveBlockEnhanced(formData, { source, destination }) {
911
951
  const destinationContainer = findContainer(clonedFormData, {
912
952
  containerId: destination.parent,
913
953
  });
954
+ const sourceContainer = findContainer(clonedFormData, {
955
+ containerId: source.parent,
956
+ });
957
+
914
958
  destinationContainer[blocksFieldName][source.id] =
915
- formData[blocksFieldName][source.parent][blocksFieldName][source.id];
959
+ sourceContainer[blocksFieldName]?.[source.id] ||
960
+ sourceContainer.data?.[blocksFieldName][source.id];
916
961
 
917
962
  destinationContainer[blocksLayoutFieldname].items = insertInArray(
918
963
  destinationContainer[blocksLayoutFieldname].items,
@@ -921,9 +966,6 @@ export function moveBlockEnhanced(formData, { source, destination }) {
921
966
  );
922
967
 
923
968
  // Remove the source block from the source parent
924
- const sourceContainer = findContainer(clonedFormData, {
925
- containerId: source.parent,
926
- });
927
969
  delete sourceContainer[blocksFieldName][source.id];
928
970
  sourceContainer[blocksLayoutFieldname].items = removeFromArray(
929
971
  sourceContainer[blocksLayoutFieldname].items,
@@ -957,23 +999,25 @@ export function moveBlockEnhanced(formData, { source, destination }) {
957
999
  * @returns {object|undefined} - The container object if found, otherwise undefined.
958
1000
  */
959
1001
  export const findContainer = (formData, { containerId }) => {
1002
+ const block =
1003
+ formData.blocks[containerId]?.data || formData.blocks[containerId];
960
1004
  if (
961
- formData.blocks[containerId] &&
962
- Object.keys(formData.blocks[containerId]).includes('blocks') &&
963
- Object.keys(formData.blocks[containerId]).includes('blocks_layout')
1005
+ block &&
1006
+ Object.keys(block).includes('blocks') &&
1007
+ Object.keys(block).includes('blocks_layout')
964
1008
  ) {
965
- return formData.blocks[containerId];
1009
+ return block;
966
1010
  }
967
1011
 
968
1012
  let container;
969
1013
  Object.keys(formData.blocks).every((blockId) => {
970
- const block = formData.blocks[blockId];
1014
+ const subBlock = formData.blocks[blockId].data || formData.blocks[blockId];
971
1015
  if (
972
- formData.blocks[blockId] &&
973
- Object.keys(formData.blocks[blockId]).includes('blocks') &&
974
- Object.keys(formData.blocks[blockId]).includes('blocks_layout')
1016
+ subBlock &&
1017
+ Object.keys(subBlock).includes('blocks') &&
1018
+ Object.keys(subBlock).includes('blocks_layout')
975
1019
  ) {
976
- container = findContainer(block, { containerId });
1020
+ container = findContainer(subBlock, { containerId });
977
1021
  }
978
1022
  if (container) {
979
1023
  return false;
@@ -985,6 +1029,47 @@ export const findContainer = (formData, { containerId }) => {
985
1029
  return container;
986
1030
  };
987
1031
 
1032
+ /**
1033
+ * Finds parent container of the specified blockId in the given formData.
1034
+ *
1035
+ * @param {object} formData - The form data object.
1036
+ * @param {object} options - The options object.
1037
+ * @param {string} options.blockId - The ID of the block to find.
1038
+ * @returns {object|undefined} - The container object if found, otherwise undefined.
1039
+ */
1040
+ export const findParent = (formData, { blockId }) => {
1041
+ const block = formData.data || formData;
1042
+
1043
+ if (block && block.blocks && Object.keys(block.blocks).includes(blockId)) {
1044
+ return block;
1045
+ }
1046
+
1047
+ let found = false;
1048
+
1049
+ if (block && block.blocks) {
1050
+ Object.keys(block.blocks).every((subBlockId) => {
1051
+ const subBlock =
1052
+ block.blocks[subBlockId].data || block.blocks[subBlockId];
1053
+ if (subBlock && subBlock.blocks) {
1054
+ if (Object.keys(subBlock.blocks).includes(blockId)) {
1055
+ found = subBlock;
1056
+ }
1057
+ const parent = findParent(subBlock, { blockId });
1058
+ if (parent) {
1059
+ found = parent;
1060
+ }
1061
+ }
1062
+ if (found) {
1063
+ return false;
1064
+ } else {
1065
+ return true;
1066
+ }
1067
+ });
1068
+ }
1069
+
1070
+ return found;
1071
+ };
1072
+
988
1073
  const _dummyIntl = {
989
1074
  formatMessage() {},
990
1075
  };