@evoke-platform/ui-components 1.16.0 → 1.17.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 (54) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +4 -8
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +144 -238
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
  6. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
  7. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
  8. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
  11. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  12. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
  13. package/dist/published/components/custom/Form/utils.d.ts +2 -3
  14. package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
  15. package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
  16. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
  17. package/dist/published/components/custom/FormField/Select/Select.js +1 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -2
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +7 -7
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -4
  21. package/dist/published/components/custom/FormV2/FormRendererContainer.js +49 -91
  22. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +17 -44
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +12 -44
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +29 -41
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +0 -14
  33. package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
  34. package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
  35. package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
  36. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +13 -19
  37. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
  38. package/dist/published/components/custom/FormV2/components/utils.d.ts +14 -12
  39. package/dist/published/components/custom/FormV2/components/utils.js +123 -159
  40. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
  41. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
  42. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +1 -6
  43. package/dist/published/components/custom/index.d.ts +0 -1
  44. package/dist/published/index.d.ts +1 -1
  45. package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
  46. package/dist/published/stories/FormRenderer.stories.d.ts +3 -6
  47. package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -20
  48. package/dist/published/stories/FormRendererData.d.ts +15 -0
  49. package/dist/published/stories/FormRendererData.js +63 -0
  50. package/dist/published/stories/sharedMswHandlers.js +4 -2
  51. package/dist/published/theme/hooks.d.ts +0 -1
  52. package/package.json +2 -3
  53. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +0 -12
  54. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +0 -197
@@ -3,32 +3,105 @@ 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 {TreeViewProperty[]} tree - The tree structure to update.
6
+ * @param {TreeItem[]} tree - The tree structure to update.
7
7
  * @param {string} nodeId - The ID of the node to update.
8
- * @param {(node: TreeViewProperty) => TreeViewProperty} updater - The function to apply to the node.
9
- * @returns {TreeViewProperty[]} - The updated tree structure.
8
+ * @param {(node: TreeItem) => TreeItem} updater - The function to apply to the node.
9
+ * @returns {TreeItem[]} - The updated tree structure.
10
10
  */
11
- export const updateTreeViewProperty = (tree, nodeId, updater) => {
11
+ export const updateTreeNode = (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: updateTreeViewProperty(node.children, nodeId, updater) };
17
+ return { ...node, children: updateTreeNode(node.children, nodeId, updater) };
18
18
  }
19
19
  else {
20
20
  return node;
21
21
  }
22
22
  });
