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