@plone/volto 18.0.0-alpha.2 → 18.0.0-alpha.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 (74) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/apps/plone/node_modules/.bin/acorn +17 -0
  3. package/apps/plone/node_modules/.bin/addon +17 -0
  4. package/apps/plone/node_modules/.bin/autoprefixer +17 -0
  5. package/apps/plone/node_modules/.bin/browserslist +17 -0
  6. package/apps/plone/node_modules/.bin/build-storybook +17 -0
  7. package/apps/plone/node_modules/.bin/changelogupdater +17 -0
  8. package/apps/plone/node_modules/.bin/eslint +17 -0
  9. package/apps/plone/node_modules/.bin/eslint-config-prettier +17 -0
  10. package/apps/plone/node_modules/.bin/i18n +17 -0
  11. package/apps/plone/node_modules/.bin/jest +17 -0
  12. package/apps/plone/node_modules/.bin/lessc +17 -0
  13. package/apps/plone/node_modules/.bin/missdev +17 -0
  14. package/apps/plone/node_modules/.bin/prettier +17 -0
  15. package/apps/plone/node_modules/.bin/razzle +17 -0
  16. package/apps/plone/node_modules/.bin/server-test +17 -0
  17. package/apps/plone/node_modules/.bin/start-server-and-test +17 -0
  18. package/apps/plone/node_modules/.bin/start-storybook +17 -0
  19. package/apps/plone/node_modules/.bin/start-test +17 -0
  20. package/apps/plone/node_modules/.bin/storybook-server +17 -0
  21. package/apps/plone/node_modules/.bin/stylelint +17 -0
  22. package/apps/plone/node_modules/.bin/tlds +17 -0
  23. package/apps/plone/node_modules/.bin/ts-jest +17 -0
  24. package/apps/plone/node_modules/.bin/tsc +17 -0
  25. package/apps/plone/node_modules/.bin/tsserver +17 -0
  26. package/apps/plone/node_modules/.bin/uuid +17 -0
  27. package/apps/plone/node_modules/.bin/webpack +17 -0
  28. package/apps/plone/node_modules/.bin/webpack-dev-server +17 -0
  29. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/acorn +17 -0
  30. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/addon +17 -0
  31. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/browserslist +17 -0
  32. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/changelogupdater +17 -0
  33. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/eslint +17 -0
  34. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/eslint-config-prettier +17 -0
  35. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/i18n +17 -0
  36. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/prettier +17 -0
  37. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/release-it +17 -0
  38. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/stylelint +17 -0
  39. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/tsc +17 -0
  40. package/apps/plone/src/addons/volto-volto-project/node_modules/.bin/tsserver +17 -0
  41. package/locales/ca/LC_MESSAGES/volto.po +1 -0
  42. package/locales/de/LC_MESSAGES/volto.po +1 -0
  43. package/locales/en/LC_MESSAGES/volto.po +1 -0
  44. package/locales/es/LC_MESSAGES/volto.po +1 -0
  45. package/locales/eu/LC_MESSAGES/volto.po +1 -0
  46. package/locales/fi/LC_MESSAGES/volto.po +1 -0
  47. package/locales/fr/LC_MESSAGES/volto.po +1 -0
  48. package/locales/it/LC_MESSAGES/volto.po +1 -0
  49. package/locales/ja/LC_MESSAGES/volto.po +1 -0
  50. package/locales/nl/LC_MESSAGES/volto.po +1 -0
  51. package/locales/pt/LC_MESSAGES/volto.po +1 -0
  52. package/locales/pt_BR/LC_MESSAGES/volto.po +1 -0
  53. package/locales/ro/LC_MESSAGES/volto.po +1 -0
  54. package/locales/volto.pot +2 -1
  55. package/locales/zh_CN/LC_MESSAGES/volto.po +1 -0
  56. package/package.json +2 -2
  57. package/packages/registry/node_modules/.bin/browserslist +17 -0
  58. package/packages/registry/node_modules/.bin/parcel +17 -0
  59. package/packages/registry/node_modules/.bin/release-it +17 -0
  60. package/packages/registry/node_modules/.bin/tsc +17 -0
  61. package/packages/registry/node_modules/.bin/tsserver +17 -0
  62. package/packages/registry/node_modules/.bin/vitest +17 -0
  63. package/packages/types/node_modules/.bin/browserslist +17 -0
  64. package/packages/types/node_modules/.bin/parcel +17 -0
  65. package/packages/types/node_modules/.bin/release-it +17 -0
  66. package/packages/types/node_modules/.bin/tsc +17 -0
  67. package/packages/types/node_modules/.bin/tsserver +17 -0
  68. package/packages/volto-slate/package.json +1 -1
  69. package/src/components/manage/Blocks/Video/Body.jsx +52 -22
  70. package/src/components/manage/Blocks/Video/Body.test.jsx +167 -0
  71. package/src/components/theme/Breadcrumbs/Breadcrumbs.jsx +19 -2
  72. package/src/components/theme/ContentMetadataTags/ContentMetadataTags.jsx +46 -39
  73. package/types/components/manage/Blocks/Video/Body.d.ts +5 -0
  74. package/types/components/manage/Blocks/Video/Body.test.d.ts +1 -0
