@evoke-platform/ui-components 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +242 -142
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
- package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/Form/utils.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +2 -8
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +4 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +229 -126
- package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +1 -1
- package/dist/published/components/custom/FormV2/components/DefaultValues.js +60 -89
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -1
- package/dist/published/components/custom/FormV2/components/FormContext.js +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +43 -16
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.d.ts +2 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +2 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +6 -2
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +16 -13
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -8
- package/dist/published/components/custom/FormV2/components/utils.js +168 -82
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +6 -1
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
- package/dist/published/stories/FormRenderer.stories.d.ts +6 -3
- package/dist/published/stories/FormRendererContainer.stories.d.ts +20 -0
- package/dist/published/theme/hooks.d.ts +3 -3
- package/package.json +3 -1
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
|
|
2
2
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
3
|
import axios from 'axios';
|
|
4
|
-
import { cloneDeep, get, isArray, isEmpty, isEqual,
|
|
5
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { cloneDeep, get, isArray, isEmpty, isEqual, isObject, pick, set } from 'lodash';
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { Skeleton, Snackbar } from '../../core';
|
|
7
7
|
import { Box } from '../../layout';
|
|
8
8
|
import ErrorComponent from '../ErrorComponent';
|
|
9
9
|
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
10
10
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
11
11
|
import Header from './components/Header';
|
|
12
|
-
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
12
|
+
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, handleFileUpload, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
13
13
|
import FormRenderer from './FormRenderer';
|
|
14
|
+
import { DepGraph } from 'dependency-graph';
|
|
14
15
|
// Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
|
|
15
16
|
function FormRendererContainer(props) {
|
|
16
17
|
return (React.createElement(ConditionalQueryClientProvider, null,
|
|
@@ -23,7 +24,7 @@ function FormRendererContainerInner(props) {
|
|
|
23
24
|
const navigateTo = useNavigate();
|
|
24
25
|
const queryClient = useQueryClient();
|
|
25
26
|
const { id: appId } = useApp();
|
|
26
|
-
const [parameters, setParameters] = useState();
|
|
27
|
+
const [parameters, setParameters] = useState([]);
|
|
27
28
|
const formDataRef = useRef();
|
|
28
29
|
// We only need the setter to force a re-render when form data updates; the value itself
|
|
29
30
|
// is intentionally not referenced elsewhere to avoid stale reads (we use formDataRef).
|
|
@@ -53,7 +54,54 @@ function FormRendererContainerInner(props) {
|
|
|
53
54
|
const [isSaving, setIsSaving] = useState(false);
|
|
54
55
|
const [lastSavedData, setLastSavedData] = useState({});
|
|
55
56
|
const [uniquePresetValues, setUniquePresetValues] = useState([]);
|
|
56
|
-
const flattenFormEntries = useMemo(() =>
|
|
57
|
+
const flattenFormEntries = useMemo(() => {
|
|
58
|
+
const graph = new DepGraph({ circular: true });
|
|
59
|
+
const unnestedEntries = getUnnestedEntries(form?.entries || []);
|
|
60
|
+
const nonInputEntries = [];
|
|
61
|
+
for (const entry of unnestedEntries) {
|
|
62
|
+
const entryId = getEntryId(entry);
|
|
63
|
+
if (entryId && entry.type !== 'readonlyField') {
|
|
64
|
+
graph.addNode(entryId, entry);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
nonInputEntries.push(entry);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const entry of unnestedEntries) {
|
|
71
|
+
const entryId = getEntryId(entry);
|
|
72
|
+
if (entryId && (entry.type === 'input' || entry.type === 'inputField')) {
|
|
73
|
+
const { defaultValue } = entry.display || {};
|
|
74
|
+
let presetValues = [];
|
|
75
|
+
if (typeof defaultValue === 'string') {
|
|
76
|
+
presetValues = extractPresetValuesFromDynamicDefaultValues([entry]);
|
|
77
|
+
}
|
|
78
|
+
else if (isObject(defaultValue) && 'criteria' in defaultValue && !isEmpty(defaultValue.criteria)) {
|
|
79
|
+
presetValues = extractPresetValuesFromCriteria(defaultValue.criteria);
|
|
80
|
+
}
|
|
81
|
+
presetValues.forEach((presetValue) => {
|
|
82
|
+
const fragments = presetValue.replace(/{{{input.|}}}|{{input.|}}/g, '').split('.');
|
|
83
|
+
// preset value references top level fields i.e name or dateOfBirth
|
|
84
|
+
if (fragments.length === 1 && graph.hasNode(fragments[0])) {
|
|
85
|
+
graph.addDependency(entryId, fragments[0]);
|
|
86
|
+
}
|
|
87
|
+
else if (fragments.length > 1) {
|
|
88
|
+
// preset value references nested fields i.e address.line1 or person.address.line1
|
|
89
|
+
const addressKeys = ['line1', 'line2', 'city', 'county', 'state', 'zipCode', 'country'];
|
|
90
|
+
if (addressKeys.includes(fragments[1]) && graph.hasNode(`${fragments[0]}.${fragments[1]}`)) {
|
|
91
|
+
graph.addDependency(entryId, `${fragments[0]}.${fragments[1]}`);
|
|
92
|
+
}
|
|
93
|
+
else if (graph.hasNode(fragments[0])) {
|
|
94
|
+
graph.addDependency(entryId, fragments[0]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return graph
|
|
101
|
+
.overallOrder()
|
|
102
|
+
.map((id) => graph.getNodeData(id))
|
|
103
|
+
.concat(nonInputEntries);
|
|
104
|
+
}, [form?.entries]);
|
|
57
105
|
const userAccount = useAuthenticationContext()?.account;
|
|
58
106
|
const objectStore = useObject(form?.objectId ?? objectId);
|
|
59
107
|
const onError = (err) => {
|
|
@@ -71,7 +119,7 @@ function FormRendererContainerInner(props) {
|
|
|
71
119
|
});
|
|
72
120
|
// trigger refetch on success
|
|
73
121
|
const { data: instance, error: instanceError } = useQuery({
|
|
74
|
-
queryKey: [objectId, instanceId, 'instance'],
|
|
122
|
+
queryKey: [objectId, instanceId, 'instance', uniquePresetValues],
|
|
75
123
|
queryFn: async () => {
|
|
76
124
|
const instance = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}`), {
|
|
77
125
|
params: {
|
|
@@ -93,6 +141,93 @@ function FormRendererContainerInner(props) {
|
|
|
93
141
|
staleTime: Infinity,
|
|
94
142
|
enabled: !!instanceId && !!sanitizedObject,
|
|
95
143
|
});
|
|
144
|
+
const getDefaultValues = useCallback(async (unnestedEntries, instanceData) => {
|
|
145
|
+
const result = cloneDeep(instanceData);
|
|
146
|
+
for (const entry of unnestedEntries) {
|
|
147
|
+
const fieldId = getEntryId(entry);
|
|
148
|
+
if (!fieldId)
|
|
149
|
+
continue;
|
|
150
|
+
const fieldValue = get(result, fieldId);
|
|
151
|
+
if ((entry.type === 'input' || entry.type === 'inputField') &&
|
|
152
|
+
isAddressProperty(entry.parameterId || entry.input?.id)) {
|
|
153
|
+
if ((isEmpty(result) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
|
|
154
|
+
entry?.display?.defaultValue &&
|
|
155
|
+
parameters) {
|
|
156
|
+
const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, result);
|
|
157
|
+
if (isArray(defaultValuesArray)) {
|
|
158
|
+
defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
|
|
159
|
+
set(result, fieldId, fieldValue);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
164
|
+
set(result, fieldId, fieldValue);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
|
|
168
|
+
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
169
|
+
if (associatedObject &&
|
|
170
|
+
'propertyId' in associatedObject &&
|
|
171
|
+
associatedObject?.propertyId === fieldId &&
|
|
172
|
+
associatedObject?.instanceId &&
|
|
173
|
+
(parameter || associatedObject.objectId) &&
|
|
174
|
+
action?.type === 'create') {
|
|
175
|
+
try {
|
|
176
|
+
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter?.objectId || associatedObject.objectId}/instances/${associatedObject.instanceId}`), {
|
|
177
|
+
params: {
|
|
178
|
+
expand: uniquePresetValues.filter((value) => value.startsWith(`{{{input.${fieldId}.`) ||
|
|
179
|
+
value.startsWith(`{{input.${fieldId}.`)),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
result[associatedObject.propertyId] = instance;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (entry.type === 'formlet') {
|
|
189
|
+
// TODO: this should eventually fetch the formletId then get the fields and default values of those fields
|
|
190
|
+
}
|
|
191
|
+
else if (entry.type !== 'readonlyField') {
|
|
192
|
+
if (isEmptyWithDefault(fieldValue, entry, result)) {
|
|
193
|
+
if (fieldId && parameters && parameters.length > 0) {
|
|
194
|
+
const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, result);
|
|
195
|
+
for (const { fieldId, fieldValue } of defaultValuesArray) {
|
|
196
|
+
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
197
|
+
if (parameter?.type === 'object') {
|
|
198
|
+
const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
|
|
199
|
+
for (const field of dependentFields) {
|
|
200
|
+
set(result, field.fieldId, field.fieldValue);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
set(result, fieldId, fieldValue);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
|
|
208
|
+
result[fieldId] = false;
|
|
209
|
+
}
|
|
210
|
+
else if (parameter?.type === 'fileContent' &&
|
|
211
|
+
(fieldValue === undefined || fieldValue === null)) {
|
|
212
|
+
result[fieldId] = instanceData['name'] ? new File([], instanceData['name']) : undefined;
|
|
213
|
+
}
|
|
214
|
+
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
215
|
+
if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
|
|
216
|
+
let RTFFieldValue = fieldValue;
|
|
217
|
+
if (!fieldValue.trim().startsWith('{\\rtf')) {
|
|
218
|
+
RTFFieldValue = plainTextToRtf(fieldValue);
|
|
219
|
+
}
|
|
220
|
+
result[fieldId] = RTFFieldValue;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
result[fieldId] = fieldValue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}, [action, parameters, associatedObject, uniquePresetValues, formDataRef, apiServices, userAccount]);
|
|
96
231
|
useEffect(() => {
|
|
97
232
|
if (!sanitizedObject)
|
|
98
233
|
return;
|
|
@@ -113,7 +248,7 @@ function FormRendererContainerInner(props) {
|
|
|
113
248
|
else {
|
|
114
249
|
setError('Action could not be found');
|
|
115
250
|
}
|
|
116
|
-
}, [sanitizedObject, actionId, form?.actionId, instanceId]);
|
|
251
|
+
}, [sanitizedObject, actionId, form?.actionId, instanceId, flattenFormEntries, parameters]);
|
|
117
252
|
const { data: navigationSlug } = useQuery({
|
|
118
253
|
queryKey: [appId, 'navigationSlug'],
|
|
119
254
|
queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
|
|
@@ -159,11 +294,12 @@ function FormRendererContainerInner(props) {
|
|
|
159
294
|
onError(error);
|
|
160
295
|
}, [sanitizedObjectError, fetchedFormError, instanceError]);
|
|
161
296
|
useEffect(() => {
|
|
162
|
-
if (!form)
|
|
297
|
+
if (!form || !action)
|
|
163
298
|
return;
|
|
164
299
|
// If no parameters are defined, then the action is synced with object properties
|
|
165
|
-
const getParamsFromObject = sanitizedObject && !action
|
|
166
|
-
|
|
300
|
+
const getParamsFromObject = sanitizedObject && !action.parameters;
|
|
301
|
+
const parameters = (getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action.parameters) ?? [];
|
|
302
|
+
setParameters(parameters.filter((param) => param.type !== 'collection' && !param.formula));
|
|
167
303
|
}, [form, action?.parameters, sanitizedObject]);
|
|
168
304
|
useEffect(() => {
|
|
169
305
|
const getInitialValues = async () => {
|
|
@@ -175,7 +311,7 @@ function FormRendererContainerInner(props) {
|
|
|
175
311
|
}
|
|
176
312
|
};
|
|
177
313
|
getInitialValues();
|
|
178
|
-
}, [instanceId, instance, flattenFormEntries]);
|
|
314
|
+
}, [instanceId, instance, flattenFormEntries, getDefaultValues]);
|
|
179
315
|
const onSubmissionSuccess = (updatedInstance) => {
|
|
180
316
|
setSnackbarError({
|
|
181
317
|
showAlert: true,
|
|
@@ -198,138 +334,98 @@ function FormRendererContainerInner(props) {
|
|
|
198
334
|
});
|
|
199
335
|
}
|
|
200
336
|
};
|
|
337
|
+
/**
|
|
338
|
+
* Manually links any newly uploaded files in the submission to the specified instance.
|
|
339
|
+
* @param submission The form submission data
|
|
340
|
+
* @param linkTo The instance to link the files to
|
|
341
|
+
*/
|
|
201
342
|
const linkFiles = async (submission, linkTo) => {
|
|
202
|
-
// Create file links for any uploaded files
|
|
343
|
+
// Create file links for any uploaded files that haven't been linked yet
|
|
203
344
|
for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
|
|
204
345
|
const files = submission[property.id];
|
|
205
346
|
if (files?.length) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
console.error('Failed to create file links:', error);
|
|
211
|
-
// Don't fail the entire submission if file linking fails
|
|
347
|
+
// Only link files that have the 'unsaved' flag (newly uploaded, not yet linked)
|
|
348
|
+
const unsavedFiles = files.filter((file) => file.unsaved);
|
|
349
|
+
if (unsavedFiles.length) {
|
|
350
|
+
await createFileLinks(unsavedFiles, linkTo, apiServices);
|
|
212
351
|
}
|
|
213
352
|
}
|
|
214
353
|
}
|
|
215
354
|
};
|
|
355
|
+
/**
|
|
356
|
+
* Strips unsaved flags from file properties before sending to API.
|
|
357
|
+
* The API doesn't expect the unsaved flag, but we need it for linking logic.
|
|
358
|
+
*/
|
|
359
|
+
const stripUnsavedFlags = (data) => {
|
|
360
|
+
const result = { ...data };
|
|
361
|
+
const fileParameters = parameters.filter((param) => param.type === 'file');
|
|
362
|
+
fileParameters.forEach((param) => {
|
|
363
|
+
if (Array.isArray(result[param.id])) {
|
|
364
|
+
result[param.id] = result[param.id].map((file) => ({
|
|
365
|
+
id: file.id,
|
|
366
|
+
name: file.name,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
return result;
|
|
371
|
+
};
|
|
216
372
|
const saveHandler = async (submission) => {
|
|
217
373
|
if (!form) {
|
|
218
374
|
return;
|
|
219
375
|
}
|
|
220
|
-
|
|
376
|
+
const formattedSubmission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
|
|
377
|
+
submission = pick(formattedSubmission, parameters.map((parameter) => parameter.id));
|
|
221
378
|
try {
|
|
222
379
|
if (action?.type === 'create') {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
380
|
+
let response = undefined;
|
|
381
|
+
if ((await objectStore.get()).rootObjectId === 'sys__file' && actionId) {
|
|
382
|
+
response = await handleFileUpload(apiServices, submission, actionId, objectId, instanceId, associatedObject && !('propertyId' in associatedObject) ? associatedObject : undefined);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
response = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/actions`), {
|
|
386
|
+
actionId: actionId,
|
|
387
|
+
instanceId: instanceId,
|
|
388
|
+
input: stripUnsavedFlags(submission),
|
|
389
|
+
});
|
|
233
390
|
}
|
|
391
|
+
// Manually link files to created instance.
|
|
392
|
+
await linkFiles(submission, { id: response.id, objectId: form.objectId });
|
|
393
|
+
onSubmissionSuccess(response);
|
|
234
394
|
}
|
|
235
395
|
else if (instanceId && action) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
396
|
+
let response = undefined;
|
|
397
|
+
if ((await objectStore.get()).rootObjectId === 'sys__file') {
|
|
398
|
+
response = await handleFileUpload(apiServices, submission, action.id, objectId, instanceId);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
response = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
402
|
+
actionId: action.id,
|
|
403
|
+
input: stripUnsavedFlags(submission),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
242
406
|
if (sanitizedObject && instance) {
|
|
407
|
+
if (!onAutosave) {
|
|
408
|
+
// For non-autosave updates, link any uploaded files to the instance.
|
|
409
|
+
await linkFiles(submission, { id: instanceId, objectId: objectId });
|
|
410
|
+
}
|
|
243
411
|
onSubmissionSuccess(response);
|
|
244
|
-
|
|
412
|
+
// Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
|
|
413
|
+
await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
245
414
|
}
|
|
246
415
|
}
|
|
247
416
|
}
|
|
248
417
|
catch (error) {
|
|
249
|
-
// Handle deleteDocuments for uploaded documents if the main submission fails
|
|
250
|
-
if (instanceId && action && sanitizedObject && instance) {
|
|
251
|
-
deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
252
|
-
}
|
|
253
418
|
setSnackbarError({
|
|
254
419
|
isError: true,
|
|
255
420
|
showAlert: true,
|
|
256
421
|
message: error.response?.data?.error?.message ?? 'An error occurred',
|
|
257
422
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const getDefaultValues = async (unnestedEntries, instanceData) => {
|
|
262
|
-
const result = {};
|
|
263
|
-
for (const entry of unnestedEntries) {
|
|
264
|
-
const fieldId = getEntryId(entry);
|
|
265
|
-
if (!fieldId)
|
|
266
|
-
continue;
|
|
267
|
-
const fieldValue = get(instanceData, fieldId);
|
|
268
|
-
if ((entry.type === 'input' || entry.type === 'inputField') &&
|
|
269
|
-
isAddressProperty(entry.parameterId || entry.input?.id)) {
|
|
270
|
-
if ((isEmpty(instanceData) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
|
|
271
|
-
entry?.display?.defaultValue &&
|
|
272
|
-
parameters) {
|
|
273
|
-
const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
|
|
274
|
-
if (isArray(defaultValuesArray)) {
|
|
275
|
-
defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
|
|
276
|
-
set(result, fieldId, fieldValue);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
281
|
-
set(result, fieldId, fieldValue);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
|
|
285
|
-
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
286
|
-
if (associatedObject?.propertyId === fieldId &&
|
|
287
|
-
associatedObject?.instanceId &&
|
|
288
|
-
parameter &&
|
|
289
|
-
action?.type === 'create') {
|
|
290
|
-
try {
|
|
291
|
-
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
|
|
292
|
-
result[associatedObject.propertyId] = instance;
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
console.error(error);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else if (entry.type !== 'readonlyField') {
|
|
299
|
-
if (isEmptyWithDefault(fieldValue, entry, instanceData)) {
|
|
300
|
-
if (fieldId && parameters && parameters.length > 0) {
|
|
301
|
-
const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
|
|
302
|
-
for (const { fieldId, fieldValue } of defaultValuesArray) {
|
|
303
|
-
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
304
|
-
if (parameter?.type === 'object') {
|
|
305
|
-
const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
|
|
306
|
-
for (const field of dependentFields) {
|
|
307
|
-
set(result, field.fieldId, field.fieldValue);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
set(result, fieldId, fieldValue);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
|
|
315
|
-
result[fieldId] = false;
|
|
316
|
-
}
|
|
317
|
-
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
318
|
-
if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
|
|
319
|
-
let RTFFieldValue = fieldValue;
|
|
320
|
-
if (!fieldValue.trim().startsWith('{\\rtf')) {
|
|
321
|
-
RTFFieldValue = plainTextToRtf(fieldValue);
|
|
322
|
-
}
|
|
323
|
-
result[fieldId] = RTFFieldValue;
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
result[fieldId] = fieldValue;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
423
|
+
if (instanceId && action && sanitizedObject && instance) {
|
|
424
|
+
// For an update, uploaded documents have been linked to the instance and need to be deleted.
|
|
425
|
+
await deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
330
426
|
}
|
|
427
|
+
throw error; // Throw error so caller knows submission failed
|
|
331
428
|
}
|
|
332
|
-
return result;
|
|
333
429
|
};
|
|
334
430
|
const removeUneditedProtectedValues = (data) => {
|
|
335
431
|
const protectedProperties = sanitizedObject?.properties?.filter((prop) => prop.protection?.maskChar);
|
|
@@ -362,11 +458,20 @@ function FormRendererContainerInner(props) {
|
|
|
362
458
|
const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
|
|
363
459
|
// Handle object instance autosave
|
|
364
460
|
if (instanceId && action?.type === 'update') {
|
|
461
|
+
const pickedSubmission = pick(submission, sanitizedObject?.properties
|
|
462
|
+
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
463
|
+
.map((property) => property.id) ?? []);
|
|
365
464
|
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
366
465
|
actionId: form.autosaveActionId,
|
|
367
|
-
input:
|
|
368
|
-
|
|
369
|
-
|
|
466
|
+
input: stripUnsavedFlags(pickedSubmission),
|
|
467
|
+
});
|
|
468
|
+
if (sanitizedObject && instance) {
|
|
469
|
+
// Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
|
|
470
|
+
await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
471
|
+
}
|
|
472
|
+
// Invalidate the instance to fetch the latest version
|
|
473
|
+
queryClient.invalidateQueries({
|
|
474
|
+
queryKey: [objectId, instanceId, 'instance'],
|
|
370
475
|
});
|
|
371
476
|
}
|
|
372
477
|
setLastSavedData(cloneDeep(formDataRef.current));
|
|
@@ -385,21 +490,19 @@ function FormRendererContainerInner(props) {
|
|
|
385
490
|
!flattenFormEntries.some((e) => (e.type === 'input' && e.parameterId === id) || (e.type === 'inputField' && e.input.id === id));
|
|
386
491
|
if (isReadOnlyField)
|
|
387
492
|
return;
|
|
388
|
-
if (parameter) {
|
|
389
|
-
|
|
493
|
+
if (parameter?.type === 'string' && parameter.enum && value) {
|
|
494
|
+
// If a single select property has a sortBy option that isn't NONE the value gets spread and doesn't save properly,
|
|
495
|
+
// this will make it correctly save the value
|
|
496
|
+
value = value.value ? value.value : value;
|
|
497
|
+
}
|
|
498
|
+
if (!isEqual(value, get(formDataRef.current, id))) {
|
|
499
|
+
if (parameter?.type === 'object' && parameters && parameters.length > 0) {
|
|
390
500
|
// On change of a related object, update default values dependent on that object
|
|
391
501
|
const dependentFields = await processValueUpdate(flattenFormEntries, parameters, value, apiServices, id, formDataRef.current, userAccount);
|
|
392
502
|
for (const field of dependentFields) {
|
|
393
503
|
onChange(field.fieldId, field.fieldValue);
|
|
394
504
|
}
|
|
395
505
|
}
|
|
396
|
-
else if (parameter.type === 'string' && parameter.enum && value) {
|
|
397
|
-
// If a single select property has a sortBy option that isn't NONE the value gets spread and doesn't save properly,
|
|
398
|
-
// this will make it correctly save the value
|
|
399
|
-
value = value.value ? value.value : value;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (!isEqual(value, get(formDataRef.current, id))) {
|
|
403
506
|
const newData = { ...formDataRef.current };
|
|
404
507
|
set(newData, id, value);
|
|
405
508
|
setFormData(newData);
|
|
@@ -429,7 +532,7 @@ function FormRendererContainerInner(props) {
|
|
|
429
532
|
border: !isLoading ? '1px solid #dbe0e4' : undefined,
|
|
430
533
|
...sx,
|
|
431
534
|
} }, !isLoading ? (React.createElement(React.Fragment, null,
|
|
432
|
-
React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, hideTitle: title?.hidden, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: instance, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: renderFooter }))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
535
|
+
React.createElement(FormRenderer, { onSubmit: onSubmit ? async (data) => await onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, hideTitle: title?.hidden, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: instance, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject && 'propertyId' in associatedObject ? associatedObject : undefined, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: renderFooter }))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
433
536
|
React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
|
|
434
537
|
React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
|
|
435
538
|
React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiServices, FormEntry, InputField, InputParameter, InputParameterReference, ObjectInstance, Reference, UserAccount } from '@evoke-platform/context';
|
|
2
2
|
import { FieldValues } from 'react-hook-form';
|
|
3
|
-
export declare function evalDefaultVals(parameters: InputParameter[],
|
|
3
|
+
export declare function evalDefaultVals(parameters: InputParameter[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference, updatedRelatedObjectParamId?: string): Promise<{
|
|
4
4
|
fieldId: string;
|
|
5
5
|
fieldValue: unknown;
|
|
6
6
|
}[]>;
|