@plone/volto 17.0.0-alpha.12 → 17.0.0-alpha.14

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 (64) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +62 -0
  3. package/README.md +2 -2
  4. package/docker-compose.yml +1 -1
  5. package/locales/ca/LC_MESSAGES/volto.po +10 -0
  6. package/locales/ca.json +1 -1
  7. package/locales/de/LC_MESSAGES/volto.po +11 -1
  8. package/locales/de.json +1 -1
  9. package/locales/en/LC_MESSAGES/volto.po +10 -0
  10. package/locales/en.json +1 -1
  11. package/locales/es/LC_MESSAGES/volto.po +53 -43
  12. package/locales/es.json +1 -1
  13. package/locales/eu/LC_MESSAGES/volto.po +10 -0
  14. package/locales/eu.json +1 -1
  15. package/locales/fi/LC_MESSAGES/volto.po +10 -0
  16. package/locales/fi.json +1 -1
  17. package/locales/fr/LC_MESSAGES/volto.po +10 -0
  18. package/locales/fr.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +10 -0
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +10 -0
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +10 -0
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +10 -0
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +10 -0
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +10 -0
  30. package/locales/ro.json +1 -1
  31. package/locales/volto.pot +11 -1
  32. package/locales/zh_CN/LC_MESSAGES/volto.po +10 -0
  33. package/locales/zh_CN.json +1 -1
  34. package/package.json +2 -1
  35. package/packages/volto-slate/package.json +1 -1
  36. package/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx +8 -3
  37. package/packages/volto-slate/src/blocks/Text/TextBlockView.jsx +20 -16
  38. package/packages/volto-slate/src/blocks/Text/extensions/withDeserializers.js +3 -1
  39. package/packages/volto-slate/src/editor/config.jsx +5 -4
  40. package/packages/volto-slate/src/editor/less/slate.less +28 -0
  41. package/packages/volto-slate/src/editor/render.jsx +68 -8
  42. package/src/components/manage/Blocks/HeroImageLeft/Edit.jsx +6 -1
  43. package/src/components/manage/Blocks/Image/Edit.jsx +11 -7
  44. package/src/components/manage/Blocks/Listing/ListingBody.jsx +30 -8
  45. package/src/components/manage/Blocks/Title/View.jsx +15 -5
  46. package/src/components/manage/Blocks/Title/View.test.jsx +16 -1
  47. package/src/components/manage/Blocks/ToC/View.jsx +8 -1
  48. package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +17 -4
  49. package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +6 -2
  50. package/src/components/manage/Contents/ContentsUploadModal.jsx +10 -5
  51. package/src/components/manage/Toast/Toast.jsx +1 -1
  52. package/src/components/manage/Widgets/FileWidget.jsx +2 -1
  53. package/src/components/theme/Anontools/Anontools.jsx +45 -72
  54. package/src/components/theme/Anontools/Anontools.test.jsx +16 -2
  55. package/src/config/index.js +1 -0
  56. package/src/helpers/Extensions/withBlockSchemaEnhancer.js +15 -11
  57. package/src/helpers/Extensions/withBlockSchemaEnhancer.test.js +145 -0
  58. package/src/helpers/FormValidation/FormValidation.js +29 -0
  59. package/src/helpers/MessageLabels/MessageLabels.js +8 -0
  60. package/src/helpers/ScrollToTop/ScrollToTop.jsx +5 -3
  61. package/src/helpers/index.js +3 -1
  62. package/src/hooks/clipboard/useClipboard.js +26 -0
  63. package/src/hooks/content/useContent.js +31 -0
  64. package/src/hooks/index.js +2 -0
@@ -1,14 +1,35 @@
1
- import React, { createRef } from 'react';
1
+ import React, { createRef, useMemo } from 'react';
2
2
  import { FormattedMessage, injectIntl } from 'react-intl';
3
3
  import cx from 'classnames';
4
4
  import { Pagination, Dimmer, Loader } from 'semantic-ui-react';
