@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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +4 -8
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +144 -238
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
- package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
- package/dist/published/components/custom/Form/utils.d.ts +2 -3
- package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/Select/Select.js +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -2
- package/dist/published/components/custom/FormV2/FormRenderer.js +7 -7
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -4
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +49 -91
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +17 -44
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +12 -44
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +29 -41
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +0 -14
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
- package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +13 -19
- package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
- package/dist/published/components/custom/FormV2/components/utils.d.ts +14 -12
- package/dist/published/components/custom/FormV2/components/utils.js +123 -159
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +1 -6
- package/dist/published/components/custom/index.d.ts +0 -1
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
- package/dist/published/stories/FormRenderer.stories.d.ts +3 -6
- package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -20
- package/dist/published/stories/FormRendererData.d.ts +15 -0
- package/dist/published/stories/FormRendererData.js +63 -0
- package/dist/published/stories/sharedMswHandlers.js +4 -2
- package/dist/published/theme/hooks.d.ts +0 -1
- package/package.json +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +0 -12
- 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 {
|
|
6
|
+
* @param {TreeItem[]} tree - The tree structure to update.
|
|
7
7
|
* @param {string} nodeId - The ID of the node to update.
|
|
8
|
-
* @param {(node:
|
|
9
|
-
* @returns {
|
|
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
|
|
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:
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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(
|
|
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)
|
package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js
CHANGED
|
@@ -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
|
|
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=
|
|
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[];
|
package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
whiteSpace: 'normal',
|
|
10
|
-
|
|
11
|
-
|
|
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 ?? {}) })) }),
|
package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js
CHANGED
|
@@ -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) =>
|
|
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
|
|
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
|
|
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(
|
|
161
|
+
await handleSubmit((data) => {
|
|
162
162
|
if (onSubmit) {
|
|
163
|
-
|
|
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,
|
|
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' &&
|
|
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
|
-
|
|
175
|
+
parameter &&
|
|
174
176
|
action?.type === 'create') {
|
|
175
177
|
try {
|
|
176
|
-
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 &&
|
|
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 ?
|
|
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' } })),
|