@evoke-platform/ui-components 1.14.0 → 1.15.1

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 (46) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +6 -3
  2. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +0 -15
  3. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +0 -10
  4. package/dist/published/components/custom/CriteriaBuilder/utils.js +2 -161
  5. package/dist/published/components/custom/Form/utils.js +3 -2
  6. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  7. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -33
  8. package/dist/published/components/custom/FormV2/FormRendererContainer.js +246 -164
  9. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  10. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  11. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +1 -1
  12. package/dist/published/components/custom/FormV2/components/DefaultValues.js +60 -89
  13. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
  14. package/dist/published/components/custom/FormV2/components/FormContext.js +0 -1
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  17. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  18. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +105 -185
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.d.ts +2 -2
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +39 -51
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +18 -26
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +18 -18
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +2 -4
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +96 -169
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +63 -15
  28. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  29. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +67 -36
  30. package/dist/published/components/custom/FormV2/components/utils.d.ts +23 -4
  31. package/dist/published/components/custom/FormV2/components/utils.js +139 -29
  32. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +28 -14
  33. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +40 -46
  34. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  35. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +56 -19
  36. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  37. package/dist/published/components/custom/index.d.ts +2 -0
  38. package/dist/published/components/custom/index.js +1 -0
  39. package/dist/published/components/custom/types.d.ts +15 -0
  40. package/dist/published/components/custom/types.js +1 -0
  41. package/dist/published/components/custom/util.d.ts +10 -0
  42. package/dist/published/components/custom/util.js +161 -1
  43. package/dist/published/index.d.ts +2 -2
  44. package/dist/published/index.js +1 -1
  45. package/dist/published/theme/hooks.d.ts +2 -3
  46. package/package.json +4 -4
@@ -1,23 +1,30 @@
1
1
  import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
2
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
2
3
  import axios from 'axios';
