@plone/volto 18.0.0-alpha.40 → 18.0.0-alpha.42

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 (102) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/locales/ca/LC_MESSAGES/volto.po +31 -1
  3. package/locales/ca.json +1 -1
  4. package/locales/de/LC_MESSAGES/volto.po +31 -1
  5. package/locales/de.json +1 -1
  6. package/locales/en/LC_MESSAGES/volto.po +31 -1
  7. package/locales/en.json +1 -1
  8. package/locales/es/LC_MESSAGES/volto.po +31 -1
  9. package/locales/es.json +1 -1
  10. package/locales/eu/LC_MESSAGES/volto.po +31 -1
  11. package/locales/eu.json +1 -1
  12. package/locales/fi/LC_MESSAGES/volto.po +31 -1
  13. package/locales/fi.json +1 -1
  14. package/locales/fr/LC_MESSAGES/volto.po +31 -1
  15. package/locales/fr.json +1 -1
  16. package/locales/hi/LC_MESSAGES/volto.po +31 -1
  17. package/locales/hi.json +1 -1
  18. package/locales/it/LC_MESSAGES/volto.po +31 -1
  19. package/locales/it.json +1 -1
  20. package/locales/ja/LC_MESSAGES/volto.po +31 -1
  21. package/locales/ja.json +1 -1
  22. package/locales/nl/LC_MESSAGES/volto.po +31 -1
  23. package/locales/nl.json +1 -1
  24. package/locales/pt/LC_MESSAGES/volto.po +31 -1
  25. package/locales/pt.json +1 -1
  26. package/locales/pt_BR/LC_MESSAGES/volto.po +31 -1
  27. package/locales/pt_BR.json +1 -1
  28. package/locales/ro/LC_MESSAGES/volto.po +31 -1
  29. package/locales/ro.json +1 -1
  30. package/locales/volto.pot +32 -2
  31. package/locales/zh_CN/LC_MESSAGES/volto.po +31 -1
  32. package/locales/zh_CN.json +1 -1
  33. package/package.json +4 -5
  34. package/razzle.config.js +2 -2
  35. package/src/components/index.js +0 -1
  36. package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +1 -1
  37. package/src/components/manage/AnchorPlugin/useLinkEditor.jsx +21 -21
  38. package/src/components/manage/Blocks/Block/BlocksForm.jsx +5 -0
  39. package/src/components/manage/Blocks/Block/Order/Item.jsx +6 -2
  40. package/src/components/manage/Blocks/Block/Order/Order.jsx +2 -0
  41. package/src/components/manage/Blocks/Container/Data.jsx +10 -2
  42. package/src/components/manage/Blocks/Image/ImageSidebar.jsx +10 -2
  43. package/src/components/manage/Blocks/Listing/ListingData.jsx +10 -2
  44. package/src/components/manage/Blocks/Maps/MapsSidebar.jsx +3 -1
  45. package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +2 -0
  46. package/src/components/manage/Blocks/Search/SearchBlockView.jsx +18 -2
  47. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +1 -1
  48. package/src/components/manage/Blocks/Teaser/Data.jsx +10 -2
  49. package/src/components/manage/Blocks/ToC/Edit.jsx +1 -0
  50. package/src/components/manage/Blocks/Video/Edit.jsx +1 -1
  51. package/src/components/manage/Blocks/Video/VideoSidebar.jsx +3 -1
  52. package/src/components/manage/Contents/Contents.jsx +1 -1
  53. package/src/components/manage/Controlpanels/ContentTypeSchema.jsx +1 -0
  54. package/src/components/manage/Controlpanels/UndoControlpanel.jsx +3 -3
  55. package/src/components/manage/Controlpanels/Users/UserGroupMembershipListing.jsx +28 -12
  56. package/src/components/manage/Controlpanels/Users/UserGroupMembershipMatrix.jsx +12 -4
  57. package/src/components/manage/Form/Form.jsx +85 -20
  58. package/src/components/manage/Form/InlineForm.jsx +4 -6
  59. package/src/components/manage/Form/ModalForm.jsx +1 -1
  60. package/src/components/manage/History/History.jsx +1 -1
  61. package/src/components/manage/Pluggable/Pluggable.test.js +1 -1
  62. package/src/components/manage/Toolbar/Toolbar.jsx +1 -1
  63. package/src/components/manage/Widgets/ArrayWidget.jsx +2 -2
  64. package/src/components/manage/Widgets/ImageWidget.jsx +34 -10
  65. package/src/components/manage/Widgets/RecurrenceWidget/EndField.jsx +7 -1
  66. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +80 -31
  67. package/src/components/theme/Login/Login.jsx +25 -4
  68. package/src/components/theme/Logout/Logout.jsx +2 -2
  69. package/src/components/theme/Search/Search.jsx +13 -5
  70. package/src/components/theme/View/View.jsx +0 -7
  71. package/src/components/theme/View/View.test.jsx +0 -3
  72. package/src/config/Widgets.jsx +1 -1
  73. package/src/config/index.js +7 -2
  74. package/src/config/validation.ts +155 -0
  75. package/src/helpers/Extensions/withBlockExtensions.jsx +1 -1
  76. package/src/helpers/FormValidation/FormValidation.jsx +109 -170
  77. package/src/helpers/FormValidation/FormValidation.test.js +836 -8
  78. package/src/helpers/FormValidation/validators.ts +203 -0
  79. package/src/helpers/MessageLabels/MessageLabels.js +28 -0
  80. package/src/helpers/Url/Url.test.js +4 -4
  81. package/src/helpers/User/User.js +1 -1
  82. package/src/hooks/client/useClient.js +1 -1
  83. package/test-setup-config.jsx +7 -0
  84. package/theme/themes/default/modules/embed.variables +1 -1
  85. package/theme/themes/pastanaga/collections/form.overrides +36 -2
  86. package/theme/themes/pastanaga/extras/blocks.less +14 -5
  87. package/theme/themes/pastanaga/extras/sidebar.less +4 -0
  88. package/theme/themes/pastanaga/extras/toolbar.less +10 -3
  89. package/tsconfig.declarations.json +3 -2
  90. package/types/components/index.d.ts +0 -1
  91. package/types/components/manage/Blocks/Block/Order/Order.d.ts +2 -1
  92. package/types/components/theme/Logout/Logout.d.ts +1 -1
  93. package/types/config/RichTextEditor/ToHTML.d.ts +1 -1
  94. package/types/config/Widgets.d.ts +2 -2
  95. package/types/config/validation.d.ts +3 -0
  96. package/types/helpers/Extensions/withBlockExtensions.d.ts +1 -1
  97. package/types/helpers/FormValidation/FormValidation.d.ts +1 -0
  98. package/types/helpers/FormValidation/validators.d.ts +29 -0
  99. package/types/helpers/MessageLabels/MessageLabels.d.ts +36 -0
  100. package/types/helpers/User/User.d.ts +1 -1
  101. package/src/components/theme/SocialSharing/SocialSharing.jsx +0 -48
  102. package/src/components/theme/SocialSharing/SocialSharing.test.jsx +0 -14
