@evoke-platform/ui-components 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +4 -8
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +144 -238
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
  6. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
  7. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
  8. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
  11. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  12. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
  13. package/dist/published/components/custom/Form/utils.d.ts +2 -3
  14. package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
  15. package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
  16. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
  17. package/dist/published/components/custom/FormField/Select/Select.js +1 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -2
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +7 -7
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -4
  21. package/dist/published/components/custom/FormV2/FormRendererContainer.js +49 -91
  22. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +17 -44
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +12 -44
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +29 -41
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +0 -14
  33. package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
  34. package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
  35. package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
  36. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +13 -19
  37. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
  38. package/dist/published/components/custom/FormV2/components/utils.d.ts +14 -12
  39. package/dist/published/components/custom/FormV2/components/utils.js +123 -159
  40. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
  41. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
  42. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +1 -6
  43. package/dist/published/components/custom/index.d.ts +0 -1
  44. package/dist/published/index.d.ts +1 -1
  45. package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
  46. package/dist/published/stories/FormRenderer.stories.d.ts +3 -6
  47. package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -20
  48. package/dist/published/stories/FormRendererData.d.ts +15 -0
  49. package/dist/published/stories/FormRendererData.js +63 -0
  50. package/dist/published/stories/sharedMswHandlers.js +4 -2
  51. package/dist/published/theme/hooks.d.ts +0 -1
  52. package/package.json +2 -3
  53. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +0 -12
  54. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +0 -197
@@ -10,7 +10,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Skel
10
10
  import { Box } from '../../../../../layout';
11
11
  import { getReadableQuery } from '../../../../CriteriaBuilder';
12
12
  import { retrieveCustomErrorMessage } from '../../../../Form/utils';
13
- import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, handleFileUpload, transformToWhere, useFormById, } from '../../utils';
13
+ import { convertPropertiesToParams, deleteDocuments, formatSubmission, getPrefixedUrl, transformToWhere, useFormById, } from '../../utils';
14
14
  import { ActionDialog } from './ActionDialog';
15
15
  import { DocumentViewerCell } from './DocumentViewerCell';
16
16
  const styles = {
@@ -258,34 +258,20 @@ const RepeatableField = (props) => {
258
258
  : dialogType === 'update'
259
259
  ? entry.display?.updateActionId
260
260
  : entry.display?.deleteActionId));
261
- const relatedProperty = relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId);
262
261
  // when save is called we know that fieldDefinition is a parameter and fieldDefinition.objectId is defined
263
- input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm : action?.type === 'delete' ? deleteForm : createForm, undefined, instance?.id && fieldDefinition.relatedPropertyId
264
- ? {
265
- instanceId: instance.id,
266
- propertyId: fieldDefinition.relatedPropertyId,
267
- objectId: !relatedProperty?.objectId ? instance.objectId : undefined,
268
- }
262
+ input = await formatSubmission(input, apiServices, fieldDefinition.objectId, selectedInstanceId, action?.type === 'update' ? updateForm : undefined, undefined, instance?.id && fieldDefinition.relatedPropertyId
263
+ ? { instanceId: instance.id, propertyId: fieldDefinition.relatedPropertyId }
269
264
  : undefined, action?.parameters ?? (relatedObject && convertPropertiesToParams(relatedObject)));
