@evoke-platform/ui-components 1.17.0 → 1.18.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 (50) 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 +3 -1
  15. package/dist/published/components/custom/FormV2/FormRenderer.js +9 -4
  16. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +5 -0
  17. package/dist/published/components/custom/FormV2/FormRendererContainer.js +116 -53
  18. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -0
  19. package/dist/published/components/custom/FormV2/components/Body.js +4 -2
  20. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +46 -17
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
  30. package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +1 -0
  31. package/dist/published/components/custom/FormV2/components/FormletRenderer.js +6 -14
  32. package/dist/published/components/custom/FormV2/components/HtmlView.js +4 -0
  33. package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.d.ts +2 -0
  34. package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.js +15 -0
  35. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +18 -19
  36. package/dist/published/components/custom/FormV2/components/types.d.ts +8 -2
  37. package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -8
  38. package/dist/published/components/custom/FormV2/components/utils.js +194 -78
  39. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +180 -2
  40. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +212 -0
  41. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +26 -10
  42. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +1 -0
  43. package/dist/published/components/custom/index.d.ts +1 -0
  44. package/dist/published/index.d.ts +1 -1
  45. package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
  46. package/dist/published/stories/FormRenderer.stories.d.ts +9 -3
  47. package/dist/published/stories/FormRenderer.stories.js +1 -0
  48. package/dist/published/stories/FormRendererContainer.stories.d.ts +25 -0
  49. package/dist/published/theme/hooks.d.ts +1 -0
  50. package/package.json +3 -2
@@ -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,9 +22,11 @@ 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;
29
+ readOnly?: boolean;
28
30
  renderFooter?: (props: FooterProps) => React.ReactNode;
29
31
  };
30
32
  export declare const FormRenderer: ((props: FormRendererProps) => React.JSX.Element) & {
@@ -14,7 +14,7 @@ import { assignIdsToSectionsAndRichText, convertPropertiesToParams, entryIsVisib
14
14
  import { handleValidation } from './components/ValidationFiles/Validation';
15
15
  import ValidationErrors from './components/ValidationFiles/ValidationErrors';
16
16
  const FormRendererInternal = (props) => {
17
- const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, hideTitle = false, fieldHeight, richTextEditor, form, instance, onChange, onAutosave, associatedObject, renderHeader, renderBody, renderFooter, } = props;
17
+ const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, hideTitle = false, fieldHeight, richTextEditor, form, instance, onChange, onAutosave, associatedObject, renderHeader, renderBody, renderFooter, readOnly, } = props;
18
18
  const { entries, name: title, objectId, actionId, display } = form;
19
19
  const { register, unregister, setValue, reset, handleSubmit, formState: { errors, isSubmitted }, getValues, } = useForm({
20
20
  defaultValues: value,
@@ -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
  }
@@ -221,6 +221,7 @@ const FormRendererInternal = (props) => {
221
221
  onCollapseAll: handleCollapseAll,
222
222
  expandedSections,
223
223
  hasAccordions: hasSections && isSmallerThanMd,
224
+ readOnly,
224
225
  })) : (React.createElement(Body, { ...{
225
226
  isInitializing,
226
227
  entries: updatedEntries,
@@ -230,8 +231,12 @@ const FormRendererInternal = (props) => {
230
231
  onCollapseAll: handleCollapseAll,
231
232
  expandedSections,
232
233
  hasAccordions: hasSections && isSmallerThanMd,
234
+ readOnly,
233
235
  } })),
234
- action && onSubmit && (renderFooter ? renderFooter(footerProps) : React.createElement(Footer, { ...footerProps }))))));
236
+ readOnly !== true &&
237
+ action &&
238
+ onSubmit &&
239
+ (renderFooter ? renderFooter(footerProps) : React.createElement(Footer, { ...footerProps }))))));
235
240
  };
