@evoke-platform/ui-components 1.13.0-dev.5 → 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 (31) hide show
  1. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  2. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
  3. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  4. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
  5. package/dist/published/components/custom/FormV2/FormRendererContainer.js +93 -86
  6. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  7. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  8. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +104 -184
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +51 -32
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +40 -38
  17. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  18. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -169
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +12 -6
  21. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  22. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +39 -17
  23. package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
  24. package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -11
  25. package/dist/published/components/custom/FormV2/components/utils.js +169 -93
  26. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
  27. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
  28. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  29. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +38 -13
  30. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  31. package/package.json +3 -2
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
- import { SavedDocumentReference } from '../../types';
2
+ import { DocumentReference } from '../../types';
3
3
  type DocumentListProps = {
4
- handleChange?: (propertyId: string, value: (File | SavedDocumentReference)[] | undefined) => void;
4
+ handleChange?: (propertyId: string, value: (File | DocumentReference)[] | undefined) => void;
5
5
  onAutosave?: (fieldId: string) => void | Promise<void>;
6
6
  id: string;
7
+ fieldType?: 'document' | 'file';
7
8
  canUpdateProperty: boolean;
8
- value: (File | SavedDocumentReference)[] | undefined;
9
+ value: (File | DocumentReference)[] | undefined;
9
10
  setSnackbarError: (type: 'error' | 'success', message: string) => void;
10
11
  };
11
12
  export declare const DocumentList: (props: DocumentListProps) => React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
- import { isEqual } from 'lodash';
2
+ import { useQuery } from '@tanstack/react-query';
3
3
  import prettyBytes from 'pretty-bytes';
4
4
  import React, { useEffect, useState } from 'react';
5
5
  import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
@@ -24,29 +24,32 @@ const viewableFileTypes = [
24
24
  'text/plain',
25
25
  ];
26
26
  export const DocumentList = (props) => {
27
- const { handleChange, onAutosave, id, canUpdateProperty, value: documents, setSnackbarError } = props;
27
+ const { handleChange, onAutosave, id, fieldType = 'document', canUpdateProperty, value: documents, setSnackbarError, } = props;
28
28
  const apiServices = useApiServices();
29
29
  const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
30
30
  // Determine property type once at component level
31
- const propertyType = object?.properties?.find((p) => p.id === id)?.type;
32
- const isFileType = propertyType === 'file';
33
- const [hasViewPermission, setHasViewPermission] = useState(fetchedOptions[`${id}ViewPermission`] ?? true);
31
+ const isFileType = fieldType === 'file';
34
32
  // savedDocuments is either FileInstance[] or DocumentType[], never a mix
35
33
  const [savedDocuments, setSavedDocuments] = useState(fetchedOptions[`${id}SavedDocuments`]);
36
34
  useEffect(() => {
37
- const currentValue = instance?.[id];
35
+ // Use documents prop (value) as the source of truth, not instance[id]
36
+ // This ensures newly uploaded files trigger a fetch even before they're saved to instance
37
+ const currentValue = documents;
38
38
  if (currentValue?.length) {
39
- const currentDocumentIds = currentValue.map((doc) => doc.id);
40
- if (currentDocumentIds.length &&
41
- // these need to be sorted otherwise it will evaluate as not equal if the ids are in different orders causing unnecessary fetches
42
- !isEqual(currentDocumentIds.slice().sort(), savedDocuments
43
- ?.map((doc) => doc.id)
44
- .slice()
45
- .sort())) {
46
- getDocuments(currentDocumentIds);
39
+ // Filter out File objects only - we want to fetch details for all DocumentReferences including unsaved ones
40
+ const currentDocumentIds = currentValue
41
+ .filter((doc) => !(doc instanceof File))
42
+ .map((doc) => doc.id);
43
+ if (currentDocumentIds.length) {
44
+ // Check if there are any document IDs that we haven't fetched yet
45
+ const savedDocumentIds = savedDocuments?.map((doc) => doc.id) ?? [];
46
+ const missingDocumentIds = currentDocumentIds.filter((id) => !savedDocumentIds.includes(id));
47
+ if (missingDocumentIds.length > 0) {
48
+ getDocuments(currentDocumentIds);
49
+ }
47
50
  }
48
51
  }
49
- }, [id, documents, object]);
52
+ }, [documents, savedDocuments]);
50
53
  useEffect(() => {
51
54
  if (fetchedOptions[`${id}SavedDocuments`]) {
52
55
  setSavedDocuments(fetchedOptions[`${id}SavedDocuments`]);
@@ -79,30 +82,31 @@ export const DocumentList = (props) => {
79
82
  }
80
83
  });
81
84
  };
