@evoke-platform/ui-components 1.6.1-dev.0 → 1.6.1-testing.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.
@@ -12,6 +12,7 @@ type DocumentProps = {
12
12
  objectId?: string;
13
13
  validate?: DocumentValidation;
14
14
  value: (File | SavedDocumentReference)[] | undefined;
15
+ onFileRejections: (fileRejectionsErrors: string[]) => void;
15
16
  };
16
17
  export declare const Document: (props: DocumentProps) => React.JSX.Element;
17
18
  export {};
@@ -1,16 +1,32 @@
1
1
  import { isNil } from 'lodash';
2
+ import prettyBytes from 'pretty-bytes';
2
3
  import React, { useEffect, useState } from 'react';
3
4
  import { useDropzone } from 'react-dropzone';
4
5
  import { UploadCloud } from '../../../../../icons';
5
6
  import { Skeleton, Snackbar, Typography } from '../../../../core';
6
7
  import { Box, Grid } from '../../../../layout';
8
+ import { createAcceptObject } from '../../../util';
7
9
  import { getPrefixedUrl } from '../../utils';
8
10
  import { DocumentList } from './DocumentList';
9
11
  export const Document = (props) => {
10
- const { id, handleChange, property, instance, canUpdateProperty, apiServices, error, objectId, value, validate } = props;
12
+ const { id, handleChange, property, instance, canUpdateProperty, apiServices, error, objectId, value, validate, onFileRejections, } = props;
11
13
  const [documents, setDocuments] = useState();
12
14
  const [hasUpdatePermission, setHasUpdatePermission] = useState();
13
15
  const [snackbarError, setSnackbarError] = useState();
16
+ let allowedTypesMessage = '';
17
+ if (validate?.allowedFileExtensions?.length) {
18
+ if (validate.allowedFileExtensions.length === 1) {
19
+ allowedTypesMessage = validate.allowedFileExtensions[0];
20
+ }
21
+ else {
22
+ allowedTypesMessage = `${validate.allowedFileExtensions.slice(0, -1).join(', ')} or ${validate.allowedFileExtensions.slice(-1)[0]}`;
23
+ }
24
+ }
25
+ const maxSizeInBytes = Number.isFinite(validate?.maxSizeInKB)
26
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
27
+ validate.maxSizeInKB * 1000 // convert to bytes
28
+ : undefined;
29
+ const formattedMaxSize = maxSizeInBytes !== undefined ? prettyBytes(maxSizeInBytes) : '';
14
30
  useEffect(() => {
15
31
  setDocuments(value);
16
32
  }, [value]);
@@ -36,15 +52,27 @@ export const Document = (props) => {
36
52
  handleChange(property.id, newDocuments);
37
53
  };
38
54
  const uploadDisabled = !!validate?.maxDocuments && (documents?.length ?? 0) >= validate.maxDocuments;
39
- const { getRootProps, getInputProps, open } = useDropzone({
55
+ const { getRootProps, getInputProps, open, fileRejections } = useDropzone({
40
56
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
57
  onDrop: (files) => handleUpload(files),
42
58
  disabled: uploadDisabled,
59
+ accept: validate?.allowedFileExtensions ? createAcceptObject(validate.allowedFileExtensions) : undefined,
60
+ maxSize: maxSizeInBytes,
43
61
  });
62
+ useEffect(() => {
63
+ const errors = [];
64
+ if (fileRejections.some((fileRejection) => fileRejection.errors.some((error) => error.code === 'file-invalid-type'))) {
65
+ errors.push(`Invalid file extension. Allowed extensions are: ${allowedTypesMessage}`);
66
+ }
67
+ if (fileRejections.some((fileRejection) => fileRejection.errors.some((error) => error.code === 'file-too-large'))) {
68
+ errors.push(`File size exceeds the max limit of ${formattedMaxSize}`);
69
+ }
70
+ onFileRejections(errors);
71
+ }, [fileRejections, onFileRejections]);
44
72
  return (React.createElement(React.Fragment, null,
45
73
  canUpdateProperty && hasUpdatePermission && (React.createElement(Box, { sx: {
46
74
  margin: '5px 0',
47
- height: '115px',
75
+ height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px',
48
76
  borderRadius: '8px',
49
77
  display: 'flex',
50
78
  justifyContent: 'center',
@@ -63,8 +91,20 @@ export const Document = (props) => {
63
91
  ' ',
64
92
  React.createElement(Typography, { component: 'span', sx: { color: uploadDisabled ? '#919EAB' : '#0075A7', fontSize: '14px' } }, "select file"),
65
93
  ' ',
66
- "to upload"))))),
67
- canUpdateProperty && isNil(hasUpdatePermission) && (React.createElement(Skeleton, { variant: "rectangular", height: "115px", sx: { margin: '5px 0', borderRadius: '8px' } })),
94
+ "to upload"),
95
+ allowedTypesMessage && (React.createElement(Typography, { sx: {
96
+ color: '#637381',
97
+ textAlign: 'center',
98
+ fontSize: '12px',
99
+ } }, `${allowedTypesMessage}.`)),
100
+ formattedMaxSize && (React.createElement(Typography, { sx: {
101
+ color: '#637381',
102
+ textAlign: 'center',
103
+ fontSize: '12px',
104
+ } }, validate?.maxDocuments === 1
105
+ ? `Max size of ${formattedMaxSize}.`
106
+ : `The max size of each document is ${formattedMaxSize}.`)))))),
107
+ canUpdateProperty && isNil(hasUpdatePermission) && (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })),
68
108
  React.createElement(DocumentList, { property: property, instance: instance, objectId: objectId, handleChange: handleChange, value: value, apiServices: apiServices, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission }),
