@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,7 +1,8 @@
1
1
  import { useApiServices, useNotification, } from '@evoke-platform/context';
2
- import { get, isEqual, omit, startCase } from 'lodash';
2
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
3
+ import { get, omit, startCase } from 'lodash';
3
4
  import { DateTime } from 'luxon';
4
- import React, { useCallback, useEffect, useState } from 'react';
5
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
5
6
  import sift from 'sift';
6
7
  import { Edit, ExpandMoreOutlined, TrashCan } from '../../../../../../icons';
7
8
  import useWidgetSize, { useFormContext } from '../../../../../../theme/hooks';
@@ -9,7 +10,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Skel
9
10
  import { Box } from '../../../../../layout';
10
11
  import { getReadableQuery } from '../../../../CriteriaBuilder';
11
12
  import { retrieveCustomErrorMessage } from '../../../../Form/utils';
12
- import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, transformToWhere, } from '../../utils';
13
+ import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, transformToWhere, useFormById, } from '../../utils';
13
14
  import { ActionDialog } from './ActionDialog';
14
15
  import { DocumentViewerCell } from './DocumentViewerCell';
15
16
  const styles = {
@@ -35,7 +36,7 @@ const styles = {
35
36
  };
36
37
  const RepeatableField = (props) => {
37
38
  const { fieldDefinition, canUpdateProperty, criteria, viewLayout, entry } = props;
38
- const { fetchedOptions, setFetchedOptions, instance, width } = useFormContext();
39
+ const { instance, width } = useFormContext();
39
40
  const { isBelow } = useWidgetSize({
40
41
  scroll: false,
41
42
  defaultWidth: width,
@@ -43,106 +44,71 @@ const RepeatableField = (props) => {
43
44
  const smallerThanMd = isBelow('md');
44
45
  const { instanceChanges } = useNotification();
45
46
  const apiServices = useApiServices();
46
- const [reloadOnErrorTrigger, setReloadOnErrorTrigger] = useState(true);
47
47
  const [criteriaObjects, setCriteriaObjects] = useState([]);
48
48
  const [selectedInstanceId, setSelectedInstanceId] = useState();
49
49
  const [dialogType, setDialogType] = useState();
50
50
  const [openDialog, setOpenDialog] = useState(false);
51
- const [users, setUsers] = useState(fetchedOptions[`${fieldDefinition.id}Users`] || []);
52
- const [error, setError] = useState(false);
53
- const [relatedInstances, setRelatedInstances] = useState(fetchedOptions[`${fieldDefinition.id}Options`] || []);
54
- const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${fieldDefinition.id}RelatedObject`]);
55
- const [hasCreateAction, setHasCreateAction] = useState(fetchedOptions[`${fieldDefinition.id}HasCreateAction`] || false);
56
- const [loading, setLoading] = useState((relatedObject && relatedInstances) || !fieldDefinition ? false : true);
57
- const [tableViewLayout, setTableViewLayout] = useState(fetchedOptions[`${fieldDefinition.id}TableViewLayout`]);
58
- const [createForm, setCreateForm] = useState(fetchedOptions[`${fieldDefinition.id}-createForm`]);
59
- const [updateForm, setUpdateForm] = useState(fetchedOptions[`${fieldDefinition.id}-updateForm`]);
60
- const [deleteForm, setDeleteForm] = useState(fetchedOptions[`${fieldDefinition.id}-deleteForm`]);
61
51
  const [snackbarError, setSnackbarError] = useState({
62
52
  showAlert: false,
63
53
  isError: false,
64
54
  });
55
+ const queryClient = useQueryClient();
56
+ const { data: relatedObject, isLoading: loadingRelatedObject } = useQuery({
57
+ queryKey: [fieldDefinition?.objectId, 'effective'],
58
+ queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective`)),
59
+ staleTime: Infinity,
60
+ enabled: !!fieldDefinition.objectId,
61
+ });
62
+ const { data: tableViewLayout, isFetching: isLayoutFetching } = useQuery({
63
+ queryKey: ['tableViewLayout', viewLayout?.id],
64
+ enabled: !!viewLayout?.id && !!relatedObject,
65
+ staleTime: Infinity,
66
+ placeholderData: () => {
67
+ return relatedObject?.viewLayout?.table
68
+ ? {
69
+ id: 'default',
70
+ name: 'Default',
71
+ objectId: relatedObject.id,
72
+ ...relatedObject.viewLayout.table,
73
+ }
74
+ : undefined;
75
+ },
76
+ queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${viewLayout.objectId}/tableLayouts/${viewLayout.id}`)),
77
+ });
78
+ const transformedCriteria = useMemo(() => (criteria ? transformToWhere(criteria) : {}), [criteria]);
79
+ const relatedInstancesQueryKey = [
80
+ instance?.id,
81
+ 'object',
82
+ fieldDefinition?.objectId,
83
+ 'instances',
84
+ fieldDefinition?.relatedPropertyId,
85
+ transformedCriteria,
86
+ ];
87
+ const { data: relatedInstances = [], refetch: refetchRelatedInstances, isLoading: loadingRelatedInstances, isError: relatedInstancesError, } = useQuery({
88
+ queryKey: relatedInstancesQueryKey,
89
+ queryFn: async () => {
90
+ const filterProperty = `${fieldDefinition.relatedPropertyId}.id`;
91
+ const filter = {
92
+ where: { [filterProperty]: instance?.id, ...transformedCriteria },
93
+ limit: 100,
94
+ };
95
+ const instances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
96
+ params: { filter: JSON.stringify(filter) },
97
+ });
98
+ return instances;
99
+ },
100
+ enabled: !!fieldDefinition.relatedPropertyId && !!fieldDefinition.objectId && !!instance?.id,
101
+ staleTime: Infinity,
102
+ });
65
103
  const createAction = relatedObject?.actions?.find((item) => item.id === entry.display?.createActionId);
66
104
  const updateAction = relatedObject?.actions?.find((item) => item.id === entry.display?.updateActionId);
67
105
  const deleteAction = relatedObject?.actions?.find((item) => item.id === entry.display?.deleteActionId);
68
- function getForm(setForm, action, formId) {
69
- if (formId === '_auto_')
70
- return;
71
- if (formId || action?.defaultFormId) {
72
- apiServices
73
- .get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
74
- .then((evokeForm) => {
75
- setForm(evokeForm);
76
- setFetchedOptions({
77
- [`${fieldDefinition.id}${action?.type === 'delete' ? '-deleteForm' : action?.type === 'create' ? '-createForm' : '-updateForm'}`]: evokeForm,
78
- });
79
- })
80
- .catch((error) => {
81
- console.error(error);
82
- });
83
- }
84
- }
85
- const fetchRelatedInstances = useCallback(async (refetch = false) => {
86
- let relatedObject;
87
- if (fieldDefinition.objectId) {
88
- if (!fetchedOptions[`${fieldDefinition.id}RelatedObject`]) {
89
- try {
90
- relatedObject = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective`));
91
- let defaultTableViewLayout;
92
- if (relatedObject.viewLayout?.table) {
93
- defaultTableViewLayout = {
94
- id: 'default',
95
- name: 'Default',
96
- objectId: relatedObject.id,
97
- ...relatedObject?.viewLayout.table,
98
- };
99
- }
100
- if (viewLayout) {
101
- apiServices
102
- .get(getPrefixedUrl(`/objects/${viewLayout.objectId}/tableLayouts/${viewLayout.id}`))
103
- .then(setTableViewLayout)
104
- .catch((err) => setTableViewLayout(defaultTableViewLayout));
105
- }
106
- else {
107
- setTableViewLayout(defaultTableViewLayout);
108
- }
109
- setRelatedObject(relatedObject);
110
- }
111
- catch (err) {
112
- console.error(err);
113
- }
114
- }
115
- if (fieldDefinition.relatedPropertyId &&
116
- fieldDefinition.objectId &&
117
- instance?.id &&
118
- (!fetchedOptions[`${fieldDefinition.id}Options`] || refetch)) {
119
- const filterProperty = `${fieldDefinition.relatedPropertyId}.id`;
120
- const transformedCriteria = criteria ? transformToWhere(criteria) : {};
121
- const filter = {
122
- where: { [filterProperty]: instance?.id, ...transformedCriteria },
123
- limit: 100,
124
- };
125
- try {
126
- const timeout = setTimeout(() => {
127
- setLoading(false);
128
- }, 300);
129
- setLoading(true);
130
- const instances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
131
- params: { filter: JSON.stringify(filter) },
132
- });
133
- clearTimeout(timeout);
134
- if (instances) {
135
- setRelatedInstances(instances);
136
- }
137
- }
138
- catch (error) {
139
- setError(true);
140
- }
141
- }
142
- setLoading(false);
143
- }
144
- relatedObject && checkCreateAccess(relatedObject);
145
- }, [fieldDefinition]);
106
+ const createFormId = entry.display?.createFormId || createAction?.defaultFormId;
107
+ const updateFormId = entry.display?.updateFormId || updateAction?.defaultFormId;
108
+ const deleteFormId = entry.display?.deleteFormId || deleteAction?.defaultFormId;
109
+ const { data: createForm } = useFormById(createFormId ?? '', apiServices);
110
+ const { data: updateForm } = useFormById(updateFormId ?? '', apiServices);
111
+ const { data: deleteForm } = useFormById(deleteFormId ?? '', apiServices);
146
112
  const fetchCriteriaObjects = useCallback(async () => {
147
113
  let objectIds = [];
148
114
  const criteriaProperties = relatedObject?.properties?.filter((property) => property.type === 'criteria' && property.objectId) ?? [];
@@ -168,41 +134,21 @@ const RepeatableField = (props) => {
168
134
  }
169
135
  setCriteriaObjects(objects);
170
136
  }, [apiServices, relatedObject, tableViewLayout]);
