@evoke-platform/ui-components 1.15.1 → 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 (39) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +238 -141
  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 -2
  16. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +4 -0
  17. package/dist/published/components/custom/FormV2/FormRendererContainer.js +85 -33
  18. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +43 -16
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
  28. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +9 -5
  29. package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
  30. package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -8
  31. package/dist/published/components/custom/FormV2/components/utils.js +165 -79
  32. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +6 -1
  33. package/dist/published/components/custom/index.d.ts +1 -0
  34. package/dist/published/index.d.ts +1 -1
  35. package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
  36. package/dist/published/stories/FormRenderer.stories.d.ts +6 -3
  37. package/dist/published/stories/FormRendererContainer.stories.d.ts +20 -0
  38. package/dist/published/theme/hooks.d.ts +1 -0
  39. package/package.json +2 -1
@@ -3,105 +3,32 @@ import { parseMongoDB } from '../util';
3
3
  /**
4
4
  * Recursively updates a node in a tree structure by applying an updater function to the node with the specified ID.
5
5
  *
6
- * @param {TreeItem[]} tree - The tree structure to update.
6
+ * @param {TreeViewProperty[]} tree - The tree structure to update.
7
7
  * @param {string} nodeId - The ID of the node to update.
8
- * @param {(node: TreeItem) => TreeItem} updater - The function to apply to the node.
9
- * @returns {TreeItem[]} - The updated tree structure.
8
+ * @param {(node: TreeViewProperty) => TreeViewProperty} updater - The function to apply to the node.
9
+ * @returns {TreeViewProperty[]} - The updated tree structure.
10
10
  */
