@evoke-platform/ui-components 1.10.0-dev.2 → 1.10.0-dev.21
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/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
- package/dist/published/components/custom/Form/utils.js +1 -0
- package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +14 -1
- package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
- package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +17 -4
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +116 -74
- package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
- package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
- package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
- 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 +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
- 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 +36 -49
- 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 +2 -1
- 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 +109 -81
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +38 -16
- package/dist/published/components/custom/FormV2/components/Header.d.ts +13 -3
- package/dist/published/components/custom/FormV2/components/Header.js +47 -8
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +44 -35
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
- package/dist/published/components/custom/FormV2/components/types.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +2 -2
- package/dist/published/components/custom/FormV2/components/utils.js +11 -14
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +433 -4
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +662 -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/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +155 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +140 -0
- package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
- package/dist/published/components/custom/index.d.ts +2 -0
- package/dist/published/components/custom/index.js +1 -0
- package/dist/published/index.d.ts +6 -6
- package/dist/published/index.js +1 -1
- package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
- package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
- package/dist/published/stories/FormRendererContainer.stories.js +5 -0
- package/dist/published/stories/FormRendererData.d.ts +12 -0
- package/dist/published/stories/FormRendererData.js +27 -44
- package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
- package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
- package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
- package/dist/published/stories/ViewDetailsV2Data.js +203 -0
- package/dist/published/stories/sharedMswHandlers.js +49 -10
- package/dist/published/theme/hooks.d.ts +4 -3
- package/package.json +4 -2
|
@@ -7,15 +7,14 @@ import { Close } from '../../../../../../icons';
|
|
|
7
7
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
8
8
|
import { Autocomplete, Button, IconButton, Link, ListItem, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
|
|
9
9
|
import { Box } from '../../../../../layout';
|
|
10
|
-
import {
|
|
10
|
+
import { getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
|
|
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);
|
|
18
|
-
const [allDefaultPages, setAllDefaultPages] = useState(defaultPages ?? {});
|
|
19
18
|
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
20
19
|
const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${id}NavigationSlug`]);
|
|
21
20
|
const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${id}RelatedObject`]);
|
|
@@ -24,7 +23,6 @@ const ObjectPropertyInput = (props) => {
|
|
|
24
23
|
const [hasFetched, setHasFetched] = useState(fetchedOptions[`${id}OptionsHaveFetched`] || false);
|
|
25
24
|
const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
|
|
26
25
|
const [layout, setLayout] = useState();
|
|
27
|
-
const [appId, setAppId] = useState(fetchedOptions[`${id}AppId`]);
|
|
28
26
|
const [form, setForm] = useState();
|
|
29
27
|
const [snackbarError, setSnackbarError] = useState({
|
|
30
28
|
showAlert: false,
|
|
@@ -90,14 +88,27 @@ const ObjectPropertyInput = (props) => {
|
|
|
90
88
|
});
|
|
91
89
|
if (updatedFilter.where) {
|
|
92
90
|
setLoadingOptions(true);
|
|
93
|
-
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), (error, instances) => {
|
|
91
|
+
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
|
|
94
92
|
if (error) {
|
|
95
93
|
console.error(error);
|
|
96
94
|
setLoadingOptions(false);
|
|
97
95
|
}
|
|
98
96
|
if (instances && instances.length > 0) {
|
|
99
97
|
setSelectedInstance(instances[0]);
|
|
100
|
-
|
|
98
|
+
try {
|
|
99
|
+
handleChangeObjectField && (await handleChangeObjectField(id, instances[0]));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error('Failed to update field:', error);
|
|
103
|
+
setLoadingOptions(false);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await onAutosave?.(id);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error('Autosave failed:', error);
|
|
111
|
+
}
|
|
101
112
|
}
|
|
102
113
|
setLoadingOptions(false);
|
|
103
114
|
});
|
|
@@ -131,7 +142,15 @@ const ObjectPropertyInput = (props) => {
|
|
|
131
142
|
}
|
|
132
143
|
});
|
|
133
144
|
}
|
|
134
|
-
}, [
|
|
145
|
+
}, [
|
|
146
|
+
fieldDefinition,
|
|
147
|
+
updatedCriteria,
|
|
148
|
+
layout,
|
|
149
|
+
fetchedOptions?.[`${id}Options`],
|
|
150
|
+
fetchedOptions?.[`${id}UpdatedCriteria`],
|
|
151
|
+
hasFetched,
|
|
152
|
+
id,
|
|
153
|
+
]);
|
|
135
154
|
const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
|
|
136
155
|
useEffect(() => {
|
|
137
156
|
if (displayOption === 'dropdown') {
|
|
@@ -143,38 +162,28 @@ const ObjectPropertyInput = (props) => {
|
|
|
143
162
|
setSelectedInstance(initialValue);
|
|
144
163
|
}, [initialValue]);
|
|
145
164
|
useEffect(() => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
else if (action) {
|
|
157
|
-
apiServices
|
|
158
|
-
.get(getPrefixedUrl('/forms'), {
|
|
159
|
-
params: {
|
|
160
|
-
filter: {
|
|
161
|
-
where: {
|
|
162
|
-
actionId: action.id,
|
|
163
|
-
objectId: fieldDefinition.objectId,
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
.then((matchingForms) => {
|
|
169
|
-
if (matchingForms.length === 1) {
|
|
170
|
-
setForm(matchingForms[0]);
|
|
165
|
+
// Early return if already fetched
|
|
166
|
+
if (fetchedOptions[`${id}Form`])
|
|
167
|
+
return;
|
|
168
|
+
const fetchForm = async () => {
|
|
169
|
+
try {
|
|
170
|
+
let evokeForm;
|
|
171
|
+
if (formId || action?.defaultFormId) {
|
|
172
|
+
evokeForm = await apiServices.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`));
|
|
171
173
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
if (evokeForm) {
|
|
175
|
+
setForm(evokeForm);
|
|
176
|
+
setFetchedOptions({
|
|
177
|
+
[`${id}Form`]: evokeForm,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error('Error fetching form:', error);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
fetchForm();
|
|
186
|
+
}, [action, formId, id, fieldDefinition.objectId, apiServices, fetchedOptions]);
|
|
178
187
|
useEffect(() => {
|
|
179
188
|
if (!fetchedOptions[`${id}RelatedObject`]) {
|
|
180
189
|
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
|
|
@@ -188,31 +197,24 @@ const ObjectPropertyInput = (props) => {
|
|
|
188
197
|
}
|
|
189
198
|
}, [fieldDefinition.objectId, fetchedOptions, id]);
|
|
190
199
|
useEffect(() => {
|
|
191
|
-
|
|
192
|
-
if (parameters &&
|
|
200
|
+
(async () => {
|
|
201
|
+
if (parameters && fetchedOptions[`${id}NavigationSlug`] === undefined) {
|
|
193
202
|
const pages = await getDefaultPages(parameters, defaultPages, findDefaultPageSlugFor);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}, [fetchedOptions, parameters, defaultPages, findDefaultPageSlugFor]);
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
if (fieldDefinition.objectId &&
|
|
202
|
-
allDefaultPages &&
|
|
203
|
-
allDefaultPages[fieldDefinition.objectId] &&
|
|
204
|
-
(!fetchedOptions?.[`${id}NavigationSlug`] || !fetchedOptions[`${id}AppId`])) {
|
|
205
|
-
apiServices.get(getPrefixedUrl(`/apps/${allDefaultPages[fieldDefinition.objectId].split('/').slice(1, 2)}/pages/${encodePageSlug(allDefaultPages[fieldDefinition.objectId].split('/').slice(2).join('/'))}`), (error, page) => {
|
|
206
|
-
if (error) {
|
|
207
|
-
console.error(error);
|
|
203
|
+
if (fieldDefinition.objectId && pages[fieldDefinition.objectId]) {
|
|
204
|
+
setNavigationSlug(pages[fieldDefinition.objectId]);
|
|
205
|
+
setFetchedOptions({
|
|
206
|
+
[`${id}NavigationSlug`]: pages[fieldDefinition.objectId],
|
|
207
|
+
});
|
|
208
208
|
}
|
|
209
209
|
else {
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// setting the nav slug to null if there is no default page for this object to avoid re-fetching
|
|
211
|
+
setFetchedOptions({
|
|
212
|
+
[`${id}NavigationSlug`]: null,
|
|
213
|
+
});
|
|
212
214
|
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}, [
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
}, [parameters, defaultPages, findDefaultPageSlugFor, fieldDefinition, fetchedOptions]);
|
|
216
218
|
const handleClose = () => {
|
|
217
219
|
setOpenCreateDialog(false);
|
|
218
220
|
};
|
|
@@ -240,12 +242,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
240
242
|
[`${id}NavigationSlug`]: navigationSlug,
|
|
241
243
|
});
|
|
242
244
|
}
|
|
243
|
-
|
|
244
|
-
setFetchedOptions({
|
|
245
|
-
[`${id}AppId`]: appId,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, appId, id]);
|
|
245
|
+
}, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, id]);
|
|
249
246
|
const dropdownOptions = [
|
|
250
247
|
...options.map((o) => ({ label: o.name, value: o.id })),
|
|
251
248
|
...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === createActionId)
|
|
@@ -383,19 +380,38 @@ const ObjectPropertyInput = (props) => {
|
|
|
383
380
|
if (selectedInstance?.id) {
|
|
384
381
|
e.preventDefault();
|
|
385
382
|
}
|
|
386
|
-
}, loading: loadingOptions, onChange: (event, value) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
383
|
+
}, loading: loadingOptions, onChange: async (event, value) => {
|
|
384
|
+
try {
|
|
385
|
+
if (isNil(value)) {
|
|
386
|
+
setDropdownInput(undefined);
|
|
387
|
+
setSelectedInstance(undefined);
|
|
388
|
+
handleChangeObjectField && (await handleChangeObjectField(id, null));
|
|
389
|
+
// Trigger autosave immediately upon clearing
|
|
390
|
+
try {
|
|
391
|
+
await onAutosave?.(id);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
console.error('Autosave failed:', error);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (value?.value === '__new__') {
|
|
398
|
+
setOpenCreateDialog(true);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
const selectedInstance = options.find((o) => o.id === value?.value);
|
|
402
|
+
setSelectedInstance(selectedInstance);
|
|
403
|
+
handleChangeObjectField && (await handleChangeObjectField(id, selectedInstance));
|
|
404
|
+
// Trigger autosave immediately upon selection
|
|
405
|
+
try {
|
|
406
|
+
await onAutosave?.(id);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
console.error('Autosave failed:', error);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
394
412
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
setSelectedInstance(selectedInstance);
|
|
398
|
-
handleChangeObjectField(id, selectedInstance);
|
|
413
|
+
catch (error) {
|
|
414
|
+
console.error('Failed to update field:', error);
|
|
399
415
|
}
|
|
400
416
|
}, selectOnFocus: false, onBlur: () => {
|
|
401
417
|
if (dropdownInput) {
|
|
@@ -434,7 +450,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
434
450
|
...params.InputProps,
|
|
435
451
|
startAdornment: selectedInstance?.id ? (React.createElement(Typography, { onClick: () => {
|
|
436
452
|
if (navigationSlug && selectedInstance?.id) {
|
|
437
|
-
navigateTo(`/${
|
|
453
|
+
navigateTo(`/${navigationSlug.replace(':instanceId', selectedInstance.id)}`);
|
|
438
454
|
}
|
|
439
455
|
}, sx: {
|
|
440
456
|
cursor: navigationSlug ? 'pointer' : 'default',
|
|
@@ -464,13 +480,25 @@ const ObjectPropertyInput = (props) => {
|
|
|
464
480
|
? '#999'
|
|
465
481
|
: '#212B36',
|
|
466
482
|
}, variant: "body2", href: navigationSlug && !isModal
|
|
467
|
-
? `${'/app'}
|
|
483
|
+
? `${'/app'}${navigationSlug.replace(':instanceId', selectedInstance?.id ?? '')}`
|
|
468
484
|
: undefined, "aria-label": selectedInstance?.name }, selectedInstance?.name ? selectedInstance?.name : readOnly && 'None')),
|
|
469
485
|
!readOnly && selectedInstance ? (React.createElement(Tooltip, { title: `Unlink` },
|
|
470
486
|
React.createElement("span", null,
|
|
471
|
-
React.createElement(IconButton, { onClick: (event) => {
|
|
487
|
+
React.createElement(IconButton, { onClick: async (event) => {
|
|
472
488
|
event.stopPropagation();
|
|
473
|
-
|
|
489
|
+
try {
|
|
490
|
+
handleChangeObjectField && (await handleChangeObjectField(id, null));
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.error('Failed to update field:', error);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
await onAutosave?.(id);
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
console.error('Autosave failed:', error);
|
|
501
|
+
}
|
|
474
502
|
setSelectedInstance(undefined);
|
|
475
503
|
}, sx: { p: 0, marginBottom: '4px' }, "aria-label": `Unlink` },
|
|
476
504
|
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();
|
|
@@ -40,9 +40,22 @@ const RelatedObjectInstance = (props) => {
|
|
|
40
40
|
});
|
|
41
41
|
const { isXs, isSm } = breakpoints;
|
|
42
42
|
const linkExistingInstance = async () => {
|
|
43
|
-
if (selectedRow) {
|
|
43
|
+
if (selectedRow && handleChangeObjectField) {
|
|
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
|
+
handleChangeObjectField && (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,19 +1,29 @@
|
|
|
1
|
-
import { Action
|
|
1
|
+
import { Action } from '@evoke-platform/context';
|
|
2
2
|
import { SxProps } from '@mui/material';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { FieldErrors } from 'react-hook-form';
|
|
5
5
|
import { ExpandedSection } from './types';
|
|
6
6
|
export type HeaderProps = {
|
|
7
7
|
hasAccordions: boolean;
|
|
8
|
-
shouldShowValidationErrors
|
|
8
|
+
shouldShowValidationErrors?: boolean;
|
|
9
|
+
validationContainerRef?: React.Ref<HTMLDivElement>;
|
|
9
10
|
title?: string;
|
|
10
11
|
expandedSections?: ExpandedSection[];
|
|
11
12
|
onExpandAll?: () => void;
|
|
12
13
|
onCollapseAll?: () => void;
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Indicates whether this is a "delete form".
|
|
16
|
+
* This flag adjusts header styling specifically for delete forms.
|
|
17
|
+
*
|
|
18
|
+
* @warning This prop is temporary and will be removed
|
|
19
|
+
* when delete form styling is finalized.
|
|
20
|
+
*/
|
|
21
|
+
isDeleteForm?: boolean;
|
|
14
22
|
errors?: FieldErrors;
|
|
15
23
|
action?: Action;
|
|
24
|
+
autosaving?: boolean;
|
|
16
25
|
sx?: SxProps;
|
|
26
|
+
autosaveEnabled?: boolean;
|
|
17
27
|
};
|
|
18
28
|
declare const Header: React.FC<HeaderProps>;
|
|
19
29
|
export type TitleProps = {
|
|
@@ -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,
|
|
10
|
+
const { title, errors, hasAccordions, shouldShowValidationErrors, validationContainerRef, sx, isDeleteForm, autosaveEnabled, } = props;
|
|
10
11
|
const { width } = useFormContext();
|
|
11
12
|
const { breakpoints, isBelow } = useWidgetSize({
|
|
12
13
|
scroll: false,
|
|
@@ -14,6 +15,8 @@ const Header = (props) => {
|
|
|
14
15
|
});
|
|
15
16
|
const isSmallerThanMd = isBelow('md');
|
|
16
17
|
const { isXs, isSm } = breakpoints;
|
|
18
|
+
const isSmall = isSm || isXs;
|
|
19
|
+
const displayValidationErrors = shouldShowValidationErrors && !isEmpty(errors);
|
|
17
20
|
return (React.createElement(Box, { sx: {
|
|
18
21
|
paddingX: isSmallerThanMd ? 2 : 3,
|
|
19
22
|
paddingTop: '0px',
|
|
@@ -22,15 +25,25 @@ const Header = (props) => {
|
|
|
22
25
|
flexWrap: 'wrap',
|
|
23
26
|
paddingY: isSm || isXs ? 2 : 3,
|
|
24
27
|
// when rendering the default delete action, we don't want a border
|
|
25
|
-
borderBottom:
|
|
28
|
+
borderBottom: isDeleteForm ? undefined : '1px solid #e9ecef',
|
|
26
29
|
gap: isSm || isXs ? 2 : 3,
|
|
27
30
|
...sx,
|
|
28
|
-
} },
|
|
29
|
-
title && (React.createElement(Box, { sx: { flex: '1 1
|
|
30
|
-
React.createElement(Title, { ...props })
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
}, ref: validationContainerRef },
|
|
32
|
+
title && (React.createElement(Box, { sx: { flex: '1 1 auto', minWidth: 0, display: 'flex', alignItems: 'center', gap: 1 } },
|
|
33
|
+
React.createElement(Title, { ...props }),
|
|
34
|
+
props.autosaving && !isSmall && React.createElement(SavingIndicator, null))),
|
|
35
|
+
hasAccordions && (React.createElement(Box, { sx: { flex: '0 0 auto', display: 'flex', alignItems: 'center' } },
|
|
36
|
+
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
37
|
+
React.createElement(AccordionActions, { ...props })),
|
|
38
|
+
autosaveEnabled && (React.createElement(Box, { sx: {
|
|
39
|
+
width: '96px',
|
|
40
|
+
minWidth: '72px',
|
|
41
|
+
display: 'flex',
|
|
42
|
+
justifyContent: 'flex-end',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
marginLeft: 0.5,
|
|
45
|
+
} }, props.autosaving && isSmall ? React.createElement(SavingIndicator, null) : React.createElement(Box, { sx: { width: '100%' } }))))),
|
|
46
|
+
displayValidationErrors ? React.createElement(ValidationErrors, { errors: errors }) : null));
|
|
34
47
|
};
|
|
35
48
|
// Default slot components for convenience
|
|
36
49
|
export const Title = ({ title }) => (React.createElement(Typography, { sx: {
|
|
@@ -60,4 +73,30 @@ export const AccordionActions = ({ onExpandAll, onCollapseAll, expandedSections
|
|
|
60
73
|
fontSize: '14px',
|
|
61
74
|
} }, "Collapse all")));
|
|
62
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* SavingIndicator displays a spinning icon with "Saving" text
|
|
78
|
+
* to indicate that an autosave operation is in progress.
|
|
79
|
+
*/
|
|
80
|
+
const SavingIndicator = () => (React.createElement(Box, { sx: {
|
|
81
|
+
display: 'flex',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
gap: 0.5,
|
|
84
|
+
} },
|
|
85
|
+
React.createElement(Typography, { sx: {
|
|
86
|
+
fontSize: '14px',
|
|
87
|
+
color: 'text.secondary',
|
|
88
|
+
} }, "Saving"),
|
|
89
|
+
React.createElement(Autorenew, { sx: {
|
|
90
|
+
fontSize: '16px',
|
|
91
|
+
color: 'text.secondary',
|
|
92
|
+
animation: 'spin 1s linear infinite',
|
|
93
|
+
'@keyframes spin': {
|
|
94
|
+
'0%': {
|
|
95
|
+
transform: 'rotate(0deg)',
|
|
96
|
+
},
|
|
97
|
+
'100%': {
|
|
98
|
+
transform: 'rotate(360deg)',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
} })));
|
|
63
102
|
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,11 +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
|
-
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
42
|
-
if (!entryIsVisible(entry, getValues(), instance)) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
41
|
+
const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
|
|
45
42
|
const { isBelow, breakpoints } = useWidgetSize({
|
|
46
43
|
scroll: false,
|
|
47
44
|
defaultWidth: width,
|
|
@@ -52,20 +49,24 @@ export function RecursiveEntryRenderer(props) {
|
|
|
52
49
|
const userAccount = useAuthenticationContext()?.account;
|
|
53
50
|
const entryId = getEntryId(entry) || 'defaultId';
|
|
54
51
|
const display = 'display' in entry ? entry.display : undefined;
|
|
55
|
-
const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues(entryId);
|
|
52
|
+
const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
|
|
56
53
|
const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
|
|
57
54
|
const middleObject = fetchedOptions[`${entryId}MiddleObject`];
|
|
58
55
|
const fieldDefinition = useMemo(() => {
|
|
59
56
|
return getFieldDefinition(entry, object, parameters, form?.id === 'documentForm');
|
|
60
57
|
}, [entry, parameters, object]);
|
|
61
58
|
const validation = fieldDefinition?.validation || {};
|
|
62
|
-
if (associatedObject?.propertyId === entryId)
|
|
63
|
-
return null;
|
|
64
59
|
useEffect(() => {
|
|
65
60
|
if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
|
|
66
61
|
fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
|
|
67
62
|
}
|
|
68
63
|
}, [fieldDefinition, instance]);
|
|
64
|
+
if (associatedObject?.propertyId === entryId)
|
|
65
|
+
return null;
|
|
66
|
+
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
67
|
+
if (!getValues || !entryIsVisible(entry, instance, getValues())) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
69
70
|
if (entry.type === 'content') {
|
|
70
71
|
return (React.createElement(Box, { dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(entry.html) }, sx: {
|
|
71
72
|
fontFamily: 'Roboto, Helvetica, Arial, sans-serif',
|
|
@@ -74,7 +75,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
74
75
|
else if ((entry.type === 'input' || entry.type === 'readonlyField' || entry.type === 'inputField') &&
|
|
75
76
|
fieldDefinition) {
|
|
76
77
|
if (isAddressProperty(entryId)) {
|
|
77
|
-
return React.createElement(AddressFields, { entry: entry, entryId: entryId, fieldDefinition: fieldDefinition });
|
|
78
|
+
return (React.createElement(AddressFields, { entry: entry, entryId: entryId, fieldDefinition: fieldDefinition, readOnly: entry.type === 'readonlyField' }));
|
|
78
79
|
}
|
|
79
80
|
else if (fieldDefinition.type === 'image') {
|
|
80
81
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -90,30 +91,42 @@ 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
|
-
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
121
|
+
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor && handleChange ? (React.createElement(RichTextEditor
|
|
122
|
+
// RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
|
|
123
|
+
, {
|
|
108
124
|
// RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
|
|
109
|
-
id: entry.uniqueId,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
rows: display?.rowCount,
|
|
115
|
-
hasError: !!errors?.[entryId],
|
|
116
|
-
})) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
|
|
125
|
+
id: entry.uniqueId, value: fieldValue, handleUpdate: (value) => handleChange(entryId, value), format: "rtf", disabled: entry.type === 'readonlyField', rows: display?.rowCount, hasError: !!errors?.[entryId] })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
|
|
126
|
+
onAutosave?.(entryId)?.catch((error) => {
|
|
127
|
+
console.error('Autosave failed:', error);
|
|
128
|
+
});
|
|
129
|
+
}, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
|
|
117
130
|
}
|
|
118
131
|
else if (fieldDefinition.type === 'document') {
|
|
119
132
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -156,18 +169,14 @@ export function RecursiveEntryRenderer(props) {
|
|
|
156
169
|
: `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
|
|
157
170
|
React.createElement(FormField, { id: entryId,
|
|
158
171
|
// TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
|
|
159
|
-
property: fieldDefinition, defaultValue: fieldValue
|
|
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'
|
|
172
|
+
property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
|
|
173
|
+
// Blur event - reads current value from formData
|
|
174
|
+
onAutosave?.(entryId)?.catch((error) => {
|
|
175
|
+
console.error('Autosave failed:', error);
|
|
176
|
+
});
|
|
177
|
+
}, 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
178
|
? 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 })));
|
|
179
|
+
: 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
180
|
}
|
|
172
181
|
}
|
|
173
182
|
else if (entry.type === 'columns') {
|
package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js
CHANGED
|
@@ -29,7 +29,7 @@ function ValidationErrors(props) {
|
|
|
29
29
|
border: '1px solid #721c24',
|
|
30
30
|
padding: '8px 24px',
|
|
31
31
|
borderRadius: '4px',
|
|
32
|
-
flex: 1,
|
|
32
|
+
flex: '1 1 100%',
|
|
33
33
|
...sx,
|
|
34
34
|
} },
|
|
35
35
|
React.createElement(Typography, { sx: { color: '#721c24', mt: '16px', mb: '8px' } }, "Please fix the following errors before submitting:"),
|