23
23
  };
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
- });
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
+ };
32
105
  /**
33
106
  * Truncates the name path if it exceeds the specified character limit.
34
107
  *
@@ -111,11 +184,9 @@ export const findTreeItemById = (nodes, nodeId) => {
111
184
  for (const node of nodes) {
112
185
  if (node.id === nodeId)
113
186
  return node;
114
- if (nodeId.startsWith(node.id)) {
115
- const found = node.children && findTreeItemById(node.children, nodeId);
116
- if (found)
117
- return found;
118
- }
187
+ const found = node.children && findTreeItemById(node.children, nodeId);
188
+ if (found)
189
+ return found;
119
190
  }
120
191
  return null;
121
192
  };
@@ -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/checkAccess?action=execute&field=_create')
46
+ ? getPrefixedUrl(`/objects/sys__file/instances/${instance.id}/checkAccess?action=update`)
47
47
  : getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=update`);
48
48
  apiServices
49
49
  .get(endpoint)
@@ -64,15 +64,12 @@ export const DocumentList = (props) => {
64
64
  }, []);
65
65
  const checkPermissions = () => {
66
66
  if (instance?.[property.id]?.length) {
67
- // For 'file' type properties, check regular object instance permissions
67
+ // For 'file' type properties, check permissions on the sys__file object
68
68
  // For 'document' type properties, check document attachment permissions
69
69
  const endpoint = property.type === 'file'
70
- ? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=read&field=content`)
70
+ ? getPrefixedUrl(`/objects/sys__file/instances/${instance.id}/checkAccess?action=view`)
71
71
  : getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=view`);
72
- apiServices
73
- .get(endpoint)
74
- .then((accessCheck) => setHasViewPermission(accessCheck.result))
75
- .catch(() => setHasViewPermission(false));
72
+ apiServices.get(endpoint).then((accessCheck) => setHasViewPermission(accessCheck.result));
76
73
  }
77
74
  };
78
75
  const isFile = (doc) => doc instanceof File;
@@ -1,4 +1,4 @@
1
- import { ActionInput, ActionInputType, ApiServices, AxiosError, FormEntry, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, PropertyType, UserAccount } from '@evoke-platform/context';
1
+ import { ActionInput, ActionInputType, ApiServices, AxiosError, FormEntry, FormletReference, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, PropertyType, UserAccount } from '@evoke-platform/context';
2
2
  import { ReactComponent } from '@formio/react';
3
3
  import { LocalDateTime } from '@js-joda/core';
4
4
  import { AutocompleteOption } from '../../core';
@@ -7,7 +7,7 @@ export declare function determineComponentType(properties: Property[], parameter
7
7
  export declare function determineParameterType(componentType: string): PropertyType;
8
8
  export declare function getFlattenEntries(entries: FormEntry[]): InputParameterReference[];
9
9
  export declare function convertFormToComponents(entries: FormEntry[], parameters: InputParameter[], object: Obj): ActionInput[];
10
- export declare function convertComponentsToForm(components: ActionInput[]): FormEntry[];
10
+ export declare function convertComponentsToForm(components: ActionInput[]): Exclude<FormEntry, FormletReference>[];
11
11
  export declare function getMiddleObjectFilter(property: Property, instance: ObjectInstance): {
12
12
  where: {
13
13
  [x: string]: string;
@@ -25,7 +25,6 @@ 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;
29
28
  }, 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[]>;
30
29
  export declare function getDefaultValue(initialValue: unknown, selectOptions?: AutocompleteOption[]): unknown;
31
30
  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[];
@@ -48,7 +48,7 @@ const AddressFieldComponent = (props) => {
48
48
  !mask ? (React.createElement(TextField, { id: id, inputRef: textFieldRef, onChange: !readOnly ? handleChange : undefined, error: error, errorMessage: errorMessage, value: value, fullWidth: true, onBlur: onBlur, size: size ?? 'medium', placeholder: readOnly ? undefined : placeholder, InputProps: {
49
49
  type: 'search',
50
50
  autoComplete: 'off',
51
- }, required: required, readOnly: readOnly, multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, inputRef: textFieldRef, sx: readOnly
51
+ }, required: required, readOnly: readOnly, multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}), sx: { backgroundColor: 'white' } })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, inputRef: textFieldRef, sx: readOnly
52
52
  ? {
53
53
  '& .MuiOutlinedInput-notchedOutline': {
54
54
  border: 'none',
@@ -4,11 +4,17 @@ import { CheckBox as CheckBoxIcon, CheckBoxOutlineBlank, Help, ToggleOff, Toggle
4
4
  import { defaultTheme } from '../../../../theme/defaultTheme';
5
5
  import { Autocomplete, Checkbox, FormControl, FormControlLabel, FormHelperText, IconButton, Switch, TextField, Tooltip, Typography, } from '../../../core';
6
6
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
7
- const descriptionStyles = {
8
- color: '#999 !important',
9
- whiteSpace: 'normal',
10
- paddingBottom: '4px',
11
- marginX: 0,
7
+ import { Box } from '../../../layout';
8
+ const styles = {
9
+ descriptionStyles: { color: '#999 !important', whiteSpace: 'normal', paddingBottom: '4px', marginX: 0 },
10
+ checkboxIconBox: {
11
+ backgroundColor: 'white',
12
+ width: '18px',
13
+ height: '18px',
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ },
12
18
  };
13
19
  const BooleanSelect = (props) => {
14
20
  const { id, property, defaultValue, error, errorMessage, readOnly, size, displayOption, label, strictlyTrue, tooltip, description, placeholder, onBlur, additionalProps, } = props;
@@ -31,7 +37,7 @@ const BooleanSelect = (props) => {
31
37
  },
32
38
  ];
33
39
  const descriptionComponent = () => {
34
- return (description && (React.createElement(FormHelperText, { sx: descriptionStyles, component: Typography }, parse(description))));
40
+ return (description && (React.createElement(FormHelperText, { sx: styles.descriptionStyles, component: Typography }, parse(description))));
35
41
  };
36
42
  const labelComponent = () => {
37
43
  return (React.createElement(Typography, { component: "span", variant: "body2", sx: { wordWrap: 'break-word', ...defaultTheme.typography.body2 } },
@@ -58,7 +64,9 @@ const BooleanSelect = (props) => {
58
64
  return displayOption === 'dropdown' ? (React.createElement(Autocomplete, { renderInput: (params) => (React.createElement(TextField, { ...params, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, sx: { background: 'white' }, placeholder: placeholder, size: size ?? 'medium' })), value: booleanOptions.find((opt) => opt.value === value) ?? '', onChange: (e, selectedValue) => handleChange(selectedValue.value), isOptionEqualToValue: (option, val) => option?.value === val?.value, options: booleanOptions, disableClearable: true, sx: { background: 'white', borderRadius: '8px' }, ...(additionalProps ?? {}), sortBy: "NONE", required: strictlyTrue })) : (React.createElement(FormControl, { required: strictlyTrue, error: error, fullWidth: true },
59
65
  React.createElement(FormControlLabel, { labelPlacement: "end", label: labelComponent(), sx: { marginLeft: '-8px' }, control: displayOption === 'switch' ? (React.createElement(Switch, { id: id, "aria-required": strictlyTrue, "aria-invalid": error, size: size ?? 'medium', name: property.id, checked: value, onChange: (e) => handleChange(e.target.checked), sx: {
60
66
  alignSelf: 'start',
61
- }, ...(additionalProps ?? {}) })) : (React.createElement(Checkbox, { id: id, "aria-required": strictlyTrue, "aria-invalid": error, size: size ?? 'medium', checked: value, name: property.id, onChange: (e) => handleChange(e.target.checked), sx: {
67
+ }, ...(additionalProps ?? {}) })) : (React.createElement(Checkbox, { id: id, "aria-required": strictlyTrue, "aria-invalid": error, size: size ?? 'medium', checked: value, name: property.id, onChange: (e) => handleChange(e.target.checked), icon: React.createElement(Box, { sx: styles.checkboxIconBox },
68
+ React.createElement(CheckBoxOutlineBlank, { fontSize: size ?? 'medium' })), checkedIcon: React.createElement(Box, { sx: styles.checkboxIconBox },
69
+ React.createElement(CheckBoxIcon, { fontSize: size ?? 'medium' })), sx: {
62
70
  alignSelf: 'start',
63
71
  padding: '4px 9px 9px 9px',
64
72
  }, ...(additionalProps ?? {}) })) }),
@@ -56,7 +56,6 @@ const InputFieldComponent = (props) => {
56
56
  : property.enum, onChange: handleSelectChange, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, error: error, errorMessage: errorMessage, fullWidth: true, onBlur: onBlur, size: size ?? 'medium', placeholder: placeholder })), disableClearable: true, value: value, isOptionEqualToValue: (option, value) => {
57
57
  return option.value === value;
58
58
  }, error: error, required: required, inputValue: inputValue ?? '', onInputChange: handleInputValueChange, ...(additionalProps ?? {}) })) : !mask || isValueProtected ? (React.createElement(TextField, { id: id, sx: {
59
- background: 'white',
60
59
  borderRadius: '8px',
61
60
  ...(readOnly && {
62
61
  '& .MuiOutlinedInput-notchedOutline': {
@@ -67,6 +66,7 @@ const InputFieldComponent = (props) => {
67
66
  backgroundColor: '#f4f6f8',
68
67
  },
69
68
  }),
69
+ backgroundColor: 'white',
70
70
  }, error: error, errorMessage: errorMessage, value: value, onChange: !readOnly ? handleChange : undefined, InputProps: { ...InputProps, endAdornment, readOnly: readOnly }, required: required, fullWidth: true, onBlur: onBlur, placeholder: readOnly ? undefined : placeholder, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, sx: readOnly
71
71
  ? {
72
72
  '& .MuiOutlinedInput-notchedOutline': {
@@ -161,7 +161,7 @@ const Select = (props) => {
161
161
  React.createElement(Typography, { variant: "caption" }, "Clear Selection"))))) : (React.createElement(Autocomplete, { multiple: property?.type === 'array', id: id, sortBy: sortBy, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, fullWidth: true, onBlur: onBlur, inputProps: {
162
162
  ...params.inputProps,
163
163
  'aria-describedby': isCombobox ? `${id}-instructions` : undefined,
164
- } })), value: value ?? (property?.type === 'array' ? [] : undefined), onChange: handleChange, options: selectOptions ?? property?.enum ?? [], inputValue: inputValue ?? '', error: error, errorMessage: errorMessage, required: required, onInputChange: handleInputValueChange, size: size, filterOptions: (options, params) => {
164
+ }, sx: { backgroundColor: 'white', borderRadius: '8px' } })), value: value ?? (property?.type === 'array' ? [] : undefined), onChange: handleChange, options: selectOptions ?? property?.enum ?? [], inputValue: inputValue ?? '', error: error, errorMessage: errorMessage, required: required, onInputChange: handleInputValueChange, size: size, filterOptions: (options, params) => {
165
165
  const filtered = filter(options, params);
166
166
  const { inputValue } = params;
167
167
  // Suggest to the user to add a new value.
@@ -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) => Promise<void> | ((data: FieldValues) => void);
12
+ onSubmit?: (data: FieldValues) => void;
13
13
  onDiscardChanges?: () => void;
14
14
  onSubmitError?: SubmitErrorHandler<FieldValues>;
15
15
  hideTitle?: boolean;
@@ -22,7 +22,6 @@ export type FormRendererProps = BaseProps & {
22
22
  associatedObject?: {
23
23
  instanceId: string;
24
24
  propertyId: string;
25
- objectId?: string;
26
25
  };
27
26
  renderHeader?: (props: HeaderProps) => React.ReactNode;
28
27
  renderBody?: (props: BodyProps) => React.ReactNode;
@@ -54,17 +54,17 @@ const FormRendererInternal = (props) => {
54
54
  enabled: !!objectId,
55
55
  });
56
56
  const updatedEntries = useMemo(() => {
57
- return object ? assignIdsToSectionsAndRichText(entries, object, parameters) : [];
57
+ return assignIdsToSectionsAndRichText(entries, object, parameters);
58
58
  }, [entries, object, parameters]);
59
59
  useEffect(() => {
60
- if (!object)
60
+ if (objectId && !object)
61
61
  return;
62
- const action = object.actions?.find((a) => a.id === actionId);
62
+ const action = object?.actions?.find((a) => a.id === actionId);
63
63
  setAction(action);
64
64
  // if forms action is synced with object properties then convertPropertiesToParams
65
- setParameters(action?.parameters ?? convertPropertiesToParams(object));
65
+ setParameters(action?.parameters ?? (object ? convertPropertiesToParams(object) : []));
66
66
  setIsInitializing(false);
67
- }, [object, actionId]);
67
+ }, [object, actionId, objectId]);
68
68
  useEffect(() => {
69
69
  const currentValues = getValues();
70
70
  if (value) {
@@ -158,9 +158,9 @@ const FormRendererInternal = (props) => {
158
158
  async function unregisterHiddenFieldsAndSubmit() {
159
159
  unregisterHiddenFields(entries ?? []);
160
160
  removeUneditedProtectedValues();
161
- await handleSubmit(async (data) => {
161
+ await handleSubmit((data) => {
162
162
  if (onSubmit) {
163
- await onSubmit(action?.type === 'delete' ? {} : data);
163
+ 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,10 +34,6 @@ 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;
41
37
  };
42
38
  renderContainer?: (state: FormRendererState) => React.ReactNode;
43
39
  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, handleFileUpload, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
12
+ import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, getVisibleEditableFieldIds, 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
@@ -164,16 +164,18 @@ function FormRendererContainerInner(props) {
164
164
  set(result, fieldId, fieldValue);
165
165
  }
166
166
  }
167
- else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
167
+ else if (entry.type !== 'sections' &&
168
+ entry.type !== 'columns' &&
169
+ entry.type !== 'content' &&
170
+ // there cannot be formlets here since the `form` is the effective form
171
+ entry.type !== 'formlet') {
168
172
  const parameter = parameters?.find((param) => param.id === fieldId);
169
- if (associatedObject &&
170
- 'propertyId' in associatedObject &&
171
- associatedObject?.propertyId === fieldId &&
173
+ if (associatedObject?.propertyId === fieldId &&
172
174
  associatedObject?.instanceId &&
173
- (parameter || associatedObject.objectId) &&
175
+ parameter &&
174
176
  action?.type === 'create') {
175
177
  try {
176
- const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter?.objectId || associatedObject.objectId}/instances/${associatedObject.instanceId}`), {
178
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`), {
177
179
  params: {
178
180
  expand: uniquePresetValues.filter((value) => value.startsWith(`{{{input.${fieldId}.`) ||
179
181
  value.startsWith(`{{input.${fieldId}.`)),
@@ -185,9 +187,6 @@ function FormRendererContainerInner(props) {
185
187
  console.error(error);
186
188
  }
187
189
  }
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
190
  else if (entry.type !== 'readonlyField') {
192
191
  if (isEmptyWithDefault(fieldValue, entry, result)) {
193
192
  if (fieldId && parameters && parameters.length > 0) {
@@ -207,10 +206,6 @@ function FormRendererContainerInner(props) {
207
206
  else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
208
207
  result[fieldId] = false;
209
208
  }
210
- else if (parameter?.type === 'fileContent' &&
211
- (fieldValue === undefined || fieldValue === null)) {
212
- result[fieldId] = instanceData['name'] ? new File([], instanceData['name']) : undefined;
213
- }
214
209
  else if (fieldValue !== undefined && fieldValue !== null) {
215
210
  if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
216
211
  let RTFFieldValue = fieldValue;
@@ -309,6 +304,10 @@ function FormRendererContainerInner(props) {
309
304
  // Deep clone to avoid reference issues
310
305
  setLastSavedData(cloneDeep(defaultValues));
311
306
  }
307
+ else {
308
+ // if there is a form with no entries
309
+ setFormData({});
310
+ }
312
311
  };
313
312
  getInitialValues();
314
313
  }, [instanceId, instance, flattenFormEntries, getDefaultValues]);
@@ -334,96 +333,59 @@ function FormRendererContainerInner(props) {
334
333
  });
335
334
  }
336
335
  };
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
- */
342
336
  const linkFiles = async (submission, linkTo) => {
343
- // Create file links for any uploaded files that haven't been linked yet
337
+ // Create file links for any uploaded files after instance creation
344
338
  for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
345
339
  const files = submission[property.id];
346
340
  if (files?.length) {
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);
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
351
347
  }
