@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
|
@@ -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';
|
|
@@ -12,7 +12,7 @@ import { assignIdsToSectionsAndRichText, convertDocToParameters, convertProperti
|
|
|
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,
|
|
@@ -134,9 +135,17 @@ const FormRendererInternal = (props) => {
|
|
|
134
135
|
unregister(fieldId);
|
|
135
136
|
}
|
|
136
137
|
};
|
|
138
|
+
const onSubmitError = (errors) => {
|
|
139
|
+
if (onSubmitErrorOverride) {
|
|
140
|
+
onSubmitErrorOverride(errors);
|
|
141
|
+
}
|
|
142
|
+
else if (validationContainerRef.current) {
|
|
143
|
+
validationContainerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
137
146
|
async function unregisterHiddenFieldsAndSubmit() {
|
|
138
147
|
unregisterHiddenFields(entries ?? []);
|
|
139
|
-
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError
|
|
148
|
+
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError(errors))();
|
|
140
149
|
}
|
|
141
150
|
const headerProps = {
|
|
142
151
|
title,
|
|
@@ -146,8 +155,10 @@ const FormRendererInternal = (props) => {
|
|
|
146
155
|
errors,
|
|
147
156
|
hasAccordions: hasSections && isSmallerThanMd,
|
|
148
157
|
shouldShowValidationErrors: isSubmitted,
|
|
149
|
-
form,
|
|
158
|
+
isDeleteForm: form.id === '',
|
|
150
159
|
action,
|
|
160
|
+
validationContainerRef: validationContainerRef,
|
|
161
|
+
autosaveEnabled: !!form.autosaveActionId,
|
|
151
162
|
};
|
|
152
163
|
const footerProps = {
|
|
153
164
|
onSubmit: unregisterHiddenFieldsAndSubmit,
|
|
@@ -155,6 +166,7 @@ const FormRendererInternal = (props) => {
|
|
|
155
166
|
action,
|
|
156
167
|
discardChangesButtonLabel: 'Discard Changes',
|
|
157
168
|
submitButtonLabel: display?.submitLabel ?? 'Submit',
|
|
169
|
+
disableDiscardChanges: !!form?.autosaveActionId,
|
|
158
170
|
};
|
|
159
171
|
return (React.createElement(Box, { ref: containerRef },
|
|
160
172
|
React.createElement(FormContext.Provider, { value: {
|
|
@@ -172,6 +184,7 @@ const FormRendererInternal = (props) => {
|
|
|
172
184
|
parameters,
|
|
173
185
|
fieldHeight,
|
|
174
186
|
handleChange: onChange,
|
|
187
|
+
onAutosave,
|
|
175
188
|
triggerFieldReset,
|
|
176
189
|
showSubmitError: isSubmitted,
|
|
177
190
|
associatedObject,
|
|
@@ -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, merge, omit, pick, set, uniq } from 'lodash';
|
|
4
|
-
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { cloneDeep, get, isArray, isEmpty, isEqual, merge, 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) => {
|
|
@@ -59,6 +79,10 @@ function FormRendererContainer(props) {
|
|
|
59
79
|
const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
60
80
|
if (action && (instanceId || action.type === 'create')) {
|
|
61
81
|
setAction(action);
|
|
82
|
+
// Clear error if action is found after being missing
|
|
83
|
+
// TODO: This entire effect should take place after form is fetched to avoid an error flickering
|
|
84
|
+
// That is, this effect should be merged with the one below that fetches the form
|
|
85
|
+
setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
|
|
62
86
|
}
|
|
63
87
|
else {
|
|
64
88
|
setError('Action could not be found');
|
|
@@ -78,28 +102,21 @@ function FormRendererContainer(props) {
|
|
|
78
102
|
setNavigationSlug(page?.slug);
|
|
79
103
|
});
|
|
80
104
|
}
|
|
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
105
|
}, []);
|
|
95
106
|
useEffect(() => {
|
|
107
|
+
const needsInstance = action?.type !== 'create' && !!instanceId;
|
|
108
|
+
// Instance and Action are loaded in the side effect above; wait for them to complete.
|
|
109
|
+
const loading = (actionId && !action) || (needsInstance && !instance);
|
|
96
110
|
if (dataType === 'documents' || form)
|
|
97
111
|
return;
|
|
98
|
-
if (
|
|
112
|
+
if (loading)
|
|
113
|
+
return;
|
|
114
|
+
if ((formId || action?.defaultFormId) && formId !== '_auto_') {
|
|
99
115
|
apiServices
|
|
100
116
|
.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
|
|
101
117
|
.then((evokeForm) => {
|
|
102
|
-
|
|
118
|
+
// If an actionId is provided, ensure it matches the form's actionId
|
|
119
|
+
if (!actionId || evokeForm?.actionId === actionId) {
|
|
103
120
|
const form = evokeForm;
|
|
104
121
|
setForm(form);
|
|
105
122
|
}
|
|
@@ -111,50 +128,27 @@ function FormRendererContainer(props) {
|
|
|
111
128
|
onError(error);
|
|
112
129
|
});
|
|
113
130
|
}
|
|
114
|
-
else if (action) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
131
|
+
else if (action?.type === 'delete' && formId === '_auto_') {
|
|
132
|
+
setForm({
|
|
133
|
+
id: '',
|
|
134
|
+
name: '',
|
|
135
|
+
entries: [
|
|
136
|
+
{
|
|
137
|
+
type: 'content',
|
|
138
|
+
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>`,
|
|
123
139
|
},
|
|
140
|
+
],
|
|
141
|
+
objectId: objectId,
|
|
142
|
+
actionId: '_delete',
|
|
143
|
+
display: {
|
|
144
|
+
submitLabel: 'Delete',
|
|
124
145
|
},
|
|
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
146
|
});
|
|
156
147
|
}
|
|
157
|
-
|
|
148
|
+
else {
|
|
149
|
+
setError('Action form could not be found');
|
|
150
|
+
}
|
|
151
|
+
}, [action, actionId, objectId, instance]);
|
|
158
152
|
useEffect(() => {
|
|
159
153
|
if (form?.id === 'documentForm') {
|
|
160
154
|
setParameters([
|
|
@@ -184,6 +178,8 @@ function FormRendererContainer(props) {
|
|
|
184
178
|
if (document && objectId) {
|
|
185
179
|
const defaultValues = await getDefaultValues(convertDocToEntries(document), document);
|
|
186
180
|
setFormData(defaultValues);
|
|
181
|
+
// Deep clone to avoid reference issues
|
|
182
|
+
setLastSavedData(cloneDeep(defaultValues));
|
|
187
183
|
if (!form) {
|
|
188
184
|
setForm({
|
|
189
185
|
id: 'documentForm',
|
|
@@ -196,6 +192,8 @@ function FormRendererContainer(props) {
|
|
|
196
192
|
else if (form && (instance || !instanceId)) {
|
|
197
193
|
const defaultValues = await getDefaultValues(form.entries, instance || {});
|
|
198
194
|
setFormData(defaultValues);
|
|
195
|
+
// Deep clone to avoid reference issues
|
|
196
|
+
setLastSavedData(cloneDeep(defaultValues));
|
|
199
197
|
}
|
|
200
198
|
};
|
|
201
199
|
getInitialValues();
|
|
@@ -245,8 +243,8 @@ function FormRendererContainer(props) {
|
|
|
245
243
|
else if (action?.type === 'create') {
|
|
246
244
|
const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
|
|
247
245
|
actionId: form.actionId,
|
|
248
|
-
input:
|
|
249
|
-
?.filter((property) =>
|
|
246
|
+
input: omit(submission, sanitizedObject?.properties
|
|
247
|
+
?.filter((property) => property.formula || property.type === 'collection')
|
|
250
248
|
.map((property) => property.id) ?? []),
|
|
251
249
|
});
|
|
252
250
|
if (response) {
|
|
@@ -256,8 +254,8 @@ function FormRendererContainer(props) {
|
|
|
256
254
|
else if (instanceId && action) {
|
|
257
255
|
const response = await objectStore.instanceAction(instanceId, {
|
|
258
256
|
actionId: action.id,
|
|
259
|
-
input:
|
|
260
|
-
?.filter((property) =>
|
|
257
|
+
input: omit(submission, sanitizedObject?.properties
|
|
258
|
+
?.filter((property) => property.formula || property.type === 'collection')
|
|
261
259
|
.map((property) => property.id) ?? []),
|
|
262
260
|
});
|
|
263
261
|
if (sanitizedObject && instance) {
|
|
@@ -346,7 +344,7 @@ function FormRendererContainer(props) {
|
|
|
346
344
|
for (const { fieldId, fieldValue } of defaultValuesArray) {
|
|
347
345
|
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
348
346
|
if (parameter?.type === 'object') {
|
|
349
|
-
const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId,
|
|
347
|
+
const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
|
|
350
348
|
for (const field of dependentFields) {
|
|
351
349
|
set(result, field.fieldId, field.fieldValue);
|
|
352
350
|
}
|
|
@@ -377,6 +375,45 @@ function FormRendererContainer(props) {
|
|
|
377
375
|
await processEntries(entries);
|
|
378
376
|
return result;
|
|
379
377
|
};
|
|
378
|
+
const handleAutosave = async (fieldId) => {
|
|
379
|
+
if (!form?.autosaveActionId || !formDataRef.current) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const currentValue = get(formDataRef.current, fieldId);
|
|
383
|
+
const lastValue = get(lastSavedData, fieldId);
|
|
384
|
+
if (isEqual(currentValue, lastValue)) {
|
|
385
|
+
return; // Field hasn't changed, skip save
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
setIsSaving(true);
|
|
389
|
+
const submission = await formatSubmission(formDataRef.current, apiServices, objectId, instanceId, form, setSnackbarError);
|
|
390
|
+
// Handle document autosave
|
|
391
|
+
if (dataType === 'documents' && document) {
|
|
392
|
+
await apiServices.patch(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/documents/${documentId}`), pick(submission, ['metadata']).metadata ?? submission);
|
|
393
|
+
setDocument((prev) => ({
|
|
394
|
+
...prev,
|
|
395
|
+
metadata: submission.metadata,
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
// Handle object instance autosave
|
|
399
|
+
else if (instanceId && action?.type === 'update') {
|
|
400
|
+
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
401
|
+
actionId: form.autosaveActionId,
|
|
402
|
+
input: pick(submission, sanitizedObject?.properties
|
|
403
|
+
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
404
|
+
.map((property) => property.id) ?? []),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
setLastSavedData(cloneDeep(formDataRef.current));
|
|
408
|
+
setIsSaving(false);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
console.error('Autosave failed:', error);
|
|
412
|
+
setIsSaving(false);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
// Autosave is enabled if form.autosaveActionId exists.
|
|
416
|
+
const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
|
|
380
417
|
async function onChange(id, value) {
|
|
381
418
|
const parameter = parameters?.find((param) => param.id === id);
|
|
382
419
|
const entries = getUnnestedEntries(form.entries);
|
|
@@ -387,7 +424,7 @@ function FormRendererContainer(props) {
|
|
|
387
424
|
if (parameter) {
|
|
388
425
|
if (parameter.type === 'object' && parameters && parameters.length > 0) {
|
|
389
426
|
// On change of a related object, update default values dependent on that object
|
|
390
|
-
const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id,
|
|
427
|
+
const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
|
|
391
428
|
for (const field of dependentFields) {
|
|
392
429
|
onChange(field.fieldId, field.fieldValue);
|
|
393
430
|
}
|
|
@@ -398,16 +435,21 @@ function FormRendererContainer(props) {
|
|
|
398
435
|
value = value.value ? value.value : value;
|
|
399
436
|
}
|
|
400
437
|
}
|
|
401
|
-
if (!isEqual(value, get(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return newData;
|
|
406
|
-
});
|
|
438
|
+
if (!isEqual(value, get(formDataRef.current, id))) {
|
|
439
|
+
const newData = { ...formDataRef.current };
|
|
440
|
+
set(newData, id, value);
|
|
441
|
+
setFormData(newData);
|
|
407
442
|
}
|
|
408
443
|
}
|
|
409
|
-
const isLoading = (instanceId && !
|
|
444
|
+
const isLoading = (instanceId && !formDataRef.current && !document) || !form || !sanitizedObject;
|
|
410
445
|
const status = error ? 'error' : isLoading ? 'loading' : 'ready';
|
|
446
|
+
// Compose a header renderer that injects the saving indicator into the rendered header
|
|
447
|
+
const composedRenderHeader = (props) => {
|
|
448
|
+
if (renderHeader) {
|
|
449
|
+
return renderHeader({ ...props, autosaving: !!form?.autosaveActionId && isSaving });
|
|
450
|
+
}
|
|
451
|
+
return React.createElement(Header, { ...props, autosaving: !!form?.autosaveActionId && isSaving });
|
|
452
|
+
};
|
|
411
453
|
const onDiscardChanges = onDiscardChangesOverride
|
|
412
454
|
? onDiscardChangesOverride
|
|
413
455
|
: async () => {
|
|
@@ -426,7 +468,7 @@ function FormRendererContainer(props) {
|
|
|
426
468
|
padding: '0px',
|
|
427
469
|
border: !isLoading ? '1px solid #dbe0e4' : undefined,
|
|
428
470
|
...sx,
|
|
429
|
-
} }, !isLoading ? (React.createElement(React.Fragment, null, form && sanitizedObject && (React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value:
|
|
471
|
+
} }, !isLoading ? (React.createElement(React.Fragment, null, form && sanitizedObject && (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
472
|
React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
|
|
431
473
|
React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
|
|
432
474
|
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;
|
|
@@ -55,7 +55,7 @@ const FieldWrapper = (props) => {
|
|
|
55
55
|
const remainingChars = maxLength ? maxLength - charCount : undefined;
|
|
56
56
|
return (React.createElement(Box, null,
|
|
57
57
|
React.createElement(Box, { sx: { padding: '10px 0' } },
|
|
58
|
-
inputType !== 'boolean' && (React.createElement(InputLabel, { htmlFor: inputId, sx: {
|
|
58
|
+
(inputType !== 'boolean' || viewOnly) && (React.createElement(InputLabel, { htmlFor: inputId, sx: {
|
|
59
59
|
display: 'flex',
|
|
60
60
|
alignItems: 'center',
|
|
61
61
|
color: viewOnly ? 'text.secondary' : 'text.primary',
|
|
@@ -8,6 +8,7 @@ export type FooterProps = {
|
|
|
8
8
|
submitButtonLabel?: string;
|
|
9
9
|
discardChangesButtonLabel?: string;
|
|
10
10
|
sx?: SxProps;
|
|
11
|
+
disableDiscardChanges?: boolean;
|
|
11
12
|
};
|
|
12
13
|
export declare const Footer: React.FC<FooterProps>;
|
|
13
14
|
export type FooterActionsProps = Omit<FooterProps, 'sx'>;
|
|
@@ -28,7 +28,7 @@ export const Footer = (props) => {
|
|
|
28
28
|
React.createElement(FooterActions, { ...props })));
|
|
29
29
|
};
|
|
30
30
|
export const FooterActions = (props) => {
|
|
31
|
-
const { action, onDiscardChanges, onSubmit, submitButtonLabel, discardChangesButtonLabel } = props;
|
|
31
|
+
const { action, onDiscardChanges, onSubmit, submitButtonLabel, discardChangesButtonLabel, disableDiscardChanges } = props;
|
|
32
32
|
const { width } = useContext(FormContext);
|
|
33
33
|
const [loading, setLoading] = React.useState(false);
|
|
34
34
|
const handleSubmit = async () => {
|
|
@@ -46,7 +46,7 @@ export const FooterActions = (props) => {
|
|
|
46
46
|
});
|
|
47
47
|
const { isXs } = breakpoints;
|
|
48
48
|
return (React.createElement(React.Fragment, null,
|
|
49
|
-
React.createElement(Button, { onClick: onDiscardChanges, variant: "outlined", sx: {
|
|
49
|
+
!disableDiscardChanges && (React.createElement(Button, { onClick: onDiscardChanges, variant: "outlined", sx: {
|
|
50
50
|
margin: '5px',
|
|
51
51
|
marginX: isXs ? '0px' : undefined,
|
|
52
52
|
color: 'black',
|
|
@@ -56,7 +56,7 @@ export const FooterActions = (props) => {
|
|
|
56
56
|
backgroundColor: '#f2f4f7',
|
|
57
57
|
border: '1px solid rgb(206, 212, 218)',
|
|
58
58
|
},
|
|
59
|
-
} }, discardChangesButtonLabel),
|
|
59
|
+
} }, discardChangesButtonLabel)),
|
|
60
60
|
React.createElement(LoadingButton, { onClick: handleSubmit, variant: "contained", sx: {
|
|
61
61
|
lineHeight: '2.75',
|
|
62
62
|
margin: '5px 0 5px 5px',
|
|
@@ -5,7 +5,7 @@ import { ExpandedSection, SimpleEditorProps } from './types';
|
|
|
5
5
|
type FormContextType = {
|
|
6
6
|
fetchedOptions: FieldValues;
|
|
7
7
|
setFetchedOptions: (newData: FieldValues) => void;
|
|
8
|
-
getValues
|
|
8
|
+
getValues?: UseFormGetValues<FieldValues>;
|
|
9
9
|
object?: Obj;
|
|
10
10
|
errors?: FieldValues;
|
|
11
11
|
instance?: FieldValues;
|
|
@@ -15,7 +15,8 @@ type FormContextType = {
|
|
|
15
15
|
setExpandedSections?: React.Dispatch<React.SetStateAction<ExpandedSection[]>>;
|
|
16
16
|
setExpandAll?: React.Dispatch<React.SetStateAction<boolean | undefined | null>>;
|
|
17
17
|
parameters?: InputParameter[];
|
|
18
|
-
handleChange
|
|
18
|
+
handleChange?: (name: string, value: unknown) => void | Promise<void>;
|
|
19
|
+
onAutosave?: (fieldId: string) => void | Promise<void>;
|
|
19
20
|
fieldHeight?: 'small' | 'medium';
|
|
20
21
|
triggerFieldReset?: boolean;
|
|
21
22
|
showSubmitError?: boolean;
|
package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts
CHANGED
|
@@ -2,7 +2,16 @@ import { InputField, InputParameter, InputParameterReference, Property, Readonly
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
interface AddressProps {
|
|
4
4
|
entry: InputParameterReference | ReadonlyField | InputField;
|
|
5
|
+
/**
|
|
6
|
+
* Indicates that the field is a readonlyField in an action form.
|
|
7
|
+
* Used for regular form read-only fields.
|
|
8
|
+
*/
|
|
5
9
|
readOnly?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Indicates that the field should not have a gray background.
|
|
12
|
+
* Used for ViewDetails widgets.
|
|
13
|
+
*/
|
|
14
|
+
viewOnly?: boolean;
|
|
6
15
|
entryId: string;
|
|
7
16
|
fieldDefinition: InputParameter | Property;
|
|
8
17
|
}
|
|
@@ -6,12 +6,12 @@ import FormField from '../../../FormField';
|
|
|
6
6
|
import FieldWrapper from '../FieldWrapper';
|
|
7
7
|
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
8
8
|
function AddressFields(props) {
|
|
9
|
-
const { entry, readOnly, entryId, fieldDefinition } = props;
|
|
10
|
-
const { getValues, instance, errors, handleChange, fieldHeight, parameters } = useFormContext();
|
|
9
|
+
const { entry, readOnly, viewOnly, entryId, fieldDefinition } = props;
|
|
10
|
+
const { getValues, instance, errors, handleChange, onAutosave, fieldHeight, parameters } = useFormContext();
|
|
11
11
|
const apiServices = useApiServices();
|
|
12
12
|
const addressObject = entryId.split('.')[0];
|
|
13
13
|
const addressField = entryId.split('.')[1];
|
|
14
|
-
const addressValues = entry.type === 'readonlyField' ? instance?.[addressObject] : getValues(addressObject);
|
|
14
|
+
const addressValues = entry.type === 'readonlyField' ? instance?.[addressObject] : getValues ? getValues(addressObject) : undefined;
|
|
15
15
|
const fieldValue = addressValues?.[addressField];
|
|
16
16
|
const display = entry?.display;
|
|
17
17
|
const validation = fieldDefinition?.validation
|
|
@@ -22,24 +22,41 @@ function AddressFields(props) {
|
|
|
22
22
|
params: { query: query },
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
|
-
const handleAddressChange = (name, value) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
25
|
+
const handleAddressChange = async (name, value) => {
|
|
26
|
+
try {
|
|
27
|
+
if (addressField === 'line1' && typeof value === 'object' && value.line1) {
|
|
28
|
+
const addressKeys = ['line1', 'city', 'county', 'state', 'zipCode'];
|
|
29
|
+
// Await each handleChange sequentially to ensure proper order
|
|
30
|
+
for (const key of addressKeys) {
|
|
31
|
+
const fullKey = `${addressObject}.${key}`;
|
|
32
|
+
if (parameters?.some((p) => p.id === fullKey)) {
|
|
33
|
+
const fieldValue = value[key];
|
|
34
|
+
handleChange && (await handleChange(fullKey, fieldValue));
|
|
35
|
+
}
|
|
33
36
|
}
|
|
34
|
-
|
|
37
|
+
// Autosave immediately after autocomplete fills all fields
|
|
38
|
+
try {
|
|
39
|
+
await onAutosave?.(entryId);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error('Autosave failed:', error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
handleChange && (await handleChange(name, value));
|
|
47
|
+
}
|
|
35
48
|
}
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('Failed to update field:', error);
|
|
38
51
|
}
|
|
39
52
|
};
|
|
40
53
|
const addressErrors = errors?.[addressObject];
|
|
41
54
|
const addressFieldError = addressErrors?.[addressField];
|
|
42
|
-
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: "string", label: display?.label || 'default', description: !readOnly ? display?.description : undefined, tooltip: display?.tooltip, value: fieldValue, maxLength: 'maxLength' in validation ? validation?.maxLength : 0, required: entry.display?.required || false, showCharCount: !readOnly && display?.charCount, viewOnly: !!readOnly, prefix: display?.prefix, suffix: display?.suffix }, !
|
|
55
|
+
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: "string", label: display?.label || 'default', description: !readOnly ? display?.description : undefined, tooltip: display?.tooltip, value: fieldValue, maxLength: 'maxLength' in validation ? validation?.maxLength : 0, required: entry.display?.required || false, showCharCount: !readOnly && display?.charCount, viewOnly: !!(viewOnly ?? readOnly), prefix: display?.prefix, suffix: display?.suffix }, !viewOnly ? (React.createElement(FormField, { property: fieldDefinition, defaultValue: fieldValue, onChange: handleAddressChange, onBlur: () => {
|
|
56
|
+
onAutosave?.(entryId)?.catch((error) => {
|
|
57
|
+
console.error('Autosave failed:', error);
|
|
58
|
+
});
|
|
59
|
+
}, isMultiLineText: !!display?.rowCount, readOnly: entry.type === 'readonlyField', ...(addressField === 'line1' && { queryAddresses }), mask: validation?.mask, placeholder: display?.placeholder, isOptionEqualToValue: isOptionEqualToValue, size: fieldHeight, error: !!addressFieldError, errorMessage: addressFieldError?.message, additionalProps: {
|
|
43
60
|
...(display?.description && {
|
|
44
61
|
inputProps: {
|
|
45
62
|
'aria-describedby': `${entryId}-description`,
|
|
@@ -87,7 +87,7 @@ export const ActionDialog = (props) => {
|
|
|
87
87
|
borderBottom: action.type === 'delete' ? undefined : '1px solid #e9ecef',
|
|
88
88
|
} },
|
|
89
89
|
action && hasAccess && !loading ? action?.name : '',
|
|
90
|
-
React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose },
|
|
90
|
+
React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose, "aria-label": "Close" },
|
|
91
91
|
React.createElement(Close, { fontSize: "small" })),
|
|
92
92
|
formHeaderProps.hasAccordions && React.createElement(AccordionActions, { ...formHeaderProps })));
|
|
93
93
|
}, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: {
|
|
@@ -6,9 +6,6 @@ export type ObjectPropertyInputProps = {
|
|
|
6
6
|
criteria?: object;
|
|
7
7
|
viewLayout?: ViewLayoutEntityReference;
|
|
8
8
|
entry: InputField | InputParameterReference | ReadonlyField;
|
|
9
|
-
createActionId?: string;
|
|
10
|
-
updateActionId?: string;
|
|
11
|
-
deleteActionId?: string;
|
|
12
9
|
};
|
|
13
10
|
declare const RepeatableField: (props: ObjectPropertyInputProps) => React.JSX.Element;
|
|
14
11
|
export default RepeatableField;
|