@evoke-platform/ui-components 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +2 -2
  2. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +0 -15
  3. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +0 -10
  4. package/dist/published/components/custom/CriteriaBuilder/utils.js +2 -161
  5. package/dist/published/components/custom/Form/utils.js +3 -2
  6. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  7. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
  8. package/dist/published/components/custom/FormV2/FormRendererContainer.js +131 -100
  9. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  10. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +105 -185
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +18 -26
  17. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +18 -18
  18. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +96 -169
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +57 -13
  22. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  23. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +61 -29
  24. package/dist/published/components/custom/FormV2/components/utils.d.ts +23 -4
  25. package/dist/published/components/custom/FormV2/components/utils.js +136 -26
  26. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +28 -14
  27. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +40 -46
  28. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  29. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +56 -19
  30. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  31. package/dist/published/components/custom/index.d.ts +2 -0
  32. package/dist/published/components/custom/index.js +1 -0
  33. package/dist/published/components/custom/types.d.ts +15 -0
  34. package/dist/published/components/custom/types.js +1 -0
  35. package/dist/published/components/custom/util.d.ts +10 -0
  36. package/dist/published/components/custom/util.js +161 -1
  37. package/dist/published/index.d.ts +2 -2
  38. package/dist/published/index.js +1 -1
  39. package/package.json +3 -4
@@ -1,23 +1,29 @@
1
1
  import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
2
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
2
3
  import axios from 'axios';
3
4
  import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set } from 'lodash';
4
- import React, { useEffect, useRef, useState } from 'react';
5
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
5
6
  import { Skeleton, Snackbar } from '../../core';
6
7
  import { Box } from '../../layout';
7
8
  import ErrorComponent from '../ErrorComponent';
9
+ import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
8
10
  import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
9
11
  import Header from './components/Header';
10
- import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
12
+ import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
11
13
  import FormRenderer from './FormRenderer';
