@plone/volto 16.21.2 → 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 (66) hide show
  1. package/.changelog.draft +28 -3
  2. package/.yarn/install-state.gz +0 -0
  3. package/CHANGELOG.md +42 -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/actions/index.js +1 -1
  10. package/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx +21 -212
  11. package/packages/volto-slate/src/blocks/Table/schema.js +122 -0
  12. package/packages/volto-slate/src/blocks/Text/index.js +2 -2
  13. package/packages/volto-slate/src/editor/index.js +4 -4
  14. package/packages/volto-slate/src/editor/plugins/StyleMenu/utils.js +14 -5
  15. package/packages/volto-slate/src/editor/ui/SlateContextToolbar.jsx +2 -2
  16. package/packages/volto-slate/src/editor/ui/index.js +15 -15
  17. package/packages/volto-slate/src/index.js +2 -2
  18. package/packages/volto-slate/src/utils/blocks.js +7 -0
  19. package/packages/volto-slate/src/widgets/RichTextWidget.jsx +15 -8
  20. package/src/components/manage/AnchorPlugin/index.jsx +2 -2
  21. package/src/components/manage/AnchorPlugin/utils/EditorUtils.js +3 -1
  22. package/src/components/manage/Blocks/Block/Style.jsx +2 -2
  23. package/src/components/manage/Blocks/Listing/getAsyncData.js +7 -2
  24. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +3 -4
  25. package/src/components/manage/Blocks/Search/components/Facets.jsx +6 -2
  26. package/src/components/manage/Blocks/Search/components/index.js +13 -13
  27. package/src/components/manage/Blocks/Search/hocs/index.js +2 -2
  28. package/src/components/manage/Blocks/Search/hocs/withQueryString.jsx +2 -2
  29. package/src/components/manage/Blocks/ToC/Schema.jsx +5 -1
  30. package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +142 -8
  31. package/src/components/manage/Blocks/ToC/variations/index.js +3 -1
  32. package/src/components/manage/Contents/Contents.jsx +8 -6
  33. package/src/components/manage/UniversalLink/UniversalLink.jsx +2 -6
  34. package/src/components/manage/UniversalLink/UniversalLink.test.jsx +36 -0
  35. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +1 -1
  36. package/src/components/theme/Login/Login.jsx +1 -1
  37. package/src/components/theme/View/AlbumView.jsx +9 -1
  38. package/src/components/theme/View/EventView.jsx +6 -2
  39. package/src/components/theme/View/FileView.jsx +23 -18
  40. package/src/components/theme/View/ImageView.jsx +37 -32
  41. package/src/components/theme/View/LinkView.jsx +4 -1
  42. package/src/components/theme/View/ListingView.jsx +33 -27
  43. package/src/components/theme/View/SummaryView.jsx +47 -38
  44. package/src/components/theme/View/TabularView.jsx +59 -53
  45. package/src/config/RichTextEditor/Blocks.jsx +2 -2
  46. package/src/config/RichTextEditor/FromHTML.jsx +2 -2
  47. package/src/config/RichTextEditor/Styles.jsx +1 -1
  48. package/src/config/index.js +1 -0
  49. package/src/config/server.js +2 -0
  50. package/src/constants/Indexes.js +3 -1
  51. package/src/express-middleware/devproxy.js +1 -1
  52. package/src/express-middleware/files.js +3 -3
  53. package/src/express-middleware/images.js +4 -4
  54. package/src/express-middleware/ok.js +16 -0
  55. package/src/express-middleware/robotstxt.js +1 -1
  56. package/src/express-middleware/sitemap.js +1 -1
  57. package/src/express-middleware/static.js +3 -3
  58. package/src/helpers/Extensions/index.js +2 -1
  59. package/src/helpers/Url/Url.js +22 -1
  60. package/src/helpers/Url/Url.test.js +41 -0
  61. package/src/helpers/index.js +9 -10
  62. package/src/middleware/index.js +2 -2
  63. package/src/start-server.js +2 -2
  64. package/theme/themes/pastanaga/extras/blocks.less +3 -1
  65. package/theme/themes/pastanaga/extras/main.less +2 -1
  66. package/theme/themes/pastanaga/extras/toc.less +29 -0
