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

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/CHANGELOG.md +11 -0
  2. package/locales/ca/LC_MESSAGES/volto.po +5 -0
  3. package/locales/ca.json +1 -1
  4. package/locales/de/LC_MESSAGES/volto.po +6 -1
  5. package/locales/de.json +1 -1
  6. package/locales/en/LC_MESSAGES/volto.po +5 -0
  7. package/locales/en.json +1 -1
  8. package/locales/es/LC_MESSAGES/volto.po +5 -0
  9. package/locales/es.json +1 -1
  10. package/locales/eu/LC_MESSAGES/volto.po +5 -0
  11. package/locales/eu.json +1 -1
  12. package/locales/fi/LC_MESSAGES/volto.po +5 -0
  13. package/locales/fi.json +1 -1
  14. package/locales/fr/LC_MESSAGES/volto.po +5 -0
  15. package/locales/fr.json +1 -1
  16. package/locales/it/LC_MESSAGES/volto.po +5 -0
  17. package/locales/it.json +1 -1
  18. package/locales/ja/LC_MESSAGES/volto.po +5 -0
  19. package/locales/ja.json +1 -1
  20. package/locales/nl/LC_MESSAGES/volto.po +5 -0
  21. package/locales/nl.json +1 -1
  22. package/locales/pt/LC_MESSAGES/volto.po +5 -0
  23. package/locales/pt.json +1 -1
  24. package/locales/pt_BR/LC_MESSAGES/volto.po +5 -0
  25. package/locales/pt_BR.json +1 -1
  26. package/locales/ro/LC_MESSAGES/volto.po +5 -0
  27. package/locales/ro.json +1 -1
  28. package/locales/volto.pot +6 -1
  29. package/locales/zh_CN/LC_MESSAGES/volto.po +5 -0
  30. package/locales/zh_CN.json +1 -1
  31. package/package.json +1 -1
  32. package/packages/volto-slate/package.json +1 -1
  33. package/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx +8 -3
  34. package/packages/volto-slate/src/blocks/Text/extensions/withDeserializers.js +3 -1
  35. package/src/components/manage/Blocks/HeroImageLeft/Edit.jsx +6 -1
  36. package/src/components/manage/Blocks/Image/Edit.jsx +11 -7
  37. package/src/components/manage/Contents/ContentsUploadModal.jsx +10 -5
  38. package/src/components/manage/Toast/Toast.jsx +1 -1
  39. package/src/components/manage/Widgets/FileWidget.jsx +2 -1
  40. package/src/config/index.js +1 -0
  41. package/src/helpers/Extensions/withBlockSchemaEnhancer.js +15 -11
  42. package/src/helpers/Extensions/withBlockSchemaEnhancer.test.js +145 -0
  43. package/src/helpers/FormValidation/FormValidation.js +29 -0
  44. package/src/helpers/MessageLabels/MessageLabels.js +4 -0
  45. package/src/helpers/index.js +3 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "17.0.0-alpha.12",
3
+ "version": "17.0.0-alpha.13",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -6,7 +6,11 @@ import { defineMessages, useIntl } from 'react-intl';
6
6
  import { useInView } from 'react-intersection-observer';
7
7
  import { Dimmer, Loader, Message, Segment } from 'semantic-ui-react';
8
8
 
9
- import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers';
9
+ import {
10
+ flattenToAppURL,
11
+ getBaseUrl,
12
+ validateFileUploadSize,
13
+ } from '@plone/volto/helpers';
10
14
  import config from '@plone/volto/registry';