3
- import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set } from 'lodash';
4
- import React, { useEffect, useRef, useState } from 'react';
4
+ import { cloneDeep, get, isArray, isEmpty, isEqual, isObject, pick, set } from 'lodash';
5
+ import React, { useCallback, useEffect, useMemo, 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, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
11
13
  import FormRenderer from './FormRenderer';
14
+ import { DepGraph } from 'dependency-graph';
15
+ // Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
12
16
  function FormRendererContainer(props) {
13
- const { instanceId, pageNavigation, dataType, title, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
17
+ return (React.createElement(ConditionalQueryClientProvider, null,
18
+ React.createElement(FormRendererContainerInner, { ...props })));
19
+ }
20
+ // Inner component that assumes QueryClient context is available
21
+ function FormRendererContainerInner(props) {
22
+ const { instanceId, pageNavigation, title, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
14
23
  const apiServices = useApiServices();
15
24
  const navigateTo = useNavigate();
25
+ const queryClient = useQueryClient();
16
26
  const { id: appId } = useApp();
17
- const [sanitizedObject, setSanitizedObject] = useState();
18
- const [navigationSlug, setNavigationSlug] = useState();
19
- const [parameters, setParameters] = useState();
20
- const [instance, setInstance] = useState();
27
+ const [parameters, setParameters] = useState([]);
21
28
  const formDataRef = useRef();
22
29
  // We only need the setter to force a re-render when form data updates; the value itself
23
30
  // is intentionally not referenced elsewhere to avoid stale reads (we use formDataRef).
@@ -46,6 +53,55 @@ function FormRendererContainer(props) {
46
53
  });
47
54
  const [isSaving, setIsSaving] = useState(false);
48
55
  const [lastSavedData, setLastSavedData] = useState({});
56
+ const [uniquePresetValues, setUniquePresetValues] = useState([]);
57
+ const flattenFormEntries = useMemo(() => {
58
+ const graph = new DepGraph({ circular: true });
59
+ const unnestedEntries = getUnnestedEntries(form?.entries || []);
60
+ const nonInputEntries = [];
61
+ for (const entry of unnestedEntries) {
62
+ const entryId = getEntryId(entry);
63
+ if (entryId && entry.type !== 'readonlyField') {
64
+ graph.addNode(entryId, entry);
65
+ }
66
+ else {
67
+ nonInputEntries.push(entry);
68
+ }
69
+ }
70
+ for (const entry of unnestedEntries) {
71
+ const entryId = getEntryId(entry);
72
+ if (entryId && (entry.type === 'input' || entry.type === 'inputField')) {
73
+ const { defaultValue } = entry.display || {};
74
+ let presetValues = [];
75
+ if (typeof defaultValue === 'string') {
76
+ presetValues = extractPresetValuesFromDynamicDefaultValues([entry]);
77
+ }
78
+ else if (isObject(defaultValue) && 'criteria' in defaultValue && !isEmpty(defaultValue.criteria)) {
79
+ presetValues = extractPresetValuesFromCriteria(defaultValue.criteria);
80
+ }
81
+ presetValues.forEach((presetValue) => {
82
+ const fragments = presetValue.replace(/{{{input.|}}}|{{input.|}}/g, '').split('.');
83
+ // preset value references top level fields i.e name or dateOfBirth
84
+ if (fragments.length === 1 && graph.hasNode(fragments[0])) {
85
+ graph.addDependency(entryId, fragments[0]);
86
+ }
87
+ else if (fragments.length > 1) {
88
+ // preset value references nested fields i.e address.line1 or person.address.line1
89
+ const addressKeys = ['line1', 'line2', 'city', 'county', 'state', 'zipCode', 'country'];
90
+ if (addressKeys.includes(fragments[1]) && graph.hasNode(`${fragments[0]}.${fragments[1]}`)) {
91
+ graph.addDependency(entryId, `${fragments[0]}.${fragments[1]}`);
92
+ }
93
+ else if (graph.hasNode(fragments[0])) {
94
+ graph.addDependency(entryId, fragments[0]);
95
+ }
96
+ }
97
+ });
98
+ }
99
+ }
100
+ return graph
101
+ .overallOrder()
102
+ .map((id) => graph.getNodeData(id))
103
+ .concat(nonInputEntries);
104
+ }, [form?.entries]);
49
105
  const userAccount = useAuthenticationContext()?.account;
50
106
  const objectStore = useObject(form?.objectId ?? objectId);
51
107
  const onError = (err) => {
@@ -53,68 +109,165 @@ function FormRendererContainer(props) {
53
109
  setSnackbarError({ ...snackbarError, isError: true });
54
110
  setError(code ?? err);
55
111
  };
56
- 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);
112
+ const { data: sanitizedObject, error: sanitizedObjectError } = useQuery({
113
+ queryKey: [form?.objectId ?? objectId, ...(instanceId ? [instanceId] : []), 'sanitized'],
114
+ queryFn: () =>
115
+ // 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'}`), { params: { sanitizedVersion: true } }),
117
+ staleTime: Infinity,
118
+ enabled: !!(form?.objectId || objectId),
119
+ });
120
+ // trigger refetch on success
121
+ const { data: instance, error: instanceError } = useQuery({
122
+ queryKey: [objectId, instanceId, 'instance', uniquePresetValues],
123
+ queryFn: async () => {
124
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}`), {
125
+ params: {
126
+ expand: uniquePresetValues
127
+ .filter((value) => value.startsWith('{{{input.') ||
128
+ value.startsWith('{{input.') ||
129
+ value.startsWith('{{{instance.'))
130
+ .map((value) => {
131
+ return value
132
+ .replace(/{{{|}}}|{{|}}/g, '')
133
+ .split('.')
134
+ .slice(1)
135
+ .join('.');
136
+ }),
137
+ },
138
+ });
139
+ return instance;
140
+ },
141
+ staleTime: Infinity,
142
+ enabled: !!instanceId && !!sanitizedObject,
143
+ });
144
+ const getDefaultValues = useCallback(async (unnestedEntries, instanceData) => {
145
+ const result = cloneDeep(instanceData);
146
+ for (const entry of unnestedEntries) {
147
+ const fieldId = getEntryId(entry);
148
+ if (!fieldId)
149
+ continue;
150
+ const fieldValue = get(result, fieldId);
151
+ if ((entry.type === 'input' || entry.type === 'inputField') &&
152
+ isAddressProperty(entry.parameterId || entry.input?.id)) {
153
+ if ((isEmpty(result) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
154
+ entry?.display?.defaultValue &&
155
+ parameters) {
156
+ const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, result);
157
+ if (isArray(defaultValuesArray)) {
158
+ defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
159
+ set(result, fieldId, fieldValue);
160
+ });
161
+ }
72
162
  }
73
- else {
74
- setError('Action could not be found');
163
+ else if (fieldValue !== undefined && fieldValue !== null) {
164
+ set(result, fieldId, fieldValue);
75
165
  }
76
166
  }
77
- catch (error) {
78
- onError(error);
167
+ else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
168
+ const parameter = parameters?.find((param) => param.id === fieldId);
169
+ if (associatedObject?.propertyId === fieldId &&
170
+ associatedObject?.instanceId &&
171
+ parameter &&
172
+ action?.type === 'create') {
173
+ try {
174
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`), {
175
+ params: {
176
+ expand: uniquePresetValues.filter((value) => value.startsWith(`{{{input.${fieldId}.`) ||
177
+ value.startsWith(`{{input.${fieldId}.`)),
178
+ },
179
+ });
180
+ result[associatedObject.propertyId] = instance;
181
+ }
182
+ catch (error) {
183
+ console.error(error);
184
+ }
185
+ }
186
+ else if (entry.type === 'formlet') {
187
+ // TODO: this should eventually fetch the formletId then get the fields and default values of those fields
188
+ }
189
+ else if (entry.type !== 'readonlyField') {
190
+ if (isEmptyWithDefault(fieldValue, entry, result)) {
191
+ if (fieldId && parameters && parameters.length > 0) {
192
+ const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, result);
193
+ for (const { fieldId, fieldValue } of defaultValuesArray) {
194
+ const parameter = parameters?.find((param) => param.id === fieldId);
195
+ if (parameter?.type === 'object') {
196
+ const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
197
+ for (const field of dependentFields) {
198
+ set(result, field.fieldId, field.fieldValue);
199
+ }
200
+ }
201
+ set(result, fieldId, fieldValue);
202
+ }
203
+ }
204
+ }
205
+ else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
206
+ result[fieldId] = false;
207
+ }
208
+ else if (fieldValue !== undefined && fieldValue !== null) {
209
+ if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
210
+ let RTFFieldValue = fieldValue;
211
+ if (!fieldValue.trim().startsWith('{\\rtf')) {
212
+ RTFFieldValue = plainTextToRtf(fieldValue);
213
+ }
214
+ result[fieldId] = RTFFieldValue;
215
+ }
216
+ else {
217
+ result[fieldId] = fieldValue;
218
+ }
219
+ }
220
+ }
79
221
  }
