@evoke-platform/ui-components 1.10.0-dev.3 → 1.10.0-dev.31

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 (71) 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 +1 -1
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
  7. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +428 -0
  8. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
  9. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  10. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  11. package/dist/published/components/custom/Form/utils.js +1 -0
  12. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +14 -1
  13. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  14. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  15. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  16. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  17. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.js +19 -7
  19. package/dist/published/components/custom/FormV2/FormRendererContainer.js +117 -74
  20. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  21. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  22. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  23. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  24. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  25. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +0 -3
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +36 -49
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +16 -4
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +109 -81
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +38 -16
  39. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  40. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  41. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +46 -35
  42. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  43. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -0
  44. package/dist/published/components/custom/FormV2/components/utils.d.ts +4 -4
  45. package/dist/published/components/custom/FormV2/components/utils.js +13 -16
  46. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +289 -45
  47. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +664 -16
  48. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  49. package/dist/published/components/custom/FormV2/tests/test-data.js +140 -0
  50. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  51. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +155 -0
  52. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  53. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  54. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  55. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  56. package/dist/published/components/custom/index.d.ts +2 -0
  57. package/dist/published/components/custom/index.js +1 -0
  58. package/dist/published/index.d.ts +6 -6
  59. package/dist/published/index.js +1 -1
  60. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  61. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  62. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  63. package/dist/published/stories/FormRendererData.d.ts +12 -0
  64. package/dist/published/stories/FormRendererData.js +29 -44
  65. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  66. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  67. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  68. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  69. package/dist/published/stories/sharedMswHandlers.js +49 -10
  70. package/dist/published/theme/hooks.d.ts +4 -3
  71. 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, 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,21 @@ function FormRendererContainer(props) {
78
102
  setNavigationSlug(page?.slug);
79
103
  });
80
104
  }
