@evoke-platform/ui-components 1.10.0-testing.9 → 1.10.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
  2. package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
  3. package/dist/published/components/core/TextField/TextField.js +1 -1
  4. package/dist/published/components/core/TextField/TextField.test.js +0 -2
  5. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
  7. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
  8. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  9. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  10. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
  11. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  12. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  13. package/dist/published/components/custom/FormField/FormField.js +17 -5
  14. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  15. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  16. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  17. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
  21. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  22. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  23. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  24. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  25. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  26. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  27. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  28. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +9 -23
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +127 -92
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
  42. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  43. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  44. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  45. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  46. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
  47. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  48. package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
  49. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  50. package/dist/published/components/custom/FormV2/components/utils.js +83 -13
  51. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +411 -44
  52. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
  53. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  54. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  55. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  56. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
  57. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  58. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  59. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  60. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  61. package/dist/published/components/custom/index.d.ts +2 -0
  62. package/dist/published/components/custom/index.js +1 -0
  63. package/dist/published/index.d.ts +6 -6
  64. package/dist/published/index.js +1 -1
  65. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  66. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  67. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  68. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  69. package/dist/published/stories/FormRendererData.d.ts +12 -0
  70. package/dist/published/stories/FormRendererData.js +26 -1
  71. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  72. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  73. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  74. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  75. package/dist/published/stories/sharedMswHandlers.js +49 -10
  76. package/dist/published/theme/hooks.d.ts +4 -3
  77. package/dist/published/types.d.ts +3 -0
  78. package/package.json +10 -8
@@ -6,7 +6,7 @@ import { Autocomplete, IconButton, Paper, TextField, Typography } from '../../..
6
6
  import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
7
7
  const UserProperty = (props) => {
8
8
  const { id, error, value, readOnly, hasDescription } = props;
9
- const { fetchedOptions, setFetchedOptions, handleChange, fieldHeight } = useFormContext();
9
+ const { fetchedOptions, setFetchedOptions, handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
10
10
  const [loadingOptions, setLoadingOptions] = useState(false);
11
11
  const apiServices = useApiServices();
12
12
  const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
@@ -40,9 +40,21 @@ const UserProperty = (props) => {
40
40
  });
41
41
  }
42
42
  }, [id]);