82
- useEffect(() => {
83
- if (!fetchedOptions[`${id}ViewPermission`]) {
84
- checkPermissions();
85
- }
86
- }, [object]);
87
- const checkPermissions = () => {
88
- if (instance?.[id]?.length) {
89
- apiServices
90
- .get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=view`))
91
- .then((viewPermissionCheck) => {
92
- setFetchedOptions({
93
- [`${id}ViewPermission`]: viewPermissionCheck.result,
94
- });
95
- setHasViewPermission(viewPermissionCheck.result);
96
- });
97
- }
98
- };
85
+ const { data: hasViewPermission = false } = useQuery({
86
+ queryKey: ['hasViewPermission', object?.id, instance?.id],
87
+ queryFn: async () => {
88
+ const endpoint = isFileType
89
+ ? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=read&field=content`)
90
+ : getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/checkAccess?action=view`);
91
+ try {
92
+ const viewPermissionCheck = await apiServices.get(endpoint);
93
+ return viewPermissionCheck.result;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ },
99
+ enabled: !!instance?.id && !!object?.id && !!instance?.[id]?.length,
100
+ staleTime: Infinity,
101
+ });
99
102
  const isFile = (doc) => doc instanceof File;
103
+ const isUnsavedFile = (doc) => isFile(doc) || !!doc.unsaved;
100
104
  const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
101
105
  const handleRemove = async (index) => {
102
106
  const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
103
107
  const newValue = updatedDocuments.length === 0 ? undefined : updatedDocuments;
104
108
  try {
105
- handleChange && (await handleChange(id, newValue));
109
+ handleChange?.(id, newValue);
106
110
  }
107
111
  catch (error) {
108
112
  console.error('Failed to update field:', error);
@@ -124,9 +128,7 @@ export const DocumentList = (props) => {
124
128
  : savedDocuments?.find((savedDocument) => savedDocument.id === doc.id)?.contentType;
125
129
  if (!isFile(doc)) {
126
130
  try {
127
- // Determine property type to use the correct endpoint
128
- const propertyType = object?.properties?.find((p) => p.id === id)?.type;
129
- const contentEndpoint = propertyType === 'file'
131
+ const contentEndpoint = isFileType
130
132
  ? getPrefixedUrl(`/files/${doc.id}/content`)
131
133
  : getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}/content`);
132
134
  const documentResponse = await apiServices.get(contentEndpoint, { responseType: 'blob' });
@@ -189,10 +191,10 @@ export const DocumentList = (props) => {
189
191
  } }, doc.name)),
190
192
  React.createElement(Grid, { item: true, xs: 12 },
191
193
  React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, getDocumentSize(doc)))),
192
- (isFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
194
+ (isUnsavedFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
193
195
  React.createElement(IconButton, { "aria-label": "open document", sx: { ...styles.icon, marginRight: '16px' }, onClick: () => openDocument(index) },
194
196
  React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))),
195
- !isFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
197
+ !isFile(doc) && !isUnsavedFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
196
198
  marginRight: '16px',
197
199
  backgroundColor: 'rgba(222, 48, 36, 0.16)',
198
200
  color: '#A91813',
@@ -1,17 +1,17 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
2
  import { Close, ExpandMore } from '@mui/icons-material';
3
+ import { useQuery } from '@tanstack/react-query';
3
4
  import React, { useEffect, useState } from 'react';
4
5
  import { useFormContext } from '../../../../../theme/hooks';
5
6
  import { Autocomplete, IconButton, Paper, TextField, Typography } from '../../../../core';
6
7
  import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
