@plone/volto 17.0.1 → 17.1.1

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 (35) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +36 -252
  3. package/addon-registry.js +6 -8
  4. package/cypress/support/commands.js +1 -2
  5. package/cypress/support/reset-fixture.js +2 -4
  6. package/jest-extender-plugin.js +3 -1
  7. package/locales/pt_BR/LC_MESSAGES/volto.po +1 -1
  8. package/locales/pt_BR.json +1 -1
  9. package/package.json +5 -4
  10. package/packages/volto-slate/package.json +1 -1
  11. package/pre-build-transpiling.js +1 -1
  12. package/razzle.config.js +6 -10
  13. package/src/components/manage/Blocks/LeadImage/LeadImageSidebar.test.jsx +3 -2
  14. package/src/components/manage/Blocks/LeadImage/View.jsx +1 -7
  15. package/src/components/manage/Blocks/LeadImage/View.test.jsx +22 -0
  16. package/src/components/manage/Blocks/LeadImage/utils.js +9 -0
  17. package/src/components/manage/Blocks/Listing/ListingData.jsx +3 -3
  18. package/src/components/manage/Blocks/Listing/ListingData.test.jsx +2 -0
  19. package/src/components/manage/UniversalLink/UniversalLink.jsx +3 -1
  20. package/src/components/theme/Comments/CommentEditModal.stories.jsx +64 -0
  21. package/src/components/theme/EventDetails/EventDetails.stories.jsx +88 -0
  22. package/src/components/theme/Header/Header.stories.jsx +47 -0
  23. package/src/components/theme/Image/Image.jsx +8 -11
  24. package/src/components/theme/Image/Image.test.jsx +11 -11
  25. package/src/components/theme/Login/Login.jsx +1 -1
  26. package/src/components/theme/Login/Login.stories.jsx +1 -1
  27. package/src/components/theme/Logo/Logo.Multilingual.test.jsx +5 -25
  28. package/src/components/theme/Logo/Logo.jsx +8 -6
  29. package/src/components/theme/Logo/Logo.stories.jsx +25 -1
  30. package/src/components/theme/Logo/Logo.test.jsx +6 -19
  31. package/src/components/theme/SearchWidget/SearchWidget.stories.jsx +36 -0
  32. package/src/config/Blocks.jsx +2 -0
  33. package/src/helpers/Url/Url.js +29 -0
  34. package/src/helpers/Url/Url.test.js +104 -0
  35. package/src/helpers/index.js +1 -0
