@plone/volto 16.21.3 → 16.22.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 (36) hide show
  1. package/.changelog.draft +28 -4
  2. package/.yarn/install-state.gz +0 -0
  3. package/CHANGELOG.md +33 -0
  4. package/cypress/support/commands.js +17 -0
  5. package/locales/it/LC_MESSAGES/volto.po +1 -1
  6. package/locales/it.json +1 -1
  7. package/package.json +1 -1
  8. package/packages/volto-slate/package.json +1 -1
  9. package/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx +21 -212
  10. package/packages/volto-slate/src/blocks/Table/schema.js +122 -0
  11. package/packages/volto-slate/src/editor/plugins/StyleMenu/utils.js +14 -5
  12. package/packages/volto-slate/src/utils/blocks.js +7 -0
  13. package/packages/volto-slate/src/widgets/RichTextWidget.jsx +15 -8
  14. package/src/components/manage/Blocks/Search/components/Facets.jsx +6 -2
  15. package/src/components/manage/Blocks/ToC/Schema.jsx +5 -1
  16. package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +142 -8
  17. package/src/components/manage/Contents/Contents.jsx +8 -6
  18. package/src/components/manage/UniversalLink/UniversalLink.jsx +2 -6
  19. package/src/components/manage/UniversalLink/UniversalLink.test.jsx +36 -0
  20. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +1 -1
  21. package/src/components/theme/Login/Login.jsx +1 -1
  22. package/src/components/theme/View/AlbumView.jsx +9 -1
  23. package/src/components/theme/View/EventView.jsx +6 -2
  24. package/src/components/theme/View/FileView.jsx +23 -18
  25. package/src/components/theme/View/ImageView.jsx +37 -32
  26. package/src/components/theme/View/LinkView.jsx +4 -1
  27. package/src/components/theme/View/ListingView.jsx +33 -27
  28. package/src/components/theme/View/SummaryView.jsx +47 -38
  29. package/src/components/theme/View/TabularView.jsx +59 -53
  30. package/src/config/index.js +1 -0
  31. package/src/config/server.js +2 -0
  32. package/src/express-middleware/ok.js +16 -0
  33. package/src/helpers/Url/Url.js +22 -1
  34. package/src/helpers/Url/Url.test.js +41 -0
  35. package/theme/themes/pastanaga/extras/main.less +2 -1
  36. package/theme/themes/pastanaga/extras/toc.less +29 -0
@@ -6,9 +6,10 @@
6
6
  import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { UniversalLink } from '@plone/volto/components';
9
- import { Container } from 'semantic-ui-react';
9
+ import { Container as SemanticContainer } from 'semantic-ui-react';
10
10
  import { FormattedMessage } from 'react-intl';
11
11
  import PreviewImage from '../PreviewImage/PreviewImage';
12
+ import config from '@plone/volto/registry';
12
13
 
13
14
  /**
14
15
  * Summary view component class.
@@ -16,43 +17,51 @@ import PreviewImage from '../PreviewImage/PreviewImage';
16
17
  * @param {Object} content Content object.
17
18
  * @returns {string} Markup of the component.
18
19
  */
19
- const SummaryView = ({ content }) => (
20
- <Container className="view-wrapper summary-view">
21
- <article id="content">
22
- <header>
23
- <h1 className="documentFirstHeading">{content.title}</h1>
24
- {content.description && (
25
- <p className="documentDescription">{content.description}</p>
26
- )}
27
- </header>
28
- <section id="content-core">
29
- {content.items.map((item) => (
30
- <article key={item.url}>
31
- <h2>
32
- <UniversalLink item={item} title={item['@type']}>
33
- {item.title}
34
- </UniversalLink>
35
- </h2>
36
- {item.image_field && (
37
- <PreviewImage
38
- item={item}
39
- alt={item.image_caption ? item.image_caption : item.title}
40
- size="thumb"
41
- className="ui image floated right clear"
42
- />
43
- )}
44
- {item.description && <p>{item.description}</p>}
45
- <p>
46
- <UniversalLink item={item}>
47
- <FormattedMessage id="Read More…" defaultMessage="Read More…" />
48
- </UniversalLink>
49
- </p>
50
- </article>
51
- ))}
52
- </section>
53
- </article>
54
- </Container>
55
- );
20
+ const SummaryView = ({ content }) => {
21
+ const Container =
22
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
23
+
24
+ return (
25
+ <Container className="view-wrapper summary-view">
26
+ <article id="content">
27
+ <header>
28
+ <h1 className="documentFirstHeading">{content.title}</h1>
29
+ {content.description && (
30
+ <p className="documentDescription">{content.description}</p>
31
+ )}
32
+ </header>
33
+ <section id="content-core">
34
+ {content.items.map((item) => (
35
+ <article key={item.url}>
36
+ <h2>
37
+ <UniversalLink item={item} title={item['@type']}>
38
+ {item.title}
39
+ </UniversalLink>
40
+ </h2>
41
+ {item.image_field && (
42
+ <PreviewImage
43
+ item={item}
44
+ alt={item.image_caption ? item.image_caption : item.title}
45
+ size="thumb"
46
+ className="ui image floated right clear"
47
+ />
48
+ )}
49
+ {item.description && <p>{item.description}</p>}
50
+ <p>
51
+ <UniversalLink item={item}>
52
+ <FormattedMessage
53
+ id="Read More…"
54
+ defaultMessage="Read More…"
55
+ />
56
+ </UniversalLink>
57
+ </p>
58
+ </article>
59
+ ))}
60
+ </section>
61
+ </article>
62
+ </Container>
63
+ );
64
+ };
56
65
 
