@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import prettyBytes from 'pretty-bytes';
|
|
3
4
|
import React, { useEffect, useState } from 'react';
|
|
4
5
|
import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
|
|
@@ -28,7 +29,6 @@ export const DocumentList = (props) => {
|
|
|
28
29
|
const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
|
|
29
30
|
// Determine property type once at component level
|
|
30
31
|
const isFileType = fieldType === 'file';
|
|
31
|
-
const [hasViewPermission, setHasViewPermission] = useState(fetchedOptions[`${id}ViewPermission`] ?? true);
|
|
32
32
|
// savedDocuments is either FileInstance[] or DocumentType[], never a mix
|
|
33
33
|
const [savedDocuments, setSavedDocuments] = useState(fetchedOptions[`${id}SavedDocuments`]);
|
|
34
34
|
useEffect(() => {
|
|
@@ -82,27 +82,23 @@ export const DocumentList = (props) => {
|
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
84
|
};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}, [object, id, fetchedOptions[`${id}ViewPermission`]]);
|
|
90
|
-
const checkPermissions = () => {
|
|
91
|
-
if (instance?.[id]?.length) {
|
|
85
|
+
const { data: hasViewPermission = false } = useQuery({
|
|
86
|
+
queryKey: ['hasViewPermission', object?.id, instance?.id],
|
|
87
|
+
queryFn: async () => {
|
|
92
88
|
const endpoint = isFileType
|
|
93
89
|
? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=read&field=content`)
|
|
94
|
-
: getPrefixedUrl(`/objects/${object
|
|
95
|
-
|
|
96
|
-
.get(endpoint)
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
};
|
|
90
|
+
: getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/checkAccess?action=view`);
|
|
91
|
+
try {
|
|
92
|
+
const viewPermissionCheck = await apiServices.get(endpoint);
|
|
93
|
+
return viewPermissionCheck.result;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
enabled: !!instance?.id && !!object?.id && !!instance?.[id]?.length,
|
|
100
|
+
staleTime: Infinity,
|
|
101
|
+
});
|
|
106
102
|
const isFile = (doc) => doc instanceof File;
|
|
107
103
|
const isUnsavedFile = (doc) => isFile(doc) || !!doc.unsaved;
|
|
108
104
|
const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
2
|
import { Close, ExpandMore } from '@mui/icons-material';
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
4
|
import React, { useEffect, useState } from 'react';
|
|
4
5
|
import { useFormContext } from '../../../../../theme/hooks';
|
|
5
6
|
import { Autocomplete, IconButton, Paper, TextField, Typography } from '../../../../core';
|
|
6
7
|
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
7
8
|
const UserProperty = (props) => {
|
|
8
9
|
const { id, error, value, readOnly, hasDescription } = props;
|
|
9
|
-
const {
|
|
10
|
+
const { handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
|
|
10
11
|
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
11
12
|
const apiServices = useApiServices();
|
|
12
|
-
const [options, setOptions] = useState(
|
|
13
|
+
const [options, setOptions] = useState([]);
|
|
13
14
|
const [openOptions, setOpenOptions] = useState(false);
|
|
14
|
-
const [users, setUsers] = useState();
|
|
15
15
|
const [userValue, setUserValue] = useState();
|
|
16
16
|
useEffect(() => {
|
|
17
17
|
if (value && typeof value == 'object' && 'name' in value && 'id' in value) {
|
|
@@ -21,25 +21,21 @@ const UserProperty = (props) => {
|
|
|
21
21
|
setUserValue(undefined);
|
|
22
22
|
}
|
|
23
23
|
}, [value]);
|
|
24
|
+
const { data: users } = useQuery({
|
|
25
|
+
queryKey: ['users'],
|
|
26
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/users`)),
|
|
27
|
+
staleTime: Infinity,
|
|
28
|
+
meta: {
|
|
29
|
+
errorMessage: 'Error fetching users: ',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
24
32
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
value: user.id,
|
|
32
|
-
})));
|
|
33
|
-
setFetchedOptions({
|
|
34
|
-
[`${id}Options`]: (userList ?? []).map((user) => ({
|
|
35
|
-
label: user.name,
|
|
36
|
-
value: user.id,
|
|
37
|
-
})),
|
|
38
|
-
});
|
|
39
|
-
setLoadingOptions(false);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}, [id]);
|
|
33
|
+
setOptions((users ?? []).map((user) => ({
|
|
34
|
+
label: user.name,
|
|
35
|
+
value: user.id,
|
|
36
|
+
})));
|
|
37
|
+
setLoadingOptions(false);
|
|
38
|
+
}, [users]);
|
|
43
39
|
async function handleChangeUserProperty(id, value) {
|
|
44
40
|
const updatedValue = typeof value?.value === 'string' ? { name: value.label, id: value.value } : null;
|
|
45
41
|
try {
|
|
@@ -1,53 +1,41 @@
|
|
|
1
1
|
import { useApiServices, useApp, useNavigate, } from '@evoke-platform/context';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import cleanDeep from 'clean-deep';
|
|
3
|
-
import { cloneDeep, debounce, isEmpty,
|
|
4
|
+
import { cloneDeep, debounce, isEmpty, isNil } from 'lodash';
|
|
4
5
|
import Handlebars from 'no-eval-handlebars';
|
|
5
|
-
import React, {
|
|
6
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
7
|
import { Close } from '../../../../../../icons';
|
|
7
8
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
8
9
|
import { Autocomplete, Button, IconButton, Link, ListItem, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
|
|
9
10
|
import { Box } from '../../../../../layout';
|
|
10
|
-
import { getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
|
|
11
|
+
import { getDefaultPages, getPrefixedUrl, transformToWhere, useFormById } from '../../utils';
|
|
11
12
|
import RelatedObjectInstance from './RelatedObjectInstance';
|
|
12
13
|
const ObjectPropertyInput = (props) => {
|
|
13
14
|
const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, relatedObjectId, } = props;
|
|
14
15
|
const { fetchedOptions, setFetchedOptions, fieldHeight, handleChange: handleChangeObjectField, onAutosave: onAutosave, instance, } = useFormContext();
|
|
15
16
|
const { defaultPages, findDefaultPageSlugFor } = useApp();
|
|
17
|
+
const [debouncedDropdownInput, setDebouncedDropdownInput] = useState();
|
|
16
18
|
const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
|
|
17
19
|
const [openCreateDialog, setOpenCreateDialog] = useState(false);
|
|
18
|
-
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
19
|
-
const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${id}NavigationSlug`]);
|
|
20
|
-
const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${id}RelatedObject`]);
|
|
21
20
|
const [dropdownInput, setDropdownInput] = useState();
|
|
22
21
|
const [openOptions, setOpenOptions] = useState(false);
|
|
23
|
-
const [hasFetched, setHasFetched] = useState(fetchedOptions[`${id}OptionsHaveFetched`] || false);
|
|
24
|
-
const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
|
|
25
22
|
const [layout, setLayout] = useState(fetchedOptions[`${id}ViewLayout`]);
|
|
26
|
-
const [form, setForm] = useState();
|
|
27
23
|
const [snackbarError, setSnackbarError] = useState({
|
|
28
24
|
showAlert: false,
|
|
29
25
|
isError: true,
|
|
30
26
|
});
|
|
27
|
+
const { data: relatedObject } = useQuery({
|
|
28
|
+
queryKey: [relatedObjectId, 'sanitized'],
|
|
29
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`)),
|
|
30
|
+
enabled: !!relatedObjectId,
|
|
31
|
+
staleTime: Infinity,
|
|
32
|
+
});
|
|
31
33
|
const action = relatedObject?.actions?.find((action) => action.id === createActionId);
|
|
32
34
|
const apiServices = useApiServices();
|
|
33
35
|
const navigateTo = useNavigate();
|
|
34
|
-
const updatedCriteria = useMemo(() => {
|
|
35
|
-
let criteria = filter ? { where: transformToWhere(filter) } : undefined;
|
|
36
|
-
if (dropdownInput) {
|
|
37
|
-
const nameQuery = transformToWhere({
|
|
38
|
-
name: {
|
|
39
|
-
like: dropdownInput,
|
|
40
|
-
options: 'i',
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
criteria = {
|
|
44
|
-
...criteria,
|
|
45
|
-
where: criteria?.where ? { and: [criteria.where, nameQuery] } : nameQuery,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return criteria;
|
|
49
|
-
}, [filter, dropdownInput]);
|
|
50
36
|
const listboxRef = useRef(null);
|
|
37
|
+
const formIdToFetch = formId || action?.defaultFormId;
|
|
38
|
+
const { data: form } = useFormById(formIdToFetch ?? '', apiServices, 'Error fetching form: ');
|
|
51
39
|
useEffect(() => {
|
|
52
40
|
if (relatedObject) {
|
|
53
41
|
let defaultViewLayout;
|
|
@@ -85,143 +73,102 @@ const ObjectPropertyInput = (props) => {
|
|
|
85
73
|
}
|
|
86
74
|
}
|
|
87
75
|
}, [displayOption, relatedObject, viewLayout]);
|
|
76
|
+
const debouncedSetDropdownInput = useMemo(() => debounce((value) => {
|
|
77
|
+
setDebouncedDropdownInput(value);
|
|
78
|
+
}, 200), []);
|
|
88
79
|
useEffect(() => {
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
debouncedSetDropdownInput(dropdownInput);
|
|
81
|
+
return () => {
|
|
82
|
+
debouncedSetDropdownInput.cancel();
|
|
83
|
+
};
|
|
84
|
+
}, [dropdownInput, debouncedSetDropdownInput]);
|
|
85
|
+
const updatedCriteria = useMemo(() => {
|
|
86
|
+
let criteria = filter ? { where: transformToWhere(filter) } : undefined;
|
|
87
|
+
if (debouncedDropdownInput) {
|
|
88
|
+
const nameQuery = transformToWhere({
|
|
89
|
+
name: {
|
|
90
|
+
like: debouncedDropdownInput,
|
|
91
|
+
options: 'i',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
criteria = {
|
|
95
|
+
...criteria,
|
|
96
|
+
where: criteria?.where ? { and: [criteria.where, nameQuery] } : nameQuery,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return criteria;
|
|
100
|
+
}, [filter, debouncedDropdownInput]);
|
|
101
|
+
const { data: defaultInstances, isLoading: isLoadingDefaultInstances } = useQuery({
|
|
102
|
+
queryKey: ['defaultInstances', relatedObjectId, updatedCriteria],
|
|
103
|
+
queryFn: async () => {
|
|
91
104
|
const updatedFilter = cleanDeep({
|
|
92
105
|
where: transformToWhere({ $and: [defaultValueCriteria, updatedCriteria?.where ?? {}] }),
|
|
93
106
|
order: orderBy && sortBy ? encodeURIComponent(sortBy + ' ' + orderBy) : undefined,
|
|
94
107
|
limit: 1,
|
|
95
108
|
});
|
|
96
109
|
if (updatedFilter.where) {
|
|
97
|
-
|
|
98
|
-
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
|
|
99
|
-
if (error) {
|
|
100
|
-
console.error(error);
|
|
101
|
-
setLoadingOptions(false);
|
|
102
|
-
}
|
|
103
|
-
if (instances && instances.length > 0) {
|
|
104
|
-
setSelectedInstance(instances[0]);
|
|
105
|
-
try {
|
|
106
|
-
handleChangeObjectField && (await handleChangeObjectField(id, instances[0]));
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
console.error('Failed to update field:', error);
|
|
110
|
-
setLoadingOptions(false);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
await onAutosave?.(id);
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
console.error('Autosave failed:', error);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
setLoadingOptions(false);
|
|
121
|
-
});
|
|
110
|
+
return apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`));
|
|
122
111
|
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
(fetchedOptions?.[`${id}Options`]).length === 0) &&
|
|
128
|
-
!hasFetched) ||
|
|
129
|
-
!isEqual(fetchedOptions?.[`${id}UpdatedCriteria`], updatedCriteria)) {
|
|
130
|
-
setFetchedOptions({ [`${id}UpdatedCriteria`]: updatedCriteria });
|
|
131
|
-
setLoadingOptions(true);
|
|
132
|
-
const updatedFilter = cloneDeep(updatedCriteria) || {};
|
|
133
|
-
updatedFilter.limit = 100;
|
|
134
|
-
const { propertyId, direction } = layout?.sort ?? {
|
|
135
|
-
propertyId: 'name',
|
|
136
|
-
direction: 'asc',
|
|
137
|
-
};
|
|
138
|
-
updatedFilter.order = `${propertyId} ${direction}`;
|
|
139
|
-
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
|
|
140
|
-
if (error) {
|
|
141
|
-
console.error(error);
|
|
142
|
-
setLoadingOptions(false);
|
|
143
|
-
}
|
|
144
|
-
if (instances) {
|
|
145
|
-
setOptions(instances);
|
|
146
|
-
setLoadingOptions(false);
|
|
147
|
-
// so if you go off a section too quickly and it doesn't fetch it re-fetches but doesn't cause an infinite loop
|
|
148
|
-
setHasFetched(true);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}, [
|
|
153
|
-
relatedObjectId,
|
|
154
|
-
updatedCriteria,
|
|
155
|
-
layout,
|
|
156
|
-
fetchedOptions?.[`${id}Options`],
|
|
157
|
-
fetchedOptions?.[`${id}UpdatedCriteria`],
|
|
158
|
-
hasFetched,
|
|
159
|
-
id,
|
|
160
|
-
]);
|
|
161
|
-
const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
|
|
162
|
-
useEffect(() => {
|
|
163
|
-
if (displayOption === 'dropdown') {
|
|
164
|
-
debouncedGetDropdownOptions();
|
|
165
|
-
return () => debouncedGetDropdownOptions.cancel();
|
|
166
|
-
}
|
|
167
|
-
}, [displayOption, debouncedGetDropdownOptions]);
|
|
168
|
-
useEffect(() => {
|
|
169
|
-
setSelectedInstance(initialValue);
|
|
170
|
-
}, [initialValue]);
|
|
112
|
+
},
|
|
113
|
+
enabled: !isEmpty(defaultValueCriteria) && !selectedInstance && (!instance || !instance[id]),
|
|
114
|
+
staleTime: Infinity,
|
|
115
|
+
});
|
|
171
116
|
useEffect(() => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let evokeForm;
|
|
178
|
-
if (formId || action?.defaultFormId) {
|
|
179
|
-
evokeForm = await apiServices.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`));
|
|
117
|
+
if (defaultInstances?.[0]) {
|
|
118
|
+
setSelectedInstance(defaultInstances[0]);
|
|
119
|
+
(async () => {
|
|
120
|
+
try {
|
|
121
|
+
handleChangeObjectField && (await handleChangeObjectField(id, defaultInstances[0]));
|
|
180
122
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
[`${id}Form`]: evokeForm,
|
|
185
|
-
});
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('Failed to update field:', error);
|
|
125
|
+
return;
|
|
186
126
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
console.error('Error fetching form:', error);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
fetchForm();
|
|
193
|
-
}, [action, formId, id, apiServices, fetchedOptions]);
|
|
194
|
-
useEffect(() => {
|
|
195
|
-
if (!fetchedOptions[`${id}RelatedObject`]) {
|
|
196
|
-
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`), (error, object) => {
|
|
197
|
-
if (error) {
|
|
198
|
-
console.error(error);
|
|
127
|
+
try {
|
|
128
|
+
await onAutosave?.(id);
|
|
199
129
|
}
|
|
200
|
-
|
|
201
|
-
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error('Autosave failed:', error);
|
|
202
132
|
}
|
|
203
|
-
});
|
|
133
|
+
})();
|
|
204
134
|
}
|
|
205
|
-
}, [
|
|
135
|
+
}, [defaultInstances]);
|
|
136
|
+
// Construct filter from debounced criteria
|
|
137
|
+
const updatedFilter = useMemo(() => {
|
|
138
|
+
const filter = cloneDeep(updatedCriteria) || {};
|
|
139
|
+
filter.limit = 100;
|
|
140
|
+
const { propertyId, direction } = layout?.sort ?? {
|
|
141
|
+
propertyId: 'name',
|
|
142
|
+
direction: 'asc',
|
|
143
|
+
};
|
|
144
|
+
filter.order = `${propertyId} ${direction}`;
|
|
145
|
+
return filter;
|
|
146
|
+
}, [updatedCriteria, layout]);
|
|
147
|
+
const {
|
|
148
|
+
// Sets the default value of options to an empty array
|
|
149
|
+
data: options = [], isLoading: isLoadingOptions, } = useQuery({
|
|
150
|
+
queryKey: ['dropdownOptions', relatedObjectId, updatedFilter],
|
|
151
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`)),
|
|
152
|
+
staleTime: 300000,
|
|
153
|
+
// Keep old data while fetching new data
|
|
154
|
+
placeholderData: (previousData) => previousData,
|
|
155
|
+
});
|
|
206
156
|
useEffect(() => {
|
|
207
|
-
(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else {
|
|
217
|
-
// setting the nav slug to null if there is no default page for this object to avoid re-fetching
|
|
218
|
-
setFetchedOptions({
|
|
219
|
-
[`${id}NavigationSlug`]: null,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
157
|
+
setSelectedInstance(initialValue);
|
|
158
|
+
}, [initialValue]);
|
|
159
|
+
const loadingOptions = isLoadingOptions || isLoadingDefaultInstances;
|
|
160
|
+
const { data: navigationSlug } = useQuery({
|
|
161
|
+
queryKey: ['navigationSlug', id, relatedObjectId],
|
|
162
|
+
queryFn: async () => {
|
|
163
|
+
const pages = await getDefaultPages([{ ...fieldDefinition, objectId: relatedObjectId }], defaultPages, findDefaultPageSlugFor);
|
|
164
|
+
if (relatedObjectId && pages[relatedObjectId]) {
|
|
165
|
+
return pages[relatedObjectId];
|
|
222
166
|
}
|
|
223
|
-
|
|
224
|
-
|
|
167
|
+
return null;
|
|
168
|
+
},
|
|
169
|
+
enabled: !!(relatedObjectId && defaultPages && findDefaultPageSlugFor),
|
|
170
|
+
staleTime: Infinity,
|
|
171
|
+
});
|
|
225
172
|
const handleClose = () => {
|
|
226
173
|
setOpenCreateDialog(false);
|
|
227
174
|
};
|
|
@@ -229,27 +176,6 @@ const ObjectPropertyInput = (props) => {
|
|
|
229
176
|
const template = Handlebars.compileAST(expression);
|
|
230
177
|
return instance ? template(instance) : undefined;
|
|
231
178
|
};
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
if (relatedObject && !fetchedOptions[`${id}RelatedObject`]) {
|
|
234
|
-
setFetchedOptions({
|
|
235
|
-
[`${id}RelatedObject`]: relatedObject,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
if (options &&
|
|
239
|
-
(!fetchedOptions[`${id}Options`] || fetchedOptions[`${id}Options`].length === 0) &&
|
|
240
|
-
hasFetched &&
|
|
241
|
-
!fetchedOptions[`${id}OptionsHaveFetched`]) {
|
|
242
|
-
setFetchedOptions({
|
|
243
|
-
[`${id}Options`]: options,
|
|
244
|
-
[`${id}OptionsHaveFetched`]: hasFetched,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
if (navigationSlug && !fetchedOptions[`${id}NavigationSlug`]) {
|
|
248
|
-
setFetchedOptions({
|
|
249
|
-
[`${id}NavigationSlug`]: navigationSlug,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, id]);
|
|
253
179
|
const dropdownOptions = [
|
|
254
180
|
...options.map((o) => ({ label: o.name, value: o.id })),
|
|
255
181
|
...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === createActionId)
|
|
@@ -422,7 +348,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
422
348
|
}
|
|
423
349
|
}, selectOnFocus: false, onBlur: () => {
|
|
424
350
|
if (dropdownInput) {
|
|
425
|
-
|
|
351
|
+
setDropdownInput(undefined);
|
|
426
352
|
}
|
|
427
353
|
}, renderInput: (params) => (React.createElement(TextField, { ...params, placeholder: selectedInstance?.id || readOnly ? '' : 'Select', readOnly: !loadingOptions && !selectedInstance?.id && readOnly, onChange: (event) => setDropdownInput(event.target.value), sx: {
|
|
428
354
|
...(!loadingOptions && selectedInstance?.id
|
|
@@ -529,7 +455,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
529
455
|
event.stopPropagation();
|
|
530
456
|
setOpenCreateDialog(true);
|
|
531
457
|
}, "aria-label": `Add` }, "Add")))),
|
|
532
|
-
React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption,
|
|
458
|
+
React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, filter: updatedCriteria, layout: layout, formId: formIdToFetch, actionId: createActionId, setSnackbarError: setSnackbarError }),
|
|
533
459
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
|
|
534
460
|
isError: snackbarError.isError,
|
|
535
461
|
showAlert: false,
|
|
@@ -15,8 +15,6 @@ export type RelatedObjectInstanceProps = BaseProps & {
|
|
|
15
15
|
isError: boolean;
|
|
16
16
|
}>>;
|
|
17
17
|
displayOption?: 'dropdown' | 'dialogBox';
|
|
18
|
-
setOptions: (options: ObjectInstance[]) => void;
|
|
19
|
-
options: ObjectInstance[];
|
|
20
18
|
filter?: Record<string, unknown>;
|
|
21
19
|
layout?: TableViewLayout;
|
|
22
20
|
formId?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
2
|
import { DialogActions } from '@mui/material';
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
3
4
|
import { isEmpty } from 'lodash';
|
|
4
5
|
import React, { useCallback, useRef, useState } from 'react';
|
|
5
6
|
import { Close } from '../../../../../../icons';
|
|
@@ -29,7 +30,7 @@ const styles = {
|
|
|
29
30
|
},
|
|
30
31
|
};
|
|
31
32
|
const RelatedObjectInstance = (props) => {
|
|
32
|
-
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError,
|
|
33
|
+
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError, } = props;
|
|
33
34
|
const { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
|
|
34
35
|
const [selectedRow, setSelectedRow] = useState();
|
|
35
36
|
const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
|
|
@@ -40,6 +41,7 @@ const RelatedObjectInstance = (props) => {
|
|
|
40
41
|
defaultWidth: width,
|
|
41
42
|
});
|
|
42
43
|
const { isXs, isSm } = breakpoints;
|
|
44
|
+
const queryClient = useQueryClient();
|
|
43
45
|
const linkExistingInstance = async () => {
|
|
44
46
|
if (selectedRow && handleChangeObjectField) {
|
|
45
47
|
setSelectedInstance(selectedRow);
|
|
@@ -93,7 +95,11 @@ const RelatedObjectInstance = (props) => {
|
|
|
93
95
|
message: 'New instance created',
|
|
94
96
|
isError: false,
|
|
95
97
|
});
|
|
96
|
-
|
|
98
|
+
// Clear option cache to then fetch newly created instance
|
|
99
|
+
queryClient.invalidateQueries({
|
|
100
|
+
queryKey: ['dropdownOptions', relatedObject.id],
|
|
101
|
+
exact: false,
|
|
102
|
+
});
|
|
97
103
|
onClose();
|
|
98
104
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
105
|
}
|
|
@@ -113,14 +119,14 @@ const RelatedObjectInstance = (props) => {
|
|
|
113
119
|
}
|
|
114
120
|
};
|
|
115
121
|
const shouldShowRadioButtons = displayOption !== 'dropdown' && mode !== 'existingOnly' && mode !== 'newOnly' && actionId;
|
|
116
|
-
const RadioButtons = () => shouldShowRadioButtons ? (React.createElement(RadioGroup, { row: true, "aria-label": "Relation Type", onChange: (event) => {
|
|
122
|
+
const RadioButtons = useCallback(() => shouldShowRadioButtons ? (React.createElement(RadioGroup, { row: true, "aria-label": "Relation Type", onChange: (event) => {
|
|
117
123
|
const { value } = event.target;
|
|
118
124
|
if (value === 'new' || value === 'existing') {
|
|
119
125
|
setRelationType(value);
|
|
120
126
|
}
|
|
121
127
|
}, value: relationType },
|
|
122
128
|
React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
|
|
123
|
-
React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null;
|
|
129
|
+
React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null, [shouldShowRadioButtons, relationType]);
|
|
124
130
|
const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: relatedObject.id, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
|
|
125
131
|
relationType === 'new' ? (React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(FormRenderer.ValidationErrors, { errors: bodyProps.errors, sx: {
|
|
126
132
|
my: isSm || isXs ? 2 : 3,
|
|
@@ -146,7 +152,7 @@ const RelatedObjectInstance = (props) => {
|
|
|
146
152
|
maxWidth: '950px',
|
|
147
153
|
width: '100%',
|
|
148
154
|
},
|
|
149
|
-
} },
|
|
155
|
+
} }, open && (React.createElement(React.Fragment, null,
|
|
150
156
|
React.createElement(DialogTitle, { sx: {
|
|
151
157
|
padding: 3,
|
|
152
158
|
borderBottom: '1px solid #e9ecef',
|
|
@@ -176,6 +182,6 @@ const RelatedObjectInstance = (props) => {
|
|
|
176
182
|
marginLeft: '8px',
|
|
177
183
|
width: '85px',
|
|
178
184
|
'&:hover': { boxShadow: 'none' },
|
|
179
|
-
}, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))))));
|
|
185
|
+
}, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))))))));
|
|
180
186
|
};
|
|
181
187
|
export default RelatedObjectInstance;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
|
|
2
2
|
import { WarningRounded } from '@mui/icons-material';
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
4
|
import DOMPurify from 'dompurify';
|
|
4
5
|
import { isEmpty } from 'lodash';
|
|
5
|
-
import React, {
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
6
7
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
7
|
-
import { TextField, Typography } from '../../../core';
|
|
8
|
+
import { Skeleton, TextField, Typography } from '../../../core';
|
|
8
9
|
import { Box } from '../../../layout';
|
|
9
10
|
import FormField from '../../FormField';
|
|
10
11
|
import AccordionSections from './AccordionSections';
|
|
@@ -18,7 +19,7 @@ import { Image } from './FormFieldTypes/Image';
|
|
|
18
19
|
import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
|
|
19
20
|
import UserProperty from './FormFieldTypes/UserProperty';
|
|
20
21
|
import FormSections from './FormSections';
|
|
21
|
-
import { entryIsVisible,
|
|
22
|
+
import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
|
|
22
23
|
function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
|
|
23
24
|
return {
|
|
24
25
|
inputId: entryId,
|
|
@@ -38,7 +39,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
|
|
|
38
39
|
}
|
|
39
40
|
export function RecursiveEntryRenderer(props) {
|
|
40
41
|
const { entry } = props;
|
|
41
|
-
const {
|
|
42
|
+
const { object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, width, } = useFormContext();
|
|
42
43
|
const { isBelow, breakpoints } = useWidgetSize({
|
|
43
44
|
scroll: false,
|
|
44
45
|
defaultWidth: width,
|
|
@@ -50,19 +51,41 @@ export function RecursiveEntryRenderer(props) {
|
|
|
50
51
|
const entryId = getEntryId(entry) || 'defaultId';
|
|
51
52
|
const display = 'display' in entry ? entry.display : undefined;
|
|
52
53
|
const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
|
|
53
|
-
const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
|
|
54
|
-
const middleObject = fetchedOptions[`${entryId}MiddleObject`];
|
|
55
54
|
const fieldDefinition = useMemo(() => {
|
|
56
55
|
if (!object)
|
|
57
56
|
return undefined;
|
|
58
57
|
return getFieldDefinition(entry, object, parameters);
|
|
59
58
|
}, [entry, parameters, object]);
|
|
60
59
|
const validation = fieldDefinition?.validation || {};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
const { data: middleObject } = useQuery({
|
|
61
|
+
queryKey: [fieldDefinition?.objectId, 'MiddleObject'],
|
|
62
|
+
queryFn: () => fetchMiddleObject(fieldDefinition, apiServices),
|
|
63
|
+
staleTime: Infinity,
|
|
64
|
+
enabled: !!(fieldDefinition?.objectId &&
|
|
65
|
+
fieldDefinition?.type === 'collection' &&
|
|
66
|
+
fieldDefinition?.manyToManyPropertyId),
|
|
67
|
+
meta: {
|
|
68
|
+
errorMessage: 'Failed to fetch middle object: ',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const { data: initialMiddleObjectInstances = [], isLoading: isLoadingInstances } = useQuery({
|
|
72
|
+
queryKey: [
|
|
73
|
+
fieldDefinition?.objectId,
|
|
74
|
+
instance?.id,
|
|
75
|
+
fieldDefinition?.relatedPropertyId,
|
|
76
|
+
'InitialMiddleObjectInstances',
|
|
77
|
+
],
|
|
78
|
+
queryFn: () => fetchInitialMiddleObjectInstances(apiServices, fieldDefinition, instance?.id),
|
|
79
|
+
staleTime: Infinity,
|
|
80
|
+
enabled: !!(fieldDefinition?.objectId &&
|
|
81
|
+
instance?.id &&
|
|
82
|
+
fieldDefinition?.type === 'collection' &&
|
|
83
|
+
fieldDefinition?.manyToManyPropertyId &&
|
|
84
|
+
fieldDefinition?.relatedPropertyId),
|
|
85
|
+
meta: {
|
|
86
|
+
errorMessage: 'Failed to fetch middle object instances: ',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
66
89
|
if (associatedObject?.propertyId === entryId)
|
|
67
90
|
return null;
|
|
68
91
|
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
@@ -103,12 +126,11 @@ export function RecursiveEntryRenderer(props) {
|
|
|
103
126
|
}
|
|
104
127
|
else if (fieldDefinition.type === 'collection') {
|
|
105
128
|
if (fieldDefinition?.manyToManyPropertyId) {
|
|
106
|
-
if (
|
|
107
|
-
return (
|
|
108
|
-
React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances:
|
|
109
|
-
initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: 'criteria' in validation && validation.criteria
|
|
129
|
+
if (!isEmpty(middleObject)) {
|
|
130
|
+
return isLoadingInstances ? (React.createElement(Skeleton, null)) : (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
131
|
+
React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: 'criteria' in validation && validation.criteria
|
|
110
132
|
? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
|
|
111
|
-
: undefined, hasDescription: !!display?.description })))
|
|
133
|
+
: undefined, hasDescription: !!display?.description })));
|
|
112
134
|
}
|
|
113
135
|
else {
|
|
114
136
|
// when in the builder preview, the middle object won't be fetched so instead show an empty field
|