@plone/volto 18.0.0-alpha.41 → 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 (96) hide show
  1. package/CHANGELOG.md +42 -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 +3 -4
  34. package/razzle.config.js +2 -2
  35. package/src/components/index.js +0 -1
  36. package/src/components/manage/Blocks/Block/BlocksForm.jsx +5 -0
  37. package/src/components/manage/Blocks/Block/Order/Item.jsx +6 -2
  38. package/src/components/manage/Blocks/Block/Order/Order.jsx +2 -0
  39. package/src/components/manage/Blocks/Container/Data.jsx +10 -2
  40. package/src/components/manage/Blocks/Image/ImageSidebar.jsx +10 -2
  41. package/src/components/manage/Blocks/Listing/ListingData.jsx +10 -2
  42. package/src/components/manage/Blocks/Maps/MapsSidebar.jsx +3 -1
  43. package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +2 -0
  44. package/src/components/manage/Blocks/Search/SearchBlockView.jsx +18 -2
  45. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +1 -1
  46. package/src/components/manage/Blocks/Teaser/Data.jsx +10 -2
  47. package/src/components/manage/Blocks/ToC/Edit.jsx +1 -0
  48. package/src/components/manage/Blocks/Video/Edit.jsx +1 -1
  49. package/src/components/manage/Blocks/Video/VideoSidebar.jsx +3 -1
  50. package/src/components/manage/Contents/Contents.jsx +1 -1
  51. package/src/components/manage/Controlpanels/ContentTypeSchema.jsx +1 -0
  52. package/src/components/manage/Controlpanels/UndoControlpanel.jsx +3 -3
  53. package/src/components/manage/Controlpanels/Users/UserGroupMembershipListing.jsx +28 -12
  54. package/src/components/manage/Controlpanels/Users/UserGroupMembershipMatrix.jsx +12 -4
  55. package/src/components/manage/Form/Form.jsx +85 -20
  56. package/src/components/manage/Form/InlineForm.jsx +2 -4
  57. package/src/components/manage/Form/ModalForm.jsx +1 -1
  58. package/src/components/manage/History/History.jsx +1 -1
  59. package/src/components/manage/Pluggable/Pluggable.test.js +1 -1
  60. package/src/components/manage/Toolbar/Toolbar.jsx +1 -1
  61. package/src/components/manage/Widgets/ArrayWidget.jsx +2 -2
  62. package/src/components/manage/Widgets/ImageWidget.jsx +6 -5
  63. package/src/components/manage/Widgets/RecurrenceWidget/EndField.jsx +7 -1
  64. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +80 -31
  65. package/src/components/theme/Search/Search.jsx +13 -5
  66. package/src/components/theme/View/View.jsx +0 -7
  67. package/src/components/theme/View/View.test.jsx +0 -3
  68. package/src/config/Widgets.jsx +1 -1
  69. package/src/config/index.js +7 -2
  70. package/src/config/validation.ts +155 -0
  71. package/src/helpers/Extensions/withBlockExtensions.jsx +1 -1
  72. package/src/helpers/FormValidation/FormValidation.jsx +109 -170
  73. package/src/helpers/FormValidation/FormValidation.test.js +836 -8
  74. package/src/helpers/FormValidation/validators.ts +203 -0
  75. package/src/helpers/MessageLabels/MessageLabels.js +28 -0
  76. package/src/helpers/Url/Url.test.js +4 -4
  77. package/src/helpers/User/User.js +1 -1
  78. package/src/hooks/client/useClient.js +1 -1
  79. package/test-setup-config.jsx +7 -0
  80. package/theme/themes/default/modules/embed.variables +1 -1
  81. package/theme/themes/pastanaga/collections/form.overrides +34 -0
  82. package/theme/themes/pastanaga/extras/sidebar.less +4 -0
  83. package/theme/themes/pastanaga/extras/toolbar.less +10 -3
  84. package/tsconfig.declarations.json +3 -2
  85. package/types/components/index.d.ts +0 -1
  86. package/types/components/manage/Blocks/Block/Order/Order.d.ts +2 -1
  87. package/types/config/RichTextEditor/ToHTML.d.ts +1 -1
  88. package/types/config/Widgets.d.ts +2 -2
  89. package/types/config/validation.d.ts +3 -0
  90. package/types/helpers/Extensions/withBlockExtensions.d.ts +1 -1
  91. package/types/helpers/FormValidation/FormValidation.d.ts +1 -0
  92. package/types/helpers/FormValidation/validators.d.ts +29 -0
  93. package/types/helpers/MessageLabels/MessageLabels.d.ts +36 -0
  94. package/types/helpers/User/User.d.ts +1 -1
  95. package/src/components/theme/SocialSharing/SocialSharing.jsx +0 -48
  96. package/src/components/theme/SocialSharing/SocialSharing.test.jsx +0 -14
