@evoke-platform/ui-components 1.13.0-dev.6 → 1.13.0-dev.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +70 -66
- 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 +104 -184
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +19 -36
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -20
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -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 +12 -6
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +38 -16
- package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
- package/dist/published/components/custom/FormV2/components/utils.js +25 -25
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +37 -12
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
- package/package.json +3 -2
|
@@ -27,7 +27,7 @@ export type FormRendererProps = BaseProps & {
|
|
|
27
27
|
renderBody?: (props: BodyProps) => React.ReactNode;
|
|
28
28
|
renderFooter?: (props: FooterProps) => React.ReactNode;
|
|
29
29
|
};
|
|
30
|
-
export declare const FormRenderer: React.
|
|
30
|
+
export declare const FormRenderer: ((props: FormRendererProps) => React.JSX.Element) & {
|
|
31
31
|
Header: React.FC<HeaderProps>;
|
|
32
32
|
Body: React.FC<BodyProps>;
|
|
33
33
|
Footer: React.FC<FooterProps>;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useApiServices, } from '@evoke-platform/context';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import { isEmpty, isEqual, omit } from 'lodash';
|
|
3
4
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
5
|
import { useForm } from 'react-hook-form';
|
|
5
6
|
import { useWidgetSize } from '../../../theme';
|
|
6
7
|
import { Box } from '../../layout';
|
|
7
8
|
import { Body } from './components/Body';
|
|
9
|
+
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
8
10
|
import { Footer, FooterActions } from './components/Footer';
|
|
9
11
|
import { FormContext } from './components/FormContext';
|
|
10
12
|
import Header, { AccordionActions, Title } from './components/Header';
|
|
11
|
-
import { assignIdsToSectionsAndRichText, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, isAddressProperty, obfuscateValue, } from './components/utils';
|
|
13
|
+
import { assignIdsToSectionsAndRichText, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, getPrefixedUrl, isAddressProperty, obfuscateValue, } from './components/utils';
|
|
12
14
|
import { handleValidation } from './components/ValidationFiles/Validation';
|
|
13
15
|
import ValidationErrors from './components/ValidationFiles/ValidationErrors';
|
|
14
16
|
const FormRendererInternal = (props) => {
|
|
@@ -23,15 +25,14 @@ const FormRendererInternal = (props) => {
|
|
|
23
25
|
defaultWidth: 1200,
|
|
24
26
|
});
|
|
25
27
|
const isSmallerThanMd = isBelow('md');
|
|
28
|
+
const apiServices = useApiServices();
|
|
26
29
|
const [expandedSections, setExpandedSections] = useState([]);
|
|
27
30
|
const [fetchedOptions, setFetchedOptions] = useState({});
|
|
28
31
|
const [expandAll, setExpandAll] = useState();
|
|
29
32
|
const [action, setAction] = useState();
|
|
30
|
-
const [object, setObject] = useState();
|
|
31
33
|
const [triggerFieldReset, setTriggerFieldReset] = useState(false);
|
|
32
34
|
const [isInitializing, setIsInitializing] = useState(true);
|
|
33
35
|
const [parameters, setParameters] = useState();
|
|
34
|
-
const objectStore = useObject(objectId);
|
|
35
36
|
const validationContainerRef = useRef(null);
|
|
36
37
|
const updateFetchedOptions = (newData) => {
|
|
37
38
|
setFetchedOptions((prev) => ({
|
|
@@ -45,32 +46,26 @@ const FormRendererInternal = (props) => {
|
|
|
45
46
|
function handleCollapseAll() {
|
|
46
47
|
setExpandAll(false);
|
|
47
48
|
}
|
|
49
|
+
const { data: object } = useQuery({
|
|
50
|
+
queryKey: [objectId, 'sanitized'],
|
|
51
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${objectId}/effective`), {
|
|
52
|
+
params: { sanitizedVersion: true },
|
|
53
|
+
}),
|
|
54
|
+
staleTime: Infinity,
|
|
55
|
+
enabled: !!objectId,
|
|
56
|
+
});
|
|
48
57
|
const updatedEntries = useMemo(() => {
|
|
49
58
|
return object ? assignIdsToSectionsAndRichText(entries, object, parameters) : [];
|
|
50
59
|
}, [entries, object, parameters]);
|
|
51
60
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
else if (object) {
|
|
62
|
-
// if forms actionId is synced with object properties
|
|
63
|
-
setParameters(convertPropertiesToParams(object));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
console.error('Failed to fetch object, action or parameters:', error);
|
|
68
|
-
}
|
|
69
|
-
finally {
|
|
70
|
-
setIsInitializing(false);
|
|
71
|
-
}
|
|
72
|
-
})();
|
|
73
|
-
}, [objectStore, actionId]);
|
|
61
|
+
if (!object || !actionId)
|
|
62
|
+
return;
|
|
63
|
+
const action = object.actions?.find((a) => a.id === actionId);
|
|
64
|
+
setAction(action);
|
|
65
|
+
// if forms action is synced with object properties then convertPropertiesToParams
|
|
66
|
+
setParameters(action?.parameters ?? convertPropertiesToParams(object));
|
|
67
|
+
setIsInitializing(false);
|
|
68
|
+
}, [object, actionId]);
|
|
74
69
|
useEffect(() => {
|
|
75
70
|
const currentValues = getValues();
|
|
76
71
|
if (value) {
|
|
@@ -243,7 +238,10 @@ const FormRendererInternal = (props) => {
|
|
|
243
238
|
} })),
|
|
244
239
|
action && onSubmit && (renderFooter ? renderFooter(footerProps) : React.createElement(Footer, { ...footerProps }))))));
|
|
245
240
|
};
|
|
246
|
-
export const FormRenderer = Object.assign(
|
|
241
|
+
export const FormRenderer = Object.assign(function FormRenderer(props) {
|
|
242
|
+
return (React.createElement(ConditionalQueryClientProvider, null,
|
|
243
|
+
React.createElement(FormRendererInternal, { ...props })));
|
|
244
|
+
}, {
|
|
247
245
|
Header,
|
|
248
246
|
Body,
|
|
249
247
|
Footer,
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import axios from 'axios';
|
|
3
4
|
import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set } from 'lodash';
|
|
4
5
|
import React, { useEffect, useRef, useState } from 'react';
|
|
5
6
|
import { Skeleton, Snackbar } from '../../core';
|
|
6
7
|
import { Box } from '../../layout';
|
|
7
8
|
import ErrorComponent from '../ErrorComponent';
|
|
9
|
+
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
8
10
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
9
11
|
import Header from './components/Header';
|
|
10
|
-
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
|
|
12
|
+
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
11
13
|
import FormRenderer from './FormRenderer';
|
|
14
|
+
// Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
|
|
12
15
|
function FormRendererContainer(props) {
|
|
13
|
-
|
|
16
|
+
return (React.createElement(ConditionalQueryClientProvider, null,
|
|
17
|
+
React.createElement(FormRendererContainerInner, { ...props })));
|
|
18
|
+
}
|
|
19
|
+
// Inner component that assumes QueryClient context is available
|
|
20
|
+
function FormRendererContainerInner(props) {
|
|
21
|
+
const { instanceId, pageNavigation, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
|
|
14
22
|
const apiServices = useApiServices();
|
|
15
23
|
const navigateTo = useNavigate();
|
|
16
24
|
const { id: appId } = useApp();
|
|
17
|
-
const [sanitizedObject, setSanitizedObject] = useState();
|
|
18
|
-
const [navigationSlug, setNavigationSlug] = useState();
|
|
19
25
|
const [parameters, setParameters] = useState();
|
|
20
26
|
const [instance, setInstance] = useState();
|
|
21
27
|
const formDataRef = useRef();
|
|
@@ -53,72 +59,68 @@ function FormRendererContainer(props) {
|
|
|
53
59
|
setSnackbarError({ ...snackbarError, isError: true });
|
|
54
60
|
setError(code ?? err);
|
|
55
61
|
};
|
|
62
|
+
const { data: sanitizedObject, error: sanitizedObjectError } = useQuery({
|
|
63
|
+
queryKey: [form?.objectId ?? objectId, ...(instanceId ? [instanceId] : []), 'sanitized'],
|
|
64
|
+
queryFn: () =>
|
|
65
|
+
// form?.objectId is needed for subtype forms to get the correct object
|
|
66
|
+
apiServices.get(getPrefixedUrl(`/objects/${form?.objectId ?? objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } }),
|
|
67
|
+
staleTime: Infinity,
|
|
68
|
+
enabled: !!(form?.objectId || objectId),
|
|
69
|
+
});
|
|
70
|
+
const { data: fetchedInstance, error: instanceError } = useQuery({
|
|
71
|
+
queryKey: [objectId, instanceId, 'instance'],
|
|
72
|
+
queryFn: () => objectStore.getInstance(instanceId),
|
|
73
|
+
staleTime: Infinity,
|
|
74
|
+
enabled: !!instanceId && !!sanitizedObject,
|
|
75
|
+
});
|
|
56
76
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
const object = await apiServices.get(getPrefixedUrl(`/objects/${form?.objectId || objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } });
|
|
64
|
-
setSanitizedObject(object);
|
|
65
|
-
const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
66
|
-
if (action && (instanceId || action.type === 'create')) {
|
|
67
|
-
setAction(action);
|
|
68
|
-
// Clear error if action is found after being missing
|
|
69
|
-
// TODO: This entire effect should take place after form is fetched to avoid an error flickering
|
|
70
|
-
// That is, this effect should be merged with the one below that fetches the form
|
|
71
|
-
setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
setError('Action could not be found');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
onError(error);
|
|
79
|
-
}
|
|
80
|
-
})();
|
|
81
|
-
}, [dataType, form, instanceId]);
|
|
77
|
+
if (fetchedInstance)
|
|
78
|
+
setInstance(fetchedInstance);
|
|
79
|
+
if (instanceError)
|
|
80
|
+
onError(instanceError);
|
|
81
|
+
}, [fetchedInstance, instanceError]);
|
|
82
82
|
useEffect(() => {
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
if (!sanitizedObject)
|
|
84
|
+
return;
|
|
85
|
+
const action = sanitizedObject.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
86
|
+
if (action && (instanceId || action.type === 'create')) {
|
|
87
|
+
setAction(action);
|
|
88
|
+
// Clear error if action is found after being missing
|
|
89
|
+
setError((prevError) => (prevError === 'Action could not be found' ? undefined : prevError));
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
else {
|
|
92
|
+
setError('Action could not be found');
|
|
93
|
+
}
|
|
94
|
+
}, [sanitizedObject, actionId, form?.actionId, instanceId]);
|
|
95
|
+
const { data: navigationSlug } = useQuery({
|
|
96
|
+
queryKey: [appId, 'navigationSlug'],
|
|
97
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
|
|
98
|
+
select: (page) => page.slug,
|
|
99
|
+
staleTime: Infinity,
|
|
100
|
+
enabled: !!pageNavigation,
|
|
101
|
+
});
|
|
102
|
+
const formIdToFetch = formId || action?.defaultFormId;
|
|
103
|
+
const { data: fetchedForm, error: fetchedFormError } = useFormById(formIdToFetch ?? '', apiServices);
|
|
91
104
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!actionId || evokeForm?.actionId === actionId) {
|
|
103
|
-
const form = evokeForm;
|
|
104
|
-
setForm(form);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
setError('Configured action ID does not match form action ID');
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
.catch((error) => {
|
|
111
|
-
onError(error);
|
|
112
|
-
});
|
|
105
|
+
if (!formIdToFetch && action) {
|
|
106
|
+
setError('Action form could not be found');
|
|
107
|
+
}
|
|
108
|
+
}, [formIdToFetch, action]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (fetchedForm) {
|
|
111
|
+
if (actionId && fetchedForm.actionId !== actionId) {
|
|
112
|
+
setError('Configured action ID does not match form action ID');
|
|
113
|
+
}
|
|
114
|
+
setForm(fetchedForm);
|
|
113
115
|
}
|
|
114
|
-
else if (action?.type === 'delete' && formId === '_auto_') {
|
|
116
|
+
else if (action?.type === 'delete' && formId === '_auto_' && instance) {
|
|
115
117
|
setForm({
|
|
116
118
|
id: '',
|
|
117
|
-
name: '',
|
|
119
|
+
name: 'Delete',
|
|
118
120
|
entries: [
|
|
119
121
|
{
|
|
120
122
|
type: 'content',
|
|
121
|
-
html: `<p style="padding-top: 24px; padding-bottom: 24px;">You are about to delete <strong>${instance
|
|
123
|
+
html: `<p style="padding-top: 24px; padding-bottom: 24px;">You are about to delete <strong>${instance.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</p>`,
|
|
122
124
|
},
|
|
123
125
|
],
|
|
124
126
|
objectId: objectId,
|
|
@@ -128,10 +130,12 @@ function FormRendererContainer(props) {
|
|
|
128
130
|
},
|
|
129
131
|
});
|
|
130
132
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
}, [fetchedForm, instance, action, formId]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const error = sanitizedObjectError || fetchedFormError;
|
|
136
|
+
if (error)
|
|
137
|
+
onError(error);
|
|
138
|
+
}, [sanitizedObjectError, fetchedFormError]);
|
|
135
139
|
useEffect(() => {
|
|
136
140
|
if (!form)
|
|
137
141
|
return;
|
|
@@ -141,7 +145,7 @@ function FormRendererContainer(props) {
|
|
|
141
145
|
}, [form, action?.parameters, sanitizedObject]);
|
|
142
146
|
useEffect(() => {
|
|
143
147
|
const getInitialValues = async () => {
|
|
144
|
-
if (form && (instance || !instanceId)) {
|
|
148
|
+
if (form && parameters && (instance || !instanceId)) {
|
|
145
149
|
const defaultValues = await getDefaultValues(form.entries, instance || {});
|
|
146
150
|
setFormData(defaultValues);
|
|
147
151
|
// Deep clone to avoid reference issues
|
|
@@ -149,7 +153,7 @@ function FormRendererContainer(props) {
|
|
|
149
153
|
}
|
|
150
154
|
};
|
|
151
155
|
getInitialValues();
|
|
152
|
-
}, [form, instance, sanitizedObject]);
|
|
156
|
+
}, [form, instance, sanitizedObject, parameters]);
|
|
153
157
|
const onSubmissionSuccess = (updatedInstance) => {
|
|
154
158
|
setSnackbarError({
|
|
155
159
|
showAlert: true,
|
|
@@ -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;
|
|
@@ -6,7 +6,6 @@ type DropdownRepeatableFieldInputProps = {
|
|
|
6
6
|
fieldDefinition: InputParameter | Property;
|
|
7
7
|
readOnly: boolean;
|
|
8
8
|
layout?: DropdownViewLayout;
|
|
9
|
-
middleObjectInstances: ObjectInstance[];
|
|
10
9
|
endObjectInstances: ObjectInstance[];
|
|
11
10
|
endObject: Pick<Obj, 'id' | 'name' | 'properties'>;
|
|
12
11
|
searchValue: string;
|
|
@@ -26,7 +25,6 @@ type DropdownRepeatableFieldInputProps = {
|
|
|
26
25
|
message?: string;
|
|
27
26
|
isError: boolean;
|
|
28
27
|
};
|
|
29
|
-
setDropdownSelections?: (middleObjectInstances: ObjectInstance[]) => void;
|
|
30
28
|
hasDescription?: boolean;
|
|
31
29
|
};
|
|
32
30
|
export type DropdownRepeatableFieldInputOption = AutocompleteOption & {
|
|
@@ -7,12 +7,9 @@ import FormField from '../../../../FormField';
|
|
|
7
7
|
import { normalizeDates } from '../../utils';
|
|
8
8
|
const isDropdownRepeatableFieldInputOption = (option) => isObject(option) && 'label' in option && 'endObjectId' in option;
|
|
9
9
|
export const DropdownRepeatableFieldInput = (props) => {
|
|
10
|
-
const { id, fieldDefinition, readOnly, layout,
|
|
10
|
+
const { id, fieldDefinition, readOnly, layout, endObjectInstances, endObject, searchValue, loading, handleSaveMiddleInstance, handleRemoveMiddleInstance, setSearchValue, selectedOptions, setSnackbarError, snackbarError, hasDescription, } = props;
|
|
11
11
|
const { fieldHeight } = useFormContext();
|
|
12
12
|
const [selectOptions, setSelectOptions] = useState([]);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
setDropdownSelections && setDropdownSelections(middleObjectInstances);
|
|
15
|
-
}, [middleObjectInstances]);
|
|
16
13
|
useEffect(() => {
|
|
17
14
|
const manyToManyPropertyId = fieldDefinition.manyToManyPropertyId;
|
|
18
15
|
if (manyToManyPropertyId) {
|