352
348
  }
353
349
  }
354
350
  };
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
- };
372
351
  const saveHandler = async (submission) => {
373
352
  if (!form) {
374
353
  return;
375
354
  }
376
- const formattedSubmission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
377
- submission = pick(formattedSubmission, parameters.map((parameter) => parameter.id));
355
+ submission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
378
356
  try {
379
357
  if (action?.type === 'create') {
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
- });
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);
390
366
  }
391
- // Manually link files to created instance.
392
- await linkFiles(submission, { id: response.id, objectId: form.objectId });
393
- onSubmissionSuccess(response);
394
367
  }
395
368
  else if (instanceId && action) {
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
- }
369
+ const response = await objectStore.instanceAction(instanceId, {
370
+ actionId: action.id,
371
+ input: pick(submission, parameters.map((parameter) => parameter.id)),
372
+ });
406
373
  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
- }
411
374
  onSubmissionSuccess(response);
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);
375
+ deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
414
376
  }
415
377
  }
416
378
  }
417
379
  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
+ }
418
384
  setSnackbarError({
419
385
  isError: true,
420
386
  showAlert: true,
421
387
  message: error.response?.data?.error?.message ?? 'An error occurred',
422
388
  });
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
- }
427
389
  throw error; // Throw error so caller knows submission failed
