@evoke-platform/ui-components 1.13.0-dev.5 → 1.13.0-dev.6
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/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +23 -20
- 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 +46 -10
- 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 +29 -23
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +1 -1
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +4 -7
- package/dist/published/components/custom/FormV2/components/utils.js +144 -68
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +1 -1
- package/package.json +1 -1
|
@@ -43,7 +43,7 @@ export const Document = (props) => {
|
|
|
43
43
|
// For 'file' type properties, check permissions on the sys__file object
|
|
44
44
|
// For 'document' type properties, check document attachment permissions
|
|
45
45
|
const endpoint = property.type === 'file'
|
|
46
|
-
? getPrefixedUrl(
|
|
46
|
+
? getPrefixedUrl('/objects/sys__file/instances/checkAccess?action=execute&field=_create')
|
|
47
47
|
: getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=update`);
|
|
48
48
|
apiServices
|
|
49
49
|
.get(endpoint)
|
package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js
CHANGED
|
@@ -64,12 +64,15 @@ export const DocumentList = (props) => {
|
|
|
64
64
|
}, []);
|
|
65
65
|
const checkPermissions = () => {
|
|
66
66
|
if (instance?.[property.id]?.length) {
|
|
67
|
-
// For 'file' type properties, check
|
|
67
|
+
// For 'file' type properties, check regular object instance permissions
|
|
68
68
|
// For 'document' type properties, check document attachment permissions
|
|
69
69
|
const endpoint = property.type === 'file'
|
|
70
|
-
? getPrefixedUrl(`/objects/sys__file/instances
|
|
70
|
+
? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=read&field=content`)
|
|
71
71
|
: getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=view`);
|
|
72
|
-
apiServices
|
|
72
|
+
apiServices
|
|
73
|
+
.get(endpoint)
|
|
74
|
+
.then((accessCheck) => setHasViewPermission(accessCheck.result))
|
|
75
|
+
.catch(() => setHasViewPermission(false));
|
|
73
76
|
}
|
|
74
77
|
};
|
|
75
78
|
const isFile = (doc) => doc instanceof File;
|
|
@@ -158,7 +158,7 @@ function FormRendererContainer(props) {
|
|
|
158
158
|
});
|
|
159
159
|
if (navigationSlug) {
|
|
160
160
|
if (navigationSlug.includes(':instanceId')) {
|
|
161
|
-
const navigateInstanceId = action?.type === 'create' ? updatedInstance
|
|
161
|
+
const navigateInstanceId = action?.type === 'create' ? updatedInstance.id : instanceId;
|
|
162
162
|
navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', navigateInstanceId ?? ':instanceId')}`);
|
|
163
163
|
}
|
|
164
164
|
else {
|
|
@@ -167,18 +167,17 @@ function FormRendererContainer(props) {
|
|
|
167
167
|
}
|
|
168
168
|
setInstance(updatedInstance);
|
|
169
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Manually links any newly uploaded files in the submission to the specified instance.
|
|
172
|
+
* @param submission The form submission data
|
|
173
|
+
* @param linkTo The instance to link the files to
|
|
174
|
+
*/
|
|
170
175
|
const linkFiles = async (submission, linkTo) => {
|
|
171
|
-
// Create file links for any uploaded files
|
|
176
|
+
// Create file links for any uploaded files that haven't been linked yet
|
|
172
177
|
for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
|
|
173
178
|
const files = submission[property.id];
|
|
174
179
|
if (files?.length) {
|
|
175
|
-
|
|
176
|
-
await createFileLinks(files, linkTo, apiServices);
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
console.error('Failed to create file links:', error);
|
|
180
|
-
// Don't fail the entire submission if file linking fails
|
|
181
|
-
}
|
|
180
|
+
await createFileLinks(files, linkTo, apiServices);
|
|
182
181
|
}
|
|
183
182
|
}
|
|
184
183
|
};
|
|
@@ -195,11 +194,9 @@ function FormRendererContainer(props) {
|
|
|
195
194
|
?.filter((property) => property.formula || property.type === 'collection')
|
|
196
195
|
.map((property) => property.id) ?? []),
|
|
197
196
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
onSubmissionSuccess(response);
|
|
202
|
-
}
|
|
197
|
+
// Manually link files to created instance.
|
|
198
|
+
await linkFiles(submission, { id: response.id, objectId: form.objectId });
|
|
199
|
+
onSubmissionSuccess(response);
|
|
203
200
|
}
|
|
204
201
|
else if (instanceId && action) {
|
|
205
202
|
const response = await objectStore.instanceAction(instanceId, {
|
|
@@ -209,21 +206,26 @@ function FormRendererContainer(props) {
|
|
|
209
206
|
.map((property) => property.id) ?? []),
|
|
210
207
|
});
|
|
211
208
|
if (sanitizedObject && instance) {
|
|
209
|
+
if (!onAutosave) {
|
|
210
|
+
// For non-autosave updates, link any uploaded files to the instance.
|
|
211
|
+
await linkFiles(submission, { id: instanceId, objectId: objectId });
|
|
212
|
+
}
|
|
212
213
|
onSubmissionSuccess(response);
|
|
213
|
-
|
|
214
|
+
// Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
|
|
215
|
+
await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
219
|
catch (error) {
|
|
218
|
-
// Handle deleteDocuments for uploaded documents if the main submission fails
|
|
219
|
-
if (instanceId && action && sanitizedObject && instance) {
|
|
220
|
-
deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
221
|
-
}
|
|
222
220
|
setSnackbarError({
|
|
223
221
|
isError: true,
|
|
224
222
|
showAlert: true,
|
|
225
223
|
message: error.response?.data?.error?.message ?? 'An error occurred',
|
|
226
224
|
});
|
|
225
|
+
if (instanceId && action && sanitizedObject && instance) {
|
|
226
|
+
// For an update, uploaded documents have been linked to the instance and need to be deleted.
|
|
227
|
+
await deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
228
|
+
}
|
|
227
229
|
throw error; // Throw error so caller knows submission failed
|
|
228
230
|
}
|
|
229
231
|
};
|
|
@@ -327,7 +329,7 @@ function FormRendererContainer(props) {
|
|
|
327
329
|
try {
|
|
328
330
|
setIsSaving(true);
|
|
329
331
|
const cleanedData = removeUneditedProtectedValues(formDataRef.current);
|
|
330
|
-
const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError);
|
|
332
|
+
const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
|
|
331
333
|
// Handle object instance autosave
|
|
332
334
|
if (instanceId && action?.type === 'update') {
|
|
333
335
|
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
@@ -336,6 +338,7 @@ function FormRendererContainer(props) {
|
|
|
336
338
|
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
337
339
|
.map((property) => property.id) ?? []),
|
|
338
340
|
});
|
|
341
|
+
await linkFiles(submission, { id: instanceId, objectId });
|
|
339
342
|
}
|
|
340
343
|
setLastSavedData(cloneDeep(formDataRef.current));
|
|
341
344
|
setIsSaving(false);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { DocumentParameterValidation } from '@evoke-platform/context';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { DocumentReference } from '../../types';
|
|
4
4
|
type DocumentProps = {
|
|
5
5
|
id: string;
|
|
6
|
+
fieldType?: 'file' | 'document';
|
|
6
7
|
canUpdateProperty: boolean;
|
|
7
8
|
error: boolean;
|
|
8
9
|
validate?: DocumentParameterValidation;
|
|
9
|
-
value: (File |
|
|
10
|
+
value: (File | DocumentReference)[] | undefined;
|
|
10
11
|
hasDescription?: boolean;
|
|
11
12
|
};
|
|
12
13
|
export declare const Document: (props: DocumentProps) => React.JSX.Element;
|
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 { isNil } from 'lodash';
|
|
3
3
|
import prettyBytes from 'pretty-bytes';
|
|
4
4
|
import React, { useCallback, 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 { fetchedOptions, setFetchedOptions, object, handleChange, onAutosave: onAutosave, instance, } = useFormContext();
|
|
15
|
+
const { fetchedOptions, setFetchedOptions, object, handleChange, onAutosave: onAutosave, instance, form, } = useFormContext();
|
|
16
16
|
const [snackbarError, setSnackbarError] = useState();
|
|
17
17
|
const [documents, setDocuments] = useState();
|
|
18
18
|
const [hasUpdatePermission, setHasUpdatePermission] = useState(fetchedOptions[`${id}UpdatePermission`]);
|
|
@@ -35,8 +35,17 @@ export const Document = (props) => {
|
|
|
35
35
|
}, [value]);
|
|
36
36
|
const checkPermissions = useCallback(() => {
|
|
37
37
|
if (canUpdateProperty && !fetchedOptions[`${id}UpdatePermission`] && instance?.id) {
|
|
38
|
+
// Find the entry to get the configured createActionId
|
|
39
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
|
|
40
|
+
const entry = allEntries?.find((entry) => getEntryId(entry) === id);
|
|
41
|
+
const createActionId = entry?.display?.createActionId ?? '_create';
|
|
42
|
+
// For 'file' type properties, check regular object instance permissions
|
|
43
|
+
// For 'document' type properties, check document attachment permissions
|
|
44
|
+
const endpoint = fieldType === 'file'
|
|
45
|
+
? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=execute&field=${createActionId}`)
|
|
46
|
+
: getPrefixedUrl(`/objects/${object?.id}/instances/${instance.id}/documents/checkAccess?action=update`);
|
|
38
47
|
apiServices
|
|
39
|
-
.get(
|
|
48
|
+
.get(endpoint)
|
|
40
49
|
.then((accessCheck) => {
|
|
41
50
|
setFetchedOptions({
|
|
42
51
|
[`${id}UpdatePermission`]: accessCheck.result,
|
|
@@ -50,16 +59,43 @@ export const Document = (props) => {
|
|
|
50
59
|
setHasUpdatePermission(false);
|
|
51
60
|
});
|
|
52
61
|
}
|
|
53
|
-
}, [
|
|
62
|
+
}, [
|
|
63
|
+
canUpdateProperty,
|
|
64
|
+
fetchedOptions[`${id}UpdatePermission`],
|
|
65
|
+
instance?.id,
|
|
66
|
+
object,
|
|
67
|
+
id,
|
|
68
|
+
apiServices,
|
|
69
|
+
form,
|
|
70
|
+
fieldType,
|
|
71
|
+
]);
|
|
54
72
|
useEffect(() => {
|
|
55
73
|
checkPermissions();
|
|
56
74
|
}, [checkPermissions]);
|
|
57
75
|
const handleUpload = async (files) => {
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
if (!files?.length) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let uploadedFiles = files;
|
|
80
|
+
// Get the createActionId from display options, default to '_create'
|
|
81
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []);
|
|
82
|
+
const entry = allEntries?.find((entry) => getEntryId(entry) === id);
|
|
83
|
+
const createActionId = entry?.display?.createActionId ?? '_create';
|
|
84
|
+
// Immediately upload files for 'file' type properties when autosave is not enabled.
|
|
85
|
+
// Linking will happen upon final submission.
|
|
86
|
+
// If autosave is enabled, upload and linking will happen in the autosave handler.
|
|
87
|
+
if (fieldType === 'file' && !onAutosave) {
|
|
88
|
+
const { successfulUploads, errorMessage } = await uploadFiles(files, apiServices, createActionId, undefined, false);
|
|
89
|
+
uploadedFiles = successfulUploads;
|
|
90
|
+
if (errorMessage) {
|
|
91
|
+
setSnackbarError({ message: errorMessage, type: 'error' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Store uploaded file references (or File objects) in form state
|
|
95
|
+
const newDocuments = [...(documents ?? []), ...uploadedFiles];
|
|
60
96
|
setDocuments(newDocuments);
|
|
61
97
|
try {
|
|
62
|
-
|
|
98
|
+
await handleChange?.(id, newDocuments);
|
|
63
99
|
}
|
|
64
100
|
catch (error) {
|
|
65
101
|
console.error('Failed to update field:', error);
|
|
@@ -127,7 +163,7 @@ export const Document = (props) => {
|
|
|
127
163
|
} }, validate?.maxDocuments === 1
|
|
128
164
|
? `Maximum size is ${formattedMaxSize}.`
|
|
129
165
|
: `The maximum size of each document is ${formattedMaxSize}.`)))))),
|
|
130
|
-
canUpdateProperty && isNil(hasUpdatePermission) ? (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:
|
|
166
|
+
canUpdateProperty && isNil(hasUpdatePermission) ? (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 })),
|
|
131
167
|
React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
|
|
132
168
|
errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
|
|
133
169
|
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,5 +1,4 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
|
-
import { isEqual } from 'lodash';
|
|
3
2
|
import prettyBytes from 'pretty-bytes';
|
|
4
3
|
import React, { useEffect, useState } from 'react';
|
|
5
4
|
import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
|
|
@@ -24,29 +23,33 @@ const viewableFileTypes = [
|
|
|
24
23
|
'text/plain',
|
|
25
24
|
];
|
|
26
25
|
export const DocumentList = (props) => {
|
|
27
|
-
const { handleChange, onAutosave, id, canUpdateProperty, value: documents, setSnackbarError } = props;
|
|
26
|
+
const { handleChange, onAutosave, id, fieldType = 'document', canUpdateProperty, value: documents, setSnackbarError, } = props;
|
|
28
27
|
const apiServices = useApiServices();
|
|
29
28
|
const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
|
|
30
29
|
// Determine property type once at component level
|
|
31
|
-
const
|
|
32
|
-
const isFileType = propertyType === 'file';
|
|
30
|
+
const isFileType = fieldType === 'file';
|
|
33
31
|
const [hasViewPermission, setHasViewPermission] = useState(fetchedOptions[`${id}ViewPermission`] ?? true);
|
|
34
32
|
// savedDocuments is either FileInstance[] or DocumentType[], never a mix
|
|
35
33
|
const [savedDocuments, setSavedDocuments] = useState(fetchedOptions[`${id}SavedDocuments`]);
|
|
36
34
|
useEffect(() => {
|
|
37
|
-
|
|
35
|
+
// Use documents prop (value) as the source of truth, not instance[id]
|
|
36
|
+
// This ensures newly uploaded files trigger a fetch even before they're saved to instance
|
|
37
|
+
const currentValue = documents;
|
|
38
38
|
if (currentValue?.length) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
// Filter out File objects only - we want to fetch details for all DocumentReferences including unsaved ones
|
|
40
|
+
const currentDocumentIds = currentValue
|
|
41
|
+
.filter((doc) => !(doc instanceof File))
|
|
42
|
+
.map((doc) => doc.id);
|
|
43
|
+
if (currentDocumentIds.length) {
|
|
44
|
+
// Check if there are any document IDs that we haven't fetched yet
|
|
45
|
+
const savedDocumentIds = savedDocuments?.map((doc) => doc.id) ?? [];
|
|
46
|
+
const missingDocumentIds = currentDocumentIds.filter((id) => !savedDocumentIds.includes(id));
|
|
47
|
+
if (missingDocumentIds.length > 0) {
|
|
48
|
+
getDocuments(currentDocumentIds);
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
}, [
|
|
52
|
+
}, [documents, savedDocuments]);
|
|
50
53
|
useEffect(() => {
|
|
51
54
|
if (fetchedOptions[`${id}SavedDocuments`]) {
|
|
52
55
|
setSavedDocuments(fetchedOptions[`${id}SavedDocuments`]);
|
|
@@ -83,26 +86,31 @@ export const DocumentList = (props) => {
|
|
|
83
86
|
if (!fetchedOptions[`${id}ViewPermission`]) {
|
|
84
87
|
checkPermissions();
|
|
85
88
|
}
|
|
86
|
-
}, [object]);
|
|
89
|
+
}, [object, id, fetchedOptions[`${id}ViewPermission`]]);
|
|
87
90
|
const checkPermissions = () => {
|
|
88
91
|
if (instance?.[id]?.length) {
|
|
92
|
+
const endpoint = isFileType
|
|
93
|
+
? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=read&field=content`)
|
|
94
|
+
: getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=view`);
|
|
89
95
|
apiServices
|
|
90
|
-
.get(
|
|
96
|
+
.get(endpoint)
|
|
91
97
|
.then((viewPermissionCheck) => {
|
|
92
98
|
setFetchedOptions({
|
|
93
99
|
[`${id}ViewPermission`]: viewPermissionCheck.result,
|
|
94
100
|
});
|
|
95
101
|
setHasViewPermission(viewPermissionCheck.result);
|
|
96
|
-
})
|
|
102
|
+
})
|
|
103
|
+
.catch(() => setHasViewPermission(false));
|
|
97
104
|
}
|
|
98
105
|
};
|
|
99
106
|
const isFile = (doc) => doc instanceof File;
|
|
107
|
+
const isUnsavedFile = (doc) => isFile(doc) || !!doc.unsaved;
|
|
100
108
|
const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
|
|
101
109
|
const handleRemove = async (index) => {
|
|
102
110
|
const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
|
|
103
111
|
const newValue = updatedDocuments.length === 0 ? undefined : updatedDocuments;
|
|
104
112
|
try {
|
|
105
|
-
handleChange
|
|
113
|
+
handleChange?.(id, newValue);
|
|
106
114
|
}
|
|
107
115
|
catch (error) {
|
|
108
116
|
console.error('Failed to update field:', error);
|
|
@@ -124,9 +132,7 @@ export const DocumentList = (props) => {
|
|
|
124
132
|
: savedDocuments?.find((savedDocument) => savedDocument.id === doc.id)?.contentType;
|
|
125
133
|
if (!isFile(doc)) {
|
|
126
134
|
try {
|
|
127
|
-
|
|
128
|
-
const propertyType = object?.properties?.find((p) => p.id === id)?.type;
|
|
129
|
-
const contentEndpoint = propertyType === 'file'
|
|
135
|
+
const contentEndpoint = isFileType
|
|
130
136
|
? getPrefixedUrl(`/files/${doc.id}/content`)
|
|
131
137
|
: getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}/content`);
|
|
132
138
|
const documentResponse = await apiServices.get(contentEndpoint, { responseType: 'blob' });
|
|
@@ -189,10 +195,10 @@ export const DocumentList = (props) => {
|
|
|
189
195
|
} }, doc.name)),
|
|
190
196
|
React.createElement(Grid, { item: true, xs: 12 },
|
|
191
197
|
React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, getDocumentSize(doc)))),
|
|
192
|
-
(
|
|
198
|
+
(isUnsavedFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
|
|
193
199
|
React.createElement(IconButton, { "aria-label": "open document", sx: { ...styles.icon, marginRight: '16px' }, onClick: () => openDocument(index) },
|
|
194
200
|
React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))),
|
|
195
|
-
!isFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
|
|
201
|
+
!isFile(doc) && !isUnsavedFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
|
|
196
202
|
marginRight: '16px',
|
|
197
203
|
backgroundColor: 'rgba(222, 48, 36, 0.16)',
|
|
198
204
|
color: '#A91813',
|
|
@@ -138,7 +138,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
138
138
|
}
|
|
139
139
|
else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
|
|
140
140
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
141
|
-
React.createElement(Document, { id: entryId, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
|
|
141
|
+
React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
|
|
142
142
|
}
|
|
143
143
|
else if (fieldDefinition.type === 'criteria') {
|
|
144
144
|
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
|
+
};
|
|
@@ -4,7 +4,7 @@ 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;
|
|
@@ -49,16 +49,13 @@ export declare const convertPropertiesToParams: (object: Obj) => InputParameter[
|
|
|
49
49
|
export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
|
|
50
50
|
export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
|
|
51
51
|
export declare const docProperties: Property[];
|
|
52
|
-
|
|
53
|
-
* Upload files using the POST /files endpoint for sys__file objects
|
|
54
|
-
*/
|
|
55
|
-
export declare const uploadFiles: (files: (File | SavedDocumentReference)[], apiServices: ApiServices, actionId?: string, metadata?: Record<string, string>, linkTo?: InstanceLink) => Promise<SavedDocumentReference[]>;
|
|
52
|
+
export declare const uploadFiles: (files: (File | DocumentReference)[], apiServices: ApiServices, actionId?: string, linkTo?: InstanceLink, shortCircuit?: boolean) => Promise<FileUploadBatchResult>;
|
|
56
53
|
/**
|
|
57
54
|
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
58
55
|
* This is used after instance creation when the instance ID becomes available
|
|
59
56
|
*/
|
|
60
|
-
export declare const createFileLinks: (
|
|
61
|
-
export declare const uploadDocuments: (files: (File |
|
|
57
|
+
export declare const createFileLinks: (fileReferences: DocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
|
|
58
|
+
export declare const uploadDocuments: (files: (File | DocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<DocumentReference[]>;
|
|
62
59
|
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
63
60
|
showAlert: boolean;
|
|
64
61
|
message?: string;
|
|
@@ -127,7 +127,15 @@ const getEntryType = (entry, parameters) => {
|
|
|
127
127
|
};
|
|
128
128
|
export function getPrefixedUrl(url) {
|
|
129
129
|
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
130
|
-
const dataMatchers = [
|
|
130
|
+
const dataMatchers = [
|
|
131
|
+
'/objects',
|
|
132
|
+
'/correspondenceTemplates',
|
|
133
|
+
'/documents',
|
|
134
|
+
'/payments',
|
|
135
|
+
'/forms',
|
|
136
|
+
'/locations',
|
|
137
|
+
'/files',
|
|
138
|
+
];
|
|
131
139
|
const signalrMatchers = ['/hubs'];
|
|
132
140
|
const accessManagementMatchers = ['/users'];
|
|
133
141
|
const workflowMatchers = ['/workflows'];
|
|
@@ -479,53 +487,90 @@ export const docProperties = [
|
|
|
479
487
|
type: 'string',
|
|
480
488
|
},
|
|
481
489
|
];
|
|
482
|
-
|
|
483
|
-
* Upload files using the POST /files endpoint for sys__file objects
|
|
484
|
-
*/
|
|
485
|
-
export const uploadFiles = async (files, apiServices, actionId = '_create', metadata, linkTo) => {
|
|
490
|
+
export const uploadFiles = async (files, apiServices, actionId = '_create', linkTo, shortCircuit = true) => {
|
|
486
491
|
// Separate already uploaded files from files that need uploading
|
|
487
492
|
const alreadyUploaded = files.filter((file) => !('size' in file));
|
|
488
493
|
const filesToUpload = files.filter((file) => 'size' in file);
|
|
489
|
-
|
|
490
|
-
|
|
494
|
+
let failedUpload = false;
|
|
495
|
+
// Upload all files in parallel, handling each result individually
|
|
496
|
+
const uploadPromises = [];
|
|
497
|
+
for (const file of filesToUpload) {
|
|
491
498
|
const formData = new FormData();
|
|
492
499
|
formData.append('file', file);
|
|
493
500
|
formData.append('actionId', actionId);
|
|
494
501
|
formData.append('objectId', 'sys__file');
|
|
495
|
-
|
|
496
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
497
|
-
formData.append(key, value);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
502
|
+
formData.append('input', JSON.stringify({ name: file.name, contentType: file.type }));
|
|
500
503
|
if (linkTo) {
|
|
501
504
|
formData.append('linkTo', JSON.stringify(linkTo));
|
|
502
505
|
}
|
|
503
|
-
const
|
|
506
|
+
const uploadPromise = (async () => {
|
|
507
|
+
try {
|
|
508
|
+
const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
|
|
509
|
+
return {
|
|
510
|
+
id: fileInstance.id,
|
|
511
|
+
name: fileInstance.name,
|
|
512
|
+
unsaved: true,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
console.error(`Failed to upload file ${file.name}:`, error);
|
|
517
|
+
failedUpload = true;
|
|
518
|
+
}
|
|
519
|
+
})();
|
|
520
|
+
uploadPromises.push(uploadPromise);
|
|
521
|
+
}
|
|
522
|
+
if (!shortCircuit) {
|
|
523
|
+
// Wait for all upload attempts to complete (successes and failures)
|
|
524
|
+
const uploadResults = await Promise.allSettled(uploadPromises);
|
|
525
|
+
const uploadedFiles = uploadResults
|
|
526
|
+
.filter((result) => result.status === 'fulfilled' && !!result.value)
|
|
527
|
+
.map((result) => result.value);
|
|
528
|
+
const failedCount = filesToUpload.length - uploadedFiles.length;
|
|
504
529
|
return {
|
|
505
|
-
|
|
506
|
-
|
|
530
|
+
successfulUploads: [...alreadyUploaded, ...uploadedFiles],
|
|
531
|
+
errorMessage: failedCount > 0 ? `Failed to upload ${failedCount} file(s)` : undefined,
|
|
507
532
|
};
|
|
508
|
-
}
|
|
533
|
+
}
|
|
509
534
|
const uploadedFiles = await Promise.all(uploadPromises);
|
|
510
|
-
|
|
535
|
+
if (failedUpload) {
|
|
536
|
+
return {
|
|
537
|
+
successfulUploads: [],
|
|
538
|
+
errorMessage: 'An error occurred when uploading files',
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
successfulUploads: [...alreadyUploaded, ...uploadedFiles],
|
|
543
|
+
};
|
|
511
544
|
};
|
|
512
545
|
/**
|
|
513
546
|
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
514
547
|
* This is used after instance creation when the instance ID becomes available
|
|
515
548
|
*/
|
|
516
|
-
export const createFileLinks = async (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
549
|
+
export const createFileLinks = async (fileReferences, linkedInstance, apiServices) => {
|
|
550
|
+
// Link files in parallel, handling each result individually
|
|
551
|
+
const linkPromises = [];
|
|
552
|
+
for (const file of fileReferences) {
|
|
553
|
+
const linkPromise = (async () => {
|
|
554
|
+
try {
|
|
555
|
+
await apiServices.post(getPrefixedUrl(`/objects/sys__fileLink/instances`), {
|
|
556
|
+
name: 'File Link',
|
|
557
|
+
file: { id: file.id, name: file.name },
|
|
558
|
+
linkedInstance,
|
|
559
|
+
}, {
|
|
560
|
+
params: {
|
|
561
|
+
actionId: '_create',
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
console.error(`Failed to create file link for ${file.name}:`, error);
|
|
567
|
+
// The file remains unlinked and can be retried later
|
|
568
|
+
}
|
|
569
|
+
})();
|
|
570
|
+
linkPromises.push(linkPromise);
|
|
571
|
+
}
|
|
572
|
+
// Wait for all linking attempts to complete (successes and failures)
|
|
573
|
+
await Promise.allSettled(linkPromises);
|
|
529
574
|
};
|
|
530
575
|
export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
|
|
531
576
|
const allDocuments = [];
|
|
@@ -553,20 +598,29 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
|
|
|
553
598
|
export const deleteDocuments = async (submittedFields, requestSuccess, apiServices, object, instance, action, setSnackbarError) => {
|
|
554
599
|
const documentProperties = action?.parameters
|
|
555
600
|
? action.parameters.filter((param) => ['document', 'file'].includes(param.type))
|
|
556
|
-
: object
|
|
601
|
+
: object.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
|
|
557
602
|
for (const docProperty of documentProperties ?? []) {
|
|
558
603
|
const savedValue = submittedFields[docProperty.id];
|
|
559
|
-
const originalValue = instance
|
|
604
|
+
const originalValue = instance[docProperty.id];
|
|
560
605
|
const documentsToRemove = requestSuccess
|
|
561
606
|
? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
|
|
562
607
|
: (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
|
|
563
608
|
for (const doc of documentsToRemove) {
|
|
564
609
|
try {
|
|
565
|
-
//
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
610
|
+
// Build context for state model
|
|
611
|
+
const fieldType = docProperty.type === 'file' ? 'file' : 'document';
|
|
612
|
+
if (fieldType === 'file') {
|
|
613
|
+
// For file properties, unlink the file. Don't delete the actual file
|
|
614
|
+
// since other instances may be using it.
|
|
615
|
+
await apiServices.post(getPrefixedUrl(`/files/${doc.id}/unlinkInstance`), {
|
|
616
|
+
linkedInstanceId: instance.id,
|
|
617
|
+
linkedObjectId: object.id,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
// For document properties, delete the document
|
|
622
|
+
await apiServices.delete(getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`));
|
|
623
|
+
}
|
|
570
624
|
}
|
|
571
625
|
catch (error) {
|
|
572
626
|
if (error) {
|
|
@@ -581,6 +635,28 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
|
|
|
581
635
|
}
|
|
582
636
|
}
|
|
583
637
|
};
|
|
638
|
+
async function handleUploads(files, propertyType, entry, apiServices, objectId, instanceId) {
|
|
639
|
+
if (propertyType === 'file') {
|
|
640
|
+
// Get the createActionId from display options, default to '_create'
|
|
641
|
+
const createActionId = entry?.display?.createActionId ?? '_create';
|
|
642
|
+
return await uploadFiles(files, apiServices, createActionId, instanceId ? { id: instanceId, objectId } : undefined);
|
|
643
|
+
}
|
|
644
|
+
else if (propertyType === 'document' && instanceId) {
|
|
645
|
+
try {
|
|
646
|
+
const docs = await uploadDocuments(files, {
|
|
647
|
+
type: '',
|
|
648
|
+
view_permission: '',
|
|
649
|
+
...entry?.documentMetadata,
|
|
650
|
+
}, apiServices, instanceId, objectId);
|
|
651
|
+
return { successfulUploads: docs };
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
console.error('Error uploading documents:', error);
|
|
655
|
+
return { successfulUploads: [], errorMessage: 'Error uploading documents' };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return { successfulUploads: [] };
|
|
659
|
+
}
|
|
584
660
|
/**
|
|
585
661
|
* Transforms a form submission into a format safe for API submission.
|
|
586
662
|
*
|
|
@@ -598,44 +674,44 @@ export async function formatSubmission(submission, apiServices, objectId, instan
|
|
|
598
674
|
if (associatedObject) {
|
|
599
675
|
delete submission[associatedObject.propertyId];
|
|
600
676
|
}
|
|
601
|
-
const allEntries = getUnnestedEntries(form?.entries ?? [])
|
|
677
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []);
|
|
602
678
|
for (const [key, value] of Object.entries(submission)) {
|
|
603
679
|
const entry = allEntries?.find((entry) => getEntryId(entry) === key);
|
|
604
680
|
if (isArray(value)) {
|
|
605
|
-
|
|
681
|
+
const propertyType = getEntryType(entry, parameters);
|
|
682
|
+
// The only array types we need to handle specially are 'file' and 'document'.
|
|
683
|
+
if (propertyType !== 'file' && propertyType !== 'document') {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
// Only upload if array contains File instances (not SavedDocumentReference).
|
|
606
687
|
const fileInArray = value.some((item) => item instanceof File);
|
|
607
688
|
if (fileInArray && apiServices && objectId) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// Only pass linkTo if instanceId exists (update action)
|
|
617
|
-
instanceId ? { id: instanceId, objectId } : undefined);
|
|
618
|
-
}
|
|
619
|
-
else if (propertyType === 'document' && instanceId) {
|
|
620
|
-
uploadedDocuments = await uploadDocuments(value, {
|
|
621
|
-
type: '',
|
|
622
|
-
view_permission: '',
|
|
623
|
-
...entry?.documentMetadata,
|
|
624
|
-
}, apiServices, instanceId, objectId);
|
|
625
|
-
}
|
|
626
|
-
submission[key] = uploadedDocuments;
|
|
627
|
-
}
|
|
628
|
-
catch (err) {
|
|
629
|
-
if (err) {
|
|
630
|
-
setSnackbarError &&
|
|
631
|
-
setSnackbarError({
|
|
632
|
-
showAlert: true,
|
|
633
|
-
message: `An error occurred while uploading associated ${propertyType === 'file' ? 'files' : 'documents'}`,
|
|
634
|
-
isError: true,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
689
|
+
const result = await handleUploads(value, propertyType ?? '', entry, apiServices, objectId, instanceId);
|
|
690
|
+
if (result.errorMessage) {
|
|
691
|
+
setSnackbarError?.({
|
|
692
|
+
showAlert: true,
|
|
693
|
+
// Provide generic message since we're ignoring a partial upload.
|
|
694
|
+
message: `An error occurred while uploading associated ${propertyType === 'file' ? 'files' : 'documents'}`,
|
|
695
|
+
isError: true,
|
|
696
|
+
});
|
|
637
697
|
return submission;
|
|
638
698
|
}
|
|
699
|
+
else {
|
|
700
|
+
// Filter out 'unsaved' flag before submission since the api doesn't know about it.
|
|
701
|
+
submission[key] = result.successfulUploads.map((file) => ({ id: file.id, name: file.name }));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Filter out 'unsaved' flag before submission since the api doesn't know about it.
|
|
706
|
+
submission[key] = value
|
|
707
|
+
// This should never happen but it's possible that the else branch
|
|
708
|
+
// is reached because either 'apiServices' or 'objectId' is undefined.
|
|
709
|
+
// If that's the case the submission will fail if we submit File blobs.
|
|
710
|
+
.filter((file) => !(file instanceof File))
|
|
711
|
+
.map((file) => ({
|
|
712
|
+
id: file.id,
|
|
713
|
+
name: file.name,
|
|
714
|
+
}));
|
|
639
715
|
}
|
|
640
716
|
// if there are address fields with no value address needs to be set to undefined to be able to submit
|
|
641
717
|
}
|
|
@@ -143,7 +143,7 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
143
143
|
}
|
|
144
144
|
else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
|
|
145
145
|
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: fieldDefinition.type, label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, prefix: display?.prefix, suffix: display?.suffix, viewOnly: true },
|
|
146
|
-
React.createElement(Document, { id: entryId, error: false, value: fieldValue, canUpdateProperty: false })));
|
|
146
|
+
React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: false, value: fieldValue, canUpdateProperty: false })));
|
|
147
147
|
}
|
|
148
148
|
else if (fieldDefinition.type === 'collection') {
|
|
149
149
|
return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
|