@evoke-platform/ui-components 1.13.0-dev.7 → 1.14.0

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