@evoke-platform/ui-components 1.10.0-testing.10 → 1.10.0-testing.12

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 (26) hide show
  1. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  2. package/dist/published/components/custom/FormV2/FormRenderer.js +3 -1
  3. package/dist/published/components/custom/FormV2/FormRendererContainer.js +82 -13
  4. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  5. package/dist/published/components/custom/FormV2/components/Footer.js +3 -3
  6. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +2 -1
  7. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +30 -13
  8. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +1 -1
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +16 -4
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +1 -0
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +70 -18
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +37 -15
  17. package/dist/published/components/custom/FormV2/components/Header.d.ts +1 -0
  18. package/dist/published/components/custom/FormV2/components/Header.js +42 -4
  19. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +31 -6
  20. package/dist/published/components/custom/FormV2/components/utils.js +2 -0
  21. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +449 -1
  22. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  23. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  24. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  25. package/dist/published/theme/hooks.d.ts +4 -3
  26. package/package.json +1 -1
@@ -11,7 +11,7 @@ import { encodePageSlug, getDefaultPages, getPrefixedUrl, transformToWhere } fro
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);
@@ -90,14 +90,27 @@ const ObjectPropertyInput = (props) => {
90
90
  });
91
91
  if (updatedFilter.where) {
92
92
  setLoadingOptions(true);
93
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), (error, instances) => {
93
+ apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
94
94
  if (error) {
95
95
  console.error(error);
96
96
  setLoadingOptions(false);
97
97
  }
98
98
  if (instances && instances.length > 0) {
99
99
  setSelectedInstance(instances[0]);
100
- handleChangeObjectField(id, instances[0]);
100
+ try {
101
+ await handleChangeObjectField(id, instances[0]);
102
+ }
103
+ catch (error) {
104
+ console.error('Failed to update field:', error);
105
+ setLoadingOptions(false);
106
+ return;
107
+ }
108
+ try {
109
+ await onAutosave?.(id);
110
+ }
111
+ catch (error) {
112
+ console.error('Autosave failed:', error);
113
+ }
101
114
  }
102
115
  setLoadingOptions(false);
103
116
  });
@@ -131,7 +144,15 @@ const ObjectPropertyInput = (props) => {
131
144
  }
132
145
  });
133
146
  }
134
- }, [fieldDefinition, updatedCriteria, layout, fetchedOptions, hasFetched, id]);
147
+ }, [
148
+ fieldDefinition,
149
+ updatedCriteria,
150
+ layout,
151
+ fetchedOptions?.[`${id}Options`],
152
+ fetchedOptions?.[`${id}UpdatedCriteria`],
153
+ hasFetched,
154
+ id,
155
+ ]);
135
156
  const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