@@ -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) => (
@@ -157,7 +156,7 @@ const InlineForm = (props) => {
157
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.",
@@ -78,6 +78,7 @@ const UnconnectedImageInput = (props) => {
78
78
  placeholderLinkInput = '',
79
79
  onSelectItem,
80
80
  } = props;
81
+ const imageValue = value?.[0]?.['@id'] || value;
81
82
 
82
83
  const intl = useIntl();
83
84
  const linkEditor = useLinkEditor();
@@ -155,7 +156,7 @@ const UnconnectedImageInput = (props) => {
155
156
  }, [restrictFileUpload]);
156
157
  const onDragLeave = React.useCallback(() => setDragging(false), []);
157
158
 
158
- return value ? (
159
+ return imageValue ? (
159
160
  <div
160
161
  className="image-upload-widget-image"
161
162
  onClick={onFocus}
@@ -166,9 +167,9 @@ const UnconnectedImageInput = (props) => {
166
167
  <img
167
168
  className={props.className}
168
169
  src={
169
- isInternalURL(value)
170
- ? `${flattenToAppURL(value)}/@@images/image/${imageSize}`
171
- : value
170
+ isInternalURL(imageValue)
171
+ ? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
172
+ : imageValue
172
173
  }
173
174
  alt=""
174
175
  />
@@ -272,7 +273,7 @@ const UnconnectedImageInput = (props) => {
272
273
  </div>
273
274
  {linkEditor.anchorNode && (
274
275
  <linkEditor.LinkEditor
275
- value={value}
276
+ value={imageValue}
276
277
  placeholder={
277
278
  placeholderLinkInput ||
278
279
  intl.formatMessage(messages.linkAnImage)
@@ -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);
@@ -37,7 +37,6 @@ import {
37
37
  FREQUENCES,
38
38
  WEEKLY_DAYS,
39
39
  MONDAYFRIDAY_DAYS,
40
- toISOString,
41
40
  rrulei18n,
42
41
  } from './Utils';
43
42
 
@@ -219,26 +218,40 @@ class RecurrenceWidget extends Component {
219
218
 
220
219
  componentDidUpdate(prevProps) {
221
220
  if (this.props.value) {
222
- if (prevProps.formData?.start !== this.props.formData?.start) {
223
- let start = this.getUTCDate(this.props.formData?.start)
224
- .startOf('day')
225
- .toDate();
226
-
227
- this.setState((prevState) => {
228
- let rruleSet = prevState.rruleSet;
229
-
230
- rruleSet = this.updateRruleSet(
231
- rruleSet,
232
- prevState.formValues,
233
- 'dtstart',
234
- start,
235
- );
236
-
237
- return {
238
- ...prevState,
239
- rruleSet,
240
- };
241
- });
221
+ const changedStart =
222
+ prevProps.formData?.start !== this.props.formData?.start;
223
+ const changedEnd = prevProps.formData?.end !== this.props.formData?.end;
224
+
225
+ if (changedStart || changedEnd) {
226
+ let start = this.getUTCDate(this.props.formData?.start).toDate();
227
+
228
+ let changeFormValues = {};
229
+ if (changedEnd) {
230
+ changeFormValues.until = this.getUTCDate(
231
+ this.props.formData?.end,
232
+ ).toDate();
233
+ }
234
+ this.setState(
235
+ (prevState) => {
236
+ let rruleSet = prevState.rruleSet;
237
+
238
+ rruleSet = this.updateRruleSet(
239
+ rruleSet,
240
+ { ...prevState.formValues, ...changeFormValues },
241
+ 'dtstart',
242
+ start,
243
+ );
244
+
245
+ return {
246
+ ...prevState,
247
+ rruleSet,
248
+ };
249
+ },
250
+ () => {
251
+ //then, after set state, set recurrence rrule value
252
+ this.saveRrule();
253
+ },
254
+ );
242
255
  }
243
256
  }
244
257
  }
@@ -250,7 +263,8 @@ class RecurrenceWidget extends Component {
250
263
  setRecurrenceStartEnd = () => {
251
264
  const start = this.props.formData?.start;
252
265
 
253
- let _start = this.getUTCDate(start).startOf('day').toDate();
266
+ // The `start` date from Plone is in UTC
267
+ const _start = new Date(start);
254
268
 
255
269
  this.setState((prevState) => {
256
270
  let rruleSet = prevState.rruleSet;
@@ -339,7 +353,7 @@ class RecurrenceWidget extends Component {
339
353
  case 'until':
340
354
  if (value != null) {
341
355
  formValues['recurrenceEnds'] = option;
342
- formValues[option] = toISOString(value);
356
+ formValues[option] = value;
343
357
  }
344
358
  break;
345
359
  case 'byweekday':
@@ -422,7 +436,24 @@ class RecurrenceWidget extends Component {
422
436
  }
423
437
  break;
424
438
  case 'until':
425
- value = value ? this.moment(new Date(value)).utc().toDate() : null;
439
+ let mDate = null;
440
+ if (value) {
441
+ mDate = this.moment(new Date(value));
442
+ if (typeof value === 'string') {
443
+ mDate = this.moment(new Date(value));
444
+ } else {
445
+ //object-->Date()
446
+ mDate = this.moment(value);
447
+ }
448
+
449
+ if (this.props.formData.end) {
450
+ //set time from formData.end
451
+ const mEnd = this.moment(new Date(this.props.formData.end));
452
+ mDate.set('hour', mEnd.get('hour'));
453
+ mDate.set('minute', mEnd.get('minute'));
454
+ }
455
+ }
456
+ value = value ? mDate.toDate() : null;
426
457
  break;
427
458
  default:
428
459
  break;
@@ -447,7 +478,7 @@ class RecurrenceWidget extends Component {
447
478
  ? value
448
479
  : rruleSet.dtstart()
449
480
  ? rruleSet.dtstart()
450
- : this.moment().utc().toDate();
481
+ : new Date();
451
482
  var exdates =
452
483
  field === 'exdates' ? value : Object.assign([], rruleSet.exdates());
453
484
 
@@ -470,13 +501,14 @@ class RecurrenceWidget extends Component {
470
501
 
471
502
  getDefaultUntil = (freq) => {
472
503
  const moment = this.moment;
504
+
473
505
  var end = this.props.formData?.end
474
- ? toISOString(this.getUTCDate(this.props.formData.end).toDate())
506
+ ? moment(new Date(this.props.formData.end))
475
507
  : null;
476
- var tomorrow = toISOString(moment().add(1, 'days').utc().toDate());
477
- var nextWeek = toISOString(moment().add(7, 'days').utc().toDate());
478
- var nextMonth = toISOString(moment().add(1, 'months').utc().toDate());
479
- var nextYear = toISOString(moment().add(1, 'years').utc().toDate());
508
+ var tomorrow = moment().add(1, 'days');
509
+ var nextWeek = moment().add(7, 'days');
510
+ var nextMonth = moment().add(1, 'months');
511
+ var nextYear = moment().add(1, 'years');
480
512
 
481
513
  var until = end;
482
514
  switch (freq) {
@@ -502,6 +534,19 @@ class RecurrenceWidget extends Component {
502
534
  break;
503
535
  }
504
536
 
537
+ if (this.props.formData.end) {
538
+ //set default end time
539
+ until.set('hour', end.get('hour'));
540
+ until.set('minute', end.get('minute'));
541
+ }
542
+ until = new Date(
543
+ until.get('year'),
544
+ until.get('month'),
545
+ until.get('date'),
546
+ until.get('hour'),
547
+ until.get('minute'),
548
+ );
549
+
505
550
  return until;
506
551
  };
507
552
 
@@ -716,9 +761,13 @@ class RecurrenceWidget extends Component {
716
761
  }
717
762
  };
718
763
 
719
- save = () => {
764
+ saveRrule = () => {
720
765
  var value = this.state.rruleSet.toString();
721
766
  this.props.onChange(this.props.id, value);
767
+ };
768
+
769
+ save = () => {
770
+ this.saveRrule();
722
771
  this.close();
723
772
  };
724
773
 
@@ -71,6 +71,7 @@ class Search extends Component {
71
71
 
72
72
  constructor(props) {
73
73
  super(props);
74
+ this.defaultPageSize = config.settings.defaultPageSize;
74
75
  this.state = { currentPage: 1, isClient: false, active: 'relevance' };
75
76
  }
76
77
 
@@ -109,19 +110,24 @@ class Search extends Component {
109
110
  const options = qs.parse(this.props.history.location.search);
110
111
  this.setState({ currentPage: 1 });
111
112
  options['use_site_search_settings'] = 1;
112
- this.props.searchContent('', options);
113
+ this.props.searchContent('', {
114
+ b_size: this.defaultPageSize,
115
+ ...options,
116
+ });
113
117
  };
114
118
 
115
119
  handleQueryPaginationChange = (e, { activePage }) => {
116
- const { settings } = config;
117
120
  window.scrollTo(0, 0);
118
121
  let options = qs.parse(this.props.history.location.search);
119
122
  options['use_site_search_settings'] = 1;
120
123
 
121
124
  this.setState({ currentPage: activePage }, () => {
122
125
  this.props.searchContent('', {
126
+ b_size: this.defaultPageSize,
123
127
  ...options,
124
- b_start: (this.state.currentPage - 1) * settings.defaultPageSize,
128
+ b_start:
129
+ (this.state.currentPage - 1) *
130
+ (options.b_size || this.defaultPageSize),
125
131
  });
126
132
  });
127
133
  };
@@ -149,7 +155,8 @@ class Search extends Component {
149
155
  * @returns {string} Markup for the component.
150
156
  */
151
157
  render() {
152
- const { settings } = config;
158
+ const options = qs.parse(this.props.history.location.search);
159
+
153
160
  return (
154
161
  <Container id="page-search">
155
162
  <Helmet title={this.props.intl.formatMessage(messages.Search)} />
@@ -282,7 +289,8 @@ class Search extends Component {
282
289
  <Pagination
283
290
  activePage={this.state.currentPage}
284
291
  totalPages={Math.ceil(
285
- this.props.search.items_total / settings.defaultPageSize,
292
+ this.props.search.items_total /
293
+ (options.b_size || this.defaultPageSize),
286
294
  )}
287
295
  onPageChange={this.handleQueryPaginationChange}
288
296
  firstItem={null}