11
- export const updateTreeNode = (tree, nodeId, updater) => {
11
+ export const updateTreeViewProperty = (tree, nodeId, updater) => {
12
12
  return tree.map((node) => {
13
13
  if (node.id === nodeId) {
14
14
  return updater(node);
15
15
  }
16
16
  else if (node.children) {
17
- return { ...node, children: updateTreeNode(node.children, nodeId, updater) };
17
+ return { ...node, children: updateTreeViewProperty(node.children, nodeId, updater) };
18
18
  }
19
19
  else {
20
20
  return node;
21
21
  }
22
22
  });
23
23
  };
24
- /**
25
- * Fetches the display name path for a given property ID within an object hierarchy.
26
- *
27
- * @param {string} propertyId - The property ID to find the display name for.
28
- * @param {Obj} rootObject - The root object to start the search from.
29
- * @param {FetchObjectFunction} fetchObject - Function to fetch an object by its ID.
30
- * @returns {Promise<string>} - A promise that resolves to the display name path.
31
- */
32
- export const fetchDisplayNamePath = async (propertyId, rootObject, fetchObject) => {
33
- const propertyInfo = await traversePropertyPath(propertyId, rootObject, fetchObject);
34
- return propertyInfo ? propertyInfo.name : '';
35
- };
36
- /**
37
- * stores full dot-notation path to each property ID in the given array of properties.
38
- *
39
- * @param {ObjectProperty[]} properties - The array of properties to update.
40
- * @param {string} parentPath - The parent path to attach to each property ID.
41
- * @returns {ObjectProperty[]} The updated array of properties with the parent path attached to each property ID.
42
- */
43
- export const setIdPaths = (properties, parentPath) => {
44
- return properties.map((prop) => {
45
- const fullPath = parentPath ? `${parentPath}.${prop.id}` : prop.id;
46
- return {
47
- ...prop,
48
- id: fullPath,
49
- };
50
- });
51
- };
52
- /**
53
- * Traverses a property path within an object hierarchy to retrieve detailed property information.
54
- *
55
- * @param {string} propertyPath - The dot-separated path of the property to traverse.
56
- * @param {Obj} rootObject - The root object from which to start the traversal.
57
- * @param {FetchObjectFunction} fetchObject - A function to fetch an object by its ID.
58
- * @returns {Promise<ObjectProperty | null>} A promise that resolves to an ObjectProperty if found, or null otherwise.
59
- */
60
- export const traversePropertyPath = async (propertyPath, rootObject, fetchObject) => {
61
- const segments = propertyPath.split('.');
62
- let currentObject = rootObject;
63
- let fullPath = '';
64
- let namePath = '';
65
- for (let i = 0; i < segments.length; i++) {
66
- const remainingPath = segments.slice(i).join('.');
67
- let prop = currentObject.properties?.find((p) => p.id === remainingPath);
68
- if (prop) {
69
- // flattened address or user properties
70
- fullPath = fullPath ? `${fullPath}.${remainingPath}` : remainingPath;
71
- namePath = namePath ? `${namePath} / ${prop.name}` : prop.name;
72
- return {
73
- ...prop,
74
- id: fullPath,
75
- name: namePath,
76
- };
77
- }
78
- else {
79
- prop = currentObject.properties?.find((p) => p.id === segments[i]);
80
- if (!prop) {
81
- return null;
82
- }
83
- fullPath = fullPath ? `${fullPath}.${prop.id}` : prop.id;
84
- namePath = namePath ? `${namePath} / ${prop.name}` : prop.name;
85
- if (i === segments.length - 1) {
86
- return {
87
- ...prop,
88
- id: fullPath,
89
- name: namePath,
90
- };
91
- }
92
- if (prop.type === 'object' && prop.objectId) {
93
- const fetchedObject = await fetchObject(prop.objectId);
94
- if (fetchedObject) {
95
- currentObject = fetchedObject;
96
- }
97
- else {
98
- return null;
99
- }
100
- }
101
- }
102
- }
103
- return null;
104
- };
24
+ export const convertTreeViewPropertyToTreeItem = (property) => ({
25
+ id: property.id,
26
+ label: property.name,
27
+ value: property.id,
28
+ type: property.type,
29
+ objectId: property.objectId,
30
+ children: property.children?.map(convertTreeViewPropertyToTreeItem),
31
+ });
105
32
  /**
106
33
  * Truncates the name path if it exceeds the specified character limit.
107
34
  *
@@ -184,9 +111,11 @@ export const findTreeItemById = (nodes, nodeId) => {
184
111
  for (const node of nodes) {
185
112
  if (node.id === nodeId)
186
113
  return node;
187
- const found = node.children && findTreeItemById(node.children, nodeId);
188
- if (found)
189
- return found;
114
+ if (nodeId.startsWith(node.id)) {
115
+ const found = node.children && findTreeItemById(node.children, nodeId);
116
+ if (found)
117
+ return found;
118
+ }
190
119
  }
191
120
  return null;
192
121
  };
@@ -43,7 +43,7 @@ export const Document = (props) => {
43
43
  // For 'file' type properties, check permissions on the sys__file object
44
44
  // For 'document' type properties, check document attachment permissions
45
45
  const endpoint = property.type === 'file'
46
- ? getPrefixedUrl(`/objects/sys__file/instances/${instance.id}/checkAccess?action=update`)
46
+ ? getPrefixedUrl('/objects/sys__file/instances/checkAccess?action=execute&field=_create')
47
47
  : getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=update`);
48
48
  apiServices
49
49
  .get(endpoint)
@@ -64,12 +64,15 @@ export const DocumentList = (props) => {
64
64
  }, []);
65
65
  const checkPermissions = () => {
66
66
  if (instance?.[property.id]?.length) {
67
- // For 'file' type properties, check permissions on the sys__file object
67
+ // For 'file' type properties, check regular object instance permissions
68
68
  // For 'document' type properties, check document attachment permissions
69
69
  const endpoint = property.type === 'file'
70
- ? getPrefixedUrl(`/objects/sys__file/instances/${instance.id}/checkAccess?action=view`)
70
+ ? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=read&field=content`)
71
71
  : getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=view`);
72
- apiServices.get(endpoint).then((accessCheck) => setHasViewPermission(accessCheck.result));
72
+ apiServices
73
+ .get(endpoint)
74
+ .then((accessCheck) => setHasViewPermission(accessCheck.result))
75
+ .catch(() => setHasViewPermission(false));
73
76
  }
74
77
  };
75
78
  const isFile = (doc) => doc instanceof File;
@@ -25,6 +25,7 @@ export declare function flattenFormComponents(components?: ActionInput[]): Actio
25
25
  export declare function addObjectPropertiesToComponentProps(properties: Property[], formComponents: any[], allCriteriaInputs?: string[], instance?: ObjectInstance, objectPropertyInputProps?: ObjectPropertyInputProps, associatedObject?: {
26
26
  instanceId?: string;
27
27
  propertyId?: string;
28
+ objectId?: string;
28
29
  }, autoSave?: (data: Record<string, unknown>) => void, readOnly?: boolean, defaultPages?: Record<string, string>, navigateTo?: (path: string) => void, queryAddresses?: (query: string) => Promise<Address[]>, apiServices?: ApiServices, isModal?: boolean, fieldHeight?: 'small' | 'medium', richTextEditor?: typeof ReactComponent): Promise<ActionInput[]>;
29
30
  export declare function getDefaultValue(initialValue: unknown, selectOptions?: AutocompleteOption[]): unknown;
30
31
  export declare const buildComponentPropsFromObjectProperties: (properties: Property[], objectId: string, instance?: ObjectInstance, objectPropertyInputProps?: ObjectPropertyInputProps, hasActionPermissions?: boolean, autoSave?: ((data: Record<string, unknown>) => void) | undefined, readOnly?: boolean, queryAddresses?: ((query: string) => Promise<Address[]>) | undefined, isModal?: boolean, fieldHeight?: 'small' | 'medium', richTextEditor?: typeof ReactComponent) => unknown[];
@@ -9,7 +9,7 @@ import ValidationErrors from './components/ValidationFiles/ValidationErrors';
9
9
  export type FormRendererProps = BaseProps & {
10
10
  richTextEditor?: ComponentType<SimpleEditorProps>;
11
11
  value?: FieldValues;
12
- onSubmit?: (data: FieldValues) => void;
12
+ onSubmit?: (data: FieldValues) => Promise<void> | ((data: FieldValues) => void);
13
13
  onDiscardChanges?: () => void;
14
14
  onSubmitError?: SubmitErrorHandler<FieldValues>;
15
15
  hideTitle?: boolean;
@@ -22,6 +22,7 @@ export type FormRendererProps = BaseProps & {
22
22
  associatedObject?: {
23
23
  instanceId: string;
24
24
  propertyId: string;
25
+ objectId?: string;
25
26
  };
26
27
  renderHeader?: (props: HeaderProps) => React.ReactNode;
27
28
  renderBody?: (props: BodyProps) => React.ReactNode;
@@ -158,9 +158,9 @@ const FormRendererInternal = (props) => {
158
158
  async function unregisterHiddenFieldsAndSubmit() {
159
159
  unregisterHiddenFields(entries ?? []);
160
160
  removeUneditedProtectedValues();
161
- await handleSubmit((data) => {
161
+ await handleSubmit(async (data) => {
162
162
  if (onSubmit) {
163
- onSubmit(action?.type === 'delete' ? {} : data);
163
+ await onSubmit(action?.type === 'delete' ? {} : data);
164
164
  // clear fetched options after successful submit to allow re-evaluation with the new instance data
165
165
  setFetchedOptions({});
166
166
  }
@@ -34,6 +34,10 @@ export type FormRendererContainerProps = BaseProps & {
34
34
  associatedObject?: {
35
35
  instanceId: string;
36
36
  propertyId: string;
37
+ objectId?: string;
38
+ } | {
39
+ instanceId: string;
40
+ objectId: string;
37
41
  };
38
42
  renderContainer?: (state: FormRendererState) => React.ReactNode;
39
43
  renderHeader?: FormRendererProps['renderHeader'];
@@ -9,7 +9,7 @@ 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
14
  import { DepGraph } from 'dependency-graph';
15
15
  // Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
@@ -166,12 +166,14 @@ function FormRendererContainerInner(props) {
166
166
  }
167
167
  else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
168
168
  const parameter = parameters?.find((param) => param.id === fieldId);
169
- if (associatedObject?.propertyId === fieldId &&
169
+ if (associatedObject &&
170
+ 'propertyId' in associatedObject &&
171
+ associatedObject?.propertyId === fieldId &&
170
172
  associatedObject?.instanceId &&
171
- parameter &&
173
+ (parameter || associatedObject.objectId) &&
172
174
  action?.type === 'create') {
173
175
  try {
174
- const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`), {
176
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter?.objectId || associatedObject.objectId}/instances/${associatedObject.instanceId}`), {
175
177
  params: {
176
178
  expand: uniquePresetValues.filter((value) => value.startsWith(`{{{input.${fieldId}.`) ||
177
179
  value.startsWith(`{{input.${fieldId}.`)),
@@ -205,6 +207,10 @@ function FormRendererContainerInner(props) {
205
207
  else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
206
208
  result[fieldId] = false;
207
209
  }
210
+ else if (parameter?.type === 'fileContent' &&
211
+ (fieldValue === undefined || fieldValue === null)) {
212
+ result[fieldId] = instanceData['name'] ? new File([], instanceData['name']) : undefined;
213
+ }
208
214
  else if (fieldValue !== undefined && fieldValue !== null) {
209
215
  if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
210
216
  let RTFFieldValue = fieldValue;
@@ -328,59 +334,96 @@ function FormRendererContainerInner(props) {
328
334
  });
329
335
  }
330
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
+ */
331
342
  const linkFiles = async (submission, linkTo) => {
332
- // Create file links for any uploaded files after instance creation
343
+ // Create file links for any uploaded files that haven't been linked yet
333
344
  for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
334
345
  const files = submission[property.id];
335
346
  if (files?.length) {
336
- try {
337
- await createFileLinks(files, linkTo, apiServices);
338
- }
339
- catch (error) {
340
- console.error('Failed to create file links:', error);
341
- // 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);
342
351
  }
343
352
  }
344
353
  }
345
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
+ };
346
372
  const saveHandler = async (submission) => {
347
373
  if (!form) {
348
374
  return;
349
375
  }
350
- 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));
351
378
  try {
352
379
  if (action?.type === 'create') {
353
- const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
354
- actionId: form.actionId,
355
- input: pick(submission, parameters.map((parameter) => parameter.id)),
356
- });
357
- if (response) {
358
- // Manually link files to created instance.
359
- await linkFiles(submission, { id: response.id, objectId: form.objectId });
360
- 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);
361
383
  }