43
- function handleChangeUserProperty(id, value) {
43
+ async function handleChangeUserProperty(id, value) {
44
44
  const updatedValue = typeof value?.value === 'string' ? { name: value.label, id: value.value } : null;
45
- handleChange(id, updatedValue);
45
+ try {
46
+ handleChange && (await handleChange(id, updatedValue));
47
+ }
48
+ catch (error) {
49
+ console.error('Failed to update field:', error);
50
+ return; // Exit early if handleChange fails
51
+ }
52
+ try {
53
+ await onAutosave?.(id);
54
+ }
55
+ catch (error) {
56
+ console.error('Autosave failed:', error);
57
+ }
46
58
  }
47
59
  return (options && (React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, popupIcon: userValue || readOnly ? '' : React.createElement(ExpandMore, null), clearIcon: !loadingOptions && userValue ? (React.createElement(IconButton, { size: "small", disableRipple: true, onKeyDown: (e) => {
48
60
  if (e.key === 'Enter') {
@@ -7,15 +7,14 @@ import { Close } from '../../../../../../icons';
7
7
  import { useFormContext } from '../../../../../../theme/hooks';
8
8
  import { Autocomplete, Button, IconButton, Link, ListItem, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
9
9
  import { Box } from '../../../../../layout';
10
- import { encodePageSlug, getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
10
+ import { getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
11
11
  import RelatedObjectInstance from './RelatedObjectInstance';
12
12
  const ObjectPropertyInput = (props) => {
13
- const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, } = props;
14
- const { fetchedOptions, setFetchedOptions, parameters, fieldHeight, handleChange: handleChangeObjectField, instance, } = useFormContext();
13
+ const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, relatedObjectId, } = props;
14
+ const { fetchedOptions, setFetchedOptions, parameters, fieldHeight, handleChange: handleChangeObjectField, onAutosave: onAutosave, instance, } = useFormContext();
15
15
  const { defaultPages, findDefaultPageSlugFor } = useApp();
16
16
  const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
17
17
  const [openCreateDialog, setOpenCreateDialog] = useState(false);
18
- const [allDefaultPages, setAllDefaultPages] = useState(defaultPages ?? {});
19
18
  const [loadingOptions, setLoadingOptions] = useState(false);
20
19
  const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${id}NavigationSlug`]);
21
20
  const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${id}RelatedObject`]);
@@ -23,8 +22,7 @@ const ObjectPropertyInput = (props) => {
23
22
  const [openOptions, setOpenOptions] = useState(false);
24
23
  const [hasFetched, setHasFetched] = useState(fetchedOptions[`${id}OptionsHaveFetched`] || false);
25
24
  const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
26
- const [layout, setLayout] = useState();
27
- const [appId, setAppId] = useState(fetchedOptions[`${id}AppId`]);
25
+ const [layout, setLayout] = useState(fetchedOptions[`${id}ViewLayout`]);
28
26
  const [form, setForm] = useState();
29
27
  const [snackbarError, setSnackbarError] = useState({
30
28
  showAlert: false,
@@ -70,10 +68,17 @@ const ObjectPropertyInput = (props) => {
70
68
  };
71
69
  }
72
70
  if (viewLayout) {
73
- apiServices
74
- .get(getPrefixedUrl(`/objects/${viewLayout.objectId}/${displayOption === 'dropdown' ? 'dropdown' : 'table'}Layouts/${viewLayout.id}`))
75
- .then(setLayout)
76
- .catch((err) => setLayout(defaultViewLayout));
71
+ if (!fetchedOptions[`${id}ViewLayout`]) {
72
+ apiServices
73
+ .get(getPrefixedUrl(`/objects/${viewLayout.objectId}/${displayOption === 'dropdown' ? 'dropdown' : 'table'}Layouts/${viewLayout.id}`))
74
+ .then((layout) => {
75
+ setLayout(layout);
76
+ setFetchedOptions({
77
+ [`${id}ViewLayout`]: layout,
78
+ });
79
+ })
80
+ .catch((err) => setLayout(defaultViewLayout));
81
+ }
77
82
  }
78
83
  else {
79
84
  setLayout(defaultViewLayout);
@@ -90,20 +95,33 @@ const ObjectPropertyInput = (props) => {
90
95
  });
91
96
  if (updatedFilter.where) {
92
97
  setLoadingOptions(true);
93
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), (error, instances) => {
98
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
94
99
  if (error) {
95
100
  console.error(error);
96
101
  setLoadingOptions(false);
97
102
  }
98
103
  if (instances && instances.length > 0) {
99
104
  setSelectedInstance(instances[0]);
100
- handleChangeObjectField(id, instances[0]);
105
+ try {
106
+ handleChangeObjectField && (await handleChangeObjectField(id, instances[0]));
107
+ }
108
+ catch (error) {
109
+ console.error('Failed to update field:', error);
110
+ setLoadingOptions(false);
111
+ return;
112
+ }
113
+ try {
114
+ await onAutosave?.(id);
115
+ }
116
+ catch (error) {
117
+ console.error('Autosave failed:', error);
118
+ }
101
119
  }
102
120
  setLoadingOptions(false);
103
121
  });
104
122
  }
105
123
  }
106
- }, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
124
+ }, [relatedObjectId, defaultValueCriteria, sortBy, orderBy]);
107
125
  const getDropdownOptions = useCallback(() => {
108
126
  if (((!fetchedOptions?.[`${id}Options`] ||
109
127
  (fetchedOptions?.[`${id}Options`]).length === 0) &&
@@ -118,7 +136,7 @@ const ObjectPropertyInput = (props) => {
118
136
  direction: 'asc',
119
137
  };
120
138
  updatedFilter.order = `${propertyId} ${direction}`;
121
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
139
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
122
140
  if (error) {
123
141
  console.error(error);
124
142
  setLoadingOptions(false);
@@ -131,7 +149,15 @@ const ObjectPropertyInput = (props) => {
131
149
  }
132
150
  });
133
151
  }
134
- }, [fieldDefinition, updatedCriteria, layout, fetchedOptions, hasFetched, id]);
152
+ }, [
153
+ relatedObjectId,
154
+ updatedCriteria,
155
+ layout,
156
+ fetchedOptions?.[`${id}Options`],
157
+ fetchedOptions?.[`${id}UpdatedCriteria`],
158
+ hasFetched,
159
+ id,
160
+ ]);
135
161
  const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
136
162
  useEffect(() => {
137
163
  if (displayOption === 'dropdown') {
@@ -143,41 +169,31 @@ const ObjectPropertyInput = (props) => {
143
169
  setSelectedInstance(initialValue);
144
170
  }, [initialValue]);
145
171
  useEffect(() => {
146
- if (formId || action?.defaultFormId) {
147
- apiServices
148
- .get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
149
- .then((evokeForm) => {
150
- setForm(evokeForm);
151
- })
152
- .catch((error) => {
153
- console.error(error);
154
- });
155
- }
156
- else if (action) {
157
- apiServices
158
- .get(getPrefixedUrl('/forms'), {
159
- params: {
160
- filter: {
161
- where: {
162
- actionId: action.id,
163
- objectId: fieldDefinition.objectId,
164
- },
165
- },
166
- },
167
- })
168
- .then((matchingForms) => {
169
- if (matchingForms.length === 1) {
170
- setForm(matchingForms[0]);
172
+ // Early return if already fetched
173
+ if (fetchedOptions[`${id}Form`])
174
+ return;
175
+ const fetchForm = async () => {
176
+ try {
177
+ let evokeForm;
178
+ if (formId || action?.defaultFormId) {
179
+ evokeForm = await apiServices.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`));
171
180
  }
172
- })
173
- .catch((error) => {
174
- console.error(error);
175
- });
176
- }
177
- }, [action, formId]);
181
+ if (evokeForm) {
182
+ setForm(evokeForm);
183
+ setFetchedOptions({
184
+ [`${id}Form`]: evokeForm,
185
+ });
186
+ }
187
+ }
188
+ catch (error) {
189
+ console.error('Error fetching form:', error);
190
+ }
191
+ };
192
+ fetchForm();
193
+ }, [action, formId, id, apiServices, fetchedOptions]);
178
194
  useEffect(() => {
179
195
  if (!fetchedOptions[`${id}RelatedObject`]) {
180
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
196
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`), (error, object) => {
181
197
  if (error) {
182
198
  console.error(error);
183
199
  }
@@ -186,33 +202,26 @@ const ObjectPropertyInput = (props) => {
186
202
  }
187
203
  });
188
204
  }
189
- }, [fieldDefinition.objectId, fetchedOptions, id]);
205
+ }, [relatedObjectId, fetchedOptions, id]);
190
206
  useEffect(() => {
191
- const fetchDefaultPages = async () => {
192
- if (parameters && !fetchedOptions['allDefaultPages']) {
207
+ (async () => {
208
+ if (parameters && fetchedOptions[`${id}NavigationSlug`] === undefined) {
193
209
  const pages = await getDefaultPages(parameters, defaultPages, findDefaultPageSlugFor);
194
- setAllDefaultPages(pages);
195
- setFetchedOptions({ [`allDefaultPages`]: pages });
196
- }
197
- };
198
- fetchDefaultPages();
199
- }, [fetchedOptions, parameters, defaultPages, findDefaultPageSlugFor]);
200
- useEffect(() => {
201
- if (fieldDefinition.objectId &&
202
- allDefaultPages &&
203
- allDefaultPages[fieldDefinition.objectId] &&
204
- (!fetchedOptions?.[`${id}NavigationSlug`] || !fetchedOptions[`${id}AppId`])) {
205
- apiServices.get(getPrefixedUrl(`/apps/${allDefaultPages[fieldDefinition.objectId].split('/').slice(1, 2)}/pages/${encodePageSlug(allDefaultPages[fieldDefinition.objectId].split('/').slice(2).join('/'))}`), (error, page) => {
206
- if (error) {
207
- console.error(error);
210
+ if (relatedObjectId && pages[relatedObjectId]) {
211
+ setNavigationSlug(pages[relatedObjectId]);
212
+ setFetchedOptions({
213
+ [`${id}NavigationSlug`]: pages[relatedObjectId],
214
+ });
208
215
  }
209
216
  else {
210
- setAppId(page?.appId);
211
- setNavigationSlug(page?.slug);
217
+ // setting the nav slug to null if there is no default page for this object to avoid re-fetching
218
+ setFetchedOptions({
219
+ [`${id}NavigationSlug`]: null,
220
+ });
212
221
  }
213
- });
214
- }
215
- }, [fieldDefinition, allDefaultPages, fetchedOptions, id]);
222
+ }
223
+ })();
224
+ }, [parameters, defaultPages, findDefaultPageSlugFor, relatedObjectId, fetchedOptions]);
216
225
  const handleClose = () => {
217
226
  setOpenCreateDialog(false);
218
227
  };
@@ -240,12 +249,7 @@ const ObjectPropertyInput = (props) => {
240
249
  [`${id}NavigationSlug`]: navigationSlug,
241
250
  });
242
251
  }
243
- if (appId && !fetchedOptions[`${id}AppId`]) {
244
- setFetchedOptions({
245
- [`${id}AppId`]: appId,
246
- });
247
- }
248
- }, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, appId, id]);
252
+ }, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, id]);
249
253
  const dropdownOptions = [
250
254
  ...options.map((o) => ({ label: o.name, value: o.id })),
251
255
  ...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === createActionId)
