@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.
- package/CHANGELOG.md +11 -0
- package/locales/ca/LC_MESSAGES/volto.po +5 -0
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +6 -1
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +5 -0
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +5 -0
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +5 -0
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +5 -0
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +5 -0
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +5 -0
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +5 -0
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +5 -0
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +5 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +5 -0
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +5 -0
- package/locales/ro.json +1 -1
- package/locales/volto.pot +6 -1
- package/locales/zh_CN/LC_MESSAGES/volto.po +5 -0
- package/locales/zh_CN.json +1 -1
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx +8 -3
- package/packages/volto-slate/src/blocks/Text/extensions/withDeserializers.js +3 -1
- package/src/components/manage/Blocks/HeroImageLeft/Edit.jsx +6 -1
- package/src/components/manage/Blocks/Image/Edit.jsx +11 -7
- package/src/components/manage/Contents/ContentsUploadModal.jsx +10 -5
- package/src/components/manage/Toast/Toast.jsx +1 -1
- package/src/components/manage/Widgets/FileWidget.jsx +2 -1
- package/src/config/index.js +1 -0
- package/src/helpers/Extensions/withBlockSchemaEnhancer.js +15 -11
- package/src/helpers/Extensions/withBlockSchemaEnhancer.test.js +145 -0
- package/src/helpers/FormValidation/FormValidation.js +29 -0
- package/src/helpers/MessageLabels/MessageLabels.js +4 -0
- package/src/helpers/index.js +3 -1
|
@@ -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 {
|
|
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 {
|
|
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 = (
|
|
182
|
-
this.
|
|
183
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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,
|
|
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
|
|
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, {
|
package/src/config/index.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
+
};
|
package/src/helpers/index.js
CHANGED
|
@@ -71,7 +71,9 @@ export {
|
|
|
71
71
|
|
|
72
72
|
export langmap from './LanguageMap/LanguageMap';
|
|
73
73
|
export Helmet from './Helmet/Helmet';
|
|
74
|
-
export FormValidation
|
|
74
|
+
export FormValidation, {
|
|
75
|
+
validateFileUploadSize,
|
|
76
|
+
} from './FormValidation/FormValidation';
|
|
75
77
|
export {
|
|
76
78
|
difference,
|
|
77
79
|
getColor,
|