@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.
Files changed (26) hide show
  1. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  2. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
  3. package/dist/published/components/custom/FormV2/FormRendererContainer.js +70 -66
  4. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  5. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  6. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  7. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  8. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +104 -184
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +19 -36
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -20
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -169
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +12 -6
  17. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  18. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +38 -16
  19. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  20. package/dist/published/components/custom/FormV2/components/utils.js +25 -25
  21. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
  22. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
  23. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  24. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +37 -12
  25. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  26. 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
- useEffect(() => {
86
- if (!fetchedOptions[`${id}ViewPermission`]) {
87
- checkPermissions();
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?.id}/instances/${instance?.id}/documents/checkAccess?action=view`);
95
- apiServices
96
- .get(endpoint)
97
- .then((viewPermissionCheck) => {
98
- setFetchedOptions({
99
- [`${id}ViewPermission`]: viewPermissionCheck.result,
100
- });
101
- setHasViewPermission(viewPermissionCheck.result);
102
- })
103
- .catch(() => setHasViewPermission(false));
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 { fetchedOptions, setFetchedOptions, handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
10
+ const { handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
10
11
  const [loadingOptions, setLoadingOptions] = useState(false);
11
12
  const apiServices = useApiServices();
12
- const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
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
- if (!fetchedOptions[`${id}Options`]) {
26
- setLoadingOptions(true);
27
- apiServices.get(getPrefixedUrl(`/users`), (error, userList) => {
28
- setUsers(userList);
29
- setOptions((userList ?? []).map((user) => ({
30
- label: user.name,
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, isEqual, isNil } from 'lodash';
4
+ import { cloneDeep, debounce, isEmpty, isNil } from 'lodash';
4
5
  import Handlebars from 'no-eval-handlebars';
5
- import React, { useCallback, useEffect, useMemo, useRef, useState } from '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
- // setting the default value when there is default criteria
90
- if (!isEmpty(defaultValueCriteria) && !selectedInstance && (!instance || !instance[id])) {
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
- setLoadingOptions(true);
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
- }, [relatedObjectId, defaultValueCriteria, sortBy, orderBy]);
125
- const getDropdownOptions = useCallback(() => {
126
- if (((!fetchedOptions?.[`${id}Options`] ||
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
- // Early return if already fetched
173
- if (fetchedOptions[`${id}Form`])
174
- return;
175
- const fetchForm = async () => {
176
- try {
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
- if (evokeForm) {
182
- setForm(evokeForm);
183
- setFetchedOptions({
184
- [`${id}Form`]: evokeForm,
185
- });
123
+ catch (error) {
124
+ console.error('Failed to update field:', error);
125
+ return;
186
126
  }
187
- }
188
- catch (error) {
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
- else {
201
- setRelatedObject(object);
130
+ catch (error) {
131
+ console.error('Autosave failed:', error);
202
132
  }
203
- });
133
+ })();
204
134
  }
205
- }, [relatedObjectId, fetchedOptions, id]);
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
- (async () => {
208
- if (fetchedOptions[`${id}NavigationSlug`] === undefined) {
209
- const pages = await getDefaultPages([{ ...fieldDefinition, objectId: relatedObjectId }], defaultPages, findDefaultPageSlugFor);
210
- if (relatedObjectId && pages[relatedObjectId]) {
211
- setNavigationSlug(pages[relatedObjectId]);
212
- setFetchedOptions({
213
- [`${id}NavigationSlug`]: pages[relatedObjectId],
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
- }, [id, fieldDefinition, defaultPages, findDefaultPageSlugFor, relatedObjectId, fetchedOptions]);
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
- getDropdownOptions();
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, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError }),
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, setOptions, options, } = props;
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
- setOptions(options.concat([response]));
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,2 +1,3 @@
1
+ import React from 'react';
1
2
  import { EntryRendererProps } from './types';
2
- export declare function RecursiveEntryRenderer(props: EntryRendererProps): any;
3
+ export declare function RecursiveEntryRenderer(props: EntryRendererProps): React.JSX.Element | null;
@@ -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, { useEffect, useMemo } from '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, fetchCollectionData, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
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 { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, width, } = useFormContext();
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
- useEffect(() => {
62
- if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
63
- fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
64
- }
65
- }, [fieldDefinition, instance]);
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 (middleObject && !isEmpty(middleObject)) {
107
- return (initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
108
- React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
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