@evoke-platform/ui-components 1.13.0-dev.5 → 1.13.0-dev.7
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/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +93 -86
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +104 -184
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
- 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 +51 -32
- 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 +40 -38
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -169
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +12 -6
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +39 -17
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -11
- package/dist/published/components/custom/FormV2/components/utils.js +169 -93
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +38 -13
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
- package/package.json +3 -2
|
@@ -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;
|
|
@@ -27,7 +27,7 @@ export type FormRendererProps = BaseProps & {
|
|
|
27
27
|
renderBody?: (props: BodyProps) => React.ReactNode;
|
|
28
28
|
renderFooter?: (props: FooterProps) => React.ReactNode;
|
|
29
29
|
};
|
|
30
|
-
export declare const FormRenderer: React.
|
|
30
|
+
export declare const FormRenderer: ((props: FormRendererProps) => React.JSX.Element) & {
|
|
31
31
|
Header: React.FC<HeaderProps>;
|
|
32
32
|
Body: React.FC<BodyProps>;
|
|
33
33
|
Footer: React.FC<FooterProps>;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useApiServices, } from '@evoke-platform/context';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import { isEmpty, isEqual, omit } from 'lodash';
|
|
3
4
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
5
|
import { useForm } from 'react-hook-form';
|
|
5
6
|
import { useWidgetSize } from '../../../theme';
|
|
6
7
|
import { Box } from '../../layout';
|
|
7
8
|
import { Body } from './components/Body';
|
|
9
|
+
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
8
10
|
import { Footer, FooterActions } from './components/Footer';
|
|
9
11
|
import { FormContext } from './components/FormContext';
|
|
10
12
|
import Header, { AccordionActions, Title } from './components/Header';
|
|
11
|
-
import { assignIdsToSectionsAndRichText, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, isAddressProperty, obfuscateValue, } from './components/utils';
|
|
13
|
+
import { assignIdsToSectionsAndRichText, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, getPrefixedUrl, isAddressProperty, obfuscateValue, } from './components/utils';
|
|
12
14
|
import { handleValidation } from './components/ValidationFiles/Validation';
|
|
13
15
|
import ValidationErrors from './components/ValidationFiles/ValidationErrors';
|
|
14
16
|
const FormRendererInternal = (props) => {
|
|
@@ -23,15 +25,14 @@ const FormRendererInternal = (props) => {
|
|
|
23
25
|
defaultWidth: 1200,
|
|
24
26
|
});
|
|
25
27
|
const isSmallerThanMd = isBelow('md');
|
|
28
|
+
const apiServices = useApiServices();
|
|
26
29
|
const [expandedSections, setExpandedSections] = useState([]);
|
|
27
30
|
const [fetchedOptions, setFetchedOptions] = useState({});
|
|
28
31
|
const [expandAll, setExpandAll] = useState();
|
|
29
32
|
const [action, setAction] = useState();
|
|
30
|
-
const [object, setObject] = useState();
|
|
31
33
|
const [triggerFieldReset, setTriggerFieldReset] = useState(false);
|
|
32
34
|
const [isInitializing, setIsInitializing] = useState(true);
|
|
33
35
|
const [parameters, setParameters] = useState();
|
|
34
|
-
const objectStore = useObject(objectId);
|
|
35
36
|
const validationContainerRef = useRef(null);
|
|
36
37
|
const updateFetchedOptions = (newData) => {
|
|
37
38
|
setFetchedOptions((prev) => ({
|
|
@@ -45,32 +46,26 @@ const FormRendererInternal = (props) => {
|
|
|
45
46
|
function handleCollapseAll() {
|
|
46
47
|
setExpandAll(false);
|
|
47
48
|
}
|
|
49
|
+
const { data: object } = useQuery({
|
|
50
|
+
queryKey: [objectId, 'sanitized'],
|
|
51
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${objectId}/effective`), {
|
|
52
|
+
params: { sanitizedVersion: true },
|
|
53
|
+
}),
|
|
54
|
+
staleTime: Infinity,
|
|
55
|
+
enabled: !!objectId,
|
|
56
|
+
});
|
|
48
57
|
const updatedEntries = useMemo(() => {
|
|
49
58
|
return object ? assignIdsToSectionsAndRichText(entries, object, parameters) : [];
|
|
50
59
|
}, [entries, object, parameters]);
|
|
51
60
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
else if (object) {
|
|
62
|
-
// if forms actionId is synced with object properties
|
|
63
|
-
setParameters(convertPropertiesToParams(object));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
console.error('Failed to fetch object, action or parameters:', error);
|
|
68
|
-
}
|
|
69
|
-
finally {
|
|
70
|
-
setIsInitializing(false);
|
|
71
|
-
}
|
|
72
|
-
})();
|
|
73
|
-
}, [objectStore, actionId]);
|
|
61
|
+
if (!object || !actionId)
|
|
62
|
+
return;
|
|
63
|
+
const action = object.actions?.find((a) => a.id === actionId);
|
|
64
|
+
setAction(action);
|
|
65
|
+
// if forms action is synced with object properties then convertPropertiesToParams
|
|
66
|
+
setParameters(action?.parameters ?? convertPropertiesToParams(object));
|
|
67
|
+
setIsInitializing(false);
|
|
68
|
+
}, [object, actionId]);
|
|
74
69
|
useEffect(() => {
|
|
75
70
|
const currentValues = getValues();
|
|
76
71
|
if (value) {
|
|
@@ -243,7 +238,10 @@ const FormRendererInternal = (props) => {
|
|
|
243
238
|
} })),
|
|
244
239
|
action && onSubmit && (renderFooter ? renderFooter(footerProps) : React.createElement(Footer, { ...footerProps }))))));
|
|
245
240
|
};
|
|
246
|
-
export const FormRenderer = Object.assign(
|
|
241
|
+
export const FormRenderer = Object.assign(function FormRenderer(props) {
|
|
242
|
+
return (React.createElement(ConditionalQueryClientProvider, null,
|
|
243
|
+
React.createElement(FormRendererInternal, { ...props })));
|
|
244
|
+
}, {
|
|
247
245
|
Header,
|
|
248
246
|
Body,
|
|
249
247
|
Footer,
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import axios from 'axios';
|
|
3
4
|
import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set } from 'lodash';
|
|
4
5
|
import React, { useEffect, useRef, useState } from 'react';
|
|
5
6
|
import { Skeleton, Snackbar } from '../../core';
|
|
6
7
|
import { Box } from '../../layout';
|
|
7
8
|
import ErrorComponent from '../ErrorComponent';
|
|
9
|
+
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
8
10
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
9
11
|
import Header from './components/Header';
|
|
10
|
-
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
|
|
12
|
+
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
11
13
|
import FormRenderer from './FormRenderer';
|
|
14
|
+
// Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
|
|
12
15
|
function FormRendererContainer(props) {
|
|
13
|
-
|
|
16
|
+
return (React.createElement(ConditionalQueryClientProvider, null,
|
|
17
|
+
React.createElement(FormRendererContainerInner, { ...props })));
|
|
18
|
+
}
|
|
19
|
+
// Inner component that assumes QueryClient context is available
|
|
20
|
+
function FormRendererContainerInner(props) {
|
|
21
|
+
const { instanceId, pageNavigation, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
|
|
14
22
|
const apiServices = useApiServices();
|
|
15
23
|
const navigateTo = useNavigate();
|
|
16
24
|
const { id: appId } = useApp();
|
|
17
|
-
const [sanitizedObject, setSanitizedObject] = useState();
|
|
18
|
-
const [navigationSlug, setNavigationSlug] = useState();
|
|
19
25
|
const [parameters, setParameters] = useState();
|
|
20
26
|
const [instance, setInstance] = useState();
|
|
21
27
|
const formDataRef = useRef();
|
|
@@ -53,72 +59,68 @@ function FormRendererContainer(props) {
|
|
|
53
59
|
setSnackbarError({ ...snackbarError, isError: true });
|
|
54
60
|
setError(code ?? err);
|
|
55
61
|
};
|
|
62
|
+
const { data: sanitizedObject, error: sanitizedObjectError } = useQuery({
|
|
63
|
+
queryKey: [form?.objectId ?? objectId, ...(instanceId ? [instanceId] : []), 'sanitized'],
|
|
64
|
+
queryFn: () =>
|
|
65
|
+
// form?.objectId is needed for subtype forms to get the correct object
|
|
66
|
+
apiServices.get(getPrefixedUrl(`/objects/${form?.objectId ?? objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } }),
|
|
67
|
+
staleTime: Infinity,
|
|
68
|
+
enabled: !!(form?.objectId || objectId),
|
|
69
|
+
});
|
|
70
|
+
const { data: fetchedInstance, error: instanceError } = useQuery({
|
|
71
|
+
queryKey: [objectId, instanceId, 'instance'],
|
|
72
|
+
queryFn: () => objectStore.getInstance(instanceId),
|
|
73
|
+
staleTime: Infinity,
|
|
74
|
+
enabled: !!instanceId && !!sanitizedObject,
|
|
75
|
+
});
|
|
56
76
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
const object = await apiServices.get(getPrefixedUrl(`/objects/${form?.objectId || objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } });
|
|
64
|
-
setSanitizedObject(object);
|
|
65
|
-
const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
66
|
-
if (action && (instanceId || action.type === 'create')) {
|
|
67
|
-
setAction(action);
|
|
68
|
-
// Clear error if action is found after being missing
|
|
69
|
-
// TODO: This entire effect should take place after form is fetched to avoid an error flickering
|
|
70
|
-
// That is, this effect should be merged with the one below that fetches the form
|
|
71
|
-
setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
setError('Action could not be found');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
onError(error);
|
|
79
|
-
}
|
|
80
|
-
})();
|
|
81
|
-
}, [dataType, form, instanceId]);
|
|
77
|
+
if (fetchedInstance)
|
|
78
|
+
setInstance(fetchedInstance);
|
|
79
|
+
if (instanceError)
|
|
80
|
+
onError(instanceError);
|
|
81
|
+
}, [fetchedInstance, instanceError]);
|
|
82
82
|
useEffect(() => {
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
if (!sanitizedObject)
|
|
84
|
+
return;
|
|
85
|
+
const action = sanitizedObject.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
86
|
+
if (action && (instanceId || action.type === 'create')) {
|
|
87
|
+
setAction(action);
|
|
88
|
+
// Clear error if action is found after being missing
|
|
89
|
+
setError((prevError) => (prevError === 'Action could not be found' ? undefined : prevError));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
setError('Action could not be found');
|
|
89
93
|
}
|
|
90
|
-
}, []);
|
|
94
|
+
}, [sanitizedObject, actionId, form?.actionId, instanceId]);
|
|
95
|
+
const { data: navigationSlug } = useQuery({
|
|
96
|
+
queryKey: [appId, 'navigationSlug'],
|
|
97
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
|
|
98
|
+
select: (page) => page.slug,
|
|
99
|
+
staleTime: Infinity,
|
|
100
|
+
enabled: !!pageNavigation,
|
|
101
|
+
});
|
|
102
|
+
const formIdToFetch = formId || action?.defaultFormId;
|
|
103
|
+
const { data: fetchedForm, error: fetchedFormError } = useFormById(formIdToFetch ?? '', apiServices);
|
|
91
104
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const loading = (actionId && !action) || (needsInstance && !instance);
|
|
95
|
-
if (form || loading)
|
|
96
|
-
return;
|
|
97
|
-
if ((formId || action?.defaultFormId) && formId !== '_auto_') {
|
|
98
|
-
apiServices
|
|
99
|
-
.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
|
|
100
|
-
.then((evokeForm) => {
|
|
101
|
-
// If an actionId is provided, ensure it matches the form's actionId
|
|
102
|
-
if (!actionId || evokeForm?.actionId === actionId) {
|
|
103
|
-
const form = evokeForm;
|
|
104
|
-
setForm(form);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
setError('Configured action ID does not match form action ID');
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
.catch((error) => {
|
|
111
|
-
onError(error);
|
|
112
|
-
});
|
|
105
|
+
if (!formIdToFetch && action) {
|
|
106
|
+
setError('Action form could not be found');
|
|
113
107
|
}
|
|
114
|
-
|
|
108
|
+
}, [formIdToFetch, action]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (fetchedForm) {
|
|
111
|
+
if (actionId && fetchedForm.actionId !== actionId) {
|
|
112
|
+
setError('Configured action ID does not match form action ID');
|
|
113
|
+
}
|
|
114
|
+
setForm(fetchedForm);
|
|
115
|
+
}
|
|
116
|
+
else if (action?.type === 'delete' && formId === '_auto_' && instance) {
|
|
115
117
|
setForm({
|
|
116
118
|
id: '',
|
|
117
|
-
name: '',
|
|
119
|
+
name: 'Delete',
|
|
118
120
|
entries: [
|
|
119
121
|
{
|
|
120
122
|
type: 'content',
|
|
121
|
-
html: `<p style="padding-top: 24px; padding-bottom: 24px;">You are about to delete <strong>${instance
|
|
123
|
+
html: `<p style="padding-top: 24px; padding-bottom: 24px;">You are about to delete <strong>${instance.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</p>`,
|
|
122
124
|
},
|
|
123
125
|
],
|
|
124
126
|
objectId: objectId,
|
|
@@ -128,10 +130,12 @@ function FormRendererContainer(props) {
|
|
|
128
130
|
},
|
|
129
131
|
});
|
|
130
132
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
}, [fetchedForm, instance, action, formId]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const error = sanitizedObjectError || fetchedFormError;
|
|
136
|
+
if (error)
|
|
137
|
+
onError(error);
|
|
138
|
+
}, [sanitizedObjectError, fetchedFormError]);
|
|
135
139
|
useEffect(() => {
|
|
136
140
|
if (!form)
|
|
137
141
|
return;
|
|
@@ -141,7 +145,7 @@ function FormRendererContainer(props) {
|
|
|
141
145
|
}, [form, action?.parameters, sanitizedObject]);
|
|
142
146
|
useEffect(() => {
|
|
143
147
|
const getInitialValues = async () => {
|
|
144
|
-
if (form && (instance || !instanceId)) {
|
|
148
|
+
if (form && parameters && (instance || !instanceId)) {
|
|
145
149
|
const defaultValues = await getDefaultValues(form.entries, instance || {});
|
|
146
150
|
setFormData(defaultValues);
|
|
147
151
|
// Deep clone to avoid reference issues
|
|
@@ -149,7 +153,7 @@ function FormRendererContainer(props) {
|
|
|
149
153
|
}
|
|
150
154
|
};
|
|
151
155
|
getInitialValues();
|
|
152
|
-
}, [form, instance, sanitizedObject]);
|
|
156
|
+
}, [form, instance, sanitizedObject, parameters]);
|
|
153
157
|
const onSubmissionSuccess = (updatedInstance) => {
|
|
154
158
|
setSnackbarError({
|
|
155
159
|
showAlert: true,
|
|
@@ -158,7 +162,7 @@ function FormRendererContainer(props) {
|
|
|
158
162
|
});
|
|
159
163
|
if (navigationSlug) {
|
|
160
164
|
if (navigationSlug.includes(':instanceId')) {
|
|
161
|
-
const navigateInstanceId = action?.type === 'create' ? updatedInstance
|
|
165
|
+
const navigateInstanceId = action?.type === 'create' ? updatedInstance.id : instanceId;
|
|
162
166
|
navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', navigateInstanceId ?? ':instanceId')}`);
|
|
163
167
|
}
|
|
164
168
|
else {
|
|
@@ -167,18 +171,17 @@ function FormRendererContainer(props) {
|
|
|
167
171
|
}
|
|
168
172
|
setInstance(updatedInstance);
|
|
169
173
|
};
|
|
174
|
+
/**
|
|
175
|
+
* Manually links any newly uploaded files in the submission to the specified instance.
|
|
176
|
+
* @param submission The form submission data
|
|
177
|
+
* @param linkTo The instance to link the files to
|
|
178
|
+
*/
|
|
170
179
|
const linkFiles = async (submission, linkTo) => {
|
|
171
|
-
// Create file links for any uploaded files
|
|
180
|
+
// Create file links for any uploaded files that haven't been linked yet
|
|
172
181
|
for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
|
|
173
182
|
const files = submission[property.id];
|
|
174
183
|
if (files?.length) {
|
|
175
|
-
|
|
176
|
-
await createFileLinks(files, linkTo, apiServices);
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
console.error('Failed to create file links:', error);
|
|
180
|
-
// Don't fail the entire submission if file linking fails
|
|
181
|
-
}
|
|
184
|
+
await createFileLinks(files, linkTo, apiServices);
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
};
|
|
@@ -195,11 +198,9 @@ function FormRendererContainer(props) {
|
|
|
195
198
|
?.filter((property) => property.formula || property.type === 'collection')
|
|
196
199
|
.map((property) => property.id) ?? []),
|
|
197
200
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
onSubmissionSuccess(response);
|
|
202
|
-
}
|
|
201
|
+
// Manually link files to created instance.
|
|
202
|
+
await linkFiles(submission, { id: response.id, objectId: form.objectId });
|
|
203
|
+
onSubmissionSuccess(response);
|
|
203
204
|
}
|
|
204
205
|
else if (instanceId && action) {
|
|
205
206
|
const response = await objectStore.instanceAction(instanceId, {
|
|
@@ -209,21 +210,26 @@ function FormRendererContainer(props) {
|
|
|
209
210
|
.map((property) => property.id) ?? []),
|
|
210
211
|
});
|
|
211
212
|
if (sanitizedObject && instance) {
|
|
213
|
+
if (!onAutosave) {
|
|
214
|
+
// For non-autosave updates, link any uploaded files to the instance.
|
|
215
|
+
await linkFiles(submission, { id: instanceId, objectId: objectId });
|
|
216
|
+
}
|
|
212
217
|
onSubmissionSuccess(response);
|
|
213
|
-
|
|
218
|
+
// Only delete the necessary files after submission succeeds to avoid deleting a file prematurely.
|
|
219
|
+
await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
}
|
|
217
223
|
catch (error) {
|
|
218
|
-
// Handle deleteDocuments for uploaded documents if the main submission fails
|
|
219
|
-
if (instanceId && action && sanitizedObject && instance) {
|
|
220
|
-
deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
221
|
-
}
|
|
222
224
|
setSnackbarError({
|
|
223
225
|
isError: true,
|
|
224
226
|
showAlert: true,
|
|
225
227
|
message: error.response?.data?.error?.message ?? 'An error occurred',
|
|
226
228
|
});
|
|
229
|
+
if (instanceId && action && sanitizedObject && instance) {
|
|
230
|
+
// For an update, uploaded documents have been linked to the instance and need to be deleted.
|
|
231
|
+
await deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
232
|
+
}
|
|
227
233
|
throw error; // Throw error so caller knows submission failed
|
|
228
234
|
}
|
|
229
235
|
};
|
|
@@ -327,7 +333,7 @@ function FormRendererContainer(props) {
|
|
|
327
333
|
try {
|
|
328
334
|
setIsSaving(true);
|
|
329
335
|
const cleanedData = removeUneditedProtectedValues(formDataRef.current);
|
|
330
|
-
const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError);
|
|
336
|
+
const submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
|
|
331
337
|
// Handle object instance autosave
|
|
332
338
|
if (instanceId && action?.type === 'update') {
|
|
333
339
|
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
@@ -336,6 +342,7 @@ function FormRendererContainer(props) {
|
|
|
336
342
|
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
337
343
|
.map((property) => property.id) ?? []),
|
|
338
344
|
});
|
|
345
|
+
await linkFiles(submission, { id: instanceId, objectId });
|
|
339
346
|
}
|
|
340
347
|
setLastSavedData(cloneDeep(formDataRef.current));
|
|
341
348
|
setIsSaving(false);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { QueryCache, QueryClient, QueryClientContext, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import React, { useContext, useState } from 'react';
|
|
3
|
+
// If FormRenderer is rendered outside a QueryClientProvider (e.g. standalone usage),
|
|
4
|
+
// we create a local QueryClient so React Query hooks still work.
|
|
5
|
+
// If a provider already exists, we reuse it to avoid fragmenting the cache.
|
|
6
|
+
function ConditionalQueryClientProvider({ children }) {
|
|
7
|
+
const existingQueryClient = useContext(QueryClientContext);
|
|
8
|
+
const [localQueryClient] = useState(() => new QueryClient({
|
|
9
|
+
queryCache: new QueryCache({
|
|
10
|
+
onError: (error, query) => {
|
|
11
|
+
const message = query.meta?.errorMessage ?? 'Something went wrong:';
|
|
12
|
+
console.error(message, error);
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
if (existingQueryClient) {
|
|
17
|
+
return React.createElement(React.Fragment, null, children);
|
|
18
|
+
}
|
|
19
|
+
return React.createElement(QueryClientProvider, { client: localQueryClient }, children);
|
|
20
|
+
}
|
|
21
|
+
export default ConditionalQueryClientProvider;
|