@plone/volto 17.6.1 → 17.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/addon-registry.js +34 -1
  3. package/package.json +1 -1
  4. package/packages/volto-slate/news/5347.bugfix +1 -0
  5. package/packages/volto-slate/news/5517.feature +1 -0
  6. package/packages/volto-slate/package.json +1 -1
  7. package/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx +4 -0
  8. package/packages/volto-slate/src/editor/extensions/insertData.js +3 -1
  9. package/packages/volto-slate/src/editor/extensions/normalizeExternalData.js +1 -1
  10. package/packages/volto-slate/src/utils/blocks.js +6 -2
  11. package/src/components/manage/Add/Add.jsx +6 -0
  12. package/src/components/manage/BlockChooser/BlockChooser.jsx +3 -1
  13. package/src/components/manage/BlockChooser/BlockChooserButton.jsx +5 -0
  14. package/src/components/manage/Blocks/Block/BlocksForm.jsx +4 -0
  15. package/src/components/manage/Blocks/Block/DefaultEdit.jsx +3 -1
  16. package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +10 -2
  17. package/src/components/manage/Blocks/Block/Settings.jsx +10 -1
  18. package/src/components/manage/Blocks/Block/StyleWrapper.jsx +7 -1
  19. package/src/components/manage/Blocks/Container/Data.jsx +4 -1
  20. package/src/components/manage/Blocks/Grid/View.jsx +2 -1
  21. package/src/components/manage/Blocks/HeroImageLeft/Data.jsx +3 -1
  22. package/src/components/manage/Blocks/Image/ImageSidebar.jsx +4 -1
  23. package/src/components/manage/Blocks/Image/View.jsx +2 -1
  24. package/src/components/manage/Blocks/Listing/ListingData.jsx +4 -1
  25. package/src/components/manage/Blocks/Listing/View.jsx +2 -1
  26. package/src/components/manage/Blocks/Maps/MapsSidebar.jsx +3 -1
  27. package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +4 -0
  28. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +46 -4
  29. package/src/components/manage/Blocks/Teaser/Data.jsx +4 -1
  30. package/src/components/manage/Blocks/Teaser/DefaultBody.jsx +2 -2
  31. package/src/components/manage/Blocks/ToC/Edit.jsx +2 -0
  32. package/src/components/manage/Blocks/Video/Body.jsx +52 -22
  33. package/src/components/manage/Blocks/Video/Body.test.jsx +167 -0
  34. package/src/components/manage/Blocks/Video/VideoSidebar.jsx +3 -1
  35. package/src/components/manage/Controlpanels/Users/UserGroupMembershipControlPanel.jsx +2 -2
  36. package/src/components/manage/Edit/Edit.jsx +1 -0
  37. package/src/components/manage/Form/Form.jsx +14 -3
  38. package/src/components/manage/Sidebar/SidebarPopup.jsx +1 -1
  39. package/src/components/manage/Widgets/RecurrenceWidget/EndField.jsx +7 -1
  40. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +79 -31
  41. package/src/components/theme/Image/Image.jsx +4 -2
  42. package/src/components/theme/Image/Image.test.jsx +32 -0
  43. package/src/components/theme/Login/Login.jsx +12 -2
  44. package/src/components/theme/Navigation/Navigation.jsx +34 -32
  45. package/src/components/theme/PreviewImage/PreviewImage.jsx +7 -2
  46. package/src/components/theme/Sitemap/Sitemap.jsx +4 -4
  47. package/src/components/theme/Sitemap/Sitemap.test.jsx +52 -0
  48. package/src/components/theme/Unauthorized/Unauthorized.jsx +12 -11
  49. package/src/helpers/Blocks/Blocks.js +67 -3
  50. package/src/helpers/Blocks/Blocks.test.js +57 -0
  51. package/src/helpers/Extensions/withBlockSchemaEnhancer.js +20 -9
  52. package/src/helpers/Url/Url.js +5 -4
  53. package/src/helpers/Url/Url.test.js +52 -0
  54. package/src/helpers/index.js +1 -0
  55. package/types/components/manage/Blocks/Image/View.d.ts +2 -1
  56. package/types/components/manage/Blocks/Video/Body.d.ts +5 -0
  57. package/types/components/manage/Blocks/Video/Body.test.d.ts +1 -0
  58. package/types/helpers/Blocks/Blocks.d.ts +3 -1
  59. package/types/helpers/Extensions/withBlockSchemaEnhancer.d.ts +3 -1
  60. package/apps/plone/node_modules/.bin/acorn +0 -17
  61. package/apps/plone/node_modules/.bin/addon +0 -17
  62. package/apps/plone/node_modules/.bin/autoprefixer +0 -17
  63. package/apps/plone/node_modules/.bin/browserslist +0 -17
  64. package/apps/plone/node_modules/.bin/build-storybook +0 -17
  65. package/apps/plone/node_modules/.bin/changelogupdater +0 -17
  66. package/apps/plone/node_modules/.bin/eslint +0 -17
  67. package/apps/plone/node_modules/.bin/eslint-config-prettier +0 -17
  68. package/apps/plone/node_modules/.bin/i18n +0 -17
  69. package/apps/plone/node_modules/.bin/jest +0 -17
  70. package/apps/plone/node_modules/.bin/lessc +0 -17
  71. package/apps/plone/node_modules/.bin/missdev +0 -17
  72. package/apps/plone/node_modules/.bin/prettier +0 -17
  73. package/apps/plone/node_modules/.bin/razzle +0 -17
  74. package/apps/plone/node_modules/.bin/server-test +0 -17
  75. package/apps/plone/node_modules/.bin/start-server-and-test +0 -17
  76. package/apps/plone/node_modules/.bin/start-storybook +0 -17
  77. package/apps/plone/node_modules/.bin/start-test +0 -17
  78. package/apps/plone/node_modules/.bin/storybook-server +0 -17
  79. package/apps/plone/node_modules/.bin/stylelint +0 -17
  80. package/apps/plone/node_modules/.bin/tlds +0 -17
  81. package/apps/plone/node_modules/.bin/ts-jest +0 -17
  82. package/apps/plone/node_modules/.bin/tsc +0 -17
  83. package/apps/plone/node_modules/.bin/tsserver +0 -17
  84. package/apps/plone/node_modules/.bin/uuid +0 -17
  85. package/apps/plone/node_modules/.bin/webpack +0 -17
  86. package/apps/plone/node_modules/.bin/webpack-dev-server +0 -17
  87. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/acorn +0 -17
  88. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/addon +0 -17
  89. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/browserslist +0 -17
  90. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/changelogupdater +0 -17
  91. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/eslint +0 -17
  92. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/eslint-config-prettier +0 -17
  93. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/i18n +0 -17
  94. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/prettier +0 -17
  95. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/release-it +0 -17
  96. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/stylelint +0 -17
  97. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/tsc +0 -17
  98. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/tsserver +0 -17
  99. package/packages/volto-slate/build/messages/src/blocks/Table/TableBlockEdit.json +0 -90
  100. package/packages/volto-slate/build/messages/src/blocks/Text/DefaultTextBlockEditor.json +0 -6
  101. package/packages/volto-slate/build/messages/src/blocks/Text/DetachedTextBlockEditor.json +0 -6
  102. package/packages/volto-slate/build/messages/src/blocks/Text/SlashMenu.json +0 -6
  103. package/packages/volto-slate/build/messages/src/editor/plugins/AdvancedLink/index.json +0 -10
  104. package/packages/volto-slate/build/messages/src/editor/plugins/Link/index.json +0 -10
  105. package/packages/volto-slate/build/messages/src/editor/plugins/Table/index.json +0 -30
  106. package/packages/volto-slate/build/messages/src/elementEditor/messages.json +0 -10
  107. package/packages/volto-slate/build/messages/src/widgets/HtmlSlateWidget.json +0 -6
  108. package/packages/volto-slate/build/messages/src/widgets/RichTextWidgetView.json +0 -6
