@evoke-platform/ui-components 1.13.0-dev.4 → 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/CriteriaBuilder/CriteriaBuilder.d.ts +4 -4
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +145 -72
- 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 +1 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +25 -22
- 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/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 +4 -2
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +5 -7
- package/dist/published/components/custom/FormV2/components/utils.js +146 -69
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +20 -8
- package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
- package/dist/published/stories/FormRenderer.stories.d.ts +3 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +5 -0
- package/dist/published/theme/hooks.d.ts +2 -1
- package/package.json +1 -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 { 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',
|
|
@@ -87,7 +87,9 @@ export function RecursiveEntryRenderer(props) {
|
|
|
87
87
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
88
88
|
React.createElement(ObjectPropertyInput, { relatedObjectId: !fieldDefinition.objectId ? display?.relatedObjectId : fieldDefinition.objectId, fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: 'criteria' in validation && validation.criteria
|
|
89
89
|
? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
|
|
90
|
-
:
|
|
90
|
+
: entry.display?.criteria
|
|
91
|
+
? updateCriteriaInputs(entry.display?.criteria, getValues(), userAccount, instance)
|
|
92
|
+
: undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
|
|
91
93
|
? display?.defaultValue.sortBy
|
|
92
94
|
: undefined, orderBy: typeof display?.defaultValue === 'object' && 'orderBy' in display.defaultValue
|
|
93
95
|
? display?.defaultValue.orderBy
|
|
@@ -136,7 +138,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
136
138
|
}
|
|
137
139
|
else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
|
|
138
140
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
139
|
-
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 })));
|
|
140
142
|
}
|
|
141
143
|
else if (fieldDefinition.type === 'criteria') {
|
|
142
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;
|
|
@@ -84,6 +81,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
|
|
|
84
81
|
}>>, associatedObject?: {
|
|
85
82
|
instanceId: string;
|
|
86
83
|
propertyId: string;
|
|
84
|
+
objectId?: string;
|
|
87
85
|
}, parameters?: InputParameter[]): Promise<FieldValues>;
|
|
88
86
|
export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
|
|
89
87
|
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[];
|
|
@@ -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
|
}
|
|
@@ -649,7 +725,8 @@ export async function formatSubmission(submission, apiServices, objectId, instan
|
|
|
649
725
|
submission[key] =
|
|
650
726
|
entry &&
|
|
651
727
|
['input', 'inputField'].includes(entry.type) &&
|
|
652
|
-
entry.display?.relatedObjectId
|
|
728
|
+
(entry.display?.relatedObjectId ||
|
|
729
|
+
associatedObject?.objectId)
|
|
653
730
|
? pick(value, 'id', 'name', 'objectId')
|
|
654
731
|
: pick(value, 'id', 'name');
|
|
655
732
|
}
|