@evoke-platform/ui-components 1.10.0-testing.11 → 1.10.0-testing.12
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/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +3 -1
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +82 -13
- package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Footer.js +3 -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/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 +31 -5
- 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 +1 -0
- package/dist/published/components/custom/FormV2/components/Header.js +42 -4
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +12 -3
- package/dist/published/components/custom/FormV2/components/utils.js +2 -0
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +449 -1
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
- package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
- package/dist/published/theme/hooks.d.ts +4 -3
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
@@ -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',
|
|
@@ -29,10 +31,20 @@ const Header = (props) => {
|
|
|
29
31
|
flex: 1,
|
|
30
32
|
},
|
|
31
33
|
} },
|
|
32
|
-
title && (React.createElement(Box, { sx: { flex: '1 1
|
|
33
|
-
React.createElement(Title, { ...props })
|
|
34
|
-
|
|
35
|
-
|
|
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%' } })))),
|
|
36
48
|
React.createElement("div", { ref: validationErrorsRef, className: 'evoke-form-renderer-header' }, shouldShowValidationErrors && !isEmpty(errors) ? React.createElement(ValidationErrors, { errors: errors }) : null)));
|
|
37
49
|
};
|
|
38
50
|
// Default slot components for convenience
|
|
@@ -63,4 +75,30 @@ export const AccordionActions = ({ onExpandAll, onCollapseAll, expandedSections
|
|
|
63
75
|
fontSize: '14px',
|
|
64
76
|
} }, "Collapse all")));
|
|
65
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
|
+
} })));
|
|
66
104
|
export default Header;
|
|
@@ -38,7 +38,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
|
|
|
38
38
|
}
|
|
39
39
|
export function RecursiveEntryRenderer(props) {
|
|
40
40
|
const { entry } = props;
|
|
41
|
-
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();
|
|
42
42
|
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
43
43
|
if (!entryIsVisible(entry, getValues(), instance)) {
|
|
44
44
|
return null;
|
|
@@ -127,7 +127,11 @@ export function RecursiveEntryRenderer(props) {
|
|
|
127
127
|
disabled: entry.type === 'readonlyField',
|
|
128
128
|
rows: display?.rowCount,
|
|
129
129
|
hasError: !!errors?.[entryId],
|
|
130
|
-
})) : (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 }))));
|
|
131
135
|
}
|
|
132
136
|
else if (fieldDefinition.type === 'document') {
|
|
133
137
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -170,7 +174,12 @@ export function RecursiveEntryRenderer(props) {
|
|
|
170
174
|
: `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
|
|
171
175
|
React.createElement(FormField, { id: entryId,
|
|
172
176
|
// TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
|
|
173
|
-
property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange,
|
|
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'
|
|
174
183
|
? display?.booleanDisplay
|
|
175
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 })));
|
|
176
185
|
}
|
|
@@ -562,6 +562,7 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
|
|
|
562
562
|
const allDocuments = [];
|
|
563
563
|
const formData = new FormData();
|
|
564
564
|
for (const [index, file] of files.entries()) {
|
|
565
|
+
// Only upload File instances; SavedDocumentReference objects are already uploaded
|
|
565
566
|
if ('size' in file) {
|
|
566
567
|
formData.append(`files[${index}]`, file);
|
|
567
568
|
}
|
|
@@ -623,6 +624,7 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
|
|
|
623
624
|
export async function formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError) {
|
|
624
625
|
for (const [key, value] of Object.entries(submission)) {
|
|
625
626
|
if (isArray(value)) {
|
|
627
|
+
// Only upload if array contains File instances (not SavedDocumentReference)
|
|
626
628
|
const fileInArray = value.some((item) => item instanceof File);
|
|
627
629
|
if (fileInArray && instanceId && apiServices && objectId) {
|
|
628
630
|
try {
|