270
- if (action?.type === 'create' && action.id) {
265
+ if (action?.type === 'create' && entry.display?.createActionId) {
271
266
  const updatedInput = {
272
267
  ...input,
273
- [fieldDefinition?.relatedPropertyId]: {
274
- id: instance?.id,
275
- objectId: !relatedProperty?.objectId ? instance?.objectId : undefined,
276
- },
268
+ [fieldDefinition?.relatedPropertyId]: { id: instance?.id },
277
269
  };
278
270
  try {
279
- let instance = undefined;
280
- if (relatedObject?.rootObjectId === 'sys__file') {
281
- instance = await handleFileUpload(apiServices, updatedInput, action.id, fieldDefinition.objectId);
282
- }
283
- else {
284
- instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
285
- actionId: action.id,
286
- input: updatedInput,
287
- });
288
- }
271
+ const instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
272
+ actionId: entry.display?.createActionId,
273
+ input: updatedInput,
274
+ });
289
275
  queryClient.setQueryData(relatedInstancesQueryKey, (oldData) => {
290
276
  if (!oldData)
291
277
  return [instance];
@@ -307,19 +293,12 @@ const RepeatableField = (props) => {
307
293
  else {
308
294
  const relatedObjectId = relatedObject?.id;
309
295
  try {
310
- let response = undefined;
311
- const submission = omit(input, relatedObject?.properties
312
- ?.filter((property) => property.formula || property.type === 'collection')
313
- .map((property) => property.id) ?? []);
314
- if (relatedObject?.rootObjectId === 'sys__file' && action?.id) {
315
- response = await handleFileUpload(apiServices, submission, action.id, relatedObjectId, selectedInstanceId);
316
- }
317
- else {
318
- response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${selectedInstanceId}/actions`), {
319
- actionId: action?.id ?? `_${action?.type}`,
320
- input: submission,
321
- });
322
- }
296
+ const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${selectedInstanceId}/actions`), {
297
+ actionId: `_${action?.type}`,
298
+ input: omit(input, relatedObject?.properties
299
+ ?.filter((property) => property.formula || property.type === 'collection')
300
+ .map((property) => property.id) ?? []),
301
+ });
323
302
  if (response && relatedObject && instance) {
324
303
  deleteDocuments(input, !!response, apiServices, relatedObject, instance, action);
325
304
  }
@@ -472,7 +451,7 @@ const RepeatableField = (props) => {
472
451
  React.createElement(TableRow, null,
473
452
  columns?.map((prop) => (React.createElement(TableCell, { sx: styles.tableCell }, prop.name))),
474
453
  canUpdateProperty && React.createElement(TableCell, { sx: { ...styles.tableCell, width: '80px' } }))),
475
- React.createElement(TableBody, null, relatedInstances?.map((relatedInstance, index) => (React.createElement(TableRow, { key: relatedInstance.id },
454
+ React.createElement(TableBody, { sx: { backgroundColor: 'white' } }, relatedInstances?.map((relatedInstance, index) => (React.createElement(TableRow, { key: relatedInstance.id },
476
455
  columns?.map((prop) => {
477
456
  return (React.createElement(TableCell, { sx: { fontSize: '16px' } }, prop.type === 'document' ? (React.createElement(DocumentViewerCell, { instance: relatedInstance, propertyId: prop.id, setSnackbarError: setSnackbarError })) : (React.createElement(Typography, { key: prop.id, sx: prop.id === 'name'
478
457
  ? {
@@ -512,13 +491,7 @@ const RepeatableField = (props) => {
512
491
  ? '_auto_'
513
492
  : deleteForm?.id
514
493
  : undefined, instanceId: selectedInstanceId, relatedParameter: fieldDefinition, associatedObject: instance?.id && fieldDefinition.relatedPropertyId
515
- ? {
516
- instanceId: instance.id,
517
- propertyId: fieldDefinition.relatedPropertyId,
518
- objectId: !relatedObject?.properties?.find((p) => p.id === fieldDefinition.relatedPropertyId)?.objectId
519
- ? instance.objectId
520
- : undefined,
521
- }
494
+ ? { instanceId: instance.id, propertyId: fieldDefinition.relatedPropertyId }
522
495
  : undefined })),
523
496
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
524
497
  };
@@ -1,13 +1,12 @@
1
1
  import { DocumentParameterValidation } from '@evoke-platform/context';
2
2
  import React from 'react';
3
- import { DocumentReference } from '../../types';
3
+ import { SavedDocumentReference } from '../../types';
4
4
  type DocumentProps = {
5
5
  id: string;
6
- fieldType?: 'file' | 'document';
7
6
  canUpdateProperty: boolean;
8
7
  error: boolean;
9
8
  validate?: DocumentParameterValidation;
10
- value: (File | DocumentReference)[] | undefined;
9
+ value: (File | SavedDocumentReference)[] | undefined;
11
10
  hasDescription?: boolean;
12
11
  };
13
12
  export declare const Document: (props: DocumentProps) => React.JSX.Element;
@@ -1,4 +1,4 @@
1
- import { useApiServices, } from '@evoke-platform/context';
1
+ import { useApiServices } from '@evoke-platform/context';
2
2
  import { useQuery } from '@tanstack/react-query';
3
3
  import prettyBytes from 'pretty-bytes';
4
4
  import React, { useEffect, useState } from 'react';
@@ -7,12 +7,12 @@ 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 { getEntryId, getPrefixedUrl, getUnnestedEntries, uploadFiles } from '../../utils';
10
+ import { getPrefixedUrl } from '../../utils';
11
11
  import { DocumentList } from './DocumentList';
12
12
  export const Document = (props) => {
13
- const { id, fieldType = 'document', canUpdateProperty, error, value, validate, hasDescription } = props;
13
+ const { id, canUpdateProperty, error, value, validate, hasDescription } = props;
14
14
  const apiServices = useApiServices();
15
- const { object, handleChange, onAutosave: onAutosave, instance, form } = useFormContext();
15
+ const { object, handleChange, onAutosave: onAutosave, instance } = useFormContext();
16
16
  const [snackbarError, setSnackbarError] = useState();
17
17
  const [documents, setDocuments] = useState();
18
18
  let allowedTypesMessage = '';
@@ -32,22 +32,11 @@ export const Document = (props) => {
32
32
  useEffect(() => {
33
33
  setDocuments(value);
34
34
  }, [value]);
35
- // Find the entry to get the configured createActionId and fileObjectId
36
- const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
37
- const entry = allEntries?.find((entry) => getEntryId(entry) === id);
38
- const createActionId = entry?.display?.createActionId ?? '_create';
39
- // Get the configured objectId from display.fileObjectId, defaulting to sys__file
40
- const fileObjectId = entry?.display?.fileObjectId ?? 'sys__file';
41
- // For 'file' type properties, check regular object instance permissions
42
- // For 'document' type properties, check document attachment permissions
43
- const endpoint = fieldType === 'file'
44
- ? getPrefixedUrl(`/objects/${fileObjectId}/instances/checkAccess?action=execute&field=${createActionId}`)
45
- : getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=update`);
46
35
  const { data: hasUpdatePermission = false, isLoading } = useQuery({
47
- queryKey: ['hasUpdatePermission', object?.id, instance?.id, fieldType, id],
36
+ queryKey: ['hasDocUpdatePermission', object?.id, instance?.id],
48
37
  queryFn: async () => {
49
38
  try {
50
- const accessCheck = await apiServices.get(endpoint);
39
+ const accessCheck = await apiServices.get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=update`));
51
40
  return accessCheck.result;
52
41
  }
53
42
  catch {
@@ -55,36 +44,14 @@ export const Document = (props) => {
55
44
  }
56
45
  },
57
46
  staleTime: Infinity,
58
- // For 'file' type fields the permission endpoint only requires the object ID, so the
59
- // query can run on create actions where no instance exists yet. For 'document' type
60
- // fields the endpoint is instance-scoped, so instance ID is still required.
61
- enabled: canUpdateProperty && !!object?.id && (fieldType === 'file' || !!instance?.id),
47
+ enabled: canUpdateProperty && !!instance?.id && !!object?.id,
62
48
  });
63
49
  const handleUpload = async (files) => {
64
- if (!files?.length) {
65
- return;
66
- }
67
- let uploadedFiles = files;
68
- // Get the createActionId and fileObjectId from form entry, default to '_create' and 'sys__file'
69
- const allEntries = getUnnestedEntries(form?.entries ?? []);
70
- const entry = allEntries?.find((entry) => getEntryId(entry) === id);
71
- const createActionId = entry?.display?.createActionId ?? '_create';
72
- const fileObjectId = entry?.display?.fileObjectId ?? 'sys__file';
73
- // Immediately upload files for 'file' type properties when autosave is not enabled.
74
- // Linking will happen upon final submission.
75
- // If autosave is enabled, upload and linking will happen in the autosave handler.
76
- if (fieldType === 'file' && !onAutosave) {
77
- const { successfulUploads, errorMessage } = await uploadFiles(files, apiServices, createActionId, fileObjectId, undefined, false);
78
- uploadedFiles = successfulUploads;
79
- if (errorMessage) {
80
- setSnackbarError({ message: errorMessage, type: 'error' });
81
- }
82
- }
83
- // Store uploaded file references (or File objects) in form state
84
- const newDocuments = [...(documents ?? []), ...uploadedFiles];
50
+ // Store File objects in form state - they will be uploaded during autosave via formatSubmission()
51
+ const newDocuments = [...(documents ?? []), ...(files ?? [])];
85
52
  setDocuments(newDocuments);
86
53
  try {
87
- await handleChange?.(id, newDocuments);
54
+ handleChange && (await handleChange(id, newDocuments));
88
55
  }
89
56
  catch (error) {
90
57
  console.error('Failed to update field:', error);
@@ -126,6 +93,7 @@ export const Document = (props) => {
126
93
  border: `1px dashed ${error ? 'red' : uploadDisabled ? '#DFE3E8' : '#858585'}`,
127
94
  position: 'relative',
128
95
  cursor: uploadDisabled ? 'cursor' : 'pointer',
96
+ backgroundColor: 'white',
129
97
  }, ...getRootProps(), onClick: open },
130
98
  React.createElement("input", { ...getInputProps({ id }), disabled: uploadDisabled, ...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined) }),
131
99
  React.createElement(Grid, { container: true, sx: { width: '100%' } },
@@ -152,7 +120,7 @@ export const Document = (props) => {
152
120
  } }, validate?.maxDocuments === 1
153
121
  ? `Maximum size is ${formattedMaxSize}.`
154
122
  : `The maximum size of each document is ${formattedMaxSize}.`)))))),
155
- 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 })),
123
+ canUpdateProperty && isLoading ? (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: documents, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission })),
156
124
  React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' }),
157
125
  errors.length > 0 && (React.createElement(Box, { display: 'flex', alignItems: 'center' },
158
126
  React.createElement(InfoRounded, { sx: { fontSize: '.75rem', marginRight: '3px', color: '#D3271B' } }),
@@ -1,12 +1,11 @@
1
1
  import React from 'react';
2
- import { DocumentReference } from '../../types';
2
+ import { SavedDocumentReference } from '../../types';
3
3
  type DocumentListProps = {
4
- handleChange?: (propertyId: string, value: (File | DocumentReference)[] | undefined) => void;
4
+ handleChange?: (propertyId: string, value: (File | SavedDocumentReference)[] | undefined) => void;
5
5
  onAutosave?: (fieldId: string) => void | Promise<void>;
6
6
  id: string;
7
- fieldType?: 'document' | 'file';
8
7
  canUpdateProperty: boolean;
9
- value: (File | DocumentReference)[] | undefined;
8
+ value: (File | SavedDocumentReference)[] | undefined;
10
9
  setSnackbarError: (type: 'error' | 'success', message: string) => void;
11
10
  };
12
11
  export declare const DocumentList: (props: DocumentListProps) => React.JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
2
  import { useQuery } from '@tanstack/react-query';
3
+ import { isEqual } from 'lodash';
3
4
  import prettyBytes from 'pretty-bytes';
4
5
  import React, { useEffect, useState } from 'react';
5
6
  import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
@@ -24,44 +25,33 @@ const viewableFileTypes = [
24
25
  'text/plain',
25
26
  ];
26
27
  export const DocumentList = (props) => {
27
- const { handleChange, onAutosave, id, fieldType = 'document', canUpdateProperty, value: documents, setSnackbarError, } = props;
28
+ const { handleChange, onAutosave, id, canUpdateProperty, value: documents, setSnackbarError } = props;
28
29
  const apiServices = useApiServices();
29
30
  const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
30
31
  // Determine property type once at component level
31
- const isFileType = fieldType === 'file';
32
+ const propertyType = object?.properties?.find((p) => p.id === id)?.type;
33
+ const isFileType = propertyType === 'file';
32
34
  // savedDocuments is either FileInstance[] or DocumentType[], never a mix
33
35
  const [savedDocuments, setSavedDocuments] = useState(fetchedOptions[`${id}SavedDocuments`]);
34
- const [deletedDocumentIds, setDeletedDocumentIds] = useState([]);
35
- // The stable list we render — only updated once savedDocuments has metadata for
36
- // every referenced ID. While waiting for metadata, the previous list is kept so
37
- // the UI doesn't flash empty sizes, missing icons, or "Deleted" chips.
38
- const [displayDocuments, setDisplayDocuments] = useState(documents);
39
36
  useEffect(() => {
40
- const currentDocumentIds = (documents ?? [])
41
- .filter((doc) => !(doc instanceof File))
42
- .map((doc) => doc.id);
43
- if (!currentDocumentIds.length) {
44
- // Only File objects metadata not needed, display immediately.
45
- setDisplayDocuments(documents);
46
- return;
47
- }
48
- const savedDocumentIds = savedDocuments?.map((doc) => doc.id) ?? [];
49
- const missingDocumentIds = currentDocumentIds.filter((docId) => !savedDocumentIds.includes(docId) && !deletedDocumentIds.includes(docId));
50
- if (missingDocumentIds.length) {
51
- // Metadata not yet available — fetch it.
52
- // If we already have a stable display, keep it to avoid flashing.
53
- // If there's nothing to show yet (initial load), display immediately so
54
- // the file names are visible while metadata loads.
55
- if (!displayDocuments?.length) {
56
- setDisplayDocuments(documents);
37
+ const currentValue = instance?.[id];
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);
57
47
  }
58
- getDocuments(currentDocumentIds);
59
48
  }
60
- else {
61
- // All IDs have metadata — safe to update the display.
62
- setDisplayDocuments(documents);
49
+ }, [id, documents, object]);
50
+ useEffect(() => {
51
+ if (fetchedOptions[`${id}SavedDocuments`]) {
52
+ setSavedDocuments(fetchedOptions[`${id}SavedDocuments`]);
63
53
  }
64
- }, [documents, savedDocuments, fieldType, apiServices]);
54
+ }, [fetchedOptions]);
65
55
  const getDocuments = (currentDocumentIds, shouldRetry = true) => {
66
56
  // For 'file' type properties, fetch sys__file instances directly
67
57
  // For 'document' type properties, fetch attachment documents
@@ -74,7 +64,7 @@ export const DocumentList = (props) => {
74
64
  // There is a short delay between when a document is uploaded and when
75
65
  // it is indexed. Therefore, try again if documents are not found.
76
66
  if (shouldRetry &&
77
- (!docs?.length ||
67
+ (!docs ||
78
68
  currentDocumentIds.some((docId) => !docs.find((doc) => docId === doc.id)))) {
79
69
  setTimeout(() => getDocuments(currentDocumentIds, false), 2000);
80
70
  }
@@ -82,9 +72,6 @@ export const DocumentList = (props) => {
82
72
  setSnackbarError('error', 'Error occurred while retrieving saved documents');
83
73
  }
84
74
  else {
85
- // IDs that were requested but not returned by the server = deleted/not found.
86
- const deletedIds = currentDocumentIds.filter((docId) => !docs?.find((doc) => doc.id === docId));
87
- setDeletedDocumentIds(deletedIds);
88
75
  setSavedDocuments(docs);
89
76
  setFetchedOptions({
90
77
  [`${id}SavedDocuments`]: docs,
@@ -110,13 +97,12 @@ export const DocumentList = (props) => {
110
97
  staleTime: Infinity,
111
98
  });
112
99
  const isFile = (doc) => doc instanceof File;
113
- const isUnsavedFile = (doc) => isFile(doc) || !!doc.unsaved;
114
100
  const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
115
101
  const handleRemove = async (index) => {
116
102
  const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
117
103
  const newValue = updatedDocuments.length === 0 ? undefined : updatedDocuments;
118
104
  try {
119
- handleChange?.(id, newValue);
105
+ handleChange && (await handleChange(id, newValue));
120
106
  }
121
107
  catch (error) {
122
108
  console.error('Failed to update field:', error);
@@ -130,7 +116,7 @@ export const DocumentList = (props) => {
130
116
  }
131
117
  };
132
118
  const openDocument = async (index) => {
133
- const doc = displayDocuments?.[index];
119
+ const doc = documents?.[index];
134
120
  if (doc) {
135
121
  let url;
136
122
  const contentType = doc instanceof File
@@ -138,7 +124,9 @@ export const DocumentList = (props) => {
138
124
  : savedDocuments?.find((savedDocument) => savedDocument.id === doc.id)?.contentType;
139
125
  if (!isFile(doc)) {
140
126
  try {
141
- const contentEndpoint = isFileType
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'
142
130
  ? getPrefixedUrl(`/files/${doc.id}/content`)
143
131
  : getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}/content`);
144
132
  const documentResponse = await apiServices.get(contentEndpoint, { responseType: 'blob' });
@@ -179,8 +167,8 @@ export const DocumentList = (props) => {
179
167
  return size;
180
168
  };
181
169
  return (React.createElement(React.Fragment, null,
182
- !displayDocuments && !canUpdateProperty && (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No documents")),
183
- !!displayDocuments?.length && (React.createElement(Box, null, displayDocuments.map((doc, index) => (React.createElement(Grid, { container: true, sx: {
170
+ !documents && !canUpdateProperty && (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No documents")),
171
+ !!documents?.length && (React.createElement(Box, null, documents.map((doc, index) => (React.createElement(Grid, { container: true, sx: {
184
172
  width: '100%',
185
173
  border: '1px solid #C4CDD5',
186
174
  borderRadius: '6px',
@@ -201,10 +189,10 @@ export const DocumentList = (props) => {
201
189
  } }, doc.name)),
202
190
  React.createElement(Grid, { item: true, xs: 12 },
203
191
  React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, getDocumentSize(doc)))),
204
- (isUnsavedFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
192
+ (isFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
205
193
  React.createElement(IconButton, { "aria-label": "open document", sx: { ...styles.icon, marginRight: '16px' }, onClick: () => openDocument(index) },
206
194
  React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))),
207
- !isFile(doc) && !isUnsavedFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
195
+ !isFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
208
196
  marginRight: '16px',
209
197
  backgroundColor: 'rgba(222, 48, 36, 0.16)',
210
198
  color: '#A91813',
@@ -29,6 +29,7 @@ const styles = {
29
29
  border: '1px dashed #858585',
30
30
  position: 'relative',
31
31
  cursor: 'pointer',
32
+ backgroundColor: 'white',
32
33
  },
33
34
  icon: {
34
35
  color: '#fff',
@@ -141,6 +141,8 @@ const UserProperty = (props) => {
141
141
  },
142
142
  }
143
143
  : {}),
144
+ backgroundColor: 'white',
145
+ borderRadius: '8px',
144
146
  } })), size: fieldHeight ?? 'medium', readOnly: readOnly, error: error })));
145
147
  };
146
148
  export default UserProperty;
@@ -102,20 +102,6 @@ const InstanceLookup = (props) => {
102
102
  },
103
103
  });
104
104
  }
105
- else if (property.type === 'fileContent') {
106
- columns.push({
107
- field: prop.id,
108
- headerName: property.name,
109
- type: 'string',
110
- flex: 1,
111
- valueGetter: (params) => {
112
- const row = params.row;
113
- // Currently the `fileContent` property type is only supported on the `sys__file` system object.
114
- // Display the 'name' property of the `sys__file` instance.
115
- return row['name'];
116
- },
117
- });
118
- }
119
105
  else if (property.type === 'date') {
120
106
  columns.push({
121
107
  field: prop.id,
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { FormletReference } from '@evoke-platform/context';
3
+ declare function FormletRenderer(props: {
4
+ entry: FormletReference;
5
+ }): React.JSX.Element;
6
+ export default FormletRenderer;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useApiServices } from '@evoke-platform/context';
3
+ import { getPrefixedUrl } from './utils';
4
+ import { useQuery } from '@tanstack/react-query';
5
+ import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
6
+ import { Skeleton, Typography } from '../../../core';
7
+ import { Box } from '../../../layout';
8
+ import { WarningRounded } from '../../../../icons';
9
+ function FormletRenderer(props) {
10
+ const { entry } = props;
11
+ const apiServices = useApiServices();
12
+ const { data: formlet, isLoading } = useQuery({
13
+ queryKey: [entry.formletId, 'formlet'],
14
+ queryFn: () => apiServices.get(getPrefixedUrl(`/formlets/${entry.formletId}`)),
15
+ staleTime: Infinity,
16
+ enabled: !!entry.formletId,
17
+ });
18
+ if (isLoading)
19
+ return React.createElement(Skeleton, null);
20
+ return formlet ? (React.createElement(React.Fragment, null, formlet.entries?.map((formletEntry, index) => (React.createElement(RecursiveEntryRenderer, { key: index, entry: formletEntry }))))) : (React.createElement(Box, { sx: {
21
+ display: 'flex',
22
+ backgroundColor: '#ffc1073b',
23
+ borderRadius: '8px',
24
+ padding: '16.5px 14px',
25
+ marginTop: '6px',
26
+ } },
27
+ React.createElement(WarningRounded, { sx: { paddingRight: '8px' }, color: "warning" }),
28
+ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "This field was not configured correctly")));
29
+ }
30
+ export default FormletRenderer;
@@ -33,14 +33,17 @@ const HtmlView = ({ value }) => {
33
33
  quillRef.current.setContents([]);
34
34
  quillRef.current.clipboard.dangerouslyPasteHTML(DOMPurify.sanitize(value));
35
35
  }, [value]);
36
- return (React.createElement(Box, { sx: {
37
- width: '100%',
38
- height: '100%',
39
- '.ql-container.ql-snow': {
40
- border: 'none',
41
- minHeight: 20,
42
- },
43
- } },
44
- React.createElement("div", { ref: containerRef })));
36
+ return (
37
+ // Needs to be wrapped in a Box to prevent quill from setting height: 100% on the container, which causes it to overflow its parent and ignore the maxHeight we set on the containerRef (only happens when in a grid item)
38
+ React.createElement(Box, null,
39
+ React.createElement(Box, { sx: {
40
+ width: '100%',
41
+ height: '100%',
42
+ '.ql-container.ql-snow': {
43
+ border: 'none',
44
+ minHeight: 20,
45
+ },
46
+ } },
47
+ React.createElement("div", { ref: containerRef }))));
45
48
  };
46
49
  export default HtmlView;
@@ -1,5 +1,6 @@
1
1
  import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
2
2
  import { WarningRounded } from '@mui/icons-material';
3
+ import { Grid } from '@mui/material';
3
4
  import { isEmpty } from 'lodash';
4
5
  import React, { useMemo } from 'react';
5
6
  import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
@@ -13,7 +14,6 @@ import DropdownRepeatableField from './FormFieldTypes/CollectionFiles/DropdownRe
13
14
  import RepeatableField from './FormFieldTypes/CollectionFiles/RepeatableField';
14
15
  import Criteria from './FormFieldTypes/Criteria';
15
16
  import { Document } from './FormFieldTypes/DocumentFiles/Document';
16
- import { FileContent } from './FormFieldTypes/FileContent';
17
17
  import { Image } from './FormFieldTypes/Image';
18
18
  import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
19
19
  import UserProperty from './FormFieldTypes/UserProperty';
@@ -21,6 +21,7 @@ import FormSections from './FormSections';
21
21
  import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
22
22
  import HtmlView from './HtmlView';
23
23
  import { useQuery } from '@tanstack/react-query';
24
+ import FormletRenderer from './FormletRenderer';
24
25
  function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
25
26
  return {
26
27
  inputId: entryId,
@@ -53,8 +54,6 @@ export function RecursiveEntryRenderer(props) {
53
54
  const display = 'display' in entry ? entry.display : undefined;
54
55
  const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
55
56
  const fieldDefinition = useMemo(() => {
56
- if (!object)
57
- return undefined;
58
57
  return getFieldDefinition(entry, object, parameters);
59
58
  }, [entry, parameters, object]);
60
59
  const validation = fieldDefinition?.validation || {};
@@ -88,9 +87,10 @@ export function RecursiveEntryRenderer(props) {
88
87
  },
89
88
  });
90
89
  const memorizedCriteria = useMemo(() => {
91
- const criteria = 'criteria' in validation && validation.criteria ? validation.criteria : display?.criteria;
92
- return getValues && criteria ? updateCriteriaInputs(criteria, getValues(), userAccount, instance) : undefined;
93
- }, [validation, getValues && getValues(), userAccount, instance, display]);
90
+ return 'criteria' in validation && validation.criteria && getValues
91
+ ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
92
+ : undefined;
93
+ }, [validation, getValues && getValues(), userAccount, instance]);
94
94
  const memorizedDefaultValueCriteria = useMemo(() => {
95
95
  return display?.defaultValue &&
96
96
  typeof display.defaultValue === 'object' &&
@@ -166,11 +166,7 @@ export function RecursiveEntryRenderer(props) {
166
166
  }
167
167
  else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
168
168
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
169
- React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
170
- }
171
- else if (fieldDefinition.type === 'fileContent') {
172
- return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
173
- React.createElement(FileContent, { id: entryId, error: !!errors?.[entryId], value: fieldValue, hasDescription: !!display?.description, canUpdateProperty: !(entry.type === 'readonlyField'), validate: validation })));
169
+ React.createElement(Document, { id: entryId, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
174
170
  }
175
171
  else if (fieldDefinition.type === 'criteria') {
176
172
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -218,16 +214,14 @@ export function RecursiveEntryRenderer(props) {
218
214
  ? display?.booleanDisplay
219
215
  : display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions: fieldDefinition?.enum, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue, protection: objectProperty?.protection })));
220
216
  }
217
+ // Forms from the FormV2 widget will get the effective form and will not need this
218
+ // but it's possible for a form passed into the FormRenderer to include a formlet type
219
+ }
220
+ else if (entry.type === 'formlet') {
221
+ return React.createElement(FormletRenderer, { entry: entry });
221
222
  }
222
223
  else if (entry.type === 'columns') {
223
- return (React.createElement(Box, { sx: {
224
- display: 'flex',
225
- alignItems: 'flex-start',
226
- gap: '30px',
227
- flexDirection: isXs ? 'column' : 'row',
228
- } }, entry.columns.map((column, colIndex) => (
229
- // calculating the width like this rather than flex={column.width} to prevent collections from being too wide
230
- React.createElement(Box, { key: colIndex, sx: { width: isXs ? '100%' : `calc(${(column.width / 12) * 100}% - 15px)` } }, column.entries?.map((columnEntry, entryIndex) => {
224
+ return (React.createElement(Grid, { container: true, columnSpacing: 4 }, entry.columns.map((column, colIndex) => (React.createElement(Grid, { key: colIndex, item: true, xs: isXs ? 12 : column.width }, column.entries?.map((columnEntry, entryIndex) => {
231
225
  return (React.createElement(RecursiveEntryRenderer, { key: entryIndex + (columnEntry?.parameterId ?? ''), entry: columnEntry }));
232
226
  }))))));
233
227
  }
@@ -12,10 +12,9 @@ export type FieldAddress = {
12
12
  export type AccessCheck = {
13
13
  result: boolean;
14
14
  };
15
- export type DocumentReference = {
15
+ export type SavedDocumentReference = {
16
16
  id: string;
17
17
  name: string;
18
- unsaved?: boolean;
19
18
  };
20
19
  export type Document = {
21
20
  id: string;
@@ -113,7 +112,3 @@ export type InstanceLink = {
113
112
  id: string;
114
113
  objectId: string;
115
114
  };
116
- export type FileUploadBatchResult = {
117
- errorMessage?: string;
118
- successfulUploads: DocumentReference[];
119
- };