@@ -0,0 +1,88 @@
1
+ import { injectIntl } from 'react-intl';
2
+ import React from 'react';
3
+ import EventDetailsComponent from './EventDetails';
4
+ import { RealStoreWrapper as Wrapper } from '@plone/volto/storybook';
5
+
6
+ const IntlEventDetailsComponent = injectIntl(EventDetailsComponent);
7
+
8
+ function StoryComponent(args) {
9
+ return (
10
+ <Wrapper
11
+ customStore={{
12
+ intl: {
13
+ locale: 'en',
14
+ messages: {},
15
+ },
16
+ }}
17
+ >
18
+ <div id="toolbar" style={{ display: 'none' }} />
19
+ <IntlEventDetailsComponent
20
+ content={{
21
+ '@id': 'http://localhost:8080/Plone/my-page',
22
+ title: 'Hello World!',
23
+ description: 'Hi',
24
+ text: {
25
+ data: '<p>Hello World!</p>',
26
+ },
27
+ ...args,
28
+ }}
29
+ />
30
+ </Wrapper>
31
+ );
32
+ }
33
+
34
+ export const EventDetails = StoryComponent.bind({});
35
+ EventDetails.args = {
36
+ attendees: ['John Doe', 'Mario Rossi'],
37
+ contact_email: 'test@example.com',
38
+ contact_name: 'John Doe',
39
+ contact_phone: '0123456789',
40
+ end: '2019-06-24T15:20:00+00:00',
41
+ event_url: 'https://www.example.com',
42
+ location: 'Volto, Plone',
43
+ open_end: false,
44
+ recurrence: 'RRULE:FREQ=DAILY;INTERVAL=7;COUNT=7',
45
+ start: '2019-06-23T15:20:00+00:00',
46
+ subjects: ['Guillotina', 'Volto'],
47
+ whole_day: false,
48
+ };
49
+ export default {
50
+ title: 'Public components/EventDetails',
51
+ component: EventDetails,
52
+ decorators: [
53
+ (Story) => (
54
+ <div className="ui segment form attached" style={{ width: '400px' }}>
55
+ <Story />
56
+ </div>
57
+ ),
58
+ ],
59
+ argTypes: {
60
+ contact_email: {
61
+ description: 'email address',
62
+ },
63
+ contact_name: {
64
+ description: 'name of the attendee',
65
+ },
66
+ contact_phone: {
67
+ description: 'contact phone of the attendee',
68
+ },
69
+ end: {
70
+ control: { type: 'date' },
71
+ description: 'end date/time of the event',
72
+ },
73
+ event_url: {
74
+ description: 'event url',
75
+ },
76
+ start: {
77
+ control: { type: 'date' },
78
+ context: 'time',
79
+ description: 'start date/time of the event',
80
+ },
81
+ whole_day: {
82
+ description: 'whether the event will be whole day or not',
83
+ },
84
+ subjects: {
85
+ description: 'subjects covered in the event meetings',
86
+ },
87
+ },
88
+ };
@@ -0,0 +1,47 @@
1
+ import { injectIntl } from 'react-intl';
2
+ import React from 'react';
3
+ import HeaderComponent from './Header';
4
+ import { RealStoreWrapper as Wrapper } from '@plone/volto/storybook';
5
+
6
+ const IntlHeaderComponent = injectIntl(HeaderComponent);
7
+
8
+ function StoryComponent(args) {
9
+ return (
10
+ <Wrapper
11
+ customStore={{
12
+ userSession: { ...args },
13
+ intl: {
14
+ locale: 'en',
15
+ messages: {},
16
+ },
17
+ }}
18
+ >
19
+ <div id="toolbar" style={{ display: 'none' }} />
20
+ <IntlHeaderComponent pathname="/blog" />
21
+ </Wrapper>
22
+ );
23
+ }
24
+
25
+ export const Default = StoryComponent.bind({});
26
+ Default.args = {
27
+ token: '',
28
+ };
29
+ export const Auth = StoryComponent.bind({});
30
+ Auth.args = {
31
+ token: '1234567890',
32
+ };
33
+
34
+ export default {
35
+ title: 'Public components/Header',
36
+ component: HeaderComponent,
37
+ decorators: [
38
+ (Story) => (
39
+ <div className="ui segment form attached" style={{ width: '900px' }}>
40
+ <Story />
41
+ </div>
42
+ ),
43
+ ],
44
+ argTypes: {
45
+ token: 'Token for userSession',
46
+ },
47
+ };
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import cx from 'classnames';
3
- import { flattenToAppURL } from '@plone/volto/helpers';
3
+ import { flattenToAppURL, flattenScales } from '@plone/volto/helpers';
4
4
 