@@ -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
+ });
@@ -1,6 +1,6 @@
1
1
  import { useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Link } from 'react-router-dom';
3
+ import { Link, useLocation } from 'react-router-dom';
4
4
  import { Breadcrumb, Container, Segment } from 'semantic-ui-react';
5
5
  import { defineMessages, useIntl } from 'react-intl';
6
6
  import { useDispatch, useSelector, shallowEqual } from 'react-redux';
@@ -19,13 +19,30 @@ const messages = defineMessages({
19
19
  id: 'Breadcrumbs',
20
20
  defaultMessage: 'Breadcrumbs',
21
21
  },
22
+ controlpanel: {
23
+ id: 'Site Setup',
24
+ defaultMessage: 'Site Setup',
25
+ },
22
26
  });
23
27
 
24
28
  const BreadcrumbsComponent = ({ pathname }) => {
25
29
  const intl = useIntl();
26
30
  const dispatch = useDispatch();
31
+ const { pathname: realPath } = useLocation();
32
+ const controlpanelItems = [
33
+ {
34
+ url: '/controlpanel',
35
+ title: intl.formatMessage(messages.controlpanel),
36
+ },
37
+ ];
27
38
 
28
- const items = useSelector((state) => state.breadcrumbs.items, shallowEqual);
39
+ const items = useSelector(
40
+ (state) =>
41
+ realPath.startsWith('/controlpanel')
42
+ ? controlpanelItems
43
+ : state.breadcrumbs.items,
44
+ shallowEqual,
45
+ );
29
46
  const root = useSelector((state) => state.breadcrumbs.root);
30
47
 
31
48
  useEffect(() => {
@@ -1,5 +1,13 @@
1
- import { toPublicURL, Helmet } from '@plone/volto/helpers';
1
+ import React, { useEffect } from 'react';
2
+ import {
3
+ toPublicURL,
4
+ Helmet,
5
+ hasApiExpander,
6
+ getBaseUrl,
7
+ } from '@plone/volto/helpers';
8
+ import { getNavroot } from '@plone/volto/actions';
2
9
  import config from '@plone/volto/registry';
10
+ import { useDispatch, useSelector } from 'react-redux';
3
11
 
4
12
  const ContentMetadataTags = (props) => {
5
13
  const {
@@ -13,23 +21,21 @@ const ContentMetadataTags = (props) => {
13
21
  description,
14
22
  } = props.content;
15
23
 
24
+ const dispatch = useDispatch();
25
+ const pathname = useSelector((state) => state.router.location.pathname);
26
+ const navroot = useSelector((state) => state.navroot?.data?.navroot);
27
+ const site = useSelector((state) => state.site?.data);
28
+
29
+ useEffect(() => {
30
+ if (pathname && !hasApiExpander('navroot', getBaseUrl(pathname))) {
31
+ dispatch(getNavroot(getBaseUrl(pathname)));
32
+ }
33
+ }, [dispatch, pathname]);
34
+
16
35
  const getContentImageInfo = () => {
17
36
  const { contentMetadataTagsImageField } = config.settings;
18
- const image_field = props.content[contentMetadataTagsImageField];
19
- const preview_image = props.content.preview_image;
20
- const preview_image_link = props.content.preview_image_link;
37
+ const image = props.content[contentMetadataTagsImageField];
21
38
  const { opengraph_image } = props.content;
22
- let image = undefined;
23
-
24
- if (opengraph_image !== undefined && opengraph_image) {
25
- image = opengraph_image;
26
- } else if (preview_image_link !== undefined && preview_image_link) {
27
- image = preview_image_link[contentMetadataTagsImageField];
28
- } else if (preview_image !== undefined && preview_image) {
29
- image = preview_image;
30
- } else if (image_field !== undefined && image_field) {
31
- image = image_field;
32
- }
33
39
 
34
40
  const contentImageInfo = {
35
41
  contentHasImage: false,
@@ -37,7 +43,10 @@ const ContentMetadataTags = (props) => {
37
43
  height: null,
38
44
  width: null,
39
45
  };
40
- contentImageInfo.contentHasImage = image?.scales?.large?.download || false;
46
+ contentImageInfo.contentHasImage =
47
+ opengraph_image?.scales?.large?.download ||
48
+ image?.scales?.large?.download ||
49
+ false;
41
50
 
42
51
  if (contentImageInfo.contentHasImage && opengraph_image?.scales?.large) {
43
52
  contentImageInfo.url = opengraph_image.scales.large.download;
@@ -54,16 +63,35 @@ const ContentMetadataTags = (props) => {
54
63
 
55
64
  const contentImageInfo = getContentImageInfo();
56
65
 
66
+ const getTitle = () => {
67
+ const includeSiteTitle =
68
+ config?.settings?.siteTitleFormat?.includeSiteTitle || false;
69
+ const titleAndSiteTitleSeparator =
70
+ config?.settings?.titleAndSiteTitleSeparator || '-';
71
+ const navRootTitle = navroot?.title;
72
+ const siteRootTitle = site?.['plone.site_title'];
73
+ const titlePart = navRootTitle || siteRootTitle;
74
+
75
+ if (includeSiteTitle && titlePart && titlePart !== title) {
76
+ return seo_title || `${title} ${titleAndSiteTitleSeparator} ${titlePart}`;
77
+ } else {
78
+ return seo_title || title;
79
+ }
80
+ };
81
+
57
82
  return (
58
83
  <>
59
84
  <Helmet>
60
- <title>{(seo_title || title)?.replace(/\u00AD/g, '')}</title>
85
+ <title>{getTitle()?.replace(/\u00AD/g, '')}</title>
86
+ <link
87
+ rel="canonical"
88
+ href={seo_canonical_url || toPublicURL(props.content['@id'])}
89
+ />
61
90
  <meta name="description" content={seo_description || description} />
62
91
  <meta
63
92
  property="og:title"
64
93
  content={opengraph_title || seo_title || title}
65
94
  />
66
- <meta property="og:type" content={'website'} />
67
95
  <meta
68
96
  property="og:url"
69
97
  content={seo_canonical_url || toPublicURL(props.content['@id'])}
@@ -75,12 +103,6 @@ const ContentMetadataTags = (props) => {
75
103
  content={toPublicURL(contentImageInfo.url)}
76
104
  />
77
105
  )}
78
- {contentImageInfo.contentHasImage && (
79
- <meta
80
- property="twitter:image"
81
- content={toPublicURL(contentImageInfo.url)}
82
- />
83
- )}
84
106
  {contentImageInfo.contentHasImage && (
85
107
  <meta property="og:image:width" content={contentImageInfo.width} />
86
108
  )}
@@ -94,21 +116,6 @@ const ContentMetadataTags = (props) => {
94
116
  />
95
117
  )}
96
118
  <meta name="twitter:card" content="summary_large_image" />
97
- <meta
98
- property="twitter:url"
99
- content={seo_canonical_url || toPublicURL(props.content['@id'])}
100
- />
101
- {/* TODO: Improve SEO backend metadata providers by adding the twitter handler */}
102
- {/* <meta property="twitter:site" content={'@my_twitter_handler'} /> */}
103
- <meta
104
- property="twitter:title"
105
- content={opengraph_title || seo_title || title}
106
- />
107
- <meta
108
- property="twitter:description"
109
- content={seo_description || description}
110
- />
111
- <meta property="twitter:domain" content={config.settings.publicURL} />
112
119
  </Helmet>
113
120
  </>
114
121
  );
@@ -8,3 +8,8 @@ declare namespace Body {
8
8
  let data: any;
9
9
  }
10
10
  }
11
+ export function getVideoIDAndPlaceholder(url: any): {
12
+ videoID: any;
13
+ listID: any;
14
+ thumbnailURL: string;
15
+ };