@@ -49,10 +49,26 @@ const applyDefaults = (data, root) => {
49
49
  v: root || '/',
50
50
  },
51
51
  ];
52
+
53
+ const searchBySearchableText = data.query.filter(
54
+ (item) => item['i'] === 'SearchableText',
55
+ ).length;
56
+
57
+ const sort_on = data?.sort_on
58
+ ? { sort_on: data.sort_on }
59
+ : searchBySearchableText === 0
60
+ ? { sort_on: 'effective' }
61
+ : {};
62
+ const sort_order = data?.sort_order
63
+ ? { sort_order: data.sort_order }
64
+ : searchBySearchableText === 0
65
+ ? { sort_order: 'descending' }
66
+ : {};
67
+
52
68
  return {
53
69
  ...data,
54
- sort_on: data?.sort_on || 'effective',
55
- sort_order: data?.sort_order || 'descending',
70
+ ...sort_on,
71
+ ...sort_order,
56
72
  query: data?.query?.length ? data.query : defaultQuery,
57
73
  };
58
74
  };
@@ -321,7 +321,7 @@ const withSearch = (options) => (WrappedComponent) => {
321
321
  const previousUrlQuery = usePrevious(urlQuery);
322
322
 
323
323
  // During first render the previousUrlQuery is undefined and urlQuery
324
- // is empty so it ressetting the facet when you are navigating but during reload we have urlQuery and we need
324
+ // is empty so it resetting the facet when you are navigating but during reload we have urlQuery and we need
325
325
  // to set the facet at first render.
326
326
  const preventOverrideOfFacetState =
327
327
  previousUrlQuery === undefined && urlQuery.length === 0;
@@ -31,8 +31,15 @@ const messages = defineMessages({
31
31
  });
32
32
 
33
33
  const TeaserData = (props) => {
34
- const { block, blocksConfig, data, onChangeBlock, navRoot, contentType } =
35
- props;
34
+ const {
35
+ block,
36
+ blocksConfig,
37
+ blocksErrors,
38
+ data,
39
+ onChangeBlock,
40
+ navRoot,
41
+ contentType,
42
+ } = props;
36
43
  const dispatch = useDispatch();
37
44
  const intl = useIntl();
38
45
 
@@ -161,6 +168,7 @@ const TeaserData = (props) => {
161
168
  actionButton={data.overwrite && ActionButton}
162
169
  navRoot={navRoot}
163
170
  contentType={contentType}
171
+ errors={blocksErrors}
164
172
  />
165
173
  );
166
174
  };
@@ -29,6 +29,7 @@ class Edit extends Component {
29
29
  block={this.props.block}
30
30
  navRoot={this.props.navRoot}
31
31
  contentType={this.props.contentType}
32
+ errors={this.props.blocksErrors}
32
33
  />
33
34
  </SidebarPortal>
34
35
  </>
@@ -116,7 +116,7 @@ class Edit extends Component {
116
116
  /**
117
117
  * Keydown handler on Variant Menu Form
118
118
  * This is required since the ENTER key is already mapped to a onKeyDown
119
- * event and needs to be overriden with a child onKeyDown.
119
+ * event and needs to be overridden with a child onKeyDown.
120
120
  * @method onKeyDownVariantMenuForm
121
121
  * @param {Object} e Event object
122
122
  * @returns {undefined}
@@ -18,7 +18,8 @@ const messages = defineMessages({
18
18
  });
19
19
 
20
20
  const VideoSidebar = (props) => {
21
- const { data, block, onChangeBlock, navRoot, contentType } = props;
21
+ const { data, block, blocksErrors, onChangeBlock, navRoot, contentType } =
22
+ props;
22
23
  const intl = useIntl();
23
24
  const schema = VideoBlockSchema({ ...props, intl });
24
25
 
@@ -44,6 +45,7 @@ const VideoSidebar = (props) => {
44
45
  block={block}
45
46
  navRoot={navRoot}
46
47
  contentType={contentType}
48
+ errors={blocksErrors}
47
49
  />
48
50
  )}
49
51
  </>
@@ -156,7 +156,7 @@ const messages = defineMessages({
156
156
  defaultMessage: 'Item(s) has been updated.',
157
157
  },
158
158
  messageReorder: {
159
- id: 'Item succesfully moved.',
159
+ id: 'Item successfully moved.',
160
160
  defaultMessage: 'Item successfully moved.',
161
161
  },
162
162
  messagePasted: {
@@ -213,6 +213,7 @@ class ContentTypeSchema extends Component {
213
213
  title: 'Form schema',
214
214
  type: 'schema',
215
215
  id: 'schema',
216
+ widget: 'schema',
216
217
  },
217
218
  },
218
219
  required: [],
@@ -389,7 +389,7 @@ class UndoControlpanel extends Component {
389
389
  }
390
390
 
391
391
  /**
392
- * Handle next and prev buttons visiblity when transactions are sorted
392
+ * Handle next and prev buttons visibility when transactions are sorted
393
393
  * @method handleSortedNextPrevButtons
394
394
  * @returns {undefined}
395
395
  */
@@ -412,7 +412,7 @@ class UndoControlpanel extends Component {
412
412
  }
413
413
 
414
414
  /**
415
- * Handle next and prev buttons visiblity when transactions are not sorted
415
+ * Handle next and prev buttons visibility when transactions are not sorted
416
416
  * @method handleNotSortedNextPrevButtons
417
417
  * @returns {undefined}
418
418
  */
@@ -435,7 +435,7 @@ class UndoControlpanel extends Component {
435
435
  }
436
436
 
437
437
  /**
438
- * Handle next, prev buttons and table visiblity
438
+ * Handle next, prev buttons and table visibility
439
439
  * @method handleTableVisiblity
440
440
  * @returns {undefined}
441
441
  */
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { cloneDeep, uniqBy } from 'lodash';
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
+ import { cloneDeep, uniqBy, debounce } from 'lodash';
3
3
  import { useIntl } from 'react-intl';
4
4
  import { useSelector, useDispatch, shallowEqual } from 'react-redux';
5
5
  import jwtDecode from 'jwt-decode';
@@ -114,25 +114,41 @@ const ListingTemplate = ({
114
114
  matrix_options = [];
115
115
  }
116
116
 
117
+ const debouncedListUsers = useMemo(
118
+ () =>
119
+ debounce((query_user, groups_filter, userLimit) => {
120
+ dispatch(
121
+ listUsers({
122
+ search: query_user,
123
+ groups_filter: groups_filter.map((el) => el.value),
124
+ limit: userLimit,
125
+ }),
126
+ );
127
+ }, 300),
128
+ [dispatch],
129
+ );
130
+
117
131
  useEffect(() => {
118
132
  // Get users.
119
133
  if (show_users) {
120
- dispatch(
121
- listUsers({
122
- search: query_user,
123
- groups_filter: groups_filter.map((el) => el.value),
124
- limit: userLimit,
125
- }),
126
- );
134
+ debouncedListUsers(query_user, groups_filter, userLimit);
127
135
  }
128
- }, [dispatch, query_user, groups_filter, show_users, userLimit]);
136
+ }, [debouncedListUsers, query_user, groups_filter, show_users, userLimit]);
137
+
138
+ const debouncedListGroups = useMemo(
139
+ () =>
140
+ debounce((query_group) => {
141
+ dispatch(listGroups(query_group));
142
+ }, 300),
143
+ [dispatch],
144
+ );
129
145
 
130
146
  useEffect(() => {
131
147
  // Get matrix groups.
132
148
  if (show_matrix_options) {
133
- dispatch(listGroups(query_group));
149
+ debouncedListGroups(query_group);
134
150
  }
135
- }, [dispatch, query_group, show_matrix_options, groups_filter]);
151
+ }, [debouncedListGroups, query_group, show_matrix_options]);
136
152
 
