@evoke-platform/ui-components 1.10.0-testing.8 → 1.10.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/core/Autocomplete/Autocomplete.js +4 -2
- package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
- package/dist/published/components/core/TextField/TextField.js +1 -1
- package/dist/published/components/core/TextField/TextField.test.js +0 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +25 -3
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +473 -0
- package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
- package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
- package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
- package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
- package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
- package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
- package/dist/published/components/custom/FormField/FormField.js +17 -5
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
- package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
- 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 +46 -8
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
- 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/DefaultValues.d.ts +2 -2
- package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
- 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 +8 -5
- 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 +2 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +6 -23
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -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 +115 -87
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
- package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
- package/dist/published/components/custom/FormV2/components/Header.js +47 -9
- package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
- package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
- package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
- package/dist/published/components/custom/FormV2/components/utils.js +83 -13
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +413 -46
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
- 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/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -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/CriteriaBuilder.stories.js +6 -0
- 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 +26 -1
- 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/dist/published/types.d.ts +3 -0
- package/package.json +12 -8
|
@@ -1,33 +1,53 @@
|
|
|
1
1
|
import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
|
|
2
2
|
import axios from 'axios';
|
|
3
|
-
import { get, isArray, isEmpty, isEqual,
|
|
4
|
-
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set, uniq } from 'lodash';
|
|
4
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { Skeleton, Snackbar } from '../../core';
|
|
6
6
|
import { Box } from '../../layout';
|
|
7
7
|
import ErrorComponent from '../ErrorComponent';
|
|
8
8
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
9
|
+
import Header from './components/Header';
|
|
9
10
|
import { convertDocToEntries, deleteDocuments, encodePageSlug, formatDataToDoc, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
|
|
10
11
|
import FormRenderer from './FormRenderer';
|
|
11
12
|
function FormRendererContainer(props) {
|
|
12
13
|
const { instanceId, pageNavigation, documentId, dataType, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
|
|
13
14
|
const apiServices = useApiServices();
|
|
14
15
|
const navigateTo = useNavigate();
|
|
15
|
-
const { id: appId
|
|
16
|
+
const { id: appId } = useApp();
|
|
16
17
|
const [hasDocumentUpdateAccess, setHasDocumentUpdateAccess] = useState();
|
|
17
|
-
const [defaultPagesWithSlugs, setDefaultPagesWithSlugs] = useState({});
|
|
18
18
|
const [sanitizedObject, setSanitizedObject] = useState();
|
|
19
19
|
const [navigationSlug, setNavigationSlug] = useState();
|
|
20
20
|
const [parameters, setParameters] = useState();
|
|
21
21
|
const [document, setDocument] = useState();
|
|
22
22
|
const [instance, setInstance] = useState();
|
|
23
|
-
const
|
|
23
|
+
const formDataRef = useRef();
|
|
24
|
+
// We only need the setter to force a re-render when form data updates; the value itself
|
|
25
|
+
// is intentionally not referenced elsewhere to avoid stale reads (we use formDataRef).
|
|
26
|
+
// Keep the setter to allow updating a version counter without declaring the value
|
|
27
|
+
// which would trigger a lint error for being unused.
|
|
28
|
+
const [, setFormDataVersion] = useState(0);
|
|
24
29
|
const [action, setAction] = useState();
|
|
30
|
+
/**
|
|
31
|
+
* Updates form data synchronously and triggers a re-render.
|
|
32
|
+
*
|
|
33
|
+
* This function uses a ref for synchronous updates (to avoid race conditions in autosave)
|
|
34
|
+
* combined with a version counter to trigger React re-renders. This ensures that:
|
|
35
|
+
* 1. formDataRef.current is updated immediately (synchronous)
|
|
36
|
+
* 2. Components that depend on formData will re-render (via version increment)
|
|
37
|
+
* 3. Autosave always reads the latest data without timing issues
|
|
38
|
+
*/
|
|
39
|
+
const setFormData = (newData) => {
|
|
40
|
+
formDataRef.current = newData;
|
|
41
|
+
setFormDataVersion((v) => v + 1);
|
|
42
|
+
};
|
|
25
43
|
const [error, setError] = useState();
|
|
26
44
|
const [form, setForm] = useState();
|
|
27
45
|
const [snackbarError, setSnackbarError] = useState({
|
|
28
46
|
showAlert: false,
|
|
29
47
|
isError: true,
|
|
30
48
|
});
|
|
49
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
50
|
+
const [lastSavedData, setLastSavedData] = useState({});
|
|
31
51
|
const userAccount = useAuthenticationContext()?.account;
|
|
32
52
|
const objectStore = useObject(form?.objectId ?? objectId);
|
|
33
53
|
const onError = (err) => {
|
|
@@ -50,15 +70,18 @@ function FormRendererContainer(props) {
|
|
|
50
70
|
}
|
|
51
71
|
else {
|
|
52
72
|
if (instanceId) {
|
|
53
|
-
objectStore.getInstance(instanceId)
|
|
54
|
-
|
|
55
|
-
});
|
|
73
|
+
const instance = await objectStore.getInstance(instanceId);
|
|
74
|
+
setInstance(instance);
|
|
56
75
|
}
|
|
57
76
|
const object = await apiServices.get(getPrefixedUrl(`/objects/${form?.objectId || objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } });
|
|
58
77
|
setSanitizedObject(object);
|
|
59
78
|
const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
60
79
|
if (action && (instanceId || action.type === 'create')) {
|
|
61
80
|
setAction(action);
|
|
81
|
+
// Clear error if action is found after being missing
|
|
82
|
+
// TODO: This entire effect should take place after form is fetched to avoid an error flickering
|
|
83
|
+
// That is, this effect should be merged with the one below that fetches the form
|
|
84
|
+
setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
|
|
62
85
|
}
|
|
63
86
|
else {
|
|
64
87
|
setError('Action could not be found');
|
|
@@ -78,28 +101,21 @@ function FormRendererContainer(props) {
|
|
|
78
101
|
setNavigationSlug(page?.slug);
|
|
79
102
|
});
|
|
80
103
|
}
|
|
81
|
-
if (defaultPages) {
|
|
82
|
-
for (const [objectId, defaultPage] of Object.entries(defaultPages)) {
|
|
83
|
-
const pageId = defaultPage.includes('/')
|
|
84
|
-
? encodePageSlug(defaultPage.split('/').slice(2).join('/'))
|
|
85
|
-
: defaultPage;
|
|
86
|
-
apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${pageId}`)).then((page) => {
|
|
87
|
-
setDefaultPagesWithSlugs({
|
|
88
|
-
...defaultPagesWithSlugs,
|
|
89
|
-
[objectId]: '/' + page.appId + '/' + page.slug,
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
104
|
}, []);
|
|
95
105
|
useEffect(() => {
|
|
106
|
+
const needsInstance = action?.type !== 'create' && !!instanceId;
|
|
107
|
+
// Instance and Action are loaded in the side effect above; wait for them to complete.
|
|
108
|
+
const loading = (actionId && !action) || (needsInstance && !instance);
|
|
96
109
|
if (dataType === 'documents' || form)
|
|
97
110
|
return;
|
|
98
|
-
if (
|
|
111
|
+
if (loading)
|
|
112
|
+
return;
|
|
113
|
+
if ((formId || action?.defaultFormId) && formId !== '_auto_') {
|
|
99
114
|
apiServices
|
|
100
115
|
.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
|
|
101
116
|
.then((evokeForm) => {
|
|
102
|
-
|
|
117
|
+
// If an actionId is provided, ensure it matches the form's actionId
|
|
118
|
+
if (!actionId || evokeForm?.actionId === actionId) {
|
|
103
119
|
const form = evokeForm;
|
|
104
120
|
setForm(form);
|
|
105
121
|
}
|
|
@@ -111,50 +127,27 @@ function FormRendererContainer(props) {
|
|
|
111
127
|
onError(error);
|
|
112
128
|
});
|
|
113
129
|
}
|
|
114
|
-
else if (action) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
130
|
+
else if (action?.type === 'delete' && formId === '_auto_') {
|
|
131
|
+
setForm({
|
|
132
|
+
id: '',
|
|
133
|
+
name: '',
|
|
134
|
+
entries: [
|
|
135
|
+
{
|
|
136
|
+
type: 'content',
|
|
137
|
+
html: `<p style="padding-top: 24px; padding-bottom: 24px;">You are about to delete <strong>${instance?.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</p>`,
|
|
123
138
|
},
|
|
139
|
+
],
|
|
140
|
+
objectId: objectId,
|
|
141
|
+
actionId: '_delete',
|
|
142
|
+
display: {
|
|
143
|
+
submitLabel: 'Delete',
|
|
124
144
|
},
|
|
125
|
-
})
|
|
126
|
-
.then((matchingForms) => {
|
|
127
|
-
if (matchingForms.length === 1) {
|
|
128
|
-
const form = matchingForms[0];
|
|
129
|
-
setForm(form);
|
|
130
|
-
// use this default form if no delete form is found
|
|
131
|
-
}
|
|
132
|
-
else if (action.type === 'delete' && instance) {
|
|
133
|
-
setForm({
|
|
134
|
-
id: '',
|
|
135
|
-
name: '',
|
|
136
|
-
entries: [
|
|
137
|
-
{
|
|
138
|
-
type: 'content',
|
|
139
|
-
html: `<p>You are about to delete <strong>${instance.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</p>`,
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
objectId: objectId,
|
|
143
|
-
actionId: '_delete',
|
|
144
|
-
display: {
|
|
145
|
-
submitLabel: 'Delete',
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
else if (instance || action.type === 'create') {
|
|
150
|
-
setError('Default action form could not be found');
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
.catch((error) => {
|
|
154
|
-
onError(error);
|
|
155
145
|
});
|
|
156
146
|
}
|
|
157
|
-
|
|
147
|
+
else {
|
|
148
|
+
setError('Action form could not be found');
|
|
149
|
+
}
|
|
150
|
+
}, [action, actionId, objectId, instance]);
|
|
158
151
|
useEffect(() => {
|
|
159
152
|
if (form?.id === 'documentForm') {
|
|
160
153
|
setParameters([
|
|
@@ -184,6 +177,8 @@ function FormRendererContainer(props) {
|
|
|
184
177
|
if (document && objectId) {
|
|
185
178
|
const defaultValues = await getDefaultValues(convertDocToEntries(document), document);
|
|
186
179
|
setFormData(defaultValues);
|
|
180
|
+
// Deep clone to avoid reference issues
|
|
181
|
+
setLastSavedData(cloneDeep(defaultValues));
|
|
187
182
|
if (!form) {
|
|
188
183
|
setForm({
|
|
189
184
|
id: 'documentForm',
|
|
@@ -196,6 +191,8 @@ function FormRendererContainer(props) {
|
|
|
196
191
|
else if (form && (instance || !instanceId)) {
|
|
197
192
|
const defaultValues = await getDefaultValues(form.entries, instance || {});
|
|
198
193
|
setFormData(defaultValues);
|
|
194
|
+
// Deep clone to avoid reference issues
|
|
195
|
+
setLastSavedData(cloneDeep(defaultValues));
|
|
199
196
|
}
|
|
200
197
|
};
|
|
201
198
|
getInitialValues();
|
|
@@ -281,102 +278,124 @@ function FormRendererContainer(props) {
|
|
|
281
278
|
};
|
|
282
279
|
const getDefaultValues = async (entries, instanceData) => {
|
|
283
280
|
const result = {};
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
281
|
+
const unnestedEntries = getUnnestedEntries(entries);
|
|
282
|
+
for (const entry of unnestedEntries) {
|
|
283
|
+
if ((entry.type === 'input' || entry.type === 'inputField') &&
|
|
284
|
+
isAddressProperty(entry.parameterId || entry.input?.id)) {
|
|
285
|
+
const fieldId = getEntryId(entry);
|
|
286
|
+
if (!fieldId)
|
|
287
|
+
continue;
|
|
288
|
+
const fieldValue = get(instanceData, fieldId);
|
|
289
|
+
if ((isEmpty(instanceData) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
|
|
290
|
+
entry?.display?.defaultValue &&
|
|
291
|
+
parameters) {
|
|
292
|
+
const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
|
|
293
|
+
if (isArray(defaultValuesArray)) {
|
|
294
|
+
defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
|
|
295
|
+
set(result, fieldId, fieldValue);
|
|
296
|
+
});
|
|
295
297
|
}
|
|
296
298
|
}
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
const fieldId = getEntryId(entry);
|
|
300
|
-
if (!fieldId)
|
|
301
|
-
return;
|
|
302
|
-
const fieldValue = get(instanceData, fieldId);
|
|
303
|
-
if ((isEmpty(instanceData) ||
|
|
304
|
-
fieldValue === undefined ||
|
|
305
|
-
fieldValue === null ||
|
|
306
|
-
fieldValue === '') &&
|
|
307
|
-
entry?.display?.defaultValue &&
|
|
308
|
-
parameters) {
|
|
309
|
-
const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
|
|
310
|
-
if (isArray(defaultValuesArray)) {
|
|
311
|
-
defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
|
|
312
|
-
set(result, fieldId, fieldValue);
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
317
|
-
set(result, fieldId, fieldValue);
|
|
318
|
-
}
|
|
299
|
+
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
300
|
+
set(result, fieldId, fieldValue);
|
|
319
301
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
302
|
+
}
|
|
303
|
+
else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
|
|
304
|
+
const fieldId = entry.type === 'input'
|
|
305
|
+
? entry.parameterId
|
|
306
|
+
: entry.type === 'inputField'
|
|
307
|
+
? entry.input?.id
|
|
308
|
+
: undefined;
|
|
309
|
+
if (fieldId) {
|
|
310
|
+
const fieldValue = instanceData?.[fieldId] ??
|
|
311
|
+
instanceData?.metadata?.[fieldId];
|
|
312
|
+
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
313
|
+
if (associatedObject?.propertyId === fieldId &&
|
|
314
|
+
associatedObject?.instanceId &&
|
|
315
|
+
parameter &&
|
|
316
|
+
action?.type === 'create') {
|
|
317
|
+
try {
|
|
318
|
+
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
|
|
319
|
+
result[associatedObject.propertyId] = instance;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.error(error);
|
|
341
323
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
324
|
+
}
|
|
325
|
+
else if (entry.type !== 'readonlyField' && isEmptyWithDefault(fieldValue, entry, instanceData)) {
|
|
326
|
+
if (fieldId && parameters && parameters.length > 0) {
|
|
327
|
+
const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
|
|
328
|
+
for (const { fieldId, fieldValue } of defaultValuesArray) {
|
|
329
|
+
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
330
|
+
if (parameter?.type === 'object') {
|
|
331
|
+
const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
|
|
332
|
+
for (const field of dependentFields) {
|
|
333
|
+
set(result, field.fieldId, field.fieldValue);
|
|
353
334
|
}
|
|
354
|
-
set(result, fieldId, fieldValue);
|
|
355
335
|
}
|
|
336
|
+
set(result, fieldId, fieldValue);
|
|
356
337
|
}
|
|
357
338
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
result[fieldId] = RTFFieldValue;
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
result[fieldId] = fieldValue;
|
|
339
|
+
}
|
|
340
|
+
else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
|
|
341
|
+
result[fieldId] = false;
|
|
342
|
+
}
|
|
343
|
+
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
344
|
+
if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
|
|
345
|
+
let RTFFieldValue = fieldValue;
|
|
346
|
+
if (!fieldValue.trim().startsWith('{\\rtf')) {
|
|
347
|
+
RTFFieldValue = plainTextToRtf(fieldValue);
|
|
371
348
|
}
|
|
349
|
+
result[fieldId] = RTFFieldValue;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
result[fieldId] = fieldValue;
|
|
372
353
|
}
|
|
373
354
|
}
|
|
374
355
|
}
|
|
375
356
|
}
|
|
376
|
-
}
|
|
377
|
-
await processEntries(entries);
|
|
357
|
+
}
|
|
378
358
|
return result;
|
|
379
359
|
};
|
|
360
|
+
const handleAutosave = async (fieldId) => {
|
|
361
|
+
if (!form?.autosaveActionId || !formDataRef.current) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const currentValue = get(formDataRef.current, fieldId);
|
|
365
|
+
const lastValue = get(lastSavedData, fieldId);
|
|
366
|
+
if (isEqual(currentValue, lastValue)) {
|
|
367
|
+
return; // Field hasn't changed, skip save
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
setIsSaving(true);
|
|
371
|
+
const submission = await formatSubmission(formDataRef.current, apiServices, objectId, instanceId, form, setSnackbarError);
|
|
372
|
+
// Handle document autosave
|
|
373
|
+
if (dataType === 'documents' && document) {
|
|
374
|
+
await apiServices.patch(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/documents/${documentId}`), pick(submission, ['metadata']).metadata ?? submission);
|
|
375
|
+
setDocument((prev) => ({
|
|
376
|
+
...prev,
|
|
377
|
+
metadata: submission.metadata,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
// Handle object instance autosave
|
|
381
|
+
else if (instanceId && action?.type === 'update') {
|
|
382
|
+
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
383
|
+
actionId: form.autosaveActionId,
|
|
384
|
+
input: pick(submission, sanitizedObject?.properties
|
|
385
|
+
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
386
|
+
.map((property) => property.id) ?? []),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
setLastSavedData(cloneDeep(formDataRef.current));
|
|
390
|
+
setIsSaving(false);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.error('Autosave failed:', error);
|
|
394
|
+
setIsSaving(false);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
// Autosave is enabled if form.autosaveActionId exists.
|
|
398
|
+
const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
|
|
380
399
|
async function onChange(id, value) {
|
|
381
400
|
const parameter = parameters?.find((param) => param.id === id);
|
|
382
401
|
const entries = getUnnestedEntries(form.entries);
|
|
@@ -387,7 +406,7 @@ function FormRendererContainer(props) {
|
|
|
387
406
|
if (parameter) {
|
|
388
407
|
if (parameter.type === 'object' && parameters && parameters.length > 0) {
|
|
389
408
|
// On change of a related object, update default values dependent on that object
|
|
390
|
-
const dependentFields = await processValueUpdate(
|
|
409
|
+
const dependentFields = await processValueUpdate(entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
|
|
391
410
|
for (const field of dependentFields) {
|
|
392
411
|
onChange(field.fieldId, field.fieldValue);
|
|
393
412
|
}
|
|
@@ -398,16 +417,21 @@ function FormRendererContainer(props) {
|
|
|
398
417
|
value = value.value ? value.value : value;
|
|
399
418
|
}
|
|
400
419
|
}
|
|
401
|
-
if (!isEqual(value, get(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return newData;
|
|
406
|
-
});
|
|
420
|
+
if (!isEqual(value, get(formDataRef.current, id))) {
|
|
421
|
+
const newData = { ...formDataRef.current };
|
|
422
|
+
set(newData, id, value);
|
|
423
|
+
setFormData(newData);
|
|
407
424
|
}
|
|
408
425
|
}
|
|
409
|
-
const isLoading = (instanceId && !
|
|
426
|
+
const isLoading = (instanceId && !formDataRef.current && !document) || !form || !sanitizedObject;
|
|
410
427
|
const status = error ? 'error' : isLoading ? 'loading' : 'ready';
|
|
428
|
+
// Compose a header renderer that injects the saving indicator into the rendered header
|
|
429
|
+
const composedRenderHeader = (props) => {
|
|
430
|
+
if (renderHeader) {
|
|
431
|
+
return renderHeader({ ...props, autosaving: !!form?.autosaveActionId && isSaving });
|
|
432
|
+
}
|
|
433
|
+
return React.createElement(Header, { ...props, autosaving: !!form?.autosaveActionId && isSaving });
|
|
434
|
+
};
|
|
411
435
|
const onDiscardChanges = onDiscardChangesOverride
|
|
412
436
|
? onDiscardChangesOverride
|
|
413
437
|
: async () => {
|
|
@@ -426,7 +450,8 @@ function FormRendererContainer(props) {
|
|
|
426
450
|
padding: '0px',
|
|
427
451
|
border: !isLoading ? '1px solid #dbe0e4' : undefined,
|
|
428
452
|
...sx,
|
|
429
|
-
} }, !isLoading ? (React.createElement(React.Fragment, null,
|
|
453
|
+
} }, !isLoading ? (React.createElement(React.Fragment, null,
|
|
454
|
+
React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: dataType !== 'documents' ? instance : document, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: document && !hasDocumentUpdateAccess ? () => React.createElement(React.Fragment, null) : renderFooter }))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
430
455
|
React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
|
|
431
456
|
React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
|
|
432
457
|
React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
|
|
@@ -4,10 +4,11 @@ import React, { useEffect } from 'react';
|
|
|
4
4
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
5
5
|
import { Accordion, AccordionDetails, AccordionSummary, Typography } from '../../../core';
|
|
6
6
|
import { Box } from '../../../layout';
|
|
7
|
+
import { ViewOnlyEntryRenderer } from '../../ViewDetailsV2';
|
|
7
8
|
import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
|
|
8
9
|
import { getErrorCountForSection } from './utils';
|
|
9
10
|
function AccordionSections(props) {
|
|
10
|
-
const { entry } = props;
|
|
11
|
+
const { entry, readOnly } = props;
|
|
11
12
|
const { errors, expandedSections, setExpandedSections, expandAll, setExpandAll, showSubmitError, width } = useFormContext();
|
|
12
13
|
const { isAbove } = useWidgetSize({
|
|
13
14
|
scroll: false,
|
|
@@ -92,6 +93,8 @@ function AccordionSections(props) {
|
|
|
92
93
|
'&:before': {
|
|
93
94
|
display: 'none',
|
|
94
95
|
},
|
|
96
|
+
...(sectionIndex === lastSection && { marginBottom: '16px !important' }),
|
|
97
|
+
...(sectionIndex === 0 && { marginTop: '16px !important' }),
|
|
95
98
|
} },
|
|
96
99
|
React.createElement(AccordionSummary, { sx: {
|
|
97
100
|
'&.Mui-expanded': {
|
|
@@ -133,7 +136,9 @@ function AccordionSections(props) {
|
|
|
133
136
|
margin: '0px',
|
|
134
137
|
marginRight: '16px',
|
|
135
138
|
} }, errorCount)))),
|
|
136
|
-
React.createElement(AccordionDetails, null,
|
|
139
|
+
React.createElement(AccordionDetails, null, readOnly
|
|
140
|
+
? section.entries?.map((sectionEntry, index) => (React.createElement(ViewOnlyEntryRenderer, { key: sectionEntry.type + index, entry: sectionEntry })))
|
|
141
|
+
: section.entries?.map((sectionEntry, index) => (React.createElement(RecursiveEntryRenderer, { key: sectionEntry.type + index, entry: sectionEntry }))))));
|
|
137
142
|
})));
|
|
138
143
|
}
|
|
139
144
|
export default AccordionSections;
|
|
@@ -7,7 +7,7 @@ export type BodyProps = {
|
|
|
7
7
|
entries: FormEntry[];
|
|
8
8
|
isInitializing: boolean;
|
|
9
9
|
errors?: FieldErrors;
|
|
10
|
-
shouldShowValidationErrors
|
|
10
|
+
shouldShowValidationErrors?: boolean;
|
|
11
11
|
hasAccordions: boolean;
|
|
12
12
|
expandedSections?: ExpandedSection[];
|
|
13
13
|
onExpandAll?: () => void;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ApiServices, FormEntry, InputField, InputParameter, InputParameterReference, ObjectInstance, Reference, UserAccount } from '@evoke-platform/context';
|
|
2
2
|
import { FieldValues } from 'react-hook-form';
|
|
3
|
-
export declare function evalDefaultVals(parameters: InputParameter[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
|
|
3
|
+
export declare function evalDefaultVals(parameters: InputParameter[], unnestedEntries: FormEntry[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
|
|
4
4
|
fieldId: string;
|
|
5
5
|
fieldValue: unknown;
|
|
6
6
|
}[]>;
|
|
7
|
-
export declare function processValueUpdate(
|
|
7
|
+
export declare function processValueUpdate(unnestedEntries: FormEntry[], parameters: InputParameter[], updatedRelatedObjectValue: ObjectInstance | null | Reference, apiServices: ApiServices, changedEntryId?: string, formValues?: FieldValues, userAccount?: UserAccount): Promise<{
|
|
8
8
|
fieldId: string;
|
|
9
9
|
fieldValue: unknown;
|
|
10
10
|
}[]>;
|