14
+ // Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
12
15
  function FormRendererContainer(props) {
13
- const { instanceId, pageNavigation, dataType, title, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
16
+ return (React.createElement(ConditionalQueryClientProvider, null,
17
+ React.createElement(FormRendererContainerInner, { ...props })));
18
+ }
19
+ // Inner component that assumes QueryClient context is available
20
+ function FormRendererContainerInner(props) {
21
+ const { instanceId, pageNavigation, title, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
14
22
  const apiServices = useApiServices();
15
23
  const navigateTo = useNavigate();
24
+ const queryClient = useQueryClient();
16
25
  const { id: appId } = useApp();
17
- const [sanitizedObject, setSanitizedObject] = useState();
18
- const [navigationSlug, setNavigationSlug] = useState();
19
26
  const [parameters, setParameters] = useState();
20
- const [instance, setInstance] = useState();
21
27
  const formDataRef = useRef();
22
28
  // We only need the setter to force a re-render when form data updates; the value itself
23
29
  // is intentionally not referenced elsewhere to avoid stale reads (we use formDataRef).
@@ -46,6 +52,8 @@ function FormRendererContainer(props) {
46
52
  });
47
53
  const [isSaving, setIsSaving] = useState(false);
48
54
  const [lastSavedData, setLastSavedData] = useState({});
55
+ const [uniquePresetValues, setUniquePresetValues] = useState([]);
56
+ const flattenFormEntries = useMemo(() => getUnnestedEntries(form?.entries || []), [form?.entries]);
49
57
  const userAccount = useAuthenticationContext()?.account;
50
58
  const objectStore = useObject(form?.objectId ?? objectId);
51
59
  const onError = (err) => {
@@ -53,68 +61,84 @@ function FormRendererContainer(props) {
53
61
  setSnackbarError({ ...snackbarError, isError: true });
54
62
  setError(code ?? err);
55
63
  };
64
+ const { data: sanitizedObject, error: sanitizedObjectError } = useQuery({
65
+ queryKey: [form?.objectId ?? objectId, ...(instanceId ? [instanceId] : []), 'sanitized'],
66
+ queryFn: () =>
67
+ // form?.objectId is needed for subtype forms to get the correct object
68
+ apiServices.get(getPrefixedUrl(`/objects/${form?.objectId ?? objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } }),
69
+ staleTime: Infinity,
70
+ enabled: !!(form?.objectId || objectId),
71
+ });
72
+ // trigger refetch on success
73
+ const { data: instance, error: instanceError } = useQuery({
74
+ queryKey: [objectId, instanceId, 'instance'],
75
+ queryFn: async () => {
76
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}`), {
77
+ params: {
78
+ expand: uniquePresetValues
79
+ .filter((value) => value.startsWith('{{{input.') ||
80
+ value.startsWith('{{input.') ||
81
+ value.startsWith('{{{instance.'))
82
+ .map((value) => {
83
+ return value
84
+ .replace(/{{{|}}}|{{|}}/g, '')
85
+ .split('.')
86
+ .slice(1)
87
+ .join('.');
88
+ }),
89
+ },
90
+ });
91
+ return instance;
92
+ },
93
+ staleTime: Infinity,
94
+ enabled: !!instanceId && !!sanitizedObject,
95
+ });
56
96
  useEffect(() => {
57
- (async () => {
58
- try {
59
- if (instanceId) {
60
- const instance = await objectStore.getInstance(instanceId);
61
- setInstance(instance);
62
- }
63
- const object = await apiServices.get(getPrefixedUrl(`/objects/${form?.objectId || objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } });
64
- setSanitizedObject(object);
65
- const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
66
- if (action && (instanceId || action.type === 'create')) {
67
- setAction(action);
68
- // Clear error if action is found after being missing
69
- // TODO: This entire effect should take place after form is fetched to avoid an error flickering
70
- // That is, this effect should be merged with the one below that fetches the form
71
- setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
72
- }
73
- else {
74
- setError('Action could not be found');
75
- }
76
- }
77
- catch (error) {
78
- onError(error);
79
- }
80
- })();
81
- }, [dataType, form, instanceId]);
97
+ if (!sanitizedObject)
98
+ return;
99
+ const allCriterias = extractAllCriteria(flattenFormEntries, parameters || []);
100
+ const uniquePresetValues = new Set();
101
+ for (const criteria of allCriterias) {
102
+ const presetValues = extractPresetValuesFromCriteria(criteria);
103
+ presetValues.forEach((value) => uniquePresetValues.add(value));
104
+ }
105
+ extractPresetValuesFromDynamicDefaultValues(flattenFormEntries).map((value) => uniquePresetValues.add(value));
106
+ setUniquePresetValues(Array.from(uniquePresetValues));
107
+ const action = sanitizedObject.actions?.find((a) => a.id === (form?.actionId || actionId));
108
+ if (action && (instanceId || action.type === 'create')) {
109
+ setAction(action);
110
+ // Clear error if action is found after being missing
111
+ setError((prevError) => (prevError === 'Action could not be found' ? undefined : prevError));
112
+ }
113
+ else {
114
+ setError('Action could not be found');
115
+ }
116
+ }, [sanitizedObject, actionId, form?.actionId, instanceId]);
117
+ const { data: navigationSlug } = useQuery({
118
+ queryKey: [appId, 'navigationSlug'],
119
+ queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
120
+ select: (page) => page.slug,
121
+ staleTime: Infinity,
122
+ enabled: !!pageNavigation,
123
+ });
124
+ const formIdToFetch = formId || action?.defaultFormId;
125
+ const { data: fetchedForm, error: fetchedFormError } = useFormById(formIdToFetch ?? '', apiServices);
82
126
  useEffect(() => {
83
- if (pageNavigation) {
84
- apiServices
85
- .get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`))
86
- .then((page) => {
87
- setNavigationSlug(page?.slug);
88
- });
127
+ if (!formIdToFetch && action) {
128
+ setError('Action form could not be found');
89
129
  }
90
- }, []);
130
+ }, [formIdToFetch, action]);
91
131
  useEffect(() => {
92
- const needsInstance = action?.type !== 'create' && !!instanceId;
93
- // Instance and Action are loaded in the side effect above; wait for them to complete.
94
- const loading = (actionId && !action) || (needsInstance && !instance);
95
- if (form || loading)
96
- return;
97
- if ((formId || action?.defaultFormId) && formId !== '_auto_') {
98
- apiServices
99
- .get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
100
- .then((evokeForm) => {
101
- // If an actionId is provided, ensure it matches the form's actionId
102
- if (!actionId || evokeForm?.actionId === actionId) {
103
- const form = evokeForm;
104
- setForm(form);
105
- }
106
- else {
107
- setError('Configured action ID does not match form action ID');
108
- }
109
- })
110
- .catch((error) => {
111
- onError(error);
112
- });
132
+ if (fetchedForm) {
133
+ if (actionId && fetchedForm.actionId !== actionId) {
134
+ setError('Configured action ID does not match form action ID');
135
+ }
136
+ setForm(fetchedForm);
113
137
  }
114
- else if (action?.type === 'delete' && formId === '_auto_') {
138
+ else if (action?.type === 'delete' && formId === '_auto_' && instance) {
115
139
  setForm({
116
140
  id: '',
117
- name: '',
141
+ name: 'Delete',
118
142
  entries: [
119
143
  {
120
144
  type: 'content',
@@ -128,10 +152,12 @@ function FormRendererContainer(props) {
128
152
  },
129
153
  });
130
154
  }
131
- else {
132
- setError('Action form could not be found');
133
- }
134
- }, [action, actionId, objectId, instance]);
155
+ }, [fetchedForm, instance, action, formId]);
156
+ useEffect(() => {
157
+ const error = sanitizedObjectError || fetchedFormError || instanceError;
158
+ if (error)
159
+ onError(error);
160
+ }, [sanitizedObjectError, fetchedFormError, instanceError]);
135
161
  useEffect(() => {
136
162
  if (!form)
137
163
  return;
@@ -141,15 +167,15 @@ function FormRendererContainer(props) {
141
167
  }, [form, action?.parameters, sanitizedObject]);
142
168
  useEffect(() => {
143
169
  const getInitialValues = async () => {
144
- if (form && (instance || !instanceId)) {
145
- const defaultValues = await getDefaultValues(form.entries, instance || {});
170
+ if (flattenFormEntries.length && (instance || !instanceId)) {
171
+ const defaultValues = await getDefaultValues(flattenFormEntries, instance || {});
146
172
  setFormData(defaultValues);
147
173
  // Deep clone to avoid reference issues
148
174
  setLastSavedData(cloneDeep(defaultValues));
149
175
  }
150
176
  };
151
177
  getInitialValues();
152
- }, [form, instance, sanitizedObject]);
178
+ }, [instanceId, instance, flattenFormEntries]);
153
179
  const onSubmissionSuccess = (updatedInstance) => {
154
180
  setSnackbarError({
155
181
  showAlert: true,
@@ -158,14 +184,19 @@ function FormRendererContainer(props) {
158
184
  });
159
185
  if (navigationSlug) {
160
186
  if (navigationSlug.includes(':instanceId')) {
161
- const navigateInstanceId = action?.type === 'create' ? updatedInstance?.id : instanceId;
187
+ const navigateInstanceId = action?.type === 'create' ? updatedInstance.id : instanceId;
162
188
  navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', navigateInstanceId ?? ':instanceId')}`);
163
189
  }
164
190
  else {
165
191
  navigateTo(`/${appId}/${navigationSlug}`);
166
192
  }
167
193
  }
168
- setInstance(updatedInstance);
194
+ if (instanceId) {
195
+ // Invalidate the instance to fetch the latest version
196
+ queryClient.invalidateQueries({
197
+ queryKey: [objectId, instanceId, 'instance'],
198
+ });
199
+ }
169
200
  };
170
201
  const linkFiles = async (submission, linkTo) => {
171
202
  // Create file links for any uploaded files after instance creation
@@ -227,9 +258,8 @@ function FormRendererContainer(props) {
227
258
  throw error; // Throw error so caller knows submission failed
228
259
  }
229
260
  };
230
- const getDefaultValues = async (entries, instanceData) => {
261
+ const getDefaultValues = async (unnestedEntries, instanceData) => {
231
262
  const result = {};
232
- const unnestedEntries = getUnnestedEntries(entries);
233
263
  for (const entry of unnestedEntries) {
234
264
  const fieldId = getEntryId(entry);
235
265
  if (!fieldId)
@@ -265,34 +295,36 @@ function FormRendererContainer(props) {
265
295
  console.error(error);
266
296
  }
267
297
  }
268
- else if (entry.type !== 'readonlyField' && isEmptyWithDefault(fieldValue, entry, instanceData)) {
269
- if (fieldId && parameters && parameters.length > 0) {
270
- const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
271
- for (const { fieldId, fieldValue } of defaultValuesArray) {
272
- const parameter = parameters?.find((param) => param.id === fieldId);
273
- if (parameter?.type === 'object') {
274
- const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
275
- for (const field of dependentFields) {
276
- set(result, field.fieldId, field.fieldValue);
298
+ else if (entry.type !== 'readonlyField') {
299
+ if (isEmptyWithDefault(fieldValue, entry, instanceData)) {
300
+ if (fieldId && parameters && parameters.length > 0) {
301
+ const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
302
+ for (const { fieldId, fieldValue } of defaultValuesArray) {
303
+ const parameter = parameters?.find((param) => param.id === fieldId);
304
+ if (parameter?.type === 'object') {
305
+ const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
306
+ for (const field of dependentFields) {
307
+ set(result, field.fieldId, field.fieldValue);
308
+ }
277
309
  }
310
+ set(result, fieldId, fieldValue);
278
311
  }
279
- set(result, fieldId, fieldValue);
280
312
  }
281
313
  }
282
- }
283
- else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
284
- result[fieldId] = false;
285
- }
286
- else if (fieldValue !== undefined && fieldValue !== null) {
287
- if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
288
- let RTFFieldValue = fieldValue;
289
- if (!fieldValue.trim().startsWith('{\\rtf')) {
290
- RTFFieldValue = plainTextToRtf(fieldValue);
291
- }
292
- result[fieldId] = RTFFieldValue;
314
+ else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
315
+ result[fieldId] = false;
293
316
  }
294
- else {
295
- result[fieldId] = fieldValue;
317
+ else if (fieldValue !== undefined && fieldValue !== null) {
318
+ if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
319
+ let RTFFieldValue = fieldValue;
320
+ if (!fieldValue.trim().startsWith('{\\rtf')) {
321
+ RTFFieldValue = plainTextToRtf(fieldValue);
322
+ }
323
+ result[fieldId] = RTFFieldValue;
324
+ }
325
+ else {
326
+ result[fieldId] = fieldValue;
327
+ }
296
328
  }
297
329
  }
298
330
  }
@@ -349,15 +381,14 @@ function FormRendererContainer(props) {
349
381
  const onAutosave = form?.autosaveActionId ? handleAutosave : undefined;
350
382
  async function onChange(id, value) {
351
383
  const parameter = parameters?.find((param) => param.id === id);
352
- const entries = getUnnestedEntries(form.entries);
353
- const isReadOnlyField = entries.some((e) => e.type === 'readonlyField' && e.propertyId === id) &&
354
- !entries.some((e) => (e.type === 'input' && e.parameterId === id) || (e.type === 'inputField' && e.input.id === id));
384
+ const isReadOnlyField = flattenFormEntries.some((e) => e.type === 'readonlyField' && e.propertyId === id) &&
385
+ !flattenFormEntries.some((e) => (e.type === 'input' && e.parameterId === id) || (e.type === 'inputField' && e.input.id === id));
355
386
  if (isReadOnlyField)
356
387
  return;
357
388
  if (parameter) {
358
389
  if (parameter.type === 'object' && parameters && parameters.length > 0) {
359
390
  // On change of a related object, update default values dependent on that object
360
- const dependentFields = await processValueUpdate(entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
391
+ const dependentFields = await processValueUpdate(flattenFormEntries, parameters, value, apiServices, id, formDataRef.current, userAccount);
361
392
  for (const field of dependentFields) {
362
393
  onChange(field.fieldId, field.fieldValue);
363
394
  }
@@ -387,7 +418,7 @@ function FormRendererContainer(props) {
387
418
  ? onDiscardChangesOverride
388
419
  : async () => {
389
420
  if (form) {
390
- const defaultValues = await getDefaultValues(form.entries, instance || {});
421
+ const defaultValues = await getDefaultValues(flattenFormEntries, instance || {});
391
422
  setFormData(defaultValues);
392
423
  }
393
424
  };
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ declare function ConditionalQueryClientProvider({ children }: {
3
+ children: React.ReactNode;
4
+ }): React.JSX.Element;
5
+ export default ConditionalQueryClientProvider;
@@ -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 { debounce, isArray, isEmpty, isEqual } from 'lodash';
3
- import React, { useCallback, useEffect, useState } from 'react';
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, initialMiddleObjectInstances, middleObject, hasDescription, viewLayout, } = props;
11
- const { fetchedOptions, setFetchedOptions, instance } = useFormContext();
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 [endObject, setEndObject] = useState(fetchedOptions[`${fieldDefinition.id}EndObject`]);
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
- const endObjectProperty = middleObject?.properties?.find((currProperty) => fieldDefinition.manyToManyPropertyId === currProperty.id);
56
- if (endObjectProperty && endObjectProperty.objectId && !fetchedOptions[`${fieldDefinition.id}EndObject`]) {
57
- setLayoutLoaded(false);
58
- apiServices.get(getPrefixedUrl(`/objects/${endObjectProperty.objectId}/effective`), { params: { filter: { fields: ['id', 'name', 'properties', 'viewLayout'] } } }, (error, effectiveObject) => {
59
- if (error) {
60
- console.error(error);
61
- }
62
- else {
63
- // If there's no error then the effective object is defined.
64
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
65
- const endObject = effectiveObject;
66
- setEndObject(endObject);
67
- let defaultLayout;
68
- if (endObject.viewLayout?.dropdown) {
69
- defaultLayout = {
70
- id: 'default',
71
- name: 'Default',
72
- objectId: endObject.id,
73
- ...endObject.viewLayout.dropdown,
74
- };
75
- }
76
- if (viewLayout) {
77
- apiServices
78
- .get(getPrefixedUrl(`/objects/${viewLayout.objectId}/dropdownLayouts/${viewLayout.id}`))
79
- .then(setLayout)
80
- .catch(() => setLayout(defaultLayout))
81
- .finally(() => setLayoutLoaded(true));
82
- }
83
- else {
84
- setLayout(defaultLayout);
85
- setLayoutLoaded(true);
86
- }
87
- }
88
- });
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 (!fetchedOptions[`${fieldDefinition.id}EndObjectInstances`] ||
146
- (isArray(fetchedOptions[`${fieldDefinition.id}EndObjectInstances`]) &&
147
- fetchedOptions[`${fieldDefinition.id}EndObjectInstances`].length === 0)) {
148
- setFetchedOptions({
149
- [`${fieldDefinition.id}EndObjectInstances`]: endObjectInstances,
150
- [`${fieldDefinition.id}EndObjectInstancesHaveFetched`]: hasFetched,
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 (!fetchedOptions[`${fieldDefinition.id}EndObject`]) {
154
- setFetchedOptions({
155
- [`${fieldDefinition.id}EndObject`]: endObject,
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
- if (!isEqual(middleObjectInstances, initialMiddleObjectInstances)) {
159
- setFetchedOptions({
160
- [`${fieldDefinition.id}MiddleObjectInstances`]: middleObjectInstances,
161
- });
88
+ else {
89
+ setLayout(defaultLayout);
90
+ setLayoutLoaded(true);
162
91
  }
163
- }, [endObjectInstances, endObject, middleObjectInstances]);
92
+ }, [endObject, viewLayout]);
93
+ const debouncedSetSearchValue = useMemo(() => debounce((value) => {
94
+ setDebouncedSearchValue(value);
95
+ }, 200), []);
164
96
  useEffect(() => {
165
- const updateFetchedOptions = (key, value) => {
166
- if (!fetchedOptions[key]) {
167
- setFetchedOptions({ [key]: value });
168
- }
97
+ debouncedSetSearchValue(searchValue);
98
+ return () => {
99
+ debouncedSetSearchValue.cancel();
169
100
  };
170
- updateFetchedOptions(`${fieldDefinition.id}EndObjectInstances`, endObjectInstances);
171
- updateFetchedOptions(`${fieldDefinition.id}EndObjectInstancesHaveFetched`, hasFetched);
172
- updateFetchedOptions(`${fieldDefinition.id}EndObject`, endObject);
173
- if (!isEqual(middleObjectInstances, initialMiddleObjectInstances)) {
174
- setFetchedOptions({ [`${fieldDefinition.id}MiddleObjectInstances`]: middleObjectInstances });
175
- }
176
- }, [
177
- endObjectInstances,
178
- endObject,
179
- middleObjectInstances,
180
- fetchedOptions,
181
- fieldDefinition.id,
182
- hasFetched,
183
- initialMiddleObjectInstances,
184
- setFetchedOptions,
185
- ]);
186
- useEffect(() => {
187
- debouncedEndObjectSearch(searchValue);
188
- return () => debouncedEndObjectSearch.cancel();
189
- }, [searchValue, debouncedEndObjectSearch]);
190
- useEffect(() => {
191
- if (layoutLoaded) {
192
- fetchEndObjectInstances();
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
- }, [fetchEndObjectInstances, layoutLoaded]);
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, middleObjectInstances: middleObjectInstances, endObjectInstances: endObjectInstances ?? [], endObject: endObject, searchValue: searchValue, loading: loading, handleSaveMiddleInstance: saveMiddleInstance, handleRemoveMiddleInstance: removeMiddleInstance, setSearchValue: setSearchValue, setSnackbarError: setSnackbarError, snackbarError: snackbarError, selectedOptions: selectedOptions, setSelectedOptions: setSelectedOptions, setDropdownSelections: setDropDownSelections, hasDescription: hasDescription }))));
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;