171
- useEffect(() => {
172
- if (!fetchedOptions[`${fieldDefinition.id}Users`]) {
173
- (async () => {
174
- try {
175
- const users = await apiServices.get(getPrefixedUrl(`/users`));
176
- setFetchedOptions({
177
- [`${fieldDefinition.id}Users`]: users,
178
- });
179
- setUsers(users);
180
- }
181
- catch (error) {
182
- console.error(error);
183
- }
184
- })();
185
- }
186
- }, [apiServices]);
187
- useEffect(() => {
188
- fetchRelatedInstances();
189
- }, [fetchRelatedInstances, reloadOnErrorTrigger, instance]);
137
+ const { data: users } = useQuery({
138
+ queryKey: ['users'],
139
+ queryFn: () => apiServices.get(getPrefixedUrl(`/users`)),
140
+ staleTime: Infinity,
141
+ meta: {
142
+ errorMessage: 'Error fetching users: ',
143
+ },
144
+ });
190
145
  useEffect(() => {
191
146
  if (relatedObject)
192
147
  fetchCriteriaObjects();
193
148
  }, [fetchCriteriaObjects, relatedObject]);
194
- useEffect(() => {
195
- if (createAction && !createForm)
196
- getForm(setCreateForm, createAction, entry.display?.createFormId);
197
- if (updateAction && !updateForm)
198
- getForm(setUpdateForm, updateAction, entry.display?.updateFormId);
199
- if (deleteAction && !deleteForm)
200
- getForm(setDeleteForm, deleteAction, entry.display?.deleteFormId);
201
- }, [entry.display, createAction, updateAction, deleteAction]);
202
149
  useEffect(() => {
203
150
  if (relatedObject?.rootObjectId) {
204
- // pass true here so while it doesn't refetch on every tab change it does refetch on changes made
205
- const callback = () => fetchRelatedInstances(true);
151
+ const callback = () => refetchRelatedInstances();
206
152
  instanceChanges?.subscribe(relatedObject?.rootObjectId, callback);
207
153
  return () => instanceChanges?.unsubscribe(relatedObject?.rootObjectId, callback);
208
154
  }
@@ -249,59 +195,30 @@ const RepeatableField = (props) => {
249
195
  };
250
196
  }
251
197
  };