@@ -3,7 +3,7 @@ import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExten
3
3
  import config from '@plone/volto/registry';
4
4
  import { hasNonValueOperation, hasDateOperation } from '../utils';
5
5
 
6
- const showFacet = (index) => {
6
+ const defaultShowFacet = (index) => {
7
7
  const { values } = index;
8
8
  return index
9
9
  ? hasNonValueOperation(index.operations || []) ||
@@ -61,7 +61,11 @@ const Facets = (props) => {
61
61
 
62
62
  // TODO :handle changing the type of facet (multi/nonmulti)
63
63
 
64
- const { view: FacetWidget, stateToValue } = resolveExtension(
64
+ const {
65
+ view: FacetWidget,
66
+ stateToValue,
67
+ showFacet = defaultShowFacet,
68
+ } = resolveExtension(
65
69
  'type',
66
70
  search.extensions.facetWidgets.types,
67
71
  facetSettings,
@@ -1,13 +1,13 @@
1
- export SearchDetails from './SearchDetails';
2
- export Facets from './Facets';
3
- export SelectFacet from './SelectFacet';
4
- export CheckboxFacet from './CheckboxFacet';
5
- export DateRangeFacet from './DateRangeFacet';
6
- export SearchInput from './SearchInput';
7
- export FilterList from './FilterList';
8
- export SortOn from './SortOn';
9
- export ToggleFacet from './ToggleFacet';
10
- export SelectFacetFilterListEntry from './SelectFacetFilterListEntry';
11
- export ToggleFacetFilterListEntry from './ToggleFacetFilterListEntry';
12
- export DateRangeFacetFilterListEntry from './DateRangeFacetFilterListEntry';
13
- export ViewSwitcher from './ViewSwitcher';
1
+ export { default as SearchDetails } from './SearchDetails';
2
+ export { default as Facets } from './Facets';
3
+ export { default as SelectFacet } from './SelectFacet';
4
+ export { default as CheckboxFacet } from './CheckboxFacet';
5
+ export { default as DateRangeFacet } from './DateRangeFacet';
6
+ export { default as SearchInput } from './SearchInput';
7
+ export { default as FilterList } from './FilterList';
8
+ export { default as SortOn } from './SortOn';
9
+ export { default as ToggleFacet } from './ToggleFacet';
10
+ export { default as SelectFacetFilterListEntry } from './SelectFacetFilterListEntry';
11
+ export { default as ToggleFacetFilterListEntry } from './ToggleFacetFilterListEntry';
12
+ export { default as DateRangeFacetFilterListEntry } from './DateRangeFacetFilterListEntry';
13
+ export { default as ViewSwitcher } from './ViewSwitcher';
@@ -1,2 +1,2 @@
1
- export withQueryString from './withQueryString';
2
- export withSearch from './withSearch';
1
+ export { default as withQueryString } from './withQueryString';
2
+ export { default as withSearch } from './withSearch';
@@ -10,7 +10,7 @@ function getDisplayName(WrappedComponent) {
10
10
  * A HOC that injects querystring metadata information from the backend.
11
11
  *
12
12
  */
13
- export default (WrappedComponent) => {
13
+ export default function withQueryString(WrappedComponent) {
14
14
  function WithQueryString(props) {
15
15
  const dispatch = useDispatch();
16
16
 
@@ -29,4 +29,4 @@ export default (WrappedComponent) => {
29
29
  WrappedComponent,
30
30
  )})`;
31
31
  return WithQueryString;
32
- };
32
+ }
@@ -10,7 +10,7 @@ const TableOfContentsSchema = ({ data }) => {
10
10
  fields: [
11
11
  'title',
12
12
  'hide_title',
13
- ...(variation === 'default' ? ['ordered'] : []),
13
+ ...(variation === 'default' ? ['ordered'] : ['sticky']),
14
14
  'levels',
15
15
  ],
16
16
  },
@@ -39,6 +39,10 @@ const TableOfContentsSchema = ({ data }) => {
39
39
  title: 'Ordered',
40
40
  type: 'boolean',
41
41
  },
42
+ sticky: {
43
+ title: 'Sticky',
44
+ type: 'boolean',
45
+ },
42
46
  },
43
47
  required: [],
44
48
  };
@@ -1,12 +1,7 @@
1
- /**
2
- * View toc block.
3
- * @module components/manage/Blocks/ToC/View
4
- */
5
-
6
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
7
2
  import PropTypes from 'prop-types';
8
3
  import { map } from 'lodash';
9
- import { Menu } from 'semantic-ui-react';
4
+ import { Menu, Dropdown } from 'semantic-ui-react';
10
5
  import { FormattedMessage, injectIntl } from 'react-intl';
11
6
  import AnchorLink from 'react-anchor-link-smooth-scroll';
12
7
 
@@ -32,6 +27,131 @@ const RenderMenuItems = ({ items }) => {
32
27
  * @extends Component
33
28
  */
34
29
  const View = ({ data, tocEntries }) => {
30
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
31
+ // When the page is resized to prevent items from the TOC from going out of the viewport,
32
+ // a dropdown menu is added containing all the items that don't fit.
33
+ const handleResize = () => {
34
+ const menuElement = document.querySelector('.responsive-menu');
35
+ const containerWidth = menuElement.offsetWidth;
36
+
37
+ // Get all divs that contain the items from the TOC, except the dropdown button
38
+ const nested = document.querySelectorAll(
39
+ '.responsive-menu .item:not(.dropdown)',
40
+ );
41
+ const nestedArray = Object.values(nested);
42
+ const middle = Math.ceil(nestedArray.length / 2);
43
+ const firstHalfNested = nestedArray.slice(0, middle);
44
+ const secondHalfNested = nestedArray.slice(middle);
45
+
46
+ const dropdown = document.querySelector('.dropdown');
47
+ const dropdownWidth = dropdown.offsetWidth;
48
+
49
+ const firstHalfNestedHiddenItems = [];
50
+
51
+ // Add a 'hidden' class for the items that should be in the dropdown
52
+ firstHalfNested.forEach((item) => {
53
+ const itemOffsetLeft = item.offsetLeft;
54
+ const itemOffsetWidth = item.offsetWidth;
55
+ if (itemOffsetLeft + itemOffsetWidth > containerWidth - dropdownWidth) {
56
+ item.classList.add('hidden');
57
+ firstHalfNestedHiddenItems.push(item);
58
+ } else {
59
+ item.classList.remove('hidden');
60
+ }
61
+ });
62
+
63
+ secondHalfNested.forEach((item) => item.classList.add('hidden-dropdown'));
64
+
65
+ const diff = firstHalfNested.length - firstHalfNestedHiddenItems.length;
66
+ const secondHalfNestedShownItems = secondHalfNested.slice(diff);
67
+ secondHalfNestedShownItems.forEach((item) =>
68
+ item.classList.remove('hidden-dropdown'),
69
+ );
70
+
71
+ // If there are elements that should be displayed in the dropdown, show the dropdown button
72
+ if (secondHalfNestedShownItems.length > 0)
73
+ dropdown.classList.remove('hidden-dropdown');
74
+ else {
75
+ dropdown.classList.add('hidden-dropdown');
76
+ }
77
+ };
78
+
79
+ const handleDropdownKeyDown = (event) => {
80
+ const dropdownMenu = document.querySelector('.menu.transition');
81
+ if (event.key === 'ArrowDown' && isDropdownOpen) {
82
+ event.preventDefault();
83
+ const menuItems = dropdownMenu.querySelectorAll(
84
+ '.item:not(.hidden-dropdown)',
85
+ );
86
+ const focusedItem = dropdownMenu.querySelector('.item.focused');
87
+ const focusedIndex = Array.from(menuItems).indexOf(focusedItem);
88
+
89
+ if (focusedIndex === -1) {
90
+ // No item is currently focused, so focus the first item
91
+ menuItems[0].classList.add('focused');
92
+ } else if (focusedIndex === menuItems.length - 1) {
93
+ // Remove focus from the currently focused item and close the dropdown
94
+ focusedItem.classList.remove('focused');
95
+ setIsDropdownOpen(false);
96
+
97
+ // Focus the next element on the page
98
+ const nextElement = dropdownMenu.nextElementSibling;
99
+ if (nextElement) {
100
+ nextElement.focus();
101
+ }
102
+ } else {
103
+ // Remove focus from the currently focused item
104
+ focusedItem.classList.remove('focused');
105
+
106
+ // Focus the next item or wrap around to the first item
107
+ const nextIndex = (focusedIndex + 1) % menuItems.length;
108
+ menuItems[nextIndex].classList.add('focused');
109
+ }
110
+ } else if (event.key === 'Enter' && isDropdownOpen) {
111
+ const focusedItem = dropdownMenu.querySelector('.item.focused');
112
+ if (focusedItem) {
113
+ focusedItem.querySelector('a').click();
114
+ focusedItem.classList.remove('focused');
115
+ }
116
+ } else if (event.key === 'Tab') {
117
+ const focusedItem = dropdownMenu.querySelector('.item.focused');
118
+ if (focusedItem) {
119
+ focusedItem.classList.remove('focused');
120
+ }
121
+ }
122
+ };
123
+
124
+ useEffect(() => {
125
+ if (data.sticky) {
126
+ const toc = document.querySelector('.horizontalMenu');
127
+ const tocPos = toc ? toc.offsetTop : 0;
128
+
129
+ const handleScroll = () => {
130
+ let scrollPos = window.scrollY;
131
+ if (scrollPos > tocPos && toc) {
132
+ toc.classList.add('sticky-toc');
133
+ } else if (scrollPos <= tocPos && toc) {
134
+ toc.classList.remove('sticky-toc');
135
+ }
136
+ };
137
+
138
+ window.addEventListener('scroll', handleScroll);
139
+
140
+ return () => {
141
+ window.removeEventListener('scroll', handleScroll);
142
+ };
143
+ }
144
+ }, [data.sticky]);
145
+
146
+ useEffect(() => {
147
+ handleResize();
148
+ window.addEventListener('resize', handleResize);
149
+
150
+ return () => {
151
+ window.removeEventListener('resize', handleResize);
152
+ };
153
+ });
154
+
35
155
  return (
36
156
  <>
37
157
  {data.title && !data.hide_title ? (
@@ -46,8 +166,22 @@ const View = ({ data, tocEntries }) => {
46
166
  ) : (
47
167
  ''
48
168
  )}
49
- <Menu>
169
+ <Menu className="responsive-menu">
50
170
  <RenderMenuItems items={tocEntries} />
171
+ <Dropdown
172
+ item
173
+ text="More"
174
+ className="hidden-dropdown"
175
+ open={isDropdownOpen}
176
+ onOpen={() => setIsDropdownOpen(true)}
177
+ onClose={() => setIsDropdownOpen(false)}
178
+ tabIndex={0}
179
+ onKeyDown={handleDropdownKeyDown}
180
+ >
181
+ <Dropdown.Menu>
182
+ <RenderMenuItems items={tocEntries} />
183
+ </Dropdown.Menu>
184
+ </Dropdown>
51
185
  </Menu>
52
186
  </>
53
187
  );
@@ -1,7 +1,7 @@
1
1
  import DefaultTocRenderer from './DefaultTocRenderer';
2
2
  import HorizontalMenu from './HorizontalMenu';
3
3
 
4
- export default [
4
+ const ToCVariations = [
5
5
  {
6
6
  id: 'default',
7
7
  title: 'Listing (default)',
@@ -14,3 +14,5 @@ export default [
14
14
  view: HorizontalMenu,
15
15
  },
16
16
  ];
17
+
18
+ export default ToCVariations;
@@ -450,7 +450,7 @@ class Contents extends Component {
450
450
  sort_on: this.props.sort?.on || 'getObjPositionInParent',
451
451
  sort_order: this.props.sort?.order || 'ascending',
452
452
  isClient: false,
453
- linkIntegrityBreakages: '',
453
+ linkIntegrityBreakages: [],
454
454
  };
455
455
  this.filterTimeout = null;
456
456
  }
@@ -1236,7 +1236,10 @@ class Contents extends Component {
1236
1236
  Show all items
1237
1237
  </Button>
1238
1238
  )}
1239
- {this.state.linkIntegrityBreakages.length > 0 ? (
1239
+ {this.state.linkIntegrityBreakages.reduce(
1240
+ (a, b) => a + b.breaches.length,
1241
+ 0,
1242
+ ) ? (
1240
1243
  <div>
1241
1244
  <h3>
1242
1245
  {this.props.intl.formatMessage(
@@ -1249,9 +1252,8 @@ class Contents extends Component {
1249
1252
  )}
1250
1253
  </p>
1251
1254
  <ul className="content">
1252
- {map(
1253
- this.state.linkIntegrityBreakages,
1254
- (item) => (
1255
+ {map(this.state.linkIntegrityBreakages, (item) =>
1256
+ item.breaches.length ? (
1255
1257
  <li key={item['@id']}>
1256
1258
  <a href={item['@id']}>{item.title}</a>
1257
1259
  <p>
@@ -1269,7 +1271,7 @@ class Contents extends Component {
1269
1271
  ))}
1270
1272
  </ul>
1271
1273
  </li>
1272
- ),
1274
+ ) : null,
1273
1275
  )}
1274
1276
  </ul>
1275
1277
  </div>
@@ -12,7 +12,6 @@ import {
12
12
  isInternalURL,
13
13
  URLUtils,
14
14
  } from '@plone/volto/helpers/Url/Url';
15
- import { matchPath } from 'react-router';
16
15
 
17
16
  import config from '@plone/volto/registry';
18
17
 
@@ -66,11 +65,8 @@ const UniversalLink = ({
66
65
  }
67
66
  }
68
67
 
69
- const isBlacklisted =
70
- (config.settings.externalRoutes ?? []).find((route) =>
71
- matchPath(flattenToAppURL(url), route.match),
72
- )?.length > 0;
73
- const isExternal = !isInternalURL(url) || isBlacklisted;
68
+ const isExternal = !isInternalURL(url);
69
+
74
70
  const isDownload = (!isExternal && url.includes('@@download')) || download;
75
71
  const isDisplayFile =
76
72
  (!isExternal && url.includes('@@display-file')) || false;
@@ -157,6 +157,42 @@ describe('UniversalLink', () => {
157
157
  );
158
158
  });
159
159
 
160
+ it('UniversalLink renders external link where link is blacklisted', () => {
161
+ const notInEN = /^(?!.*(#|\/en|\/static|\/controlpanel|\/cypress|\/login|\/logout|\/contact-form)).*$/;
162
+ config.settings.externalRoutes = [
163
+ {
164
+ match: {
165
+ path: notInEN,
166
+ exact: false,
167
+ strict: false,
168
+ },
169
+ url(payload) {
170
+ return payload.location.pathname;
171
+ },
172
+ },
173
+ ];
174
+
175
+ const { getByTitle } = render(
176
+ <Provider store={store}>
177
+ <MemoryRouter>
178
+ <UniversalLink
179
+ href="http://localhost:3000/blacklisted-app"
180
+ title="External blacklisted app"
181
+ >
182
+ <h1>Title</h1>
183
+ </UniversalLink>
184
+ </MemoryRouter>
185
+ </Provider>,
186
+ );
187
+
188
+ expect(getByTitle('External blacklisted app').getAttribute('target')).toBe(
189
+ '_blank',
190
+ );
191
+ expect(getByTitle('External blacklisted app').getAttribute('rel')).toBe(
192
+ 'noopener noreferrer',
193
+ );
194
+ });
195
+
160
196
  it('check UniversalLink does not break with error in item', () => {
161
197
  const component = renderer.create(
162
198
  <Provider store={store}>
@@ -427,7 +427,7 @@ class RecurrenceWidget extends Component {
427
427
  break;
428
428
  }
429
429
 
430
- if (value) {
430
+ if (value === 0 || value) {
431
431
  //set value
432
432
  values[field] = value;
433
433
  } else {
@@ -239,7 +239,7 @@ class Login extends Component {
239
239
  <Input
240
240
  type="password"
241
241
  id="password"
242
- autocomplete="current-password"
242
+ autoComplete="current-password"
243
243
  name="password"
244
244
  placeholder={this.props.intl.formatMessage(
245
245
  messages.password,
@@ -5,9 +5,14 @@
5
5
 
6
6
  import React, { Component } from 'react';
7
7
  import PropTypes from 'prop-types';
8
- import { Container, GridColumn, Segment } from 'semantic-ui-react';
8
+ import {
9
+ Container as SemanticContainer,
10
+ GridColumn,
11
+ Segment,
12
+ } from 'semantic-ui-react';
9
13
  import { Button, Modal, Grid } from 'semantic-ui-react';
10
14
  import { Icon, UniversalLink, PreviewImage } from '@plone/volto/components';
15
+ import config from '@plone/volto/registry';
11
16
 
12
17
  import openSVG from '@plone/volto/icons/open.svg';
13
18
  import aheadSVG from '@plone/volto/icons/ahead.svg';
@@ -56,6 +61,9 @@ class AlbumView extends Component {
56
61
 
57
62
  render() {
58
63
  const { content } = this.props;
64
+ const Container =
65
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
66
+
59
67
  return (
60
68
  <Container className="view-wrapper">
61
69
  <article id="content">
@@ -9,6 +9,8 @@ import { hasBlocksData, flattenHTMLToAppURL } from '@plone/volto/helpers';
9
9
  import { Image, Grid } from 'semantic-ui-react';
10
10
  import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks';
11
11
  import { EventDetails } from '@plone/volto/components';
12
+ import { Container as SemanticContainer } from 'semantic-ui-react';
13
+ import config from '@plone/volto/registry';
12
14
 
13
15
  const EventTextfieldView = ({ content }) => (
14
16
  <React.Fragment>
@@ -41,9 +43,11 @@ const EventTextfieldView = ({ content }) => (
41
43
  */
42
44
  const EventView = (props) => {
43
45
  const { content } = props;
46
+ const Container =
47
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
44
48
 
45
49
  return (
46
- <div id="page-document" className="ui container view-wrapper event-view">
50
+ <Container id="page-document" className="view-wrapper event-view">
47
51
  <Grid>
48
52
  <Grid.Column width={7} className="mobile hidden">
49
53
  {hasBlocksData(content) ? (
@@ -83,7 +87,7 @@ const EventView = (props) => {
83
87
  )}
84
88
  </Grid.Column>
85
89
  </Grid>
86
- </div>
90
+ </Container>
87
91
  );
88
92
  };
89
93
 
@@ -5,9 +5,9 @@
5
5
 
6
6
  import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
- import { Container } from 'semantic-ui-react';
9
-
8
+ import { Container as SemanticContainer } from 'semantic-ui-react';
10
9
  import { flattenToAppURL } from '@plone/volto/helpers';
10
+ import config from '@plone/volto/registry';
11
11
 
12
12
  /**
13
13
  * File view component class.
@@ -15,22 +15,27 @@ import { flattenToAppURL } from '@plone/volto/helpers';
15
15
  * @params {object} content Content object.
16
16
  * @returns {string} Markup of the component.
17
17
  */
18
- const FileView = ({ content }) => (
19
- <Container className="view-wrapper">
20
- <h1 className="documentFirstHeading">
21
- {content.title}
22
- {content.subtitle && ` - ${content.subtitle}`}
23
- </h1>
24
- {content.description && (
25
- <p className="documentDescription">{content.description}</p>
26
- )}
27
- {content.file?.download && (
28
- <a href={flattenToAppURL(content.file.download)}>
29
- {content.file.filename}
30
- </a>
31
- )}
32
- </Container>
33
- );
18
+ const FileView = ({ content }) => {
19
+ const Container =
20
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
21
+
22
+ return (
23
+ <Container className="view-wrapper">
24
+ <h1 className="documentFirstHeading">
25
+ {content.title}
26
+ {content.subtitle && ` - ${content.subtitle}`}
27
+ </h1>
28
+ {content.description && (
29
+ <p className="documentDescription">{content.description}</p>
30
+ )}
31
+ {content.file?.download && (
32
+ <a href={flattenToAppURL(content.file.download)}>
33
+ {content.file.filename}
34
+ </a>
35
+ )}
36
+ </Container>
37
+ );
38
+ };
34
39
 
35
40
  /**
36
41
  * Property types.
@@ -5,11 +5,11 @@
5
5
 
6
6
  import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
- import { Container } from 'semantic-ui-react';
8
+ import { Container as SemanticContainer } from 'semantic-ui-react';
9
9
  import { FormattedMessage } from 'react-intl';
10
10
  import prettybytes from 'pretty-bytes';
11
-
12
11
  import { flattenToAppURL } from '@plone/volto/helpers';
12
+ import config from '@plone/volto/registry';
13
13
 
14
14
  /**
15
15
  * Image view component class.
@@ -17,37 +17,42 @@ import { flattenToAppURL } from '@plone/volto/helpers';
17
17
  * @params {object} content Content object.
18
18
  * @returns {string} Markup of the component.
19
19
  */
20
- const ImageView = ({ content }) => (
21
- <Container className="view-wrapper">
22
- <h1 className="documentFirstHeading">
23
- {content.title}
24
- {content.subtitle && ` - ${content.subtitle}`}
25
- </h1>
26
- {content.description && (
27
- <p className="documentDescription">{content.description}</p>
28
- )}
29
- {content?.image?.download && (
30
- <a href={flattenToAppURL(content.image.download)}>
31
- <img
32
- alt={content.title}
33
- src={flattenToAppURL(content.image.scales.preview.download)}
34
- />
35
- <figcaption>
36
- <FormattedMessage
37
- id="Size: {size}"
38
- defaultMessage="Size: {size}"
39
- values={{ size: prettybytes(content.image.size) }}
40
- />
41
- &nbsp; &mdash; &nbsp;
42
- <FormattedMessage
43
- id="Click to download full sized image"
44
- defaultMessage="Click to download full sized image"
20
+ const ImageView = ({ content }) => {
21
+ const Container =
22
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
23
+
24
+ return (
25
+ <Container className="view-wrapper">
26
+ <h1 className="documentFirstHeading">
27
+ {content.title}
28
+ {content.subtitle && ` - ${content.subtitle}`}
29
+ </h1>
30
+ {content.description && (
31
+ <p className="documentDescription">{content.description}</p>
32
+ )}
33
+ {content?.image?.download && (
34
+ <a href={flattenToAppURL(content.image.download)}>
35
+ <img
36
+ alt={content.title}
37
+ src={flattenToAppURL(content.image.scales.preview.download)}
45
38
  />
46
- </figcaption>
47
- </a>
48
- )}
49
- </Container>
50
- );
39
+ <figcaption>
40
+ <FormattedMessage
41
+ id="Size: {size}"
42
+ defaultMessage="Size: {size}"
43
+ values={{ size: prettybytes(content.image.size) }}
44
+ />
45
+ &nbsp; &mdash; &nbsp;
46
+ <FormattedMessage
47
+ id="Click to download full sized image"
48
+ defaultMessage="Click to download full sized image"
49
+ />
50
+ </figcaption>
51
+ </a>
52
+ )}
53
+ </Container>
54
+ );
55
+ };
51
56
 
52
57
  /**
53
58
  * Property types.
@@ -6,7 +6,7 @@
6
6
  import React, { Component } from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { isInternalURL, flattenToAppURL } from '@plone/volto/helpers';
9
- import { Container } from 'semantic-ui-react';
9
+ import { Container as SemanticContainer } from 'semantic-ui-react';
10
10
  import { UniversalLink } from '@plone/volto/components';
11
11
  import { FormattedMessage } from 'react-intl';
12
12
  import config from '@plone/volto/registry';
@@ -60,6 +60,9 @@ class LinkView extends Component {
60
60
  render() {
61
61
  const { remoteUrl } = this.props.content;
62
62
  const { openExternalLinkInNewTab } = config.settings;
63
+ const Container =
64
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
65
+
63
66
  return (
64
67
  <Container id="page-document">
65
68
  <h1 className="documentFirstHeading">{this.props.content.title}</h1>