236
241
  export const FormRenderer = Object.assign(function FormRenderer(props) {
237
242
  return (React.createElement(ConditionalQueryClientProvider, null,
@@ -24,6 +24,7 @@ export type FormRendererContainerProps = BaseProps & {
24
24
  };
25
25
  display?: {
26
26
  fieldHeight?: 'small' | 'medium';
27
+ readOnly?: boolean;
27
28
  };
28
29
  actionId?: string;
29
30
  objectId: string;
@@ -34,6 +35,10 @@ export type FormRendererContainerProps = BaseProps & {
34
35
  associatedObject?: {
35
36
  instanceId: string;
36
37
  propertyId: string;
38
+ objectId?: string;
39
+ } | {
40
+ instanceId: string;
41
+ objectId: string;
37
42
  };
38
43
  renderContainer?: (state: FormRendererState) => React.ReactNode;
39
44
  renderHeader?: FormRendererProps['renderHeader'];
@@ -1,6 +1,7 @@
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 { DepGraph } from 'dependency-graph';
4
5
  import { cloneDeep, get, isArray, isEmpty, isEqual, isObject, pick, set } from 'lodash';
5
6
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
7
  import { Skeleton, Snackbar } from '../../core';
@@ -9,9 +10,8 @@ import ErrorComponent from '../ErrorComponent';
9
10
  import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
10
11
  import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
11
12
  import Header from './components/Header';
12
- import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, getVisibleEditableFieldIds, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
13
+ import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, handleFileUpload, getVisibleEditableFieldIds, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
13
14
  import FormRenderer from './FormRenderer';
14
- import { DepGraph } from 'dependency-graph';
15
15
  // Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
16
16
  function FormRendererContainer(props) {
17
17
  return (React.createElement(ConditionalQueryClientProvider, null,
@@ -110,10 +110,16 @@ function FormRendererContainerInner(props) {
110
110
  setError(code ?? err);
111
111
  };
112
112
  const { data: sanitizedObject, error: sanitizedObjectError } = useQuery({
113
- queryKey: [form?.objectId ?? objectId, ...(instanceId ? [instanceId] : []), 'sanitized'],
113
+ queryKey: [
114
+ form?.objectId ?? objectId,
115
+ ...(instanceId ? [instanceId] : []),
116
+ display?.readOnly ? 'unsanitized' : 'sanitized',
117
+ ],
114
118
  queryFn: () =>
115
119
  // form?.objectId is needed for subtype forms to get the correct object
116
- apiServices.get(getPrefixedUrl(`/objects/${form?.objectId ?? objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } }),
120
+ apiServices.get(getPrefixedUrl(`/objects/${form?.objectId ?? objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`),
121
+ // when the form is readonly the action is still needed, so we need to get the unsanitized object
122
+ { params: { sanitizedVersion: display?.readOnly ? false : true } }),
117
123
  staleTime: Infinity,
118
124
  enabled: !!(form?.objectId || objectId),
119
125
  });
@@ -170,12 +176,14 @@ function FormRendererContainerInner(props) {
170
176
  // there cannot be formlets here since the `form` is the effective form
171
177
  entry.type !== 'formlet') {
172
178
  const parameter = parameters?.find((param) => param.id === fieldId);
173
- if (associatedObject?.propertyId === fieldId &&
179
+ if (associatedObject &&
180
+ 'propertyId' in associatedObject &&
181
+ associatedObject?.propertyId === fieldId &&
174
182
  associatedObject?.instanceId &&
175
- parameter &&
183
+ (parameter || associatedObject.objectId) &&
176
184
  action?.type === 'create') {
177
185
  try {
178
- const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`), {
186
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter?.objectId || associatedObject.objectId}/instances/${associatedObject.instanceId}`), {
179
187
  params: {
180
188
  expand: uniquePresetValues.filter((value) => value.startsWith(`{{{input.${fieldId}.`) ||
181
189
  value.startsWith(`{{input.${fieldId}.`)),
@@ -206,6 +214,10 @@ function FormRendererContainerInner(props) {
206
214
  else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
207
215
  result[fieldId] = false;
208
216
  }
217
+ else if (parameter?.type === 'fileContent' &&
218
+ (fieldValue === undefined || fieldValue === null)) {
219
+ result[fieldId] = instanceData['name'] ? new File([], instanceData['name']) : undefined;
220
+ }
209
221
  else if (fieldValue !== undefined && fieldValue !== null) {
210
222
  if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
211
223
  let RTFFieldValue = fieldValue;
@@ -222,28 +234,39 @@ function FormRendererContainerInner(props) {
222
234
  }
223
235
  }
224
236
  return result;
225
- }, [action, parameters, associatedObject, uniquePresetValues, formDataRef, apiServices, userAccount]);
237
+ }, [action, associatedObject, uniquePresetValues, formDataRef, apiServices, userAccount]);
226
238
  useEffect(() => {
227
- if (!sanitizedObject)
239
+ if (!sanitizedObject) {
228
240
  return;
229
- const allCriterias = extractAllCriteria(flattenFormEntries, parameters || []);
230
- const uniquePresetValues = new Set();
231
- for (const criteria of allCriterias) {
232
- const presetValues = extractPresetValuesFromCriteria(criteria);
233
- presetValues.forEach((value) => uniquePresetValues.add(value));
234
241
  }
235
- extractPresetValuesFromDynamicDefaultValues(flattenFormEntries).map((value) => uniquePresetValues.add(value));
236
- setUniquePresetValues(Array.from(uniquePresetValues));
237
242
  const action = sanitizedObject.actions?.find((a) => a.id === (form?.actionId || actionId));
238
243
  if (action && (instanceId || action.type === 'create')) {
239
244
  setAction(action);
240
- // Clear error if action is found after being missing
241
245
  setError((prevError) => (prevError === 'Action could not be found' ? undefined : prevError));
246
+ const getParamsFromObject = !action.parameters;
247
+ const parameters = (getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action.parameters) ?? [];
248
+ setParameters(parameters.filter((param) => param.type !== 'collection' && !param.formula));
242
249
  }
243
250
  else {
244
251
  setError('Action could not be found');
252
+ setAction(undefined);
253
+ setParameters([]);
254
+ }
255
+ }, [sanitizedObject, actionId, form?.actionId, instanceId]);
256
+ useEffect(() => {
257
+ if (!flattenFormEntries.length || !parameters.length) {
258
+ setUniquePresetValues([]);
259
+ return;
245
260
  }
246
- }, [sanitizedObject, actionId, form?.actionId, instanceId, flattenFormEntries, parameters]);
261
+ const allCriterias = extractAllCriteria(flattenFormEntries, parameters);
262
+ const uniquePresetValues = new Set();
263
+ for (const criteria of allCriterias) {
264
+ const presetValues = extractPresetValuesFromCriteria(criteria);
265
+ presetValues.forEach((value) => uniquePresetValues.add(value));
266
+ }
267
+ extractPresetValuesFromDynamicDefaultValues(flattenFormEntries).forEach((value) => uniquePresetValues.add(value));
268
+ setUniquePresetValues(Array.from(uniquePresetValues));
269
+ }, [flattenFormEntries, parameters]);
247
270
  const { data: navigationSlug } = useQuery({
248
271
  queryKey: [appId, 'navigationSlug'],
249
272
  queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
@@ -288,14 +311,6 @@ function FormRendererContainerInner(props) {
288
311
  if (error)
289
312
  onError(error);
290
313
  }, [sanitizedObjectError, fetchedFormError, instanceError]);
291
- useEffect(() => {
292
- if (!form || !action)
293
- return;
294
- // If no parameters are defined, then the action is synced with object properties
295
- const getParamsFromObject = sanitizedObject && !action.parameters;
296
- const parameters = (getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action.parameters) ?? [];
297
- setParameters(parameters.filter((param) => param.type !== 'collection' && !param.formula));
298
- }, [form, action?.parameters, sanitizedObject]);
299
314
  useEffect(() => {
300
315
  const getInitialValues = async () => {
301
316
  if (flattenFormEntries.length && (instance || !instanceId)) {
@@ -333,59 +348,96 @@ function FormRendererContainerInner(props) {
333
348
  });
334
349
  }
335
350
  };
351
+ /**
352
+ * Manually links any newly uploaded files in the submission to the specified instance.
353
+ * @param submission The form submission data
354
+ * @param linkTo The instance to link the files to
355
+ */
336
356
  const linkFiles = async (submission, linkTo) => {
337
- // Create file links for any uploaded files after instance creation
357
+ // Create file links for any uploaded files that haven't been linked yet
338
358
  for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
339
359
  const files = submission[property.id];
340
360
  if (files?.length) {
341
- try {
342
- await createFileLinks(files, linkTo, apiServices);
343
- }
344
- catch (error) {
345
- console.error('Failed to create file links:', error);
346
- // Don't fail the entire submission if file linking fails
361
+ // Only link files that have the 'unsaved' flag (newly uploaded, not yet linked)
362
+ const unsavedFiles = files.filter((file) => file.unsaved);
363
+ if (unsavedFiles.length) {
364
+ await createFileLinks(unsavedFiles, linkTo, apiServices);
347
365
  }
348
366
  }
349
367
  }
350
368
  };
369
+ /**
370
+ * Strips unsaved flags from file properties before sending to API.
371
+ * The API doesn't expect the unsaved flag, but we need it for linking logic.
372
+ */
373
+ const stripUnsavedFlags = (data) => {
374
+ const result = { ...data };
375
+ const fileParameters = parameters.filter((param) => param.type === 'file');
376
+ fileParameters.forEach((param) => {
377
+ if (Array.isArray(result[param.id])) {
378
+ result[param.id] = result[param.id].map((file) => ({
379
+ id: file.id,
380
+ name: file.name,
381
+ }));
382
+ }
383
+ });
384
+ return result;
385
+ };
351
386
  const saveHandler = async (submission) => {
352
387
  if (!form) {
353
388
  return;
354
389
  }
355
- submission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
390
+ const formattedSubmission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
391
+ submission = pick(formattedSubmission, parameters.map((parameter) => parameter.id));
356
392
  try {
357
393
  if (action?.type === 'create') {
358
- const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
359
- actionId: form.actionId,
360
- input: pick(submission, parameters.map((parameter) => parameter.id)),
361
- });
362
- if (response) {
363
- // Manually link files to created instance.
364
- await linkFiles(submission, { id: response.id, objectId: form.objectId });
365
- onSubmissionSuccess(response);
394
+ let response = undefined;
395
+ if ((await objectStore.get()).rootObjectId === 'sys__file' && actionId) {
396
+ response = await handleFileUpload(apiServices, submission, actionId, objectId, instanceId, associatedObject && !('propertyId' in associatedObject) ? associatedObject : undefined);
366
397
  }
398
+ else {
399
+ response = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/actions`), {
400
+ actionId: actionId,
401
+ instanceId: instanceId,
402
+ input: stripUnsavedFlags(submission),
403
+ });
404
+ }
405
+ // Manually link files to created instance.
406
+ await linkFiles(submission, { id: response.id, objectId: form.objectId });
407
+ onSubmissionSuccess(response);
367
408
  }
368
409
  else if (instanceId && action) {
369
- const response = await objectStore.instanceAction(instanceId, {
370
- actionId: action.id,
371
- input: pick(submission, parameters.map((parameter) => parameter.id)),
372
- });
410
+ let response = undefined;
411
+ if ((await objectStore.get()).rootObjectId === 'sys__file' && action.type !== 'delete') {
412
+ response = await handleFileUpload(apiServices, submission, action.id, objectId, instanceId);
413
+ }
414
+ else {
415
+ response = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
416
+ actionId: action.id,
417
+ input: stripUnsavedFlags(submission),
418
+ });
419
+ }
373
420
  if (sanitizedObject && instance) {
421
+ if (!onAutosave) {
422
+ // For non-autosave updates, link any uploaded files to the instance.
423
+ await linkFiles(submission, { id: instanceId, objectId: objectId });
424
+ }
374
425
  onSubmissionSuccess(response);
375
- deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
426
+ // Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
427
+ await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
376
428
  }
377
429
  }
378
430
  }
379
431
  catch (error) {
380
- // Handle deleteDocuments for uploaded documents if the main submission fails
381
- if (instanceId && action && sanitizedObject && instance) {
382
- deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
383
- }
384
432
  setSnackbarError({
385
433
  isError: true,
386
434
  showAlert: true,
387
435
  message: error.response?.data?.error?.message ?? 'An error occurred',
388
436
  });
437
+ if (instanceId && action && sanitizedObject && instance) {
438
+ // For an update, uploaded documents have been linked to the instance and need to be deleted.
439
+ await deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
440
+ }
389
441
  throw error; // Throw error so caller knows submission failed
390
442
  }
391
443
  };
@@ -427,9 +479,20 @@ function FormRendererContainerInner(props) {
427
479
  const submission = await formatSubmission(scopedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
428
480
  // Handle object instance autosave
429
481
  if (instanceId && action?.type === 'update') {
482
+ const pickedSubmission = pick(submission, sanitizedObject?.properties
483
+ ?.filter((property) => !property.formula && property.type !== 'collection')
484
+ .map((property) => property.id) ?? []);
430
485
  await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
431
486
  actionId: form.autosaveActionId,
432
- input: submission,
487
+ input: stripUnsavedFlags(pickedSubmission),
488
+ });
489
+ if (sanitizedObject && instance) {
490
+ // Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
491
+ await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
492
+ }
493
+ // Invalidate the instance to fetch the latest version
494
+ queryClient.invalidateQueries({
495
+ queryKey: [objectId, instanceId, 'instance'],
433
496
  });
434
497
  }
435
498
  setLastSavedData(cloneDeep(formDataRef.current));
@@ -490,7 +553,7 @@ function FormRendererContainerInner(props) {
490
553
  border: !isLoading ? '1px solid #dbe0e4' : undefined,
491
554
  ...sx,
492
555
  } }, !isLoading ? (React.createElement(React.Fragment, null,
493
- 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' } },
556
+ 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, readOnly: display?.readOnly ?? false }))) : (React.createElement(Box, { sx: { padding: '20px' } },
494
557
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
495
558
  React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
496
559
  React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
@@ -13,6 +13,7 @@ export type BodyProps = {
13
13
  onExpandAll?: () => void;
14
14
  onCollapseAll?: () => void;
15
15
  sx?: SxProps;
16
+ readOnly?: boolean;
16
17
  };
17
18
  export declare const Body: React.FC<BodyProps>;
18
19
  export default Body;
@@ -3,9 +3,11 @@ import { FormContext } from '../..';
3
3
  import useWidgetSize from '../../../../theme/hooks';
4
4
  import { Skeleton } from '../../../core';
5
5
  import Box from '../../../layout/Box/Box';
6
+ import ViewDetailsEntryRenderer from '../../ViewDetailsV2/InstanceEntryRenderer';
6
7
  import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
8
+ import { convertToReadOnly } from './utils';
7
9
  export const Body = (props) => {
8
- const { entries, isInitializing, sx } = props;
10
+ const { entries, isInitializing, sx, readOnly } = props;
9
11
  const { width } = useContext(FormContext);
10
12
  const { breakpoints } = useWidgetSize({
11
13
  scroll: false,
@@ -22,6 +24,6 @@ export const Body = (props) => {
22
24
  React.createElement(Skeleton, { width: '32%', sx: { borderRadius: '8px', height: '40px' } })),
23
25
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
24
26
  React.createElement(Skeleton, { width: '49%', sx: { borderRadius: '8px', height: '40px' } }),
25
- React.createElement(Skeleton, { width: '49%', sx: { borderRadius: '8px', height: '40px' } })))) : (React.createElement(Box, { sx: { paddingX: isSm || isXs ? 2 : 3, paddingY: isSm || isXs ? '6px' : '14px', ...sx } }, entries.map((entry, index) => (React.createElement(RecursiveEntryRenderer, { key: index, entry: entry })))))));
27
+ React.createElement(Skeleton, { width: '49%', sx: { borderRadius: '8px', height: '40px' } })))) : (React.createElement(Box, { sx: { paddingX: isSm || isXs ? 2 : 3, paddingY: isSm || isXs ? '6px' : '14px', ...sx } }, entries.map((entry, index) => readOnly === true ? (React.createElement(ViewDetailsEntryRenderer, { key: index, entry: convertToReadOnly(entry) })) : (React.createElement(RecursiveEntryRenderer, { key: index, entry: entry })))))));
26
28
  };
27
29
  export default Body;
@@ -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;