@evoke-platform/ui-components 1.15.1 → 1.16.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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +238 -141
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
- package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/Form/utils.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +2 -2
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +4 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +85 -33
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +43 -16
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +9 -5
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -8
- package/dist/published/components/custom/FormV2/components/utils.js +165 -79
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +6 -1
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
- package/dist/published/stories/FormRenderer.stories.d.ts +6 -3
- package/dist/published/stories/FormRendererContainer.stories.d.ts +20 -0
- package/dist/published/theme/hooks.d.ts +1 -0
- package/package.json +2 -1
package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useApiServices } from '@evoke-platform/context';
|
|
1
|
+
import { useApiServices, } from '@evoke-platform/context';
|
|
2
2
|
import { useQuery } from '@tanstack/react-query';
|
|
3
3
|
import prettyBytes from 'pretty-bytes';
|
|
4
4
|
import React, { useEffect, useState } from 'react';
|
|
@@ -7,12 +7,12 @@ import { InfoRounded, UploadCloud } from '../../../../../../icons';
|
|
|
7
7
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
8
8
|
import { Skeleton, Snackbar, Typography } from '../../../../../core';
|
|
9
9
|
import { Box, Grid } from '../../../../../layout';
|
|
10
|
-
import { getPrefixedUrl } from '../../utils';
|
|
10
|
+
import { getEntryId, getPrefixedUrl, getUnnestedEntries, uploadFiles } from '../../utils';
|
|
11
11
|
import { DocumentList } from './DocumentList';
|
|
12
12
|
export const Document = (props) => {
|
|
13
|
-
const { id, canUpdateProperty, error, value, validate, hasDescription } = props;
|
|
13
|
+
const { id, fieldType = 'document', canUpdateProperty, error, value, validate, hasDescription } = props;
|
|
14
14
|
const apiServices = useApiServices();
|
|
15
|
-
const { object, handleChange, onAutosave: onAutosave, instance } = useFormContext();
|
|
15
|
+
const { object, handleChange, onAutosave: onAutosave, instance, form } = useFormContext();
|
|
16
16
|
const [snackbarError, setSnackbarError] = useState();
|
|
17
17
|
const [documents, setDocuments] = useState();
|
|
18
18
|
let allowedTypesMessage = '';
|
|
@@ -32,11 +32,22 @@ export const Document = (props) => {
|
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
setDocuments(value);
|
|
34
34
|
}, [value]);
|
|
35
|
+
// Find the entry to get the configured createActionId and fileObjectId
|
|
36
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
|
|
37
|
+
const entry = allEntries?.find((entry) => getEntryId(entry) === id);
|
|
38
|
+
const createActionId = entry?.display?.createActionId ?? '_create';
|
|
39
|
+
// Get the configured objectId from display.fileObjectId, defaulting to sys__file
|
|
40
|
+
const fileObjectId = entry?.display?.fileObjectId ?? 'sys__file';
|
|
41
|
+
// For 'file' type properties, check regular object instance permissions
|
|
42
|
+
// For 'document' type properties, check document attachment permissions
|
|
43
|
+
const endpoint = fieldType === 'file'
|
|
44
|
+
? getPrefixedUrl(`/objects/${fileObjectId}/instances/checkAccess?action=execute&field=${createActionId}`)
|
|
45
|
+
: getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=update`);
|
|
35
46
|
const { data: hasUpdatePermission = false, isLoading } = useQuery({
|
|
36
|
-
queryKey: ['
|
|
47
|
+
queryKey: ['hasUpdatePermission', object?.id, instance?.id, fieldType, id],
|
|
37
48
|
queryFn: async () => {
|
|
38
49
|
try {
|
|
39
|
-
const accessCheck = await apiServices.get(
|
|
50
|
+
const accessCheck = await apiServices.get(endpoint);
|
|
40
51
|
return accessCheck.result;
|
|
41
52
|
}
|
|
42
53
|
catch {
|
|
@@ -44,14 +55,36 @@ export const Document = (props) => {
|
|
|
44
55
|
}
|
|
45
56
|
},
|
|
46
57
|
staleTime: Infinity,
|
|
47
|
-
|
|
58
|
+
// For 'file' type fields the permission endpoint only requires the object ID, so the
|
|
59
|
+
// query can run on create actions where no instance exists yet. For 'document' type
|
|
60
|
+
// fields the endpoint is instance-scoped, so instance ID is still required.
|
|
61
|
+
enabled: canUpdateProperty && !!object?.id && (fieldType === 'file' || !!instance?.id),
|
|
48
62
|
});
|
|
49
63
|
const handleUpload = async (files) => {
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
if (!files?.length) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let uploadedFiles = files;
|
|
68
|
+
// Get the createActionId and fileObjectId from form entry, default to '_create' and 'sys__file'
|
|
69
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []);
|
|
70
|
+
const entry = allEntries?.find((entry) => getEntryId(entry) === id);
|
|
71
|
+
const createActionId = entry?.display?.createActionId ?? '_create';
|
|
72
|
+
const fileObjectId = entry?.display?.fileObjectId ?? 'sys__file';
|
|
73
|
+
// Immediately upload files for 'file' type properties when autosave is not enabled.
|
|
74
|
+
// Linking will happen upon final submission.
|
|
75
|
+
// If autosave is enabled, upload and linking will happen in the autosave handler.
|
|
76
|
+
if (fieldType === 'file' && !onAutosave) {
|
|
77
|
+
const { successfulUploads, errorMessage } = await uploadFiles(files, apiServices, createActionId, fileObjectId, undefined, false);
|
|
78
|
+
uploadedFiles = successfulUploads;
|
|
79
|
+
if (errorMessage) {
|
|
80
|
+
setSnackbarError({ message: errorMessage, type: 'error' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Store uploaded file references (or File objects) in form state
|
|
84
|
+
const newDocuments = [...(documents ?? []), ...uploadedFiles];
|
|
52
85
|
setDocuments(newDocuments);
|
|
53
86
|
try {
|
|
54
|
-
|
|
87
|
+
await handleChange?.(id, newDocuments);
|
|
55
88
|
}
|
|
56
89
|
catch (error) {
|
|
57
90
|
console.error('Failed to update field:', error);
|
|
@@ -119,7 +152,7 @@ export const Document = (props) => {
|
|
|
119
152
|
} }, validate?.maxDocuments === 1
|
|
120
153
|
? `Maximum size is ${formattedMaxSize}.`
|
|
121
154
|
: `The maximum size of each document is ${formattedMaxSize}.`)))))),
|
|
122
|
-
canUpdateProperty && isLoading ? (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })) : (React.createElement(DocumentList, { id: id, handleChange: handleChange, onAutosave: onAutosave, value: documents, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission })),
|
|
155
|
+
canUpdateProperty && isLoading ? (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })) : (React.createElement(DocumentList, { id: id, fieldType: fieldType, handleChange: handleChange, onAutosave: onAutosave, value: documents, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission })),
|
|
123
156
|
React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
|
|
124
157
|
errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
|
|
125
158
|
React.createElement(InfoRounded, { sx: { fontSize: '.75rem', marginRight: '3px', color: '#D3271B' } }),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { DocumentReference } from '../../types';
|
|
3
3
|
type DocumentListProps = {
|
|
4
|
-
handleChange?: (propertyId: string, value: (File |
|
|
4
|
+
handleChange?: (propertyId: string, value: (File | DocumentReference)[] | undefined) => void;
|
|
5
5
|
onAutosave?: (fieldId: string) => void | Promise<void>;
|
|
6
6
|
id: string;
|
|
7
|
+
fieldType?: 'document' | 'file';
|
|
7
8
|
canUpdateProperty: boolean;
|
|
8
|
-
value: (File |
|
|
9
|
+
value: (File | DocumentReference)[] | undefined;
|
|
9
10
|
setSnackbarError: (type: 'error' | 'success', message: string) => void;
|
|
10
11
|
};
|
|
11
12
|
export declare const DocumentList: (props: DocumentListProps) => React.JSX.Element;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
2
|
import { useQuery } from '@tanstack/react-query';
|
|
3
|
-
import { isEqual } from 'lodash';
|
|
4
3
|
import prettyBytes from 'pretty-bytes';
|
|
5
4
|
import React, { useEffect, useState } from 'react';
|
|
6
5
|
import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
|
|
@@ -25,33 +24,44 @@ const viewableFileTypes = [
|
|
|
25
24
|
'text/plain',
|
|
26
25
|
];
|
|
27
26
|
export const DocumentList = (props) => {
|
|
28
|
-
const { handleChange, onAutosave, id, canUpdateProperty, value: documents, setSnackbarError } = props;
|
|
27
|
+
const { handleChange, onAutosave, id, fieldType = 'document', canUpdateProperty, value: documents, setSnackbarError, } = props;
|
|
29
28
|
const apiServices = useApiServices();
|
|
30
29
|
const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
|
|
31
30
|
// Determine property type once at component level
|
|
32
|
-
const
|
|
33
|
-
const isFileType = propertyType === 'file';
|
|
31
|
+
const isFileType = fieldType === 'file';
|
|
34
32
|
// savedDocuments is either FileInstance[] or DocumentType[], never a mix
|
|
35
33
|
const [savedDocuments, setSavedDocuments] = useState(fetchedOptions[`${id}SavedDocuments`]);
|
|
34
|
+
const [deletedDocumentIds, setDeletedDocumentIds] = useState([]);
|
|
35
|
+
// The stable list we render — only updated once savedDocuments has metadata for
|
|
36
|
+
// every referenced ID. While waiting for metadata, the previous list is kept so
|
|
37
|
+
// the UI doesn't flash empty sizes, missing icons, or "Deleted" chips.
|
|
38
|
+
const [displayDocuments, setDisplayDocuments] = useState(documents);
|
|
36
39
|
useEffect(() => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
const currentDocumentIds = (documents ?? [])
|
|
41
|
+
.filter((doc) => !(doc instanceof File))
|
|
42
|
+
.map((doc) => doc.id);
|
|
43
|
+
if (!currentDocumentIds.length) {
|
|
44
|
+
// Only File objects — metadata not needed, display immediately.
|
|
45
|
+
setDisplayDocuments(documents);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const savedDocumentIds = savedDocuments?.map((doc) => doc.id) ?? [];
|
|
49
|
+
const missingDocumentIds = currentDocumentIds.filter((docId) => !savedDocumentIds.includes(docId) && !deletedDocumentIds.includes(docId));
|
|
50
|
+
if (missingDocumentIds.length) {
|
|
51
|
+
// Metadata not yet available — fetch it.
|
|
52
|
+
// If we already have a stable display, keep it to avoid flashing.
|
|
53
|
+
// If there's nothing to show yet (initial load), display immediately so
|
|
54
|
+
// the file names are visible while metadata loads.
|
|
55
|
+
if (!displayDocuments?.length) {
|
|
56
|
+
setDisplayDocuments(documents);
|
|
47
57
|
}
|
|
58
|
+
getDocuments(currentDocumentIds);
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
setSavedDocuments(fetchedOptions[`${id}SavedDocuments`]);
|
|
60
|
+
else {
|
|
61
|
+
// All IDs have metadata — safe to update the display.
|
|
62
|
+
setDisplayDocuments(documents);
|
|
53
63
|
}
|
|
54
|
-
}, [
|
|
64
|
+
}, [documents, savedDocuments, fieldType, apiServices]);
|
|
55
65
|
const getDocuments = (currentDocumentIds, shouldRetry = true) => {
|
|
56
66
|
// For 'file' type properties, fetch sys__file instances directly
|
|
57
67
|
// For 'document' type properties, fetch attachment documents
|
|
@@ -64,7 +74,7 @@ export const DocumentList = (props) => {
|
|
|
64
74
|
// There is a short delay between when a document is uploaded and when
|
|
65
75
|
// it is indexed. Therefore, try again if documents are not found.
|
|
66
76
|
if (shouldRetry &&
|
|
67
|
-
(!docs ||
|
|
77
|
+
(!docs?.length ||
|
|
68
78
|
currentDocumentIds.some((docId) => !docs.find((doc) => docId === doc.id)))) {
|
|
69
79
|
setTimeout(() => getDocuments(currentDocumentIds, false), 2000);
|
|
70
80
|
}
|
|
@@ -72,6 +82,9 @@ export const DocumentList = (props) => {
|
|
|
72
82
|
setSnackbarError('error', 'Error occurred while retrieving saved documents');
|
|
73
83
|
}
|
|
74
84
|
else {
|
|
85
|
+
// IDs that were requested but not returned by the server = deleted/not found.
|
|
86
|
+
const deletedIds = currentDocumentIds.filter((docId) => !docs?.find((doc) => doc.id === docId));
|
|
87
|
+
setDeletedDocumentIds(deletedIds);
|
|
75
88
|
setSavedDocuments(docs);
|
|
76
89
|
setFetchedOptions({
|
|
77
90
|
[`${id}SavedDocuments`]: docs,
|
|
@@ -97,12 +110,13 @@ export const DocumentList = (props) => {
|
|
|
97
110
|
staleTime: Infinity,
|
|
98
111
|
});
|
|
99
112
|
const isFile = (doc) => doc instanceof File;
|
|
113
|
+
const isUnsavedFile = (doc) => isFile(doc) || !!doc.unsaved;
|
|
100
114
|
const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
|
|
101
115
|
const handleRemove = async (index) => {
|
|
102
116
|
const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
|
|
103
117
|
const newValue = updatedDocuments.length === 0 ? undefined : updatedDocuments;
|
|
104
118
|
try {
|
|
105
|
-
handleChange
|
|
119
|
+
handleChange?.(id, newValue);
|
|
106
120
|
}
|
|
107
121
|
catch (error) {
|
|
108
122
|
console.error('Failed to update field:', error);
|
|
@@ -116,7 +130,7 @@ export const DocumentList = (props) => {
|
|
|
116
130
|
}
|
|
117
131
|
};
|
|
118
132
|
const openDocument = async (index) => {
|
|
119
|
-
const doc =
|
|
133
|
+
const doc = displayDocuments?.[index];
|
|
120
134
|
if (doc) {
|
|
121
135
|
let url;
|
|
122
136
|
const contentType = doc instanceof File
|
|
@@ -124,9 +138,7 @@ export const DocumentList = (props) => {
|
|
|
124
138
|
: savedDocuments?.find((savedDocument) => savedDocument.id === doc.id)?.contentType;
|
|
125
139
|
if (!isFile(doc)) {
|
|
126
140
|
try {
|
|
127
|
-
|
|
128
|
-
const propertyType = object?.properties?.find((p) => p.id === id)?.type;
|
|
129
|
-
const contentEndpoint = propertyType === 'file'
|
|
141
|
+
const contentEndpoint = isFileType
|
|
130
142
|
? getPrefixedUrl(`/files/${doc.id}/content`)
|
|
131
143
|
: getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}/content`);
|
|
132
144
|
const documentResponse = await apiServices.get(contentEndpoint, { responseType: 'blob' });
|
|
@@ -167,8 +179,8 @@ export const DocumentList = (props) => {
|
|
|
167
179
|
return size;
|
|
168
180
|
};
|
|
169
181
|
return (React.createElement(React.Fragment, null,
|
|
170
|
-
!
|
|
171
|
-
!!
|
|
182
|
+
!displayDocuments && !canUpdateProperty && (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No documents")),
|
|
183
|
+
!!displayDocuments?.length && (React.createElement(Box, null, displayDocuments.map((doc, index) => (React.createElement(Grid, { container: true, sx: {
|
|
172
184
|
width: '100%',
|
|
173
185
|
border: '1px solid #C4CDD5',
|
|
174
186
|
borderRadius: '6px',
|
|
@@ -189,10 +201,10 @@ export const DocumentList = (props) => {
|
|
|
189
201
|
} }, doc.name)),
|
|
190
202
|
React.createElement(Grid, { item: true, xs: 12 },
|
|
191
203
|
React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, getDocumentSize(doc)))),
|
|
192
|
-
(
|
|
204
|
+
(isUnsavedFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
|
|
193
205
|
React.createElement(IconButton, { "aria-label": "open document", sx: { ...styles.icon, marginRight: '16px' }, onClick: () => openDocument(index) },
|
|
194
206
|
React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))),
|
|
195
|
-
!isFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
|
|
207
|
+
!isFile(doc) && !isUnsavedFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
|
|
196
208
|
marginRight: '16px',
|
|
197
209
|
backgroundColor: 'rgba(222, 48, 36, 0.16)',
|
|
198
210
|
color: '#A91813',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DocumentParameterValidation } from '@evoke-platform/context';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
type FileContentProps = {
|
|
4
|
+
id: string;
|
|
5
|
+
canUpdateProperty: boolean;
|
|
6
|
+
error: boolean;
|
|
7
|
+
validate?: DocumentParameterValidation;
|
|
8
|
+
value: File | undefined;
|
|
9
|
+
hasDescription?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const FileContent: (props: FileContentProps) => React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useApiServices } from '@evoke-platform/context';
|
|
2
|
+
import prettyBytes from 'pretty-bytes';
|
|
3
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { useDropzone } from 'react-dropzone';
|
|
5
|
+
import { FileWithExtension, InfoRounded, LaunchRounded, TrashCan, UploadCloud } from '../../../../../icons';
|
|
6
|
+
import { useFormContext } from '../../../../../theme/hooks';
|
|
7
|
+
import { IconButton, Snackbar, Typography } from '../../../../core';
|
|
8
|
+
import { Box, Grid } from '../../../../layout';
|
|
9
|
+
import { getPrefixedUrl } from '../utils';
|
|
10
|
+
import mime from 'mime';
|
|
11
|
+
export const FileContent = (props) => {
|
|
12
|
+
const { id, canUpdateProperty, error, validate, hasDescription, value } = props;
|
|
13
|
+
const { handleChange, instance } = useFormContext();
|
|
14
|
+
const apiServices = useApiServices();
|
|
15
|
+
const [snackbarError, setSnackbarError] = useState();
|
|
16
|
+
const [file, setFile] = useState(value);
|
|
17
|
+
const [hasReadAccess, setHasReadAccess] = useState(false);
|
|
18
|
+
const maxSizeInBytes = Number.isFinite(validate?.maxSizeInKB)
|
|
19
|
+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
20
|
+
validate.maxSizeInKB * 1000 // convert to bytes
|
|
21
|
+
: undefined;
|
|
22
|
+
const formattedMaxSize = maxSizeInBytes !== undefined ? prettyBytes(maxSizeInBytes) : '';
|
|
23
|
+
const { getRootProps, getInputProps, open, fileRejections } = useDropzone({
|
|
24
|
+
onDrop: (files) => handleUpload(files),
|
|
25
|
+
disabled: !!file,
|
|
26
|
+
accept: validate?.allowedFileExtensions
|
|
27
|
+
? {
|
|
28
|
+
'type/custom': validate.allowedFileExtensions.map((extension) => extension.startsWith('.') ? extension : `.${extension}`),
|
|
29
|
+
}
|
|
30
|
+
: undefined,
|
|
31
|
+
maxSize: maxSizeInBytes,
|
|
32
|
+
});
|
|
33
|
+
let allowedTypesMessage = '';
|
|
34
|
+
if (validate?.allowedFileExtensions?.length) {
|
|
35
|
+
if (validate.allowedFileExtensions.length === 1) {
|
|
36
|
+
allowedTypesMessage = `Only ${validate.allowedFileExtensions[0]} files are allowed`;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
allowedTypesMessage = `Allowed file types are: ${validate.allowedFileExtensions.slice(0, -1).join(', ')} or ${validate.allowedFileExtensions.slice(-1)[0]}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setFile(value);
|
|
44
|
+
}, [value]);
|
|
45
|
+
const checkPermissions = useCallback(async () => {
|
|
46
|
+
if (instance) {
|
|
47
|
+
try {
|
|
48
|
+
const response = await apiServices.get(getPrefixedUrl(`/objects/${instance.objectId}/instances/${instance.id}/checkAccess`), {
|
|
49
|
+
params: {
|
|
50
|
+
action: 'read',
|
|
51
|
+
field: 'content',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
setHasReadAccess(response.result);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
setHasReadAccess(false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
setHasReadAccess(true);
|
|
62
|
+
}
|
|
63
|
+
}, [apiServices, instance]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
checkPermissions();
|
|
66
|
+
}, [checkPermissions]);
|
|
67
|
+
const handleUpload = async (files) => {
|
|
68
|
+
const file = files ? files[0] : undefined;
|
|
69
|
+
if (file)
|
|
70
|
+
setFile(file);
|
|
71
|
+
try {
|
|
72
|
+
handleChange && (await handleChange(id, file));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error('Failed to update field:', error);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const viewFileContent = async () => {
|
|
80
|
+
const contentType = instance ? instance.contentType : file?.type;
|
|
81
|
+
let url = '';
|
|
82
|
+
if (file && file.size > 0) {
|
|
83
|
+
url = URL.createObjectURL(file);
|
|
84
|
+
}
|
|
85
|
+
else if (instance) {
|
|
86
|
+
try {
|
|
87
|
+
const contentResponse = await apiServices.get(getPrefixedUrl(`/files/${instance.id}/content`), { responseType: 'blob' });
|
|
88
|
+
const blob = new Blob([contentResponse], { type: contentType });
|
|
89
|
+
url = window.URL.createObjectURL(blob);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
setSnackbarError({
|
|
93
|
+
message: `Viewing the contents of ${instance.name} is currently not available.`,
|
|
94
|
+
type: 'error',
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (contentType &&
|
|
100
|
+
[
|
|
101
|
+
'application/pdf',
|
|
102
|
+
'image/jpeg',
|
|
103
|
+
'image/jpg',
|
|
104
|
+
'image/png',
|
|
105
|
+
'image/gif',
|
|
106
|
+
'image/bmp',
|
|
107
|
+
'image/webp',
|
|
108
|
+
'text/plain',
|
|
109
|
+
].includes(contentType)) {
|
|
110
|
+
window.open(url, '_blank');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const link = document.createElement('a');
|
|
114
|
+
link.href = url;
|
|
115
|
+
link.setAttribute('download', file?.name ?? '');
|
|
116
|
+
document.body.appendChild(link);
|
|
117
|
+
link.click();
|
|
118
|
+
// Clean up and remove the link
|
|
119
|
+
link.parentNode?.removeChild(link);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const errors = [];
|
|
123
|
+
if (fileRejections.some((fileRejection) => fileRejection.errors.some((error) => error.code === 'file-invalid-type'))) {
|
|
124
|
+
errors.push(`Invalid file extension. ${allowedTypesMessage}`);
|
|
125
|
+
}
|
|
126
|
+
if (fileRejections.some((fileRejection) => fileRejection.errors.some((error) => error.code === 'file-too-large'))) {
|
|
127
|
+
errors.push(`File size exceeds the maximum limit of ${formattedMaxSize}`);
|
|
128
|
+
}
|
|
129
|
+
return (React.createElement(React.Fragment, null,
|
|
130
|
+
file ? (React.createElement(Grid, { container: true, sx: {
|
|
131
|
+
width: '100%',
|
|
132
|
+
border: '1px solid #C4CDD5',
|
|
133
|
+
borderRadius: '6px',
|
|
134
|
+
margin: '5px 2px',
|
|
135
|
+
padding: ' 8px',
|
|
136
|
+
display: 'flex',
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
} },
|
|
139
|
+
React.createElement(Grid, { item: true, sx: { display: 'flex', justifyContent: 'center', padding: '7px', marginLeft: '4px' } },
|
|
140
|
+
React.createElement(FileWithExtension, { fontFamily: "Arial", fileExtension: mime.getExtension(file.type && file.type !== '' ? file.type : instance?.['contentType']) ?? undefined, sx: { height: '1.5em', width: '1.5em' } })),
|
|
141
|
+
React.createElement(Grid, { item: true, sx: { flex: 1, justifyContent: 'center', paddingBottom: '5px' } },
|
|
142
|
+
React.createElement(Grid, { item: true, xs: 12 },
|
|
143
|
+
React.createElement(Typography, { sx: {
|
|
144
|
+
fontSize: '14px',
|
|
145
|
+
fontWeight: 700,
|
|
146
|
+
lineHeight: '15px',
|
|
147
|
+
paddingTop: '8px',
|
|
148
|
+
} }, file.name)),
|
|
149
|
+
file.size ? (React.createElement(Grid, { item: true, xs: 12 },
|
|
150
|
+
React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, prettyBytes(file.size)))) : null),
|
|
151
|
+
canUpdateProperty && (React.createElement(Grid, { item: true },
|
|
152
|
+
React.createElement(IconButton, { "aria-label": "Delete File", sx: { padding: '3px', color: '#637381', marginRight: '10px' }, onClick: () => {
|
|
153
|
+
setFile(undefined);
|
|
154
|
+
handleChange && handleChange(id, undefined);
|
|
155
|
+
} },
|
|
156
|
+
React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } })))),
|
|
157
|
+
hasReadAccess && (React.createElement(Grid, { item: true },
|
|
158
|
+
React.createElement(IconButton, { "aria-label": "View File Content", sx: { padding: '3px', color: '#637381', marginRight: '16px' }, onClick: () => viewFileContent() },
|
|
159
|
+
React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))))) : canUpdateProperty ? (React.createElement(Box, { sx: {
|
|
160
|
+
margin: '5px 0',
|
|
161
|
+
height: formattedMaxSize || allowedTypesMessage ? '115px' : '90px',
|
|
162
|
+
borderRadius: '8px',
|
|
163
|
+
display: 'flex',
|
|
164
|
+
justifyContent: 'center',
|
|
165
|
+
alignItems: 'center',
|
|
166
|
+
border: `1px dashed ${error ? 'red' : '#858585'}`,
|
|
167
|
+
position: 'relative',
|
|
168
|
+
cursor: 'pointer',
|
|
169
|
+
}, ...getRootProps(), onClick: open },
|
|
170
|
+
React.createElement("input", { ...getInputProps({ id }), ...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined) }),
|
|
171
|
+
React.createElement(Grid, { container: true, sx: { width: '100%' } },
|
|
172
|
+
React.createElement(Grid, { item: true, xs: 12, sx: { display: 'flex', justifyContent: 'center', paddingBottom: '7px' } },
|
|
173
|
+
React.createElement(UploadCloud, { sx: { color: '#919EAB', width: '50px', height: '30px' } })),
|
|
174
|
+
React.createElement(Grid, { item: true, xs: 12 },
|
|
175
|
+
React.createElement(Typography, { variant: "body2", sx: { color: '#212B36', textAlign: 'center' } },
|
|
176
|
+
"Drag and drop or",
|
|
177
|
+
' ',
|
|
178
|
+
React.createElement(Typography, { component: 'span', color: 'primary', sx: { fontSize: '14px' } }, "select file"),
|
|
179
|
+
' ',
|
|
180
|
+
"to upload"),
|
|
181
|
+
allowedTypesMessage && (React.createElement(Typography, { sx: {
|
|
182
|
+
color: '#637381',
|
|
183
|
+
textAlign: 'center',
|
|
184
|
+
fontSize: '12px',
|
|
185
|
+
} },
|
|
186
|
+
allowedTypesMessage,
|
|
187
|
+
".")),
|
|
188
|
+
formattedMaxSize && (React.createElement(Typography, { sx: {
|
|
189
|
+
color: '#637381',
|
|
190
|
+
textAlign: 'center',
|
|
191
|
+
fontSize: '12px',
|
|
192
|
+
} }, `Maximum size is ${formattedMaxSize}.`)))))) : (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No file uploaded.")),
|
|
193
|
+
React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
|
|
194
|
+
errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
|
|
195
|
+
React.createElement(InfoRounded, { sx: { fontSize: '.75rem', marginRight: '3px', color: '#D3271B' } }),
|
|
196
|
+
React.createElement(Typography, { fontSize: '12px', color: '#D3271B', sx: { lineHeight: '18px' } }, errors.join('; ') + '.')))));
|
|
197
|
+
};
|
|
@@ -102,6 +102,20 @@ const InstanceLookup = (props) => {
|
|
|
102
102
|
},
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
|
+
else if (property.type === 'fileContent') {
|
|
106
|
+
columns.push({
|
|
107
|
+
field: prop.id,
|
|
108
|
+
headerName: property.name,
|
|
109
|
+
type: 'string',
|
|
110
|
+
flex: 1,
|
|
111
|
+
valueGetter: (params) => {
|
|
112
|
+
const row = params.row;
|
|
113
|
+
// Currently the `fileContent` property type is only supported on the `sys__file` system object.
|
|
114
|
+
// Display the 'name' property of the `sys__file` instance.
|
|
115
|
+
return row['name'];
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
105
119
|
else if (property.type === 'date') {
|
|
106
120
|
columns.push({
|
|
107
121
|
field: prop.id,
|
|
@@ -13,6 +13,7 @@ import DropdownRepeatableField from './FormFieldTypes/CollectionFiles/DropdownRe
|
|
|
13
13
|
import RepeatableField from './FormFieldTypes/CollectionFiles/RepeatableField';
|
|
14
14
|
import Criteria from './FormFieldTypes/Criteria';
|
|
15
15
|
import { Document } from './FormFieldTypes/DocumentFiles/Document';
|
|
16
|
+
import { FileContent } from './FormFieldTypes/FileContent';
|
|
16
17
|
import { Image } from './FormFieldTypes/Image';
|
|
17
18
|
import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
|
|
18
19
|
import UserProperty from './FormFieldTypes/UserProperty';
|
|
@@ -87,10 +88,9 @@ export function RecursiveEntryRenderer(props) {
|
|
|
87
88
|
},
|
|
88
89
|
});
|
|
89
90
|
const memorizedCriteria = useMemo(() => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}, [validation, getValues && getValues(), userAccount, instance]);
|
|
91
|
+
const criteria = 'criteria' in validation && validation.criteria ? validation.criteria : display?.criteria;
|
|
92
|
+
return getValues && criteria ? updateCriteriaInputs(criteria, getValues(), userAccount, instance) : undefined;
|
|
93
|
+
}, [validation, getValues && getValues(), userAccount, instance, display]);
|
|
94
94
|
const memorizedDefaultValueCriteria = useMemo(() => {
|
|
95
95
|
return display?.defaultValue &&
|
|
96
96
|
typeof display.defaultValue === 'object' &&
|
|
@@ -166,7 +166,11 @@ export function RecursiveEntryRenderer(props) {
|
|
|
166
166
|
}
|
|
167
167
|
else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
|
|
168
168
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
169
|
-
React.createElement(Document, { id: entryId, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
|
|
169
|
+
React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
|
|
170
|
+
}
|
|
171
|
+
else if (fieldDefinition.type === 'fileContent') {
|
|
172
|
+
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
173
|
+
React.createElement(FileContent, { id: entryId, error: !!errors?.[entryId], value: fieldValue, hasDescription: !!display?.description, canUpdateProperty: !(entry.type === 'readonlyField'), validate: validation })));
|
|
170
174
|
}
|
|
171
175
|
else if (fieldDefinition.type === 'criteria') {
|
|
172
176
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -12,9 +12,10 @@ export type FieldAddress = {
|
|
|
12
12
|
export type AccessCheck = {
|
|
13
13
|
result: boolean;
|
|
14
14
|
};
|
|
15
|
-
export type
|
|
15
|
+
export type DocumentReference = {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
18
|
+
unsaved?: boolean;
|
|
18
19
|
};
|
|
19
20
|
export type Document = {
|
|
20
21
|
id: string;
|
|
@@ -112,3 +113,7 @@ export type InstanceLink = {
|
|
|
112
113
|
id: string;
|
|
113
114
|
objectId: string;
|
|
114
115
|
};
|
|
116
|
+
export type FileUploadBatchResult = {
|
|
117
|
+
errorMessage?: string;
|
|
118
|
+
successfulUploads: DocumentReference[];
|
|
119
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount
|
|
2
|
+
import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, ObjWithRoot, PanelViewEntry, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
|
|
3
3
|
import { LocalDateTime } from '@js-joda/core';
|
|
4
4
|
import { FieldErrors, FieldValues } from 'react-hook-form';
|
|
5
5
|
import { ObjectProperty } from '../../../../types';
|
|
6
6
|
import { AutocompleteOption } from '../../../core';
|
|
7
|
-
import {
|
|
7
|
+
import { DocumentReference, FileUploadBatchResult, InstanceLink } from './types';
|
|
8
8
|
export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
|
|
9
9
|
export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
|
|
10
10
|
export declare function isAddressProperty(key: string): boolean;
|
|
@@ -50,16 +50,13 @@ export declare const convertPropertiesToParams: (object: Obj) => InputParameter[
|
|
|
50
50
|
export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
|
|
51
51
|
export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
|
|
52
52
|
export declare const docProperties: Property[];
|
|
53
|
-
|
|
54
|
-
* Upload files using the POST /files endpoint for sys__file objects
|
|
55
|
-
*/
|
|
56
|
-
export declare const uploadFiles: (files: (File | SavedDocumentReference)[], apiServices: ApiServices, actionId?: string, metadata?: Record<string, string>, linkTo?: InstanceLink) => Promise<SavedDocumentReference[]>;
|
|
53
|
+
export declare const uploadFiles: (files: (File | DocumentReference)[], apiServices: ApiServices, actionId?: string, fileObjectId?: string, linkTo?: InstanceLink, shortCircuit?: boolean) => Promise<FileUploadBatchResult>;
|
|
57
54
|
/**
|
|
58
55
|
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
59
56
|
* This is used after instance creation when the instance ID becomes available
|
|
60
57
|
*/
|
|
61
|
-
export declare const createFileLinks: (
|
|
62
|
-
export declare const uploadDocuments: (files: (File |
|
|
58
|
+
export declare const createFileLinks: (fileReferences: DocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
|
|
59
|
+
export declare const uploadDocuments: (files: (File | DocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<DocumentReference[]>;
|
|
63
60
|
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
64
61
|
showAlert: boolean;
|
|
65
62
|
message?: string;
|
|
@@ -85,6 +82,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
|
|
|
85
82
|
}>>, associatedObject?: {
|
|
86
83
|
instanceId: string;
|
|
87
84
|
propertyId: string;
|
|
85
|
+
objectId?: string;
|
|
88
86
|
}, parameters?: InputParameter[]): Promise<FieldValues>;
|
|
89
87
|
export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
|
|
90
88
|
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
|
|
@@ -103,6 +101,10 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | Pa
|
|
|
103
101
|
export declare function plainTextToRtf(plainText: string): string;
|
|
104
102
|
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
105
103
|
export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
|
|
104
|
+
export declare function handleFileUpload(apiServices: ApiServices, submission: FieldValues, actionId: string, objectId?: string, instanceId?: string, linkTo?: {
|
|
105
|
+
instanceId: string;
|
|
106
|
+
objectId: string;
|
|
107
|
+
}): Promise<ObjectInstance>;
|
|
106
108
|
export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
|
|
107
109
|
/**
|
|
108
110
|
* Extract all values from a criteria/filter object.
|