@evoke-platform/ui-components 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +2 -2
- 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 -27
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +131 -100
- 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 +105 -185
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
- 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/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 +57 -13
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +61 -29
- package/dist/published/components/custom/FormV2/components/utils.d.ts +23 -4
- package/dist/published/components/custom/FormV2/components/utils.js +136 -26
- 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/package.json +3 -4
|
@@ -1,23 +1,29 @@
|
|
|
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
4
|
import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set } from 'lodash';
|
|
4
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import React, { 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
|
+
// 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, title, 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();
|
|
24
|
+
const queryClient = useQueryClient();
|
|
16
25
|
const { id: appId } = useApp();
|
|
17
|
-
const [sanitizedObject, setSanitizedObject] = useState();
|
|
18
|
-
const [navigationSlug, setNavigationSlug] = useState();
|
|
19
26
|
const [parameters, setParameters] = useState();
|
|
20
|
-
const [instance, setInstance] = useState();
|
|
21
27
|
const formDataRef = useRef();
|
|
22
28
|
// We only need the setter to force a re-render when form data updates; the value itself
|
|
23
29
|
// is intentionally not referenced elsewhere to avoid stale reads (we use formDataRef).
|
|
@@ -46,6 +52,8 @@ function FormRendererContainer(props) {
|
|
|
46
52
|
});
|
|
47
53
|
const [isSaving, setIsSaving] = useState(false);
|
|
48
54
|
const [lastSavedData, setLastSavedData] = useState({});
|
|
55
|
+
const [uniquePresetValues, setUniquePresetValues] = useState([]);
|
|
56
|
+
const flattenFormEntries = useMemo(() => getUnnestedEntries(form?.entries || []), [form?.entries]);
|
|
49
57
|
const userAccount = useAuthenticationContext()?.account;
|
|
50
58
|
const objectStore = useObject(form?.objectId ?? objectId);
|
|
51
59
|
const onError = (err) => {
|
|
@@ -53,68 +61,84 @@ function FormRendererContainer(props) {
|
|
|
53
61
|
setSnackbarError({ ...snackbarError, isError: true });
|
|
54
62
|
setError(code ?? err);
|
|
55
63
|
};
|
|
64
|
+
const { data: sanitizedObject, error: sanitizedObjectError } = useQuery({
|
|
65
|
+
queryKey: [form?.objectId ?? objectId, ...(instanceId ? [instanceId] : []), 'sanitized'],
|
|
66
|
+
queryFn: () =>
|
|
67
|
+
// form?.objectId is needed for subtype forms to get the correct object
|
|
68
|
+
apiServices.get(getPrefixedUrl(`/objects/${form?.objectId ?? objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } }),
|
|
69
|
+
staleTime: Infinity,
|
|
70
|
+
enabled: !!(form?.objectId || objectId),
|
|
71
|
+
});
|
|
72
|
+
// trigger refetch on success
|
|
73
|
+
const { data: instance, error: instanceError } = useQuery({
|
|
74
|
+
queryKey: [objectId, instanceId, 'instance'],
|
|
75
|
+
queryFn: async () => {
|
|
76
|
+
const instance = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}`), {
|
|
77
|
+
params: {
|
|
78
|
+
expand: uniquePresetValues
|
|
79
|
+
.filter((value) => value.startsWith('{{{input.') ||
|
|
80
|
+
value.startsWith('{{input.') ||
|
|
81
|
+
value.startsWith('{{{instance.'))
|
|
82
|
+
.map((value) => {
|
|
83
|
+
return value
|
|
84
|
+
.replace(/{{{|}}}|{{|}}/g, '')
|
|
85
|
+
.split('.')
|
|
86
|
+
.slice(1)
|
|
87
|
+
.join('.');
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return instance;
|
|
92
|
+
},
|
|
93
|
+
staleTime: Infinity,
|
|
94
|
+
enabled: !!instanceId && !!sanitizedObject,
|
|
95
|
+
});
|
|
56
96
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
if (!sanitizedObject)
|
|
98
|
+
return;
|
|
99
|
+
const allCriterias = extractAllCriteria(flattenFormEntries, parameters || []);
|
|
100
|
+
const uniquePresetValues = new Set();
|
|
101
|
+
for (const criteria of allCriterias) {
|
|
102
|
+
const presetValues = extractPresetValuesFromCriteria(criteria);
|
|
103
|
+
presetValues.forEach((value) => uniquePresetValues.add(value));
|
|
104
|
+
}
|
|
105
|
+
extractPresetValuesFromDynamicDefaultValues(flattenFormEntries).map((value) => uniquePresetValues.add(value));
|
|
106
|
+
setUniquePresetValues(Array.from(uniquePresetValues));
|
|
107
|
+
const action = sanitizedObject.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
108
|
+
if (action && (instanceId || action.type === 'create')) {
|
|
109
|
+
setAction(action);
|
|
110
|
+
// Clear error if action is found after being missing
|
|
111
|
+
setError((prevError) => (prevError === 'Action could not be found' ? undefined : prevError));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
setError('Action could not be found');
|
|
115
|
+
}
|
|
116
|
+
}, [sanitizedObject, actionId, form?.actionId, instanceId]);
|
|
117
|
+
const { data: navigationSlug } = useQuery({
|
|
118
|
+
queryKey: [appId, 'navigationSlug'],
|
|
119
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
|
|
120
|
+
select: (page) => page.slug,
|
|
121
|
+
staleTime: Infinity,
|
|
122
|
+
enabled: !!pageNavigation,
|
|
123
|
+
});
|
|
124
|
+
const formIdToFetch = formId || action?.defaultFormId;
|
|
125
|
+
const { data: fetchedForm, error: fetchedFormError } = useFormById(formIdToFetch ?? '', apiServices);
|
|
82
126
|
useEffect(() => {
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`))
|
|
86
|
-
.then((page) => {
|
|
87
|
-
setNavigationSlug(page?.slug);
|
|
88
|
-
});
|
|
127
|
+
if (!formIdToFetch && action) {
|
|
128
|
+
setError('Action form could not be found');
|
|
89
129
|
}
|
|
90
|
-
}, []);
|
|
130
|
+
}, [formIdToFetch, action]);
|
|
91
131
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
});
|
|
132
|
+
if (fetchedForm) {
|
|
133
|
+
if (actionId && fetchedForm.actionId !== actionId) {
|
|
134
|
+
setError('Configured action ID does not match form action ID');
|
|
135
|
+
}
|
|
136
|
+
setForm(fetchedForm);
|
|
113
137
|
}
|
|
114
|
-
else if (action?.type === 'delete' && formId === '_auto_') {
|
|
138
|
+
else if (action?.type === 'delete' && formId === '_auto_' && instance) {
|
|
115
139
|
setForm({
|
|
116
140
|
id: '',
|
|
117
|
-
name: '',
|
|
141
|
+
name: 'Delete',
|
|
118
142
|
entries: [
|
|
119
143
|
{
|
|
120
144
|
type: 'content',
|
|
@@ -128,10 +152,12 @@ function FormRendererContainer(props) {
|
|
|
128
152
|
},
|
|
129
153
|
});
|
|
130
154
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
}, [fetchedForm, instance, action, formId]);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const error = sanitizedObjectError || fetchedFormError || instanceError;
|
|
158
|
+
if (error)
|
|
159
|
+
onError(error);
|
|
160
|
+
}, [sanitizedObjectError, fetchedFormError, instanceError]);
|
|
135
161
|
useEffect(() => {
|
|
136
162
|
if (!form)
|
|
137
163
|
return;
|
|
@@ -141,15 +167,15 @@ function FormRendererContainer(props) {
|
|
|
141
167
|
}, [form, action?.parameters, sanitizedObject]);
|
|
142
168
|
useEffect(() => {
|
|
143
169
|
const getInitialValues = async () => {
|
|
144
|
-
if (
|
|
145
|
-
const defaultValues = await getDefaultValues(
|
|
170
|
+
if (flattenFormEntries.length && (instance || !instanceId)) {
|
|
171
|
+
const defaultValues = await getDefaultValues(flattenFormEntries, instance || {});
|
|
146
172
|
setFormData(defaultValues);
|
|
147
173
|
// Deep clone to avoid reference issues
|
|
148
174
|
setLastSavedData(cloneDeep(defaultValues));
|
|
149
175
|
}
|
|
150
176
|
};
|
|
151
177
|
getInitialValues();
|
|
152
|
-
}, [
|
|
178
|
+
}, [instanceId, instance, flattenFormEntries]);
|
|
153
179
|
const onSubmissionSuccess = (updatedInstance) => {
|
|
154
180
|
setSnackbarError({
|
|
155
181
|
showAlert: true,
|
|
@@ -158,14 +184,19 @@ function FormRendererContainer(props) {
|
|
|
158
184
|
});
|
|
159
185
|
if (navigationSlug) {
|
|
160
186
|
if (navigationSlug.includes(':instanceId')) {
|
|
161
|
-
const navigateInstanceId = action?.type === 'create' ? updatedInstance
|
|
187
|
+
const navigateInstanceId = action?.type === 'create' ? updatedInstance.id : instanceId;
|
|
162
188
|
navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', navigateInstanceId ?? ':instanceId')}`);
|
|
163
189
|
}
|
|
164
190
|
else {
|
|
165
191
|
navigateTo(`/${appId}/${navigationSlug}`);
|
|
166
192
|
}
|
|
167
193
|
}
|
|
168
|
-
|
|
194
|
+
if (instanceId) {
|
|
195
|
+
// Invalidate the instance to fetch the latest version
|
|
196
|
+
queryClient.invalidateQueries({
|
|
197
|
+
queryKey: [objectId, instanceId, 'instance'],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
169
200
|
};
|
|
170
201
|
const linkFiles = async (submission, linkTo) => {
|
|
171
202
|
// Create file links for any uploaded files after instance creation
|
|
@@ -227,9 +258,8 @@ function FormRendererContainer(props) {
|
|
|
227
258
|
throw error; // Throw error so caller knows submission failed
|
|
228
259
|
}
|
|
229
260
|
};
|
|
230
|
-
const getDefaultValues = async (
|
|
261
|
+
const getDefaultValues = async (unnestedEntries, instanceData) => {
|
|
231
262
|
const result = {};
|
|
232
|
-
const unnestedEntries = getUnnestedEntries(entries);
|
|
233
263
|
for (const entry of unnestedEntries) {
|
|
234
264
|
const fieldId = getEntryId(entry);
|
|
235
265
|
if (!fieldId)
|
|
@@ -265,34 +295,36 @@ function FormRendererContainer(props) {
|
|
|
265
295
|
console.error(error);
|
|
266
296
|
}
|
|
267
297
|
}
|
|
268
|
-
else if (entry.type !== 'readonlyField'
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
298
|
+
else if (entry.type !== 'readonlyField') {
|
|
299
|
+
if (isEmptyWithDefault(fieldValue, entry, instanceData)) {
|
|
300
|
+
if (fieldId && parameters && parameters.length > 0) {
|
|
301
|
+
const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
|
|
302
|
+
for (const { fieldId, fieldValue } of defaultValuesArray) {
|
|
303
|
+
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
304
|
+
if (parameter?.type === 'object') {
|
|
305
|
+
const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
|
|
306
|
+
for (const field of dependentFields) {
|
|
307
|
+
set(result, field.fieldId, field.fieldValue);
|
|
308
|
+
}
|
|
277
309
|
}
|
|
310
|
+
set(result, fieldId, fieldValue);
|
|
278
311
|
}
|
|
279
|
-
set(result, fieldId, fieldValue);
|
|
280
312
|
}
|
|
281
313
|
}
|
|
282
|
-
|
|
283
|
-
|
|
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;
|
|
314
|
+
else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
|
|
315
|
+
result[fieldId] = false;
|
|
293
316
|
}
|
|
294
|
-
else {
|
|
295
|
-
|
|
317
|
+
else if (fieldValue !== undefined && fieldValue !== null) {
|
|
318
|
+
if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
|
|
319
|
+
let RTFFieldValue = fieldValue;
|
|
320
|
+
if (!fieldValue.trim().startsWith('{\\rtf')) {
|
|
321
|
+
RTFFieldValue = plainTextToRtf(fieldValue);
|
|
322
|
+
}
|
|
323
|
+
result[fieldId] = RTFFieldValue;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
result[fieldId] = fieldValue;
|
|
327
|
+
}
|
|
296
328
|
}
|
|
297
329
|
}
|
|
298
330
|
}
|
|
@@ -349,15 +381,14 @@ function FormRendererContainer(props) {
|
|
|
349
381
|
const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
|
|
350
382
|
async function onChange(id, value) {
|
|
351
383
|
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));
|
|
384
|
+
const isReadOnlyField = flattenFormEntries.some((e) => e.type === 'readonlyField' && e.propertyId === id) &&
|
|
385
|
+
!flattenFormEntries.some((e) => (e.type === 'input' && e.parameterId === id) || (e.type === 'inputField' && e.input.id === id));
|
|
355
386
|
if (isReadOnlyField)
|
|
356
387
|
return;
|
|
357
388
|
if (parameter) {
|
|
358
389
|
if (parameter.type === 'object' && parameters && parameters.length > 0) {
|
|
359
390
|
// On change of a related object, update default values dependent on that object
|
|
360
|
-
const dependentFields = await processValueUpdate(
|
|
391
|
+
const dependentFields = await processValueUpdate(flattenFormEntries, parameters, value, apiServices, id, formDataRef.current, userAccount);
|
|
361
392
|
for (const field of dependentFields) {
|
|
362
393
|
onChange(field.fieldId, field.fieldValue);
|
|
363
394
|
}
|
|
@@ -387,7 +418,7 @@ function FormRendererContainer(props) {
|
|
|
387
418
|
? onDiscardChangesOverride
|
|
388
419
|
: async () => {
|
|
389
420
|
if (form) {
|
|
390
|
-
const defaultValues = await getDefaultValues(
|
|
421
|
+
const defaultValues = await getDefaultValues(flattenFormEntries, instance || {});
|
|
391
422
|
setFormData(defaultValues);
|
|
392
423
|
}
|
|
393
424
|
};
|
|
@@ -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,24 +1,21 @@
|
|
|
1
1
|
import { useApiServices, useNotification, } from '@evoke-platform/context';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { debounce, isEmpty } from 'lodash';
|
|
4
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
5
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
5
6
|
import { Skeleton } from '../../../../../core';
|
|
6
7
|
import { retrieveCustomErrorMessage } from '../../../../Form/utils';
|
|
7
8
|
import { getMiddleObject, getMiddleObjectFilter, getPrefixedUrl, transformToWhere } from '../../utils';
|
|
8
9
|
import { DropdownRepeatableFieldInput } from './DropdownRepeatableFieldInput';
|
|
9
10
|
const DropdownRepeatableField = (props) => {
|
|
10
|
-
const { id, fieldDefinition, criteria, readOnly,
|
|
11
|
-
const {
|
|
11
|
+
const { id, fieldDefinition, criteria, readOnly, middleObject, hasDescription, viewLayout, initialMiddleObjectInstances, } = props;
|
|
12
|
+
const { instance } = useFormContext();
|
|
12
13
|
const [layout, setLayout] = useState();
|
|
13
|
-
const [loading, setLoading] = useState(false);
|
|
14
14
|
const [layoutLoaded, setLayoutLoaded] = useState(false);
|
|
15
15
|
const [searchValue, setSearchValue] = useState('');
|
|
16
16
|
const [middleObjectInstances, setMiddleObjectInstances] = useState(initialMiddleObjectInstances);
|
|
17
|
-
const [
|
|
18
|
-
const [endObjectInstances, setEndObjectInstances] = useState(fetchedOptions[`${fieldDefinition.id}EndObjectInstances`] || []);
|
|
19
|
-
const [initialLoading, setInitialLoading] = useState(endObjectInstances ? false : true);
|
|
17
|
+
const [debouncedSearchValue, setDebouncedSearchValue] = useState();
|
|
20
18
|
const [selectedOptions, setSelectedOptions] = useState([]);
|
|
21
|
-
const [hasFetched, setHasFetched] = useState(!!fetchedOptions[`${fieldDefinition.id}EndObjectInstancesHaveFetched`] || false);
|
|
22
19
|
const [snackbarError, setSnackbarError] = useState({
|
|
23
20
|
showAlert: false,
|
|
24
21
|
isError: true,
|
|
@@ -41,6 +38,12 @@ const DropdownRepeatableField = (props) => {
|
|
|
41
38
|
const newInstances = await getMiddleObjectInstances();
|
|
42
39
|
setMiddleObjectInstances(newInstances);
|
|
43
40
|
};
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
instanceChanges?.subscribe(middleObject.rootObjectId, () => {
|
|
43
|
+
fetchMiddleObjectInstances();
|
|
44
|
+
});
|
|
45
|
+
return () => instanceChanges?.unsubscribe(middleObject.rootObjectId);
|
|
46
|
+
}, [instanceChanges, fetchMiddleObjectInstances, middleObject]);
|
|
44
47
|
const setDropDownSelections = (instances) => {
|
|
45
48
|
setSelectedOptions(instances
|
|
46
49
|
.filter((currInstance) => fieldDefinition.manyToManyPropertyId in currInstance)
|
|
@@ -52,146 +55,86 @@ const DropdownRepeatableField = (props) => {
|
|
|
52
55
|
.sort((instanceA, instanceB) => instanceA.label.localeCompare(instanceB.label)));
|
|
53
56
|
};
|
|
54
57
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
setEndObject(endObject);
|
|
67
|
-
let defaultLayout;
|
|
68
|
-
if (endObject.viewLayout?.dropdown) {
|
|
69
|
-
defaultLayout = {
|
|
70
|
-
id: 'default',
|
|
71
|
-
name: 'Default',
|
|
72
|
-
objectId: endObject.id,
|
|
73
|
-
...endObject.viewLayout.dropdown,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (viewLayout) {
|
|
77
|
-
apiServices
|
|
78
|
-
.get(getPrefixedUrl(`/objects/${viewLayout.objectId}/dropdownLayouts/${viewLayout.id}`))
|
|
79
|
-
.then(setLayout)
|
|
80
|
-
.catch(() => setLayout(defaultLayout))
|
|
81
|
-
.finally(() => setLayoutLoaded(true));
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
setLayout(defaultLayout);
|
|
85
|
-
setLayoutLoaded(true);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}, [middleObject, viewLayout]);
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
instanceChanges?.subscribe(middleObject.rootObjectId, () => {
|
|
93
|
-
fetchMiddleObjectInstances();
|
|
94
|
-
});
|
|
95
|
-
return () => instanceChanges?.unsubscribe(middleObject.rootObjectId);
|
|
96
|
-
}, [instanceChanges, fetchMiddleObjectInstances]);
|
|
97
|
-
const fetchEndObjectInstances = useCallback((searchedName) => {
|
|
98
|
-
if ((fieldDefinition.objectId &&
|
|
99
|
-
fieldDefinition.manyToManyPropertyId &&
|
|
100
|
-
endObjectInstances.length === 0 &&
|
|
101
|
-
!hasFetched) ||
|
|
102
|
-
(searchedName !== undefined && searchedName !== '')) {
|
|
103
|
-
setLoading(true);
|
|
104
|
-
const endObjectProperty = middleObject.properties?.find((currProperty) => fieldDefinition.manyToManyPropertyId === currProperty.id);
|
|
105
|
-
if (endObjectProperty?.objectId) {
|
|
106
|
-
const { propertyId, direction } = layout?.sort ?? {
|
|
107
|
-
propertyId: 'name',
|
|
108
|
-
direction: 'asc',
|
|
109
|
-
};
|
|
110
|
-
const filter = {
|
|
111
|
-
limit: 100,
|
|
112
|
-
order: `${propertyId} ${direction}`,
|
|
113
|
-
};
|
|
114
|
-
let searchCriteria = criteria && !isEmpty(criteria) ? transformToWhere(criteria) : {};
|
|
115
|
-
if (searchedName?.length) {
|
|
116
|
-
const nameCriteria = transformToWhere({
|
|
117
|
-
name: {
|
|
118
|
-
like: searchedName,
|
|
119
|
-
options: 'i',
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
searchCriteria = !isEmpty(criteria)
|
|
123
|
-
? {
|
|
124
|
-
and: [searchCriteria, nameCriteria],
|
|
125
|
-
}
|
|
126
|
-
: nameCriteria;
|
|
127
|
-
}
|
|
128
|
-
filter.where = searchCriteria;
|
|
129
|
-
apiServices.get(getPrefixedUrl(`/objects/${endObjectProperty.objectId}/instances`), { params: { filter: JSON.stringify(filter) } }, (error, instances) => {
|
|
130
|
-
if (!error && instances) {
|
|
131
|
-
setEndObjectInstances(instances);
|
|
132
|
-
setHasFetched(true);
|
|
133
|
-
}
|
|
134
|
-
setInitialLoading(false);
|
|
135
|
-
setLoading(false);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
else if (endObjectInstances.length !== 0) {
|
|
140
|
-
setInitialLoading(false);
|
|
141
|
-
}
|
|
142
|
-
}, [fieldDefinition.objectId, fieldDefinition.manyToManyPropertyId, middleObject]);
|
|
143
|
-
const debouncedEndObjectSearch = useCallback(debounce(fetchEndObjectInstances, 500), [fetchEndObjectInstances]);
|
|
58
|
+
setDropDownSelections(middleObjectInstances);
|
|
59
|
+
}, [middleObjectInstances]);
|
|
60
|
+
const endObjectProperty = useMemo(() => middleObject.properties?.find((currProperty) => fieldDefinition.manyToManyPropertyId === currProperty.id), [middleObject.properties, fieldDefinition.manyToManyPropertyId]);
|
|
61
|
+
const { data: endObject } = useQuery({
|
|
62
|
+
queryKey: [endObjectProperty?.objectId, 'endObject', 'effective'],
|
|
63
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${endObjectProperty?.objectId}/effective`), {
|
|
64
|
+
params: { filter: { fields: ['id', 'name', 'properties', 'viewLayout'] } },
|
|
65
|
+
}),
|
|
66
|
+
staleTime: Infinity,
|
|
67
|
+
enabled: !!endObjectProperty?.objectId,
|
|
68
|
+
});
|
|
144
69
|
useEffect(() => {
|
|
145
|
-
if (!
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
70
|
+
if (!endObject)
|
|
71
|
+
return;
|
|
72
|
+
let defaultLayout;
|
|
73
|
+
if (endObject.viewLayout?.dropdown) {
|
|
74
|
+
defaultLayout = {
|
|
75
|
+
id: 'default',
|
|
76
|
+
name: 'Default',
|
|
77
|
+
objectId: endObject.id,
|
|
78
|
+
...endObject.viewLayout.dropdown,
|
|
79
|
+
};
|
|
152
80
|
}
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
81
|
+
if (viewLayout) {
|
|
82
|
+
apiServices
|
|
83
|
+
.get(getPrefixedUrl(`/objects/${viewLayout.objectId}/dropdownLayouts/${viewLayout.id}`))
|
|
84
|
+
.then(setLayout)
|
|
85
|
+
.catch(() => setLayout(defaultLayout))
|
|
86
|
+
.finally(() => setLayoutLoaded(true));
|
|
157
87
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
88
|
+
else {
|
|
89
|
+
setLayout(defaultLayout);
|
|
90
|
+
setLayoutLoaded(true);
|
|
162
91
|
}
|
|
163
|
-
}, [
|
|
92
|
+
}, [endObject, viewLayout]);
|
|
93
|
+
const debouncedSetSearchValue = useMemo(() => debounce((value) => {
|
|
94
|
+
setDebouncedSearchValue(value);
|
|
95
|
+
}, 200), []);
|
|
164
96
|
useEffect(() => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
97
|
+
debouncedSetSearchValue(searchValue);
|
|
98
|
+
return () => {
|
|
99
|
+
debouncedSetSearchValue.cancel();
|
|
169
100
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
fieldDefinition.id,
|
|
182
|
-
hasFetched,
|
|
183
|
-
initialMiddleObjectInstances,
|
|
184
|
-
setFetchedOptions,
|
|
185
|
-
]);
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
debouncedEndObjectSearch(searchValue);
|
|
188
|
-
return () => debouncedEndObjectSearch.cancel();
|
|
189
|
-
}, [searchValue, debouncedEndObjectSearch]);
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
if (layoutLoaded) {
|
|
192
|
-
fetchEndObjectInstances();
|
|
101
|
+
}, [searchValue, debouncedSetSearchValue, layoutLoaded]);
|
|
102
|
+
const searchCriteria = useMemo(() => {
|
|
103
|
+
let searchCriteria = criteria && !isEmpty(criteria) ? transformToWhere(criteria) : {};
|
|
104
|
+
if (debouncedSearchValue?.length) {
|
|
105
|
+
const nameCriteria = transformToWhere({
|
|
106
|
+
name: {
|
|
107
|
+
like: debouncedSearchValue,
|
|
108
|
+
options: 'i',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
searchCriteria = !isEmpty(criteria) ? { and: [searchCriteria, nameCriteria] } : nameCriteria;
|
|
193
112
|
}
|
|
194
|
-
|
|
113
|
+
return searchCriteria;
|
|
114
|
+
}, [criteria, debouncedSearchValue]);
|
|
115
|
+
// Construct filter from debounced criteria
|
|
116
|
+
const updatedFilter = useMemo(() => {
|
|
117
|
+
const { propertyId, direction } = layout?.sort ?? {
|
|
118
|
+
propertyId: 'name',
|
|
119
|
+
direction: 'asc',
|
|
120
|
+
};
|
|
121
|
+
const filter = {
|
|
122
|
+
limit: 100,
|
|
123
|
+
order: `${propertyId} ${direction}`,
|
|
124
|
+
where: searchCriteria,
|
|
125
|
+
};
|
|
126
|
+
return filter;
|
|
127
|
+
}, [searchCriteria, layout]);
|
|
128
|
+
const { data: endObjectInstances = [], isLoading: initialLoading } = useQuery({
|
|
129
|
+
queryKey: ['endObjectInstances', endObjectProperty.objectId, updatedFilter],
|
|
130
|
+
queryFn: async () => {
|
|
131
|
+
return apiServices.get(getPrefixedUrl(`/objects/${endObjectProperty.objectId}/instances`), { params: { filter: JSON.stringify(updatedFilter) } });
|
|
132
|
+
},
|
|
133
|
+
enabled: !!fieldDefinition.objectId && !!fieldDefinition.manyToManyPropertyId && !!endObjectProperty?.objectId,
|
|
134
|
+
staleTime: Infinity,
|
|
135
|
+
// Keep old instances while filtering
|
|
136
|
+
placeholderData: (previousData) => previousData,
|
|
137
|
+
});
|
|
195
138
|
const saveMiddleInstance = async (endObjectId, endObjectName) => {
|
|
196
139
|
if (fieldDefinition.objectId) {
|
|
197
140
|
const middleObject = getMiddleObject(fieldDefinition, endObjectId, endObjectName, instance);
|
|
@@ -228,6 +171,6 @@ const DropdownRepeatableField = (props) => {
|
|
|
228
171
|
});
|
|
229
172
|
}
|
|
230
173
|
};
|
|
231
|
-
return initialLoading || !middleObject || !middleObjectInstances || !endObjectInstances || !endObject ? (React.createElement(Skeleton, null)) : (React.createElement(React.Fragment, null, middleObjectInstances && endObject && (React.createElement(DropdownRepeatableFieldInput, { id: id, fieldDefinition: fieldDefinition, readOnly: readOnly || !middleObject.actions?.some((action) => action.id === '_create'), layout: layout,
|
|
174
|
+
return initialLoading || !middleObject || !middleObjectInstances || !endObjectInstances || !endObject ? (React.createElement(Skeleton, null)) : (React.createElement(React.Fragment, null, middleObjectInstances && endObject && (React.createElement(DropdownRepeatableFieldInput, { id: id, fieldDefinition: fieldDefinition, readOnly: readOnly || !middleObject.actions?.some((action) => action.id === '_create'), layout: layout, endObjectInstances: endObjectInstances ?? [], endObject: endObject, searchValue: searchValue, loading: initialLoading, handleSaveMiddleInstance: saveMiddleInstance, handleRemoveMiddleInstance: removeMiddleInstance, setSearchValue: setSearchValue, setSnackbarError: setSnackbarError, snackbarError: snackbarError, selectedOptions: selectedOptions, setSelectedOptions: setSelectedOptions, hasDescription: hasDescription }))));
|
|
232
175
|
};
|
|
233
176
|
export default DropdownRepeatableField;
|