57
66
  /**
58
67
  * Property types.
@@ -6,8 +6,9 @@
6
6
  import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { UniversalLink } from '@plone/volto/components';
9
- import { Container, Table } from 'semantic-ui-react';
9
+ import { Container as SemanticContainer, Table } from 'semantic-ui-react';
10
10
  import { FormattedMessage } from 'react-intl';
11
+ import config from '@plone/volto/registry';
11
12
 
12
13
  /**
13
14
  * Tabular view component class.
@@ -15,59 +16,64 @@ import { FormattedMessage } from 'react-intl';
15
16
  * @param {Object} content Content object.
16
17
  * @returns {string} Markup of the component.
17
18
  */
18
- const TabularView = ({ content }) => (
19
- <Container className="view-wrapper">
20
- <article id="content">
21
- <header>
22
- <h1 className="documentFirstHeading">{content.title}</h1>
23
- {content.description && (
24
- <p className="documentDescription">{content.description}</p>
25
- )}
26
- </header>
27
- <section id="content-core">
28
- <Table celled padded>
29
- <Table.Header>
30
- <Table.Row>
31
- <Table.HeaderCell>
32
- <FormattedMessage id="Title" defaultMessage="Title" />
33
- </Table.HeaderCell>
34
- <Table.HeaderCell>
35
- <FormattedMessage
36
- id="Description"
37
- defaultMessage="Description"
38
- />
39
- </Table.HeaderCell>
40
- <Table.HeaderCell>
41
- <FormattedMessage id="Type" defaultMessage="Type" />
42
- </Table.HeaderCell>
43
- <Table.HeaderCell>
44
- <FormattedMessage id="State" defaultMessage="State" />
45
- </Table.HeaderCell>
46
- </Table.Row>
47
- </Table.Header>
48
- <Table.Body>
49
- {content.items.map((item) => (
50
- <Table.Row key={item.url}>
51
- <Table.Cell>
52
- <UniversalLink
53
- item={item}
54
- className="summary url"
55
- title={item['@type']}
56
- >
57
- {item.title}
58
- </UniversalLink>
59
- </Table.Cell>
60
- <Table.Cell>{item.description}</Table.Cell>
61
- <Table.Cell>{item['@type']}</Table.Cell>
62
- <Table.Cell>{item.review_state}</Table.Cell>
19
+ const TabularView = ({ content }) => {
20
+ const Container =
21
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
22
+
23
+ return (
24
+ <Container className="view-wrapper">
25
+ <article id="content">
26
+ <header>
27
+ <h1 className="documentFirstHeading">{content.title}</h1>
28
+ {content.description && (
29
+ <p className="documentDescription">{content.description}</p>
30
+ )}
31
+ </header>
32
+ <section id="content-core">
33
+ <Table celled padded>
34
+ <Table.Header>
35
+ <Table.Row>
36
+ <Table.HeaderCell>
37
+ <FormattedMessage id="Title" defaultMessage="Title" />
38
+ </Table.HeaderCell>
39
+ <Table.HeaderCell>
40
+ <FormattedMessage
41
+ id="Description"
42
+ defaultMessage="Description"
43
+ />
44
+ </Table.HeaderCell>
45
+ <Table.HeaderCell>
46
+ <FormattedMessage id="Type" defaultMessage="Type" />
47
+ </Table.HeaderCell>
48
+ <Table.HeaderCell>
49
+ <FormattedMessage id="State" defaultMessage="State" />
50
+ </Table.HeaderCell>
63
51
  </Table.Row>
64
- ))}
65
- </Table.Body>
66
- </Table>
67
- </section>
68
- </article>
69
- </Container>
70
- );
52
+ </Table.Header>
53
+ <Table.Body>
54
+ {content.items.map((item) => (
55
+ <Table.Row key={item.url}>
56
+ <Table.Cell>
57
+ <UniversalLink
58
+ item={item}
59
+ className="summary url"
60
+ title={item['@type']}
61
+ >
62
+ {item.title}
63
+ </UniversalLink>
64
+ </Table.Cell>
65
+ <Table.Cell>{item.description}</Table.Cell>
66
+ <Table.Cell>{item['@type']}</Table.Cell>
67
+ <Table.Cell>{item.review_state}</Table.Cell>
68
+ </Table.Row>
69
+ ))}
70
+ </Table.Body>
71
+ </Table>
72
+ </section>
73
+ </article>
74
+ </Container>
75
+ );
76
+ };
71
77
 