11
15
  import {
12
16
  BlockDataForm,
@@ -71,6 +75,7 @@ export const DefaultTextBlockEditor = (props) => {
71
75
  const { slate } = config.settings;
72
76
  const { textblockExtensions } = slate;
73
77
  const { value } = data;
78
+ const intl = useIntl();
74
79
 
75
80
  // const [addNewBlockOpened, setAddNewBlockOpened] = React.useState();
76
81
  const [showDropzone, setShowDropzone] = React.useState(false);
@@ -106,6 +111,7 @@ export const DefaultTextBlockEditor = (props) => {
106
111
  files.forEach((file) => {
107
112
  const [mime] = file.type.split('/');
108
113
  if (mime !== 'image') return;
114
+ if (!validateFileUploadSize(file, intl.formatMessage)) return;
109
115
 
110
116
  readAsDataURL(file).then((data) => {
111
117
  const fields = data.match(/^data:(.*);(.*),(.*)$/);
@@ -127,7 +133,7 @@ export const DefaultTextBlockEditor = (props) => {
127
133
  });
128
134
  setShowDropzone(false);
129
135
  },
130
- [pathname, uploadContent, block],
136
+ [pathname, uploadContent, block, intl.formatMessage],
131
137
  );
132
138
 
133
139
  const { loaded, loading } = uploadRequest;
@@ -178,7 +184,6 @@ export const DefaultTextBlockEditor = (props) => {
178
184
  instructions = formDescription;
179
185
  }
180
186
 
181
- const intl = useIntl();
182
187
  const placeholder =
183
188
  data.placeholder || formTitle || intl.formatMessage(messages.text);
184
189
  const schema = TextBlockSchema(data);
@@ -1,7 +1,7 @@
1
1
  import isUrl from 'is-url';
2
2
  import imageExtensions from 'image-extensions';
3
3
  import { blockTagDeserializer } from '@plone/volto-slate/editor/deserialize';
4
- import { getBaseUrl } from '@plone/volto/helpers';
4
+ import { getBaseUrl, validateFileUploadSize } from '@plone/volto/helpers';
5
5
  import { v4 as uuid } from 'uuid';
6
6
  import { Transforms } from 'slate';
7
7
 
@@ -66,7 +66,9 @@ export const withDeserializers = (editor) => {
66
66
  ...editor.dataTransferHandlers,
67
67
  files: (files) => {
68
68
  const unprocessed = [];
69
+ const { intl } = editor.getBlockProps();
69
70
  for (const file of files) {
71
+ if (!validateFileUploadSize(file, intl.formatMessage)) return;
70
72
  const reader = new FileReader();
71
73
  const [mime] = file.type.split('/');
72
74
  if (mime === 'image') {
@@ -14,7 +14,11 @@ import { defineMessages, injectIntl } from 'react-intl';
14
14
  import cx from 'classnames';
15
15
 
16
16
  import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
17
- import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers';
17
+ import {
18
+ flattenToAppURL,
19
+ getBaseUrl,
20
+ validateFileUploadSize,
21
+ } from '@plone/volto/helpers';
18
22
  import { createContent } from '@plone/volto/actions';
19
23
  import { Icon, SidebarPortal, LinkMore } from '@plone/volto/components';
20
24
 
@@ -275,6 +279,7 @@ class EditComponent extends Component {
275
279
  */
276
280
  onUploadImage({ target }) {
277
281
  const file = target.files[0];
282
+ if (!validateFileUploadSize(file, this.props.intl.formatMessage)) return;
278
283
  this.setState({
279
284
  uploading: true,
280
285
  });
@@ -21,6 +21,7 @@ import {
21
21
  flattenToAppURL,
22
22
  getBaseUrl,
23
23
  isInternalURL,
24
+ validateFileUploadSize,
24
25
  } from '@plone/volto/helpers';
25
26
 
26
27
  import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
@@ -125,6 +126,7 @@ class Edit extends Component {
125
126
  onUploadImage = (e) => {
126
127
  e.stopPropagation();
127
128
  const file = e.target.files[0];
129
+ if (!validateFileUploadSize(file, this.props.intl.formatMessage)) return;
128
130
  this.setState({
129
131
  uploading: true,
130
132
  });
@@ -178,23 +180,25 @@ class Edit extends Component {
178
180
  * @param {array} files File objects
179
181
  * @returns {undefined}
180
182
  */
181
- onDrop = (file) => {
182
- this.setState({
183
- uploading: true,
184
- });
183
+ onDrop = (files) => {
184
+ if (!validateFileUploadSize(files[0], this.props.intl.formatMessage)) {
185
+ this.setState({ dragging: false });
186
+ return;
187
+ }
188
+ this.setState({ uploading: true });
185
189
 
186
- readAsDataURL(file[0]).then((data) => {
190
+ readAsDataURL(files[0]).then((data) => {
187
191
  const fields = data.match(/^data:(.*);(.*),(.*)$/);
188
192
  this.props.createContent(
189
193
  getBaseUrl(this.props.pathname),
190
194
  {
191
195
  '@type': 'Image',
192
- title: file[0].name,
196
+ title: files[0].name,
193
197
  image: {
194
198
  data: fields[3],
195
199
  encoding: fields[2],
196
200
  'content-type': fields[1],
197
- filename: file[0].name,
201
+ filename: files[0].name,
198
202
  },
199
203
  },
200
204
  this.props.block,
@@ -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, {
@@ -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
+ };
@@ -332,4 +332,8 @@ export const messages = defineMessages({
332
332
  id: 'Filter',
333
333
  defaultMessage: 'Filter',
334
334
  },
335
+ fileTooLarge: {
336
+ id: 'fileTooLarge',
337
+ defaultMessage: 'This website does not accept files larger than {limit}',
338
+ },
335
339
  });
@@ -71,7 +71,9 @@ export {
71
71
 
72
72
  export langmap from './LanguageMap/LanguageMap';
73
73
  export Helmet from './Helmet/Helmet';
74
- export FormValidation from './FormValidation/FormValidation';
74
+ export FormValidation, {
75
+ validateFileUploadSize,
76
+ } from './FormValidation/FormValidation';
75
77
  export {
76
78
  difference,
77
79
  getColor,