@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.
Files changed (31) hide show
  1. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  2. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
  3. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  4. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
  5. package/dist/published/components/custom/FormV2/FormRendererContainer.js +93 -86
  6. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  7. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  8. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +104 -184
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +51 -32
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +40 -38
  17. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  18. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -169
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +12 -6
  21. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  22. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +39 -17
  23. package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
  24. package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -11
  25. package/dist/published/components/custom/FormV2/components/utils.js +169 -93
  26. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
  27. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
  28. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  29. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +38 -13
  30. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  31. 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(`/objects/sys__file/instances/${instance.id}/checkAccess?action=update`)
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)
@@ -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 permissions on the sys__file object
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/${instance.id}/checkAccess?action=view`)
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.get(endpoint).then((accessCheck) => setHasViewPermission(accessCheck.result));
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.FC<FormRendererProps> & {
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 { useObject } from '@evoke-platform/context';
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
- (async () => {
53
- try {
54
- const object = await objectStore.get({ sanitized: true });
55
- const action = object?.actions?.find((a) => a.id === actionId);
56
- setObject(object);
57
- setAction(action);
58
- if (action?.parameters) {
59
- setParameters(action.parameters);
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(FormRendererInternal, {
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
- const { instanceId, pageNavigation, dataType, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
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
- (async () => {
58
- try {
59
- if (instanceId) {
60
- const instance = await objectStore.getInstance(instanceId);
61
- setInstance(instance);
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 (pageNavigation) {
84
- apiServices
85
- .get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`))
86
- .then((page) => {
87
- setNavigationSlug(page?.slug);
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
- const needsInstance = action?.type !== 'create' && !!instanceId;
93
- // Instance and Action are loaded in the side effect above; wait for them to complete.
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
- else if (action?.type === 'delete' && formId === '_auto_') {
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?.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</p>`,
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
- else {
132
- setError('Action form could not be found');
133
- }
134
- }, [action, actionId, objectId, instance]);
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?.id : instanceId;
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 after instance creation
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
- try {
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
- if (response) {
199
- // Manually link files to created instance.
200
- await linkFiles(submission, { id: response.id, objectId: form.objectId });
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
- deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
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,5 @@
1
+ import React from 'react';
2
+ declare function ConditionalQueryClientProvider({ children }: {
3
+ children: React.ReactNode;
4
+ }): React.JSX.Element;
5
+ export default ConditionalQueryClientProvider;
@@ -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;