7
8
  const UserProperty = (props) => {
8
9
  const { id, error, value, readOnly, hasDescription } = props;
9
- const { fetchedOptions, setFetchedOptions, handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
10
+ const { handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
10
11
  const [loadingOptions, setLoadingOptions] = useState(false);
11
12
  const apiServices = useApiServices();
12
- const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
13
+ const [options, setOptions] = useState([]);
13
14
  const [openOptions, setOpenOptions] = useState(false);
14
- const [users, setUsers] = useState();
15
15
  const [userValue, setUserValue] = useState();
16
16
  useEffect(() => {
17
17
  if (value && typeof value == 'object' && 'name' in value && 'id' in value) {
@@ -21,25 +21,21 @@ const UserProperty = (props) => {
21
21
  setUserValue(undefined);
22
22
  }
23
23
  }, [value]);
24
+ const { data: users } = useQuery({
25
+ queryKey: ['users'],
26
+ queryFn: () => apiServices.get(getPrefixedUrl(`/users`)),
27
+ staleTime: Infinity,
28
+ meta: {
29
+ errorMessage: 'Error fetching users: ',
30
+ },
31
+ });
24
32
  useEffect(() => {
25
- if (!fetchedOptions[`${id}Options`]) {
26
- setLoadingOptions(true);
27
- apiServices.get(getPrefixedUrl(`/users`), (error, userList) => {
28
- setUsers(userList);
29
- setOptions((userList ?? []).map((user) => ({
30
- label: user.name,
31
- value: user.id,
32
- })));
33
- setFetchedOptions({
34
- [`${id}Options`]: (userList ?? []).map((user) => ({
35
- label: user.name,
36
- value: user.id,
37
- })),
38
- });
39
- setLoadingOptions(false);
40
- });
41
- }
42
- }, [id]);
33
+ setOptions((users ?? []).map((user) => ({
34
+ label: user.name,
35
+ value: user.id,
36
+ })));
37
+ setLoadingOptions(false);
38
+ }, [users]);
43
39
  async function handleChangeUserProperty(id, value) {
44
40
  const updatedValue = typeof value?.value === 'string' ? { name: value.label, id: value.value } : null;
45
41
  try {
@@ -1,53 +1,41 @@
1
1
  import { useApiServices, useApp, useNavigate, } from '@evoke-platform/context';
2
+ import { useQuery } from '@tanstack/react-query';
2
3
  import cleanDeep from 'clean-deep';
3
- import { cloneDeep, debounce, isEmpty, isEqual, isNil } from 'lodash';
4
+ import { cloneDeep, debounce, isEmpty, isNil } from 'lodash';
4
5
  import Handlebars from 'no-eval-handlebars';
5
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
6
7
  import { Close } from '../../../../../../icons';
7
8
  import { useFormContext } from '../../../../../../theme/hooks';
8
9
  import { Autocomplete, Button, IconButton, Link, ListItem, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
9
10
  import { Box } from '../../../../../layout';
10
- import { getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
11
+ import { getDefaultPages, getPrefixedUrl, transformToWhere, useFormById } from '../../utils';
11
12
  import RelatedObjectInstance from './RelatedObjectInstance';
12
13
  const ObjectPropertyInput = (props) => {
13
14
  const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, relatedObjectId, } = props;
14
15
  const { fetchedOptions, setFetchedOptions, fieldHeight, handleChange: handleChangeObjectField, onAutosave: onAutosave, instance, } = useFormContext();
15
16
  const { defaultPages, findDefaultPageSlugFor } = useApp();
17
+ const [debouncedDropdownInput, setDebouncedDropdownInput] = useState();
16
18
  const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
17
19
  const [openCreateDialog, setOpenCreateDialog] = useState(false);
18
- const [loadingOptions, setLoadingOptions] = useState(false);
19
- const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${id}NavigationSlug`]);
20
- const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${id}RelatedObject`]);
21
20
  const [dropdownInput, setDropdownInput] = useState();
22
21
  const [openOptions, setOpenOptions] = useState(false);
23
- const [hasFetched, setHasFetched] = useState(fetchedOptions[`${id}OptionsHaveFetched`] || false);
24
- const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
25
22
  const [layout, setLayout] = useState(fetchedOptions[`${id}ViewLayout`]);
26
- const [form, setForm] = useState();
27
23
  const [snackbarError, setSnackbarError] = useState({
28
24
  showAlert: false,
29
25
  isError: true,
30
26
  });
27
+ const { data: relatedObject } = useQuery({
28
+ queryKey: [relatedObjectId, 'sanitized'],
29
+ queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`)),
30
+ enabled: !!relatedObjectId,
31
+ staleTime: Infinity,
32
+ });
31
33
  const action = relatedObject?.actions?.find((action) => action.id === createActionId);
32
34
  const apiServices = useApiServices();
33
35
  const navigateTo = useNavigate();
34
- const updatedCriteria = useMemo(() => {
35
- let criteria = filter ? { where: transformToWhere(filter) } : undefined;
36
- if (dropdownInput) {
37
- const nameQuery = transformToWhere({
38
- name: {
39
- like: dropdownInput,
40
- options: 'i',
41
- },
42
- });
43
- criteria = {
44
- ...criteria,
45
- where: criteria?.where ? { and: [criteria.where, nameQuery] } : nameQuery,
46
- };
47
- }
48
- return criteria;
49
- }, [filter, dropdownInput]);
50
36
  const listboxRef = useRef(null);
37
+ const formIdToFetch = formId || action?.defaultFormId;
38
+ const { data: form } = useFormById(formIdToFetch ?? '', apiServices, 'Error fetching form: ');
51
39
  useEffect(() => {
52
40
  if (relatedObject) {
53
41
  let defaultViewLayout;
@@ -85,143 +73,102 @@ const ObjectPropertyInput = (props) => {
85
73
  }
86
74
  }
87
75
  }, [displayOption, relatedObject, viewLayout]);
76
+ const debouncedSetDropdownInput = useMemo(() => debounce((value) => {
77
+ setDebouncedDropdownInput(value);
78
+ }, 200), []);
88
79
  useEffect(() => {
89
- // setting the default value when there is default criteria
90
- if (!isEmpty(defaultValueCriteria) && !selectedInstance && (!instance || !instance[id])) {
80
+ debouncedSetDropdownInput(dropdownInput);
81
+ return () => {
82
+ debouncedSetDropdownInput.cancel();
83
+ };
84
+ }, [dropdownInput, debouncedSetDropdownInput]);
85
+ const updatedCriteria = useMemo(() => {
86
+ let criteria = filter ? { where: transformToWhere(filter) } : undefined;
87
+ if (debouncedDropdownInput) {
88
+ const nameQuery = transformToWhere({
89
+ name: {
90
+ like: debouncedDropdownInput,
91
+ options: 'i',
92
+ },
93
+ });
94
+ criteria = {
95
+ ...criteria,
96
+ where: criteria?.where ? { and: [criteria.where, nameQuery] } : nameQuery,
97
+ };
98
+ }
99
+ return criteria;
100
+ }, [filter, debouncedDropdownInput]);
101
+ const { data: defaultInstances, isLoading: isLoadingDefaultInstances } = useQuery({
102
+ queryKey: ['defaultInstances', relatedObjectId, updatedCriteria],
103
+ queryFn: async () => {
91
104
  const updatedFilter = cleanDeep({
92
105
  where: transformToWhere({ $and: [defaultValueCriteria, updatedCriteria?.where ?? {}] }),
93
106
  order: orderBy && sortBy ? encodeURIComponent(sortBy + ' ' + orderBy) : undefined,
94
107
  limit: 1,
95
108
  });
96
109
  if (updatedFilter.where) {
97
- setLoadingOptions(true);
98
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
99
- if (error) {
100
- console.error(error);
101
- setLoadingOptions(false);
102
- }
103
- if (instances && instances.length > 0) {
104
- setSelectedInstance(instances[0]);
105
- try {
106
- handleChangeObjectField && (await handleChangeObjectField(id, instances[0]));
107
- }
108
- catch (error) {
109
- console.error('Failed to update field:', error);
110
- setLoadingOptions(false);
111
- return;
112
- }
113
- try {
114
- await onAutosave?.(id);
115
- }
116
- catch (error) {
117
- console.error('Autosave failed:', error);
118
- }
119
- }
120
- setLoadingOptions(false);
121
- });
110
+ return apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`));
122
111
  }
123
- }
124
- }, [relatedObjectId, defaultValueCriteria, sortBy, orderBy]);
125
- const getDropdownOptions = useCallback(() => {
126
- if (((!fetchedOptions?.[`${id}Options`] ||
127
- (fetchedOptions?.[`${id}Options`]).length === 0) &&
128
- !hasFetched) ||
129
- !isEqual(fetchedOptions?.[`${id}UpdatedCriteria`], updatedCriteria)) {
130
- setFetchedOptions({ [`${id}UpdatedCriteria`]: updatedCriteria });
131
- setLoadingOptions(true);
132
- const updatedFilter = cloneDeep(updatedCriteria) || {};
133
- updatedFilter.limit = 100;
134
- const { propertyId, direction } = layout?.sort ?? {
135
- propertyId: 'name',
136
- direction: 'asc',
137
- };
138
- updatedFilter.order = `${propertyId} ${direction}`;
139
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
140
- if (error) {
141
- console.error(error);
142
- setLoadingOptions(false);
143
- }
144
- if (instances) {
145
- setOptions(instances);
146
- setLoadingOptions(false);
147
- // so if you go off a section too quickly and it doesn't fetch it re-fetches but doesn't cause an infinite loop
148
- setHasFetched(true);
149
- }
150
- });
151
- }
152
- }, [
153
- relatedObjectId,
154
- updatedCriteria,
155
- layout,
156
- fetchedOptions?.[`${id}Options`],
157
- fetchedOptions?.[`${id}UpdatedCriteria`],
158
- hasFetched,
159
- id,
160
- ]);
161
- const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
162
- useEffect(() => {
163
- if (displayOption === 'dropdown') {
164
- debouncedGetDropdownOptions();
165
- return () => debouncedGetDropdownOptions.cancel();
166
- }
167
- }, [displayOption, debouncedGetDropdownOptions]);
168
- useEffect(() => {
169
- setSelectedInstance(initialValue);
170
- }, [initialValue]);
112
+ },
113
+ enabled: !isEmpty(defaultValueCriteria) && !selectedInstance && (!instance || !instance[id]),
114
+ staleTime: Infinity,
115
+ });
171
116
  useEffect(() => {
172
- // Early return if already fetched
173
- if (fetchedOptions[`${id}Form`])
174
- return;
175
- const fetchForm = async () => {
176
- try {
177
- let evokeForm;
178
- if (formId || action?.defaultFormId) {
179
- evokeForm = await apiServices.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`));
117
+ if (defaultInstances?.[0]) {
118
+ setSelectedInstance(defaultInstances[0]);
119
+ (async () => {
120
+ try {
121
+ handleChangeObjectField && (await handleChangeObjectField(id, defaultInstances[0]));
180
122
  }
181
- if (evokeForm) {
182
- setForm(evokeForm);
183
- setFetchedOptions({
184
- [`${id}Form`]: evokeForm,
185
- });
123
+ catch (error) {
124
+ console.error('Failed to update field:', error);
125
+ return;
186
126
  }
187
- }
188
- catch (error) {
189
- console.error('Error fetching form:', error);
190
- }
191
- };
192
- fetchForm();
193
- }, [action, formId, id, apiServices, fetchedOptions]);
194
- useEffect(() => {
195
- if (!fetchedOptions[`${id}RelatedObject`]) {
196
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`), (error, object) => {
197
- if (error) {
198
- console.error(error);
127
+ try {
128
+ await onAutosave?.(id);
199
129
  }
200
- else {
201
- setRelatedObject(object);
130
+ catch (error) {
131
+ console.error('Autosave failed:', error);
202
132
  }
203
- });
133
+ })();
204
134
  }
205
- }, [relatedObjectId, fetchedOptions, id]);
135
+ }, [defaultInstances]);
136
+ // Construct filter from debounced criteria
137
+ const updatedFilter = useMemo(() => {
138
+ const filter = cloneDeep(updatedCriteria) || {};
139
+ filter.limit = 100;
140
+ const { propertyId, direction } = layout?.sort ?? {
141
+ propertyId: 'name',
142
+ direction: 'asc',
143
+ };
144
+ filter.order = `${propertyId} ${direction}`;
145
+ return filter;
146
+ }, [updatedCriteria, layout]);
147
+ const {
148
+ // Sets the default value of options to an empty array
149
+ data: options = [], isLoading: isLoadingOptions, } = useQuery({
150
+ queryKey: ['dropdownOptions', relatedObjectId, updatedFilter],
151
+ queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`)),
152
+ staleTime: 300000,
153
+ // Keep old data while fetching new data
154
+ placeholderData: (previousData) => previousData,
155
+ });
206
156
  useEffect(() => {
207
- (async () => {
208
- if (fetchedOptions[`${id}NavigationSlug`] === undefined) {
209
- const pages = await getDefaultPages([{ ...fieldDefinition, objectId: relatedObjectId }], defaultPages, findDefaultPageSlugFor);
210
- if (relatedObjectId && pages[relatedObjectId]) {
211
- setNavigationSlug(pages[relatedObjectId]);
212
- setFetchedOptions({
213
- [`${id}NavigationSlug`]: pages[relatedObjectId],
214
- });
215
- }
216
- else {
217
- // setting the nav slug to null if there is no default page for this object to avoid re-fetching
218
- setFetchedOptions({
219
- [`${id}NavigationSlug`]: null,
220
- });
221
- }
157
+ setSelectedInstance(initialValue);
158
+ }, [initialValue]);
159
+ const loadingOptions = isLoadingOptions || isLoadingDefaultInstances;
160
+ const { data: navigationSlug } = useQuery({
161
+ queryKey: ['navigationSlug', id, relatedObjectId],
162
+ queryFn: async () => {
163
+ const pages = await getDefaultPages([{ ...fieldDefinition, objectId: relatedObjectId }], defaultPages, findDefaultPageSlugFor);
164
+ if (relatedObjectId && pages[relatedObjectId]) {
165
+ return pages[relatedObjectId];
222
166
  }
223
- })();
224
- }, [id, fieldDefinition, defaultPages, findDefaultPageSlugFor, relatedObjectId, fetchedOptions]);
167
+ return null;
168
+ },
169
+ enabled: !!(relatedObjectId && defaultPages && findDefaultPageSlugFor),
170
+ staleTime: Infinity,
171
+ });
225
172
  const handleClose = () => {
226
173
  setOpenCreateDialog(false);
227
174
  };
@@ -229,27 +176,6 @@ const ObjectPropertyInput = (props) => {
229
176
  const template = Handlebars.compileAST(expression);
230
177
  return instance ? template(instance) : undefined;
231
178
  };
232
- useEffect(() => {
233
- if (relatedObject && !fetchedOptions[`${id}RelatedObject`]) {
234
- setFetchedOptions({
235
- [`${id}RelatedObject`]: relatedObject,
236
- });
237
- }
238
- if (options &&
239
- (!fetchedOptions[`${id}Options`] || fetchedOptions[`${id}Options`].length === 0) &&
240
- hasFetched &&
241
- !fetchedOptions[`${id}OptionsHaveFetched`]) {
242
- setFetchedOptions({
243
- [`${id}Options`]: options,
244
- [`${id}OptionsHaveFetched`]: hasFetched,
245
- });
246
- }
247
- if (navigationSlug && !fetchedOptions[`${id}NavigationSlug`]) {
248
- setFetchedOptions({
249
- [`${id}NavigationSlug`]: navigationSlug,
250
- });
251
- }
252
- }, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, id]);
253
179
  const dropdownOptions = [
254
180
  ...options.map((o) => ({ label: o.name, value: o.id })),
255
181
  ...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === createActionId)
@@ -422,7 +348,7 @@ const ObjectPropertyInput = (props) => {
422
348
  }
423
349
  }, selectOnFocus: false, onBlur: () => {
424
350
  if (dropdownInput) {
425
- getDropdownOptions();
351
+ setDropdownInput(undefined);
426
352
  }
427
353
  }, renderInput: (params) => (React.createElement(TextField, { ...params, placeholder: selectedInstance?.id || readOnly ? '' : 'Select', readOnly: !loadingOptions && !selectedInstance?.id && readOnly, onChange: (event) => setDropdownInput(event.target.value), sx: {
428
354
  ...(!loadingOptions && selectedInstance?.id
@@ -529,7 +455,7 @@ const ObjectPropertyInput = (props) => {
529
455
  event.stopPropagation();
530
456
  setOpenCreateDialog(true);
531
457
  }, "aria-label": `Add` }, "Add")))),
532
- React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError }),
458
+ React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, filter: updatedCriteria, layout: layout, formId: formIdToFetch, actionId: createActionId, setSnackbarError: setSnackbarError }),
533
459
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
534
460
  isError: snackbarError.isError,
535
461
  showAlert: false,
@@ -15,8 +15,6 @@ export type RelatedObjectInstanceProps = BaseProps & {
15
15
  isError: boolean;
16
16
  }>>;
17
17
  displayOption?: 'dropdown' | 'dialogBox';
18
- setOptions: (options: ObjectInstance[]) => void;
19
- options: ObjectInstance[];
20
18
  filter?: Record<string, unknown>;
21
19
  layout?: TableViewLayout;
22
20
  formId?: string;
@@ -1,5 +1,6 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
2
  import { DialogActions } from '@mui/material';
3
+ import { useQueryClient } from '@tanstack/react-query';
3
4
  import { isEmpty } from 'lodash';
4
5
  import React, { useCallback, useRef, useState } from 'react';
5
6
  import { Close } from '../../../../../../icons';
@@ -29,7 +30,7 @@ const styles = {
29
30
  },
30
31
  };
31
32
  const RelatedObjectInstance = (props) => {
32
- const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError, setOptions, options, } = props;
33
+ const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError, } = props;
33
34
  const { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
34
35
  const [selectedRow, setSelectedRow] = useState();
35
36
  const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
@@ -40,6 +41,7 @@ const RelatedObjectInstance = (props) => {
40
41
  defaultWidth: width,
41
42
  });
42
43
  const { isXs, isSm } = breakpoints;
44
+ const queryClient = useQueryClient();
43
45
  const linkExistingInstance = async () => {
44
46
  if (selectedRow && handleChangeObjectField) {
45
47
  setSelectedInstance(selectedRow);
@@ -93,7 +95,11 @@ const RelatedObjectInstance = (props) => {
93
95
  message: 'New instance created',
94
96
  isError: false,
95
97
  });
96
- setOptions(options.concat([response]));
98
+ // Clear option cache to then fetch newly created instance
99
+ queryClient.invalidateQueries({
100
+ queryKey: ['dropdownOptions', relatedObject.id],
101
+ exact: false,
102
+ });
97
103
  onClose();
98
104
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
105
  }
@@ -113,14 +119,14 @@ const RelatedObjectInstance = (props) => {
113
119
  }
114
120
  };
115
121
  const shouldShowRadioButtons = displayOption !== 'dropdown' && mode !== 'existingOnly' && mode !== 'newOnly' && actionId;
116
- const RadioButtons = () => shouldShowRadioButtons ? (React.createElement(RadioGroup, { row: true, "aria-label": "Relation Type", onChange: (event) => {
122
+ const RadioButtons = useCallback(() => shouldShowRadioButtons ? (React.createElement(RadioGroup, { row: true, "aria-label": "Relation Type", onChange: (event) => {
117
123
  const { value } = event.target;
118
124
  if (value === 'new' || value === 'existing') {
119
125
  setRelationType(value);
120
126
  }
121
127
  }, value: relationType },
122
128
  React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
123
- React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null;
129
+ React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null, [shouldShowRadioButtons, relationType]);
124
130
  const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: relatedObject.id, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
125
131
  relationType === 'new' ? (React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(FormRenderer.ValidationErrors, { errors: bodyProps.errors, sx: {
126
132
  my: isSm || isXs ? 2 : 3,
@@ -146,7 +152,7 @@ const RelatedObjectInstance = (props) => {
146
152
  maxWidth: '950px',
147
153
  width: '100%',
148
154
  },
149
- } },
155
+ } }, open && (React.createElement(React.Fragment, null,
150
156
  React.createElement(DialogTitle, { sx: {
151
157
  padding: 3,
152
158
  borderBottom: '1px solid #e9ecef',
@@ -176,6 +182,6 @@ const RelatedObjectInstance = (props) => {
176
182
  marginLeft: '8px',
177
183
  width: '85px',
178
184
  '&:hover': { boxShadow: 'none' },
179
- }, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))))));
185
+ }, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))))))));
180
186
  };
181
187
  export default RelatedObjectInstance;
@@ -1,2 +1,3 @@
1
+ import React from 'react';
1
2
  import { EntryRendererProps } from './types';
2
- export declare function RecursiveEntryRenderer(props: EntryRendererProps): any;
3
+ export declare function RecursiveEntryRenderer(props: EntryRendererProps): React.JSX.Element | null;