80
- })();
81
- }, [dataType, form, instanceId]);
82
- useEffect(() => {
83
- if (pageNavigation) {
84
- apiServices
85
- .get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`))
86
- .then((page) => {
87
- setNavigationSlug(page?.slug);
88
- });
89
222
  }
90
- }, []);
223
+ return result;
224
+ }, [action, parameters, associatedObject, uniquePresetValues, formDataRef, apiServices, userAccount]);
91
225
  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)
226
+ if (!sanitizedObject)
96
227
  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
- });
228
+ const allCriterias = extractAllCriteria(flattenFormEntries, parameters || []);
229
+ const uniquePresetValues = new Set();
230
+ for (const criteria of allCriterias) {
231
+ const presetValues = extractPresetValuesFromCriteria(criteria);
232
+ presetValues.forEach((value) => uniquePresetValues.add(value));
233
+ }
234
+ extractPresetValuesFromDynamicDefaultValues(flattenFormEntries).map((value) => uniquePresetValues.add(value));
235
+ setUniquePresetValues(Array.from(uniquePresetValues));
236
+ const action = sanitizedObject.actions?.find((a) => a.id === (form?.actionId || actionId));
237
+ if (action && (instanceId || action.type === 'create')) {
238
+ setAction(action);
239
+ // Clear error if action is found after being missing
240
+ setError((prevError) => (prevError === 'Action could not be found' ? undefined : prevError));
113
241
  }
114
- else if (action?.type === 'delete' && formId === '_auto_') {
242
+ else {
243
+ setError('Action could not be found');
244
+ }
245
+ }, [sanitizedObject, actionId, form?.actionId, instanceId, flattenFormEntries, parameters]);
246
+ const { data: navigationSlug } = useQuery({
247
+ queryKey: [appId, 'navigationSlug'],
248
+ queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
249
+ select: (page) => page.slug,
250
+ staleTime: Infinity,
251
+ enabled: !!pageNavigation,
252
+ });
253
+ const formIdToFetch = formId || action?.defaultFormId;
254
+ const { data: fetchedForm, error: fetchedFormError } = useFormById(formIdToFetch ?? '', apiServices);
255
+ useEffect(() => {
256
+ if (!formIdToFetch && action) {
257
+ setError('Action form could not be found');
258
+ }
259
+ }, [formIdToFetch, action]);
260
+ useEffect(() => {
261
+ if (fetchedForm) {
262
+ if (actionId && fetchedForm.actionId !== actionId) {
263
+ setError('Configured action ID does not match form action ID');
264
+ }
265
+ setForm(fetchedForm);
266
+ }
267
+ else if (action?.type === 'delete' && formId === '_auto_' && instance) {
115
268
  setForm({
116
269
  id: '',
117
- name: '',
270
+ name: 'Delete',
118
271
  entries: [
119
272
  {
120
273
  type: 'content',
@@ -128,28 +281,31 @@ function FormRendererContainer(props) {
128
281
  },
129
282
  });
130
283
  }
131
- else {
132
- setError('Action form could not be found');
133
- }
134
- }, [action, actionId, objectId, instance]);
284
+ }, [fetchedForm, instance, action, formId]);
135
285
  useEffect(() => {
136
- if (!form)
286
+ const error = sanitizedObjectError || fetchedFormError || instanceError;
287
+ if (error)
288
+ onError(error);
289
+ }, [sanitizedObjectError, fetchedFormError, instanceError]);
290
+ useEffect(() => {
291
+ if (!form || !action)
137
292
  return;
138
293
  // If no parameters are defined, then the action is synced with object properties
139
- const getParamsFromObject = sanitizedObject && !action?.parameters;
140
- setParameters(getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action?.parameters);
294
+ const getParamsFromObject = sanitizedObject && !action.parameters;
295
+ const parameters = (getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action.parameters) ?? [];
296
+ setParameters(parameters.filter((param) => param.type !== 'collection' && !param.formula));
141
297
  }, [form, action?.parameters, sanitizedObject]);
142
298
  useEffect(() => {
143
299
  const getInitialValues = async () => {
144
- if (form && (instance || !instanceId)) {
145
- const defaultValues = await getDefaultValues(form.entries, instance || {});
300
+ if (flattenFormEntries.length && (instance || !instanceId)) {
301
+ const defaultValues = await getDefaultValues(flattenFormEntries, instance || {});
146
302
  setFormData(defaultValues);
147
303
  // Deep clone to avoid reference issues
148
304
  setLastSavedData(cloneDeep(defaultValues));
149
305
  }
150
306
  };
151
307
  getInitialValues();
152
- }, [form, instance, sanitizedObject]);
308
+ }, [instanceId, instance, flattenFormEntries, getDefaultValues]);
153
309
  const onSubmissionSuccess = (updatedInstance) => {
154
310
  setSnackbarError({
155
311
  showAlert: true,
@@ -158,14 +314,19 @@ function FormRendererContainer(props) {
158
314
  });
159
315
  if (navigationSlug) {
160
316
  if (navigationSlug.includes(':instanceId')) {
161
- const navigateInstanceId = action?.type === 'create' ? updatedInstance?.id : instanceId;
317
+ const navigateInstanceId = action?.type === 'create' ? updatedInstance.id : instanceId;
162
318
  navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', navigateInstanceId ?? ':instanceId')}`);
163
319
  }
164
320
  else {
165
321
  navigateTo(`/${appId}/${navigationSlug}`);
166
322
  }
167
323
  }