136
157
  useEffect(() => {
137
158
  if (displayOption === 'dropdown') {
@@ -383,19 +404,38 @@ const ObjectPropertyInput = (props) => {
383
404
  if (selectedInstance?.id) {
384
405
  e.preventDefault();
385
406
  }
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);
407
+ }, loading: loadingOptions, onChange: async (event, value) => {
408
+ try {
409
+ if (isNil(value)) {
410
+ setDropdownInput(undefined);
411
+ setSelectedInstance(undefined);
412
+ await handleChangeObjectField(id, null);
413
+ // Trigger autosave immediately upon clearing
414
+ try {
415
+ await onAutosave?.(id);
416
+ }
417
+ catch (error) {
418
+ console.error('Autosave failed:', error);
419
+ }
420
+ }
421
+ else if (value?.value === '__new__') {
422
+ setOpenCreateDialog(true);
423
+ }
424
+ else {
425
+ const selectedInstance = options.find((o) => o.id === value?.value);
426
+ setSelectedInstance(selectedInstance);
427
+ await handleChangeObjectField(id, selectedInstance);
428
+ // Trigger autosave immediately upon selection
429
+ try {
430
+ await onAutosave?.(id);
431
+ }
432
+ catch (error) {
433
+ console.error('Autosave failed:', error);
434
+ }
435
+ }
394
436
  }
395
- else {
396
- const selectedInstance = options.find((o) => o.id === value?.value);
397
- setSelectedInstance(selectedInstance);
398
- handleChangeObjectField(id, selectedInstance);
437
+ catch (error) {
438
+ console.error('Failed to update field:', error);
399
439
  }
400
440
  }, selectOnFocus: false, onBlur: () => {
401
441
  if (dropdownInput) {
@@ -468,9 +508,21 @@ const ObjectPropertyInput = (props) => {
468
508
  : undefined, "aria-label": selectedInstance?.name }, selectedInstance?.name ? selectedInstance?.name : readOnly && 'None')),
469
509
  !readOnly && selectedInstance ? (React.createElement(Tooltip, { title: `Unlink` },
470
510
  React.createElement("span", null,
471
- React.createElement(IconButton, { onClick: (event) => {
511
+ React.createElement(IconButton, { onClick: async (event) => {
472
512
  event.stopPropagation();
473
- handleChangeObjectField(id, null);
513
+ try {
514
+ await handleChangeObjectField(id, null);
515
+ }
516
+ catch (error) {
517
+ console.error('Failed to update field:', error);
518
+ return;
519
+ }
520
+ try {
521
+ await onAutosave?.(id);
522
+ }
523
+ catch (error) {
524
+ console.error('Autosave failed:', error);
525
+ }
474
526
  setSelectedInstance(undefined);
475
527
  }, sx: { p: 0, marginBottom: '4px' }, "aria-label": `Unlink` },
476
528
  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();
@@ -42,7 +42,20 @@ const RelatedObjectInstance = (props) => {
42
42
  const linkExistingInstance = async () => {
43
43
  if (selectedRow) {
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
+ 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) {
@@ -14,6 +14,7 @@ export type HeaderProps = {
14
14
  form: EvokeForm;
15
15
  errors?: FieldErrors;
16
16
  action?: Action;
17
+ autosaving?: boolean;
17
18
  sx?: SxProps;
18
19
  };
19
20
  declare const Header: React.FC<HeaderProps>;
@@ -1,5 +1,6 @@
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';
@@ -14,6 +15,7 @@ const Header = (props) => {
14
15
  });
15
16
  const isSmallerThanMd = isBelow('md');
16
17
  const { isXs, isSm } = breakpoints;
18
+ const isSmall = isSm || isXs;
17
19
  return (React.createElement(Box, { sx: {
18
20
  paddingX: isSmallerThanMd ? 2 : 3,
19
21
  paddingTop: '0px',
@@ -29,10 +31,20 @@ const Header = (props) => {
29
31
  flex: 1,
30
32
  },
31
33
  } },
32
- title && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
33
- React.createElement(Title, { ...props }))),
34
- hasAccordions && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
35
- React.createElement(AccordionActions, { ...props }))),
34
+ title && (React.createElement(Box, { sx: { flex: '1 1 auto', minWidth: 0, display: 'flex', alignItems: 'center', gap: 1 } },
35
+ React.createElement(Title, { ...props }),
36
+ props.autosaving && !isSmall && React.createElement(SavingIndicator, null))),
37
+ hasAccordions && (React.createElement(Box, { sx: { flex: '0 0 auto', display: 'flex', alignItems: 'center' } },
38
+ React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
39
+ React.createElement(AccordionActions, { ...props })),
40
+ React.createElement(Box, { sx: {
41
+ width: '96px',
42
+ minWidth: '72px',
43
+ display: 'flex',
44
+ justifyContent: 'flex-end',
45
+ alignItems: 'center',
46
+ marginLeft: 0.5,
47
+ } }, props.autosaving && isSmall ? React.createElement(SavingIndicator, null) : React.createElement(Box, { sx: { width: '100%' } })))),
36
48
  React.createElement("div", { ref: validationErrorsRef, className: 'evoke-form-renderer-header' }, shouldShowValidationErrors && !isEmpty(errors) ? React.createElement(ValidationErrors, { errors: errors }) : null)));
37
49
  };
38
50
  // Default slot components for convenience
@@ -63,4 +75,30 @@ export const AccordionActions = ({ onExpandAll, onCollapseAll, expandedSections
63
75
  fontSize: '14px',
64
76
  } }, "Collapse all")));
65
77
  };
78
+ /**
79
+ * SavingIndicator displays a spinning icon with "Saving" text
80
+ * to indicate that an autosave operation is in progress.
81
+ */
82
+ const SavingIndicator = () => (React.createElement(Box, { sx: {
83
+ display: 'flex',
84
+ alignItems: 'center',
85
+ gap: 0.5,
86
+ } },
87
+ React.createElement(Typography, { sx: {
88
+ fontSize: '14px',
89
+ color: 'text.secondary',
90
+ } }, "Saving"),
91
+ React.createElement(Autorenew, { sx: {
92
+ fontSize: '16px',
93
+ color: 'text.secondary',
94
+ animation: 'spin 1s linear infinite',
95
+ '@keyframes spin': {
96
+ '0%': {
97
+ transform: 'rotate(0deg)',
98
+ },
99
+ '100%': {
100
+ transform: 'rotate(360deg)',
101
+ },
102
+ },
103
+ } })));
66
104
  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,7 +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
+ const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
41
42
  // If the entry is hidden, clear its value and any nested values, and skip rendering
42
43
  if (!entryIsVisible(entry, getValues(), instance)) {
43
44
  return null;
@@ -97,9 +98,24 @@ export function RecursiveEntryRenderer(props) {
97
98
  React.createElement(UserProperty, { id: entryId, value: fieldValue, error: !!errors?.[entryId], readOnly: entry.type === 'readonlyField', hasDescription: !!display?.description })));
98
99
  }
99
100
  else if (fieldDefinition.type === 'collection') {
100
- return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
101
- 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) },
102
- React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry })));
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
+ }
103
119
  }