@@ -18,7 +18,7 @@ const messages = defineMessages({
18
18
  });
19
19
 
20
20
  const TeaserDefaultTemplate = (props) => {
21
- const { className, data, isEditMode } = props;
21
+ const { className, data, isEditMode, style } = props;
22
22
  const intl = useIntl();
23
23
  const href = data.href?.[0];
24
24
  const image = data.preview_image?.[0];
@@ -27,7 +27,7 @@ const TeaserDefaultTemplate = (props) => {
27
27
  const { openExternalLinkInNewTab } = config.settings;
28
28
 
29
29
  return (
30
- <div className={cx('block teaser', className)}>
30
+ <div className={cx('block teaser', className)} style={style}>
31
31
  <>
32
32
  {!href && isEditMode && (
33
33
  <Message>
@@ -27,6 +27,8 @@ class Edit extends Component {
27
27
  onChangeBlock={this.props.onChangeBlock}
28
28
  formData={this.props.data}
29
29
  block={this.props.block}
30
+ navRoot={this.props.navRoot}
31
+ contentType={this.props.contentType}
30
32
  />
31
33
  </SidebarPortal>
32
34
  </>
@@ -5,39 +5,68 @@ import { Embed, Message } from 'semantic-ui-react';
5
5
  import cx from 'classnames';
6
6
  import { isInternalURL, flattenToAppURL } from '@plone/volto/helpers';
7
7
 
8
- const Body = ({ data, isEditMode }) => {
9
- let placeholder = data.preview_image
10
- ? isInternalURL(data.preview_image)
11
- ? `${flattenToAppURL(data.preview_image)}/@@images/image`
12
- : data.preview_image
13
- : null;
14
-
8
+ //Extracting videoID, listID and thumbnailURL from the video URL
9
+ const getVideoIDAndPlaceholder = (url) => {
15
10
  let videoID = null;
16
11
  let listID = null;
12
+ let thumbnailURL = null;
17
13
 
18
- if (data.url) {
19
- if (data.url.match('youtu')) {
20
- if (data.url.match('list')) {
21
- const matches = data.url.match(/^.*\?list=(.*)|^.*&list=(.*)$/);
14
+ if (url) {
15
+ if (url.match('youtu')) {
16
+ if (url.match('list')) {
17
+ const matches = url.match(/^.*\?list=(.*)|^.*&list=(.*)$/);
22
18
  listID = matches[1] || matches[2];
23
- } else {
24
- videoID = data.url.match(/.be\//)
25
- ? data.url.match(/^.*\.be\/(.*)/)[1]
26
- : data.url.match(/^.*\?v=(.*)$/)[1];
19
+
20
+ let thumbnailID = null;
21
+ if (url.match(/\?v=(.*)&list/)) {
22
+ thumbnailID = url.match(/^.*\?v=(.*)&list(.*)/)[1];
23
+ }
24
+ if (url.match(/\?v=(.*)\?list/)) {
25
+ thumbnailID = url.match(/^.*\?v=(.*)\?list(.*)/)[1];
26
+ }
27
+ thumbnailURL =
28
+ 'https://img.youtube.com/vi/' + thumbnailID + '/sddefault.jpg';
29
+ } else if (url.match('live')) {
30
+ videoID = url.match(/^.*\/live\/(.*)/)[1];
31
+ } else if (url.match(/\.be\//)) {
32
+ videoID = url.match(/^.*\.be\/(.*)/)[1];
33
+ } else if (url.match(/\?v=/)) {
34
+ videoID = url.match(/^.*\?v=(.*)$/)[1];
27
35
  }
28
36
 
29
- if (!placeholder) {
37
+ if (videoID) {
38
+ let thumbnailID = videoID;
39
+ if (videoID.match(/\?si=/)) {
40
+ thumbnailID = videoID.match(/(.*)\?si=(.*)/)[1];
41
+ }
30
42
  //load video preview image from youtube
31
- placeholder =
32
- 'https://img.youtube.com/vi/' + videoID + '/sddefault.jpg';
43
+ thumbnailURL =
44
+ 'https://img.youtube.com/vi/' + thumbnailID + '/sddefault.jpg';
33
45
  }
34
- } else if (data.url.match('vimeo')) {
35
- videoID = data.url.match(/^.*\.com\/(.*)/)[1];
36
- if (!placeholder) {
37
- placeholder = 'https://vumbnail.com/' + videoID + '.jpg';
46
+ } else if (url.match('vimeo')) {
47
+ videoID = url.match(/^.*\.com\/(.*)/)[1];
48
+ if (videoID) {
49
+ let thumbnailID = videoID;
50
+ if (videoID.match(/\?si=/)) {
51
+ thumbnailID = videoID.match(/(.*)\?si=(.*)/)[1];
52
+ }
53
+ thumbnailURL = 'https://vumbnail.com/' + thumbnailID + '.jpg';
38
54
  }
39
55
  }
40
56
  }
57
+ return { videoID, listID, thumbnailURL };
58
+ };
59
+
60
+ const Body = ({ data, isEditMode }) => {
61
+ let placeholder = data.preview_image
62
+ ? isInternalURL(data.preview_image)
63
+ ? `${flattenToAppURL(data.preview_image)}/@@images/image`
64
+ : data.preview_image
65
+ : null;
66
+
67
+ const { videoID, listID, thumbnailURL } = getVideoIDAndPlaceholder(data.url);
68
+
69
+ placeholder = !placeholder ? thumbnailURL : placeholder;
41
70
 
42
71
  const ref = React.createRef();
43
72
  const onKeyDown = (e) => {
@@ -130,3 +159,4 @@ Body.propTypes = {
130
159
  };
131
160
 
132
161
  export default Body;
162
+ export { getVideoIDAndPlaceholder };
@@ -0,0 +1,167 @@
1
+ import React from 'react';
2
+ import renderer from 'react-test-renderer';
3
+ import configureStore from 'redux-mock-store';
4
+ import { Provider } from 'react-intl-redux';
5
+ import Body from './Body';
6
+ import { getVideoIDAndPlaceholder } from './Body';
7
+ import config from '@plone/volto/registry';
8
+
9
+ config.blocks.blocksConfig = {
10
+ video: {
11
+ id: 'video',
12
+ title: 'Video',
13
+ group: 'media',
14
+ extensions: {},
15
+ variations: [],
16
+ restricted: false,
17
+ mostUsed: true,
18
+ sidebarTab: 1,
19
+ security: {
20
+ addPermission: [],
21
+ view: [],
22
+ },
23
+ },
24
+ };
25
+
26
+ const mockStore = configureStore();
27
+
28
+ test('renders a youtube video component with "list" in its url', () => {
29
+ const url =
30
+ 'https://www.youtube.com/watch?v=KwRSRRyuk-Q&list=PLGN9BI-OAQkQmEqf6O8jeyoFY1b2hD1uL&index=1';
31
+ const videoDetails = getVideoIDAndPlaceholder(url);
32
+ expect(videoDetails).toEqual({
33
+ videoID: null,
34
+ listID: 'PLGN9BI-OAQkQmEqf6O8jeyoFY1b2hD1uL&index=1',
35
+ thumbnailURL: 'https://img.youtube.com/vi/KwRSRRyuk-Q/sddefault.jpg',
36
+ });
37
+ });
38
+
39
+ test('extracts video details from a youtube video with "/live/" in its url', () => {
40
+ const url = 'https://www.youtube.com/live/ISdHvS6Ck3k?si=COeVakmC1lI6jQy3';
41
+ const videoDetails = getVideoIDAndPlaceholder(url);
42
+ expect(videoDetails).toEqual({
43
+ videoID: 'ISdHvS6Ck3k?si=COeVakmC1lI6jQy3',
44
+ listID: null,
45
+ thumbnailURL: 'https://img.youtube.com/vi/ISdHvS6Ck3k/sddefault.jpg',
46
+ });
47
+ });
48
+
49
+ test('extracts video details from a youtube video with ".be/" in its url', () => {
50
+ const url = 'https://youtu.be/P9j-xYdWT28?si=zZ2putStJbPBLCdt';
51
+ const videoDetails = getVideoIDAndPlaceholder(url);
52
+ expect(videoDetails).toEqual({
53
+ videoID: 'P9j-xYdWT28?si=zZ2putStJbPBLCdt',
54
+ listID: null,
55
+ thumbnailURL: 'https://img.youtube.com/vi/P9j-xYdWT28/sddefault.jpg',
56
+ });
57
+ });
58
+
59
+ test('extracts video details from a youtube video with "?v=" in its url', () => {
60
+ const url = 'https://www.youtube.com/watch?v=KUd6e105u_I';
61
+ const videoDetails = getVideoIDAndPlaceholder(url);
62
+ expect(videoDetails).toEqual({
63
+ videoID: 'KUd6e105u_I',
64
+ listID: null,
65
+ thumbnailURL: 'https://img.youtube.com/vi/KUd6e105u_I/sddefault.jpg',
66
+ });
67
+ });
68
+
69
+ test('extracts video details from a vimeo video url', () => {
70
+ const url = 'https://vimeo.com/639449679';
71
+ const videoDetails = getVideoIDAndPlaceholder(url);
72
+ expect(videoDetails).toEqual({
73
+ videoID: '639449679',
74
+ listID: null,
75
+ thumbnailURL: 'https://vumbnail.com/639449679.jpg',
76
+ });
77
+ });
78
+
79
+ test('renders a youtube video body component', () => {
80
+ const store = mockStore({
81
+ intl: {
82
+ locale: 'en',
83
+ messages: {},
84
+ },
85
+ });
86
+
87
+ const component = renderer.create(
88
+ <Provider store={store}>
89
+ <Body
90
+ data={{
91
+ '@type': 'video',
92
+ url: 'https://www.youtube.com/watch?v=KwRSRRyuk-Q&list=PLGN9BI-OAQkQmEqf6O8jeyoFY1b2hD1uL&index=1',
93
+ }}
94
+ />
95
+ </Provider>,
96
+ );
97
+ const json = component.toJSON();
98
+ expect(json).toMatchSnapshot();
99
+ });
100
+
101
+ test('renders a youtube video body component in edit mode', () => {
102
+ const store = mockStore({
103
+ intl: {
104
+ locale: 'en',
105
+ messages: {},
106
+ },
107
+ });
108
+
109
+ const component = renderer.create(
110
+ <Provider store={store}>
111
+ <Body
112
+ data={{
113
+ '@type': 'video',
114
+ url: 'https://www.youtube.com/watch?v=KwRSRRyuk-Q&list=PLGN9BI-OAQkQmEqf6O8jeyoFY1b2hD1uL&index=1',
115
+ }}
116
+ isEditMode={true}
117
+ />
118
+ </Provider>,
119
+ );
120
+ const json = component.toJSON();
121
+ expect(json).toMatchSnapshot();
122
+ });
123
+
124
+ test('renders invalid video body component with invalid URL', () => {
125
+ const store = mockStore({
126
+ intl: {
127
+ locale: 'en',
128
+ messages: {},
129
+ },
130
+ });
131
+
132
+ const component = renderer.create(
133
+ <Provider store={store}>
134
+ <Body
135
+ data={{
136
+ '@type': 'video',
137
+ url: 'https://www.google.com',
138
+ }}
139
+ />
140
+ </Provider>,
141
+ );
142
+ const json = component.toJSON();
143
+ expect(json).toMatchSnapshot();
144
+ });
145
+
146
+ test('renders a error message for invalid video URL in edit mode', () => {
147
+ const store = mockStore({
148
+ intl: {
149
+ locale: 'en',
150
+ messages: {},
151
+ },
152
+ });
153
+
154
+ const component = renderer.create(
155
+ <Provider store={store}>
156
+ <Body
157
+ data={{
158
+ '@type': 'video',
159
+ url: 'https://www.google.com',
160
+ }}
161
+ isEditMode={true}
162
+ />
163
+ </Provider>,
164
+ );
165
+ const json = component.toJSON();
166
+ expect(json).toMatchSnapshot();
167
+ });
@@ -18,7 +18,7 @@ const messages = defineMessages({
18
18
  });
19
19
 
20
20
  const VideoSidebar = (props) => {
21
- const { data, block, onChangeBlock } = props;
21
+ const { data, block, onChangeBlock, navRoot, contentType } = props;
22
22
  const intl = useIntl();
23
23
  const schema = VideoBlockSchema({ ...props, intl });
24
24
 
@@ -42,6 +42,8 @@ const VideoSidebar = (props) => {
42
42
  onChangeBlock={onChangeBlock}
43
43
  formData={data}
44
44
  block={block}
45
+ navRoot={navRoot}
46
+ contentType={contentType}
45
47
  />
46
48
  )}
47
49
  </>
@@ -3,7 +3,7 @@
3
3
  * TODO Enrich with features of user control panel. Then replace user control panel.
4
4
  */
5
5
  import React, { useEffect } from 'react';
6
- import { find, toNumber } from 'lodash';
6
+ import { find } from 'lodash';
7
7
  import { Portal } from 'react-portal';
8
8
  import { useHistory } from 'react-router';
9
9
  import { Link, useLocation } from 'react-router-dom';
@@ -38,7 +38,7 @@ const UserGroupMembershipPanel = () => {
38
38
  (state) => state.controlpanels.systeminformation,
39
39
  );
40
40
  const can_use_group_membership_panel = systeminformation
41
- ? toNumber(systeminformation?.plone_restapi_version.slice(0, 4)) >= 8.24
41
+ ? parseFloat(systeminformation?.plone_restapi_version.slice(0, 4)) >= 8.24
42
42
  : false;
43
43
  const actions = useSelector((state) => state.actions?.actions ?? {});
44
44
  const ploneSetupAction = find(actions.user, {
@@ -286,6 +286,7 @@ class Edit extends Component {
286
286
  <Form
287
287
  isEditForm
288
288
  ref={this.form}
289
+ navRoot={this.props.content?.['@components']?.navroot?.navroot || {}}
289
290
  schema={this.props.schema}
290
291
  type={this.props.content?.['@type']}
291
292
  formData={this.props.content}
@@ -148,7 +148,10 @@ class Form extends Component {
148
148
  ...formData,
149
149
  };
150
150
  }
151
- // defaults for block editor; should be moved to schema on server side
151
+
152
+ // We initialize the formData snapshot in here, before the initial data checks
153
+ const initialFormData = cloneDeep(formData);
154
+
152
155
  // Adding fallback in case the fields are empty, so we are sure that the edit form
153
156
  // shows at least the default blocks
154
157
  if (
@@ -200,7 +203,7 @@ class Form extends Component {
200
203
 
201
204
  this.state = {
202
205
  formData,
203
- initialFormData: cloneDeep(formData),
206
+ initialFormData,
204
207
  errors: {},
205
208
  selected: selectedBlock,
206
209
  multiSelected: [],
@@ -541,7 +544,13 @@ class Form extends Component {
541
544
  */
542
545
  render() {
543
546
  const { settings } = config;
544
- const { schema: originalSchema, onCancel, onSubmit } = this.props;
547
+ const {
548
+ schema: originalSchema,
549
+ onCancel,
550
+ onSubmit,
551
+ navRoot,
552
+ type,
553
+ } = this.props;
545
554
  const { formData } = this.state;
546
555
  const schema = this.removeBlocksLayoutFields(originalSchema);
547
556
  const Container =
@@ -590,6 +599,8 @@ class Form extends Component {
590
599
  onChangeField={this.onChangeField}
591
600
  onSelectBlock={this.onSelectBlock}
592
601
  properties={formData}
602
+ navRoot={navRoot}
603
+ type={type}
593
604
  pathname={this.props.pathname}
594
605
  selectedBlock={this.state.selected}
595
606
  multiSelected={this.state.multiSelected}
@@ -9,7 +9,7 @@ const DEFAULT_TIMEOUT = 500;
9
9
  const SidebarPopup = (props) => {
10
10
  const { children, open, onClose, overlay } = props;
11
11
 
12
- const asideElement = React.createRef();
12
+ const asideElement = React.useRef();
13
13
 
14
14
  const handleClickOutside = (e) => {
15
15
  if (asideElement && doesNodeContainClick(asideElement.current, e)) return;
@@ -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);
@@ -36,7 +36,6 @@ import {
36
36
  FREQUENCES,
37
37
  WEEKLY_DAYS,
38
38
  MONDAYFRIDAY_DAYS,
39
- toISOString,
40
39
  rrulei18n,
41
40
  } from './Utils';
42
41
 
@@ -218,26 +217,41 @@ class RecurrenceWidget extends Component {
218
217
 
219
218
  componentDidUpdate(prevProps) {
220
219
  if (this.props.value) {
221
- if (prevProps.formData?.start !== this.props.formData?.start) {
222
- let start = this.getUTCDate(this.props.formData?.start)
223
- .startOf('day')
224
- .toDate();
225
-
226
- this.setState((prevState) => {
227
- let rruleSet = prevState.rruleSet;
228
-
229
- rruleSet = this.updateRruleSet(
230
- rruleSet,
231
- prevState.formValues,
232
- 'dtstart',
233
- start,
234
- );
235
-
236
- return {
237
- ...prevState,
238
- rruleSet,
239
- };
240
- });
220
+ const changedStart =
221
+ prevProps.formData?.start !== this.props.formData?.start;
222
+ const changedEnd = prevProps.formData?.end !== this.props.formData?.end;
223
+
224
+ if (changedStart || changedEnd) {
225
+ let start = this.getUTCDate(this.props.formData?.start).toDate();
226
+ // let end = this.getUTCDate(this.props.formData?.end).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
+ );
241
255
  }
242
256
  }
243
257
  }
@@ -249,7 +263,7 @@ class RecurrenceWidget extends Component {
249
263
  setRecurrenceStartEnd = () => {
250
264
  const start = this.props.formData?.start;
251
265
 
252
- let _start = this.getUTCDate(start).startOf('day').toDate();
266
+ const _start = new Date(start); //The date is already in utc from plone, so this is not necessary: this.getUTCDate(start).startOf('day').toDate();
253
267
 
254
268
  this.setState((prevState) => {
255
269
  let rruleSet = prevState.rruleSet;
@@ -338,7 +352,7 @@ class RecurrenceWidget extends Component {
338
352
  case 'until':
339
353
  if (value != null) {
340
354
  formValues['recurrenceEnds'] = option;
341
- formValues[option] = toISOString(value);
355
+ formValues[option] = value;
342
356
  }
343
357
  break;
344
358
  case 'byweekday':
@@ -421,7 +435,24 @@ class RecurrenceWidget extends Component {
421
435
  }
422
436
  break;
423
437
  case 'until':
424
- value = value ? this.moment(new Date(value)).utc().toDate() : null;
438
+ let mDate = null;
439
+ if (value) {
440
+ mDate = this.moment(new Date(value));
441
+ if (typeof value === 'string') {
442
+ mDate = this.moment(new Date(value));
443
+ } else {
444
+ //object-->Date()
445
+ mDate = this.moment(value);
446
+ }
447
+
448
+ if (this.props.formData.end) {
449
+ //set time from formData.end
450
+ const mEnd = this.moment(new Date(this.props.formData.end));
451
+ mDate.set('hour', mEnd.get('hour'));
452
+ mDate.set('minute', mEnd.get('minute'));
453
+ }
454
+ }
455
+ value = value ? mDate.toDate() : null;
425
456
  break;
426
457
  default:
427
458
  break;
@@ -446,7 +477,7 @@ class RecurrenceWidget extends Component {
446
477
  ? value
447
478
  : rruleSet.dtstart()
448
479
  ? rruleSet.dtstart()
449
- : this.moment().utc().toDate();
480
+ : new Date();
450
481
  var exdates =
451
482
  field === 'exdates' ? value : Object.assign([], rruleSet.exdates());
452
483
 
@@ -470,12 +501,12 @@ class RecurrenceWidget extends Component {
470
501
  getDefaultUntil = (freq) => {
471
502
  const moment = this.moment;
472
503
  var end = this.props.formData?.end
473
- ? toISOString(this.getUTCDate(this.props.formData.end).toDate())
504
+ ? moment(new Date(this.props.formData.end))
474
505
  : null;
475
- var tomorrow = toISOString(moment().add(1, 'days').utc().toDate());
476
- var nextWeek = toISOString(moment().add(7, 'days').utc().toDate());
477
- var nextMonth = toISOString(moment().add(1, 'months').utc().toDate());
478
- var nextYear = toISOString(moment().add(1, 'years').utc().toDate());
506
+ var tomorrow = moment().add(1, 'days');
507
+ var nextWeek = moment().add(7, 'days');
508
+ var nextMonth = moment().add(1, 'months');
509
+ var nextYear = moment().add(1, 'years');
479
510
 
480
511
  var until = end;
481
512
  switch (freq) {
@@ -500,6 +531,19 @@ class RecurrenceWidget extends Component {
500
531
  default:
501
532
  break;
502
533
  }
534
+ if (this.props.formData.end) {
535
+ //set default end time
536
+ until.set('hour', end.get('hour'));
537
+ until.set('minute', end.get('minute'));
538
+ }
539
+
540
+ until = new Date(
541
+ until.get('year'),
542
+ until.get('month'),
543
+ until.get('date'),
544
+ until.get('hour'),
545
+ until.get('minute'),
546
+ );
503
547
 
504
548
  return until;
505
549
  };
@@ -715,9 +759,13 @@ class RecurrenceWidget extends Component {
715
759
  }
716
760
  };
717
761
 
718
- save = () => {
762
+ saveRrule = () => {
719
763
  var value = this.state.rruleSet.toString();
720
764
  this.props.onChange(this.props.id, value);
765
+ };
766
+
767
+ save = () => {
768
+ this.saveRrule();
721
769
  this.close();
722
770
  };
723
771
 
@@ -45,8 +45,10 @@ export default function Image({
45
45
  if (!image) return null;
46
46
 
47
47
  const isSvg = image['content-type'] === 'image/svg+xml';
48
+ // In case `base_path` is present (`preview_image_link`) use it as base path
49
+ const basePath = image.base_path || item['@id'];
48
50
 
49
- attrs.src = `${flattenToAppURL(item['@id'])}/${image.download}`;
51
+ attrs.src = `${flattenToAppURL(basePath)}/${image.download}`;
50
52
  attrs.width = image.width;
51
53
  attrs.height = image.height;
52
54
  attrs.className = cx(className, { responsive });
@@ -61,7 +63,7 @@ export default function Image({
61
63
  attrs.srcSet = sortedScales
62
64
  .map(
63
65
  (scale) =>
64
- `${flattenToAppURL(item['@id'])}/${scale.download} ${scale.width}w`,
66
+ `${flattenToAppURL(basePath)}/${scale.download} ${scale.width}w`,
65
67
  )
66
68
  .join(', ');
67
69
  }
@@ -113,6 +113,38 @@ test('renders an image component from a catalog brain', () => {
113
113
  expect(json).toMatchSnapshot();
114
114
  });
115
115
 
116
+ test('renders an image component from a catalog brain using `preview_image_link`', () => {
117
+ const component = renderer.create(
118
+ <Image
119
+ item={{
120
+ '@id': 'http://localhost:3000/blog/blog-post',
121
+ image_field: 'preview_image_link',
122
+ image_scales: {
123
+ preview_image_link: [
124
+ {
125
+ base_path: '/image.png',
126
+ download: '@@images/image.png',
127
+ width: 400,
128
+ height: 400,
129
+ scales: {
130
+ preview: {
131
+ download: '@@images/image-400.png',
132
+ width: 400,
133
+ height: 400,
134
+ },
135
+ },
136
+ },
137
+ ],
138
+ },
139
+ }}
140
+ imageField="preview_image_link"
141
+ alt="alt text"
142
+ />,
143
+ );
144
+ const json = component.toJSON();
145
+ expect(json).toMatchSnapshot();
146
+ });
147
+
116
148
  test('renders an image component from a string src', () => {
117
149
  const component = renderer.create(
118
150
  <Image
@@ -77,8 +77,9 @@ const Login = (props) => {
77
77
  qs.parse(props.location?.search ?? location.search).return_url ||
78
78
  location.pathname.replace(/\/login\/?$/, '').replace(/\/logout\/?$/, '') ||
79
79
  '/';
80
+
80
81
  useEffect(() => {
81
- if (token && !props.isLogout) {
82
+ if (token && !(props.isLogout || location?.state?.isLogout)) {
82
83
  history.push(returnUrl || '/');
83
84
  if (toast.isActive('loggedOut')) {
84
85
  toast.dismiss('loggedOut');
@@ -108,7 +109,16 @@ const Login = (props) => {
108
109
  dispatch(resetLoginRequest());
109
110
  }
110
111
  };
111
- }, [dispatch, token, error, intl, history, returnUrl, props.isLogout]);
112
+ }, [
113
+ dispatch,
114
+ token,
115
+ error,
116
+ intl,
117
+ history,
118
+ returnUrl,
119
+ props.isLogout,
120
+ location?.state?.isLogout,
121
+ ]);
112
122
 
113
123
  const onLogin = (event) => {
114
124
  dispatch(