@evoke-platform/ui-components 1.10.0-testing.11 → 1.10.0-testing.13
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.
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +3 -1
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +82 -13
- package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Footer.js +3 -3
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +30 -13
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +16 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +70 -18
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +37 -15
- package/dist/published/components/custom/FormV2/components/Header.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Header.js +44 -6
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +12 -3
- package/dist/published/components/custom/FormV2/components/utils.js +2 -0
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +449 -1
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
- package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
- package/dist/published/theme/hooks.d.ts +4 -3
- package/package.json +1 -1
|
@@ -16,7 +16,8 @@ export type FormRendererProps = BaseProps & {
|
|
|
16
16
|
form: EvokeForm;
|
|
17
17
|
title?: string | React.ReactNode;
|
|
18
18
|
instance?: ObjectInstance | Document;
|
|
19
|
-
onChange: (id: string, value: unknown) => void
|
|
19
|
+
onChange: (id: string, value: unknown) => void | Promise<void>;
|
|
20
|
+
onAutosave?: (fieldId: string) => void | Promise<void>;
|
|
20
21
|
associatedObject?: {
|
|
21
22
|
instanceId?: string;
|
|
22
23
|
propertyId?: string;
|
|
@@ -12,7 +12,7 @@ import { assignIdsToSectionsAndRichText, convertDocToParameters, convertProperti
|
|
|
12
12
|
import { handleValidation } from './components/ValidationFiles/Validation';
|
|
13
13
|
import ValidationErrors from './components/ValidationFiles/ValidationErrors';
|
|
14
14
|
const FormRendererInternal = (props) => {
|
|
15
|
-
const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, fieldHeight, richTextEditor, form, instance, onChange, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
15
|
+
const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, fieldHeight, richTextEditor, form, instance, onChange, onAutosave, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
16
16
|
const { entries, name: title, objectId, actionId, display } = form;
|
|
17
17
|
const { register, unregister, setValue, reset, handleSubmit, formState: { errors, isSubmitted }, getValues, } = useForm({
|
|
18
18
|
defaultValues: value,
|
|
@@ -165,6 +165,7 @@ const FormRendererInternal = (props) => {
|
|
|
165
165
|
action,
|
|
166
166
|
discardChangesButtonLabel: 'Discard Changes',
|
|
167
167
|
submitButtonLabel: display?.submitLabel ?? 'Submit',
|
|
168
|
+
disableDiscardChanges: !!form?.autosaveActionId,
|
|
168
169
|
};
|
|
169
170
|
return (React.createElement(Box, { ref: containerRef },
|
|
170
171
|
React.createElement(FormContext.Provider, { value: {
|
|
@@ -182,6 +183,7 @@ const FormRendererInternal = (props) => {
|
|
|
182
183
|
parameters,
|
|
183
184
|
fieldHeight,
|
|
184
185
|
handleChange: onChange,
|
|
186
|
+
onAutosave,
|
|
185
187
|
triggerFieldReset,
|
|
186
188
|
showSubmitError: isSubmitted,
|
|
187
189
|
associatedObject,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
|
|
2
2
|
import axios from 'axios';
|
|
3
|
-
import { get, isArray, isEmpty, isEqual, merge, omit, pick, set, uniq } from 'lodash';
|
|
4
|
-
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { cloneDeep, get, isArray, isEmpty, isEqual, merge, omit, pick, set, uniq } from 'lodash';
|
|
4
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { Skeleton, Snackbar } from '../../core';
|
|
6
6
|
import { Box } from '../../layout';
|
|
7
7
|
import ErrorComponent from '../ErrorComponent';
|
|
8
8
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
9
|
+
import Header from './components/Header';
|
|
9
10
|
import { convertDocToEntries, deleteDocuments, encodePageSlug, formatDataToDoc, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
|
|
10
11
|
import FormRenderer from './FormRenderer';
|
|
11
12
|
function FormRendererContainer(props) {
|
|
@@ -20,14 +21,34 @@ function FormRendererContainer(props) {
|
|
|
20
21
|
const [parameters, setParameters] = useState();
|
|
21
22
|
const [document, setDocument] = useState();
|
|
22
23
|
const [instance, setInstance] = useState();
|
|
23
|
-
const
|
|
24
|
+
const formDataRef = useRef();
|
|
25
|
+
// We only need the setter to force a re-render when form data updates; the value itself
|
|
26
|
+
// is intentionally not referenced elsewhere to avoid stale reads (we use formDataRef).
|
|
27
|
+
// Keep the setter to allow updating a version counter without declaring the value
|
|
28
|
+
// which would trigger a lint error for being unused.
|
|
29
|
+
const [, setFormDataVersion] = useState(0);
|
|
24
30
|
const [action, setAction] = useState();
|
|
31
|
+
/**
|
|
32
|
+
* Updates form data synchronously and triggers a re-render.
|
|
33
|
+
*
|
|
34
|
+
* This function uses a ref for synchronous updates (to avoid race conditions in autosave)
|
|
35
|
+
* combined with a version counter to trigger React re-renders. This ensures that:
|
|
36
|
+
* 1. formDataRef.current is updated immediately (synchronous)
|
|
37
|
+
* 2. Components that depend on formData will re-render (via version increment)
|
|
38
|
+
* 3. Autosave always reads the latest data without timing issues
|
|
39
|
+
*/
|
|
40
|
+
const setFormData = (newData) => {
|
|
41
|
+
formDataRef.current = newData;
|
|
42
|
+
setFormDataVersion((v) => v + 1);
|
|
43
|
+
};
|
|
25
44
|
const [error, setError] = useState();
|
|
26
45
|
const [form, setForm] = useState();
|
|
27
46
|
const [snackbarError, setSnackbarError] = useState({
|
|
28
47
|
showAlert: false,
|
|
29
48
|
isError: true,
|
|
30
49
|
});
|
|
50
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
51
|
+
const [lastSavedData, setLastSavedData] = useState({});
|
|
31
52
|
const userAccount = useAuthenticationContext()?.account;
|
|
32
53
|
const objectStore = useObject(form?.objectId ?? objectId);
|
|
33
54
|
const onError = (err) => {
|
|
@@ -189,6 +210,8 @@ function FormRendererContainer(props) {
|
|
|
189
210
|
if (document && objectId) {
|
|
190
211
|
const defaultValues = await getDefaultValues(convertDocToEntries(document), document);
|
|
191
212
|
setFormData(defaultValues);
|
|
213
|
+
// Deep clone to avoid reference issues
|
|
214
|
+
setLastSavedData(cloneDeep(defaultValues));
|
|
192
215
|
if (!form) {
|
|
193
216
|
setForm({
|
|
194
217
|
id: 'documentForm',
|
|
@@ -201,6 +224,8 @@ function FormRendererContainer(props) {
|
|
|
201
224
|
else if (form && (instance || !instanceId)) {
|
|
202
225
|
const defaultValues = await getDefaultValues(form.entries, instance || {});
|
|
203
226
|
setFormData(defaultValues);
|
|
227
|
+
// Deep clone to avoid reference issues
|
|
228
|
+
setLastSavedData(cloneDeep(defaultValues));
|
|
204
229
|
}
|
|
205
230
|
};
|
|
206
231
|
getInitialValues();
|
|
@@ -351,7 +376,7 @@ function FormRendererContainer(props) {
|
|
|
351
376
|
for (const { fieldId, fieldValue } of defaultValuesArray) {
|
|
352
377
|
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
353
378
|
if (parameter?.type === 'object') {
|
|
354
|
-
const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId,
|
|
379
|
+
const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
|
|
355
380
|
for (const field of dependentFields) {
|
|
356
381
|
set(result, field.fieldId, field.fieldValue);
|
|
357
382
|
}
|
|
@@ -382,6 +407,45 @@ function FormRendererContainer(props) {
|
|
|
382
407
|
await processEntries(entries);
|
|
383
408
|
return result;
|
|
384
409
|
};
|
|
410
|
+
const handleAutosave = async (fieldId) => {
|
|
411
|
+
if (!form?.autosaveActionId || !formDataRef.current) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const currentValue = get(formDataRef.current, fieldId);
|
|
415
|
+
const lastValue = get(lastSavedData, fieldId);
|
|
416
|
+
if (isEqual(currentValue, lastValue)) {
|
|
417
|
+
return; // Field hasn't changed, skip save
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
setIsSaving(true);
|
|
421
|
+
const submission = await formatSubmission(formDataRef.current, apiServices, objectId, instanceId, form, setSnackbarError);
|
|
422
|
+
// Handle document autosave
|
|
423
|
+
if (dataType === 'documents' && document) {
|
|
424
|
+
await apiServices.patch(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/documents/${documentId}`), pick(submission, ['metadata']).metadata ?? submission);
|
|
425
|
+
setDocument((prev) => ({
|
|
426
|
+
...prev,
|
|
427
|
+
metadata: submission.metadata,
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
// Handle object instance autosave
|
|
431
|
+
else if (instanceId && action?.type === 'update') {
|
|
432
|
+
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
433
|
+
actionId: form.autosaveActionId,
|
|
434
|
+
input: pick(submission, sanitizedObject?.properties
|
|
435
|
+
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
436
|
+
.map((property) => property.id) ?? []),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
setLastSavedData(cloneDeep(formDataRef.current));
|
|
440
|
+
setIsSaving(false);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
console.error('Autosave failed:', error);
|
|
444
|
+
setIsSaving(false);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
// Autosave is enabled if form.autosaveActionId exists.
|
|
448
|
+
const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
|
|
385
449
|
async function onChange(id, value) {
|
|
386
450
|
const parameter = parameters?.find((param) => param.id === id);
|
|
387
451
|
const entries = getUnnestedEntries(form.entries);
|
|
@@ -392,7 +456,7 @@ function FormRendererContainer(props) {
|
|
|
392
456
|
if (parameter) {
|
|
393
457
|
if (parameter.type === 'object' && parameters && parameters.length > 0) {
|
|
394
458
|
// On change of a related object, update default values dependent on that object
|
|
395
|
-
const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id,
|
|
459
|
+
const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
|
|
396
460
|
for (const field of dependentFields) {
|
|
397
461
|
onChange(field.fieldId, field.fieldValue);
|
|
398
462
|
}
|
|
@@ -403,16 +467,21 @@ function FormRendererContainer(props) {
|
|
|
403
467
|
value = value.value ? value.value : value;
|
|
404
468
|
}
|
|
405
469
|
}
|
|
406
|
-
if (!isEqual(value, get(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return newData;
|
|
411
|
-
});
|
|
470
|
+
if (!isEqual(value, get(formDataRef.current, id))) {
|
|
471
|
+
const newData = { ...formDataRef.current };
|
|
472
|
+
set(newData, id, value);
|
|
473
|
+
setFormData(newData);
|
|
412
474
|
}
|
|
413
475
|
}
|
|
414
|
-
const isLoading = (instanceId && !
|
|
476
|
+
const isLoading = (instanceId && !formDataRef.current && !document) || !form || !sanitizedObject;
|
|
415
477
|
const status = error ? 'error' : isLoading ? 'loading' : 'ready';
|
|
478
|
+
// Compose a header renderer that injects the saving indicator into the rendered header
|
|
479
|
+
const composedRenderHeader = (props) => {
|
|
480
|
+
if (renderHeader) {
|
|
481
|
+
return renderHeader({ ...props, autosaving: !!form?.autosaveActionId && isSaving });
|
|
482
|
+
}
|
|
483
|
+
return React.createElement(Header, { ...props, autosaving: !!form?.autosaveActionId && isSaving });
|
|
484
|
+
};
|
|
416
485
|
const onDiscardChanges = onDiscardChangesOverride
|
|
417
486
|
? onDiscardChangesOverride
|
|
418
487
|
: async () => {
|
|
@@ -431,7 +500,7 @@ function FormRendererContainer(props) {
|
|
|
431
500
|
padding: '0px',
|
|
432
501
|
border: !isLoading ? '1px solid #dbe0e4' : undefined,
|
|
433
502
|
...sx,
|
|
434
|
-
} }, !isLoading ? (React.createElement(React.Fragment, null, form && sanitizedObject && (React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value:
|
|
503
|
+
} }, !isLoading ? (React.createElement(React.Fragment, null, form && sanitizedObject && (React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: dataType !== 'documents' ? instance : document, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: document && !hasDocumentUpdateAccess ? () => React.createElement(React.Fragment, null) : renderFooter })))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
435
504
|
React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
|
|
436
505
|
React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
|
|
437
506
|
React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
|
|
@@ -8,6 +8,7 @@ export type FooterProps = {
|
|
|
8
8
|
submitButtonLabel?: string;
|
|
9
9
|
discardChangesButtonLabel?: string;
|
|
10
10
|
sx?: SxProps;
|
|
11
|
+
disableDiscardChanges?: boolean;
|
|
11
12
|
};
|
|
12
13
|
export declare const Footer: React.FC<FooterProps>;
|
|
13
14
|
export type FooterActionsProps = Omit<FooterProps, 'sx'>;
|
|
@@ -28,7 +28,7 @@ export const Footer = (props) => {
|
|
|
28
28
|
React.createElement(FooterActions, { ...props })));
|
|
29
29
|
};
|
|
30
30
|
export const FooterActions = (props) => {
|
|
31
|
-
const { action, onDiscardChanges, onSubmit, submitButtonLabel, discardChangesButtonLabel } = props;
|
|
31
|
+
const { action, onDiscardChanges, onSubmit, submitButtonLabel, discardChangesButtonLabel, disableDiscardChanges } = props;
|
|
32
32
|
const { width } = useContext(FormContext);
|
|
33
33
|
const [loading, setLoading] = React.useState(false);
|
|
34
34
|
const handleSubmit = async () => {
|
|
@@ -46,7 +46,7 @@ export const FooterActions = (props) => {
|
|
|
46
46
|
});
|
|
47
47
|
const { isXs } = breakpoints;
|
|
48
48
|
return (React.createElement(React.Fragment, null,
|
|
49
|
-
React.createElement(Button, { onClick: onDiscardChanges, variant: "outlined", sx: {
|
|
49
|
+
!disableDiscardChanges && (React.createElement(Button, { onClick: onDiscardChanges, variant: "outlined", sx: {
|
|
50
50
|
margin: '5px',
|
|
51
51
|
marginX: isXs ? '0px' : undefined,
|
|
52
52
|
color: 'black',
|
|
@@ -56,7 +56,7 @@ export const FooterActions = (props) => {
|
|
|
56
56
|
backgroundColor: '#f2f4f7',
|
|
57
57
|
border: '1px solid rgb(206, 212, 218)',
|
|
58
58
|
},
|
|
59
|
-
} }, discardChangesButtonLabel),
|
|
59
|
+
} }, discardChangesButtonLabel)),
|
|
60
60
|
React.createElement(LoadingButton, { onClick: handleSubmit, variant: "contained", sx: {
|
|
61
61
|
lineHeight: '2.75',
|
|
62
62
|
margin: '5px 0 5px 5px',
|
|
@@ -15,7 +15,8 @@ type FormContextType = {
|
|
|
15
15
|
setExpandedSections?: React.Dispatch<React.SetStateAction<ExpandedSection[]>>;
|
|
16
16
|
setExpandAll?: React.Dispatch<React.SetStateAction<boolean | undefined | null>>;
|
|
17
17
|
parameters?: InputParameter[];
|
|
18
|
-
handleChange: (name: string, value: unknown) => void
|
|
18
|
+
handleChange: (name: string, value: unknown) => void | Promise<void>;
|
|
19
|
+
onAutosave?: (fieldId: string) => void | Promise<void>;
|
|
19
20
|
fieldHeight?: 'small' | 'medium';
|
|
20
21
|
triggerFieldReset?: boolean;
|
|
21
22
|
showSubmitError?: boolean;
|
|
@@ -7,7 +7,7 @@ import FieldWrapper from '../FieldWrapper';
|
|
|
7
7
|
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
8
8
|
function AddressFields(props) {
|
|
9
9
|
const { entry, readOnly, entryId, fieldDefinition } = props;
|
|
10
|
-
const { getValues, instance, errors, handleChange, fieldHeight, parameters } = useFormContext();
|
|
10
|
+
const { getValues, instance, errors, handleChange, onAutosave, fieldHeight, parameters } = useFormContext();
|
|
11
11
|
const apiServices = useApiServices();
|
|
12
12
|
const addressObject = entryId.split('.')[0];
|
|
13
13
|
const addressField = entryId.split('.')[1];
|
|
@@ -22,24 +22,41 @@ function AddressFields(props) {
|
|
|
22
22
|
params: { query: query },
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
|
-
const handleAddressChange = (name, value) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
25
|
+
const handleAddressChange = async (name, value) => {
|
|
26
|
+
try {
|
|
27
|
+
if (addressField === 'line1' && typeof value === 'object' && value.line1) {
|
|
28
|
+
const addressKeys = ['line1', 'city', 'county', 'state', 'zipCode'];
|
|
29
|
+
// Await each handleChange sequentially to ensure proper order
|
|
30
|
+
for (const key of addressKeys) {
|
|
31
|
+
const fullKey = `${addressObject}.${key}`;
|
|
32
|
+
if (parameters?.some((p) => p.id === fullKey)) {
|
|
33
|
+
const fieldValue = value[key];
|
|
34
|
+
await handleChange(fullKey, fieldValue);
|
|
35
|
+
}
|
|
33
36
|
}
|
|
34
|
-
|
|
37
|
+
// Autosave immediately after autocomplete fills all fields
|
|
38
|
+
try {
|
|
39
|
+
await onAutosave?.(entryId);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error('Autosave failed:', error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
await handleChange(name, value);
|
|
47
|
+
}
|
|
35
48
|
}
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('Failed to update field:', error);
|
|
38
51
|
}
|
|
39
52
|
};
|
|
40
53
|
const addressErrors = errors?.[addressObject];
|
|
41
54
|
const addressFieldError = addressErrors?.[addressField];
|
|
42
|
-
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: "string", label: display?.label || 'default', description: !readOnly ? display?.description : undefined, tooltip: display?.tooltip, value: fieldValue, maxLength: 'maxLength' in validation ? validation?.maxLength : 0, required: entry.display?.required || false, showCharCount: !readOnly && display?.charCount, viewOnly: !!readOnly, prefix: display?.prefix, suffix: display?.suffix }, !readOnly ? (React.createElement(FormField, { property: fieldDefinition, defaultValue: fieldValue, onChange: handleAddressChange,
|
|
55
|
+
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: "string", label: display?.label || 'default', description: !readOnly ? display?.description : undefined, tooltip: display?.tooltip, value: fieldValue, maxLength: 'maxLength' in validation ? validation?.maxLength : 0, required: entry.display?.required || false, showCharCount: !readOnly && display?.charCount, viewOnly: !!readOnly, prefix: display?.prefix, suffix: display?.suffix }, !readOnly ? (React.createElement(FormField, { property: fieldDefinition, defaultValue: fieldValue, onChange: handleAddressChange, onBlur: () => {
|
|
56
|
+
onAutosave?.(entryId)?.catch((error) => {
|
|
57
|
+
console.error('Autosave failed:', error);
|
|
58
|
+
});
|
|
59
|
+
}, isMultiLineText: !!display?.rowCount, readOnly: entry.type === 'readonlyField', ...(addressField === 'line1' && { queryAddresses }), mask: validation?.mask, placeholder: display?.placeholder, isOptionEqualToValue: isOptionEqualToValue, size: fieldHeight, error: !!addressFieldError, errorMessage: addressFieldError?.message, additionalProps: {
|
|
43
60
|
...(display?.description && {
|
|
44
61
|
inputProps: {
|
|
45
62
|
'aria-describedby': `${entryId}-description`,
|
|
@@ -8,7 +8,7 @@ import { addressProperties, getPrefixedUrl } from '../utils';
|
|
|
8
8
|
export default function Criteria(props) {
|
|
9
9
|
const { value, canUpdateProperty, fieldDefinition, error } = props;
|
|
10
10
|
const apiServices = useApiServices();
|
|
11
|
-
const { fetchedOptions, setFetchedOptions, handleChange } = useFormContext();
|
|
11
|
+
const { fetchedOptions, setFetchedOptions, handleChange, onAutosave } = useFormContext();
|
|
12
12
|
const [loadingError, setLoadingError] = useState(false);
|
|
13
13
|
const [loading, setLoading] = useState(false);
|
|
14
14
|
const [properties, setProperties] = useState(fetchedOptions[`${fieldDefinition.id}Options`] || []);
|
|
@@ -57,9 +57,22 @@ export default function Criteria(props) {
|
|
|
57
57
|
useEffect(() => {
|
|
58
58
|
fetchProperties();
|
|
59
59
|
}, [fetchProperties]);
|
|
60
|
-
const handleUpdate = (criteria) => {
|
|
60
|
+
const handleUpdate = async (criteria) => {
|
|
61
61
|
if (criteria || value) {
|
|
62
|
-
|
|
62
|
+
const newValue = criteria ?? null;
|
|
63
|
+
try {
|
|
64
|
+
await handleChange(fieldDefinition.id, newValue);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error('Failed to update field:', error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await onAutosave?.(fieldDefinition.id);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('Autosave failed:', error);
|
|
75
|
+
}
|
|
63
76
|
}
|
|
64
77
|
};
|
|
65
78
|
if (loadingError) {
|
package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js
CHANGED
|
@@ -12,7 +12,7 @@ import { DocumentList } from './DocumentList';
|
|
|
12
12
|
export const Document = (props) => {
|
|
13
13
|
const { id, canUpdateProperty, error, value, validate, hasDescription } = props;
|
|
14
14
|
const apiServices = useApiServices();
|
|
15
|
-
const { fetchedOptions, setFetchedOptions, object, handleChange, instance } = useFormContext();
|
|
15
|
+
const { fetchedOptions, setFetchedOptions, object, handleChange, onAutosave: onAutosave, instance, } = useFormContext();
|
|
16
16
|
const [snackbarError, setSnackbarError] = useState();
|
|
17
17
|
const [documents, setDocuments] = useState();
|
|
18
18
|
const [hasUpdatePermission, setHasUpdatePermission] = useState(fetchedOptions[`${id}UpdatePermission`]);
|
|
@@ -49,13 +49,25 @@ export const Document = (props) => {
|
|
|
49
49
|
checkPermissions();
|
|
50
50
|
}, [checkPermissions]);
|
|
51
51
|
const handleUpload = async (files) => {
|
|
52
|
+
// Store File objects in form state - they will be uploaded during autosave via formatSubmission()
|
|
52
53
|
const newDocuments = [...(documents ?? []), ...(files ?? [])];
|
|
53
54
|
setDocuments(newDocuments);
|
|
54
|
-
|
|
55
|
+
try {
|
|
56
|
+
await handleChange(id, newDocuments);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error('Failed to update field:', error);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
await onAutosave?.(id);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error('Autosave failed:', error);
|
|
67
|
+
}
|
|
55
68
|
};
|
|
56
69
|
const uploadDisabled = !!validate?.maxDocuments && (documents?.length ?? 0) >= validate.maxDocuments;
|
|
57
70
|
const { getRootProps, getInputProps, open, fileRejections } = useDropzone({
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
71
|
onDrop: (files) => handleUpload(files),
|
|
60
72
|
disabled: uploadDisabled,
|
|
61
73
|
accept: validate?.allowedFileExtensions
|
|
@@ -109,7 +121,7 @@ export const Document = (props) => {
|
|
|
109
121
|
} }, validate?.maxDocuments === 1
|
|
110
122
|
? `Maximum size is ${formattedMaxSize}.`
|
|
111
123
|
: `The maximum size of each document is ${formattedMaxSize}.`)))))),
|
|
112
|
-
canUpdateProperty && isNil(hasUpdatePermission) ? (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })) : (React.createElement(DocumentList, { id: id, handleChange: handleChange, value: value, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission })),
|
|
124
|
+
canUpdateProperty && isNil(hasUpdatePermission) ? (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })) : (React.createElement(DocumentList, { id: id, handleChange: handleChange, onAutosave: onAutosave, value: value, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission })),
|
|
113
125
|
React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
|
|
114
126
|
errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
|
|
115
127
|
React.createElement(InfoRounded, { sx: { fontSize: '.75rem', marginRight: '3px', color: '#D3271B' } }),
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { SavedDocumentReference } from '../../types';
|
|
3
3
|
type DocumentListProps = {
|
|
4
4
|
handleChange: (propertyId: string, value: (File | SavedDocumentReference)[] | undefined) => void;
|
|
5
|
+
onAutosave?: (fieldId: string) => void | Promise<void>;
|
|
5
6
|
id: string;
|
|
6
7
|
canUpdateProperty: boolean;
|
|
7
8
|
value: (File | SavedDocumentReference)[] | undefined;
|
|
@@ -24,7 +24,7 @@ const viewableFileTypes = [
|
|
|
24
24
|
'text/plain',
|
|
25
25
|
];
|
|
26
26
|
export const DocumentList = (props) => {
|
|
27
|
-
const { handleChange, id, canUpdateProperty, value: documents, setSnackbarError } = props;
|
|
27
|
+
const { handleChange, onAutosave, id, canUpdateProperty, value: documents, setSnackbarError } = props;
|
|
28
28
|
const apiServices = useApiServices();
|
|
29
29
|
const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
|
|
30
30
|
const [hasViewPermission, setHasViewPermission] = useState(fetchedOptions[`${id}ViewPermission`] ?? true);
|
|
@@ -88,9 +88,22 @@ export const DocumentList = (props) => {
|
|
|
88
88
|
};
|
|
89
89
|
const isFile = (doc) => doc instanceof File;
|
|
90
90
|
const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
|
|
91
|
-
const handleRemove = (index) => {
|
|
91
|
+
const handleRemove = async (index) => {
|
|
92
92
|
const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
|
|
93
|
-
|
|
93
|
+
const newValue = updatedDocuments.length === 0 ? undefined : updatedDocuments;
|
|
94
|
+
try {
|
|
95
|
+
await handleChange(id, newValue);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('Failed to update field:', error);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await onAutosave?.(id);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('Autosave failed:', error);
|
|
106
|
+
}
|
|
94
107
|
};
|
|
95
108
|
const openDocument = async (index) => {
|
|
96
109
|
const doc = documents?.[index];
|
|
@@ -55,7 +55,7 @@ const styles = {
|
|
|
55
55
|
};
|
|
56
56
|
export const Image = (props) => {
|
|
57
57
|
const { id, canUpdateProperty, error, value, hasDescription } = props;
|
|
58
|
-
const { handleChange } = useFormContext();
|
|
58
|
+
const { handleChange, onAutosave: onAutosave } = useFormContext();
|
|
59
59
|
const [image, setImage] = useState();
|
|
60
60
|
useEffect(() => {
|
|
61
61
|
if (typeof value === 'string') {
|
|
@@ -63,15 +63,41 @@ export const Image = (props) => {
|
|
|
63
63
|
}
|
|
64
64
|
}, [value]);
|
|
65
65
|
const handleUpload = async (file) => {
|
|
66
|
+
// max file size 300KB
|
|
66
67
|
if (file?.size && file.size <= 300000) {
|
|
67
|
-
const dataUrl =
|
|
68
|
+
const dataUrl = await blobToDataUrl(file);
|
|
68
69
|
setImage(dataUrl);
|
|
69
|
-
|
|
70
|
+
try {
|
|
71
|
+
await handleChange(id, dataUrl);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('Failed to update field:', error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await onAutosave?.(id);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error('Autosave failed:', error);
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
};
|
|
72
|
-
const handleRemove = (e) => {
|
|
85
|
+
const handleRemove = async (e) => {
|
|
73
86
|
setImage(null);
|
|
74
|
-
|
|
87
|
+
try {
|
|
88
|
+
await handleChange(id, '');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Failed to update field:', error);
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await onAutosave?.(id);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error('Autosave failed:', error);
|
|
100
|
+
}
|
|
75
101
|
e.stopPropagation();
|
|
76
102
|
};
|
|
77
103
|
const { getRootProps, getInputProps, open } = useDropzone({
|
|
@@ -6,7 +6,7 @@ import { Autocomplete, IconButton, Paper, TextField, Typography } from '../../..
|
|
|
6
6
|
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
7
7
|
const UserProperty = (props) => {
|
|
8
8
|
const { id, error, value, readOnly, hasDescription } = props;
|
|
9
|
-
const { fetchedOptions, setFetchedOptions, handleChange, fieldHeight } = useFormContext();
|
|
9
|
+
const { fetchedOptions, setFetchedOptions, handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
|
|
10
10
|
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
11
11
|
const apiServices = useApiServices();
|
|
12
12
|
const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
|
|
@@ -40,9 +40,21 @@ const UserProperty = (props) => {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
}, [id]);
|
|
43
|
-
function handleChangeUserProperty(id, value) {
|
|
43
|
+
async function handleChangeUserProperty(id, value) {
|
|
44
44
|
const updatedValue = typeof value?.value === 'string' ? { name: value.label, id: value.value } : null;
|
|
45
|
-
|
|
45
|
+
try {
|
|
46
|
+
await handleChange(id, updatedValue);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('Failed to update field:', error);
|
|
50
|
+
return; // Exit early if handleChange fails
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await onAutosave?.(id);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('Autosave failed:', error);
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
return (options && (React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, popupIcon: userValue || readOnly ? '' : React.createElement(ExpandMore, null), clearIcon: !loadingOptions && userValue ? (React.createElement(IconButton, { size: "small", disableRipple: true, onKeyDown: (e) => {
|
|
48
60
|
if (e.key === 'Enter') {
|