104
120
  else if (fieldDefinition.type === 'richText') {
105
121
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, richTextEditor ? (React.createElement(richTextEditor, {
@@ -111,7 +127,11 @@ export function RecursiveEntryRenderer(props) {
111
127
  disabled: entry.type === 'readonlyField',
112
128
  rows: display?.rowCount,
113
129
  hasError: !!errors?.[entryId],
114
- })) : (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 }))));
130
+ })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, onBlur: () => {
131
+ onAutosave?.(entryId)?.catch((error) => {
132
+ console.error('Autosave failed:', error);
133
+ });
134
+ }, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
115
135
  }
116
136
  else if (fieldDefinition.type === 'document') {
117
137
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -154,7 +174,12 @@ export function RecursiveEntryRenderer(props) {
154
174
  : `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
155
175
  React.createElement(FormField, { id: entryId,
156
176
  // TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
157
- 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, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
177
+ property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, onBlur: () => {
178
+ // Blur event - reads current value from formData
179
+ onAutosave?.(entryId)?.catch((error) => {
180
+ console.error('Autosave failed:', error);
181
+ });
182
+ }, 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'
158
183
  ? display?.booleanDisplay
159
184
  : 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 })));
160
185
  }
@@ -562,6 +562,7 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
562
562
  const allDocuments = [];
563
563
  const formData = new FormData();
564
564
  for (const [index, file] of files.entries()) {
565
+ // Only upload File instances; SavedDocumentReference objects are already uploaded
565
566
  if ('size' in file) {
566
567
  formData.append(`files[${index}]`, file);
567
568
  }
@@ -623,6 +624,7 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
623
624
  export async function formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError) {
624
625
  for (const [key, value] of Object.entries(submission)) {
625
626
  if (isArray(value)) {
627
+ // Only upload if array contains File instances (not SavedDocumentReference)
626
628
  const fileInArray = value.some((item) => item instanceof File);
627
629
  if (fileInArray && instanceId && apiServices && objectId) {
628
630
  try {