@evoke-platform/ui-components 1.10.0-dev.2 → 1.10.0-dev.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +1 -1
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +430 -0
  4. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
  5. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  6. package/dist/published/components/custom/Form/utils.js +1 -0
  7. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +14 -1
  8. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  9. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  10. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  11. package/dist/published/components/custom/FormV2/FormRenderer.js +17 -4
  12. package/dist/published/components/custom/FormV2/FormRendererContainer.js +113 -74
  13. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  14. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  15. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  16. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  17. package/dist/published/components/custom/FormV2/components/Footer.js +3 -3
  18. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +1 -1
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +0 -3
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +36 -49
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +16 -4
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +109 -81
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +38 -16
  32. package/dist/published/components/custom/FormV2/components/Header.d.ts +13 -3
  33. package/dist/published/components/custom/FormV2/components/Header.js +47 -8
  34. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +44 -35
  35. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  36. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -0
  37. package/dist/published/components/custom/FormV2/components/utils.d.ts +2 -2
  38. package/dist/published/components/custom/FormV2/components/utils.js +11 -14
  39. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +433 -4
  40. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +662 -13
  41. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  42. package/dist/published/components/custom/FormV2/tests/test-data.js +140 -0
  43. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  44. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +155 -0
  45. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  46. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +140 -0
  47. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  48. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  49. package/dist/published/components/custom/index.d.ts +2 -0
  50. package/dist/published/components/custom/index.js +1 -0
  51. package/dist/published/index.d.ts +6 -6
  52. package/dist/published/index.js +1 -1
  53. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  54. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  55. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  56. package/dist/published/stories/FormRendererData.d.ts +12 -0
  57. package/dist/published/stories/FormRendererData.js +27 -44
  58. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  59. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  60. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  61. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  62. package/dist/published/stories/sharedMswHandlers.js +49 -10
  63. package/dist/published/theme/hooks.d.ts +4 -3
  64. package/package.json +4 -2
@@ -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
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();
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,14 +88,27 @@ 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/${fieldDefinition.objectId}/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
  });
@@ -131,7 +142,15 @@ const ObjectPropertyInput = (props) => {
131
142
  }
132
143
  });
133
144
  }