72
78
  /**
73
79
  * Property types.
@@ -74,6 +74,7 @@ let config = {
74
74
  port,
75
75
  // The URL Volto is going to be served (see sensible defaults above)
76
76
  publicURL,
77
+ okRoute: '/ok',
77
78
  apiPath,
78
79
  apiExpanders: [
79
80
  // Add the following expanders for only issuing a single request.
@@ -1,6 +1,7 @@
1
1
  import imagesMiddleware from '@plone/volto/express-middleware/images';
2
2
  import filesMiddleware from '@plone/volto/express-middleware/files';
3
3
  import robotstxtMiddleware from '@plone/volto/express-middleware/robotstxt';
4
+ import okMiddleware from '@plone/volto/express-middleware/ok';
4
5
  import sitemapMiddleware from '@plone/volto/express-middleware/sitemap';
5
6
  import staticsMiddleware from '@plone/volto/express-middleware/static';
6
7
  import devProxyMiddleware from '@plone/volto/express-middleware/devproxy';
@@ -11,6 +12,7 @@ const settings = {
11
12
  filesMiddleware(),
12
13
  imagesMiddleware(),
13
14
  robotstxtMiddleware(),
15
+ okMiddleware(),
14
16
  sitemapMiddleware(),
15
17
  staticsMiddleware(),
16
18
  ],
@@ -0,0 +1,16 @@
1
+ import express from 'express';
2
+ import config from '@plone/volto/registry';
3
+
4
+ const ok = function (req, res, next) {
5
+ res.type('text/plain');
6
+ res.set('Expires', 'Sat, 1 Jan 2000 00:00:00 GMT');
7
+ res.set('Cache-Control', 'max-age=0, must-revalidate, private');
8
+ res.send('ok');
9
+ };
10
+
11
+ export default function () {
12
+ const middleware = express.Router();
13
+ middleware.all(config?.settings?.okRoute || '/ok', ok);
14
+ middleware.id = 'ok';
15
+ return middleware;
16
+ }
@@ -3,7 +3,7 @@
3
3
  * @module helpers/Url
4
4
  */
5
5
 
6
- import { last, memoize } from 'lodash';
6
+ import { last, memoize, isArray, isObject, isString } from 'lodash';
7
7
  import { urlRegex, telRegex, mailRegex } from './urlRegex';
8
8
  import prependHttp from 'prepend-http';
9
9
  import config from '@plone/volto/registry';
@@ -251,6 +251,27 @@ export function isUrl(url) {
251
251
  return urlRegex().test(url);
252
252
  }
253
253
 