5
5
  /**
6
6
  * Image component
@@ -36,22 +36,19 @@ export default function Image({
36
36
  const imageFieldWithDefault = imageField || item.image_field || 'image';
37
37
 
38
38
  const image = isFromRealObject
39
- ? item[imageFieldWithDefault]
40
- : item.image_scales[imageFieldWithDefault]?.[0];
39
+ ? flattenScales(item['@id'], item[imageFieldWithDefault])
40
+ : flattenScales(
41
+ item['@id'],
42
+ item.image_scales[imageFieldWithDefault]?.[0],
43
+ );
41
44
 
42
45
  if (!image) return null;
43
46
 
44
47
  const isSvg = image['content-type'] === 'image/svg+xml';
45
48
 
46
- const baseUrl = isFromRealObject ? '' : flattenToAppURL(item['@id'] + '/');
47
-
48
- attrs.src = `${baseUrl}${flattenToAppURL(image.download)}`;
49
+ attrs.src = `${flattenToAppURL(item['@id'])}/${image.download}`;
49
50
  attrs.width = image.width;
50
51
  attrs.height = image.height;
51
- attrs.style = {
52
- aspectRatio: `${image.width} / ${image.height}`,
53
- ...imageProps.style,
54
- };
55
52
  attrs.className = cx(className, { responsive });
56
53
 
57
54
  if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
@@ -64,7 +61,7 @@ export default function Image({
64
61
  attrs.srcSet = sortedScales
65
62
  .map(
66
63
  (scale) =>
67
- `${baseUrl}${flattenToAppURL(scale.download)} ${scale.width}w`,
64
+ `${flattenToAppURL(item['@id'])}/${scale.download} ${scale.width}w`,
68
65
  )
69
66
  .join(', ');
70
67
  }
@@ -6,14 +6,14 @@ test('renders an image component with fetchpriority high', () => {
6
6
  const component = renderer.create(
7
7
  <Image
8
8
  item={{
9
+ '@id': 'http://localhost:3000/image',
9
10
  image: {
10
- download: 'http://localhost:3000/image/@@images/image/image.png',
11
+ download: 'http://localhost:3000/image/@@images/image.png',
11
12
  width: 400,
12
13
  height: 400,
13
14
  scales: {
14
15
  preview: {
15
- download:
16
- 'http://localhost:3000/image/@@images/image/image-400.png',
16
+ download: 'http://localhost:3000/image/@@images/image-400.png',
17
17
  width: 400,
18
18
  height: 400,
19
19
  },
@@ -32,14 +32,14 @@ test('renders an image component with lazy loading', () => {
32
32
  const component = renderer.create(
33
33
  <Image
34
34
  item={{
35
+ '@id': 'http://localhost:3000/image',
35
36
  image: {
36
- download: 'http://localhost:3000/image/@@images/image/image.png',
37
+ download: 'http://localhost:3000/image/@@images/image.png',
37
38
  width: 400,
38
39
  height: 400,
39
40
  scales: {
40
41
  preview: {
41
- download:
42
- 'http://localhost:3000/image/@@images/image/image-400.png',
42
+ download: 'http://localhost:3000/image/@@images/image-400.png',
43
43
  width: 400,
44
44
  height: 400,
45
45
  },
@@ -59,14 +59,14 @@ test('renders an image component with responsive class', () => {
59
59
  const component = renderer.create(
60
60
  <Image
61
61
  item={{
62
+ '@id': 'http://localhost:3000/image',
62
63
  image: {
63
- download: 'http://localhost:3000/image/@@images/image/image.png',
64
+ download: 'http://localhost:3000/image/@@images/image-1200.png',
64
65
  width: 400,
65
66
  height: 400,
66
67
  scales: {
67
68
  preview: {
68
- download:
69
- 'http://localhost:3000/image/@@images/image/image-400.png',
69
+ download: 'http://localhost:3000/image/@@images/image-400.png',
70
70
  width: 400,
71
71
  height: 400,
72
72
  },
@@ -91,12 +91,12 @@ test('renders an image component from a catalog brain', () => {
91
91
  image_scales: {
92
92
  image: [
93
93
  {
94
- download: '@@images/image/image.png',
94
+ download: '@@images/image.png',
95
95
  width: 400,
96
96
  height: 400,
97
97
  scales: {
98
98
  preview: {
99
- download: '@@images/image/image-400.png',
99
+ download: '@@images/image-400.png',
100
100
  width: 400,
101
101
  height: 400,
102
102
  },
@@ -74,7 +74,7 @@ const Login = (props) => {
74
74
  const error = useSelector((state) => state.userSession.login.error);
75
75
  const loading = useSelector((state) => state.userSession.login.loading);
76
76
  const returnUrl =
77
- qs.parse(props.location.search ?? location.search).return_url ||
77
+ qs.parse(props.location?.search ?? location.search).return_url ||
78
78
  location.pathname.replace(/\/login\/?$/, '').replace(/\/logout\/?$/, '') ||
79
79
  '/';
80
80
  useEffect(() => {
@@ -18,7 +18,7 @@ const StoryComponent = (args) => {
18
18
  return (
19
19
  <Provider store={store}>
20
20
  <Wrapper>
21
- <Login />
21
+ <Login location={{ search: {} }} />
22
22
  </Wrapper>
23
23
  </Provider>
24
24
  );
@@ -24,23 +24,18 @@ describe('Multilingual Logo', () => {
24
24
  data: {
25
25
  id: 'http://localhost:3000/@navroot',
26
26
  navroot: {
27
- '@id': 'http://localhost:3000',
27
+ '@id': 'http://localhost:3000/en',
28
28
  title: 'Plone Site',
29
29
  },
30
30
  },
31
31
  },
32
- router: {
33
- location: {
34
- pathname: '/',
35
- },
36
- },
37
32
  site: {
38
33
  data: {},
39
34
  },
40
35
  });
41
36
  const component = renderer.create(
42
37
  <Provider store={store}>
43
- <MemoryRouter>
38
+ <MemoryRouter initialEntries={[{ pathname: '/' }]}>
44
39
  <Logo />
45
40
  </MemoryRouter>
46
41
  </Provider>,
@@ -64,11 +59,6 @@ describe('Multilingual Logo', () => {
64
59
  },
65
60
  },
66
61
  },
67
- router: {
68
- location: {
69
- pathname: '/en',
70
- },
71
- },
72
62
  site: {
73
63
  data: {
74
64
  'plone.site_title': 'Plone Site',
@@ -77,7 +67,7 @@ describe('Multilingual Logo', () => {
77
67
  });
78
68
  const component = renderer.create(
79
69
  <Provider store={store}>
80
- <MemoryRouter>
70
+ <MemoryRouter initialEntries={[{ pathname: '/en' }]}>
81
71
  <Logo />
82
72
  </MemoryRouter>
83
73
  </Provider>,
@@ -101,11 +91,6 @@ describe('Multilingual Logo', () => {
101
91
  },
102
92
  },
103
93
  },
104
- router: {
105
- location: {
106
- pathname: '/en',
107
- },
108
- },
109
94
  site: {
110
95
  data: {
111
96
  'plone.site_logo':
@@ -115,7 +100,7 @@ describe('Multilingual Logo', () => {
115
100
  });
116
101
  const component = renderer.create(
117
102
  <Provider store={store}>
118
- <MemoryRouter>
103
+ <MemoryRouter initialEntries={[{ pathname: '/en' }]}>
119
104
  <Logo />
120
105
  </MemoryRouter>
121
106
  </Provider>,
@@ -139,11 +124,6 @@ describe('Multilingual Logo', () => {
139
124
  },
140
125
  },
141
126
  },
142
- router: {
143
- location: {
144
- pathname: '/en/my/path',
145
- },
146
- },
147
127
  site: {
148
128
  data: {
149
129
  'plone.site_logo':
@@ -153,7 +133,7 @@ describe('Multilingual Logo', () => {
153
133
  });
154
134
  const component = renderer.create(
155
135
  <Provider store={store}>
156
- <MemoryRouter>
136
+ <MemoryRouter initialEntries={[{ pathname: '/en/my/path' }]}>
157
137
  <Logo />
158
138
  </MemoryRouter>
159
139
  </Provider>,
@@ -7,12 +7,12 @@ import { Image } from 'semantic-ui-react';
7
7
  import { ConditionalLink } from '@plone/volto/components';
8
8
  import LogoImage from '@plone/volto/components/theme/Logo/Logo.svg';
9
9
  import { useSelector, useDispatch } from 'react-redux';
10
+ import { useLocation } from 'react-router-dom';
10
11
  import { getNavroot } from '@plone/volto/actions';
11
12
  import {
12
13
  flattenToAppURL,
13
14
  hasApiExpander,
14
15
  getBaseUrl,
15
- toPublicURL,
16
16
  } from '@plone/volto/helpers';
17
17
 
18
18
  /**
@@ -22,7 +22,7 @@ import {
22
22
  * @returns {string} Markup of the component.
23
23
  */