69
109
  React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' })));
70
110
  };
@@ -16,6 +16,7 @@ export declare class DocumentComponent extends ReactComponent {
16
16
  */
17
17
  manageFormErrors(): void;
18
18
  handleChange: (key: string, value?: (File | SavedDocumentReference)[] | null) => void;
19
+ handleFileRejections(fileRejectionsErrors: string[]): void;
19
20
  handleValidation(value?: (File | SavedDocumentReference)[] | null): void;
20
21
  beforeSubmit(): Promise<void>;
21
22
  attachReact(element: Element): void;
@@ -26,6 +26,7 @@ export class DocumentComponent extends ReactComponent {
26
26
  };
27
27
  this.errorDetails = {};
28
28
  this.handleChange = this.handleChange.bind(this);
29
+ this.handleFileRejections = this.handleFileRejections.bind(this);
29
30
  }
30
31
  init() {
31
32
  this.on(`api-error`, (details) => {
@@ -87,6 +88,12 @@ export class DocumentComponent extends ReactComponent {
87
88
  }
88
89
  });
89
90
  }
91
+ handleFileRejections(fileRejectionsErrors) {
92
+ delete this.errorDetails['file-rejections'];
93
+ if (fileRejectionsErrors.length > 0) {
94
+ this.errorDetails['file-rejections'] = `${fileRejectionsErrors.join('; ')}.`;
95
+ }
96
+ }
90
97
  handleValidation(value) {
91
98
  const validate = this.component.validate;
92
99
  const amountOfDocuments = value?.length ?? 0;
@@ -131,6 +138,6 @@ export class DocumentComponent extends ReactComponent {
131
138
  const inputId = `${this.component.id}-input`;
132
139
  return ReactDOM.render(React.createElement("div", null,
133
140
  React.createElement(FormComponentWrapper, { ...this.component, inputId: inputId, viewOnly: !this.component.canUpdateProperty, errorMessage: this.errorMessages() },
134
- React.createElement(Document, { ...this.component, id: inputId, handleChange: this.handleChange, error: this.hasErrors(), value: this.dataValue }))), root);
141
+ React.createElement(Document, { ...this.component, id: inputId, handleChange: this.handleChange, error: this.hasErrors(), value: this.dataValue, onFileRejections: this.handleFileRejections }))), root);
135
142
  }
136
143
  }
