@plone/volto 17.0.0-alpha.16 → 17.0.0-alpha.18

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 (45) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +59 -0
  3. package/cypress/support/commands.js +17 -0
  4. package/locales/it/LC_MESSAGES/volto.po +1 -1
  5. package/locales/it.json +1 -1
  6. package/package.json +2 -2
  7. package/packages/volto-slate/package.json +1 -1
  8. package/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx +21 -212
  9. package/packages/volto-slate/src/blocks/Table/schema.js +122 -0
  10. package/packages/volto-slate/src/editor/plugins/StyleMenu/utils.js +14 -5
  11. package/packages/volto-slate/src/utils/blocks.js +7 -0
  12. package/packages/volto-slate/src/widgets/RichTextWidget.jsx +15 -8
  13. package/src/components/manage/Blocks/Grid/View.jsx +1 -0
  14. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +3 -4
  15. package/src/components/manage/Blocks/Search/components/Facets.jsx +6 -2
  16. package/src/components/manage/Blocks/Teaser/Body.jsx +0 -1
  17. package/src/components/manage/Blocks/Teaser/DefaultBody.jsx +20 -15
  18. package/src/components/manage/Blocks/ToC/Schema.jsx +5 -1
  19. package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +142 -8
  20. package/src/components/manage/UniversalLink/UniversalLink.jsx +2 -6
  21. package/src/components/manage/UniversalLink/UniversalLink.test.jsx +36 -0
  22. package/src/components/theme/Anontools/Anontools.jsx +3 -4
  23. package/src/components/theme/Breadcrumbs/Breadcrumbs.jsx +52 -99
  24. package/src/components/theme/Breadcrumbs/Breadcrumbs.stories.jsx +14 -13
  25. package/src/components/theme/Comments/CommentEditModal.jsx +63 -115
  26. package/src/components/theme/ContactForm/ContactForm.jsx +108 -192
  27. package/src/components/theme/ContactForm/ContactForm.stories.jsx +1 -1
  28. package/src/components/theme/ContactForm/ContactForm.test.jsx +2 -3
  29. package/src/components/theme/Header/Header.jsx +2 -2
  30. package/src/components/theme/Login/Login.jsx +1 -1
  31. package/src/components/theme/SearchWidget/SearchWidget.jsx +38 -98
  32. package/src/components/theme/View/AlbumView.jsx +9 -1
  33. package/src/components/theme/View/EventView.jsx +6 -2
  34. package/src/components/theme/View/FileView.jsx +23 -18
  35. package/src/components/theme/View/ImageView.jsx +37 -32
  36. package/src/components/theme/View/LinkView.jsx +53 -78
  37. package/src/components/theme/View/ListingView.jsx +33 -27
  38. package/src/components/theme/View/SummaryView.jsx +47 -38
  39. package/src/components/theme/View/TabularView.jsx +59 -53
  40. package/src/hooks/client/useClient.js +11 -0
  41. package/src/hooks/index.js +1 -1
  42. package/theme/themes/pastanaga/extras/main.less +2 -1
  43. package/theme/themes/pastanaga/extras/toc.less +29 -0
  44. package/src/hooks/content/useContent.js +0 -31
  45. package/src/hooks/userSession/useToken.js +0 -5
@@ -1,22 +1,15 @@
1
- /**
2
- * Contact Form container.
3
- * @module components/theme/ContactForm/ContactForm
4
- */
5
-
6
- import React, { Component } from 'react';
7
- import PropTypes from 'prop-types';
8
- import { Helmet } from '@plone/volto/helpers';
9
- import { connect } from 'react-redux';
10
- import { compose } from 'redux';
1
+ import { useCallback, useEffect } from 'react';
11
2
  import { Portal } from 'react-portal';
12
3
  import { Container, Message, Icon } from 'semantic-ui-react';
13
- import { defineMessages, injectIntl } from 'react-intl';
14
- import { Link, withRouter } from 'react-router-dom';
4
+ import { defineMessages, useIntl } from 'react-intl';
5
+ import { Link, useHistory, useLocation } from 'react-router-dom';
15
6
  import { toast } from 'react-toastify';
16
7
 