24
24
  const Logo = () => {
25
- const pathname = useSelector((state) => state.router.location.pathname);
25
+ const pathname = useLocation().pathname;
26
26
  const site = useSelector((state) => state.site.data);
27
27
  const navroot = useSelector((state) => state.navroot.data);
28
28
  const dispatch = useDispatch();
@@ -33,14 +33,16 @@ const Logo = () => {
33
33
  }
34
34
  }, [dispatch, pathname]);
35
35
 
36
- // remove trailing slash
37
- const currentURL = toPublicURL(pathname).replace(/\/$/, '');
36
+ const navRootPath = flattenToAppURL(navroot?.navroot?.['@id']) || '/';
37
+ const currentURLIsNavRoot = pathname !== navRootPath;
38
38
 
39
39
  return (
40
40
  <ConditionalLink
41
- href={navroot?.navroot?.['@id']}
41
+ href={navRootPath}
42
42
  title={navroot?.navroot?.title}
43
- condition={currentURL !== navroot?.navroot?.['@id']}
43
+ // In case that the content returns 404, there is no information about the portal
44
+ // then render the link anyways to get out of the Unauthorized page
45
+ condition={!navroot || currentURLIsNavRoot}
44
46
  >
45
47
  <Image
46
48
  src={
@@ -4,7 +4,31 @@ import Wrapper from '@plone/volto/storybook';
4
4
 
5
5
  const StoryComponent = (args) => {
6
6
  return (
7
- <Wrapper>
7
+ <Wrapper
8
+ customStore={{
9
+ intl: {
10
+ locale: 'en',
11
+ messages: {},
12
+ },
13
+ navroot: {
14
+ data: {
15
+ id: 'http://localhost:3000/@navroot',
16
+ navroot: {
17
+ '@id': 'http://localhost:3000',
18
+ title: 'Plone Site',
19
+ },
20
+ },
21
+ },
22
+ router: {
23
+ location: {
24
+ pathname: '/',
25
+ },
26
+ },
27
+ site: {
28
+ data: {},
29
+ },
30
+ }}
31
+ >
8
32
  <Logo />
9
33
  </Wrapper>
10
34
  );
@@ -30,18 +30,13 @@ describe('Logo', () => {
30
30
  },
31
31
  },
32
32
  },
33
- router: {
34
- location: {
35
- pathname: '/',
36
- },
37
- },
38
33
  site: {
39
34
  data: {},
40
35
  },
41
36
  });