@@ -383,19 +387,38 @@ const ObjectPropertyInput = (props) => {
383
387
  if (selectedInstance?.id) {
384
388
  e.preventDefault();
385
389
  }
386
- }, loading: loadingOptions, onChange: (event, value) => {
387
- if (isNil(value)) {
388
- setDropdownInput(undefined);
389
- setSelectedInstance(undefined);
390
- handleChangeObjectField(id, null);
391
- }
392
- else if (value?.value === '__new__') {
393
- setOpenCreateDialog(true);
390
+ }, loading: loadingOptions, onChange: async (event, value) => {
391
+ try {
392
+ if (isNil(value)) {
393
+ setDropdownInput(undefined);
394
+ setSelectedInstance(undefined);
395
+ handleChangeObjectField && (await handleChangeObjectField(id, null));
396
+ // Trigger autosave immediately upon clearing
397
+ try {
398
+ await onAutosave?.(id);
399
+ }
400
+ catch (error) {
401
+ console.error('Autosave failed:', error);
402
+ }
403
+ }
404
+ else if (value?.value === '__new__') {
405
+ setOpenCreateDialog(true);
406
+ }
407
+ else {
408
+ const selectedInstance = options.find((o) => o.id === value?.value);
409
+ setSelectedInstance(selectedInstance);
410
+ handleChangeObjectField && (await handleChangeObjectField(id, selectedInstance));
411
+ // Trigger autosave immediately upon selection
412
+ try {
413
+ await onAutosave?.(id);
414
+ }
415
+ catch (error) {
416
+ console.error('Autosave failed:', error);
417
+ }
418
+ }
394
419
  }
395
- else {
396
- const selectedInstance = options.find((o) => o.id === value?.value);
397
- setSelectedInstance(selectedInstance);
398
- handleChangeObjectField(id, selectedInstance);
420
+ catch (error) {
421
+ console.error('Failed to update field:', error);
399
422
  }
400
423
  }, selectOnFocus: false, onBlur: () => {
401
424
  if (dropdownInput) {
@@ -434,7 +457,7 @@ const ObjectPropertyInput = (props) => {
434
457
  ...params.InputProps,
435
458
  startAdornment: selectedInstance?.id ? (React.createElement(Typography, { onClick: () => {
436
459
  if (navigationSlug && selectedInstance?.id) {
437
- navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', selectedInstance.id)}`);
460
+ navigateTo(`/${navigationSlug.replace(':instanceId', selectedInstance.id)}`);
438
461
  }
439
462
  }, sx: {
440
463
  cursor: navigationSlug ? 'pointer' : 'default',
@@ -464,13 +487,25 @@ const ObjectPropertyInput = (props) => {
464
487
  ? '#999'
465
488
  : '#212B36',
466
489
  }, variant: "body2", href: navigationSlug && !isModal
467
- ? `${'/app'}/${appId}/${navigationSlug.replace(':instanceId', selectedInstance?.id ?? '')}`
490
+ ? `${'/app'}${navigationSlug.replace(':instanceId', selectedInstance?.id ?? '')}`
468
491
  : undefined, "aria-label": selectedInstance?.name }, selectedInstance?.name ? selectedInstance?.name : readOnly && 'None')),
469
492
  !readOnly && selectedInstance ? (React.createElement(Tooltip, { title: `Unlink` },
470
493
  React.createElement("span", null,
471
- React.createElement(IconButton, { onClick: (event) => {
494
+ React.createElement(IconButton, { onClick: async (event) => {
472
495
  event.stopPropagation();
473
- handleChangeObjectField(id, null);
496
+ try {
497
+ handleChangeObjectField && (await handleChangeObjectField(id, null));
498
+ }
499
+ catch (error) {
500
+ console.error('Failed to update field:', error);
501
+ return;
502
+ }
503
+ try {
504
+ await onAutosave?.(id);
505
+ }
506
+ catch (error) {
507
+ console.error('Autosave failed:', error);
508
+ }
474
509
  setSelectedInstance(undefined);
475
510
  }, sx: { p: 0, marginBottom: '4px' }, "aria-label": `Unlink` },
476
511
  React.createElement(Close, { sx: { width: '20px', height: '20px' } }))))) : (React.createElement(Button, { sx: {
@@ -494,7 +529,7 @@ const ObjectPropertyInput = (props) => {
494
529
  event.stopPropagation();
495
530
  setOpenCreateDialog(true);
496
531
  }, "aria-label": `Add` }, "Add")))),
497
- React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError, fieldDefinition: fieldDefinition }),
532
+ React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError }),
498
533
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
499
534
  isError: snackbarError.isError,
500
535
  showAlert: false,
@@ -1,11 +1,11 @@
1
- import { InputParameter, Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
1
+ import { Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
2
2
  import React from 'react';
3
3
  import { BaseProps } from '../../types';
4
4
  export type RelatedObjectInstanceProps = BaseProps & {
5
5
  id: string;
6
6
  open: boolean;
7
7
  title: string;
8
- relatedObject: Obj | undefined;
8
+ relatedObject?: Obj;
9
9
  setSelectedInstance: (selectedInstance: ObjectInstance) => void;
10
10
  handleClose: () => void;
11
11
  mode: 'default' | 'existingOnly' | 'newOnly';
@@ -21,7 +21,6 @@ export type RelatedObjectInstanceProps = BaseProps & {
21
21
  layout?: TableViewLayout;
22
22
  formId?: string;
23
23
  actionId?: string;
24
- fieldDefinition: InputParameter;
25
24
  };
26
25
  declare const RelatedObjectInstance: (props: RelatedObjectInstanceProps) => React.JSX.Element;
27
26
  export default RelatedObjectInstance;
@@ -6,6 +6,7 @@ import { Close } from '../../../../../../icons';
6
6
  import useWidgetSize, { useFormContext } from '../../../../../../theme/hooks';
7
7
  import { Button, Dialog, DialogContent, DialogTitle, FormControlLabel, IconButton, Radio, RadioGroup, } from '../../../../../core';
8
8
  import Box from '../../../../../layout/Box/Box';
9
+ import ErrorComponent from '../../../../ErrorComponent';
9
10
  import FormRenderer from '../../../FormRenderer';
10
11
  import FormRendererContainer from '../../../FormRendererContainer';
11
12
  import Body from '../../Body';
@@ -28,8 +29,8 @@ const styles = {
28
29
  },
29
30
  };
30
31
  const RelatedObjectInstance = (props) => {
31
- const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, fieldDefinition, setSnackbarError, setOptions, options, } = props;
32
- const { handleChange: handleChangeObjectField, richTextEditor, fieldHeight, width } = useFormContext();
32
+ const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError, setOptions, options, } = props;
33
+ const { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
33
34
  const [selectedRow, setSelectedRow] = useState();
34
35
  const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
35
36
  const apiServices = useApiServices();
@@ -40,9 +41,22 @@ const RelatedObjectInstance = (props) => {
40
41
  });
41
42
  const { isXs, isSm } = breakpoints;
42
43
  const linkExistingInstance = async () => {
43
- if (selectedRow) {
44
+ if (selectedRow && handleChangeObjectField) {
44
45
  setSelectedInstance(selectedRow);
45
- handleChangeObjectField(id, selectedRow);
46
+ try {
47
+ await handleChangeObjectField(id, selectedRow);
48
+ }
49
+ catch (error) {
50
+ console.error('Failed to update field:', error);
51
+ onClose();
52
+ return;
53
+ }
54
+ try {
55
+ await onAutosave?.(id);
56
+ }
57
+ catch (error) {
58
+ console.error('Autosave failed:', error);
59
+ }
46
60
  }
47
61
  onClose();
48
62
  };
@@ -56,22 +70,31 @@ const RelatedObjectInstance = (props) => {
56
70
  }
57
71
  submission = await formatSubmission(submission, apiServices, relatedObject.id);
58
72
  try {
59
- await apiServices
60
- .post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
73
+ const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
61
74
  actionId: actionId,
62
75
  input: submission,
63
- })
64
- .then((response) => {
65
- handleChangeObjectField(id, response);
66
- setSelectedInstance(response);
67
- setSnackbarError({
68
- showAlert: true,
69
- message: 'New instance created',
70
- isError: false,
71
- });
72
- setOptions(options.concat([response]));
73
- onClose();
74
76
  });
77
+ try {
78
+ handleChangeObjectField && (await handleChangeObjectField(id, response));
79
+ }
80
+ catch (error) {
81
+ console.error('Failed to update field:', error);
82
+ return;
83
+ }
84
+ try {
85
+ await onAutosave?.(id);
86
+ }
87
+ catch (error) {
88
+ console.error('Autosave failed:', error);
89
+ }
90
+ setSelectedInstance(response);
91
+ setSnackbarError({
92
+ showAlert: true,
93
+ message: 'New instance created',
94
+ isError: false,
95
+ });
96
+ setOptions(options.concat([response]));
97
+ onClose();
75
98
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
99
  }
77
100
  catch (err) {
@@ -98,7 +121,7 @@ const RelatedObjectInstance = (props) => {
98
121
  }, value: relationType },
99
122
  React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
100
123
  React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null;
101
- const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: fieldDefinition.objectId, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
124
+ const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: relatedObject.id, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
102
125
  relationType === 'new' ? (React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(FormRenderer.ValidationErrors, { errors: bodyProps.errors, sx: {
103
126
  my: isSm || isXs ? 2 : 3,
104
127
  } })) : null)) : null,
@@ -114,7 +137,7 @@ const RelatedObjectInstance = (props) => {
114
137
  React.createElement(RadioButtons, null),
115
138
  defaultContainer)),
116
139
  status === 'ready' && defaultContainer));