17
- import { Form, Toolbar, Toast } from '@plone/volto/components';
18
8
  import { emailNotification } from '@plone/volto/actions';
19
- import { getBaseUrl } from '@plone/volto/helpers';
9
+ import { useDispatch, useSelector } from 'react-redux';
10
+ import { Form, Toolbar, Toast } from '@plone/volto/components';
11
+ import { getBaseUrl, Helmet, usePrevious } from '@plone/volto/helpers';
12
+ import { useClient } from '@plone/volto/hooks';
20
13
 
21
14
  const messages = defineMessages({
22
15
  send: {
@@ -65,195 +58,118 @@ const messages = defineMessages({
65
58
  },
66
59
  });
67
60
 
68
- /**
69
- * ContactForm class.
70
- * @class ContactForm
71
- * @extends Component
72
- */
73
- export class ContactFormComponent extends Component {
74
- /**
75
- * Property types.
76
- * @property {Object} propTypes Property types.
77
- * @static
78
- */
79
- static propTypes = {
80
- emailNotification: PropTypes.func.isRequired,
81
- error: PropTypes.shape({
82
- message: PropTypes.string,
83
- }),
84
- loading: PropTypes.bool,
85
- loaded: PropTypes.bool,
86
- pathname: PropTypes.string.isRequired,
87
- };
61
+ const useEmailNotification = () => {
62
+ const loading = useSelector((state) => state.emailNotification.loading);
63
+ const loaded = useSelector((state) => state.emailNotification.loaded);
64
+ const error = useSelector((state) => state.emailNotification.error);
88
65
 
89
- /**
90
- * Default properties.
91
- * @property {Object} defaultProps Default properties.
92
- * @static
93
- */
94
- static defaultProps = {
95
- error: null,
96
- loading: null,
97
- loaded: null,
98
- };
66
+ return { loading, loaded, error };
67
+ };
68
+
69
+ const ContactFormComponent = () => {
70
+ const history = useHistory();
71
+ const { pathname } = useLocation();
72
+ const dispatch = useDispatch();
73
+ const intl = useIntl();
74
+ const isClient = useClient();
99
75
 
100
- /**
101
- * Constructor
102
- * @method constructor
103
- * @param {Object} props Component properties
104
- * @constructs WysiwygEditor
105
- */
106
- constructor(props) {
107
- super(props);
108
- this.onSubmit = this.onSubmit.bind(this);
109
- this.onCancel = this.onCancel.bind(this);
110
- this.state = { isClient: false };
111
- }
76
+ const { loaded, loading, error } = useEmailNotification();
112
77
 
113
- /**
114
- * Component will receive props
115
- * @method componentWillReceiveProps
116
- * @param {Object} nextProps Next properties
117
- * @returns {undefined}
118
- */
119
- UNSAFE_componentWillReceiveProps(nextProps) {
120
- if (this.props.loading && nextProps.loaded) {
78
+ const prevloading = usePrevious(loading);
79
+
80
+ useEffect(() => {
81
+ if (prevloading && loaded) {
121
82
  toast.success(
122
83
  <Toast
123
84
  success
124
- title={this.props.intl.formatMessage(messages.success)}
125
- content={this.props.intl.formatMessage(messages.messageSent)}
85
+ title={intl.formatMessage(messages.success)}
86
+ content={intl.formatMessage(messages.messageSent)}
126
87
  />,
127
88
  );
128
89
  }
129
- }
90
+ }, [intl, loaded, prevloading]);
130
91
 
131
- /**
132
- * Component did mount
133
- * @method componentDidMount
134
- * @returns {undefined}
135
- */
136
- componentDidMount() {
137
- this.setState({ isClient: true });
138
- }
92
+ const onSubmit = (data) => {
93
+ const { from, message, name, subject } = data;
94
+ dispatch(emailNotification(from, message, name, subject));
95
+ };
139
96
 
140
- /**
141
- * On submit handler
142
- * @method onSubmit
143
- * @param {Object} data Data object.
144
- * @returns {undefined}
145
- */
146
- onSubmit(data) {
147
- this.props.emailNotification(
148
- data.from,
149
- data.message,
150
- data.name,
151
- data.subject,
152
- );
153
- }
97
+ const onCancel = useCallback(() => {
98
+ history.goBack();
99
+ }, [history]);
154
100
 
155
- /**
156
- * Cancel handler
157
- * @method onCancel
158
- * @returns {undefined}
159
- */
160
- onCancel() {
161
- this.props.history.goBack();
162
- }
163
- /**
164
- * Render method.
165
- * @method render
166
- * @returns {string} Markup for the component.
167
- */
168
- render() {
169
- return (
170
- <div id="contact-form">
171
- <Container id="view">
172
- <Helmet title={this.props.intl.formatMessage(messages.contactForm)} />
173
- {this.props.error && (
174
- <Message
175
- icon="warning"
176
- negative
177
- attached
178
- header={this.props.intl.formatMessage(messages.error)}
179
- content={this.props.error.message}
180
- />
181
- )}
182
- <Form
183
- onSubmit={this.onSubmit}
184
- onCancel={this.onCancel}
185
- formData={{ blocksLayoutFieldname: {} }}
186
- submitLabel={this.props.intl.formatMessage(messages.send)}
187
- resetAfterSubmit
188
- title={this.props.intl.formatMessage(messages.contactForm)}
189
- loading={this.props.loading}
190
- schema={{
191
- fieldsets: [
192
- {
193
- fields: ['name', 'from', 'subject', 'message'],
194
- id: 'default',
195
- title: this.props.intl.formatMessage(messages.default),
196
- },
197
- ],
198
- properties: {
199
- name: {
200
- title: this.props.intl.formatMessage(messages.name),
201
- type: 'string',
202
- },
203
- from: {
204
- title: this.props.intl.formatMessage(messages.from),
205
- type: 'email',
206
- },
207
- subject: {
208
- title: this.props.intl.formatMessage(messages.subject),
209
- type: 'string',
210
- },
211
- message: {
212
- title: this.props.intl.formatMessage(messages.message),
213
- type: 'string',
214
- widget: 'textarea',
215
- },
216
- },
217
- required: ['from', 'message'],
218
- }}
101
+ return (
102
+ <div id="contact-form">
103
+ <Container id="view">
104
+ <Helmet title={intl.formatMessage(messages.contactForm)} />
105
+ {error && (
106
+ <Message
107
+ icon="warning"
108
+ negative
109
+ attached
110
+ header={intl.formatMessage(messages.error)}
111
+ content={error.message}
219
112
  />
220
- {this.state.isClient && (
221
- <Portal node={document.getElementById('toolbar')}>
222
- <Toolbar
223
- pathname={this.props.pathname}
224
- hideDefaultViewButtons
225
- inner={
226
- <Link
227
- to={`${getBaseUrl(this.props.pathname)}`}
228
- className="item"
229
- >
230
- <Icon
231
- name="arrow left"
232
- size="big"
233
- color="blue"
234
- title={this.props.intl.formatMessage(messages.back)}
235
- />
236
- </Link>
237
- }
238
- />
239
- </Portal>
240
- )}
241
- </Container>
242
- </div>
243
- );
244
- }
245
- }
113
+ )}
114
+ <Form
115
+ onSubmit={onSubmit}
116
+ onCancel={onCancel}
117
+ formData={{ blocksLayoutFieldname: {} }}
118
+ submitLabel={intl.formatMessage(messages.send)}
119
+ resetAfterSubmit
120
+ title={intl.formatMessage(messages.contactForm)}
121
+ loading={loading}
122
+ schema={{
123
+ fieldsets: [
124
+ {
125
+ fields: ['name', 'from', 'subject', 'message'],
126
+ id: 'default',
127
+ title: intl.formatMessage(messages.default),
128
+ },
129
+ ],
130
+ properties: {
131
+ name: {
132
+ title: intl.formatMessage(messages.name),
133
+ type: 'string',
134
+ },
135
+ from: {
136
+ title: intl.formatMessage(messages.from),
137
+ type: 'email',
138
+ },
139
+ subject: {
140
+ title: intl.formatMessage(messages.subject),
141
+ type: 'string',
142
+ },
143
+ message: {
144
+ title: intl.formatMessage(messages.message),
145
+ type: 'string',
146
+ widget: 'textarea',
147
+ },
148
+ },
149
+ required: ['from', 'message'],
150
+ }}
151
+ />
152
+ {isClient && (
153
+ <Portal node={document.getElementById('toolbar')}>
154
+ <Toolbar
155
+ pathname={pathname}
156
+ hideDefaultViewButtons
157
+ inner={
158
+ <Link to={`${getBaseUrl(pathname)}`} className="item">
159
+ <Icon
160
+ name="arrow left"
161
+ size="big"
162
+ color="blue"
163
+ title={intl.formatMessage(messages.back)}
164
+ />
165
+ </Link>
166
+ }
167
+ />
168
+ </Portal>
169
+ )}
170
+ </Container>
171
+ </div>
172
+ );
173
+ };
246
174
 
247
- export default compose(
248
- withRouter,
249
- injectIntl,
250
- connect(
251
- (state, props) => ({
252
- loading: state.emailNotification.loading,
253
- loaded: state.emailNotification.loaded,
254
- error: state.emailNotification.error,
255
- pathname: props.location.pathname,
256
- }),
257
- { emailNotification },
258
- ),
259
- )(ContactFormComponent);
175
+ export default ContactFormComponent;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ContactFormComponent } from './ContactForm';
2
+ import ContactFormComponent from './ContactForm';
3
3
  import { injectIntl } from 'react-intl';
4
4
  import { RealStoreWrapper as Wrapper } from '@plone/volto/storybook';
5
5
 
@@ -10,7 +10,6 @@ jest.mock('react-portal', () => ({
10
10
  }));
11
11
 
12
12
  const mockStore = configureStore();
13
-
14
13
  describe('Contact form', () => {
15
14
  it('renders a contact form', () => {
16
15
  const store = mockStore({
@@ -29,7 +28,7 @@ describe('Contact form', () => {
29
28
  const component = renderer.create(
30
29
  <Provider store={store}>
31
30
  <MemoryRouter>
32
- <ContactForm location={{ pathname: '/blog' }} />
31
+ <ContactForm />
33
32
  </MemoryRouter>
34
33
  </Provider>,
35
34
  );
@@ -56,7 +55,7 @@ describe('Contact form', () => {
56
55
  const component = renderer.create(
57
56
  <Provider store={store}>
58
57
  <MemoryRouter>
59
- <ContactForm location={{ pathname: '/' }} />
58
+ <ContactForm />
60
59
  </MemoryRouter>
61
60
  </Provider>,
62
61
  );
@@ -1,6 +1,6 @@
1
1
  import { Container, Segment } from 'semantic-ui-react';
2
2
  import PropTypes from 'prop-types';
3
- import { useToken } from '@plone/volto/hooks/userSession/useToken';
3
+ import { useSelector, shallowEqual } from 'react-redux';
4
4
 
5
5
  import {
6
6
  Anontools,
@@ -11,7 +11,7 @@ import {
11
11
  } from '@plone/volto/components';
12
12
 
13
13
  const Header = ({ pathname }) => {
14
- const token = useToken();
14
+ const token = useSelector((state) => state.userSession.token, shallowEqual);
15
15
 
16
16
  return (
17
17
  <Segment basic className="header-wrapper" role="banner">
@@ -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,
@@ -1,14 +1,7 @@
1
- /**
2
- * Search widget component.
3
- * @module components/theme/SearchWidget/SearchWidget
4
- */
5
-
6
- import React, { Component } from 'react';
7
- import { withRouter } from 'react-router-dom';
1
+ import { useState } from 'react';
2
+ import { useHistory } from 'react-router-dom';
8
3
  import { Form, Input } from 'semantic-ui-react';
9
- import { compose } from 'redux';
10
- import { PropTypes } from 'prop-types';
11
- import { defineMessages, injectIntl } from 'react-intl';
4
+ import { defineMessages, useIntl } from 'react-intl';
12
5
 
13
6
  import { Icon } from '@plone/volto/components';
14
7
  import zoomSVG from '@plone/volto/icons/zoom.svg';
@@ -24,96 +17,43 @@ const messages = defineMessages({
24
17
  },
25
18
  });
26
19
 
27
- /**
28
- * SearchWidget component class.
29
- * @class SearchWidget
30
- * @extends Component
31
- */
32
- class SearchWidget extends Component {
33
- /**
34
- * Property types.
35
- * @property {Object} propTypes Property types.
36
- * @static
37
- */
38
- static propTypes = {
39
- pathname: PropTypes.string,
20
+ const SearchWidget = (props) => {
21
+ const intl = useIntl();
22
+ const [text, setText] = useState('');
23
+ const history = useHistory();
24
+ const onChangeText = (event, { value }) => {
25
+ setText(value);
40
26
  };
41
-
42
- /**
43
- * Constructor
44
- * @method constructor
45
- * @param {Object} props Component properties
46
- * @constructs WysiwygEditor
47
- */
48
- constructor(props) {
49
- super(props);
50
- this.onChangeText = this.onChangeText.bind(this);
51
- this.onSubmit = this.onSubmit.bind(this);
52
- this.state = {
53
- text: '',
54
- };
55
- }
56
-
57
- /**
58
- * On change text
59
- * @method onChangeText
60
- * @param {object} event Event object.
61
- * @param {string} value Text value.
62
- * @returns {undefined}
63
- */
64
- onChangeText(event, { value }) {
65
- this.setState({
66
- text: value,
67
- });
68
- }
69
-
70
- /**
71
- * Submit handler
72
- * @method onSubmit
73
- * @param {event} event Event object.
74
- * @returns {undefined}
75
- */
76
- onSubmit(event) {
27
+ const pathname = props.pathname;
28
+ const onSubmit = (event) => {
77
29
  const path =
78
- this.props.pathname?.length > 0
79
- ? `&path=${encodeURIComponent(this.props.pathname)}`
80
- : '';
81
- this.props.history.push(
82
- `/search?SearchableText=${encodeURIComponent(this.state.text)}${path}`,
83
- );
30
+ pathname?.length > 0 ? `&path=${encodeURIComponent(pathname)}` : '';
31
+
32
+ history.push(`/search?SearchableText=${encodeURIComponent(text)}${path}`);
84
33
  // reset input value
85
- this.setState({
86
- text: '',
87
- });
34
+ setText('');
88
35
  event.preventDefault();
89
- }
90
-
91
- /**
92
- * Render method.
93
- * @method render
94
- * @returns {string} Markup for the component.
95
- */
96
- render() {
97
- return (
98
- <Form action="/search" onSubmit={this.onSubmit}>
99
- <Form.Field className="searchbox">
100
- <Input
101
- aria-label={this.props.intl.formatMessage(messages.search)}
102
- onChange={this.onChangeText}
103
- name="SearchableText"
104
- value={this.state.text}
105
- transparent
106
- autoComplete="off"
107
- placeholder={this.props.intl.formatMessage(messages.searchSite)}
108
- title={this.props.intl.formatMessage(messages.search)}
109
- />
110
- <button aria-label={this.props.intl.formatMessage(messages.search)}>
111
- <Icon name={zoomSVG} size="18px" />
112
- </button>
113
- </Form.Field>
114
- </Form>
115
- );
116
- }
117
- }
36
+ };
118
37
 
119
- export default compose(withRouter, injectIntl)(SearchWidget);
38
+ return (
39
+ <Form action="/search" onSubmit={onSubmit}>
40
+ <Form.Field className="searchbox">
41
+ <Input
42
+ aria-label={intl.formatMessage(messages.search)}
43
+ onChange={onChangeText}
44
+ name="SearchableText"
45
+ value={text}
46
+ transparent
47
+ autoComplete="off"
48
+ placeholder={intl.formatMessage(messages.searchSite)}
49
+ title={intl.formatMessage(messages.search)}
50
+ />
51
+ <button aria-label={intl.formatMessage(messages.search)}>
52
+ <Icon name={zoomSVG} size="18px" />
53
+ </button>
54
+ </Form.Field>
55
+ </Form>
56
+ );
57
+ };
58
+
59
+ export default SearchWidget;
@@ -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.