@evoke-platform/ui-components 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +2 -2
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +4 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +85 -33
- 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 +43 -16
- 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/RecursiveEntryRenderer.js +9 -5
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -8
- package/dist/published/components/custom/FormV2/components/utils.js +165 -79
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +6 -1
- 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 +6 -3
- package/dist/published/stories/FormRendererContainer.stories.d.ts +20 -0
- package/dist/published/theme/hooks.d.ts +1 -0
- package/package.json +2 -1
|
@@ -3,105 +3,32 @@ import { parseMongoDB } from '../util';
|
|
|
3
3
|
/**
|
|
4
4
|
* Recursively updates a node in a tree structure by applying an updater function to the node with the specified ID.
|
|
5
5
|
*
|
|
6
|
-
* @param {
|
|
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,6 +22,7 @@ export type FormRendererProps = BaseProps & {
|
|
|
22
22
|
associatedObject?: {
|
|
23
23
|
instanceId: string;
|
|
24
24
|
propertyId: string;
|
|
25
|
+
objectId?: string;
|
|
25
26
|
};
|
|
26
27
|
renderHeader?: (props: HeaderProps) => React.ReactNode;
|
|
27
28
|
renderBody?: (props: BodyProps) => React.ReactNode;
|
|
@@ -158,9 +158,9 @@ const FormRendererInternal = (props) => {
|
|
|
158
158
|
async function unregisterHiddenFieldsAndSubmit() {
|
|
159
159
|
unregisterHiddenFields(entries ?? []);
|
|
160
160
|
removeUneditedProtectedValues();
|
|
161
|
-
await handleSubmit((data) => {
|
|
161
|
+
await handleSubmit(async (data) => {
|
|
162
162
|
if (onSubmit) {
|
|
163
|
-
onSubmit(action?.type === 'delete' ? {} : data);
|
|
163
|
+
await onSubmit(action?.type === 'delete' ? {} : data);
|
|
164
164
|
// clear fetched options after successful submit to allow re-evaluation with the new instance data
|
|
165
165
|
setFetchedOptions({});
|
|
166
166
|
}
|
|
@@ -34,6 +34,10 @@ export type FormRendererContainerProps = BaseProps & {
|
|
|
34
34
|
associatedObject?: {
|
|
35
35
|
instanceId: string;
|
|
36
36
|
propertyId: string;
|
|
37
|
+
objectId?: string;
|
|
38
|
+
} | {
|
|
39
|
+
instanceId: string;
|
|
40
|
+
objectId: string;
|
|
37
41
|
};
|
|
38
42
|
renderContainer?: (state: FormRendererState) => React.ReactNode;
|
|
39
43
|
renderHeader?: FormRendererProps['renderHeader'];
|
|
@@ -9,7 +9,7 @@ import ErrorComponent from '../ErrorComponent';
|
|
|
9
9
|
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
10
10
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
11
11
|
import Header from './components/Header';
|
|
12
|
-
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
12
|
+
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, handleFileUpload, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
13
13
|
import FormRenderer from './FormRenderer';
|
|
14
14
|
import { DepGraph } from 'dependency-graph';
|
|
15
15
|
// Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
|
|
@@ -166,12 +166,14 @@ function FormRendererContainerInner(props) {
|
|
|
166
166
|
}
|
|
167
167
|
else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
|
|
168
168
|
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
169
|
-
if (associatedObject
|
|
169
|
+
if (associatedObject &&
|
|
170
|
+
'propertyId' in associatedObject &&
|
|
171
|
+
associatedObject?.propertyId === fieldId &&
|
|
170
172
|
associatedObject?.instanceId &&
|
|
171
|
-
parameter &&
|
|
173
|
+
(parameter || associatedObject.objectId) &&
|
|
172
174
|
action?.type === 'create') {
|
|
173
175
|
try {
|
|
174
|
-
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`), {
|
|
176
|
+
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter?.objectId || associatedObject.objectId}/instances/${associatedObject.instanceId}`), {
|
|
175
177
|
params: {
|
|
176
178
|
expand: uniquePresetValues.filter((value) => value.startsWith(`{{{input.${fieldId}.`) ||
|
|
177
179
|
value.startsWith(`{{input.${fieldId}.`)),
|
|
@@ -205,6 +207,10 @@ function FormRendererContainerInner(props) {
|
|
|
205
207
|
else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
|
|
206
208
|
result[fieldId] = false;
|
|
207
209
|
}
|
|
210
|
+
else if (parameter?.type === 'fileContent' &&
|
|
211
|
+
(fieldValue === undefined || fieldValue === null)) {
|
|
212
|
+
result[fieldId] = instanceData['name'] ? new File([], instanceData['name']) : undefined;
|
|
213
|
+
}
|
|
208
214
|
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
209
215
|
if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
|
|
210
216
|
let RTFFieldValue = fieldValue;
|
|
@@ -328,59 +334,96 @@ function FormRendererContainerInner(props) {
|
|
|
328
334
|
});
|
|
329
335
|
}
|
|
330
336
|
};
|
|
337
|
+
/**
|
|
338
|
+
* Manually links any newly uploaded files in the submission to the specified instance.
|
|
339
|
+
* @param submission The form submission data
|
|
340
|
+
* @param linkTo The instance to link the files to
|
|
341
|
+
*/
|
|
331
342
|
const linkFiles = async (submission, linkTo) => {
|
|
332
|
-
// Create file links for any uploaded files
|
|
343
|
+
// Create file links for any uploaded files that haven't been linked yet
|
|
333
344
|
for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
|
|
334
345
|
const files = submission[property.id];
|
|
335
346
|
if (files?.length) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
console.error('Failed to create file links:', error);
|
|
341
|
-
// Don't fail the entire submission if file linking fails
|
|
347
|
+
// Only link files that have the 'unsaved' flag (newly uploaded, not yet linked)
|
|
348
|
+
const unsavedFiles = files.filter((file) => file.unsaved);
|
|
349
|
+
if (unsavedFiles.length) {
|
|
350
|
+
await createFileLinks(unsavedFiles, linkTo, apiServices);
|
|
342
351
|
}
|
|
343
352
|
}
|
|
344
353
|
}
|
|
345
354
|
};
|
|
355
|
+
/**
|
|
356
|
+
* Strips unsaved flags from file properties before sending to API.
|
|
357
|
+
* The API doesn't expect the unsaved flag, but we need it for linking logic.
|
|
358
|
+
*/
|
|
359
|
+
const stripUnsavedFlags = (data) => {
|
|
360
|
+
const result = { ...data };
|
|
361
|
+
const fileParameters = parameters.filter((param) => param.type === 'file');
|
|
362
|
+
fileParameters.forEach((param) => {
|
|
363
|
+
if (Array.isArray(result[param.id])) {
|
|
364
|
+
result[param.id] = result[param.id].map((file) => ({
|
|
365
|
+
id: file.id,
|
|
366
|
+
name: file.name,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
return result;
|
|
371
|
+
};
|
|
346
372
|
const saveHandler = async (submission) => {
|
|
347
373
|
if (!form) {
|
|
348
374
|
return;
|
|
349
375
|
}
|
|
350
|
-
|
|
376
|
+
const formattedSubmission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
|
|
377
|
+
submission = pick(formattedSubmission, parameters.map((parameter) => parameter.id));
|
|
351
378
|
try {
|
|
352
379
|
if (action?.type === 'create') {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
});
|
|
357
|
-
if (response) {
|
|
358
|
-
// Manually link files to created instance.
|
|
359
|
-
await linkFiles(submission, { id: response.id, objectId: form.objectId });
|
|
360
|
-
onSubmissionSuccess(response);
|
|
380
|
+
let response = undefined;
|
|
381
|
+
if ((await objectStore.get()).rootObjectId === 'sys__file' && actionId) {
|
|
382
|
+
response = await handleFileUpload(apiServices, submission, actionId, objectId, instanceId, associatedObject && !('propertyId' in associatedObject) ? associatedObject : undefined);
|
|
361
383
|
}
|
|
384
|
+
else {
|
|
385
|
+
response = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/actions`), {
|
|
386
|
+
actionId: actionId,
|
|
387
|
+
instanceId: instanceId,
|
|
388
|
+
input: stripUnsavedFlags(submission),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
// Manually link files to created instance.
|
|
392
|
+
await linkFiles(submission, { id: response.id, objectId: form.objectId });
|
|
393
|
+
onSubmissionSuccess(response);
|
|
362
394
|
}
|
|
363
395
|
else if (instanceId && action) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
396
|
+
let response = undefined;
|
|
397
|
+
if ((await objectStore.get()).rootObjectId === 'sys__file') {
|
|
398
|
+
response = await handleFileUpload(apiServices, submission, action.id, objectId, instanceId);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
response = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
402
|
+
actionId: action.id,
|
|
403
|
+
input: stripUnsavedFlags(submission),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
368
406
|
if (sanitizedObject && instance) {
|
|
407
|
+
if (!onAutosave) {
|
|
408
|
+
// For non-autosave updates, link any uploaded files to the instance.
|
|
409
|
+
await linkFiles(submission, { id: instanceId, objectId: objectId });
|
|
410
|
+
}
|
|
369
411
|
onSubmissionSuccess(response);
|
|
370
|
-
|
|
412
|
+
// Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
|
|
413
|
+
await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
371
414
|
}
|
|
372
415
|
}
|
|
373
416
|
}
|
|
374
417
|
catch (error) {
|
|
375
|
-
// Handle deleteDocuments for uploaded documents if the main submission fails
|
|
376
|
-
if (instanceId && action && sanitizedObject && instance) {
|
|
377
|
-
deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
378
|
-
}
|
|
379
418
|
setSnackbarError({
|
|
380
419
|
isError: true,
|
|
381
420
|
showAlert: true,
|
|
382
421
|
message: error.response?.data?.error?.message ?? 'An error occurred',
|
|
383
422
|
});
|
|
423
|
+
if (instanceId && action && sanitizedObject && instance) {
|
|
424
|
+
// For an update, uploaded documents have been linked to the instance and need to be deleted.
|
|
425
|
+
await deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
426
|
+
}
|
|
384
427
|
throw error; // Throw error so caller knows submission failed
|
|
385
428
|
}
|
|
386
429
|
};
|
|
@@ -415,11 +458,20 @@ function FormRendererContainerInner(props) {
|
|
|
415
458
|
const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
|
|
416
459
|
// Handle object instance autosave
|
|
417
460
|
if (instanceId && action?.type === 'update') {
|
|
461
|
+
const pickedSubmission = pick(submission, sanitizedObject?.properties
|
|
462
|
+
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
463
|
+
.map((property) => property.id) ?? []);
|
|
418
464
|
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
419
465
|
actionId: form.autosaveActionId,
|
|
420
|
-
input:
|
|
421
|
-
|
|
422
|
-
|
|
466
|
+
input: stripUnsavedFlags(pickedSubmission),
|
|
467
|
+
});
|
|
468
|
+
if (sanitizedObject && instance) {
|
|
469
|
+
// Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
|
|
470
|
+
await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
471
|
+
}
|
|
472
|
+
// Invalidate the instance to fetch the latest version
|
|
473
|
+
queryClient.invalidateQueries({
|
|
474
|
+
queryKey: [objectId, instanceId, 'instance'],
|
|
423
475
|
});
|
|
424
476
|
}
|
|
425
477
|
setLastSavedData(cloneDeep(formDataRef.current));
|
|
@@ -480,7 +532,7 @@ function FormRendererContainerInner(props) {
|
|
|
480
532
|
border: !isLoading ? '1px solid #dbe0e4' : undefined,
|
|
481
533
|
...sx,
|
|
482
534
|
} }, !isLoading ? (React.createElement(React.Fragment, null,
|
|
483
|
-
React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, hideTitle: title?.hidden, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: instance, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: renderFooter }))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
535
|
+
React.createElement(FormRenderer, { onSubmit: onSubmit ? async (data) => await onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, hideTitle: title?.hidden, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: instance, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject && 'propertyId' in associatedObject ? associatedObject : undefined, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: renderFooter }))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
484
536
|
React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
|
|
485
537
|
React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
|
|
486
538
|
React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
|
|
@@ -10,7 +10,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Skel
|
|
|
10
10
|
import { Box } from '../../../../../layout';
|
|
11
11
|
import { getReadableQuery } from '../../../../CriteriaBuilder';
|
|
12
12
|
import { retrieveCustomErrorMessage } from '../../../../Form/utils';
|
|
13
|
-
import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, transformToWhere, useFormById, } from '../../utils';
|
|
13
|
+
import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, handleFileUpload, transformToWhere, useFormById, } from '../../utils';
|
|
14
14
|
import { ActionDialog } from './ActionDialog';
|
|
15
15
|
import { DocumentViewerCell } from './DocumentViewerCell';
|
|
16
16
|
const styles = {
|
|
@@ -258,20 +258,34 @@ const RepeatableField = (props) => {
|
|
|
258
258
|
: dialogType === 'update'
|
|
259
259
|
? entry.display?.updateActionId
|
|
260
260
|
: entry.display?.deleteActionId));
|
|
261
|
+
const relatedProperty = relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId);
|
|
261
262
|
// when save is called we know that fieldDefinition is a parameter and fieldDefinition.objectId is defined
|
|
262
|
-
input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm :
|
|
263
|
-
? {
|
|
263
|
+
input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm : action?.type === 'delete' ? deleteForm : createForm, undefined, instance?.id && fieldDefinition.relatedPropertyId
|
|
264
|
+
? {
|
|
265
|
+
instanceId: instance.id,
|
|
266
|
+
propertyId: fieldDefinition.relatedPropertyId,
|
|
267
|
+
objectId: !relatedProperty?.objectId ? instance.objectId : undefined,
|
|
268
|
+
}
|
|
264
269
|
: undefined, action?.parameters ?? (relatedObject && convertPropertiesToParams(relatedObject)));
|
|
265
|
-
if (action?.type === 'create' &&
|
|
270
|
+
if (action?.type === 'create' && action.id) {
|
|
266
271
|
const updatedInput = {
|
|
267
272
|
...input,
|
|
268
|
-
[fieldDefinition?.relatedPropertyId]: {
|
|
273
|
+
[fieldDefinition?.relatedPropertyId]: {
|
|
274
|
+
id: instance?.id,
|
|
275
|
+
objectId: !relatedProperty?.objectId ? instance?.objectId : undefined,
|
|
276
|
+
},
|
|
269
277
|
};
|
|
270
278
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
279
|
+
let instance = undefined;
|
|
280
|
+
if (relatedObject?.rootObjectId === 'sys__file') {
|
|
281
|
+
instance = await handleFileUpload(apiServices, updatedInput, action.id, fieldDefinition.objectId);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
|
|
285
|
+
actionId: action.id,
|
|
286
|
+
input: updatedInput,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
275
289
|
queryClient.setQueryData(relatedInstancesQueryKey, (oldData) => {
|
|
276
290
|
if (!oldData)
|
|
277
291
|
return [instance];
|
|
@@ -293,12 +307,19 @@ const RepeatableField = (props) => {
|
|
|
293
307
|
else {
|
|
294
308
|
const relatedObjectId = relatedObject?.id;
|
|
295
309
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
310
|
+
let response = undefined;
|
|
311
|
+
const submission = omit(input, relatedObject?.properties
|
|
312
|
+
?.filter((property) => property.formula || property.type === 'collection')
|
|
313
|
+
.map((property) => property.id) ?? []);
|
|
314
|
+
if (relatedObject?.rootObjectId === 'sys__file' && action?.id) {
|
|
315
|
+
response = await handleFileUpload(apiServices, submission, action.id, relatedObjectId, selectedInstanceId);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${selectedInstanceId}/actions`), {
|
|
319
|
+
actionId: action?.id ?? `_${action?.type}`,
|
|
320
|
+
input: submission,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
302
323
|
if (response && relatedObject && instance) {
|
|
303
324
|
deleteDocuments(input, !!response, apiServices, relatedObject, instance, action);
|
|
304
325
|
}
|
|
@@ -491,7 +512,13 @@ const RepeatableField = (props) => {
|
|
|
491
512
|
? '_auto_'
|
|
492
513
|
: deleteForm?.id
|
|
493
514
|
: undefined, instanceId: selectedInstanceId, relatedParameter: fieldDefinition, associatedObject: instance?.id && fieldDefinition.relatedPropertyId
|
|
494
|
-
? {
|
|
515
|
+
? {
|
|
516
|
+
instanceId: instance.id,
|
|
517
|
+
propertyId: fieldDefinition.relatedPropertyId,
|
|
518
|
+
objectId: !relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId)?.objectId
|
|
519
|
+
? instance.objectId
|
|
520
|
+
: undefined,
|
|
521
|
+
}
|
|
495
522
|
: undefined })),
|
|
496
523
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
|
|
497
524
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { DocumentParameterValidation } from '@evoke-platform/context';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { DocumentReference } from '../../types';
|
|
4
4
|
type DocumentProps = {
|
|
5
5
|
id: string;
|
|
6
|
+
fieldType?: 'file' | 'document';
|
|
6
7
|
canUpdateProperty: boolean;
|
|
7
8
|
error: boolean;
|
|
8
9
|
validate?: DocumentParameterValidation;
|
|
9
|
-
value: (File |
|
|
10
|
+
value: (File | DocumentReference)[] | undefined;
|
|
10
11
|
hasDescription?: boolean;
|
|
11
12
|
};
|
|
12
13
|
export declare const Document: (props: DocumentProps) => React.JSX.Element;
|