137
153
  const onSelectOptionHandler = (selectedvalue, checked, singleClick) => {
138
154
  singleClick = singleClick ?? false;
@@ -1,9 +1,9 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
2
  import { useSelector, useDispatch } from 'react-redux';
3
3
  import { useIntl } from 'react-intl';
4
4
  import { Checkbox, Form, Input } from 'semantic-ui-react';
5
5
 
6
- import { isEqual } from 'lodash';
6
+ import { isEqual, debounce } from 'lodash';
7
7
 
8
8
  import { messages } from '@plone/volto/helpers';
9
9
  import { listGroups } from '@plone/volto/actions'; // getRegistry
@@ -38,12 +38,20 @@ const UserGroupMembershipMatrix = ({ many_users, many_groups }) => {
38
38
  });
39
39
  }
40
40
 
41
+ const debouncedListGroups = useMemo(
42
+ () =>
43
+ debounce((query_group_filter) => {
44
+ dispatch(listGroups('', query_group_filter));
45
+ }, 300),
46
+ [dispatch],
47
+ );
48
+
41
49
  useEffect(() => {
42
50
  // TODO fetch group for at least query_group_filter.length > 1?
43
51
  if (!many_groups || (many_groups && query_group_filter.length > 1)) {
44
- dispatch(listGroups('', query_group_filter));
52
+ debouncedListGroups(query_group_filter);
45
53
  }
46
- }, [dispatch, many_groups, query_group_filter]);
54
+ }, [debouncedListGroups, many_groups, query_group_filter]);
47
55
 