134
- }, [fieldDefinition, updatedCriteria, layout, fetchedOptions, hasFetched, id]);
145
+ }, [
146
+ fieldDefinition,
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,38 +162,28 @@ 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, fieldDefinition.objectId, apiServices, fetchedOptions]);
178
187
  useEffect(() => {
179
188
  if (!fetchedOptions[`${id}RelatedObject`]) {
180
189
  apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
@@ -188,31 +197,24 @@ const ObjectPropertyInput = (props) => {
188
197
  }
189
198
  }, [fieldDefinition.objectId, 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 (fieldDefinition.objectId && pages[fieldDefinition.objectId]) {
204
+ setNavigationSlug(pages[fieldDefinition.objectId]);
205
+ setFetchedOptions({
206
+ [`${id}NavigationSlug`]: pages[fieldDefinition.objectId],
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, fieldDefinition, 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: {
@@ -29,7 +29,7 @@ const styles = {
29
29
  };
30
30
  const RelatedObjectInstance = (props) => {
31
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 { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
33
33
  const [selectedRow, setSelectedRow] = useState();
34
34
  const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
35
35
  const apiServices = useApiServices();
@@ -40,9 +40,22 @@ const RelatedObjectInstance = (props) => {
40
40
  });
41
41
  const { isXs, isSm } = breakpoints;
42
42
  const linkExistingInstance = async () => {
43
- if (selectedRow) {
43
+ if (selectedRow && handleChangeObjectField) {
44
44
  setSelectedInstance(selectedRow);
45
- handleChangeObjectField(id, selectedRow);
45
+ try {
46
+ await handleChangeObjectField(id, selectedRow);
47
+ }
48
+ catch (error) {
49
+ console.error('Failed to update field:', error);
50
+ onClose();
51
+ return;
52
+ }
53
+ try {
54
+ await onAutosave?.(id);
55
+ }
56
+ catch (error) {
57
+ console.error('Autosave failed:', error);
58
+ }
46
59
  }
47
60
  onClose();
48
61
  };
@@ -56,22 +69,31 @@ const RelatedObjectInstance = (props) => {
56
69
  }
57
70
  submission = await formatSubmission(submission, apiServices, relatedObject.id);
58
71
  try {
59
- await apiServices
60
- .post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
72
+ const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
61
73
  actionId: actionId,
62
74
  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
75
  });
76
+ try {
77
+ handleChangeObjectField && (await handleChangeObjectField(id, response));
78
+ }
79
+ catch (error) {
80
+ console.error('Failed to update field:', error);
81
+ return;
82
+ }
83
+ try {
84
+ await onAutosave?.(id);
85
+ }
86
+ catch (error) {
87
+ console.error('Autosave failed:', error);
88
+ }
89
+ setSelectedInstance(response);
90
+ setSnackbarError({
91
+ showAlert: true,
92
+ message: 'New instance created',
93
+ isError: false,
94
+ });
95
+ setOptions(options.concat([response]));
96
+ onClose();
75
97
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
98
  }
77
99
  catch (err) {
@@ -1,19 +1,29 @@
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
+ /**
15
+ * Indicates whether this is a "delete form".
16
+ * This flag adjusts header styling specifically for delete forms.
17
+ *
18
+ * @warning This prop is temporary and will be removed
19
+ * when delete form styling is finalized.
20
+ */
21
+ isDeleteForm?: boolean;
14
22
  errors?: FieldErrors;
15
23
  action?: Action;
24
+ autosaving?: boolean;
16
25
  sx?: SxProps;
26
+ autosaveEnabled?: boolean;
17
27
  };
18
28
  declare const Header: React.FC<HeaderProps>;
19
29
  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, isDeleteForm, 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',
@@ -22,15 +25,25 @@ const Header = (props) => {
22
25
  flexWrap: 'wrap',
23
26
  paddingY: isSm || isXs ? 2 : 3,
24
27
  // when rendering the default delete action, we don't want a border
25
- borderBottom: !form.id ? undefined : '1px solid #e9ecef',
28
+ borderBottom: isDeleteForm ? undefined : '1px solid #e9ecef',
26
29
  gap: isSm || isXs ? 2 : 3,
27
30
  ...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));
31
+ }, ref: validationContainerRef },
32
+ title && (React.createElement(Box, { sx: { flex: '1 1 auto', minWidth: 0, display: 'flex', alignItems: 'center', gap: 1 } },
33
+ React.createElement(Title, { ...props }),
34
+ props.autosaving && !isSmall && React.createElement(SavingIndicator, null))),
35
+ hasAccordions && (React.createElement(Box, { sx: { flex: '0 0 auto', display: 'flex', alignItems: 'center' } },
36
+ React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
37
+ React.createElement(AccordionActions, { ...props })),
38
+ autosaveEnabled && (React.createElement(Box, { sx: {
39
+ width: '96px',
40
+ minWidth: '72px',
41
+ display: 'flex',
42
+ justifyContent: 'flex-end',
43
+ alignItems: 'center',
44
+ marginLeft: 0.5,
45
+ } }, props.autosaving && isSmall ? React.createElement(SavingIndicator, null) : React.createElement(Box, { sx: { width: '100%' } }))))),
46
+ displayValidationErrors ? React.createElement(ValidationErrors, { errors: errors }) : null));
34
47
  };
35
48
  // Default slot components for convenience
36
49
  export const Title = ({ title }) => (React.createElement(Typography, { sx: {
@@ -60,4 +73,30 @@ export const AccordionActions = ({ onExpandAll, onCollapseAll, expandedSections
60
73
  fontSize: '14px',
61
74
  } }, "Collapse all")));
62
75
  };
76
+ /**
77
+ * SavingIndicator displays a spinning icon with "Saving" text
78
+ * to indicate that an autosave operation is in progress.
79
+ */
80
+ const SavingIndicator = () => (React.createElement(Box, { sx: {
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ gap: 0.5,
84
+ } },
85
+ React.createElement(Typography, { sx: {
86
+ fontSize: '14px',
87
+ color: 'text.secondary',
88
+ } }, "Saving"),
89
+ React.createElement(Autorenew, { sx: {
90
+ fontSize: '16px',
91
+ color: 'text.secondary',
92
+ animation: 'spin 1s linear infinite',
93
+ '@keyframes spin': {
94
+ '0%': {
95
+ transform: 'rotate(0deg)',
96
+ },
97
+ '100%': {
98
+ transform: 'rotate(360deg)',
99
+ },
100
+ },
101
+ } })));
63
102
  export default Header;