252
- const checkCreateAccess = useCallback((relatedObject) => {
253
- if (fieldDefinition.objectId &&
254
- canUpdateProperty &&
255
- !fetchedOptions[`${fieldDefinition.id}HasCreateAction`]) {
256
- apiServices
257
- .get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/checkAccess`), {
198
+ const { data: hasCreatePermission } = useQuery({
199
+ queryKey: [fieldDefinition.objectId, entry.display?.createActionId, 'hasCreatePermission '],
200
+ queryFn: async () => {
201
+ const checkAccess = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/checkAccess`), {
258
202
  params: { action: 'execute', field: entry.display?.createActionId, scope: 'data' },
259
- })
260
- .then((checkAccess) => {
261
- const action = relatedObject.actions?.find((item) => item.id === entry.display?.createActionId);
262
- if (action && fieldDefinition.relatedPropertyId) {
263
- const { relatedObjectProperty, criteria } = retrieveCriteria(fieldDefinition.relatedPropertyId, action, relatedObject);
264
- if (!criteria || JSON.stringify(criteria).includes('{{{input.') || !relatedObjectProperty) {
265
- setHasCreateAction(checkAccess.result);
266
- }
267
- else {
268
- const validate = sift(criteria);
269
- setHasCreateAction(validate(instance) && checkAccess.result);
270
- }
203
+ });
204
+ const action = relatedObject.actions?.find((item) => item.id === entry.display?.createActionId);
205
+ if (action && fieldDefinition.relatedPropertyId) {
206
+ const { relatedObjectProperty, criteria } = retrieveCriteria(fieldDefinition.relatedPropertyId, action, relatedObject);
207
+ if (!criteria || JSON.stringify(criteria).includes('{{{input.') || !relatedObjectProperty) {
208
+ return checkAccess.result;
271
209
  }
272
210
  else {
273
- setHasCreateAction(false);
211
+ const validate = sift(criteria);
212
+ return validate(instance) && checkAccess.result;
274
213
  }
275
- });
276
- }
277
- }, [fieldDefinition, canUpdateProperty, fetchedOptions, entry.display?.createActionId, instance, apiServices]);
278
- useEffect(() => {
279
- // Re-check create access when instance changes to re-evaluate the criteria
280
- relatedObject && checkCreateAccess(relatedObject);
281
- }, [relatedObject, checkCreateAccess]);
282
- useEffect(() => {
283
- const updatedOptions = {};
284
- if ((relatedInstances && !fetchedOptions[`${fieldDefinition.id}Options`]) ||
285
- fetchedOptions[`${fieldDefinition.id}Options`].length === 0 ||
286
- !isEqual(relatedInstances, fetchedOptions[`${fieldDefinition.id}Options`])) {
287
- updatedOptions[`${fieldDefinition.id}Options`] = relatedInstances;
288
- }
289
- if (relatedObject && !fetchedOptions[`${fieldDefinition.id}RelatedObject`]) {
290
- updatedOptions[`${fieldDefinition.id}RelatedObject`] = relatedObject;
291
- }
292
- if (tableViewLayout && !fetchedOptions[`${fieldDefinition.id}TableViewLayout`]) {
293
- updatedOptions[`${fieldDefinition.id}TableViewLayout`] = tableViewLayout;
294
- }
295
- if ((hasCreateAction || hasCreateAction === false) && !fetchedOptions[`${fieldDefinition.id}HasCreateAction`]) {
296
- updatedOptions[`${fieldDefinition.id}HasCreateAction`] = hasCreateAction;
297
- }
298
- else if (!hasCreateAction && relatedObject) {
299
- checkCreateAccess(relatedObject);
300
- }
301
- if (Object.keys(updatedOptions).length > 0) {
302
- setFetchedOptions(updatedOptions);
303
- }
304
- }, [relatedObject, relatedInstances, hasCreateAction, tableViewLayout]);
214
+ }
215
+ else {
216
+ return false;
217
+ }
218
+ },
219
+ staleTime: Infinity,
220
+ enabled: !!fieldDefinition.objectId && canUpdateProperty && !!relatedObject,
221
+ });
305
222
  const deleteRow = (id) => {
306
223
  setDialogType('delete');
307
224
  setSelectedInstanceId(id);
@@ -317,7 +234,7 @@ const RepeatableField = (props) => {
317
234
  setSelectedInstanceId(id);
318
235
  setOpenDialog(true);
319
236
  };
320
- const ErrorComponent = () => loading ? (React.createElement("div", null,
237
+ const ErrorComponent = () => loadingRelatedObject || loadingRelatedInstances ? (React.createElement("div", null,
321
238
  React.createElement(Typography, { sx: {
322
239
  fontSize: '14px',
323
240
  color: '#727c84',
@@ -333,7 +250,7 @@ const RepeatableField = (props) => {
333
250
  backgroundColor: 'transparent',
334
251
  },
335
252
  'min-width': '44px',
336
- }, variant: "text", onClick: () => setReloadOnErrorTrigger((prevState) => !prevState) }, "Retry")));
253
+ }, variant: "text", onClick: () => refetchRelatedInstances() }, "Retry")));
337
254
  const save = async (input) => {
338
255
  const action = relatedObject?.actions?.find((a) => a.id ===
339
256
  (dialogType === 'create'
@@ -355,8 +272,11 @@ const RepeatableField = (props) => {
355
272
  actionId: entry.display?.createActionId,
356
273
  input: updatedInput,
357
274
  });
358
- const hasAccess = fieldDefinition?.relatedPropertyId && fieldDefinition.relatedPropertyId in instance;
359
- hasAccess && setRelatedInstances([...relatedInstances, instance]);
275
+ queryClient.setQueryData(relatedInstancesQueryKey, (oldData) => {
276
+ if (!oldData)
277
+ return [instance];
278
+ return [...oldData, instance];
279
+ });
360
280
  setOpenDialog(false);
361
281
  setDialogType(undefined);
362
282
  setSelectedInstanceId(undefined);
@@ -382,12 +302,12 @@ const RepeatableField = (props) => {
382
302
  if (response && relatedObject && instance) {
383
303
  deleteDocuments(input, !!response, apiServices, relatedObject, instance, action);
384
304
  }
385
- if (action?.type === 'delete') {
386
- setRelatedInstances((prevInstances) => prevInstances.filter((instance) => instance.id !== selectedInstanceId));
387
- }
388
- else {
389
- setRelatedInstances((prevInstances) => prevInstances.map((i) => (i.id === instance?.id ? instance : i)));
390
- }
305
+ queryClient.setQueryData(relatedInstancesQueryKey, (oldData = []) => {
306
+ if (action?.type === 'delete') {
307
+ return oldData.filter((i) => i.id !== selectedInstanceId);
308
+ }
309
+ return oldData.map((i) => (i.id === instance?.id ? instance : i));
310
+ });
391
311
  setOpenDialog(false);
392
312
  setDialogType(undefined);
393
313
  setSelectedInstanceId(undefined);
@@ -461,12 +381,12 @@ const RepeatableField = (props) => {
461
381
  }
462
382
  return value;
463
383
  };
464
- return loading ? (React.createElement(React.Fragment, null,
384
+ return loadingRelatedObject || loadingRelatedInstances || isLayoutFetching ? (React.createElement(React.Fragment, null,
465
385
  React.createElement(Skeleton, null),
466
386
  React.createElement(Skeleton, null),
467
387
  React.createElement(Skeleton, null))) : (React.createElement(React.Fragment, null,
468
388
  React.createElement(Box, { sx: { padding: '10px 0' } },
469
- !relatedInstances?.length ? (!error ? (React.createElement(Typography, { sx: { margin: '-10px 0', color: 'rgb(114 124 132)', fontSize: '14px' } }, "No items added")) : (React.createElement(ErrorComponent, null))) : smallerThanMd ? (React.createElement(React.Fragment, null, relatedInstances?.map((relatedInstance, index) => (React.createElement(Accordion, { key: relatedInstance.id, sx: {
389
+ !relatedInstances?.length ? (!relatedInstancesError ? (React.createElement(Typography, { sx: { margin: '-10px 0', color: 'rgb(114 124 132)', fontSize: '14px' } }, "No items added")) : (React.createElement(ErrorComponent, null))) : smallerThanMd ? (React.createElement(React.Fragment, null, relatedInstances?.map((relatedInstance, index) => (React.createElement(Accordion, { key: relatedInstance.id, sx: {
470
390
  border: '1px solid #dbe0e4',
471
391
  borderTop: index === 0 ? undefined : 'none',
472
392
  boxShadow: 'none',
@@ -556,7 +476,7 @@ const RepeatableField = (props) => {
556
476
  entry.display?.deleteActionId && (React.createElement(IconButton, { "aria-label": `delete-collection-instance-${index}`, onClick: () => deleteRow(relatedInstance.id) },
557
477
  React.createElement(Tooltip, { title: "Delete" },
558
478
  React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } }))))))))))))),
559
- hasCreateAction && entry.display?.createActionId && (React.createElement(Button, { variant: "contained", sx: styles.addButton, disabled: !createAction, onClick: addRow, "aria-label": 'Add' }, "Add"))),
479
+ hasCreatePermission && entry.display?.createActionId && (React.createElement(Button, { variant: "contained", sx: styles.addButton, disabled: !createAction, onClick: addRow, "aria-label": 'Add' }, "Add"))),
560
480
  relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, onClose: () => setOpenDialog(false), onSubmit: save, action: relatedObject?.actions?.find((a) => a.id ===
561
481
  (dialogType === 'create'
562
482
  ? entry.display?.createActionId
@@ -1,62 +1,46 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
- import React, { useCallback, useEffect, useState } from 'react';
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import React from 'react';
3
4
  import { useFormContext } from '../../../../../theme/hooks';
4
- import { Button, CircularProgress, Typography } from '../../../../core';
5
+ import { Button, CircularProgress, Skeleton, Typography } from '../../../../core';
5
6
  import { Box } from '../../../../layout';
6
7
  import CriteriaBuilder from '../../../CriteriaBuilder';
7
8
  import { addressProperties, getPrefixedUrl } from '../utils';
8
9
  export default function Criteria(props) {
9
10
  const { value, canUpdateProperty, fieldDefinition, error } = props;
10
11
  const apiServices = useApiServices();
11
- const { fetchedOptions, setFetchedOptions, handleChange, onAutosave } = useFormContext();
12
- const [loadingError, setLoadingError] = useState(false);
13
- const [loading, setLoading] = useState(false);
14
- const [properties, setProperties] = useState(fetchedOptions[`${fieldDefinition.id}Options`] || []);
15
- const fetchProperties = useCallback(async () => {
16
- if (fieldDefinition.objectId && !fetchedOptions[`${fieldDefinition.id}Options`]) {
17
- setLoading(true);
18
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective/properties`), { params: { fields: ['properties'] } }, (error, properties) => {
19
- if (error) {
20
- console.error('Error fetching object properties', error);
21
- setLoadingError(true);
12
+ const { handleChange, onAutosave } = useFormContext();
13
+ const { data: properties = [], error: loadingError, refetch: fetchProperties, isFetching: loading, } = useQuery({
14
+ queryKey: [fieldDefinition?.objectId, 'effective properties'],
15
+ queryFn: async () => {
16
+ const properties = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective/properties`));
17
+ return properties.flatMap((prop) => {
18
+ if (prop.type === 'object' || prop.type === 'user') {
19
+ return [
20
+ {
21
+ id: `${prop.id}.id`,
22
+ name: `${prop.name} Id`,
23
+ type: 'string',
24
+ },
25
+ {
26
+ id: `${prop.id}.name`,
27
+ name: `${prop.name} Name`,
28
+ type: 'string',
29
+ },
30
+ ];
22
31
  }
23
- if (properties) {
24
- const flattenProperties = properties.flatMap((prop) => {
25
- if (prop.type === 'object' || prop.type === 'user') {
26
- return [
27
- {
28
- id: `${prop.id}.id`,
29
- name: `${prop.name} Id`,
30
- type: 'string',
31
- },
32
- {
33
- id: `${prop.id}.name`,
34
- name: `${prop.name} Name`,
35
- type: 'string',
36
- },
37
- ];
38
- }
39
- else if (prop.type === 'address') {
40
- return addressProperties(prop);
41
- }
42
- return prop;
43
- });
44
- setProperties(flattenProperties);
45
- setFetchedOptions({
46
- [`${fieldDefinition.id}Options`]: flattenProperties.map((prop) => ({
47
- id: prop.id,
48
- name: prop.name,
49
- })),
50
- });
51
- setLoadingError(false);
32
+ if (prop.type === 'address') {
33
+ return addressProperties(prop);
52
34
  }
53
- setLoading(false);
35
+ return prop;
54
36
  });
55
- }
56
- }, [fieldDefinition.objectId, apiServices]);
57
- useEffect(() => {
58
- fetchProperties();
59
- }, [fetchProperties]);
37
+ },
38
+ staleTime: Infinity,
39
+ enabled: !!fieldDefinition?.objectId,
40
+ meta: {
41
+ errorMessage: 'Error fetching object properties: ',
42
+ },
43
+ });
60
44
  const handleUpdate = async (criteria) => {
61
45
  if (criteria || value) {
62
46
  const newValue = criteria ?? null;
@@ -82,9 +66,12 @@ export default function Criteria(props) {
82
66
  padding: 0,
83
67
  '&:hover': { backgroundColor: 'transparent' },
84
68
  minWidth: '44px',
85
- }, variant: "text", onClick: fetchProperties, disabled: loading }, "Retry"),
69
+ }, variant: "text", onClick: () => fetchProperties(), disabled: loading }, "Retry"),
86
70
  loading && React.createElement(CircularProgress, { size: 20, sx: { paddingLeft: '10px' } })));
87
71
  }
72
+ if (loading) {
73
+ return React.createElement(Skeleton, null);
74
+ }
88
75
  return !!value || canUpdateProperty ? (React.createElement(Box, { sx: { borderRadius: '8px', border: error ? '1px solid #FF0000' : '1px solid #ddd' } },
89
76
  React.createElement(CriteriaBuilder, { criteria: value ?? undefined, properties: properties, setCriteria: handleUpdate, disabled: !canUpdateProperty, hideBorder: true, presetValues: [
90
77
  {
@@ -1,12 +1,13 @@
1
1
  import { DocumentParameterValidation } from '@evoke-platform/context';
2
2
  import React from 'react';
3
- import { SavedDocumentReference } from '../../types';
3
+ import { DocumentReference } from '../../types';
4
4
  type DocumentProps = {
5
5
  id: string;
6
+ fieldType?: 'file' | 'document';
6
7
  canUpdateProperty: boolean;
7
8
  error: boolean;
8
9
  validate?: DocumentParameterValidation;
9
- value: (File | SavedDocumentReference)[] | undefined;
10
+ value: (File | DocumentReference)[] | undefined;
10
11
  hasDescription?: boolean;
11
12
  };
12
13
  export declare const Document: (props: DocumentProps) => React.JSX.Element;
@@ -1,21 +1,20 @@
1
- import { useApiServices } from '@evoke-platform/context';
2
- import { isNil } from 'lodash';
1
+ import { useApiServices, } from '@evoke-platform/context';
2
+ import { useQuery } from '@tanstack/react-query';
3
3
  import prettyBytes from 'pretty-bytes';
4
- import React, { useCallback, useEffect, useState } from 'react';
4
+ import React, { useEffect, useState } from 'react';
5
5
  import { useDropzone } from 'react-dropzone';
6
6
  import { InfoRounded, UploadCloud } from '../../../../../../icons';
7
7
  import { useFormContext } from '../../../../../../theme/hooks';
8
8
  import { Skeleton, Snackbar, Typography } from '../../../../../core';
9
9
  import { Box, Grid } from '../../../../../layout';
10
- import { getPrefixedUrl } from '../../utils';
10
+ import { getEntryId, getPrefixedUrl, getUnnestedEntries, uploadFiles } from '../../utils';
11
11
  import { DocumentList } from './DocumentList';
12
12
  export const Document = (props) => {
13
- const { id, canUpdateProperty, error, value, validate, hasDescription } = props;
13
+ const { id, fieldType = 'document', canUpdateProperty, error, value, validate, hasDescription } = props;
14
14
  const apiServices = useApiServices();
15
- const { fetchedOptions, setFetchedOptions, object, handleChange, onAutosave: onAutosave, instance, } = useFormContext();
15
+ const { object, handleChange, onAutosave: onAutosave, instance, form } = useFormContext();
16
16
  const [snackbarError, setSnackbarError] = useState();
17
17
  const [documents, setDocuments] = useState();
18
- const [hasUpdatePermission, setHasUpdatePermission] = useState(fetchedOptions[`${id}UpdatePermission`]);
19
18
  let allowedTypesMessage = '';
20
19
  if (validate?.allowedFileExtensions?.length) {
21
20
  if (validate.allowedFileExtensions.length === 1) {
@@ -33,33 +32,53 @@ export const Document = (props) => {
33
32
  useEffect(() => {
34
33
  setDocuments(value);
35
34
  }, [value]);
36
- const checkPermissions = useCallback(() => {
37
- if (canUpdateProperty && !fetchedOptions[`${id}UpdatePermission`] && instance?.id) {
38
- apiServices
39
- .get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=update`))
40
- .then((accessCheck) => {
41
- setFetchedOptions({
42
- [`${id}UpdatePermission`]: accessCheck.result,
43
- });
44
- setHasUpdatePermission(accessCheck.result);
45
- })
46
- .catch(() => {
47
- setFetchedOptions({
48
- [`${id}UpdatePermission`]: false,
49
- });
50
- setHasUpdatePermission(false);
51
- });
52
- }
53
- }, [canUpdateProperty, fetchedOptions, instance, object]);
54
- useEffect(() => {
55
- checkPermissions();
56
- }, [checkPermissions]);
35
+ const { data: hasUpdatePermission = false, isLoading } = useQuery({
36
+ queryKey: ['hasDocUpdatePermission', object?.id, instance?.id],
37
+ queryFn: async () => {
38
+ // Find the entry to get the configured createActionId
39
+ const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
40
+ const entry = allEntries?.find((entry) => getEntryId(entry) === id);
41
+ const createActionId = entry?.display?.createActionId ?? '_create';
42
+ // For 'file' type properties, check regular object instance permissions
43
+ // For 'document' type properties, check document attachment permissions
44
+ const endpoint = fieldType === 'file'
45
+ ? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=execute&field=${createActionId}`)
46
+ : getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/checkAccess?action=update`);
47
+ try {
48
+ const accessCheck = await apiServices.get(endpoint);
49
+ return accessCheck.result;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ },
55
+ staleTime: Infinity,
56
+ enabled: canUpdateProperty && !!instance?.id && !!object?.id,
57
+ });
57
58
  const handleUpload = async (files) => {
58
- // Store File objects in form state - they will be uploaded during autosave via formatSubmission()
59
- const newDocuments = [...(documents ?? []), ...(files ?? [])];
59
+ if (!files?.length) {
60
+ return;
61
+ }
62
+ let uploadedFiles = files;
63
+ // Get the createActionId from display options, default to '_create'
64
+ const allEntries = getUnnestedEntries(form?.entries ?? []);
65
+ const entry = allEntries?.find((entry) => getEntryId(entry) === id);
66
+ const createActionId = entry?.display?.createActionId ?? '_create';
67
+ // Immediately upload files for 'file' type properties when autosave is not enabled.
68
+ // Linking will happen upon final submission.
69
+ // If autosave is enabled, upload and linking will happen in the autosave handler.
70
+ if (fieldType === 'file' && !onAutosave) {
71
+ const { successfulUploads, errorMessage } = await uploadFiles(files, apiServices, createActionId, undefined, false);
72
+ uploadedFiles = successfulUploads;
73
+ if (errorMessage) {
74
+ setSnackbarError({ message: errorMessage, type: 'error' });
75
+ }
76
+ }
77
+ // Store uploaded file references (or File objects) in form state
78
+ const newDocuments = [...(documents ?? []), ...uploadedFiles];
60
79
  setDocuments(newDocuments);
61
80
  try {
62
- handleChange && (await handleChange(id, newDocuments));
81
+ await handleChange?.(id, newDocuments);
63
82
  }
64
83
  catch (error) {
65
84
  console.error('Failed to update field:', error);
@@ -127,7 +146,7 @@ export const Document = (props) => {
127
146
  } }, validate?.maxDocuments === 1
128
147
  ? `Maximum size is ${formattedMaxSize}.`
129
148
  : `The maximum size of each document is ${formattedMaxSize}.`)))))),
130
- canUpdateProperty && isNil(hasUpdatePermission) ? (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })) : (React.createElement(DocumentList, { id: id, handleChange: handleChange, onAutosave: onAutosave, value: value, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission })),
149
+ canUpdateProperty && isLoading ? (React.createElement(Skeleton, { variant: "rectangular", height: formattedMaxSize || allowedTypesMessage ? '136px' : '115px', sx: { margin: '5px 0', borderRadius: '8px' } })) : (React.createElement(DocumentList, { id: id, fieldType: fieldType, handleChange: handleChange, onAutosave: onAutosave, value: documents, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission })),
131
150
  React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
132
151
  errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
133
152
  React.createElement(InfoRounded, { sx: { fontSize: '.75rem', marginRight: '3px', color: '#D3271B' } }),