384
+ else {
385
+ response = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/actions`), {
386
+ actionId: actionId,
387
+ instanceId: instanceId,
388
+ input: stripUnsavedFlags(submission),
389
+ });
390
+ }
391
+ // Manually link files to created instance.
392
+ await linkFiles(submission, { id: response.id, objectId: form.objectId });
393
+ onSubmissionSuccess(response);
362
394
  }
363
395
  else if (instanceId && action) {
364
- const response = await objectStore.instanceAction(instanceId, {
365
- actionId: action.id,
366
- input: pick(submission, parameters.map((parameter) => parameter.id)),
367
- });
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
+ }
368
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
+ }
369
411
  onSubmissionSuccess(response);
370
- 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);
371
414
  }
372
415
  }
373
416
  }
374
417
  catch (error) {
375
- // Handle deleteDocuments for uploaded documents if the main submission fails
376
- if (instanceId && action && sanitizedObject && instance) {
377
- deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
378
- }
379
418
  setSnackbarError({
380
419
  isError: true,
381
420
  showAlert: true,
382
421
  message: error.response?.data?.error?.message ?? 'An error occurred',
383
422
  });
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);
426
+ }
384
427
  throw error; // Throw error so caller knows submission failed
385
428
  }
386
429
  };
@@ -415,11 +458,20 @@ function FormRendererContainerInner(props) {
415
458
  const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
416
459
  // Handle object instance autosave
417
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) ?? []);
418
464
  await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
419
465
  actionId: form.autosaveActionId,
420
- input: pick(submission, sanitizedObject?.properties
421
- ?.filter((property) => !property.formula && property.type !== 'collection')
422
- .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'],
423
475
  });
424
476
  }
425
477
  setLastSavedData(cloneDeep(formDataRef.current));
@@ -480,7 +532,7 @@ function FormRendererContainerInner(props) {
480
532
  border: !isLoading ? '1px solid #dbe0e4' : undefined,
481
533
  ...sx,
482
534
  } }, !isLoading ? (React.createElement(React.Fragment, null,
483
- 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' } },
484
536
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
485
537
  React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
486
538
  React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
@@ -22,6 +22,7 @@ type FormContextType = {
22
22
  associatedObject?: {
23
23
  instanceId: string;
24
24
  propertyId: string;
25
+ objectId?: string;
25
26
  };
26
27
  form?: EvokeForm;
27
28
  width: number;
@@ -13,6 +13,7 @@ export type ActionDialogProps = {
13
13
  associatedObject?: {
14
14
  instanceId: string;
15
15
  propertyId: string;
16
+ objectId?: string;
16
17
  };
17
18
  };
18
19
  export declare const ActionDialog: (props: ActionDialogProps) => React.JSX.Element;
@@ -10,7 +10,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Skel
10
10
  import { Box } from '../../../../../layout';
11
11
  import { getReadableQuery } from '../../../../CriteriaBuilder';
12
12
  import { retrieveCustomErrorMessage } from '../../../../Form/utils';
13
- import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, transformToWhere, useFormById, } from '../../utils';
13
+ import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, handleFileUpload, transformToWhere, useFormById, } from '../../utils';
14
14
  import { ActionDialog } from './ActionDialog';
15
15
  import { DocumentViewerCell } from './DocumentViewerCell';
16
16
  const styles = {
@@ -258,20 +258,34 @@ const RepeatableField = (props) => {
258
258
  : dialogType === 'update'
259
259
  ? entry.display?.updateActionId
260
260
  : entry.display?.deleteActionId));
261
+ const relatedProperty = relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId);
261
262
  // when save is called we know that fieldDefinition is a parameter and fieldDefinition.objectId is defined
262
- input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm : undefined, undefined, instance?.id && fieldDefinition.relatedPropertyId
263
- ? { instanceId: instance.id, propertyId: fieldDefinition.relatedPropertyId }
263
+ input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm : action?.type === 'delete' ? deleteForm : createForm, undefined, instance?.id && fieldDefinition.relatedPropertyId
264
+ ? {
265
+ instanceId: instance.id,
266
+ propertyId: fieldDefinition.relatedPropertyId,
267
+ objectId: !relatedProperty?.objectId ? instance.objectId : undefined,
268
+ }
264
269
  : undefined, action?.parameters ?? (relatedObject && convertPropertiesToParams(relatedObject)));
265
- if (action?.type === 'create' && entry.display?.createActionId) {
270
+ if (action?.type === 'create' && action.id) {
266
271
  const updatedInput = {
267
272
  ...input,
268
- [fieldDefinition?.relatedPropertyId]: { id: instance?.id },
273
+ [fieldDefinition?.relatedPropertyId]: {
274
+ id: instance?.id,
275
+ objectId: !relatedProperty?.objectId ? instance?.objectId : undefined,
276
+ },
269
277
  };
270
278
  try {
271
- const instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
272
- actionId: entry.display?.createActionId,
273
- input: updatedInput,
274
- });
279
+ let instance = undefined;
280
+ if (relatedObject?.rootObjectId === 'sys__file') {
281
+ instance = await handleFileUpload(apiServices, updatedInput, action.id, fieldDefinition.objectId);
282
+ }
283
+ else {
284
+ instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
285
+ actionId: action.id,
286
+ input: updatedInput,
287
+ });
288
+ }
275
289
  queryClient.setQueryData(relatedInstancesQueryKey, (oldData) => {
276
290
  if (!oldData)
277
291
  return [instance];
@@ -293,12 +307,19 @@ const RepeatableField = (props) => {
293
307
  else {
294
308
  const relatedObjectId = relatedObject?.id;
295
309
  try {
296
- const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${selectedInstanceId}/actions`), {
297
- actionId: `_${action?.type}`,
298
- input: omit(input, relatedObject?.properties
299
- ?.filter((property) => property.formula || property.type === 'collection')
300
- .map((property) => property.id) ?? []),
301
- });
310
+ let response = undefined;
311
+ const submission = omit(input, relatedObject?.properties
312
+ ?.filter((property) => property.formula || property.type === 'collection')
313
+ .map((property) => property.id) ?? []);
314
+ if (relatedObject?.rootObjectId === 'sys__file' && action?.id) {
315
+ response = await handleFileUpload(apiServices, submission, action.id, relatedObjectId, selectedInstanceId);
316
+ }
317
+ else {
318
+ response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${selectedInstanceId}/actions`), {
319
+ actionId: action?.id ?? `_${action?.type}`,
320
+ input: submission,
321
+ });
322
+ }
302
323
  if (response && relatedObject && instance) {
303
324
  deleteDocuments(input, !!response, apiServices, relatedObject, instance, action);
304
325
  }
@@ -491,7 +512,13 @@ const RepeatableField = (props) => {
491
512
  ? '_auto_'
492
513
  : deleteForm?.id
493
514
  : undefined, instanceId: selectedInstanceId, relatedParameter: fieldDefinition, associatedObject: instance?.id && fieldDefinition.relatedPropertyId
494
- ? { instanceId: instance.id, propertyId: fieldDefinition.relatedPropertyId }
515
+ ? {
516
+ instanceId: instance.id,
517
+ propertyId: fieldDefinition.relatedPropertyId,
518
+ objectId: !relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId)?.objectId
519
+ ? instance.objectId
520
+ : undefined,
521
+ }
495
522
  : undefined })),
496
523
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
497
524
  };
@@ -1,12 +1,13 @@
1
1
  import { DocumentParameterValidation } from '@evoke-platform/context';
2
2
  import React from 'react';
3
- import { SavedDocumentReference } from '../../types';
3
+ import { DocumentReference } from '../../types';
4
4
  type DocumentProps = {
5
5
  id: string;
6
+ fieldType?: 'file' | 'document';
6
7
  canUpdateProperty: boolean;
7
8
  error: boolean;
8
9
  validate?: DocumentParameterValidation;
9
- value: (File | SavedDocumentReference)[] | undefined;
10
+ value: (File | DocumentReference)[] | undefined;
10
11
  hasDescription?: boolean;
11
12
  };
12
13
  export declare const Document: (props: DocumentProps) => React.JSX.Element;