428
390
  }
429
391
  };
@@ -447,6 +409,12 @@ function FormRendererContainerInner(props) {
447
409
  if (!form?.autosaveActionId || !formDataRef.current) {
448
410
  return;
449
411
  }
412
+ const visibleEditableFieldIds = getVisibleEditableFieldIds(form.entries ?? [], instance, formDataRef.current);
413
+ const allowedParameterIds = parameters?.filter((parameter) => parameter.type !== 'collection').map((parameter) => parameter.id) ?? [];
414
+ const autosaveFieldIds = visibleEditableFieldIds.filter((id) => allowedParameterIds.includes(id));
415
+ if (!autosaveFieldIds.includes(fieldId)) {
416
+ return;
417
+ }
450
418
  const currentValue = get(formDataRef.current, fieldId);
451
419
  const lastValue = get(lastSavedData, fieldId);
452
420
  if (isEqual(currentValue, lastValue)) {
@@ -455,23 +423,13 @@ function FormRendererContainerInner(props) {
455
423
  try {
456
424
  setIsSaving(true);
457
425
  const cleanedData = removeUneditedProtectedValues(formDataRef.current);
458
- const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
426
+ const scopedData = pick(cleanedData, autosaveFieldIds);
427
+ const submission = await formatSubmission(scopedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
459
428
  // Handle object instance autosave
460
429
  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) ?? []);
464
430
  await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
465
431
  actionId: form.autosaveActionId,
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'],
432
+ input: submission,
475
433
  });
476
434
  }
