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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +6 -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 +115 -87
  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
@@ -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`]);
@@ -24,7 +23,6 @@ const ObjectPropertyInput = (props) => {
24
23
  const [hasFetched, setHasFetched] = useState(fetchedOptions[`${id}OptionsHaveFetched`] || false);
25
24
  const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
26
25
  const [layout, setLayout] = useState();
27
- const [appId, setAppId] = useState(fetchedOptions[`${id}AppId`]);
28
26
  const [form, setForm] = useState();
29
27
  const [snackbarError, setSnackbarError] = useState({
30
28
  showAlert: false,
@@ -90,20 +88,33 @@ const ObjectPropertyInput = (props) => {
90
88
  });
91
89
  if (updatedFilter.where) {
92
90
  setLoadingOptions(true);
93
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), (error, instances) => {
91
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
94
92
  if (error) {
95
93
  console.error(error);
96
94
  setLoadingOptions(false);
97
95
  }
98
96
  if (instances && instances.length > 0) {
99
97
  setSelectedInstance(instances[0]);
100
- handleChangeObjectField(id, instances[0]);
98
+ try {
99
+ handleChangeObjectField && (await handleChangeObjectField(id, instances[0]));
100
+ }
101
+ catch (error) {
102
+ console.error('Failed to update field:', error);
103
+ setLoadingOptions(false);
104
+ return;
105
+ }
106
+ try {
107
+ await onAutosave?.(id);
108
+ }
109
+ catch (error) {
110
+ console.error('Autosave failed:', error);
111
+ }
101
112
  }
102
113
  setLoadingOptions(false);
103
114
  });
104
115
  }
105
116
  }
106
- }, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
117
+ }, [relatedObjectId, defaultValueCriteria, sortBy, orderBy]);
107
118
  const getDropdownOptions = useCallback(() => {
108
119
  if (((!fetchedOptions?.[`${id}Options`] ||
109
120
  (fetchedOptions?.[`${id}Options`]).length === 0) &&
@@ -118,7 +129,7 @@ const ObjectPropertyInput = (props) => {
118
129
  direction: 'asc',
119
130
  };
120
131
  updatedFilter.order = `${propertyId} ${direction}`;
121
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
132
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
122
133
  if (error) {
123
134
  console.error(error);
124
135
  setLoadingOptions(false);
@@ -131,7 +142,15 @@ const ObjectPropertyInput = (props) => {
131
142
  }
132
143
  });
133
144
  }
134
- }, [fieldDefinition, updatedCriteria, layout, fetchedOptions, hasFetched, id]);
145
+ }, [
146
+ relatedObjectId,
147
+ updatedCriteria,
148
+ layout,
149
+ fetchedOptions?.[`${id}Options`],
150
+ fetchedOptions?.[`${id}UpdatedCriteria`],
151
+ hasFetched,
152
+ id,
153
+ ]);
135
154
  const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
136
155
  useEffect(() => {
137
156
  if (displayOption === 'dropdown') {
@@ -143,41 +162,31 @@ const ObjectPropertyInput = (props) => {
143
162
  setSelectedInstance(initialValue);
144
163
  }, [initialValue]);
145
164
  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]);
165
+ // Early return if already fetched
166
+ if (fetchedOptions[`${id}Form`])
167
+ return;
168
+ const fetchForm = async () => {
169
+ try {
170
+ let evokeForm;
171
+ if (formId || action?.defaultFormId) {
172
+ evokeForm = await apiServices.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`));
171
173
  }