168
- setInstance(updatedInstance);
324
+ if (instanceId) {
325
+ // Invalidate the instance to fetch the latest version
326
+ queryClient.invalidateQueries({
327
+ queryKey: [objectId, instanceId, 'instance'],
328
+ });
329
+ }
169
330
  };
170
331
  const linkFiles = async (submission, linkTo) => {
171
332
  // Create file links for any uploaded files after instance creation
@@ -191,9 +352,7 @@ function FormRendererContainer(props) {
191
352
  if (action?.type === 'create') {
192
353
  const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
193
354
  actionId: form.actionId,
194
- input: omit(submission, sanitizedObject?.properties
195
- ?.filter((property) => property.formula || property.type === 'collection')
196
- .map((property) => property.id) ?? []),
355
+ input: pick(submission, parameters.map((parameter) => parameter.id)),
197
356
  });
198
357
  if (response) {
199
358
  // Manually link files to created instance.
@@ -204,9 +363,7 @@ function FormRendererContainer(props) {
204
363
  else if (instanceId && action) {
205
364
  const response = await objectStore.instanceAction(instanceId, {
206
365
  actionId: action.id,
207
- input: omit(submission, sanitizedObject?.properties
208
- ?.filter((property) => property.formula || property.type === 'collection')
209
- .map((property) => property.id) ?? []),
366
+ input: pick(submission, parameters.map((parameter) => parameter.id)),
210
367
  });
211
368
  if (sanitizedObject && instance) {
212
369
  onSubmissionSuccess(response);
@@ -227,78 +384,6 @@ function FormRendererContainer(props) {
227
384
  throw error; // Throw error so caller knows submission failed
228
385
  }
229
386
  };
230
- const getDefaultValues = async (entries, instanceData) => {
231
- const result = {};
232
- const unnestedEntries = getUnnestedEntries(entries);
233
- for (const entry of unnestedEntries) {
234
- const fieldId = getEntryId(entry);
235
- if (!fieldId)
236
- continue;
237
- const fieldValue = get(instanceData, fieldId);
238
- if ((entry.type === 'input' || entry.type === 'inputField') &&
239
- isAddressProperty(entry.parameterId || entry.input?.id)) {
240
- if ((isEmpty(instanceData) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
241
- entry?.display?.defaultValue &&
242
- parameters) {
243
- const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
244
- if (isArray(defaultValuesArray)) {
245
- defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
246
- set(result, fieldId, fieldValue);
247
- });
248
- }
249
- }
250
- else if (fieldValue !== undefined && fieldValue !== null) {
251
- set(result, fieldId, fieldValue);
252
- }
253
- }
254
- else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
255
- const parameter = parameters?.find((param) => param.id === fieldId);
256
- if (associatedObject?.propertyId === fieldId &&
257
- associatedObject?.instanceId &&
258
- parameter &&
259
- action?.type === 'create') {
260
- try {
261
- const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
262
- result[associatedObject.propertyId] = instance;
263
- }
264
- catch (error) {
265
- console.error(error);
266
- }
267
- }
268
- else if (entry.type !== 'readonlyField' && isEmptyWithDefault(fieldValue, entry, instanceData)) {
269
- if (fieldId && parameters && parameters.length > 0) {
270
- const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
271
- for (const { fieldId, fieldValue } of defaultValuesArray) {
272
- const parameter = parameters?.find((param) => param.id === fieldId);
273
- if (parameter?.type === 'object') {
274
- const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
275
- for (const field of dependentFields) {
276
- set(result, field.fieldId, field.fieldValue);
277
- }
278
- }
279
- set(result, fieldId, fieldValue);
280
- }
281
- }
282
- }
283
- else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
284
- result[fieldId] = false;
285
- }
286
- else if (fieldValue !== undefined && fieldValue !== null) {
287
- if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
288
- let RTFFieldValue = fieldValue;
289
- if (!fieldValue.trim().startsWith('{\\rtf')) {
290
- RTFFieldValue = plainTextToRtf(fieldValue);
291
- }
292
- result[fieldId] = RTFFieldValue;
293
- }
294
- else {
295
- result[fieldId] = fieldValue;
296
- }
297
- }
298
- }
299
- }
300
- return result;
301
- };
302
387
  const removeUneditedProtectedValues = (data) => {
303
388
  const protectedProperties = sanitizedObject?.properties?.filter((prop) => prop.protection?.maskChar);
304
389
  if (!protectedProperties || protectedProperties.length === 0) {
@@ -349,26 +434,23 @@ function FormRendererContainer(props) {
349
434
  const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
350
435
  async function onChange(id, value) {
351
436
  const parameter = parameters?.find((param) => param.id === id);
352
- const entries = getUnnestedEntries(form.entries);
353
- const isReadOnlyField = entries.some((e) => e.type === 'readonlyField' && e.propertyId === id) &&
354
- !entries.some((e) => (e.type === 'input' && e.parameterId === id) || (e.type === 'inputField' && e.input.id === id));
437
+ const isReadOnlyField = flattenFormEntries.some((e) => e.type === 'readonlyField' && e.propertyId === id) &&
438
+ !flattenFormEntries.some((e) => (e.type === 'input' && e.parameterId === id) || (e.type === 'inputField' && e.input.id === id));
355
439
  if (isReadOnlyField)
356
440
  return;
357
- if (parameter) {
358
- if (parameter.type === 'object' && parameters && parameters.length > 0) {
441
+ if (parameter?.type === 'string' && parameter.enum && value) {
442
+ // If a single select property has a sortBy option that isn't NONE the value gets spread and doesn't save properly,
443
+ // this will make it correctly save the value
444
+ value = value.value ? value.value : value;
445
+ }
446
+ if (!isEqual(value, get(formDataRef.current, id))) {
447
+ if (parameter?.type === 'object' && parameters && parameters.length > 0) {
359
448
  // On change of a related object, update default values dependent on that object
360
- const dependentFields = await processValueUpdate(entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
449
+ const dependentFields = await processValueUpdate(flattenFormEntries, parameters, value, apiServices, id, formDataRef.current, userAccount);
361
450
  for (const field of dependentFields) {
362
451
  onChange(field.fieldId, field.fieldValue);
363
452
  }
364
453
  }
365
- else if (parameter.type === 'string' && parameter.enum && value) {
366
- // If a single select property has a sortBy option that isn't NONE the value gets spread and doesn't save properly,
367
- // this will make it correctly save the value
368
- value = value.value ? value.value : value;
369
- }
370
- }
371
- if (!isEqual(value, get(formDataRef.current, id))) {
372
454
  const newData = { ...formDataRef.current };
373
455
  set(newData, id, value);
374
456
  setFormData(newData);
@@ -387,7 +469,7 @@ function FormRendererContainer(props) {
387
469
  ? onDiscardChangesOverride
388
470
  : async () => {
389
471
  if (form) {
390
- const defaultValues = await getDefaultValues(form.entries, instance || {});
472
+ const defaultValues = await getDefaultValues(flattenFormEntries, instance || {});
391
473
  setFormData(defaultValues);
392
474
  }
393
475
  };
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import { ApiServices, FormEntry, InputField, InputParameter, InputParameterReference, ObjectInstance, Reference, UserAccount } from '@evoke-platform/context';
2
2
  import { FieldValues } from 'react-hook-form';
3
- export declare function evalDefaultVals(parameters: InputParameter[], unnestedEntries: FormEntry[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
3
+ export declare function evalDefaultVals(parameters: InputParameter[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference, updatedRelatedObjectParamId?: string): Promise<{
4
4
  fieldId: string;
5
5
  fieldValue: unknown;
6
6
  }[]>;