@@ -1,6 +1,7 @@
1
1
  import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
2
2
  import { WarningRounded } from '@mui/icons-material';
3
3
  import DOMPurify from 'dompurify';
4
+ import { isEmpty } from 'lodash';
4
5
  import React, { useEffect, useMemo } from 'react';
5
6
  import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
6
7
  import { TextField, Typography } from '../../../core';
@@ -37,11 +38,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
37
38
  }
38
39
  export function RecursiveEntryRenderer(props) {
39
40
  const { entry } = props;
40
- const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
41
- // If the entry is hidden, clear its value and any nested values, and skip rendering
42
- if (!entryIsVisible(entry, getValues(), instance)) {
43
- return null;
44
- }
41
+ const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
45
42
  const { isBelow, breakpoints } = useWidgetSize({
46
43
  scroll: false,
47
44
  defaultWidth: width,
@@ -52,20 +49,24 @@ export function RecursiveEntryRenderer(props) {
52
49
  const userAccount = useAuthenticationContext()?.account;
53
50
  const entryId = getEntryId(entry) || 'defaultId';
54
51
  const display = 'display' in entry ? entry.display : undefined;
55
- const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues(entryId);
52
+ const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
56
53
  const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
57
54
  const middleObject = fetchedOptions[`${entryId}MiddleObject`];
58
55
  const fieldDefinition = useMemo(() => {
59
56
  return getFieldDefinition(entry, object, parameters, form?.id === 'documentForm');
60
57
  }, [entry, parameters, object]);
61
58
  const validation = fieldDefinition?.validation || {};
62
- if (associatedObject?.propertyId === entryId)
63
- return null;
64
59
  useEffect(() => {
65
60
  if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
66
61
  fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
67
62
  }
68
63
  }, [fieldDefinition, instance]);
64
+ if (associatedObject?.propertyId === entryId)
65
+ return null;
66
+ // If the entry is hidden, clear its value and any nested values, and skip rendering
67
+ if (!getValues || !entryIsVisible(entry, instance, getValues())) {
68
+ return null;
69
+ }
69
70
  if (entry.type === 'content') {
70
71
  return (React.createElement(Box, { dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(entry.html) }, sx: {
71
72
  fontFamily: 'Roboto, Helvetica, Arial, sans-serif',
@@ -74,7 +75,7 @@ export function RecursiveEntryRenderer(props) {
74
75
  else if ((entry.type === 'input' || entry.type === 'readonlyField' || entry.type === 'inputField') &&
75
76
  fieldDefinition) {
76
77
  if (isAddressProperty(entryId)) {
77
- return React.createElement(AddressFields, { entry: entry, entryId: entryId, fieldDefinition: fieldDefinition });
78
+ return (React.createElement(AddressFields, { entry: entry, entryId: entryId, fieldDefinition: fieldDefinition, readOnly: entry.type === 'readonlyField' }));
78
79
  }
79
80
  else if (fieldDefinition.type === 'image') {
80
81
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -90,30 +91,42 @@ export function RecursiveEntryRenderer(props) {
90
91
  ? display?.defaultValue.orderBy
91
92
  : undefined, defaultValueCriteria: typeof display?.defaultValue === 'object' && 'criteria' in display.defaultValue
92
93
  ? display?.defaultValue?.criteria
93
- : undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description,
94
- // formId={display?.createFormId} // TODO: this should be added as part of the builder update
95
- createActionId: '_create' })));
94
+ : undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description, formId: display?.createFormId, createActionId: display?.createActionId })));
96
95
  }
97
96
  else if (fieldDefinition.type === 'user') {
98
97
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
99
98
  React.createElement(UserProperty, { id: entryId, value: fieldValue, error: !!errors?.[entryId], readOnly: entry.type === 'readonlyField', hasDescription: !!display?.description })));
100
99
  }