@@ -233,6 +233,12 @@ export function convertFormToComponents(entries, parameters, object) {
233
233
  maxDocuments: parameter.type === 'document'
234
234
  ? parameter.validation?.maxDocuments
235
235
  : undefined,
236
+ allowedFileExtensions: parameter.type === 'document'
237
+ ? parameter.validation?.allowedFileExtensions
238
+ : undefined,
239
+ maxSizeInKB: parameter.type === 'document'
240
+ ? parameter.validation?.maxSizeInKB
241
+ : undefined,
236
242
  customMessage: ['integer', 'number', 'date', 'time', 'document'].includes(parameter.type ?? '')
237
243
  ? parameter.validation
238
244
  ?.errorMessage
@@ -1,11 +1,13 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
2
  import { isNil } from 'lodash';
3
+ import prettyBytes from 'pretty-bytes';
3
4
  import React, { useEffect, useState } from 'react';
4
5
  import { useDropzone } from 'react-dropzone';
5
- import { UploadCloud } from '../../../../../../icons';
6
+ import { InfoRounded, UploadCloud } from '../../../../../../icons';
6
7
  import { useFormContext } from '../../../../../../theme/hooks';
7
8
  import { Skeleton, Snackbar, Typography } from '../../../../../core';
8
9
  import { Box, Grid } from '../../../../../layout';
10
+ import { createAcceptObject } from '../../../../util';
9
11
  import { getPrefixedUrl } from '../../utils';
10
12
  import { DocumentList } from './DocumentList';
11
13
  export const Document = (props) => {
@@ -15,6 +17,20 @@ export const Document = (props) => {
15
17
  const [snackbarError, setSnackbarError] = useState();
16
18
  const [documents, setDocuments] = useState();
17
19
  const [hasUpdatePermission, setHasUpdatePermission] = useState(fetchedOptions[`${id}UpdatePermission`]);
20
+ let allowedTypesMessage = '';
21
+ if (validate?.allowedFileExtensions?.length) {
22
+ if (validate.allowedFileExtensions.length === 1) {
23
+ allowedTypesMessage = validate.allowedFileExtensions[0];
24
+ }
25
+ else {
26
+ allowedTypesMessage = `${validate.allowedFileExtensions.slice(0, -1).join(', ')} or ${validate.allowedFileExtensions.slice(-1)[0]}`;
27
+ }
28
+ }
29
+ const maxSizeInBytes = Number.isFinite(validate?.maxSizeInKB)
30
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
31
+ validate.maxSizeInKB * 1000 // convert to bytes
32
+ : undefined;
33
+ const formattedMaxSize = maxSizeInBytes !== undefined ? prettyBytes(maxSizeInBytes) : '';
18
34
  useEffect(() => {
19
35
  setDocuments(value);
20
36
  }, [value]);
@@ -39,15 +55,24 @@ export const Document = (props) => {
39
55
  handleChange(id, newDocuments);
40
56
  };
41
57
  const uploadDisabled = !!validate?.maxDocuments && (documents?.length ?? 0) >= validate.maxDocuments;
42
- const { getRootProps, getInputProps, open } = useDropzone({
58
+ const { getRootProps, getInputProps, open, fileRejections } = useDropzone({
43
59
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
60
  onDrop: (files) => handleUpload(files),
45
61
  disabled: uploadDisabled,
62
+ accept: validate?.allowedFileExtensions ? createAcceptObject(validate.allowedFileExtensions) : undefined,
63
+ maxSize: maxSizeInBytes,
46
64
  });
65
+ const errors = [];
66
+ if (fileRejections.some((fileRejection) => fileRejection.errors.some((error) => error.code === 'file-invalid-type'))) {
67
+ errors.push(`Invalid file extension. Allowed extensions are: ${allowedTypesMessage}`);
68
+ }
69
+ if (fileRejections.some((fileRejection) => fileRejection.errors.some((error) => error.code === 'file-too-large'))) {
70
+ errors.push(`File size exceeds the max limit of ${formattedMaxSize}`);
71
+ }
47
72
  return (React.createElement(React.Fragment, null,
48
73
  canUpdateProperty && hasUpdatePermission && (React.createElement(Box, { sx: {
49
74
  margin: '5px 0',
50
- height: '115px',
75
+ height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px',
51
76
  borderRadius: '8px',
52
77
  display: 'flex',
53
78
  justifyContent: 'center',
@@ -66,8 +91,23 @@ export const Document = (props) => {
66
91
  ' ',
67
92
  React.createElement(Typography, { component: 'span', color: uploadDisabled ? '#919EAB' : 'primary', sx: { fontSize: '14px' } }, "select file"),
68
93
  ' ',
69
- "to upload"))))),
70
- canUpdateProperty && isNil(hasUpdatePermission) && (React.createElement(Skeleton, { variant: "rectangular", height: "115px", sx: { margin: '5px 0', borderRadius: '8px' } })),
94
+ "to upload"),
95
+ allowedTypesMessage && (React.createElement(Typography, { sx: {
96
+ color: '#637381',
97
+ textAlign: 'center',
98
+ fontSize: '12px',
99
+ } }, `${allowedTypesMessage}.`)),
100
+ formattedMaxSize && (React.createElement(Typography, { sx: {
101
+ color: '#637381',
102
+ textAlign: 'center',
103
+ fontSize: '12px',
104
+ } }, validate?.maxDocuments === 1
105
+ ? `Max size of ${formattedMaxSize}.`
106
+ : `The max size of each document is ${formattedMaxSize}.`)))))),
107
+ canUpdateProperty && isNil(hasUpdatePermission) && (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })),
71
108
  React.createElement(DocumentList, { id: id, instance: instance, handleChange: handleChange, value: value, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission }),