117
- }, sx: { border: 'none' } })), [formId, actionId, fieldDefinition, fieldHeight, richTextEditor, RadioButtons]);
140
+ }, sx: { border: 'none' } })), [formId, actionId, relatedObject, fieldHeight, richTextEditor, RadioButtons]);
118
141
  return (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: open, onClose: (e, reason) => reason !== 'backdropClick' && handleClose(), sx: {
119
142
  background: 'none',
120
143
  }, PaperProps: {
@@ -137,7 +160,7 @@ const RelatedObjectInstance = (props) => {
137
160
  title,
138
161
  React.createElement(IconButton, { onClick: onClose, "aria-label": "Close" },
139
162
  React.createElement(Close, { fontSize: "small" })))),
140
- relationType === 'new' ? (React.createElement(DialogForm, null)) : ((mode === 'default' || mode === 'existingOnly') &&
163
+ relationType === 'new' ? (relatedObject ? (React.createElement(DialogForm, null)) : (React.createElement(ErrorComponent, { code: "Misconfigured" }))) : ((mode === 'default' || mode === 'existingOnly') &&
141
164
  relatedObject && (React.createElement(React.Fragment, null,
142
165
  React.createElement(DialogContent, { sx: styles.dialogContent },
143
166
  shouldShowRadioButtons && React.createElement(RadioButtons, null),
@@ -1,19 +1,21 @@
1
- import { Action, EvokeForm } from '@evoke-platform/context';
1
+ import { Action } from '@evoke-platform/context';
2
2
  import { SxProps } from '@mui/material';
3
3
  import React from 'react';
4
4
  import { FieldErrors } from 'react-hook-form';
5
5
  import { ExpandedSection } from './types';
6
6
  export type HeaderProps = {
7
7
  hasAccordions: boolean;
8
- shouldShowValidationErrors: boolean;
8
+ shouldShowValidationErrors?: boolean;
9
+ validationContainerRef?: React.Ref<HTMLDivElement>;
9
10
  title?: string;
10
11
  expandedSections?: ExpandedSection[];
11
12
  onExpandAll?: () => void;
12
13
  onCollapseAll?: () => void;
13
- form: EvokeForm;
14
14
  errors?: FieldErrors;
15
15
  action?: Action;
16
+ autosaving?: boolean;
16
17
  sx?: SxProps;
18
+ autosaveEnabled?: boolean;
17
19
  };
18
20
  declare const Header: React.FC<HeaderProps>;
19
21
  export type TitleProps = {
@@ -1,12 +1,13 @@
1
1
  import { isEmpty } from 'lodash';
2
2
  import React from 'react';
3
+ import { Autorenew } from '../../../../icons';
3
4
  import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
4
5
  import Button from '../../../core/Button/Button';
5
6
  import { Typography } from '../../../core/Typography';
6
7
  import Box from '../../../layout/Box/Box';
7
8
  import ValidationErrors from './ValidationFiles/ValidationErrors';
8
9
  const Header = (props) => {
9
- const { title, errors, hasAccordions, shouldShowValidationErrors, form, sx } = props;
10
+ const { title, errors, hasAccordions, shouldShowValidationErrors, validationContainerRef, sx, autosaveEnabled } = props;
10
11
  const { width } = useFormContext();
11
12
  const { breakpoints, isBelow } = useWidgetSize({
12
13
  scroll: false,
@@ -14,6 +15,8 @@ const Header = (props) => {
14
15
  });
15
16
  const isSmallerThanMd = isBelow('md');
16
17
  const { isXs, isSm } = breakpoints;
18
+ const isSmall = isSm || isXs;
19
+ const displayValidationErrors = shouldShowValidationErrors && !isEmpty(errors);
17
20
  return (React.createElement(Box, { sx: {
18
21
  paddingX: isSmallerThanMd ? 2 : 3,
19
22
  paddingTop: '0px',
@@ -21,16 +24,25 @@ const Header = (props) => {
21
24
  alignItems: 'center',
22
25
  flexWrap: 'wrap',
23
26
  paddingY: isSm || isXs ? 2 : 3,
24
- // when rendering the default delete action, we don't want a border
25
- borderBottom: !form.id ? undefined : '1px solid #e9ecef',
27
+ borderBottom: '1px solid #e9ecef',
26
28
  gap: isSm || isXs ? 2 : 3,
27
29
  ...sx,
28
- } },
29
- title && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
30
- React.createElement(Title, { ...props }))),
31
- hasAccordions && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
32
- React.createElement(AccordionActions, { ...props }))),
33
- shouldShowValidationErrors && !isEmpty(errors) ? React.createElement(ValidationErrors, { errors: errors }) : null));
30
+ }, ref: validationContainerRef },
31
+ title && (React.createElement(Box, { sx: { flex: '1 1 auto', minWidth: 0, display: 'flex', alignItems: 'center', gap: 1 } },
32
+ React.createElement(Title, { ...props }),
33
+ props.autosaving && !isSmall && React.createElement(SavingIndicator, null))),
34
+ hasAccordions && (React.createElement(Box, { sx: { flex: '0 0 auto', display: 'flex', alignItems: 'center' } },
35
+ React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
36
+ React.createElement(AccordionActions, { ...props })),
37
+ autosaveEnabled && (React.createElement(Box, { sx: {
38
+ width: '96px',
39
+ minWidth: '72px',
40
+ display: 'flex',
41
+ justifyContent: 'flex-end',
42
+ alignItems: 'center',
43
+ marginLeft: 0.5,
44
+ } }, props.autosaving && isSmall ? React.createElement(SavingIndicator, null) : React.createElement(Box, { sx: { width: '100%' } }))))),
45
+ displayValidationErrors ? React.createElement(ValidationErrors, { errors: errors }) : null));
34
46
  };