5
+ import Slugger from 'github-slugger';
5
6
  import { Icon } from '@plone/volto/components';
7
+ import { renderLinkElement } from '@plone/volto-slate/editor/render';
6
8
  import config from '@plone/volto/registry';
7
9
  import withQuerystringResults from './withQuerystringResults';
8
10
 
9
11
  import paginationLeftSVG from '@plone/volto/icons/left-key.svg';
10
12
  import paginationRightSVG from '@plone/volto/icons/right-key.svg';
11
13
 
14
+ const Headline = ({ headlineTag, id, data = {}, listingItems, isEditMode }) => {
15
+ let attr = { id };
16
+ const slug = Slugger.slug(data.headline);
17
+ attr.id = slug || id;
18
+ const LinkedHeadline = useMemo(() => renderLinkElement(headlineTag), [
19
+ headlineTag,
20
+ ]);
21
+ return (
22
+ <LinkedHeadline
23
+ mode={!isEditMode && 'view'}
24
+ children={data.headline}
25
+ attributes={attr}
26
+ className={cx('headline', {
27
+ emptyListing: !listingItems?.length > 0,
28
+ })}
29
+ />
30
+ );
31
+ };
32
+
12
33
  const ListingBody = withQuerystringResults((props) => {
13
34
  const {
14
35
  data = {},
@@ -22,6 +43,7 @@ const ListingBody = withQuerystringResults((props) => {
22
43
  nextBatch,
23
44
  isFolderContentsListing,
24
45
  hasLoaded,
46
+ id,
25
47
  } = props;
26
48
 
27
49
  let ListingBodyTemplate;
@@ -50,13 +72,13 @@ const ListingBody = withQuerystringResults((props) => {
50
72
  return (
51
73
  <>
52
74
  {data.headline && (
53
- <HeadlineTag
54
- className={cx('headline', {
55
- emptyListing: !listingItems?.length > 0,
56
- })}
57
- >
58
- {data.headline}
59
- </HeadlineTag>
75
+ <Headline
76
+ headlineTag={HeadlineTag}
77
+ id={id}
78
+ listingItems={listingItems}
79
+ data={data}
80
+ isEditMode={isEditMode}
81
+ />
60
82
  )}
61
83
  {listingItems?.length > 0 ? (
62
84
  <div ref={listingRef}>
@@ -3,19 +3,29 @@
3
3
  * @module volto-slate/blocks/Title/TitleBlockView
4
4
  */
5
5
 
6
- import React from 'react';
6
+ import React, { useMemo } from 'react';
7
7
  import PropTypes from 'prop-types';
8
+ import Slugger from 'github-slugger';
9
+ import { renderLinkElement } from '@plone/volto-slate/editor/render';
8
10
 
9
11
  /**
10
12
  * View title block component.
11
13
  * @class View
12
14
  * @extends Component
13
15
  */
14
- const TitleBlockView = ({ properties, metadata }) => {
16
+ const TitleBlockView = ({ properties, metadata, id, children }) => {
17
+ let attr = { id };
18
+ const title = (properties || metadata)['title'];
19
+ const slug = Slugger.slug(title);
20
+ attr.id = slug || id;
21
+ const LinkedTitle = useMemo(() => renderLinkElement('h1'), []);
15
22
  return (
16
- <h1 className="documentFirstHeading">
17
- {(metadata || properties)['title'] || ''}
18
- </h1>
23
+ <LinkedTitle
24
+ mode="view"
25
+ children={title ?? children}
26
+ attributes={attr}
27
+ className={'documentFirstHeading'}
28
+ />
19
29
  );
20
30
  };
21
31
 
@@ -1,10 +1,25 @@
1
1
  import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
+ import configureStore from 'redux-mock-store';
4
+ import { Provider } from 'react-intl-redux';
5
+ import { MemoryRouter } from 'react-router-dom';
3
6
  import View from './View';
4
7
 
8
+ const mockStore = configureStore();
9
+
5
10
  test('renders a view title component', () => {
11
+ const store = mockStore({
12
+ intl: {
13
+ locale: 'en',
14
+ messages: {},
15
+ },
16
+ });
6
17
  const component = renderer.create(
7
- <View properties={{ title: 'My Title' }} />,
18
+ <Provider store={store}>
19
+ <MemoryRouter>
20
+ <View properties={{ title: 'My Title' }} id="a123" />
21
+ </MemoryRouter>
22
+ </Provider>,
8
23
  );
9
24
  const json = component.toJSON();
10
25
  expect(json).toMatchSnapshot();
@@ -56,7 +56,14 @@ const View = (props) => {
56
56
  const items = [];
57
57
  if (!level || !levels.includes(level)) return;
58
58
  tocEntriesLayout.push(id);
59
- tocEntries[id] = { level, title: title || block.plaintext, items, id };
59
+ tocEntries[id] = {
60
+ level,
61
+ title: title || block.plaintext,
62
+ items,
63
+ id,
64
+ override_toc: block.override_toc,
65
+ plaintext: block.plaintext,
66
+ };
60
67
  if (level < rootLevel) {
61
68
  rootLevel = level;
62
69
  }
@@ -8,15 +8,27 @@ import PropTypes from 'prop-types';
8
8
  import { map } from 'lodash';
9
9
  import { List } from 'semantic-ui-react';
10
10
  import { FormattedMessage, injectIntl } from 'react-intl';
11
+ import { useHistory } from 'react-router-dom';
11
12
  import AnchorLink from 'react-anchor-link-smooth-scroll';
13
+ import Slugger from 'github-slugger';
12
14
 
13
- const RenderListItems = ({ items, data }) => {
15
+ const RenderListItems = ({ items, data, history }) => {
14
16
  return map(items, (item) => {
15
- const { id, level, title } = item;
17
+ const { id, level, title, override_toc, plaintext } = item;
18
+ const slug = override_toc
19
+ ? Slugger.slug(plaintext)
20
+ : Slugger.slug(title) || id;
16
21
  return (
17
22
  item && (
18
23
  <List.Item key={id} className={`item headline-${level}`} as="li">
19
- <AnchorLink href={`#${id}`}>{title}</AnchorLink>
24
+ <AnchorLink
25
+ href={`#${slug}`}
26
+ onClick={(e) => {
27
+ history.push({ hash: slug });
28
+ }}
29
+ >
30
+ {title}
31
+ </AnchorLink>
20
32
  {item.items?.length > 0 && (
21
33
  <List
22
34
  ordered={data.ordered}
@@ -38,6 +50,7 @@ const RenderListItems = ({ items, data }) => {
38
50
  * @extends Component
39
51
  */
40
52
  const View = ({ data, tocEntries }) => {
53
+ const history = useHistory();
41
54
  return (
42
55
  <>
43
56
  {data.title && !data.hide_title ? (
@@ -57,7 +70,7 @@ const View = ({ data, tocEntries }) => {
57
70
  bulleted={!data.ordered}
58
71
  as={data.ordered ? 'ol' : 'ul'}
59
72
  >
60
- <RenderListItems items={tocEntries} data={data} />
73
+ <RenderListItems items={tocEntries} data={data} history={history} />
61
74
  </List>
62
75
  </>
63
76
  );
@@ -9,15 +9,19 @@ import { map } from 'lodash';
9
9
  import { Menu } from 'semantic-ui-react';
10
10
  import { FormattedMessage, injectIntl } from 'react-intl';
11
11
  import AnchorLink from 'react-anchor-link-smooth-scroll';
12
+ import Slugger from 'github-slugger';
12
13
 
13
14
  const RenderMenuItems = ({ items }) => {
14
15
  return map(items, (item) => {
15
- const { id, level, title } = item;
16
+ const { id, level, title, override_toc, plaintext } = item;
17
+ const slug = override_toc
18
+ ? Slugger.slug(plaintext)
19
+ : Slugger.slug(title) || id;
16
20
  return (
17
21
  item && (
18
22
  <React.Fragment key={id}>
19
23
  <Menu.Item className={`headline-${level}`}>
20
- <AnchorLink href={`#${id}`}>{title}</AnchorLink>
24
+ <AnchorLink href={`#${slug}`}>{title}</AnchorLink>
21
25
  </Menu.Item>
22
26
  {item.items?.length > 0 && <RenderMenuItems items={item.items} />}
23
27
  </React.Fragment>
@@ -25,6 +25,7 @@ import { readAsDataURL } from 'promise-file-reader';
25
25
  import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
26
26
  import { FormattedRelativeDate } from '@plone/volto/components';
27
27
  import { createContent } from '@plone/volto/actions';
28
+ import { validateFileUploadSize } from '@plone/volto/helpers';
28
29
 
29
30
  const Dropzone = loadable(() => import('react-dropzone'));
30
31
 
@@ -121,14 +122,18 @@ class ContentsUploadModal extends Component {
121
122
  * @returns {undefined}
122
123
  */
123
124
  onDrop = async (files) => {
125
+ const validFiles = [];
124
126
  for (let i = 0; i < files.length; i++) {
125
- await readAsDataURL(files[i]).then((data) => {
126
- const fields = data.match(/^data:(.*);(.*),(.*)$/);
127
- files[i].preview = fields[0];
128
- });
127
+ if (validateFileUploadSize(files[i], this.props.intl.formatMessage)) {
128
+ await readAsDataURL(files[i]).then((data) => {
129
+ const fields = data.match(/^data:(.*);(.*),(.*)$/);
130
+ files[i].preview = fields[0];
131
+ });
132
+ validFiles.push(files[i]);
133
+ }
129
134
  }
130
135
  this.setState({
131
- files: concat(this.state.files, files),
136
+ files: concat(this.state.files, validFiles),
132
137
  });
133
138
  };
134
139
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Icon } from '@plone/volto/components';
3
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
4
4
 
5
5
  import successSVG from '@plone/volto/icons/ready.svg';
6
6
  import infoSVG from '@plone/volto/icons/info.svg';
@@ -11,7 +11,7 @@ import { injectIntl } from 'react-intl';
11
11
  import deleteSVG from '@plone/volto/icons/delete.svg';
12
12
  import { Icon, FormFieldWrapper } from '@plone/volto/components';
13
13
  import loadable from '@loadable/component';
14
- import { flattenToAppURL } from '@plone/volto/helpers';
14
+ import { flattenToAppURL, validateFileUploadSize } from '@plone/volto/helpers';
15
15
  import { defineMessages, useIntl } from 'react-intl';
16
16
 
17
17
  const imageMimetypes = [
@@ -95,6 +95,7 @@ const FileWidget = (props) => {
95
95
  */
96
96
  const onDrop = (files) => {
97
97
  const file = files[0];
98
+ if (!validateFileUploadSize(file, intl.formatMessage)) return;
98
99
  readAsDataURL(file).then((data) => {
99
100
  const fields = data.match(/^data:(.*);(.*),(.*)$/);
100
101
  onChange(id, {
@@ -1,83 +1,56 @@
1
- /**
2
- * Anontools component.
3
- * @module components/theme/Anontools/Anontools
4
- */
5
-
6
- import React, { Component } from 'react';
7
1
  import PropTypes from 'prop-types';
8
- import { connect } from 'react-redux';
9
2
  import { Link } from 'react-router-dom';
10
3
  import { Menu } from 'semantic-ui-react';
11
4
  import { FormattedMessage } from 'react-intl';
5
+ import { flattenToAppURL } from '@plone/volto/helpers';
6
+ import { useToken } from '@plone/volto/hooks/userSession/useToken';
7
+ import { useContent } from '@plone/volto/hooks/content/useContent';
12
8
  import config from '@plone/volto/registry';
13
9
 
14
- /**
15
- * Anontools container class.
16
- */
17
- export class Anontools extends Component {
18
- /**
19
- * Property types.
20
- * @property {Object} propTypes Property types.
21
- * @static
22
- */
23
- static propTypes = {
24
- token: PropTypes.string,
25
- content: PropTypes.shape({
26
- '@id': PropTypes.string,
27
- }),
28
- };
29
-
30
- /**
31
- * Default properties.
32
- * @property {Object} defaultProps Default properties.
33
- * @static
34
- */
35
- static defaultProps = {
36
- token: null,
37
- content: {
38
- '@id': null,
39
- },
40
- };
10
+ const Anontools = () => {
11
+ const token = useToken();
12
+ const { data: content } = useContent();
41
13
 
42
- /**
43
- * Render method.
44
- * @method render
45
- * @returns {string} Markup for the component.
46
- */
47
- render() {
48
- const { settings } = config;
49
- return (
50
- !this.props.token && (
51
- <Menu pointing secondary floated="right">
14
+ const { settings } = config;
15
+ return (
16
+ !token && (
17
+ <Menu pointing secondary floated="right">
18
+ <Menu.Item>
19
+ <Link
20
+ aria-label="login"
21
+ to={`/login${
22
+ content?.['@id']
23
+ ? `?return_url=${flattenToAppURL(content['@id'])}`
24
+ : ''
25
+ }`}
26
+ >
27
+ <FormattedMessage id="Log in" defaultMessage="Log in" />
28
+ </Link>
29
+ </Menu.Item>
30
+ {settings.showSelfRegistration && (
52
31
  <Menu.Item>
53
- <Link
54
- aria-label="login"
55
- to={`/login${
56
- this.props.content?.['@id']
57
- ? `?return_url=${this.props.content['@id'].replace(
58
- settings.apiPath,
59
- '',
60
- )}`
61
- : ''
62
- }`}
63
- >
64
- <FormattedMessage id="Log in" defaultMessage="Log in" />
32
+ <Link aria-label="register" to="/register">
33
+ <FormattedMessage id="Register" defaultMessage="Register" />
65
34
  </Link>
66
35
  </Menu.Item>
67
- {settings.showSelfRegistration && (
68
- <Menu.Item>
69
- <Link aria-label="register" to="/register">
70
- <FormattedMessage id="Register" defaultMessage="Register" />
71
- </Link>
72
- </Menu.Item>
73
- )}
74
- </Menu>
75
- )
76
- );
77
- }
78
- }
36
+ )}
37
+ </Menu>
38
+ )
39
+ );
40
+ };
41
+
42
+ export default Anontools;
43
+
44
+ Anontools.propTypes = {
45
+ token: PropTypes.string,
46
+ content: PropTypes.shape({
47
+ '@id': PropTypes.string,
48
+ }),
49
+ };
79
50
 
80
- export default connect((state) => ({
81
- token: state.userSession.token,
82
- content: state.content.data,
83
- }))(Anontools);
51
+ Anontools.defaultProps = {
52
+ token: null,
53
+ content: {
54
+ '@id': null,
55
+ },
56
+ };
@@ -12,7 +12,14 @@ describe('Anontools', () => {
12
12
  it('renders an anontools component when no token is specified', () => {
13
13
  const store = mockStore({
14
14
  userSession: { token: null },
15
- content: { data: { '@id': 'myid' } },
15
+ content: {
16
+ data: { '@id': 'myid' },
17
+ get: {
18
+ loading: false,
19
+ loaded: true,
20
+ error: null,
21
+ },
22
+ },
16
23
  intl: {
17
24
  locale: 'en',
18
25
  messages: {},
@@ -32,7 +39,14 @@ describe('Anontools', () => {
32
39
  it('should not render an anontools component when a token is specified', () => {
33
40
  const store = mockStore({
34
41
  userSession: { token: '1234' },
35
- content: { data: {} },
42
+ content: {
43
+ data: {},
44
+ get: {
45
+ loading: false,
46
+ loaded: true,
47
+ error: null,
48
+ },
49
+ },
36
50
  intl: {
37
51
  locale: 'en',
38
52
  messages: {},
@@ -150,6 +150,7 @@ let config = {
150
150
  },
151
151
  appExtras: [],
152
152
  maxResponseSize: 2000000000, // This is superagent default (200 mb)
153
+ maxFileUploadSize: null,
153
154
  serverConfig,
154
155
  storeExtenders: [],
155
156
  showTags: true,
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { defineMessages } from 'react-intl';
3
3
  import { useIntl } from 'react-intl';
4
+ import { find, isEmpty } from 'lodash';
4
5
  import config from '@plone/volto/registry';
5
6
  import { cloneDeepSchema } from '@plone/volto/helpers/Utils/Utils';
6
7
 
@@ -291,20 +292,23 @@ export const EMPTY_STYLES_SCHEMA = {
291
292
  };
292
293
 
293
294
  /**
294
- * Creates the `styles` field and fieldset in a schema
295
+ * Adds the `styles` field and 'styling' fieldset in a given schema
295
296
  */
296
297
  export const addStyling = ({ schema, formData, intl }) => {
297
- schema.fieldsets.push({
298
- id: 'styling',
299
- title: intl.formatMessage(messages.styling),
300
- fields: ['styles'],
301
- });
298
+ if (isEmpty(find(schema.fieldsets, { id: 'styling' }))) {
299
+ schema.fieldsets.push({
300
+ id: 'styling',
301
+ title: intl.formatMessage(messages.styling),
302
+ fields: ['styles'],
303
+ });
304
+
305
+ schema.properties.styles = {
306
+ widget: 'object',
307
+ title: intl.formatMessage(messages.styling),
308
+ schema: cloneDeepSchema(EMPTY_STYLES_SCHEMA),
309
+ };
310
+ }
302
311
 
303
- schema.properties.styles = {
304
- widget: 'object',
305
- title: intl.formatMessage(messages.styling),
306
- schema: EMPTY_STYLES_SCHEMA,
307
- };
308
312
  return schema;
309
313
  };
310
314
 
@@ -2,6 +2,7 @@ import {
2
2
  addExtensionFieldToSchema,
3
3
  applySchemaEnhancer,
4
4
  composeSchema,
5
+ addStyling,
5
6
  } from './withBlockSchemaEnhancer';
6
7
 
7
8
  import config from '@plone/volto/registry';
@@ -246,3 +247,147 @@ describe('composeSchema', () => {
246
247
  expect(res).toStrictEqual([6, 9]);
247
248
  });
248
249
  });
250
+
251
+ describe('addStyling', () => {
252
+ it('returns an enhanced schema with the styling wrapper object on it', () => {
253
+ const intl = { formatMessage: () => 'Styling' };
254
+
255
+ const schema = {
256
+ fieldsets: [
257
+ {
258
+ id: 'default',
259
+ title: 'Default',
260
+ fields: [],
261
+ },
262
+ ],
263
+ properties: {},
264
+ required: [],
265
+ };
266
+
267
+ const result = addStyling({ schema, intl });
268
+
269
+ expect(result).toStrictEqual({
270
+ fieldsets: [
271
+ { id: 'default', title: 'Default', fields: [] },
272
+ { id: 'styling', title: 'Styling', fields: ['styles'] },
273
+ ],
274
+ properties: {
275
+ styles: {
276
+ widget: 'object',
277
+ title: 'Styling',
278
+ schema: {
279
+ fieldsets: [
280
+ {
281
+ fields: [],
282
+ id: 'default',
283
+ title: 'Default',
284
+ },
285
+ ],
286
+ properties: {},
287
+ required: [],
288
+ },
289
+ },
290
+ },
291
+ required: [],
292
+ });
293
+ });
294
+
295
+ it('multiple schema enhancers', () => {
296
+ const intl = { formatMessage: () => 'Styling' };
297
+
298
+ const schema1 = {
299
+ fieldsets: [
300
+ {
301
+ id: 'default',
302
+ title: 'Default',
303
+ fields: [],
304
+ },
305
+ ],
306
+ properties: {},
307
+ required: [],
308
+ };
309
+
310
+ const schema2 = {
311
+ fieldsets: [
312
+ {
313
+ id: 'default',
314
+ title: 'Default',
315
+ fields: [],
316
+ },
317
+ ],
318
+ properties: {},
319
+ required: [],
320
+ };
321
+
322
+ const result = addStyling({ schema: schema1, intl });
323
+
324
+ // We add some fields to the styling schema
325
+ result.properties.styles.schema.properties.align = {
326
+ widget: 'align',
327
+ title: 'align',
328
+ actions: ['left', 'right', 'center'],
329
+ default: 'left',
330
+ };
331
+
332
+ result.properties.styles.schema.fieldsets[0].fields = ['align'];
333
+
334
+ const result2 = addStyling({ schema: schema2, intl });
335
+
336
+ expect(result).toStrictEqual({
337
+ fieldsets: [
338
+ { id: 'default', title: 'Default', fields: [] },
339
+ { id: 'styling', title: 'Styling', fields: ['styles'] },
340
+ ],
341
+ properties: {
342
+ styles: {
343
+ widget: 'object',
344
+ title: 'Styling',
345
+ schema: {
346
+ fieldsets: [
347
+ {
348
+ fields: ['align'],
349
+ id: 'default',
350
+ title: 'Default',
351
+ },
352
+ ],
353
+ properties: {
354
+ align: {
355
+ widget: 'align',
356
+ title: 'align',
357
+ actions: ['left', 'right', 'center'],
358
+ default: 'left',
359
+ },
360
+ },
361
+ required: [],
362
+ },
363
+ },
364
+ },
365
+ required: [],
366
+ });
367
+
368
+ expect(result2).toStrictEqual({
369
+ fieldsets: [
370
+ { id: 'default', title: 'Default', fields: [] },
371
+ { id: 'styling', title: 'Styling', fields: ['styles'] },
372
+ ],
373
+ properties: {
374
+ styles: {
375
+ widget: 'object',
376
+ title: 'Styling',
377
+ schema: {
378
+ fieldsets: [
379
+ {
380
+ fields: [],
381
+ id: 'default',
382
+ title: 'Default',
383
+ },
384
+ ],
385
+ properties: {},
386
+ required: [],
387
+ },
388
+ },
389
+ },
390
+ required: [],
391
+ });
392
+ });
393
+ });
@@ -1,5 +1,8 @@
1
1
  import { map, uniq, keys, intersection, isEmpty } from 'lodash';
2
2
  import { messages } from '../MessageLabels/MessageLabels';
3
+ import config from '@plone/volto/registry';
4
+ import { toast } from 'react-toastify';
5
+ import Toast from '@plone/volto/components/manage/Toast/Toast';
3
6
 
4
7
  /**
5
8
  * Will return the intl message if invalid
@@ -369,3 +372,29 @@ class FormValidation {
369
372
  }
370
373
 
371
374
  export default FormValidation;
375
+
376
+ /**
377
+ * Check if a file upload is within the maximum size limit.
378
+ * @param {File} file
379
+ * @param {Function} intlFunc
380
+ * @returns {Boolean}
381
+ */
382
+ export const validateFileUploadSize = (file, intlFunc) => {
383
+ const isValid =
384
+ !config.settings.maxFileUploadSize ||
385
+ file.size <= config.settings.maxFileUploadSize;
386
+ if (!isValid) {
387
+ toast.error(
388
+ <Toast
389
+ error
390
+ title={intlFunc(messages.error)}
391
+ content={intlFunc(messages.fileTooLarge, {
392
+ limit: `${Math.floor(
393
+ config.settings.maxFileUploadSize / 1024 / 1024,
394
+ )}MB`,
395
+ })}
396
+ />,
397
+ );
398
+ }
399
+ return isValid;
400
+ };