@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.
Files changed (78) hide show
  1. package/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
  2. package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
  3. package/dist/published/components/core/TextField/TextField.js +1 -1
  4. package/dist/published/components/core/TextField/TextField.test.js +0 -2
  5. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
  7. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
  8. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  9. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  10. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
  11. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  12. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  13. package/dist/published/components/custom/FormField/FormField.js +17 -5
  14. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  15. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  16. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  17. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
  21. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  22. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  23. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  24. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  25. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  26. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  27. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  28. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +9 -23
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +127 -92
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
  42. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  43. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  44. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  45. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  46. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
  47. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  48. package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
  49. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  50. package/dist/published/components/custom/FormV2/components/utils.js +83 -13
  51. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +411 -44
  52. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
  53. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  54. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  55. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  56. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
  57. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  58. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  59. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  60. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  61. package/dist/published/components/custom/index.d.ts +2 -0
  62. package/dist/published/components/custom/index.js +1 -0
  63. package/dist/published/index.d.ts +6 -6
  64. package/dist/published/index.js +1 -1
  65. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  66. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  67. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  68. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  69. package/dist/published/stories/FormRendererData.d.ts +12 -0
  70. package/dist/published/stories/FormRendererData.js +26 -1
  71. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  72. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  73. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  74. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  75. package/dist/published/stories/sharedMswHandlers.js +49 -10
  76. package/dist/published/theme/hooks.d.ts +4 -3
  77. package/dist/published/types.d.ts +3 -0
  78. 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(), instance)) {
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
- await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError?.(errors))();
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
- (actionId || form.id === 'documentForm') &&
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, merge, omit, pick, set, uniq } from 'lodash';
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, defaultPages } = useApp();
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 [formData, setFormData] = useState();
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).then((instance) => {
54
- setInstance(instance);
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 (formId || action?.defaultFormId) {
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
- if (evokeForm?.actionId === actionId) {
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
- apiServices
116
- .get(getPrefixedUrl('/forms'), {
117
- params: {
118
- filter: {
119
- where: {
120
- actionId: action.id,
121
- objectId: objectId,
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
- }, [action, objectId, instance]);
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 processEntries = async (entries) => {
285
- if (!entries)
286
- return;
287
- for (const entry of entries) {
288
- if (entry.type === 'sections' || entry.type === 'columns') {
289
- const subEntries = entry.type === 'sections' ? entry.sections : entry.columns;
290
- for (const subEntry of subEntries) {
291
- if (subEntry.entries) {
292
- const nested = await getDefaultValues(subEntry.entries, instanceData);
293
- merge(result, nested);
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 ((entry.type === 'input' || entry.type === 'inputField') &&
298
- isAddressProperty(entry.parameterId || entry.input?.id)) {
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
- else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
321
- const fieldId = entry.type === 'input'
322
- ? entry.parameterId
323
- : entry.type === 'inputField'
324
- ? entry.input?.id
325
- : undefined;
326
- if (fieldId) {
327
- const fieldValue = instanceData?.[fieldId] ??
328
- instanceData?.metadata?.[fieldId];
329
- const parameter = parameters?.find((param) => param.id === fieldId);
330
- if (associatedObject?.propertyId === fieldId &&
331
- associatedObject?.instanceId &&
332
- parameter &&
333
- action?.type === 'create') {
334
- try {
335
- const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
336
- result[associatedObject.propertyId] = instance;
337
- }
338
- catch (error) {
339
- console.error(error);
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
- else if (entry.type !== 'readonlyField' &&
343
- isEmptyWithDefault(fieldValue, entry, instanceData)) {
344
- if (fieldId && parameters && parameters.length > 0) {
345
- const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
346
- for (const { fieldId, fieldValue } of defaultValuesArray) {
347
- const parameter = parameters?.find((param) => param.id === fieldId);
348
- if (parameter?.type === 'object') {
349
- const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formData, userAccount);
350
- for (const field of dependentFields) {
351
- set(result, field.fieldId, field.fieldValue);
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
- else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
359
- result[fieldId] = false;
360
- }
361
- else if (fieldValue !== undefined && fieldValue !== null) {
362
- if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
363
- let RTFFieldValue = fieldValue;
364
- if (!fieldValue.trim().startsWith('{\\rtf')) {
365
- RTFFieldValue = plainTextToRtf(fieldValue);
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(form?.entries, parameters, value, apiServices, id, formData, userAccount);
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(formData, id))) {
402
- setFormData((prev) => {
403
- const newData = { ...prev };
404
- set(newData, id, value);
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 && !formData && !document) || !form || !sanitizedObject;
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, form && sanitizedObject && (React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value: formData, form: form, instance: dataType !== 'documents' ? instance : document, onChange: onChange, associatedObject: associatedObject, renderHeader: renderHeader, renderBody: renderBody, renderFooter: document && !hasDocumentUpdateAccess ? () => React.createElement(React.Fragment, null) : renderFooter })))) : (React.createElement(Box, { sx: { padding: '20px' } },
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, section.entries?.map((sectionEntry, index) => (React.createElement(RecursiveEntryRenderer, { key: sectionEntry.type + index, entry: sectionEntry }))))));
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: boolean;
10
+ shouldShowValidationErrors?: boolean;
11
11
  hasAccordions: boolean;
12
12
  expandedSections?: ExpandedSection[];
13
13
  onExpandAll?: () => void;