@evoke-platform/ui-components 1.16.0 → 1.17.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 +4 -8
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +144 -238
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
- package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
- package/dist/published/components/custom/Form/utils.d.ts +2 -3
- package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/Select/Select.js +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -2
- package/dist/published/components/custom/FormV2/FormRenderer.js +7 -7
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -4
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +49 -91
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +17 -44
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +12 -44
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +29 -41
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +0 -14
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
- package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +13 -19
- package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
- package/dist/published/components/custom/FormV2/components/utils.d.ts +14 -12
- package/dist/published/components/custom/FormV2/components/utils.js +123 -159
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +1 -6
- package/dist/published/components/custom/index.d.ts +0 -1
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
- package/dist/published/stories/FormRenderer.stories.d.ts +3 -6
- package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -20
- package/dist/published/stories/FormRendererData.d.ts +15 -0
- package/dist/published/stories/FormRendererData.js +63 -0
- package/dist/published/stories/sharedMswHandlers.js +4 -2
- package/dist/published/theme/hooks.d.ts +0 -1
- package/package.json +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +0 -12
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +0 -197
|
@@ -10,7 +10,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Skel
|
|
|
10
10
|
import { Box } from '../../../../../layout';
|
|
11
11
|
import { getReadableQuery } from '../../../../CriteriaBuilder';
|
|
12
12
|
import { retrieveCustomErrorMessage } from '../../../../Form/utils';
|
|
13
|
-
import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl,
|
|
13
|
+
import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, transformToWhere, useFormById, } from '../../utils';
|
|
14
14
|
import { ActionDialog } from './ActionDialog';
|
|
15
15
|
import { DocumentViewerCell } from './DocumentViewerCell';
|
|
16
16
|
const styles = {
|
|
@@ -258,34 +258,20 @@ const RepeatableField = (props) => {
|
|
|
258
258
|
: dialogType === 'update'
|
|
259
259
|
? entry.display?.updateActionId
|
|
260
260
|
: entry.display?.deleteActionId));
|
|
261
|
-
const relatedProperty = relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId);
|
|
262
261
|
// when save is called we know that fieldDefinition is a parameter and fieldDefinition.objectId is defined
|
|
263
|
-
input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm :
|
|
264
|
-
? {
|
|
265
|
-
instanceId: instance.id,
|
|
266
|
-
propertyId: fieldDefinition.relatedPropertyId,
|
|
267
|
-
objectId: !relatedProperty?.objectId ? instance.objectId : undefined,
|
|
268
|
-
}
|
|
262
|
+
input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm : undefined, undefined, instance?.id && fieldDefinition.relatedPropertyId
|
|
263
|
+
? { instanceId: instance.id, propertyId: fieldDefinition.relatedPropertyId }
|
|
269
264
|
: undefined, action?.parameters ?? (relatedObject && convertPropertiesToParams(relatedObject)));
|
|
270
|
-
if (action?.type === 'create' &&
|
|
265
|
+
if (action?.type === 'create' && entry.display?.createActionId) {
|
|
271
266
|
const updatedInput = {
|
|
272
267
|
...input,
|
|
273
|
-
[fieldDefinition?.relatedPropertyId]: {
|
|
274
|
-
id: instance?.id,
|
|
275
|
-
objectId: !relatedProperty?.objectId ? instance?.objectId : undefined,
|
|
276
|
-
},
|
|
268
|
+
[fieldDefinition?.relatedPropertyId]: { id: instance?.id },
|
|
277
269
|
};
|
|
278
270
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
|
|
285
|
-
actionId: action.id,
|
|
286
|
-
input: updatedInput,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
271
|
+
const instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
|
|
272
|
+
actionId: entry.display?.createActionId,
|
|
273
|
+
input: updatedInput,
|
|
274
|
+
});
|
|
289
275
|
queryClient.setQueryData(relatedInstancesQueryKey, (oldData) => {
|
|
290
276
|
if (!oldData)
|
|
291
277
|
return [instance];
|
|
@@ -307,19 +293,12 @@ const RepeatableField = (props) => {
|
|
|
307
293
|
else {
|
|
308
294
|
const relatedObjectId = relatedObject?.id;
|
|
309
295
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${selectedInstanceId}/actions`), {
|
|
319
|
-
actionId: action?.id ?? `_${action?.type}`,
|
|
320
|
-
input: submission,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
296
|
+
const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${selectedInstanceId}/actions`), {
|
|
297
|
+
actionId: `_${action?.type}`,
|
|
298
|
+
input: omit(input, relatedObject?.properties
|
|
299
|
+
?.filter((property) => property.formula || property.type === 'collection')
|
|
300
|
+
.map((property) => property.id) ?? []),
|
|
301
|
+
});
|
|
323
302
|
if (response && relatedObject && instance) {
|
|
324
303
|
deleteDocuments(input, !!response, apiServices, relatedObject, instance, action);
|
|
325
304
|
}
|
|
@@ -472,7 +451,7 @@ const RepeatableField = (props) => {
|
|
|
472
451
|
React.createElement(TableRow, null,
|
|
473
452
|
columns?.map((prop) => (React.createElement(TableCell, { sx: styles.tableCell }, prop.name))),
|
|
474
453
|
canUpdateProperty && React.createElement(TableCell, { sx: { ...styles.tableCell, width: '80px' } }))),
|
|
475
|
-
React.createElement(TableBody,
|
|
454
|
+
React.createElement(TableBody, { sx: { backgroundColor: 'white' } }, relatedInstances?.map((relatedInstance, index) => (React.createElement(TableRow, { key: relatedInstance.id },
|
|
476
455
|
columns?.map((prop) => {
|
|
477
456
|
return (React.createElement(TableCell, { sx: { fontSize: '16px' } }, prop.type === 'document' ? (React.createElement(DocumentViewerCell, { instance: relatedInstance, propertyId: prop.id, setSnackbarError: setSnackbarError })) : (React.createElement(Typography, { key: prop.id, sx: prop.id === 'name'
|
|
478
457
|
? {
|
|
@@ -512,13 +491,7 @@ const RepeatableField = (props) => {
|
|
|
512
491
|
? '_auto_'
|
|
513
492
|
: deleteForm?.id
|
|
514
493
|
: undefined, instanceId: selectedInstanceId, relatedParameter: fieldDefinition, associatedObject: instance?.id && fieldDefinition.relatedPropertyId
|
|
515
|
-
? {
|
|
516
|
-
instanceId: instance.id,
|
|
517
|
-
propertyId: fieldDefinition.relatedPropertyId,
|
|
518
|
-
objectId: !relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId)?.objectId
|
|
519
|
-
? instance.objectId
|
|
520
|
-
: undefined,
|
|
521
|
-
}
|
|
494
|
+
? { instanceId: instance.id, propertyId: fieldDefinition.relatedPropertyId }
|
|
522
495
|
: undefined })),
|
|
523
496
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
|
|
524
497
|
};
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { DocumentParameterValidation } from '@evoke-platform/context';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { SavedDocumentReference } from '../../types';
|
|
4
4
|
type DocumentProps = {
|
|
5
5
|
id: string;
|
|
6
|
-
fieldType?: 'file' | 'document';
|
|
7
6
|
canUpdateProperty: boolean;
|
|
8
7
|
error: boolean;
|
|
9
8
|
validate?: DocumentParameterValidation;
|
|
10
|
-
value: (File |
|
|
9
|
+
value: (File | SavedDocumentReference)[] | undefined;
|
|
11
10
|
hasDescription?: boolean;
|
|
12
11
|
};
|
|
13
12
|
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
|
|
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 {
|
|
10
|
+
import { getPrefixedUrl } from '../../utils';
|
|
11
11
|
import { DocumentList } from './DocumentList';
|
|
12
12
|
export const Document = (props) => {
|
|
13
|
-
const { id,
|
|
13
|
+
const { id, canUpdateProperty, error, value, validate, hasDescription } = props;
|
|
14
14
|
const apiServices = useApiServices();
|
|
15
|
-
const { object, handleChange, onAutosave: onAutosave, instance
|
|
15
|
+
const { object, handleChange, onAutosave: onAutosave, instance } = useFormContext();
|
|
16
16
|
const [snackbarError, setSnackbarError] = useState();
|
|
17
17
|
const [documents, setDocuments] = useState();
|
|
18
18
|
let allowedTypesMessage = '';
|
|
@@ -32,22 +32,11 @@ 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`);
|
|
46
35
|
const { data: hasUpdatePermission = false, isLoading } = useQuery({
|
|
47
|
-
queryKey: ['
|
|
36
|
+
queryKey: ['hasDocUpdatePermission', object?.id, instance?.id],
|
|
48
37
|
queryFn: async () => {
|
|
49
38
|
try {
|
|
50
|
-
const accessCheck = await apiServices.get(
|
|
39
|
+
const accessCheck = await apiServices.get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=update`));
|
|
51
40
|
return accessCheck.result;
|
|
52
41
|
}
|
|
53
42
|
catch {
|
|
@@ -55,36 +44,14 @@ export const Document = (props) => {
|
|
|
55
44
|
}
|
|
56
45
|
},
|
|
57
46
|
staleTime: Infinity,
|
|
58
|
-
|
|
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),
|
|
47
|
+
enabled: canUpdateProperty && !!instance?.id && !!object?.id,
|
|
62
48
|
});
|
|
63
49
|
const handleUpload = async (files) => {
|
|
64
|
-
|
|
65
|
-
|
|
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];
|
|
50
|
+
// Store File objects in form state - they will be uploaded during autosave via formatSubmission()
|
|
51
|
+
const newDocuments = [...(documents ?? []), ...(files ?? [])];
|
|
85
52
|
setDocuments(newDocuments);
|
|
86
53
|
try {
|
|
87
|
-
await handleChange
|
|
54
|
+
handleChange && (await handleChange(id, newDocuments));
|
|
88
55
|
}
|
|
89
56
|
catch (error) {
|
|
90
57
|
console.error('Failed to update field:', error);
|
|
@@ -126,6 +93,7 @@ export const Document = (props) => {
|
|
|
126
93
|
border: `1px dashed ${error ? 'red' : uploadDisabled ? '#DFE3E8' : '#858585'}`,
|
|
127
94
|
position: 'relative',
|
|
128
95
|
cursor: uploadDisabled ? 'cursor' : 'pointer',
|
|
96
|
+
backgroundColor: 'white',
|
|
129
97
|
}, ...getRootProps(), onClick: open },
|
|
130
98
|
React.createElement("input", { ...getInputProps({ id }), disabled: uploadDisabled, ...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined) }),
|
|
131
99
|
React.createElement(Grid, { container: true, sx: { width: '100%' } },
|
|
@@ -152,7 +120,7 @@ export const Document = (props) => {
|
|
|
152
120
|
} }, validate?.maxDocuments === 1
|
|
153
121
|
? `Maximum size is ${formattedMaxSize}.`
|
|
154
122
|
: `The maximum size of each document is ${formattedMaxSize}.`)))))),
|
|
155
|
-
canUpdateProperty && isLoading ? (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })) : (React.createElement(DocumentList, { id: id,
|
|
123
|
+
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 })),
|
|
156
124
|
React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
|
|
157
125
|
errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
|
|
158
126
|
React.createElement(InfoRounded, { sx: { fontSize: '.75rem', marginRight: '3px', color: '#D3271B' } }),
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { SavedDocumentReference } from '../../types';
|
|
3
3
|
type DocumentListProps = {
|
|
4
|
-
handleChange?: (propertyId: string, value: (File |
|
|
4
|
+
handleChange?: (propertyId: string, value: (File | SavedDocumentReference)[] | undefined) => void;
|
|
5
5
|
onAutosave?: (fieldId: string) => void | Promise<void>;
|
|
6
6
|
id: string;
|
|
7
|
-
fieldType?: 'document' | 'file';
|
|
8
7
|
canUpdateProperty: boolean;
|
|
9
|
-
value: (File |
|
|
8
|
+
value: (File | SavedDocumentReference)[] | undefined;
|
|
10
9
|
setSnackbarError: (type: 'error' | 'success', message: string) => void;
|
|
11
10
|
};
|
|
12
11
|
export declare const DocumentList: (props: DocumentListProps) => React.JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
2
|
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { isEqual } from 'lodash';
|
|
3
4
|
import prettyBytes from 'pretty-bytes';
|
|
4
5
|
import React, { useEffect, useState } from 'react';
|
|
5
6
|
import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
|
|
@@ -24,44 +25,33 @@ const viewableFileTypes = [
|
|
|
24
25
|
'text/plain',
|
|
25
26
|
];
|
|
26
27
|
export const DocumentList = (props) => {
|
|
27
|
-
const { handleChange, onAutosave, id,
|
|
28
|
+
const { handleChange, onAutosave, id, canUpdateProperty, value: documents, setSnackbarError } = props;
|
|
28
29
|
const apiServices = useApiServices();
|
|
29
30
|
const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
|
|
30
31
|
// Determine property type once at component level
|
|
31
|
-
const
|
|
32
|
+
const propertyType = object?.properties?.find((p) => p.id === id)?.type;
|
|
33
|
+
const isFileType = propertyType === 'file';
|
|
32
34
|
// savedDocuments is either FileInstance[] or DocumentType[], never a mix
|
|
33
35
|
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);
|
|
39
36
|
useEffect(() => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
.map((doc) => doc.id);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
|
37
|
+
const currentValue = instance?.[id];
|
|
38
|
+
if (currentValue?.length) {
|
|
39
|
+
const currentDocumentIds = currentValue.map((doc) => doc.id);
|
|
40
|
+
if (currentDocumentIds.length &&
|
|
41
|
+
// these need to be sorted otherwise it will evaluate as not equal if the ids are in different orders causing unnecessary fetches
|
|
42
|
+
!isEqual(currentDocumentIds.slice().sort(), savedDocuments
|
|
43
|
+
?.map((doc) => doc.id)
|
|
44
|
+
.slice()
|
|
45
|
+
.sort())) {
|
|
46
|
+
getDocuments(currentDocumentIds);
|
|
57
47
|
}
|
|
58
|
-
getDocuments(currentDocumentIds);
|
|
59
48
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
}, [id, documents, object]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (fetchedOptions[`${id}SavedDocuments`]) {
|
|
52
|
+
setSavedDocuments(fetchedOptions[`${id}SavedDocuments`]);
|
|
63
53
|
}
|
|
64
|
-
}, [
|
|
54
|
+
}, [fetchedOptions]);
|
|
65
55
|
const getDocuments = (currentDocumentIds, shouldRetry = true) => {
|
|
66
56
|
// For 'file' type properties, fetch sys__file instances directly
|
|
67
57
|
// For 'document' type properties, fetch attachment documents
|
|
@@ -74,7 +64,7 @@ export const DocumentList = (props) => {
|
|
|
74
64
|
// There is a short delay between when a document is uploaded and when
|
|
75
65
|
// it is indexed. Therefore, try again if documents are not found.
|
|
76
66
|
if (shouldRetry &&
|
|
77
|
-
(!docs
|
|
67
|
+
(!docs ||
|
|
78
68
|
currentDocumentIds.some((docId) => !docs.find((doc) => docId === doc.id)))) {
|
|
79
69
|
setTimeout(() => getDocuments(currentDocumentIds, false), 2000);
|
|
80
70
|
}
|
|
@@ -82,9 +72,6 @@ export const DocumentList = (props) => {
|
|
|
82
72
|
setSnackbarError('error', 'Error occurred while retrieving saved documents');
|
|
83
73
|
}
|
|
84
74
|
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);
|
|
88
75
|
setSavedDocuments(docs);
|
|
89
76
|
setFetchedOptions({
|
|
90
77
|
[`${id}SavedDocuments`]: docs,
|
|
@@ -110,13 +97,12 @@ export const DocumentList = (props) => {
|
|
|
110
97
|
staleTime: Infinity,
|
|
111
98
|
});
|
|
112
99
|
const isFile = (doc) => doc instanceof File;
|
|
113
|
-
const isUnsavedFile = (doc) => isFile(doc) || !!doc.unsaved;
|
|
114
100
|
const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
|
|
115
101
|
const handleRemove = async (index) => {
|
|
116
102
|
const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
|
|
117
103
|
const newValue = updatedDocuments.length === 0 ? undefined : updatedDocuments;
|
|
118
104
|
try {
|
|
119
|
-
handleChange
|
|
105
|
+
handleChange && (await handleChange(id, newValue));
|
|
120
106
|
}
|
|
121
107
|
catch (error) {
|
|
122
108
|
console.error('Failed to update field:', error);
|
|
@@ -130,7 +116,7 @@ export const DocumentList = (props) => {
|
|
|
130
116
|
}
|
|
131
117
|
};
|
|
132
118
|
const openDocument = async (index) => {
|
|
133
|
-
const doc =
|
|
119
|
+
const doc = documents?.[index];
|
|
134
120
|
if (doc) {
|
|
135
121
|
let url;
|
|
136
122
|
const contentType = doc instanceof File
|
|
@@ -138,7 +124,9 @@ export const DocumentList = (props) => {
|
|
|
138
124
|
: savedDocuments?.find((savedDocument) => savedDocument.id === doc.id)?.contentType;
|
|
139
125
|
if (!isFile(doc)) {
|
|
140
126
|
try {
|
|
141
|
-
|
|
127
|
+
// Determine property type to use the correct endpoint
|
|
128
|
+
const propertyType = object?.properties?.find((p) => p.id === id)?.type;
|
|
129
|
+
const contentEndpoint = propertyType === 'file'
|
|
142
130
|
? getPrefixedUrl(`/files/${doc.id}/content`)
|
|
143
131
|
: getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}/content`);
|
|
144
132
|
const documentResponse = await apiServices.get(contentEndpoint, { responseType: 'blob' });
|
|
@@ -179,8 +167,8 @@ export const DocumentList = (props) => {
|
|
|
179
167
|
return size;
|
|
180
168
|
};
|
|
181
169
|
return (React.createElement(React.Fragment, null,
|
|
182
|
-
!
|
|
183
|
-
!!
|
|
170
|
+
!documents && !canUpdateProperty && (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No documents")),
|
|
171
|
+
!!documents?.length && (React.createElement(Box, null, documents.map((doc, index) => (React.createElement(Grid, { container: true, sx: {
|
|
184
172
|
width: '100%',
|
|
185
173
|
border: '1px solid #C4CDD5',
|
|
186
174
|
borderRadius: '6px',
|
|
@@ -201,10 +189,10 @@ export const DocumentList = (props) => {
|
|
|
201
189
|
} }, doc.name)),
|
|
202
190
|
React.createElement(Grid, { item: true, xs: 12 },
|
|
203
191
|
React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, getDocumentSize(doc)))),
|
|
204
|
-
(
|
|
192
|
+
(isFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
|
|
205
193
|
React.createElement(IconButton, { "aria-label": "open document", sx: { ...styles.icon, marginRight: '16px' }, onClick: () => openDocument(index) },
|
|
206
194
|
React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))),
|
|
207
|
-
!isFile(doc) &&
|
|
195
|
+
!isFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
|
|
208
196
|
marginRight: '16px',
|
|
209
197
|
backgroundColor: 'rgba(222, 48, 36, 0.16)',
|
|
210
198
|
color: '#A91813',
|
|
@@ -102,20 +102,6 @@ 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
|
-
}
|
|
119
105
|
else if (property.type === 'date') {
|
|
120
106
|
columns.push({
|
|
121
107
|
field: prop.id,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useApiServices } from '@evoke-platform/context';
|
|
3
|
+
import { getPrefixedUrl } from './utils';
|
|
4
|
+
import { useQuery } from '@tanstack/react-query';
|
|
5
|
+
import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
|
|
6
|
+
import { Skeleton, Typography } from '../../../core';
|
|
7
|
+
import { Box } from '../../../layout';
|
|
8
|
+
import { WarningRounded } from '../../../../icons';
|
|
9
|
+
function FormletRenderer(props) {
|
|
10
|
+
const { entry } = props;
|
|
11
|
+
const apiServices = useApiServices();
|
|
12
|
+
const { data: formlet, isLoading } = useQuery({
|
|
13
|
+
queryKey: [entry.formletId, 'formlet'],
|
|
14
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/formlets/${entry.formletId}`)),
|
|
15
|
+
staleTime: Infinity,
|
|
16
|
+
enabled: !!entry.formletId,
|
|
17
|
+
});
|
|
18
|
+
if (isLoading)
|
|
19
|
+
return React.createElement(Skeleton, null);
|
|
20
|
+
return formlet ? (React.createElement(React.Fragment, null, formlet.entries?.map((formletEntry, index) => (React.createElement(RecursiveEntryRenderer, { key: index, entry: formletEntry }))))) : (React.createElement(Box, { sx: {
|
|
21
|
+
display: 'flex',
|
|
22
|
+
backgroundColor: '#ffc1073b',
|
|
23
|
+
borderRadius: '8px',
|
|
24
|
+
padding: '16.5px 14px',
|
|
25
|
+
marginTop: '6px',
|
|
26
|
+
} },
|
|
27
|
+
React.createElement(WarningRounded, { sx: { paddingRight: '8px' }, color: "warning" }),
|
|
28
|
+
React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "This field was not configured correctly")));
|
|
29
|
+
}
|
|
30
|
+
export default FormletRenderer;
|
|
@@ -33,14 +33,17 @@ const HtmlView = ({ value }) => {
|
|
|
33
33
|
quillRef.current.setContents([]);
|
|
34
34
|
quillRef.current.clipboard.dangerouslyPasteHTML(DOMPurify.sanitize(value));
|
|
35
35
|
}, [value]);
|
|
36
|
-
return (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
return (
|
|
37
|
+
// Needs to be wrapped in a Box to prevent quill from setting height: 100% on the container, which causes it to overflow its parent and ignore the maxHeight we set on the containerRef (only happens when in a grid item)
|
|
38
|
+
React.createElement(Box, null,
|
|
39
|
+
React.createElement(Box, { sx: {
|
|
40
|
+
width: '100%',
|
|
41
|
+
height: '100%',
|
|
42
|
+
'.ql-container.ql-snow': {
|
|
43
|
+
border: 'none',
|
|
44
|
+
minHeight: 20,
|
|
45
|
+
},
|
|
46
|
+
} },
|
|
47
|
+
React.createElement("div", { ref: containerRef }))));
|
|
45
48
|
};
|
|
46
49
|
export default HtmlView;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
|
|
2
2
|
import { WarningRounded } from '@mui/icons-material';
|
|
3
|
+
import { Grid } from '@mui/material';
|
|
3
4
|
import { isEmpty } from 'lodash';
|
|
4
5
|
import React, { useMemo } from 'react';
|
|
5
6
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
@@ -13,7 +14,6 @@ import DropdownRepeatableField from './FormFieldTypes/CollectionFiles/DropdownRe
|
|
|
13
14
|
import RepeatableField from './FormFieldTypes/CollectionFiles/RepeatableField';
|
|
14
15
|
import Criteria from './FormFieldTypes/Criteria';
|
|
15
16
|
import { Document } from './FormFieldTypes/DocumentFiles/Document';
|
|
16
|
-
import { FileContent } from './FormFieldTypes/FileContent';
|
|
17
17
|
import { Image } from './FormFieldTypes/Image';
|
|
18
18
|
import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
|
|
19
19
|
import UserProperty from './FormFieldTypes/UserProperty';
|
|
@@ -21,6 +21,7 @@ import FormSections from './FormSections';
|
|
|
21
21
|
import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
|
|
22
22
|
import HtmlView from './HtmlView';
|
|
23
23
|
import { useQuery } from '@tanstack/react-query';
|
|
24
|
+
import FormletRenderer from './FormletRenderer';
|
|
24
25
|
function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
|
|
25
26
|
return {
|
|
26
27
|
inputId: entryId,
|
|
@@ -53,8 +54,6 @@ export function RecursiveEntryRenderer(props) {
|
|
|
53
54
|
const display = 'display' in entry ? entry.display : undefined;
|
|
54
55
|
const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
|
|
55
56
|
const fieldDefinition = useMemo(() => {
|
|
56
|
-
if (!object)
|
|
57
|
-
return undefined;
|
|
58
57
|
return getFieldDefinition(entry, object, parameters);
|
|
59
58
|
}, [entry, parameters, object]);
|
|
60
59
|
const validation = fieldDefinition?.validation || {};
|
|
@@ -88,9 +87,10 @@ export function RecursiveEntryRenderer(props) {
|
|
|
88
87
|
},
|
|
89
88
|
});
|
|
90
89
|
const memorizedCriteria = useMemo(() => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
return 'criteria' in validation && validation.criteria && getValues
|
|
91
|
+
? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
|
|
92
|
+
: undefined;
|
|
93
|
+
}, [validation, getValues && getValues(), userAccount, instance]);
|
|
94
94
|
const memorizedDefaultValueCriteria = useMemo(() => {
|
|
95
95
|
return display?.defaultValue &&
|
|
96
96
|
typeof display.defaultValue === 'object' &&
|
|
@@ -166,11 +166,7 @@ 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,
|
|
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 })));
|
|
169
|
+
React.createElement(Document, { id: entryId, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
|
|
174
170
|
}
|
|
175
171
|
else if (fieldDefinition.type === 'criteria') {
|
|
176
172
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -218,16 +214,14 @@ export function RecursiveEntryRenderer(props) {
|
|
|
218
214
|
? display?.booleanDisplay
|
|
219
215
|
: display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions: fieldDefinition?.enum, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue, protection: objectProperty?.protection })));
|
|
220
216
|
}
|
|
217
|
+
// Forms from the FormV2 widget will get the effective form and will not need this
|
|
218
|
+
// but it's possible for a form passed into the FormRenderer to include a formlet type
|
|
219
|
+
}
|
|
220
|
+
else if (entry.type === 'formlet') {
|
|
221
|
+
return React.createElement(FormletRenderer, { entry: entry });
|
|
221
222
|
}
|
|
222
223
|
else if (entry.type === 'columns') {
|
|
223
|
-
return (React.createElement(
|
|
224
|
-
display: 'flex',
|
|
225
|
-
alignItems: 'flex-start',
|
|
226
|
-
gap: '30px',
|
|
227
|
-
flexDirection: isXs ? 'column' : 'row',
|
|
228
|
-
} }, entry.columns.map((column, colIndex) => (
|
|
229
|
-
// calculating the width like this rather than flex={column.width} to prevent collections from being too wide
|
|
230
|
-
React.createElement(Box, { key: colIndex, sx: { width: isXs ? '100%' : `calc(${(column.width / 12) * 100}% - 15px)` } }, column.entries?.map((columnEntry, entryIndex) => {
|
|
224
|
+
return (React.createElement(Grid, { container: true, columnSpacing: 4 }, entry.columns.map((column, colIndex) => (React.createElement(Grid, { key: colIndex, item: true, xs: isXs ? 12 : column.width }, column.entries?.map((columnEntry, entryIndex) => {
|
|
231
225
|
return (React.createElement(RecursiveEntryRenderer, { key: entryIndex + (columnEntry?.parameterId ?? ''), entry: columnEntry }));
|
|
232
226
|
}))))));
|
|
233
227
|
}
|
|
@@ -12,10 +12,9 @@ export type FieldAddress = {
|
|
|
12
12
|
export type AccessCheck = {
|
|
13
13
|
result: boolean;
|
|
14
14
|
};
|
|
15
|
-
export type
|
|
15
|
+
export type SavedDocumentReference = {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
18
|
-
unsaved?: boolean;
|
|
19
18
|
};
|
|
20
19
|
export type Document = {
|
|
21
20
|
id: string;
|
|
@@ -113,7 +112,3 @@ export type InstanceLink = {
|
|
|
113
112
|
id: string;
|
|
114
113
|
objectId: string;
|
|
115
114
|
};
|
|
116
|
-
export type FileUploadBatchResult = {
|
|
117
|
-
errorMessage?: string;
|
|
118
|
-
successfulUploads: DocumentReference[];
|
|
119
|
-
};
|