@evoke-platform/ui-components 1.10.0-testing.9 → 1.10.1-dev.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 +24 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
- 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 +9 -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 +127 -92
- 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 +411 -44
- 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 +10 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useObject } from '@evoke-platform/context';
|
|
2
2
|
import { isEmpty, isEqual, omit } from 'lodash';
|
|
3
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useForm } from 'react-hook-form';
|
|
5
5
|
import { useWidgetSize } from '../../../theme';
|
|
6
6
|
import { Box } from '../../layout';
|
|
@@ -8,11 +8,11 @@ import { Body } from './components/Body';
|
|
|
8
8
|
import { Footer, FooterActions } from './components/Footer';
|
|
9
9
|
import { FormContext } from './components/FormContext';
|
|
10
10
|
import Header, { AccordionActions, Title } from './components/Header';
|
|
11
|
-
import { assignIdsToSectionsAndRichText, convertDocToParameters, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, isAddressProperty, } from './components/utils';
|
|
11
|
+
import { assignIdsToSectionsAndRichText, convertDocToParameters, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, isAddressProperty, obfuscateValue, } from './components/utils';
|
|
12
12
|
import { handleValidation } from './components/ValidationFiles/Validation';
|
|
13
13
|
import ValidationErrors from './components/ValidationFiles/ValidationErrors';
|
|
14
14
|
const FormRendererInternal = (props) => {
|
|
15
|
-
const { onSubmit, onDiscardChanges, onSubmitError, value, fieldHeight, richTextEditor, form, instance, onChange, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
15
|
+
const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, fieldHeight, richTextEditor, form, instance, onChange, onAutosave, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
16
16
|
const { entries, name: title, objectId, actionId, display } = form;
|
|
17
17
|
const { register, unregister, setValue, reset, handleSubmit, formState: { errors, isSubmitted }, getValues, } = useForm({
|
|
18
18
|
defaultValues: value,
|
|
@@ -32,6 +32,7 @@ const FormRendererInternal = (props) => {
|
|
|
32
32
|
const [isInitializing, setIsInitializing] = useState(true);
|
|
33
33
|
const [parameters, setParameters] = useState();
|
|
34
34
|
const objectStore = useObject(objectId);
|
|
35
|
+
const validationContainerRef = useRef(null);
|
|
35
36
|
const updateFetchedOptions = (newData) => {
|
|
36
37
|
setFetchedOptions((prev) => ({
|
|
37
38
|
...prev,
|
|
@@ -45,7 +46,7 @@ const FormRendererInternal = (props) => {
|
|
|
45
46
|
setExpandAll(false);
|
|
46
47
|
}
|
|
47
48
|
const updatedEntries = useMemo(() => {
|
|
48
|
-
return assignIdsToSectionsAndRichText(entries, object, parameters);
|
|
49
|
+
return object ? assignIdsToSectionsAndRichText(entries, object, parameters) : [];
|
|
49
50
|
}, [entries, object, parameters]);
|
|
50
51
|
useEffect(() => {
|
|
51
52
|
(async () => {
|
|
@@ -78,6 +79,15 @@ const FormRendererInternal = (props) => {
|
|
|
78
79
|
if (value) {
|
|
79
80
|
for (const key of Object.keys(currentValues)) {
|
|
80
81
|
if (!isEqual(currentValues[key], value[key])) {
|
|
82
|
+
// For protected properties, don't validate initial obfuscated value
|
|
83
|
+
const property = object?.properties?.find((prop) => prop.id === key);
|
|
84
|
+
const isProtectedProperty = property?.protection?.maskChar;
|
|
85
|
+
if (isProtectedProperty) {
|
|
86
|
+
if (value[key] === obfuscateValue(value[key], property)) {
|
|
87
|
+
setValue(key, value[key], { shouldValidate: false });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
81
91
|
setValue(key, value[key], { shouldValidate: true });
|
|
82
92
|
}
|
|
83
93
|
}
|
|
@@ -108,7 +118,7 @@ const FormRendererInternal = (props) => {
|
|
|
108
118
|
}
|
|
109
119
|
});
|
|
110
120
|
}
|
|
111
|
-
if (!entryIsVisible(entry, getValues()
|
|
121
|
+
if (!entryIsVisible(entry, instance, getValues())) {
|
|
112
122
|
if (entry.type === 'sections' || entry.type === 'columns') {
|
|
113
123
|
const fieldsToUnregister = getNestedParameterIds(entry);
|
|
114
124
|
fieldsToUnregister.forEach(processFieldUnregister);
|
|
@@ -121,6 +131,22 @@ const FormRendererInternal = (props) => {
|
|
|
121
131
|
}
|
|
122
132
|
});
|
|
123
133
|
};
|
|
134
|
+
const removeUneditedProtectedValues = () => {
|
|
135
|
+
const protectedProperties = object?.properties?.filter((prop) => prop.protection?.maskChar);
|
|
136
|
+
if (!protectedProperties || protectedProperties.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
protectedProperties.forEach((property) => {
|
|
140
|
+
const fieldId = property.id;
|
|
141
|
+
const originalValue = instance?.[fieldId];
|
|
142
|
+
const value = getValues(fieldId);
|
|
143
|
+
// When protected value hasn't been edited or viewed, unregister to
|
|
144
|
+
// avoid saving the obfuscated value.
|
|
145
|
+
if (value === originalValue) {
|
|
146
|
+
processFieldUnregister(fieldId);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
};
|
|
124
150
|
const processFieldUnregister = (fieldId) => {
|
|
125
151
|
if (isAddressProperty(fieldId)) {
|
|
126
152
|
// Unregister entire addressObject to clear hidden field errors, then restore existing values since unregistering address.line1 etc is not working
|
|
@@ -134,9 +160,18 @@ const FormRendererInternal = (props) => {
|
|
|
134
160
|
unregister(fieldId);
|
|
135
161
|
}
|
|
136
162
|
};
|
|
163
|
+
const onSubmitError = (errors) => {
|
|
164
|
+
if (onSubmitErrorOverride) {
|
|
165
|
+
onSubmitErrorOverride(errors);
|
|
166
|
+
}
|
|
167
|
+
else if (validationContainerRef.current) {
|
|
168
|
+
validationContainerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
137
171
|
async function unregisterHiddenFieldsAndSubmit() {
|
|
138
172
|
unregisterHiddenFields(entries ?? []);
|
|
139
|
-
|
|
173
|
+
removeUneditedProtectedValues();
|
|
174
|
+
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError(errors))();
|
|
140
175
|
}
|
|
141
176
|
const headerProps = {
|
|
142
177
|
title,
|
|
@@ -146,8 +181,9 @@ const FormRendererInternal = (props) => {
|
|
|
146
181
|
errors,
|
|
147
182
|
hasAccordions: hasSections && isSmallerThanMd,
|
|
148
183
|
shouldShowValidationErrors: isSubmitted,
|
|
149
|
-
form,
|
|
150
184
|
action,
|
|
185
|
+
validationContainerRef: validationContainerRef,
|
|
186
|
+
autosaveEnabled: !!form.autosaveActionId,
|
|
151
187
|
};
|
|
152
188
|
const footerProps = {
|
|
153
189
|
onSubmit: unregisterHiddenFieldsAndSubmit,
|
|
@@ -155,6 +191,7 @@ const FormRendererInternal = (props) => {
|
|
|
155
191
|
action,
|
|
156
192
|
discardChangesButtonLabel: 'Discard Changes',
|
|
157
193
|
submitButtonLabel: display?.submitLabel ?? 'Submit',
|
|
194
|
+
disableDiscardChanges: !!form?.autosaveActionId,
|
|
158
195
|
};
|
|
159
196
|
return (React.createElement(Box, { ref: containerRef },
|
|
160
197
|
React.createElement(FormContext.Provider, { value: {
|
|
@@ -172,6 +209,7 @@ const FormRendererInternal = (props) => {
|
|
|
172
209
|
parameters,
|
|
173
210
|
fieldHeight,
|
|
174
211
|
handleChange: onChange,
|
|
212
|
+
onAutosave,
|
|
175
213
|
triggerFieldReset,
|
|
176
214
|
showSubmitError: isSubmitted,
|
|
177
215
|
associatedObject,
|
|
@@ -200,7 +238,7 @@ const FormRendererInternal = (props) => {
|
|
|
200
238
|
expandedSections,
|
|
201
239
|
hasAccordions: hasSections && isSmallerThanMd,
|
|
202
240
|
} })),
|
|
203
|
-
(
|
|
241
|
+
(action || form.id === 'documentForm') &&
|
|
204
242
|
onSubmit &&
|
|
205
243
|
(renderFooter ? renderFooter(footerProps) : React.createElement(Footer, { ...footerProps }))))));
|
|
206
244
|
};
|
|
@@ -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;
|