172
- })
173
- .catch((error) => {
174
- console.error(error);
175
- });
176
- }
177
- }, [action, formId]);
174
+ if (evokeForm) {
175
+ setForm(evokeForm);
176
+ setFetchedOptions({
177
+ [`${id}Form`]: evokeForm,
178
+ });
179
+ }
180
+ }
181
+ catch (error) {
182
+ console.error('Error fetching form:', error);
183
+ }
184
+ };
185
+ fetchForm();
186
+ }, [action, formId, id, apiServices, fetchedOptions]);
178
187
  useEffect(() => {
179
188
  if (!fetchedOptions[`${id}RelatedObject`]) {
180
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
189
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`), (error, object) => {
181
190
  if (error) {
182
191
  console.error(error);
183
192
  }
@@ -186,33 +195,26 @@ const ObjectPropertyInput = (props) => {
186
195
  }
187
196
  });
188
197
  }
189
- }, [fieldDefinition.objectId, fetchedOptions, id]);
198
+ }, [relatedObjectId, fetchedOptions, id]);
190
199
  useEffect(() => {
191
- const fetchDefaultPages = async () => {
192
- if (parameters && !fetchedOptions['allDefaultPages']) {
200
+ (async () => {
201
+ if (parameters && fetchedOptions[`${id}NavigationSlug`] === undefined) {
193
202
  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);
203
+ if (relatedObjectId && pages[relatedObjectId]) {
204
+ setNavigationSlug(pages[relatedObjectId]);
205
+ setFetchedOptions({
206
+ [`${id}NavigationSlug`]: pages[relatedObjectId],
207
+ });
208
208
  }
209
209
  else {
210
- setAppId(page?.appId);
211
- setNavigationSlug(page?.slug);
210
+ // setting the nav slug to null if there is no default page for this object to avoid re-fetching
211
+ setFetchedOptions({
212
+ [`${id}NavigationSlug`]: null,
213
+ });
212
214
  }
213
- });
214
- }
215
- }, [fieldDefinition, allDefaultPages, fetchedOptions, id]);
215
+ }
216
+ })();
217
+ }, [parameters, defaultPages, findDefaultPageSlugFor, relatedObjectId, fetchedOptions]);
216
218
  const handleClose = () => {
217
219
  setOpenCreateDialog(false);
218
220
  };
@@ -240,12 +242,7 @@ const ObjectPropertyInput = (props) => {
240
242
  [`${id}NavigationSlug`]: navigationSlug,
241
243
  });
242
244
  }
243
- if (appId && !fetchedOptions[`${id}AppId`]) {
244
- setFetchedOptions({
245
- [`${id}AppId`]: appId,
246
- });
247
- }
248
- }, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, appId, id]);
245
+ }, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, id]);
249
246
  const dropdownOptions = [
250
247
  ...options.map((o) => ({ label: o.name, value: o.id })),
251
248
  ...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === createActionId)
@@ -383,19 +380,38 @@ const ObjectPropertyInput = (props) => {
383
380
  if (selectedInstance?.id) {
384
381
  e.preventDefault();
385
382
  }
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);
383
+ }, loading: loadingOptions, onChange: async (event, value) => {
384
+ try {
385
+ if (isNil(value)) {
386
+ setDropdownInput(undefined);
387
+ setSelectedInstance(undefined);
388
+ handleChangeObjectField && (await handleChangeObjectField(id, null));
389
+ // Trigger autosave immediately upon clearing
390
+ try {
391
+ await onAutosave?.(id);
392
+ }
393
+ catch (error) {
394
+ console.error('Autosave failed:', error);
395
+ }
396
+ }
397
+ else if (value?.value === '__new__') {
398
+ setOpenCreateDialog(true);
399
+ }
400
+ else {
401
+ const selectedInstance = options.find((o) => o.id === value?.value);
402
+ setSelectedInstance(selectedInstance);
403
+ handleChangeObjectField && (await handleChangeObjectField(id, selectedInstance));
404
+ // Trigger autosave immediately upon selection
405
+ try {
406
+ await onAutosave?.(id);
407
+ }
408
+ catch (error) {
409
+ console.error('Autosave failed:', error);
410
+ }
411
+ }
394
412
  }
395
- else {
396
- const selectedInstance = options.find((o) => o.id === value?.value);
397
- setSelectedInstance(selectedInstance);
398
- handleChangeObjectField(id, selectedInstance);
413
+ catch (error) {
414
+ console.error('Failed to update field:', error);
399
415
  }
400
416
  }, selectOnFocus: false, onBlur: () => {
401
417
  if (dropdownInput) {
@@ -434,7 +450,7 @@ const ObjectPropertyInput = (props) => {
434
450
  ...params.InputProps,
435
451
  startAdornment: selectedInstance?.id ? (React.createElement(Typography, { onClick: () => {
436
452
  if (navigationSlug && selectedInstance?.id) {
437
- navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', selectedInstance.id)}`);
453
+ navigateTo(`/${navigationSlug.replace(':instanceId', selectedInstance.id)}`);
438
454
  }
439
455
  }, sx: {
440
456
  cursor: navigationSlug ? 'pointer' : 'default',
@@ -464,13 +480,25 @@ const ObjectPropertyInput = (props) => {
464
480
  ? '#999'
465
481
  : '#212B36',
466
482
  }, variant: "body2", href: navigationSlug && !isModal
467
- ? `${'/app'}/${appId}/${navigationSlug.replace(':instanceId', selectedInstance?.id ?? '')}`
483
+ ? `${'/app'}${navigationSlug.replace(':instanceId', selectedInstance?.id ?? '')}`
468
484
  : undefined, "aria-label": selectedInstance?.name }, selectedInstance?.name ? selectedInstance?.name : readOnly && 'None')),
469
485
  !readOnly && selectedInstance ? (React.createElement(Tooltip, { title: `Unlink` },
470
486
  React.createElement("span", null,
471
- React.createElement(IconButton, { onClick: (event) => {
487
+ React.createElement(IconButton, { onClick: async (event) => {
472
488
  event.stopPropagation();
473
- handleChangeObjectField(id, null);
489
+ try {
490
+ handleChangeObjectField && (await handleChangeObjectField(id, null));
491
+ }
492
+ catch (error) {
493
+ console.error('Failed to update field:', error);
494
+ return;
495
+ }
496
+ try {
497
+ await onAutosave?.(id);
498
+ }
499
+ catch (error) {
500
+ console.error('Autosave failed:', error);
501
+ }
474
502
  setSelectedInstance(undefined);
475
503
  }, sx: { p: 0, marginBottom: '4px' }, "aria-label": `Unlink` },
476
504
  React.createElement(Close, { sx: { width: '20px', height: '20px' } }))))) : (React.createElement(Button, { sx: {
@@ -494,7 +522,7 @@ const ObjectPropertyInput = (props) => {
494
522
  event.stopPropagation();
495
523
  setOpenCreateDialog(true);
496
524
  }, "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 }),
525
+ 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
526
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
499
527
  isError: snackbarError.isError,
500
528
  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;