@evoke-platform/ui-components 1.10.0-testing.8 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) 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 +25 -3
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
  7. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +473 -0
  8. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
  9. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
  10. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  11. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  12. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
  13. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  14. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  15. package/dist/published/components/custom/FormField/FormField.js +17 -5
  16. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  17. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  18. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  19. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  20. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  21. package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
  22. package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
  23. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  24. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  25. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  26. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  27. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  28. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  29. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  30. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +6 -23
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +115 -87
  42. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  43. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
  44. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  45. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  46. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  47. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  48. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
  49. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  50. package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
  51. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  52. package/dist/published/components/custom/FormV2/components/utils.js +83 -13
  53. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +413 -46
  54. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
  55. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  56. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  57. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  58. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
  59. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  60. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  61. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  62. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  63. package/dist/published/components/custom/index.d.ts +2 -0
  64. package/dist/published/components/custom/index.js +1 -0
  65. package/dist/published/index.d.ts +6 -6
  66. package/dist/published/index.js +1 -1
  67. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  68. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  69. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  70. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  71. package/dist/published/stories/FormRendererData.d.ts +12 -0
  72. package/dist/published/stories/FormRendererData.js +26 -1
  73. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  74. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  75. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  76. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  77. package/dist/published/stories/sharedMswHandlers.js +49 -10
  78. package/dist/published/theme/hooks.d.ts +4 -3
  79. package/dist/published/types.d.ts +3 -0
  80. package/package.json +12 -8
@@ -1,33 +1,53 @@
1
1
  import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
2
2
  import axios from 'axios';
3
- import { get, isArray, isEmpty, isEqual, 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;
@@ -1,10 +1,10 @@
1
1
  import { ApiServices, FormEntry, InputField, InputParameter, InputParameterReference, ObjectInstance, Reference, UserAccount } from '@evoke-platform/context';
2
2
  import { FieldValues } from 'react-hook-form';
3
- export declare function evalDefaultVals(parameters: InputParameter[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
3
+ export declare function evalDefaultVals(parameters: InputParameter[], unnestedEntries: FormEntry[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
4
4
  fieldId: string;
5
5
  fieldValue: unknown;
6
6
  }[]>;
7
- export declare function processValueUpdate(entries: FormEntry[] | undefined, parameters: InputParameter[], updatedRelatedObjectValue: ObjectInstance | null | Reference, apiServices: ApiServices, changedEntryId?: string, formValues?: FieldValues, userAccount?: UserAccount): Promise<{
7
+ export declare function processValueUpdate(unnestedEntries: FormEntry[], parameters: InputParameter[], updatedRelatedObjectValue: ObjectInstance | null | Reference, apiServices: ApiServices, changedEntryId?: string, formValues?: FieldValues, userAccount?: UserAccount): Promise<{
8
8
  fieldId: string;
9
9
  fieldValue: unknown;
10
10
  }[]>;