@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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +238 -141
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
- package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/Form/utils.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +3 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +9 -4
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +5 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +116 -53
- package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Body.js +4 -2
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +46 -17
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +6 -14
- package/dist/published/components/custom/FormV2/components/HtmlView.js +4 -0
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.js +15 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +18 -19
- package/dist/published/components/custom/FormV2/components/types.d.ts +8 -2
- package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -8
- package/dist/published/components/custom/FormV2/components/utils.js +194 -78
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +180 -2
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +212 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +26 -10
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +1 -0
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
- package/dist/published/stories/FormRenderer.stories.d.ts +9 -3
- package/dist/published/stories/FormRenderer.stories.js +1 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +25 -0
- package/dist/published/theme/hooks.d.ts +1 -0
- 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 {
|
|
6
|
+
* @param {TreeViewProperty[]} 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: TreeViewProperty) => TreeViewProperty} updater - The function to apply to the node.
|
|
9
|
+
* @returns {TreeViewProperty[]} - The updated tree structure.
|
|
10
10
|
*/
|
|
11
|
-
export const
|
|
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:
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
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)
|
package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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: [
|
|
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'}`),
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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:
|
|
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' } })),
|
|
@@ -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;
|