81
- if (defaultPages) {
82
- for (const [objectId, defaultPage] of Object.entries(defaultPages)) {
83
- const pageId = defaultPage.includes('/')
84
- ? encodePageSlug(defaultPage.split('/').slice(2).join('/'))
85
- : defaultPage;
86
- apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${pageId}`)).then((page) => {
87
- setDefaultPagesWithSlugs({
88
- ...defaultPagesWithSlugs,
89
- [objectId]: '/' + page.appId + '/' + page.slug,
90
- });
91
- });
92
- }
93
- }
94
105
  }, []);
95
106
  useEffect(() => {
107
+ const needsInstance = action?.type !== 'create' && !!instanceId;
108
+ // Instance and Action are loaded in the side effect above; wait for them to complete.
109
+ const loading = (actionId && !action) || (needsInstance && !instance);
96
110
  if (dataType === 'documents' || form)
97
111
  return;
98
- if (formId || action?.defaultFormId) {
112
+ if (loading)
113
+ return;
114
+ if ((formId || action?.defaultFormId) && formId !== '_auto_') {
99
115
  apiServices
100
116
  .get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
101
117
  .then((evokeForm) => {
102
- if (evokeForm?.actionId === actionId) {
118
+ // If an actionId is provided, ensure it matches the form's actionId
119
+ if (!actionId || evokeForm?.actionId === actionId) {
103
120
  const form = evokeForm;
104
121
  setForm(form);
105
122
  }
@@ -111,50 +128,27 @@ function FormRendererContainer(props) {
111
128
  onError(error);
112
129
  });
113
130
  }
114
- else if (action) {
115
- apiServices
116
- .get(getPrefixedUrl('/forms'), {
117
- params: {
118
- filter: {
119
- where: {
120
- actionId: action.id,
121
- objectId: objectId,
122
- },
131
+ else if (action?.type === 'delete' && formId === '_auto_') {
132
+ setForm({
133
+ id: '',
134
+ name: '',
135
+ entries: [
136
+ {
137
+ type: 'content',
138
+ 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
139
  },
140
+ ],
141
+ objectId: objectId,
142
+ actionId: '_delete',
143
+ display: {
144
+ submitLabel: 'Delete',
124
145
  },
125
- })
126
- .then((matchingForms) => {
127
- if (matchingForms.length === 1) {
128
- const form = matchingForms[0];
129
- setForm(form);
130
- // use this default form if no delete form is found
131
- }
132
- else if (action.type === 'delete' && instance) {
133
- setForm({
134
- id: '',
135
- name: '',
136
- entries: [
137
- {
138
- type: 'content',
139
- html: `<p>You are about to delete <strong>${instance.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</p>`,
140
- },
141
- ],
142
- objectId: objectId,
143
- actionId: '_delete',
144
- display: {
145
- submitLabel: 'Delete',
146
- },
147
- });
148
- }
149
- else if (instance || action.type === 'create') {
150
- setError('Default action form could not be found');
151
- }
152
- })
153
- .catch((error) => {
154
- onError(error);
155
146
  });
156
147
  }
157
- }, [action, objectId, instance]);
148
+ else {
149
+ setError('Action form could not be found');
150
+ }
151
+ }, [action, actionId, objectId, instance]);
158
152
  useEffect(() => {
159
153
  if (form?.id === 'documentForm') {
160
154
  setParameters([
@@ -184,6 +178,8 @@ function FormRendererContainer(props) {
184
178
  if (document && objectId) {
185
179
  const defaultValues = await getDefaultValues(convertDocToEntries(document), document);
186
180
  setFormData(defaultValues);
181
+ // Deep clone to avoid reference issues
182
+ setLastSavedData(cloneDeep(defaultValues));
187
183
  if (!form) {
188
184
  setForm({
189
185
  id: 'documentForm',
@@ -196,6 +192,8 @@ function FormRendererContainer(props) {
196
192
  else if (form && (instance || !instanceId)) {
197
193
  const defaultValues = await getDefaultValues(form.entries, instance || {});
198
194
  setFormData(defaultValues);
195
+ // Deep clone to avoid reference issues
196
+ setLastSavedData(cloneDeep(defaultValues));
199
197
  }
200
198
  };
201
199
  getInitialValues();
@@ -245,8 +243,8 @@ function FormRendererContainer(props) {
245
243
  else if (action?.type === 'create') {
246
244
  const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
247
245
  actionId: form.actionId,
248
- input: pick(submission, sanitizedObject?.properties
249
- ?.filter((property) => !property.formula && property.type !== 'collection')
246
+ input: omit(submission, sanitizedObject?.properties
247
+ ?.filter((property) => property.formula || property.type === 'collection')
250
248
  .map((property) => property.id) ?? []),
251
249
  });
252
250
  if (response) {
@@ -256,8 +254,8 @@ function FormRendererContainer(props) {
256
254
  else if (instanceId && action) {
257
255
  const response = await objectStore.instanceAction(instanceId, {
258
256
  actionId: action.id,
259
- input: pick(submission, sanitizedObject?.properties
260
- ?.filter((property) => !property.formula && property.type !== 'collection')
257
+ input: omit(submission, sanitizedObject?.properties
258
+ ?.filter((property) => property.formula || property.type === 'collection')
261
259
  .map((property) => property.id) ?? []),
262
260
  });
263
261
  if (sanitizedObject && instance) {
@@ -346,7 +344,7 @@ function FormRendererContainer(props) {
346
344
  for (const { fieldId, fieldValue } of defaultValuesArray) {
347
345
  const parameter = parameters?.find((param) => param.id === fieldId);
348
346
  if (parameter?.type === 'object') {
349
- const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formData, userAccount);
347
+ const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
350
348
  for (const field of dependentFields) {
351
349
  set(result, field.fieldId, field.fieldValue);
352
350
  }
@@ -377,6 +375,45 @@ function FormRendererContainer(props) {
377
375
  await processEntries(entries);
378
376
  return result;
379
377
  };
378
+ const handleAutosave = async (fieldId) => {
379
+ if (!form?.autosaveActionId || !formDataRef.current) {
380
+ return;
381
+ }
382
+ const currentValue = get(formDataRef.current, fieldId);
383
+ const lastValue = get(lastSavedData, fieldId);
384
+ if (isEqual(currentValue, lastValue)) {
385
+ return; // Field hasn't changed, skip save
386
+ }
387
+ try {
388
+ setIsSaving(true);
389
+ const submission = await formatSubmission(formDataRef.current, apiServices, objectId, instanceId, form, setSnackbarError);
390
+ // Handle document autosave
391
+ if (dataType === 'documents' && document) {
392
+ await apiServices.patch(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/documents/${documentId}`), pick(submission, ['metadata']).metadata ?? submission);
393
+ setDocument((prev) => ({
394
+ ...prev,
395
+ metadata: submission.metadata,
396
+ }));
397
+ }
398
+ // Handle object instance autosave
399
+ else if (instanceId && action?.type === 'update') {
400
+ await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
401
+ actionId: form.autosaveActionId,
402
+ input: pick(submission, sanitizedObject?.properties
403
+ ?.filter((property) => !property.formula && property.type !== 'collection')
404
+ .map((property) => property.id) ?? []),
405
+ });
406
+ }
407
+ setLastSavedData(cloneDeep(formDataRef.current));
408
+ setIsSaving(false);
409
+ }
410
+ catch (error) {
411
+ console.error('Autosave failed:', error);
412
+ setIsSaving(false);
413
+ }
414
+ };
415
+ // Autosave is enabled if form.autosaveActionId exists.
416
+ const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
380
417
  async function onChange(id, value) {
381
418
  const parameter = parameters?.find((param) => param.id === id);
382
419
  const entries = getUnnestedEntries(form.entries);
@@ -387,7 +424,7 @@ function FormRendererContainer(props) {
387
424
  if (parameter) {
388
425
  if (parameter.type === 'object' && parameters && parameters.length > 0) {
389
426
  // On change of a related object, update default values dependent on that object
390
- const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id, formData, userAccount);
427
+ const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
391
428
  for (const field of dependentFields) {
392
429
  onChange(field.fieldId, field.fieldValue);
393
430
  }
@@ -398,16 +435,21 @@ function FormRendererContainer(props) {
398
435
  value = value.value ? value.value : value;
399
436
  }
400
437
  }
401
- if (!isEqual(value, get(formData, id))) {
402
- setFormData((prev) => {
403
- const newData = { ...prev };
404
- set(newData, id, value);
405
- return newData;
406
- });
438
+ if (!isEqual(value, get(formDataRef.current, id))) {
439
+ const newData = { ...formDataRef.current };
440
+ set(newData, id, value);
441
+ setFormData(newData);
407
442
  }
408
443
  }
409
- const isLoading = (instanceId && !formData && !document) || !form || !sanitizedObject;
444
+ const isLoading = (instanceId && !formDataRef.current && !document) || !form || !sanitizedObject;
410
445
  const status = error ? 'error' : isLoading ? 'loading' : 'ready';
446
+ // Compose a header renderer that injects the saving indicator into the rendered header
447
+ const composedRenderHeader = (props) => {
448
+ if (renderHeader) {
449
+ return renderHeader({ ...props, autosaving: !!form?.autosaveActionId && isSaving });
450
+ }
451
+ return React.createElement(Header, { ...props, autosaving: !!form?.autosaveActionId && isSaving });
452
+ };
411
453
  const onDiscardChanges = onDiscardChangesOverride
412
454
  ? onDiscardChangesOverride
413
455
  : async () => {
@@ -426,7 +468,8 @@ function FormRendererContainer(props) {
426
468
  padding: '0px',
427
469
  border: !isLoading ? '1px solid #dbe0e4' : undefined,
428
470
  ...sx,
429
- } }, !isLoading ? (React.createElement(React.Fragment, null, form && sanitizedObject && (React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value: 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' } },
471
+ } }, !isLoading ? (React.createElement(React.Fragment, null,
472
+ 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
473
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
431
474
  React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
432
475
  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'>;
@@ -6,7 +6,7 @@ import { Box } from '../../../layout';
6
6
  import { FormContext } from './FormContext';
7
7
  /* Default FormRenderer Footer. Displays a submit button and cancel changes button. */
8
8
  export const Footer = (props) => {
9
- const { action, sx } = props;
9
+ const { sx } = props;
10
10
  const { width } = useContext(FormContext);
11
11
  const { isBelow, breakpoints } = useWidgetSize({
12
12
  scroll: false,
@@ -20,15 +20,18 @@ export const Footer = (props) => {
20
20
  padding: isSmallerThanMd ? '16px' : '20px',
21
21
  justifyContent: isXs ? 'center' : 'flex-end',
22
22
  alignItems: 'center',
23
- borderTop: action?.type !== 'delete' ? '1px solid #f4f6f8' : 'none',
23
+ borderTop: '1px solid #f4f6f8',
24
24
  borderRadius: '0px 0px 6px 6px',
25
25
  zIndex: 3,
26
+ width: '100%',
27
+ // this will ensure footer does not exceed form width when form has padding
28
+ maxWidth: width - (isSmallerThanMd ? 32 : 40),
26
29
  ...sx,
27
30
  } },
28
31
  React.createElement(FooterActions, { ...props })));
29
32
  };
30
33
  export const FooterActions = (props) => {
31
- const { action, onDiscardChanges, onSubmit, submitButtonLabel, discardChangesButtonLabel } = props;
34
+ const { action, onDiscardChanges, onSubmit, submitButtonLabel, discardChangesButtonLabel, disableDiscardChanges } = props;
32
35
  const { width } = useContext(FormContext);
33
36
  const [loading, setLoading] = React.useState(false);
34
37
  const handleSubmit = async () => {
@@ -46,7 +49,7 @@ export const FooterActions = (props) => {
46
49
  });
47
50
  const { isXs } = breakpoints;
48
51
  return (React.createElement(React.Fragment, null,
49
- React.createElement(Button, { onClick: onDiscardChanges, variant: "outlined", sx: {
52
+ !disableDiscardChanges && (React.createElement(Button, { onClick: onDiscardChanges, variant: "outlined", sx: {
50
53
  margin: '5px',
51
54
  marginX: isXs ? '0px' : undefined,
52
55
  color: 'black',
@@ -56,7 +59,7 @@ export const FooterActions = (props) => {
56
59
  backgroundColor: '#f2f4f7',
57
60
  border: '1px solid rgb(206, 212, 218)',
58
61
  },
59
- } }, discardChangesButtonLabel),
62
+ } }, discardChangesButtonLabel)),
60
63
  React.createElement(LoadingButton, { onClick: handleSubmit, variant: "contained", sx: {
61
64
  lineHeight: '2.75',
62
65
  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`,
@@ -84,7 +84,7 @@ export const ActionDialog = (props) => {
84
84
  objectId: relatedParameter.objectId, onSubmit: handleFormSave, onDiscardChanges: onClose, onSubmitError: handleSaveError, richTextEditor: richTextEditor, associatedObject: associatedObject, renderHeader: (formHeaderProps) => {
85
85
  return (React.createElement(DialogTitle, { sx: {
86
86
  ...styles.dialogTitle,
87
- borderBottom: action.type === 'delete' ? undefined : '1px solid #e9ecef',
87
+ borderBottom: '1px solid #e9ecef',
88
88
  } },
89
89
  action && hasAccess && !loading ? action?.name : '',
90
90
  React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose, "aria-label": "Close" },
@@ -99,7 +99,7 @@ export const ActionDialog = (props) => {
99
99
  React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(ValidationErrors, { errors: bodyProps.errors, sx: {
100
100
  my: isSm || isXs ? 2 : 3,
101
101
  } })) : null),
102
- React.createElement(Body, { ...bodyProps, sx: { padding: 0 } }))), renderFooter: (footerProps) => (React.createElement(DialogActions, { sx: { padding: 0 } },
102
+ React.createElement(Body, { ...bodyProps, sx: { padding: 0 } }))), renderFooter: (footerProps) => (React.createElement(DialogActions, { sx: { padding: 0, margin: 0 } },
103
103
  React.createElement(Footer, { ...footerProps, discardChangesButtonLabel: "Cancel" }))), renderContainer: ({ status, error, defaultContainer }) => (React.createElement(React.Fragment, null,
104
104
  status === 'ready' && defaultContainer,
105
105
  status === 'loading' && (React.createElement(DialogContent, null,
@@ -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;