@evoke-platform/ui-components 1.13.0-dev.7 → 1.14.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.d.ts +4 -4
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +72 -145
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
- package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
- package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
- package/dist/published/components/custom/Form/utils.d.ts +0 -1
- package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +1 -2
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -2
- package/dist/published/components/custom/FormV2/FormRenderer.js +29 -26
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +3 -1
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +88 -95
- package/dist/published/components/custom/FormV2/components/Body.js +1 -1
- package/dist/published/components/custom/FormV2/components/Footer.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +143 -86
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +4 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +186 -106
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +49 -36
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +32 -51
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +38 -40
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +21 -17
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +169 -95
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +6 -12
- package/dist/published/components/custom/FormV2/components/FormSections.js +0 -1
- package/dist/published/components/custom/FormV2/components/Header.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Header.js +19 -8
- package/dist/published/components/custom/FormV2/components/HtmlView.d.ts +9 -0
- package/dist/published/components/custom/FormV2/components/HtmlView.js +46 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +1 -2
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +20 -46
- package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
- package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -11
- package/dist/published/components/custom/FormV2/components/utils.js +104 -181
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +17 -50
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +131 -40
- package/dist/published/components/custom/HistoryLog/HistoryData.js +1 -2
- package/dist/published/components/custom/HistoryLog/index.js +1 -2
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +1 -2
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +22 -61
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +5 -8
- package/dist/published/stories/Backdrop.stories.d.ts +2 -2
- package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
- package/dist/published/stories/FormLabel.stories.d.ts +2 -2
- package/dist/published/stories/FormRenderer.stories.d.ts +3 -3
- package/dist/published/stories/FormRendererContainer.stories.d.ts +15 -5
- package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +9 -0
- package/dist/published/theme/hooks.d.ts +1 -2
- package/package.json +11 -17
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +0 -5
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +0 -21
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
|
|
2
|
-
import { useQuery } from '@tanstack/react-query';
|
|
3
2
|
import axios from 'axios';
|
|
4
3
|
import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set } from 'lodash';
|
|
5
4
|
import React, { useEffect, useRef, useState } from 'react';
|
|
6
5
|
import { Skeleton, Snackbar } from '../../core';
|
|
7
6
|
import { Box } from '../../layout';
|
|
8
7
|
import ErrorComponent from '../ErrorComponent';
|
|
9
|
-
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
10
8
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
11
9
|
import Header from './components/Header';
|
|
12
|
-
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf,
|
|
10
|
+
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
|
|
13
11
|
import FormRenderer from './FormRenderer';
|
|
14
|
-
// Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
|
|
15
12
|
function FormRendererContainer(props) {
|
|
16
|
-
|
|
17
|
-
React.createElement(FormRendererContainerInner, { ...props })));
|
|
18
|
-
}
|
|
19
|
-
// Inner component that assumes QueryClient context is available
|
|
20
|
-
function FormRendererContainerInner(props) {
|
|
21
|
-
const { instanceId, pageNavigation, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
|
|
13
|
+
const { instanceId, pageNavigation, dataType, title, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
|
|
22
14
|
const apiServices = useApiServices();
|
|
23
15
|
const navigateTo = useNavigate();
|
|
24
16
|
const { id: appId } = useApp();
|
|
17
|
+
const [sanitizedObject, setSanitizedObject] = useState();
|
|
18
|
+
const [navigationSlug, setNavigationSlug] = useState();
|
|
25
19
|
const [parameters, setParameters] = useState();
|
|
26
20
|
const [instance, setInstance] = useState();
|
|
27
21
|
const formDataRef = useRef();
|
|
@@ -59,68 +53,72 @@ function FormRendererContainerInner(props) {
|
|
|
59
53
|
setSnackbarError({ ...snackbarError, isError: true });
|
|
60
54
|
setError(code ?? err);
|
|
61
55
|
};
|
|
62
|
-
const { data: sanitizedObject, error: sanitizedObjectError } = useQuery({
|
|
63
|
-
queryKey: [form?.objectId ?? objectId, ...(instanceId ? [instanceId] : []), 'sanitized'],
|
|
64
|
-
queryFn: () =>
|
|
65
|
-
// form?.objectId is needed for subtype forms to get the correct object
|
|
66
|
-
apiServices.get(getPrefixedUrl(`/objects/${form?.objectId ?? objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } }),
|
|
67
|
-
staleTime: Infinity,
|
|
68
|
-
enabled: !!(form?.objectId || objectId),
|
|
69
|
-
});
|
|
70
|
-
const { data: fetchedInstance, error: instanceError } = useQuery({
|
|
71
|
-
queryKey: [objectId, instanceId, 'instance'],
|
|
72
|
-
queryFn: () => objectStore.getInstance(instanceId),
|
|
73
|
-
staleTime: Infinity,
|
|
74
|
-
enabled: !!instanceId && !!sanitizedObject,
|
|
75
|
-
});
|
|
76
56
|
useEffect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
const formIdToFetch = formId || action?.defaultFormId;
|
|
103
|
-
const { data: fetchedForm, error: fetchedFormError } = useFormById(formIdToFetch ?? '', apiServices);
|
|
57
|
+
(async () => {
|
|
58
|
+
try {
|
|
59
|
+
if (instanceId) {
|
|
60
|
+
const instance = await objectStore.getInstance(instanceId);
|
|
61
|
+
setInstance(instance);
|
|
62
|
+
}
|
|
63
|
+
const object = await apiServices.get(getPrefixedUrl(`/objects/${form?.objectId || objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } });
|
|
64
|
+
setSanitizedObject(object);
|
|
65
|
+
const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
66
|
+
if (action && (instanceId || action.type === 'create')) {
|
|
67
|
+
setAction(action);
|
|
68
|
+
// Clear error if action is found after being missing
|
|
69
|
+
// TODO: This entire effect should take place after form is fetched to avoid an error flickering
|
|
70
|
+
// That is, this effect should be merged with the one below that fetches the form
|
|
71
|
+
setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
setError('Action could not be found');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
onError(error);
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
}, [dataType, form, instanceId]);
|
|
104
82
|
useEffect(() => {
|
|
105
|
-
if (
|
|
106
|
-
|
|
83
|
+
if (pageNavigation) {
|
|
84
|
+
apiServices
|
|
85
|
+
.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`))
|
|
86
|
+
.then((page) => {
|
|
87
|
+
setNavigationSlug(page?.slug);
|
|
88
|
+
});
|
|
107
89
|
}
|
|
108
|
-
}, [
|
|
90
|
+
}, []);
|
|
109
91
|
useEffect(() => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
const needsInstance = action?.type !== 'create' && !!instanceId;
|
|
93
|
+
// Instance and Action are loaded in the side effect above; wait for them to complete.
|
|
94
|
+
const loading = (actionId && !action) || (needsInstance && !instance);
|
|
95
|
+
if (form || loading)
|
|
96
|
+
return;
|
|
97
|
+
if ((formId || action?.defaultFormId) && formId !== '_auto_') {
|
|
98
|
+
apiServices
|
|
99
|
+
.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
|
|
100
|
+
.then((evokeForm) => {
|
|
101
|
+
// If an actionId is provided, ensure it matches the form's actionId
|
|
102
|
+
if (!actionId || evokeForm?.actionId === actionId) {
|
|
103
|
+
const form = evokeForm;
|
|
104
|
+
setForm(form);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
setError('Configured action ID does not match form action ID');
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
onError(error);
|
|
112
|
+
});
|
|
115
113
|
}
|
|
116
|
-
else if (action?.type === 'delete' && formId === '_auto_'
|
|
114
|
+
else if (action?.type === 'delete' && formId === '_auto_') {
|
|
117
115
|
setForm({
|
|
118
116
|
id: '',
|
|
119
|
-
name: '
|
|
117
|
+
name: '',
|
|
120
118
|
entries: [
|
|
121
119
|
{
|
|
122
120
|
type: 'content',
|
|
123
|
-
html: `<p style="
|
|
121
|
+
html: `<p><span style="font-size: 16px;">You are about to delete <strong>${instance?.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</span></p>`,
|
|
124
122
|
},
|
|
125
123
|
],
|
|
126
124
|
objectId: objectId,
|
|
@@ -130,12 +128,10 @@ function FormRendererContainerInner(props) {
|
|
|
130
128
|
},
|
|
131
129
|
});
|
|
132
130
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
onError(error);
|
|
138
|
-
}, [sanitizedObjectError, fetchedFormError]);
|
|
131
|
+
else {
|
|
132
|
+
setError('Action form could not be found');
|
|
133
|
+
}
|
|
134
|
+
}, [action, actionId, objectId, instance]);
|
|
139
135
|
useEffect(() => {
|
|
140
136
|
if (!form)
|
|
141
137
|
return;
|
|
@@ -145,7 +141,7 @@ function FormRendererContainerInner(props) {
|
|
|
145
141
|
}, [form, action?.parameters, sanitizedObject]);
|
|
146
142
|
useEffect(() => {
|
|
147
143
|
const getInitialValues = async () => {
|
|
148
|
-
if (form &&
|
|
144
|
+
if (form && (instance || !instanceId)) {
|
|
149
145
|
const defaultValues = await getDefaultValues(form.entries, instance || {});
|
|
150
146
|
setFormData(defaultValues);
|
|
151
147
|
// Deep clone to avoid reference issues
|
|
@@ -153,7 +149,7 @@ function FormRendererContainerInner(props) {
|
|
|
153
149
|
}
|
|
154
150
|
};
|
|
155
151
|
getInitialValues();
|
|
156
|
-
}, [form, instance, sanitizedObject
|
|
152
|
+
}, [form, instance, sanitizedObject]);
|
|
157
153
|
const onSubmissionSuccess = (updatedInstance) => {
|
|
158
154
|
setSnackbarError({
|
|
159
155
|
showAlert: true,
|
|
@@ -162,7 +158,7 @@ function FormRendererContainerInner(props) {
|
|
|
162
158
|
});
|
|
163
159
|
if (navigationSlug) {
|
|
164
160
|
if (navigationSlug.includes(':instanceId')) {
|
|
165
|
-
const navigateInstanceId = action?.type === 'create' ? updatedInstance
|
|
161
|
+
const navigateInstanceId = action?.type === 'create' ? updatedInstance?.id : instanceId;
|
|
166
162
|
navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', navigateInstanceId ?? ':instanceId')}`);
|
|
167
163
|
}
|
|
168
164
|
else {
|
|
@@ -171,17 +167,18 @@ function FormRendererContainerInner(props) {
|
|
|
171
167
|
}
|
|
172
168
|
setInstance(updatedInstance);
|
|
173
169
|
};
|
|
174
|
-
/**
|
|
175
|
-
* Manually links any newly uploaded files in the submission to the specified instance.
|
|
176
|
-
* @param submission The form submission data
|
|
177
|
-
* @param linkTo The instance to link the files to
|
|
178
|
-
*/
|
|
179
170
|
const linkFiles = async (submission, linkTo) => {
|
|
180
|
-
// Create file links for any uploaded files
|
|
171
|
+
// Create file links for any uploaded files after instance creation
|
|
181
172
|
for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
|
|
182
173
|
const files = submission[property.id];
|
|
183
174
|
if (files?.length) {
|
|
184
|
-
|
|
175
|
+
try {
|
|
176
|
+
await createFileLinks(files, linkTo, apiServices);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error('Failed to create file links:', error);
|
|
180
|
+
// Don't fail the entire submission if file linking fails
|
|
181
|
+
}
|
|
185
182
|
}
|
|
186
183
|
}
|
|
187
184
|
};
|
|
@@ -198,9 +195,11 @@ function FormRendererContainerInner(props) {
|
|
|
198
195
|
?.filter((property) => property.formula || property.type === 'collection')
|
|
199
196
|
.map((property) => property.id) ?? []),
|
|
200
197
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
if (response) {
|
|
199
|
+
// Manually link files to created instance.
|
|
200
|
+
await linkFiles(submission, { id: response.id, objectId: form.objectId });
|
|
201
|
+
onSubmissionSuccess(response);
|
|
202
|
+
}
|
|
204
203
|
}
|
|
205
204
|
else if (instanceId && action) {
|
|
206
205
|
const response = await objectStore.instanceAction(instanceId, {
|
|
@@ -210,26 +209,21 @@ function FormRendererContainerInner(props) {
|
|
|
210
209
|
.map((property) => property.id) ?? []),
|
|
211
210
|
});
|
|
212
211
|
if (sanitizedObject && instance) {
|
|
213
|
-
if (!onAutosave) {
|
|
214
|
-
// For non-autosave updates, link any uploaded files to the instance.
|
|
215
|
-
await linkFiles(submission, { id: instanceId, objectId: objectId });
|
|
216
|
-
}
|
|
217
212
|
onSubmissionSuccess(response);
|
|
218
|
-
|
|
219
|
-
await deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
213
|
+
deleteDocuments(submission, true, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
220
214
|
}
|
|
221
215
|
}
|
|
222
216
|
}
|
|
223
217
|
catch (error) {
|
|
218
|
+
// Handle deleteDocuments for uploaded documents if the main submission fails
|
|
219
|
+
if (instanceId && action && sanitizedObject && instance) {
|
|
220
|
+
deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
221
|
+
}
|
|
224
222
|
setSnackbarError({
|
|
225
223
|
isError: true,
|
|
226
224
|
showAlert: true,
|
|
227
225
|
message: error.response?.data?.error?.message ?? 'An error occurred',
|
|
228
226
|
});
|
|
229
|
-
if (instanceId && action && sanitizedObject && instance) {
|
|
230
|
-
// For an update, uploaded documents have been linked to the instance and need to be deleted.
|
|
231
|
-
await deleteDocuments(submission, false, apiServices, sanitizedObject, instance, action, setSnackbarError);
|
|
232
|
-
}
|
|
233
227
|
throw error; // Throw error so caller knows submission failed
|
|
234
228
|
}
|
|
235
229
|
};
|
|
@@ -261,10 +255,10 @@ function FormRendererContainerInner(props) {
|
|
|
261
255
|
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
262
256
|
if (associatedObject?.propertyId === fieldId &&
|
|
263
257
|
associatedObject?.instanceId &&
|
|
264
|
-
|
|
258
|
+
parameter &&
|
|
265
259
|
action?.type === 'create') {
|
|
266
260
|
try {
|
|
267
|
-
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter
|
|
261
|
+
const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
|
|
268
262
|
result[associatedObject.propertyId] = instance;
|
|
269
263
|
}
|
|
270
264
|
catch (error) {
|
|
@@ -342,7 +336,6 @@ function FormRendererContainerInner(props) {
|
|
|
342
336
|
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
343
337
|
.map((property) => property.id) ?? []),
|
|
344
338
|
});
|
|
345
|
-
await linkFiles(submission, { id: instanceId, objectId });
|
|
346
339
|
}
|
|
347
340
|
setLastSavedData(cloneDeep(formDataRef.current));
|
|
348
341
|
setIsSaving(false);
|
|
@@ -405,7 +398,7 @@ function FormRendererContainerInner(props) {
|
|
|
405
398
|
border: !isLoading ? '1px solid #dbe0e4' : undefined,
|
|
406
399
|
...sx,
|
|
407
400
|
} }, !isLoading ? (React.createElement(React.Fragment, null,
|
|
408
|
-
React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: instance, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: renderFooter }))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
401
|
+
React.createElement(FormRenderer, { onSubmit: onSubmit ? (data) => onSubmit(data, saveHandler) : saveHandler, onSubmitError: onSubmitError, onDiscardChanges: onDiscardChanges, richTextEditor: richTextEditor, hideTitle: title?.hidden, fieldHeight: display?.fieldHeight ?? 'medium', value: formDataRef.current, form: form, instance: instance, onChange: onChange, onAutosave: onAutosave, associatedObject: associatedObject, renderHeader: composedRenderHeader, renderBody: renderBody, renderFooter: renderFooter }))) : (React.createElement(Box, { sx: { padding: '20px' } },
|
|
409
402
|
React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
|
|
410
403
|
React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
|
|
411
404
|
React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
|
|
@@ -22,6 +22,6 @@ export const Body = (props) => {
|
|
|
22
22
|
React.createElement(Skeleton, { width: '32%', sx: { borderRadius: '8px', height: '40px' } })),
|
|
23
23
|
React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
|
|
24
24
|
React.createElement(Skeleton, { width: '49%', sx: { borderRadius: '8px', height: '40px' } }),
|
|
25
|
-
React.createElement(Skeleton, { width: '49%', sx: { borderRadius: '8px', height: '40px' } })))) : (React.createElement(Box, { sx: { paddingX: isSm || isXs ? 2 : 3, ...sx } }, entries.map((entry, index) => (React.createElement(RecursiveEntryRenderer, { key: index, entry: entry })))))));
|
|
25
|
+
React.createElement(Skeleton, { width: '49%', sx: { borderRadius: '8px', height: '40px' } })))) : (React.createElement(Box, { sx: { paddingX: isSm || isXs ? 2 : 3, paddingY: isSm || isXs ? '6px' : '14px', ...sx } }, entries.map((entry, index) => (React.createElement(RecursiveEntryRenderer, { key: index, entry: entry })))))));
|
|
26
26
|
};
|
|
27
27
|
export default Body;
|
|
@@ -20,7 +20,7 @@ export const Footer = (props) => {
|
|
|
20
20
|
padding: isSmallerThanMd ? '16px' : '20px',
|
|
21
21
|
justifyContent: isXs ? 'center' : 'flex-end',
|
|
22
22
|
alignItems: 'center',
|
|
23
|
-
borderTop: '1px solid #
|
|
23
|
+
borderTop: '1px solid #e9ecef',
|
|
24
24
|
borderRadius: '0px 0px 6px 6px',
|
|
25
25
|
zIndex: 3,
|
|
26
26
|
width: '100%',
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { useApiServices, useNotification, } from '@evoke-platform/context';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { debounce, isArray, isEmpty, isEqual } from 'lodash';
|
|
3
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
4
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
6
5
|
import { Skeleton } from '../../../../../core';
|
|
7
6
|
import { retrieveCustomErrorMessage } from '../../../../Form/utils';
|
|
8
7
|
import { getMiddleObject, getMiddleObjectFilter, getPrefixedUrl, transformToWhere } from '../../utils';
|
|
9
8
|
import { DropdownRepeatableFieldInput } from './DropdownRepeatableFieldInput';
|
|
10
9
|
const DropdownRepeatableField = (props) => {
|
|
11
|
-
const { id, fieldDefinition, criteria, readOnly, middleObject, hasDescription, viewLayout,
|
|
12
|
-
const { instance } = useFormContext();
|
|
10
|
+
const { id, fieldDefinition, criteria, readOnly, initialMiddleObjectInstances, middleObject, hasDescription, viewLayout, } = props;
|
|
11
|
+
const { fetchedOptions, setFetchedOptions, instance } = useFormContext();
|
|
13
12
|
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 [
|
|
17
|
+
const [endObject, setEndObject] = useState(fetchedOptions[`${fieldDefinition.id}EndObject`]);
|
|
18
|
+
const [endObjectInstances, setEndObjectInstances] = useState(fetchedOptions[`${fieldDefinition.id}EndObjectInstances`] || []);
|
|
19
|
+
const [initialLoading, setInitialLoading] = useState(endObjectInstances ? false : true);
|
|
18
20
|
const [selectedOptions, setSelectedOptions] = useState([]);
|
|
21
|
+
const [hasFetched, setHasFetched] = useState(!!fetchedOptions[`${fieldDefinition.id}EndObjectInstancesHaveFetched`] || false);
|
|
19
22
|
const [snackbarError, setSnackbarError] = useState({
|
|
20
23
|
showAlert: false,
|
|
21
24
|
isError: true,
|
|
@@ -38,12 +41,6 @@ const DropdownRepeatableField = (props) => {
|
|
|
38
41
|
const newInstances = await getMiddleObjectInstances();
|
|
39
42
|
setMiddleObjectInstances(newInstances);
|
|
40
43
|
};
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
instanceChanges?.subscribe(middleObject.rootObjectId, () => {
|
|
43
|
-
fetchMiddleObjectInstances();
|
|
44
|
-
});
|
|
45
|
-
return () => instanceChanges?.unsubscribe(middleObject.rootObjectId);
|
|
46
|
-
}, [instanceChanges, fetchMiddleObjectInstances, middleObject]);
|
|
47
44
|
const setDropDownSelections = (instances) => {
|
|
48
45
|
setSelectedOptions(instances
|
|
49
46
|
.filter((currInstance) => fieldDefinition.manyToManyPropertyId in currInstance)
|
|
@@ -55,86 +52,146 @@ const DropdownRepeatableField = (props) => {
|
|
|
55
52
|
.sort((instanceA, instanceB) => instanceA.label.localeCompare(instanceB.label)));
|
|
56
53
|
};
|
|
57
54
|
useEffect(() => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
55
|
+
const endObjectProperty = middleObject?.properties?.find((currProperty) => fieldDefinition.manyToManyPropertyId === currProperty.id);
|
|
56
|
+
if (endObjectProperty && endObjectProperty.objectId && !fetchedOptions[`${fieldDefinition.id}EndObject`]) {
|
|
57
|
+
setLayoutLoaded(false);
|
|
58
|
+
apiServices.get(getPrefixedUrl(`/objects/${endObjectProperty.objectId}/effective`), { params: { filter: { fields: ['id', 'name', 'properties', 'viewLayout'] } } }, (error, effectiveObject) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
console.error(error);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// If there's no error then the effective object is defined.
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
65
|
+
const endObject = effectiveObject;
|
|
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
|
+
});
|
|
80
89
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
87
138
|
}
|
|
88
|
-
else {
|
|
89
|
-
|
|
90
|
-
setLayoutLoaded(true);
|
|
139
|
+
else if (endObjectInstances.length !== 0) {
|
|
140
|
+
setInitialLoading(false);
|
|
91
141
|
}
|
|
92
|
-
}, [
|
|
93
|
-
const
|
|
94
|
-
setDebouncedSearchValue(value);
|
|
95
|
-
}, 200), []);
|
|
142
|
+
}, [fieldDefinition.objectId, fieldDefinition.manyToManyPropertyId, middleObject]);
|
|
143
|
+
const debouncedEndObjectSearch = useCallback(debounce(fetchEndObjectInstances, 500), [fetchEndObjectInstances]);
|
|
96
144
|
useEffect(() => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
},
|
|
145
|
+
if (!fetchedOptions[`${fieldDefinition.id}EndObjectInstances`] ||
|
|
146
|
+
(isArray(fetchedOptions[`${fieldDefinition.id}EndObjectInstances`]) &&
|
|
147
|
+
fetchedOptions[`${fieldDefinition.id}EndObjectInstances`].length === 0)) {
|
|
148
|
+
setFetchedOptions({
|
|
149
|
+
[`${fieldDefinition.id}EndObjectInstances`]: endObjectInstances,
|
|
150
|
+
[`${fieldDefinition.id}EndObjectInstancesHaveFetched`]: hasFetched,
|
|
110
151
|
});
|
|
111
|
-
searchCriteria = !isEmpty(criteria) ? { and: [searchCriteria, nameCriteria] } : nameCriteria;
|
|
112
152
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
153
|
+
if (!fetchedOptions[`${fieldDefinition.id}EndObject`]) {
|
|
154
|
+
setFetchedOptions({
|
|
155
|
+
[`${fieldDefinition.id}EndObject`]: endObject,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!isEqual(middleObjectInstances, initialMiddleObjectInstances)) {
|
|
159
|
+
setFetchedOptions({
|
|
160
|
+
[`${fieldDefinition.id}MiddleObjectInstances`]: middleObjectInstances,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}, [endObjectInstances, endObject, middleObjectInstances]);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const updateFetchedOptions = (key, value) => {
|
|
166
|
+
if (!fetchedOptions[key]) {
|
|
167
|
+
setFetchedOptions({ [key]: value });
|
|
168
|
+
}
|
|
125
169
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
updateFetchedOptions(`${fieldDefinition.id}EndObjectInstances`, endObjectInstances);
|
|
171
|
+
updateFetchedOptions(`${fieldDefinition.id}EndObjectInstancesHaveFetched`, hasFetched);
|
|
172
|
+
updateFetchedOptions(`${fieldDefinition.id}EndObject`, endObject);
|
|
173
|
+
if (!isEqual(middleObjectInstances, initialMiddleObjectInstances)) {
|
|
174
|
+
setFetchedOptions({ [`${fieldDefinition.id}MiddleObjectInstances`]: middleObjectInstances });
|
|
175
|
+
}
|
|
176
|
+
}, [
|
|
177
|
+
endObjectInstances,
|
|
178
|
+
endObject,
|
|
179
|
+
middleObjectInstances,
|
|
180
|
+
fetchedOptions,
|
|
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();
|
|
193
|
+
}
|
|
194
|
+
}, [fetchEndObjectInstances, layoutLoaded]);
|
|
138
195
|
const saveMiddleInstance = async (endObjectId, endObjectName) => {
|
|
139
196
|
if (fieldDefinition.objectId) {
|
|
140
197
|
const middleObject = getMiddleObject(fieldDefinition, endObjectId, endObjectName, instance);
|
|
@@ -171,6 +228,6 @@ const DropdownRepeatableField = (props) => {
|
|
|
171
228
|
});
|
|
172
229
|
}
|
|
173
230
|
};
|
|
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:
|
|
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, middleObjectInstances: middleObjectInstances, endObjectInstances: endObjectInstances ?? [], endObject: endObject, searchValue: searchValue, loading: loading, handleSaveMiddleInstance: saveMiddleInstance, handleRemoveMiddleInstance: removeMiddleInstance, setSearchValue: setSearchValue, setSnackbarError: setSnackbarError, snackbarError: snackbarError, selectedOptions: selectedOptions, setSelectedOptions: setSelectedOptions, setDropdownSelections: setDropDownSelections, hasDescription: hasDescription }))));
|
|
175
232
|
};
|
|
176
233
|
export default DropdownRepeatableField;
|
|
@@ -6,6 +6,7 @@ type DropdownRepeatableFieldInputProps = {
|
|
|
6
6
|
fieldDefinition: InputParameter | Property;
|
|
7
7
|
readOnly: boolean;
|
|
8
8
|
layout?: DropdownViewLayout;
|
|
9
|
+
middleObjectInstances: ObjectInstance[];
|
|
9
10
|
endObjectInstances: ObjectInstance[];
|
|
10
11
|
endObject: Pick<Obj, 'id' | 'name' | 'properties'>;
|
|
11
12
|
searchValue: string;
|
|
@@ -25,6 +26,7 @@ type DropdownRepeatableFieldInputProps = {
|
|
|
25
26
|
message?: string;
|
|
26
27
|
isError: boolean;
|
|
27
28
|
};
|
|
29
|
+
setDropdownSelections?: (middleObjectInstances: ObjectInstance[]) => void;
|
|
28
30
|
hasDescription?: boolean;
|
|
29
31
|
};
|
|
30
32
|
export type DropdownRepeatableFieldInputOption = AutocompleteOption & {
|