@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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +6 -3
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +0 -15
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +0 -10
- package/dist/published/components/custom/CriteriaBuilder/utils.js +2 -161
- package/dist/published/components/custom/Form/utils.js +3 -2
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +25 -33
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +246 -164
- 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/DefaultValues.d.ts +1 -1
- package/dist/published/components/custom/FormV2/components/DefaultValues.js +60 -89
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormContext.js +0 -1
- 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 +105 -185
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.d.ts +2 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +39 -51
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +18 -26
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +18 -18
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +2 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +96 -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 +63 -15
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +67 -36
- package/dist/published/components/custom/FormV2/components/utils.d.ts +23 -4
- package/dist/published/components/custom/FormV2/components/utils.js +139 -29
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +28 -14
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +40 -46
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +56 -19
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
- package/dist/published/components/custom/index.d.ts +2 -0
- package/dist/published/components/custom/index.js +1 -0
- package/dist/published/components/custom/types.d.ts +15 -0
- package/dist/published/components/custom/types.js +1 -0
- package/dist/published/components/custom/util.d.ts +10 -0
- package/dist/published/components/custom/util.js +161 -1
- package/dist/published/index.d.ts +2 -2
- package/dist/published/index.js +1 -1
- package/dist/published/theme/hooks.d.ts +2 -3
- 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,
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
57
|
-
(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
163
|
+
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
164
|
+
set(result, fieldId, fieldValue);
|
|
75
165
|
}
|
|
76
166
|
}
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
132
|
-
setError('Action form could not be found');
|
|
133
|
-
}
|
|
134
|
-
}, [action, actionId, objectId, instance]);
|
|
284
|
+
}, [fetchedForm, instance, action, formId]);
|
|
135
285
|
useEffect(() => {
|
|
136
|
-
|
|
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
|
|
140
|
-
|
|
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 (
|
|
145
|
-
const defaultValues = await getDefaultValues(
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
353
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
472
|
+
const defaultValues = await getDefaultValues(flattenFormEntries, instance || {});
|
|
391
473
|
setFormData(defaultValues);
|
|
392
474
|
}
|
|
393
475
|
};
|
|
@@ -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[],
|
|
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
|
}[]>;
|