35
47
  // Default slot components for convenience
36
48
  export const Title = ({ title }) => (React.createElement(Typography, { sx: {
@@ -60,4 +72,30 @@ export const AccordionActions = ({ onExpandAll, onCollapseAll, expandedSections
60
72
  fontSize: '14px',
61
73
  } }, "Collapse all")));
62
74
  };
75
+ /**
76
+ * SavingIndicator displays a spinning icon with "Saving" text
77
+ * to indicate that an autosave operation is in progress.
78
+ */
79
+ const SavingIndicator = () => (React.createElement(Box, { sx: {
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ gap: 0.5,
83
+ } },
84
+ React.createElement(Typography, { sx: {
85
+ fontSize: '14px',
86
+ color: 'text.secondary',
87
+ } }, "Saving"),
88
+ React.createElement(Autorenew, { sx: {
89
+ fontSize: '16px',
90
+ color: 'text.secondary',
91
+ animation: 'spin 1s linear infinite',
92
+ '@keyframes spin': {
93
+ '0%': {
94
+ transform: 'rotate(0deg)',
95
+ },
96
+ '100%': {
97
+ transform: 'rotate(360deg)',
98
+ },
99
+ },
100
+ } })));
63
101
  export default Header;
@@ -0,0 +1,16 @@
1
+ import { PropertyProtection as PropertyProtectionType } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ import { ObjectProperty } from '../../../../types';
4
+ type PropertyProtectionProps = {
5
+ parameter: ObjectProperty;
6
+ protection?: PropertyProtectionType;
7
+ mask?: string;
8
+ canEdit: boolean;
9
+ value: unknown;
10
+ handleChange?: (value: unknown) => void;
11
+ setCurrentDisplayValue: (value: unknown) => void;
12
+ mode: 'mask' | 'full' | 'edit';
13
+ setMode: (mode: 'mask' | 'full' | 'edit') => void;
14
+ };
15
+ declare const PropertyProtection: React.FC<PropertyProtectionProps>;
16
+ export default PropertyProtection;