101
100
  else if (fieldDefinition.type === 'collection') {
102
- return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
103
- React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] || initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: validation?.criteria, hasDescription: !!display?.description })))) : (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
104
- React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry, createActionId: '_create', updateActionId: '_update', deleteActionId: '_delete' })));
101
+ if (fieldDefinition?.manyToManyPropertyId) {
102
+ if (middleObject && !isEmpty(middleObject)) {
103
+ return (initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
104
+ React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
105
+ initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: validation?.criteria, hasDescription: !!display?.description }))));
106
+ }
107
+ else {
108
+ // when in the builder preview, the middle object won't be fetched so instead show an empty field
109
+ const singleSelectProperty = structuredClone(fieldDefinition);
110
+ singleSelectProperty.type = 'choices';
111
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
112
+ React.createElement(FormField, { id: entryId, property: singleSelectProperty, defaultValue: fieldValue || getValues(entryId), selectOptions: [], size: fieldHeight })));
113
+ }
114
+ }
115
+ else {
116
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
117
+ React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry })));
118
+ }
105
119
  }
106
120
  else if (fieldDefinition.type === 'richText') {
107
- return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, richTextEditor ? (React.createElement(richTextEditor, {
121
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor && handleChange ? (React.createElement(RichTextEditor
122
+ // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
123
+ , {
108
124
  // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
109
- id: entry.uniqueId,
110
- value: fieldValue,
111
- handleUpdate: (value) => handleChange(entryId, value),
112
- format: 'rtf',
113
- disabled: entry.type === 'readonlyField',
114
- rows: display?.rowCount,
115
- hasError: !!errors?.[entryId],
116
- })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
125
+ id: entry.uniqueId, value: fieldValue, handleUpdate: (value) => handleChange(entryId, value), format: "rtf", disabled: entry.type === 'readonlyField', rows: display?.rowCount, hasError: !!errors?.[entryId] })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
126
+ onAutosave?.(entryId)?.catch((error) => {
127
+ console.error('Autosave failed:', error);
128
+ });
129
+ }, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
117
130
  }
118
131
  else if (fieldDefinition.type === 'document') {
119
132
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -156,18 +169,14 @@ export function RecursiveEntryRenderer(props) {
156
169
  : `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
157
170
  React.createElement(FormField, { id: entryId,
158
171
  // TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
159
- property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, mask: validation?.mask, isOptionEqualToValue: isOptionEqualToValue, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, required: entry.display?.required || false, getOptionLabel: (option) => {
160
- if (typeof option === 'string') {
161
- return (entry?.enumWithLabels?.find((e) => e.value === option)
162
- ?.label ?? option);
163
- }
164
- else {
165
- return (entry?.enumWithLabels?.find((e) => e.value === option.value)?.label ?? String(option.value));
166
- }
167
- }, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
172
+ property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
173
+ // Blur event - reads current value from formData
174
+ onAutosave?.(entryId)?.catch((error) => {
175
+ console.error('Autosave failed:', error);
176
+ });
177
+ }, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, mask: validation?.mask, isOptionEqualToValue: isOptionEqualToValue, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, required: entry.display?.required || false, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
168
178
  ? display?.booleanDisplay
169
- : display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions: entry?.enumWithLabels &&
170
- entry.enumWithLabels, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue })));
179
+ : display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions: fieldDefinition?.enum, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue })));
171
180
  }
172
181
  }
173
182
  else if (entry.type === 'columns') {
@@ -29,7 +29,7 @@ function ValidationErrors(props) {
29
29
  border: '1px solid #721c24',
30
30
  padding: '8px 24px',
31
31
  borderRadius: '4px',
32
- flex: 1,
32
+ flex: '1 1 100%',
33
33
  ...sx,
34
34
  } },
35
35
  React.createElement(Typography, { sx: { color: '#721c24', mt: '16px', mb: '8px' } }, "Please fix the following errors before submitting:"),
@@ -93,6 +93,7 @@ export type EntryRendererProps = {
93
93
  };
94
94
  export type SectionsProps = {
95
95
  entry: ExpandedSections;
96
+ readOnly?: boolean;
96
97
  };
97
98
  export type DocumentData = {
98
99
  id: string;