@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.
Files changed (46) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +242 -142
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
  6. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
  7. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
  8. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
  11. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  12. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
  13. package/dist/published/components/custom/Form/utils.d.ts +1 -0
  14. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  15. package/dist/published/components/custom/FormV2/FormRenderer.js +2 -8
  16. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +4 -0
  17. package/dist/published/components/custom/FormV2/FormRendererContainer.js +229 -126
  18. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +1 -1
  19. package/dist/published/components/custom/FormV2/components/DefaultValues.js +60 -89
  20. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -1
  21. package/dist/published/components/custom/FormV2/components/FormContext.js +0 -1
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +43 -16
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.d.ts +2 -2
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +3 -2
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +2 -4
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +6 -2
  35. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +16 -13
  36. package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
  37. package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -8
  38. package/dist/published/components/custom/FormV2/components/utils.js +168 -82
  39. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +6 -1
  40. package/dist/published/components/custom/index.d.ts +1 -0
  41. package/dist/published/index.d.ts +1 -1
  42. package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
  43. package/dist/published/stories/FormRenderer.stories.d.ts +6 -3
  44. package/dist/published/stories/FormRendererContainer.stories.d.ts +20 -0
  45. package/dist/published/theme/hooks.d.ts +3 -3
  46. 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, omit, pick, set } from 'lodash';
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(() => getUnnestedEntries(form?.entries || []), [form?.entries]);
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?.parameters;
166
- setParameters(getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action?.parameters);
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 after instance creation
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
- try {
207
- await createFileLinks(files, linkTo, apiServices);
208
- }
209
- catch (error) {
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
- submission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
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
- const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
224
- actionId: form.actionId,
225
- input: omit(submission, sanitizedObject?.properties
226
- ?.filter((property) => property.formula || property.type === 'collection')
227
- .map((property) => property.id) ?? []),
228
- });
229
- if (response) {
230
- // Manually link files to created instance.
231
- await linkFiles(submission, { id: response.id, objectId: form.objectId });
232
- onSubmissionSuccess(response);
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
- const response = await objectStore.instanceAction(instanceId, {
237
- actionId: action.id,
238
- input: omit(submission, sanitizedObject?.properties
239
- ?.filter((property) => property.formula || property.type === 'collection')
240
- .map((property) => property.id) ?? []),
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
- deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
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
- throw error; // Throw error so caller knows submission failed
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: pick(submission, sanitizedObject?.properties
368
- ?.filter((property) => !property.formula && property.type !== 'collection')
369
- .map((property) => property.id) ?? []),
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
- if (parameter.type === 'object' && parameters && parameters.length > 0) {
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[], unnestedEntries: FormEntry[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
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
  }[]>;