477
435
  setLastSavedData(cloneDeep(formDataRef.current));
@@ -508,7 +466,7 @@ function FormRendererContainerInner(props) {
508
466
  setFormData(newData);
509
467
  }
510
468
  }
511
- const isLoading = (instanceId && !formDataRef.current) || !form || !sanitizedObject;
469
+ const isLoading = !form || !sanitizedObject || (instanceId && formDataRef.current === undefined);
512
470
  const status = error ? 'error' : isLoading ? 'loading' : 'ready';
513
471
  // Compose a header renderer that injects the saving indicator into the rendered header
514
472
  const composedRenderHeader = (props) => {
@@ -532,7 +490,7 @@ function FormRendererContainerInner(props) {
532
490
  border: !isLoading ? '1px solid #dbe0e4' : undefined,
533
491
  ...sx,
534
492
  } }, !isLoading ? (React.createElement(React.Fragment, null,
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' } },
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' } },
536
494
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
537
495
  React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
538
496
  React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
@@ -22,7 +22,6 @@ type FormContextType = {
22
22
  associatedObject?: {
23
23
  instanceId: string;
24
24
  propertyId: string;
25
- objectId?: string;
26
25
  };
27
26
  form?: EvokeForm;
28
27
  width: number;
@@ -13,7 +13,6 @@ export type ActionDialogProps = {
13
13
  associatedObject?: {
14
14
  instanceId: string;
15
15
  propertyId: string;
16
- objectId?: string;
17
16
  };
18
17
  };
19
18
  export declare const ActionDialog: (props: ActionDialogProps) => React.JSX.Element;
@@ -85,6 +85,9 @@ export const DropdownRepeatableFieldInput = (props) => {
85
85
  : undefined),
86
86
  }, onChange: (event) => {
87
87
  setSearchValue(event.target.value);
88
+ }, sx: {
89
+ backgroundColor: 'white',
90
+ borderRadius: '8px',
88
91
  } })),
89
92
  loading,
90
93
  sortBy: 'NONE',