48
56
  const onReset = (event) => {
49
57
  // event.preventDefault();
@@ -12,6 +12,7 @@ import {
12
12
  FormValidation,
13
13
  getBlocksFieldname,
14
14
  getBlocksLayoutFieldname,
15
+ hasBlocksData,
15
16
  messages,
16
17
  } from '@plone/volto/helpers';
17
18
  import aheadSVG from '@plone/volto/icons/ahead.svg';
@@ -349,7 +350,7 @@ class Form extends Component {
349
350
 
350
351
  /**
351
352
  * If user clicks on input, the form will be not considered pristine
352
- * this will avoid onBlur effects without interraction with the form
353
+ * this will avoid onBlur effects without interaction with the form
353
354
  * @param {Object} e event
354
355
  */
355
356
  onClickInput(e) {
@@ -527,30 +528,92 @@ class Form extends Component {
527
528
  })
528
529
  : {};
529
530
 
530
- if (keys(errors).length > 0) {
531
+ let blocksErrors = {};
532
+
533
+ if (hasBlocksData(formData)) {
534
+ // Validate blocks
535
+ const blocks = this.state.formData[getBlocksFieldname(formData)];
536
+ const blocksLayout =
537
+ this.state.formData[getBlocksLayoutFieldname(formData)];
538
+ const defaultSchema = {
539
+ properties: {},
540
+ fieldsets: [],
541
+ required: [],
542
+ };
543
+ blocksLayout.items.forEach((block) => {
544
+ let blockSchema =
545
+ config.blocks.blocksConfig[blocks[block]['@type']].blockSchema ||
546
+ defaultSchema;
547
+ if (typeof blockSchema === 'function') {
548
+ blockSchema = blockSchema({
549
+ intl: this.props.intl,
550
+ formData: blocks[block],
551
+ });
552
+ }
553
+ const blockErrors = FormValidation.validateFieldsPerFieldset({
554
+ schema: blockSchema,
555
+ formData: blocks[block],
556
+ formatMessage: this.props.intl.formatMessage,
557
+ });
558
+ if (keys(blockErrors).length > 0) {
559
+ blocksErrors = {
560
+ ...blocksErrors,
561
+ [block]: { ...blockErrors },
562
+ };
563
+ }
564
+ });
565
+ }
566
+
567
+ if (keys(errors).length > 0 || keys(blocksErrors).length > 0) {
531
568
  const activeIndex = FormValidation.showFirstTabWithErrors({
532
569
  errors,
533
570
  schema: this.props.schema,
534
571
  });
535
- this.setState(
536
- {
537
- errors,
538
- activeIndex,
539
- },
540
- () => {
541
- Object.keys(errors).forEach((err) =>
542
- toast.error(
543
- <Toast
544
- error
545
- title={this.props.schema.properties[err].title || err}
546
- content={errors[err].join(', ')}
547
- />,
548
- ),
549
- );
572
+
573
+ this.setState({
574
+ errors: {
575
+ ...errors,
576
+ ...(!isEmpty(blocksErrors) && { blocks: blocksErrors }),
550
577
  },
551
- );
552
- // Changes the focus to the metadata tab in the sidebar if error
553
- this.props.setSidebarTab(0);
578
+ activeIndex,
579
+ });
580
+
581
+ if (keys(errors).length > 0) {
582
+ // Changes the focus to the metadata tab in the sidebar if error
583
+ Object.keys(errors).forEach((err) =>
584
+ toast.error(
585
+ <Toast
586
+ error
587
+ title={this.props.schema.properties[err].title || err}
588
+ content={errors[err].join(', ')}
589
+ />,
590
+ ),
591
+ );
592
+ this.props.setSidebarTab(0);
593
+ } else if (keys(blocksErrors).length > 0) {
594
+ const errorField = Object.entries(
595
+ Object.entries(blocksErrors)[0][1],
596
+ )[0][0];
597
+ const errorMessage = Object.entries(
598
+ Object.entries(blocksErrors)[0][1],
599
+ )[0][1];
600
+ toast.error(
601
+ <Toast
602
+ error
603
+ title={this.props.intl.formatMessage(
604
+ messages.blocksFieldsErrorTitle,
605
+ { errorField },
606
+ )}
607
+ content={errorMessage}
608
+ />,
609
+ );
610
+ this.props.setSidebarTab(1);
611
+ this.props.setUIState({
612
+ selected: Object.keys(blocksErrors)[0],
613
+ multiSelected: [],
614
+ hovered: null,
615
+ });
616
+ }
554
617
  } else {
555
618
  // Get only the values that have been modified (Edit forms), send all in case that
556
619
  // it's an add form
@@ -730,6 +793,8 @@ class Form extends Component {
730
793
  history={this.props.history}
731
794
  location={this.props.location}
732
795
  token={this.props.token}
796
+ errors={this.state.errors}
797
+ blocksErrors={this.state.errors.blocks}
733
798
  />
734
799
  {this.state.isClient &&
735
800
  this.state.sidebarMetadataIsAvailable &&
@@ -142,7 +142,6 @@ const InlineForm = (props) => {
142
142
  content={error.message}
143
143
  />
144
144
  )}
145
-
146
145
  <div id={`blockform-fieldset-${defaultFieldset.id}`}>
147
146
  <Segment className="form attached">
148
147
  {map(defaultFieldset.fields, (field, index) => (
@@ -153,11 +152,11 @@ const InlineForm = (props) => {
153
152
  focus={index === focusIndex}
154
153
  value={formData[field]}
155
154
  required={schema.required.indexOf(field) !== -1}
156
- onChange={(id, value) => {
157
- onChangeField(id, value);
155
+ onChange={(id, value, itemInfo) => {
156
+ onChangeField(id, value, itemInfo);
158
157
  }}
159
158
  key={field}
160
- error={errors[field]}
159
+ error={errors?.[block]?.[field] || {}}
161
160
  block={block}
162
161
  />
163
162
  ))}
@@ -166,7 +165,6 @@ const InlineForm = (props) => {
166
165
  )}
167
166
  </Segment>
168
167
  </div>
169
-
170
168
  {other.map((fieldset, index) => (
171
169
  <Accordion fluid styled className="form" key={fieldset.id}>
172
170
  <div key={fieldset.id} id={`blockform-fieldset-${fieldset.id}`}>
@@ -199,7 +197,7 @@ const InlineForm = (props) => {
199
197
  onChangeField(id, value);
200
198
  }}
201
199
  key={field}
202
- error={errors[field]}
200
+ error={errors?.[block]?.[field] || {}}
203
201
  block={block}
204
202
  />
205
203
  ))}
@@ -137,7 +137,7 @@ class ModalForm extends Component {
137
137
 
138
138
  /**
139
139
  * If user clicks on input, the form will be not considered pristine
140
- * this will avoid onBlur effects without interraction with the form
140
+ * this will avoid onBlur effects without interaction with the form
141
141
  * @param {Object} e event
142
142
  */
143
143
  onClickInput(e) {
@@ -345,7 +345,7 @@ export default compose(
345
345
  asyncConnect([
346
346
  {
347
347
  key: 'actions',
348
- // Dispatch async/await to make the operation syncronous, otherwise it returns
348
+ // Dispatch async/await to make the operation synchronous, otherwise it returns
349
349
  // before the promise is resolved
350
350
  promise: async ({ location, store: { dispatch } }) =>
351
351
  await dispatch(listActions(getBaseUrl(location.pathname))),
@@ -31,7 +31,7 @@ describe('<Pluggable />', () => {
31
31
  expect(container).toMatchSnapshot();
32
32
  });
33
33
 
34
- it('Works with an empy Plug', () => {
34
+ it('Works with an empty Plug', () => {
35
35
  const { container } = render(
36
36
  <PluggablesProvider>
37
37
  <Pluggable name="test" />
@@ -623,7 +623,7 @@ class Toolbar extends Component {
623
623
  aria-label={this.props.intl.formatMessage(
624
624
  messages.shrinkToolbar,
625
625
  )}
626
- className={cx({
626
+ className={cx('toolbar-handler-button', {
627
627
  [this.props.content?.review_state]:
628
628
  this.props.content?.review_state,
629
629
  })}
@@ -129,7 +129,7 @@ const compareOption = (inputValue = '', option, accessors) => {
129
129
  * @class ArrayWidget
130
130
  * @extends Component
131
131
  *
132
- * A createable select array widget will be rendered if the named vocabulary is
132
+ * A creatable select array widget will be rendered if the named vocabulary is
133
133
  * in the widget definition (hint) like:
134
134
  *
135
135
  * ```
@@ -280,7 +280,7 @@ class ArrayWidget extends Component {
280
280
  const { SortableContainer } = this.props.reactSortableHOC;
281
281
  const Select = this.props.reactSelect.default;
282
282
  const SortableSelect =
283
- // It will be only createable if the named vocabulary is in the widget definition
283
+ // It will be only creatable if the named vocabulary is in the widget definition
284
284
  // (hint) like:
285
285
  // list_field_voc_unconstrained = schema.List(
286
286
  // title=u"List field with values from vocabulary but not constrained to them.",
@@ -14,6 +14,7 @@ import {
14
14
  getBaseUrl,
15
15
  isInternalURL,
16
16
  validateFileUploadSize,
17
+ usePrevious,
17
18
  } from '@plone/volto/helpers';
18
19
  import { createContent } from '@plone/volto/actions';
19
20
  import { readAsDataURL } from 'promise-file-reader';
@@ -77,6 +78,7 @@ const UnconnectedImageInput = (props) => {
77
78
  placeholderLinkInput = '',
78
79
  onSelectItem,
79
80
  } = props;
81
+ const imageValue = value?.[0]?.['@id'] || value;
80
82
 
81
83
  const intl = useIntl();
82
84
  const linkEditor = useLinkEditor();
@@ -91,13 +93,14 @@ const UnconnectedImageInput = (props) => {
91
93
 
92
94
  const requestId = `image-upload-${id}`;
93
95
 
94
- const { loading, loaded } = props.request;
96
+ const loaded = props.request.loaded;
95
97
  const { content } = props;
96
98
  const imageId = content?.['@id'];
97
99
  const image = content?.image;
100
+ let loading = false;
98
101
 
99
102
  useEffect(() => {
100
- if (uploading && !loading && loaded) {
103
+ if (uploading && loading && loaded) {
101
104
  setUploading(false);
102
105
  onChange(id, imageId, {
103
106
  image_field: 'image',
@@ -106,6 +109,8 @@ const UnconnectedImageInput = (props) => {
106
109
  }
107
110
  }, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
108
111
 
112
+ loading = usePrevious(props.request?.loading);
113
+
109
114
  const handleUpload = React.useCallback(
110
115
  (eventOrFile) => {
111
116
  if (restrictFileUpload === true) return;
@@ -151,7 +156,7 @@ const UnconnectedImageInput = (props) => {
151
156
  }, [restrictFileUpload]);
152
157
  const onDragLeave = React.useCallback(() => setDragging(false), []);
153
158
 
154
- return value ? (
159
+ return imageValue ? (
155
160
  <div
156
161
  className="image-upload-widget-image"
157
162
  onClick={onFocus}
@@ -161,7 +166,11 @@ const UnconnectedImageInput = (props) => {
161
166
  {selected && <ImageToolbar {...props} />}
162
167
  <img
163
168
  className={props.className}
164
- src={`${flattenToAppURL(value)}/@@images/image/${imageSize}`}
169
+ src={
170
+ isInternalURL(imageValue)
171
+ ? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
172
+ : imageValue
173
+ }
165
174
  alt=""
166
175
  />
167
176
  </div>
@@ -264,7 +273,7 @@ const UnconnectedImageInput = (props) => {
264
273
  </div>
265
274
  {linkEditor.anchorNode && (
266
275
  <linkEditor.LinkEditor
267
- value={value}
276
+ value={imageValue}
268
277
  placeholder={
269
278
  placeholderLinkInput ||
270
279
  intl.formatMessage(messages.linkAnImage)
@@ -302,10 +311,25 @@ export const ImageInput = compose(
302
311
  ),
303
312
  )(withObjectBrowser(UnconnectedImageInput));
304
313
 
305
- const ImageUploadWidget = (props) => (
306
- <FormFieldWrapper {...props} className="image-upload-widget">
307
- <ImageInput {...props} />
308
- </FormFieldWrapper>
309
- );
314
+ const ImageUploadWidget = (props) => {
315
+ const { fieldSet, id, title } = props;
316
+ return (
317
+ <FormFieldWrapper
318
+ {...props}
319
+ columns={1}
320
+ className="block image-upload-widget"
321
+ >
322
+ <div className="wrapper">
323
+ <label
324
+ id={`fieldset-${fieldSet}-field-label-${id}`}
325
+ htmlFor={`field-${id}`}
326
+ >
327
+ {title}
328
+ </label>
329
+ </div>
330
+ <ImageInput {...props} />
331
+ </FormFieldWrapper>
332
+ );
333
+ };
310
334
 
311
335
  export default ImageUploadWidget;
@@ -81,7 +81,13 @@ const EndField = ({ value, count, until, onChange, intl }) => {
81
81
  id="until"
82
82
  title={intl.formatMessage(messages.recurrenceEndsUntil)}
83
83
  dateOnly={true}
84
- value={until || ''}
84
+ value={
85
+ until
86
+ ? typeof until === 'string'
87
+ ? until
88
+ : until?.toISOString()
89
+ : ''
90
+ }
85
91
  resettable={false}
86
92
  onChange={(id, value) => {
87
93
  onChange(id, value === '' ? undefined : value);