@evoke-platform/ui-components 1.10.0-dev.1 → 1.10.0-dev.10
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.js +1 -1
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +430 -0
- package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
- package/dist/published/components/custom/Form/utils.js +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +15 -3
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -2
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +92 -18
- package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Footer.js +4 -3
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +30 -13
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +0 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +31 -27
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +16 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +32 -7
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +70 -18
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +37 -15
- package/dist/published/components/custom/FormV2/components/Header.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/Header.js +47 -6
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +33 -19
- package/dist/published/components/custom/FormV2/components/utils.js +4 -7
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +432 -4
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +651 -13
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +140 -0
- package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
- package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -10
- package/dist/published/stories/FormRendererContainer.stories.js +7 -3
- package/dist/published/stories/FormRendererData.js +3 -43
- package/dist/published/theme/hooks.d.ts +4 -3
- package/package.json +3 -1
|
@@ -32,7 +32,6 @@ const styles = {
|
|
|
32
32
|
},
|
|
33
33
|
icon: {
|
|
34
34
|
color: '#fff',
|
|
35
|
-
zIndex: 40,
|
|
36
35
|
fontSize: '16px',
|
|
37
36
|
},
|
|
38
37
|
deleteIcon: {
|
|
@@ -56,7 +55,7 @@ const styles = {
|
|
|
56
55
|
};
|
|
57
56
|
export const Image = (props) => {
|
|
58
57
|
const { id, canUpdateProperty, error, value, hasDescription } = props;
|
|
59
|
-
const { handleChange } = useFormContext();
|
|
58
|
+
const { handleChange, onAutosave: onAutosave } = useFormContext();
|
|
60
59
|
const [image, setImage] = useState();
|
|
61
60
|
useEffect(() => {
|
|
62
61
|
if (typeof value === 'string') {
|
|
@@ -64,15 +63,41 @@ export const Image = (props) => {
|
|
|
64
63
|
}
|
|
65
64
|
}, [value]);
|
|
66
65
|
const handleUpload = async (file) => {
|
|
66
|
+
// max file size 300KB
|
|
67
67
|
if (file?.size && file.size <= 300000) {
|
|
68
|
-
const dataUrl =
|
|
68
|
+
const dataUrl = await blobToDataUrl(file);
|
|
69
69
|
setImage(dataUrl);
|
|
70
|
-
|
|
70
|
+
try {
|
|
71
|
+
await handleChange(id, dataUrl);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('Failed to update field:', error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await onAutosave?.(id);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error('Autosave failed:', error);
|
|
82
|
+
}
|
|
71
83
|
}
|
|
72
84
|
};
|
|
73
|
-
const handleRemove = (e) => {
|
|
85
|
+
const handleRemove = async (e) => {
|
|
74
86
|
setImage(null);
|
|
75
|
-
|
|
87
|
+
try {
|
|
88
|
+
await handleChange(id, '');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Failed to update field:', error);
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await onAutosave?.(id);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error('Autosave failed:', error);
|
|
100
|
+
}
|
|
76
101
|
e.stopPropagation();
|
|
77
102
|
};
|
|
78
103
|
const { getRootProps, getInputProps, open } = useDropzone({
|
|
@@ -81,7 +106,7 @@ export const Image = (props) => {
|
|
|
81
106
|
accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.svg'] },
|
|
82
107
|
});
|
|
83
108
|
return (React.createElement(React.Fragment, null, image ? (React.createElement(Box, { sx: styles.imageContainer },
|
|
84
|
-
React.createElement(Box, { sx: { position: 'relative', left: 0
|
|
109
|
+
React.createElement(Box, { sx: { position: 'relative', left: 0 } },
|
|
85
110
|
React.createElement(CardMedia, { component: "img", image: image, alt: 'Uploaded Image', sx: styles.image }),
|
|
86
111
|
canUpdateProperty && (React.createElement(IconButton, { onClick: handleRemove, "aria-label": "Remove image", sx: styles.deleteIcon },
|
|
87
112
|
React.createElement(ClearRounded, { sx: styles.icon })))))) : canUpdateProperty ? (React.createElement(Box, { sx: {
|
|
@@ -6,7 +6,7 @@ import { Autocomplete, IconButton, Paper, TextField, Typography } from '../../..
|
|
|
6
6
|
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
7
7
|
const UserProperty = (props) => {
|
|
8
8
|
const { id, error, value, readOnly, hasDescription } = props;
|
|
9
|
-
const { fetchedOptions, setFetchedOptions, handleChange, fieldHeight } = useFormContext();
|
|
9
|
+
const { fetchedOptions, setFetchedOptions, handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
|
|
10
10
|
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
11
11
|
const apiServices = useApiServices();
|
|
12
12
|
const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
|
|
@@ -40,9 +40,21 @@ const UserProperty = (props) => {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
}, [id]);
|
|
43
|
-
function handleChangeUserProperty(id, value) {
|
|
43
|
+
async function handleChangeUserProperty(id, value) {
|
|
44
44
|
const updatedValue = typeof value?.value === 'string' ? { name: value.label, id: value.value } : null;
|
|
45
|
-
|
|
45
|
+
try {
|
|
46
|
+
await handleChange(id, updatedValue);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('Failed to update field:', error);
|
|
50
|
+
return; // Exit early if handleChange fails
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await onAutosave?.(id);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('Autosave failed:', error);
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
return (options && (React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, popupIcon: userValue || readOnly ? '' : React.createElement(ExpandMore, null), clearIcon: !loadingOptions && userValue ? (React.createElement(IconButton, { size: "small", disableRipple: true, onKeyDown: (e) => {
|
|
48
60
|
if (e.key === 'Enter') {
|
|
@@ -11,7 +11,7 @@ import { encodePageSlug, getDefaultPages, getPrefixedUrl, transformToWhere } fro
|
|
|
11
11
|
import RelatedObjectInstance from './RelatedObjectInstance';
|
|
12
12
|
const ObjectPropertyInput = (props) => {
|
|
13
13
|
const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, } = props;
|
|
14
|
-
const { fetchedOptions, setFetchedOptions, parameters, fieldHeight, handleChange: handleChangeObjectField, instance, } = useFormContext();
|
|
14
|
+
const { fetchedOptions, setFetchedOptions, parameters, fieldHeight, handleChange: handleChangeObjectField, onAutosave: onAutosave, instance, } = useFormContext();
|
|
15
15
|
const { defaultPages, findDefaultPageSlugFor } = useApp();
|
|
16
16
|
const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
|
|
17
17
|
const [openCreateDialog, setOpenCreateDialog] = useState(false);
|
|
@@ -90,14 +90,27 @@ const ObjectPropertyInput = (props) => {
|
|
|
90
90
|
});
|
|
91
91
|
if (updatedFilter.where) {
|
|
92
92
|
setLoadingOptions(true);
|
|
93
|
-
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), (error, instances) => {
|
|
93
|
+
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
|
|
94
94
|
if (error) {
|
|
95
95
|
console.error(error);
|
|
96
96
|
setLoadingOptions(false);
|
|
97
97
|
}
|
|
98
98
|
if (instances && instances.length > 0) {
|
|
99
99
|
setSelectedInstance(instances[0]);
|
|
100
|
-
|
|
100
|
+
try {
|
|
101
|
+
await handleChangeObjectField(id, instances[0]);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error('Failed to update field:', error);
|
|
105
|
+
setLoadingOptions(false);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await onAutosave?.(id);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error('Autosave failed:', error);
|
|
113
|
+
}
|
|
101
114
|
}
|
|
102
115
|
setLoadingOptions(false);
|
|
103
116
|
});
|
|
@@ -131,7 +144,15 @@ const ObjectPropertyInput = (props) => {
|
|
|
131
144
|
}
|
|
132
145
|
});
|
|
133
146
|
}
|
|
134
|
-
}, [
|
|
147
|
+
}, [
|
|
148
|
+
fieldDefinition,
|
|
149
|
+
updatedCriteria,
|
|
150
|
+
layout,
|
|
151
|
+
fetchedOptions?.[`${id}Options`],
|
|
152
|
+
fetchedOptions?.[`${id}UpdatedCriteria`],
|
|
153
|
+
hasFetched,
|
|
154
|
+
id,
|
|
155
|
+
]);
|
|
135
156
|
const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
|
|
136
157
|
useEffect(() => {
|
|
137
158
|
if (displayOption === 'dropdown') {
|
|
@@ -383,19 +404,38 @@ const ObjectPropertyInput = (props) => {
|
|
|
383
404
|
if (selectedInstance?.id) {
|
|
384
405
|
e.preventDefault();
|
|
385
406
|
}
|
|
386
|
-
}, loading: loadingOptions, onChange: (event, value) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
407
|
+
}, loading: loadingOptions, onChange: async (event, value) => {
|
|
408
|
+
try {
|
|
409
|
+
if (isNil(value)) {
|
|
410
|
+
setDropdownInput(undefined);
|
|
411
|
+
setSelectedInstance(undefined);
|
|
412
|
+
await handleChangeObjectField(id, null);
|
|
413
|
+
// Trigger autosave immediately upon clearing
|
|
414
|
+
try {
|
|
415
|
+
await onAutosave?.(id);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
console.error('Autosave failed:', error);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else if (value?.value === '__new__') {
|
|
422
|
+
setOpenCreateDialog(true);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
const selectedInstance = options.find((o) => o.id === value?.value);
|
|
426
|
+
setSelectedInstance(selectedInstance);
|
|
427
|
+
await handleChangeObjectField(id, selectedInstance);
|
|
428
|
+
// Trigger autosave immediately upon selection
|
|
429
|
+
try {
|
|
430
|
+
await onAutosave?.(id);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
console.error('Autosave failed:', error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
394
436
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
setSelectedInstance(selectedInstance);
|
|
398
|
-
handleChangeObjectField(id, selectedInstance);
|
|
437
|
+
catch (error) {
|
|
438
|
+
console.error('Failed to update field:', error);
|
|
399
439
|
}
|
|
400
440
|
}, selectOnFocus: false, onBlur: () => {
|
|
401
441
|
if (dropdownInput) {
|
|
@@ -468,9 +508,21 @@ const ObjectPropertyInput = (props) => {
|
|
|
468
508
|
: undefined, "aria-label": selectedInstance?.name }, selectedInstance?.name ? selectedInstance?.name : readOnly && 'None')),
|
|
469
509
|
!readOnly && selectedInstance ? (React.createElement(Tooltip, { title: `Unlink` },
|
|
470
510
|
React.createElement("span", null,
|
|
471
|
-
React.createElement(IconButton, { onClick: (event) => {
|
|
511
|
+
React.createElement(IconButton, { onClick: async (event) => {
|
|
472
512
|
event.stopPropagation();
|
|
473
|
-
|
|
513
|
+
try {
|
|
514
|
+
await handleChangeObjectField(id, null);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
console.error('Failed to update field:', error);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await onAutosave?.(id);
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
console.error('Autosave failed:', error);
|
|
525
|
+
}
|
|
474
526
|
setSelectedInstance(undefined);
|
|
475
527
|
}, sx: { p: 0, marginBottom: '4px' }, "aria-label": `Unlink` },
|
|
476
528
|
React.createElement(Close, { sx: { width: '20px', height: '20px' } }))))) : (React.createElement(Button, { sx: {
|
|
@@ -29,7 +29,7 @@ const styles = {
|
|
|
29
29
|
};
|
|
30
30
|
const RelatedObjectInstance = (props) => {
|
|
31
31
|
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, fieldDefinition, setSnackbarError, setOptions, options, } = props;
|
|
32
|
-
const { handleChange: handleChangeObjectField, richTextEditor, fieldHeight, width } = useFormContext();
|
|
32
|
+
const { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
|
|
33
33
|
const [selectedRow, setSelectedRow] = useState();
|
|
34
34
|
const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
|
|
35
35
|
const apiServices = useApiServices();
|
|
@@ -42,7 +42,20 @@ const RelatedObjectInstance = (props) => {
|
|
|
42
42
|
const linkExistingInstance = async () => {
|
|
43
43
|
if (selectedRow) {
|
|
44
44
|
setSelectedInstance(selectedRow);
|
|
45
|
-
|
|
45
|
+
try {
|
|
46
|
+
await handleChangeObjectField(id, selectedRow);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('Failed to update field:', error);
|
|
50
|
+
onClose();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await onAutosave?.(id);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Autosave failed:', error);
|
|
58
|
+
}
|
|
46
59
|
}
|
|
47
60
|
onClose();
|
|
48
61
|
};
|
|
@@ -56,22 +69,31 @@ const RelatedObjectInstance = (props) => {
|
|
|
56
69
|
}
|
|
57
70
|
submission = await formatSubmission(submission, apiServices, relatedObject.id);
|
|
58
71
|
try {
|
|
59
|
-
await apiServices
|
|
60
|
-
.post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
|
|
72
|
+
const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
|
|
61
73
|
actionId: actionId,
|
|
62
74
|
input: submission,
|
|
63
|
-
})
|
|
64
|
-
.then((response) => {
|
|
65
|
-
handleChangeObjectField(id, response);
|
|
66
|
-
setSelectedInstance(response);
|
|
67
|
-
setSnackbarError({
|
|
68
|
-
showAlert: true,
|
|
69
|
-
message: 'New instance created',
|
|
70
|
-
isError: false,
|
|
71
|
-
});
|
|
72
|
-
setOptions(options.concat([response]));
|
|
73
|
-
onClose();
|
|
74
75
|
});
|
|
76
|
+
try {
|
|
77
|
+
await handleChangeObjectField(id, response);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('Failed to update field:', error);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await onAutosave?.(id);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error('Autosave failed:', error);
|
|
88
|
+
}
|
|
89
|
+
setSelectedInstance(response);
|
|
90
|
+
setSnackbarError({
|
|
91
|
+
showAlert: true,
|
|
92
|
+
message: 'New instance created',
|
|
93
|
+
isError: false,
|
|
94
|
+
});
|
|
95
|
+
setOptions(options.concat([response]));
|
|
96
|
+
onClose();
|
|
75
97
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
98
|
}
|
|
77
99
|
catch (err) {
|
|
@@ -6,6 +6,7 @@ import { ExpandedSection } from './types';
|
|
|
6
6
|
export type HeaderProps = {
|
|
7
7
|
hasAccordions: boolean;
|
|
8
8
|
shouldShowValidationErrors: boolean;
|
|
9
|
+
validationErrorsRef?: React.Ref<HTMLDivElement>;
|
|
9
10
|
title?: string;
|
|
10
11
|
expandedSections?: ExpandedSection[];
|
|
11
12
|
onExpandAll?: () => void;
|
|
@@ -13,6 +14,7 @@ export type HeaderProps = {
|
|
|
13
14
|
form: EvokeForm;
|
|
14
15
|
errors?: FieldErrors;
|
|
15
16
|
action?: Action;
|
|
17
|
+
autosaving?: boolean;
|
|
16
18
|
sx?: SxProps;
|
|
17
19
|
};
|
|
18
20
|
declare const Header: React.FC<HeaderProps>;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { isEmpty } from 'lodash';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { Autorenew } from '../../../../icons';
|
|
3
4
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
4
5
|
import Button from '../../../core/Button/Button';
|
|
5
6
|
import { Typography } from '../../../core/Typography';
|
|
6
7
|
import Box from '../../../layout/Box/Box';
|
|
7
8
|
import ValidationErrors from './ValidationFiles/ValidationErrors';
|
|
8
9
|
const Header = (props) => {
|
|
9
|
-
const { title, errors, hasAccordions, shouldShowValidationErrors, form, sx } = props;
|
|
10
|
+
const { title, errors, hasAccordions, shouldShowValidationErrors, validationErrorsRef, form, sx } = props;
|
|
10
11
|
const { width } = useFormContext();
|
|
11
12
|
const { breakpoints, isBelow } = useWidgetSize({
|
|
12
13
|
scroll: false,
|
|
@@ -14,6 +15,7 @@ const Header = (props) => {
|
|
|
14
15
|
});
|
|
15
16
|
const isSmallerThanMd = isBelow('md');
|
|
16
17
|
const { isXs, isSm } = breakpoints;
|
|
18
|
+
const isSmall = isSm || isXs;
|
|
17
19
|
return (React.createElement(Box, { sx: {
|
|
18
20
|
paddingX: isSmallerThanMd ? 2 : 3,
|
|
19
21
|
paddingTop: '0px',
|
|
@@ -25,12 +27,25 @@ const Header = (props) => {
|
|
|
25
27
|
borderBottom: !form.id ? undefined : '1px solid #e9ecef',
|
|
26
28
|
gap: isSm || isXs ? 2 : 3,
|
|
27
29
|
...sx,
|
|
30
|
+
'.evoke-form-renderer-header': {
|
|
31
|
+
flex: 1,
|
|
32
|
+
},
|
|
28
33
|
} },
|
|
29
|
-
title && (React.createElement(Box, { sx: { flex: '1 1
|
|
30
|
-
React.createElement(Title, { ...props })
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
title && (React.createElement(Box, { sx: { flex: '1 1 auto', minWidth: 0, display: 'flex', alignItems: 'center', gap: 1 } },
|
|
35
|
+
React.createElement(Title, { ...props }),
|
|
36
|
+
props.autosaving && !isSmall && React.createElement(SavingIndicator, null))),
|
|
37
|
+
hasAccordions && (React.createElement(Box, { sx: { flex: '0 0 auto', display: 'flex', alignItems: 'center' } },
|
|
38
|
+
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
39
|
+
React.createElement(AccordionActions, { ...props })),
|
|
40
|
+
React.createElement(Box, { sx: {
|
|
41
|
+
width: '96px',
|
|
42
|
+
minWidth: '72px',
|
|
43
|
+
display: 'flex',
|
|
44
|
+
justifyContent: 'flex-end',
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
marginLeft: 0.5,
|
|
47
|
+
} }, props.autosaving && isSmall ? React.createElement(SavingIndicator, null) : React.createElement(Box, { sx: { width: '100%' } })))),
|
|
48
|
+
React.createElement("div", { ref: validationErrorsRef, className: 'evoke-form-renderer-header' }, shouldShowValidationErrors && !isEmpty(errors) ? React.createElement(ValidationErrors, { errors: errors }) : null)));
|
|
34
49
|
};
|
|
35
50
|
// Default slot components for convenience
|
|
36
51
|
export const Title = ({ title }) => (React.createElement(Typography, { sx: {
|
|
@@ -60,4 +75,30 @@ export const AccordionActions = ({ onExpandAll, onCollapseAll, expandedSections
|
|
|
60
75
|
fontSize: '14px',
|
|
61
76
|
} }, "Collapse all")));
|
|
62
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* SavingIndicator displays a spinning icon with "Saving" text
|
|
80
|
+
* to indicate that an autosave operation is in progress.
|
|
81
|
+
*/
|
|
82
|
+
const SavingIndicator = () => (React.createElement(Box, { sx: {
|
|
83
|
+
display: 'flex',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
gap: 0.5,
|
|
86
|
+
} },
|
|
87
|
+
React.createElement(Typography, { sx: {
|
|
88
|
+
fontSize: '14px',
|
|
89
|
+
color: 'text.secondary',
|
|
90
|
+
} }, "Saving"),
|
|
91
|
+
React.createElement(Autorenew, { sx: {
|
|
92
|
+
fontSize: '16px',
|
|
93
|
+
color: 'text.secondary',
|
|
94
|
+
animation: 'spin 1s linear infinite',
|
|
95
|
+
'@keyframes spin': {
|
|
96
|
+
'0%': {
|
|
97
|
+
transform: 'rotate(0deg)',
|
|
98
|
+
},
|
|
99
|
+
'100%': {
|
|
100
|
+
transform: 'rotate(360deg)',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
} })));
|
|
63
104
|
export default Header;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
|
|
2
2
|
import { WarningRounded } from '@mui/icons-material';
|
|
3
3
|
import DOMPurify from 'dompurify';
|
|
4
|
+
import { isEmpty } from 'lodash';
|
|
4
5
|
import React, { useEffect, useMemo } from 'react';
|
|
5
6
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
6
7
|
import { TextField, Typography } from '../../../core';
|
|
@@ -37,7 +38,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
|
|
|
37
38
|
}
|
|
38
39
|
export function RecursiveEntryRenderer(props) {
|
|
39
40
|
const { entry } = props;
|
|
40
|
-
const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
|
|
41
|
+
const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
|
|
41
42
|
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
42
43
|
if (!entryIsVisible(entry, getValues(), instance)) {
|
|
43
44
|
return null;
|
|
@@ -90,18 +91,31 @@ export function RecursiveEntryRenderer(props) {
|
|
|
90
91
|
? display?.defaultValue.orderBy
|
|
91
92
|
: undefined, defaultValueCriteria: typeof display?.defaultValue === 'object' && 'criteria' in display.defaultValue
|
|
92
93
|
? display?.defaultValue?.criteria
|
|
93
|
-
: undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description,
|
|
94
|
-
// formId={display?.createFormId} // TODO: this should be added as part of the builder update
|
|
95
|
-
createActionId: '_create' })));
|
|
94
|
+
: undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description, formId: display?.createFormId, createActionId: display?.createActionId })));
|
|
96
95
|
}
|
|
97
96
|
else if (fieldDefinition.type === 'user') {
|
|
98
97
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
99
98
|
React.createElement(UserProperty, { id: entryId, value: fieldValue, error: !!errors?.[entryId], readOnly: entry.type === 'readonlyField', hasDescription: !!display?.description })));
|
|
100
99
|
}
|
|
101
100
|
else if (fieldDefinition.type === 'collection') {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if (fieldDefinition?.manyToManyPropertyId) {
|
|
102
|
+
if (middleObject && !isEmpty(middleObject)) {
|
|
103
|
+
return (initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
104
|
+
React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
|
|
105
|
+
initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: validation?.criteria, hasDescription: !!display?.description }))));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// when in the builder preview, the middle object won't be fetched so instead show an empty field
|
|
109
|
+
const singleSelectProperty = structuredClone(fieldDefinition);
|
|
110
|
+
singleSelectProperty.type = 'choices';
|
|
111
|
+
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
112
|
+
React.createElement(FormField, { id: entryId, property: singleSelectProperty, defaultValue: fieldValue || getValues(entryId), selectOptions: [], size: fieldHeight })));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
117
|
+
React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry })));
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
else if (fieldDefinition.type === 'richText') {
|
|
107
121
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, richTextEditor ? (React.createElement(richTextEditor, {
|
|
@@ -113,7 +127,11 @@ export function RecursiveEntryRenderer(props) {
|
|
|
113
127
|
disabled: entry.type === 'readonlyField',
|
|
114
128
|
rows: display?.rowCount,
|
|
115
129
|
hasError: !!errors?.[entryId],
|
|
116
|
-
})) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange,
|
|
130
|
+
})) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, onBlur: () => {
|
|
131
|
+
onAutosave?.(entryId)?.catch((error) => {
|
|
132
|
+
console.error('Autosave failed:', error);
|
|
133
|
+
});
|
|
134
|
+
}, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
|
|
117
135
|
}
|
|
118
136
|
else if (fieldDefinition.type === 'document') {
|
|
119
137
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -156,18 +174,14 @@ export function RecursiveEntryRenderer(props) {
|
|
|
156
174
|
: `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
|
|
157
175
|
React.createElement(FormField, { id: entryId,
|
|
158
176
|
// TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
|
|
159
|
-
property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return (entry?.enumWithLabels?.find((e) => e.value === option.value)?.label ?? String(option.value));
|
|
166
|
-
}
|
|
167
|
-
}, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
|
|
177
|
+
property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, onBlur: () => {
|
|
178
|
+
// Blur event - reads current value from formData
|
|
179
|
+
onAutosave?.(entryId)?.catch((error) => {
|
|
180
|
+
console.error('Autosave failed:', error);
|
|
181
|
+
});
|
|
182
|
+
}, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, mask: validation?.mask, isOptionEqualToValue: isOptionEqualToValue, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, required: entry.display?.required || false, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
|
|
168
183
|
? display?.booleanDisplay
|
|
169
|
-
: display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions:
|
|
170
|
-
entry.enumWithLabels, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue })));
|
|
184
|
+
: 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 })));
|
|
171
185
|
}
|
|
172
186
|
}
|
|
173
187
|
else if (entry.type === 'columns') {
|
|
@@ -482,11 +482,6 @@ export function convertDocToEntries(document) {
|
|
|
482
482
|
sortBy: 'ASC',
|
|
483
483
|
},
|
|
484
484
|
},
|
|
485
|
-
enumWithLabels: [
|
|
486
|
-
{ label: 'Public', value: 'Public' },
|
|
487
|
-
{ label: 'Private', value: 'Private' },
|
|
488
|
-
{ label: 'Portal', value: 'Portal' },
|
|
489
|
-
],
|
|
490
485
|
});
|
|
491
486
|
}
|
|
492
487
|
entries.push({
|
|
@@ -507,8 +502,8 @@ export function formatDataToDoc(data) {
|
|
|
507
502
|
uploadedDate: data.uploadedDate,
|
|
508
503
|
versionId: data.versionId,
|
|
509
504
|
metadata: {
|
|
510
|
-
type: data.type,
|
|
511
|
-
view_permission: data.view_permission,
|
|
505
|
+
type: data.type ?? '',
|
|
506
|
+
view_permission: data.view_permission ?? '',
|
|
512
507
|
},
|
|
513
508
|
};
|
|
514
509
|
}
|
|
@@ -567,6 +562,7 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
|
|
|
567
562
|
const allDocuments = [];
|
|
568
563
|
const formData = new FormData();
|
|
569
564
|
for (const [index, file] of files.entries()) {
|
|
565
|
+
// Only upload File instances; SavedDocumentReference objects are already uploaded
|
|
570
566
|
if ('size' in file) {
|
|
571
567
|
formData.append(`files[${index}]`, file);
|
|
572
568
|
}
|
|
@@ -628,6 +624,7 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
|
|
|
628
624
|
export async function formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError) {
|
|
629
625
|
for (const [key, value] of Object.entries(submission)) {
|
|
630
626
|
if (isArray(value)) {
|
|
627
|
+
// Only upload if array contains File instances (not SavedDocumentReference)
|
|
631
628
|
const fileInArray = value.some((item) => item instanceof File);
|
|
632
629
|
if (fileInArray && instanceId && apiServices && objectId) {
|
|
633
630
|
try {
|