42
37
  const component = renderer.create(
43
38
  <Provider store={store}>
44
- <MemoryRouter>
39
+ <MemoryRouter initialEntries={[{ pathname: '/' }]}>
45
40
  <Logo />
46
41
  </MemoryRouter>
47
42
  </Provider>,
@@ -64,11 +59,6 @@ describe('Logo', () => {
64
59
  },
65
60
  },
66
61
  },
67
- router: {
68
- location: {
69
- pathname: '/',
70
- },
71
- },
72
62
  site: {
73
63
  data: {
74
64
  'plone.site_logo':
@@ -78,7 +68,7 @@ describe('Logo', () => {
78
68
  });
79
69
  const component = renderer.create(
80
70
  <Provider store={store}>
81
- <MemoryRouter>
71
+ <MemoryRouter initialEntries={[{ pathname: '/' }]}>
82
72
  <Logo />
83
73
  </MemoryRouter>
84
74
  </Provider>,
@@ -86,6 +76,7 @@ describe('Logo', () => {
86
76
  const json = component.toJSON();
87
77
  expect(json).toMatchSnapshot();
88
78
  });
79
+
89
80
  it('renders a logo component with default config in a non-root url', () => {
90
81
  const store = mockStore({
91
82
  intl: {
@@ -115,7 +106,7 @@ describe('Logo', () => {
115
106
  });
116
107
  const component = renderer.create(
117
108
  <Provider store={store}>
118
- <MemoryRouter>
109
+ <MemoryRouter initialEntries={[{ pathname: '/some-page' }]}>
119
110
  <Logo />
120
111
  </MemoryRouter>
121
112
  </Provider>,
@@ -123,6 +114,7 @@ describe('Logo', () => {
123
114
  const json = component.toJSON();
124
115
  expect(json).toMatchSnapshot();
125
116
  });
117
+
126
118
  it('renders a logo component with a custom logo in a non-root url', () => {
127
119
  const store = mockStore({
128
120
  intl: {
@@ -138,11 +130,6 @@ describe('Logo', () => {
138
130
  },
139
131
  },
140
132
  },
141
- router: {
142
- location: {
143
- pathname: '/some-page',
144
- },
145
- },
146
133
  site: {
147
134
  data: {
148
135
  'plone.site_logo':
@@ -152,7 +139,7 @@ describe('Logo', () => {
152
139
  });
153
140
  const component = renderer.create(
154
141
  <Provider store={store}>
155
- <MemoryRouter>
142
+ <MemoryRouter initialEntries={[{ pathname: '/some-page' }]}>
156
143
  <Logo />
157
144
  </MemoryRouter>
158
145
  </Provider>,
@@ -0,0 +1,36 @@
1
+ import { injectIntl } from 'react-intl';
2
+ import React from 'react';
3
+ import SearchWidgetComponent from './SearchWidget';
4
+ import { RealStoreWrapper as Wrapper } from '@plone/volto/storybook';
5
+
6
+ const IntlSearchWidgetComponent = injectIntl(SearchWidgetComponent);
7
+
8
+ function StoryComponent(args) {
9
+ return (
10
+ <Wrapper
11
+ customStore={{
12
+ intl: {
13
+ locale: 'en',
14
+ messages: {},
15
+ },
16
+ }}
17
+ >
18
+ <div id="toolbar" style={{ display: 'none' }} />
19
+ <IntlSearchWidgetComponent {...args} pathname="/blog" />
20
+ </Wrapper>
21
+ );
22
+ }
23
+
24
+ export const SearchWidget = StoryComponent.bind({});
25
+
26
+ export default {
27
+ title: 'Public components/SearchWidget',
28
+ component: SearchWidget,
29
+ decorators: [
30
+ (Story) => (
31
+ <div className="ui segment form attached" style={{ width: '400px' }}>
32
+ <Story />
33
+ </div>
34
+ ),
35
+ ],
36
+ };
@@ -76,6 +76,7 @@ import {
76
76
  } from '@plone/volto/components/manage/Blocks/Search/components';
77
77
  import getListingBlockAsyncData from '@plone/volto/components/manage/Blocks/Listing/getAsyncData';
78
78
  import { getImageBlockSizes } from '@plone/volto/components/manage/Blocks/Image/utils';
79
+ import { getLeadImageBlockSizes } from '@plone/volto/components/manage/Blocks/LeadImage/utils';
79
80
 
80
81
  // block sidebar schemas (not the Dexterity Layout block settings schemas)
81
82
  import HeroImageLeftBlockSchema from '@plone/volto/components/manage/Blocks/HeroImageLeft/schema';
@@ -272,6 +273,7 @@ const blocksConfig = {
272
273
  restricted: ({ properties }) => !properties.hasOwnProperty('image'),
273
274
  mostUsed: false,
274
275
  sidebarTab: 1,
276
+ getSizes: getLeadImageBlockSizes,
275
277
  },
276
278
  listing: {
277
279
  id: 'listing',
@@ -355,3 +355,32 @@ export const URLUtils = {
355
355
  isUrl,
356
356
  checkAndNormalizeUrl,
357
357
  };
358
+
359
+ /**
360
+ * Given an image info object, it does flatten all the scales information to
361
+ * match the ones stored in the catalog
362
+ * 'http://localhost:3000/{path}/@@images/{scalefile}' -> '@images/{scalefile}'
363
+ * @function flattenScales
364
+ * @param {string} path path of the content object
365
+ * @param {object} image image information object
366
+ * @returns {object} New object with the flattened scale URLs
367
+ */
368
+ export function flattenScales(path, image) {
369
+ function removeObjectIdFromURL(path, scale) {
370
+ return scale.replace(`${path}/`, '');
371
+ }
372
+ if (!image) return;
373
+
374
+ const imageInfo = {
375
+ ...image,
376
+ download: flattenToAppURL(removeObjectIdFromURL(path, image.download)),
377
+ };
378
+
379
+ Object.keys(imageInfo.scales).forEach((key) => {
380
+ imageInfo.scales[key].download = flattenToAppURL(
381
+ removeObjectIdFromURL(path, image.scales[key].download),
382
+ );
383
+ });
384
+
385
+ return imageInfo;
386
+ }