72
- React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' })));
109
+ React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
110
+ errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
111
+ React.createElement(InfoRounded, { color: 'error', sx: { fontSize: '.75rem', marginRight: '3px' } }),
112
+ React.createElement(Typography, { fontSize: '12px', color: 'error', sx: { lineHeight: '18px' } }, errors.join('; ') + '.')))));
73
113
  };
@@ -130,7 +130,7 @@ export function RecursiveEntryRenderer(props) {
130
130
  }
131
131
  else if (fieldDefinition.type === 'document') {
132
132
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
133
- React.createElement(Document, { id: entryId, handleChange: handleChange, error: !!errors[entryId], value: fieldValue, instance: instance, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description })));
133
+ React.createElement(Document, { id: entryId, handleChange: handleChange, error: !!errors[entryId], value: fieldValue, instance: instance, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
134
134
  }
135
135
  else if (fieldDefinition.type === 'criteria') {
136
136
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
@@ -1 +1,2 @@
1
1
  export declare function difference(object?: any, base?: any): any;
2
+ export declare const createAcceptObject: (allowedTypes: string[]) => Record<string, string[]> | undefined;
@@ -1,4 +1,5 @@
1
1
  import { isEqual, isObject, transform } from 'lodash';
2
+ import mime from 'mime-types';
2
3
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
4
  export function difference(object, base) {
4
5
  if (!object) {
@@ -14,3 +15,30 @@ export function difference(object, base) {
14
15
  }
15
16
  });
16
17
  }
18
+ // Helper function to convert file extensions array to dropzone accept format
19
+ export const createAcceptObject = (allowedTypes) => {
20
+ if (!allowedTypes.length)
21
+ return undefined;
22
+ const acceptObject = {};
23
+ const customExtensions = [];
24
+ // First pass: collect custom extensions from allowedTypes that don't map to standard MIME types
25
+ allowedTypes.forEach((extension) => {
26
+ const mimeType = mime.lookup(extension);
27
+ // It's a custom extension
28
+ if (!mimeType) {
29
+ customExtensions.push(extension.startsWith('.') ? extension : `.${extension}`);
30
+ }
31
+ else {
32
+ // If it is mapped to a value, add it to the accept object
33
+ if (!acceptObject[mimeType]) {
34
+ acceptObject[mimeType] = [];
35
+ }
36
+ acceptObject[mimeType].push(extension.startsWith('.') ? extension : `.${extension}`);
37
+ }
38
+ });
39
+ if (customExtensions.length) {
40
+ // Add custom files extensions to the accept object
41
+ acceptObject['file/custom'] = customExtensions;
42
+ }
43
+ return acceptObject;
44
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.6.1-dev.0",
3
+ "version": "1.6.1-testing.0",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -56,6 +56,7 @@
56
56
  "@types/jest": "^28.1.4",
57
57
  "@types/json-logic-js": "^2.0.8",
58
58
  "@types/luxon": "^3.4.2",
59
+ "@types/mime-types": "^3.0.1",
59
60
  "@types/nanoid-dictionary": "^4.2.3",
60
61
  "@types/node": "^18.0.0",
61
62
  "@types/react": "^18.0.17",
@@ -87,7 +88,7 @@
87
88
  "yalc": "^1.0.0-pre.53"
88
89
  },
89
90
  "peerDependencies": {
90
- "@evoke-platform/context": "^1.3.0-dev.6",
91
+ "@evoke-platform/context": "^1.3.0-testing.9",
91
92
  "react": "^18.1.0",
92
93
  "react-dom": "^18.1.0"
93
94
  },
@@ -119,6 +120,7 @@
119
120
  "formiojs": "^4.15.0-rc.23",
120
121
  "html-react-parser": "^5.1.18",
121
122
  "luxon": "^2.5.2",
123
+ "mime-type": "^5.0.3",
122
124
  "msw": "^2.7.3",
123
125
  "nanoid": "^5.0.8",
124
126
  "nanoid-dictionary": "^4.3.0",