@evoke-platform/ui-components 1.10.0-testing.2 → 1.10.0-testing.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.
Files changed (64) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +1 -1
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +430 -0
  4. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
  5. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  6. package/dist/published/components/custom/Form/utils.js +1 -0
  7. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +14 -1
  8. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  9. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  10. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  11. package/dist/published/components/custom/FormV2/FormRenderer.js +17 -4
  12. package/dist/published/components/custom/FormV2/FormRendererContainer.js +112 -73
  13. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  14. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  15. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  16. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  17. package/dist/published/components/custom/FormV2/components/Footer.js +3 -3
  18. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +1 -1
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +0 -3
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +31 -48
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +16 -4
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +109 -81
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +38 -16
  32. package/dist/published/components/custom/FormV2/components/Header.d.ts +13 -3
  33. package/dist/published/components/custom/FormV2/components/Header.js +47 -8
  34. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +44 -35
  35. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  36. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -0
  37. package/dist/published/components/custom/FormV2/components/utils.d.ts +2 -2
  38. package/dist/published/components/custom/FormV2/components/utils.js +11 -14
  39. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +433 -4
  40. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +642 -13
  41. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  42. package/dist/published/components/custom/FormV2/tests/test-data.js +140 -0
  43. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  44. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +155 -0
  45. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  46. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +140 -0
  47. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  48. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  49. package/dist/published/components/custom/index.d.ts +2 -0
  50. package/dist/published/components/custom/index.js +1 -0
  51. package/dist/published/index.d.ts +6 -6
  52. package/dist/published/index.js +1 -1
  53. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  54. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  55. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  56. package/dist/published/stories/FormRendererData.d.ts +12 -0
  57. package/dist/published/stories/FormRendererData.js +27 -44
  58. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  59. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  60. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  61. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  62. package/dist/published/stories/sharedMswHandlers.js +49 -10
  63. package/dist/published/theme/hooks.d.ts +4 -3
  64. 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?.(errors))();
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, 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) => {
@@ -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,18 @@ 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(() => {
96
107
  if (dataType === 'documents' || form)
97
108
  return;
109
+ if (actionId && !action)
110
+ return; // Action is loaded in the side effect above; wait for it to complete
98
111
  if (formId || action?.defaultFormId) {
99
112
  apiServices
100
113
  .get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
101
114
  .then((evokeForm) => {
102
- if (evokeForm?.actionId === actionId) {
115
+ // If an actionId is provided, ensure it matches the form's actionId
116
+ if (!actionId || evokeForm?.actionId === actionId) {
103
117
  const form = evokeForm;
104
118
  setForm(form);
105
119
  }
@@ -111,50 +125,27 @@ function FormRendererContainer(props) {
111
125
  onError(error);
112
126
  });
113
127
  }
114
- else if (action) {
115
- apiServices
116
- .get(getPrefixedUrl('/forms'), {
117
- params: {
118
- filter: {
119
- where: {
120
- actionId: action.id,
121
- objectId: objectId,
122
- },
128
+ else if (action?.type === 'delete' && instance) {
129
+ setForm({
130
+ id: '',
131
+ name: '',
132
+ entries: [
133
+ {
134
+ type: 'content',
135
+ 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
136
  },
137
+ ],
138
+ objectId: objectId,
139
+ actionId: '_delete',
140
+ display: {
141
+ submitLabel: 'Delete',
124
142
  },
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
143
  });
156
144
  }
157
- }, [action, objectId, instance]);
145
+ else {
146
+ setError('Action form could not be found');
147
+ }
148
+ }, [action, actionId, objectId, instance]);
158
149
  useEffect(() => {
159
150
  if (form?.id === 'documentForm') {
160
151
  setParameters([
@@ -184,6 +175,8 @@ function FormRendererContainer(props) {
184
175
  if (document && objectId) {
185
176
  const defaultValues = await getDefaultValues(convertDocToEntries(document), document);
186
177
  setFormData(defaultValues);
178
+ // Deep clone to avoid reference issues
179
+ setLastSavedData(cloneDeep(defaultValues));
187
180
  if (!form) {
188
181
  setForm({
189
182
  id: 'documentForm',
@@ -196,6 +189,8 @@ function FormRendererContainer(props) {
196
189
  else if (form && (instance || !instanceId)) {
197
190
  const defaultValues = await getDefaultValues(form.entries, instance || {});
198
191
  setFormData(defaultValues);
192
+ // Deep clone to avoid reference issues
193
+ setLastSavedData(cloneDeep(defaultValues));
199
194
  }
200
195
  };
201
196
  getInitialValues();
@@ -245,8 +240,8 @@ function FormRendererContainer(props) {
245
240
  else if (action?.type === 'create') {
246
241
  const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
247
242
  actionId: form.actionId,
248
- input: pick(submission, sanitizedObject?.properties
249
- ?.filter((property) => !property.formula && property.type !== 'collection')
243
+ input: omit(submission, sanitizedObject?.properties
244
+ ?.filter((property) => property.formula || property.type === 'collection')
250
245
  .map((property) => property.id) ?? []),
251
246
  });
252
247
  if (response) {
@@ -256,8 +251,8 @@ function FormRendererContainer(props) {
256
251
  else if (instanceId && action) {
257
252
  const response = await objectStore.instanceAction(instanceId, {
258
253
  actionId: action.id,
259
- input: pick(submission, sanitizedObject?.properties
260
- ?.filter((property) => !property.formula && property.type !== 'collection')
254
+ input: omit(submission, sanitizedObject?.properties
255
+ ?.filter((property) => property.formula || property.type === 'collection')
261
256
  .map((property) => property.id) ?? []),
262
257
  });
263
258
  if (sanitizedObject && instance) {
@@ -346,7 +341,7 @@ function FormRendererContainer(props) {
346
341
  for (const { fieldId, fieldValue } of defaultValuesArray) {
347
342
  const parameter = parameters?.find((param) => param.id === fieldId);
348
343
  if (parameter?.type === 'object') {
349
- const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formData, userAccount);
344
+ const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
350
345
  for (const field of dependentFields) {
351
346
  set(result, field.fieldId, field.fieldValue);
352
347
  }
@@ -377,6 +372,45 @@ function FormRendererContainer(props) {
377
372
  await processEntries(entries);
378
373
  return result;
379
374
  };
375
+ const handleAutosave = async (fieldId) => {
376
+ if (!form?.autosaveActionId || !formDataRef.current) {
377
+ return;
378
+ }
379
+ const currentValue = get(formDataRef.current, fieldId);
380
+ const lastValue = get(lastSavedData, fieldId);
381
+ if (isEqual(currentValue, lastValue)) {
382
+ return; // Field hasn't changed, skip save
383
+ }
384
+ try {
385
+ setIsSaving(true);
386
+ const submission = await formatSubmission(formDataRef.current, apiServices, objectId, instanceId, form, setSnackbarError);
387
+ // Handle document autosave
388
+ if (dataType === 'documents' && document) {
389
+ await apiServices.patch(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/documents/${documentId}`), pick(submission, ['metadata']).metadata ?? submission);
390
+ setDocument((prev) => ({
391
+ ...prev,
392
+ metadata: submission.metadata,
393
+ }));
394
+ }
395
+ // Handle object instance autosave
396
+ else if (instanceId && action?.type === 'update') {
397
+ await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
398
+ actionId: form.autosaveActionId,
399
+ input: pick(submission, sanitizedObject?.properties
400
+ ?.filter((property) => !property.formula && property.type !== 'collection')
401
+ .map((property) => property.id) ?? []),
402
+ });
403
+ }
404
+ setLastSavedData(cloneDeep(formDataRef.current));
405
+ setIsSaving(false);
406
+ }
407
+ catch (error) {
408
+ console.error('Autosave failed:', error);
409
+ setIsSaving(false);
410
+ }
411
+ };
412
+ // Autosave is enabled if form.autosaveActionId exists.
413
+ const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
380
414
  async function onChange(id, value) {
381
415
  const parameter = parameters?.find((param) => param.id === id);
382
416
  const entries = getUnnestedEntries(form.entries);
@@ -387,7 +421,7 @@ function FormRendererContainer(props) {
387
421
  if (parameter) {
388
422
  if (parameter.type === 'object' && parameters && parameters.length > 0) {
389
423
  // 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);
424
+ const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
391
425
  for (const field of dependentFields) {
392
426
  onChange(field.fieldId, field.fieldValue);
393
427
  }
@@ -398,16 +432,21 @@ function FormRendererContainer(props) {
398
432
  value = value.value ? value.value : value;
399
433
  }
400
434
  }
401
- if (!isEqual(value, get(formData, id))) {
402
- setFormData((prev) => {
403
- const newData = { ...prev };
404
- set(newData, id, value);
405
- return newData;
406
- });
435
+ if (!isEqual(value, get(formDataRef.current, id))) {
436
+ const newData = { ...formDataRef.current };
437
+ set(newData, id, value);
438
+ setFormData(newData);
407
439
  }
408
440
  }
409
- const isLoading = (instanceId && !formData && !document) || !form || !sanitizedObject;
441
+ const isLoading = (instanceId && !formDataRef.current && !document) || !form || !sanitizedObject;
410
442
  const status = error ? 'error' : isLoading ? 'loading' : 'ready';
443
+ // Compose a header renderer that injects the saving indicator into the rendered header
444
+ const composedRenderHeader = (props) => {
445
+ if (renderHeader) {
446
+ return renderHeader({ ...props, autosaving: !!form?.autosaveActionId && isSaving });
447
+ }
448
+ return React.createElement(Header, { ...props, autosaving: !!form?.autosaveActionId && isSaving });
449
+ };
411
450
  const onDiscardChanges = onDiscardChangesOverride
412
451
  ? onDiscardChangesOverride
413
452
  : async () => {
@@ -426,7 +465,7 @@ function FormRendererContainer(props) {
426
465
  padding: '0px',
427
466
  border: !isLoading ? '1px solid #dbe0e4' : undefined,
428
467
  ...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' } },
468
+ } }, !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
469
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
431
470
  React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
432
471
  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;
@@ -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: UseFormGetValues<FieldValues>;
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: (name: string, value: unknown) => void;
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;
@@ -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
- if (addressField === 'line1' && typeof value === 'object' && value.line1) {
27
- const addressKeys = ['line1', 'city', 'county', 'state', 'zipCode'];
28
- addressKeys.forEach((key) => {
29
- const fullKey = `${addressObject}.${key}`;
30
- if (parameters?.some((p) => p.id === fullKey)) {
31
- const fieldValue = value[key];
32
- handleChange(fullKey, fieldValue);
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
- else {
37
- handleChange(name, value);
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 }, !readOnly ? (React.createElement(FormField, { property: fieldDefinition, defaultValue: fieldValue, onChange: handleAddressChange, 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: {
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;