254
+ /**
255
+ * Get field url
256
+ * @method getFieldURL
257
+ * @param {object} data
258
+ * @returns {string | any} URL string value if field is of url type or any.
259
+ */
260
+ export const getFieldURL = (data) => {
261
+ let url = data;
262
+ const _isObject = data && isObject(data) && !isArray(data);
263
+ if (_isObject && data['@type'] === 'URL') {
264
+ url = data['value'] ?? data['url'] ?? data['href'] ?? data;
265
+ } else if (_isObject) {
266
+ url = data['@id'] ?? data['url'] ?? data['href'] ?? data;
267
+ }
268
+ if (isArray(data)) {
269
+ url = data.map((item) => getFieldURL(item));
270
+ }
271
+ if (isString(url) && isInternalURL(url)) return flattenToAppURL(url);
272
+ return url;
273
+ };
274
+
254
275
  /**
255
276
  * Normalize URL, adds protocol (if required eg. user has not entered the protocol)
256
277
  * @method normalizeUrl
@@ -10,6 +10,7 @@ import {
10
10
  isCmsUi,
11
11
  isInternalURL,
12
12
  isUrl,
13
+ getFieldURL,
13
14
  normalizeUrl,
14
15
  removeProtocol,
15
16
  addAppURL,
@@ -260,6 +261,46 @@ describe('Url', () => {
260
261
  expect(isUrl(href)).toBe(false);
261
262
  });
262
263
  });
264
+ describe('getFieldURL', () => {
265
+ it('returns app URL if the field is a string', () => {
266
+ const field = `${settings.apiPath}/foo/bar`;
267
+ expect(getFieldURL(field)).toBe('/foo/bar');
268
+ });
269
+ it('returns app URL if the field is an object with "@id"', () => {
270
+ const field = { '@id': '/foo/bar' };
271
+ expect(getFieldURL(field)).toBe('/foo/bar');
272
+ });
273
+ it('returns app URL if the field is an object with type URL', () => {
274
+ const field = { '@type': 'URL', value: '/foo/bar' };
275
+ expect(getFieldURL(field)).toBe('/foo/bar');
276
+ });
277
+ it('returns app URL if the field is an object with url or href properties', () => {
278
+ const fieldUrl = { url: '/foo/bar' };
279
+ const fieldHref = { href: '/foo/bar' };
280
+ expect(getFieldURL(fieldUrl)).toBe('/foo/bar');
281
+ expect(getFieldURL(fieldHref)).toBe('/foo/bar');
282
+ });
283
+ it('returns array of app URL if the field is an array of strings', () => {
284
+ const field = [
285
+ `${settings.apiPath}/foo/bar/1`,
286
+ `${settings.apiPath}/foo/bar/2`,
287
+ ];
288
+ expect(getFieldURL(field)).toStrictEqual(['/foo/bar/1', '/foo/bar/2']);
289
+ });
290
+ it('returns array of app URL if the field is an array of objects', () => {
291
+ const field = [
292
+ {
293
+ '@type': 'URL',
294
+ value: `${settings.apiPath}/foo/bar/1`,
295
+ },
296
+ {
297
+ '@type': 'URL',
298
+ value: `${settings.apiPath}/foo/bar/2`,
299
+ },
300
+ ];
301
+ expect(getFieldURL(field)).toStrictEqual(['/foo/bar/1', '/foo/bar/2']);
302
+ });
303
+ });
263
304
  describe('normalizeUrl', () => {
264
305
  it('normalizeUrl test', () => {
265
306
  const href = `www.example.com`;
@@ -26,7 +26,7 @@ body {
26
26
  &.has-toolbar-menu-open,
27
27
  &.has-mobile-menu-open {
28
28
  // The body scroll locker when the toolbar or the mobile menu are active in mobile.
29
- @media only screen and (max-width: @largestMobileScreen) {
29
+ @media only screen and (max-width: @largestTabletScreen) {
30
30
  overflow: hidden;
31
31
  }
32
32
  }
@@ -588,4 +588,5 @@ body.has-toolbar-collapsed .mobile-menu {
588
588
  @import 'login';
589
589
  @import 'language-selector';
590
590
  @import 'views';
591
+ @import 'toc';
591
592
  .loadUIOverrides();
@@ -0,0 +1,29 @@
1
+ .table-of-contents.horizontalMenu.sticky-toc {
2
+ position: sticky;
3
+ z-index: 1;
4
+ top: 0;
5
+ }
6
+
7
+ .table-of-contents.horizontalMenu .hidden {
8
+ z-index: -1;
9
+ pointer-events: none;
10
+ visibility: hidden;
11
+ }
12
+
13
+ .table-of-contents.horizontalMenu .hidden-dropdown {
14
+ display: none !important;
15
+ }
16
+
17
+ .table-of-contents.horizontalMenu .item.dropdown {
18
+ position: absolute !important;
19
+ right: 0;
20
+ }
21
+
22
+ .table-of-contents.horizontalMenu > .ui.menu {
23
+ position: relative;
24
+ }
25
+
26
+ .table-of-contents.horizontalMenu .item.dropdown .focused > a {
27
+ border: 2px solid black !important;
28
+ border-radius: 4px;
29
+ }