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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +4 -4
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +72 -145
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
  6. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
  7. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
  8. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
  11. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  12. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
  13. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  14. package/dist/published/components/custom/Form/utils.d.ts +0 -1
  15. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +1 -2
  16. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -2
  17. package/dist/published/components/custom/FormV2/FormRenderer.js +29 -26
  18. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +3 -1
  19. package/dist/published/components/custom/FormV2/FormRendererContainer.js +88 -95
  20. package/dist/published/components/custom/FormV2/components/Body.js +1 -1
  21. package/dist/published/components/custom/FormV2/components/Footer.js +1 -1
  22. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +143 -86
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +2 -0
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +4 -1
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +186 -106
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +49 -36
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +32 -51
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +38 -40
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +21 -17
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +169 -95
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -0
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +6 -12
  38. package/dist/published/components/custom/FormV2/components/FormSections.js +0 -1
  39. package/dist/published/components/custom/FormV2/components/Header.d.ts +1 -0
  40. package/dist/published/components/custom/FormV2/components/Header.js +19 -8
  41. package/dist/published/components/custom/FormV2/components/HtmlView.d.ts +9 -0
  42. package/dist/published/components/custom/FormV2/components/HtmlView.js +46 -0
  43. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +1 -2
  44. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +20 -46
  45. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
  46. package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -11
  47. package/dist/published/components/custom/FormV2/components/utils.js +104 -181
  48. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +17 -50
  49. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +131 -40
  50. package/dist/published/components/custom/HistoryLog/HistoryData.js +1 -2
  51. package/dist/published/components/custom/HistoryLog/index.js +1 -2
  52. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +1 -2
  53. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +22 -61
  54. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +3 -0
  55. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +5 -8
  56. package/dist/published/stories/Backdrop.stories.d.ts +2 -2
  57. package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
  58. package/dist/published/stories/FormLabel.stories.d.ts +2 -2
  59. package/dist/published/stories/FormRenderer.stories.d.ts +3 -3
  60. package/dist/published/stories/FormRendererContainer.stories.d.ts +15 -5
  61. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +9 -0
  62. package/dist/published/theme/hooks.d.ts +1 -2
  63. package/package.json +11 -17
  64. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +0 -5
  65. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +0 -21
@@ -1,11 +1,9 @@
1
1
  import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
2
2
  import { WarningRounded } from '@mui/icons-material';
3
- import { useQuery } from '@tanstack/react-query';
4
- import DOMPurify from 'dompurify';
5
3
  import { isEmpty } from 'lodash';
6
- import React, { useMemo } from 'react';
4
+ import React, { useEffect, useMemo } from 'react';
7
5
  import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
8
- import { Skeleton, TextField, Typography } from '../../../core';
6
+ import { TextField, Typography } from '../../../core';
9
7
  import { Box } from '../../../layout';
10
8
  import FormField from '../../FormField';
11
9
  import AccordionSections from './AccordionSections';
@@ -19,7 +17,8 @@ import { Image } from './FormFieldTypes/Image';
19
17
  import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
20
18
  import UserProperty from './FormFieldTypes/UserProperty';
21
19
  import FormSections from './FormSections';
22
- import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
20
+ import { entryIsVisible, fetchCollectionData, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
21
+ import HtmlView from './HtmlView';
23
22
  function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
24
23
  return {
25
24
  inputId: entryId,
@@ -39,7 +38,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
39
38
  }
40
39
  export function RecursiveEntryRenderer(props) {
41
40
  const { entry } = props;
42
- const { object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, width, } = useFormContext();
41
+ const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, width, } = useFormContext();
43
42
  const { isBelow, breakpoints } = useWidgetSize({
44
43
  scroll: false,
45
44
  defaultWidth: width,
@@ -51,41 +50,19 @@ export function RecursiveEntryRenderer(props) {
51
50
  const entryId = getEntryId(entry) || 'defaultId';
52
51
  const display = 'display' in entry ? entry.display : undefined;
53
52
  const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
53
+ const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
54
+ const middleObject = fetchedOptions[`${entryId}MiddleObject`];
54
55
  const fieldDefinition = useMemo(() => {
55
56
  if (!object)
56
57
  return undefined;
57
58
  return getFieldDefinition(entry, object, parameters);
58
59
  }, [entry, parameters, object]);
59
60
  const validation = fieldDefinition?.validation || {};
60
- const { data: middleObject } = useQuery({
61
- queryKey: [fieldDefinition?.objectId, 'MiddleObject'],
62
- queryFn: () => fetchMiddleObject(fieldDefinition, apiServices),
63
- staleTime: Infinity,
64
- enabled: !!(fieldDefinition?.objectId &&
65
- fieldDefinition?.type === 'collection' &&
66
- fieldDefinition?.manyToManyPropertyId),
67
- meta: {
68
- errorMessage: 'Failed to fetch middle object: ',
69
- },
70
- });
71
- const { data: initialMiddleObjectInstances = [], isLoading: isLoadingInstances } = useQuery({
72
- queryKey: [
73
- fieldDefinition?.objectId,
74
- instance?.id,
75
- fieldDefinition?.relatedPropertyId,
76
- 'InitialMiddleObjectInstances',
77
- ],
78
- queryFn: () => fetchInitialMiddleObjectInstances(apiServices, fieldDefinition, instance?.id),
79
- staleTime: Infinity,
80
- enabled: !!(fieldDefinition?.objectId &&
81
- instance?.id &&
82
- fieldDefinition?.type === 'collection' &&
83
- fieldDefinition?.manyToManyPropertyId &&
84
- fieldDefinition?.relatedPropertyId),
85
- meta: {
86
- errorMessage: 'Failed to fetch middle object instances: ',
87
- },
88
- });
61
+ useEffect(() => {
62
+ if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
63
+ fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
64
+ }
65
+ }, [fieldDefinition, instance]);
89
66
  if (associatedObject?.propertyId === entryId)
90
67
  return null;
91
68
  // If the entry is hidden, clear its value and any nested values, and skip rendering
@@ -93,9 +70,7 @@ export function RecursiveEntryRenderer(props) {
93
70
  return null;
94
71
  }
95
72
  if (entry.type === 'content') {
96
- return (React.createElement(Box, { dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(entry.html) }, sx: {
97
- fontFamily: 'Roboto, Helvetica, Arial, sans-serif',
98
- } }));
73
+ return React.createElement(HtmlView, { value: entry.html });
99
74
  }
100
75
  else if ((entry.type === 'input' || entry.type === 'readonlyField' || entry.type === 'inputField') &&
101
76
  fieldDefinition) {
@@ -110,9 +85,7 @@ export function RecursiveEntryRenderer(props) {
110
85
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
111
86
  React.createElement(ObjectPropertyInput, { relatedObjectId: !fieldDefinition.objectId ? display?.relatedObjectId : fieldDefinition.objectId, fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: 'criteria' in validation && validation.criteria
112
87
  ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
113
- : entry.display?.criteria
114
- ? updateCriteriaInputs(entry.display?.criteria, getValues(), userAccount, instance)
115
- : undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
88
+ : undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
116
89
  ? display?.defaultValue.sortBy
117
90
  : undefined, orderBy: typeof display?.defaultValue === 'object' && 'orderBy' in display.defaultValue
118
91
  ? display?.defaultValue.orderBy
@@ -126,11 +99,12 @@ export function RecursiveEntryRenderer(props) {
126
99
  }
127
100
  else if (fieldDefinition.type === 'collection') {
128
101
  if (fieldDefinition?.manyToManyPropertyId) {
129
- if (!isEmpty(middleObject)) {
130
- return isLoadingInstances ? (React.createElement(Skeleton, null)) : (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
131
- React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: 'criteria' in validation && validation.criteria
102
+ if (middleObject && !isEmpty(middleObject)) {
103
+ return (initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
104
+ React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
105
+ initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: 'criteria' in validation && validation.criteria
132
106
  ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
133
- : undefined, hasDescription: !!display?.description })));
107
+ : undefined, hasDescription: !!display?.description }))));
134
108
  }
135
109
  else {
136
110
  // when in the builder preview, the middle object won't be fetched so instead show an empty field
@@ -160,7 +134,7 @@ export function RecursiveEntryRenderer(props) {
160
134
  }
161
135
  else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
162
136
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
163
- React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
137
+ React.createElement(Document, { id: entryId, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
164
138
  }
165
139
  else if (fieldDefinition.type === 'criteria') {
166
140
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -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
- };
@@ -1,10 +1,10 @@
1
1
  /// <reference types="react" />
2
- import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, ObjWithRoot, PanelViewEntry, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
2
+ import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
3
3
  import { LocalDateTime } from '@js-joda/core';
4
4
  import { FieldErrors, FieldValues } from 'react-hook-form';
5
5
  import { ObjectProperty } from '../../../../types';
6
6
  import { AutocompleteOption } from '../../../core';
7
- import { DocumentReference, FileUploadBatchResult, InstanceLink } from './types';
7
+ import { InstanceLink, SavedDocumentReference } from './types';
8
8
  export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
9
9
  export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
10
10
  export declare function isAddressProperty(key: string): boolean;
@@ -41,8 +41,7 @@ export declare function getDefaultPages(parameters: InputParameter[], defaultPag
41
41
  [x: string]: string;
42
42
  }>;
43
43
  export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount, instance?: Record<string, unknown>): Record<string, unknown>;
44
- export declare const fetchMiddleObject: (fieldDefinition: InputParameter | Property, apiServices: ApiServices) => Promise<ObjWithRoot | undefined>;
45
- export declare const fetchInitialMiddleObjectInstances: (apiServices: ApiServices, fieldDefinition: InputParameter | Property, instanceId: string) => Promise<ObjectInstance[]>;
44
+ export declare function fetchCollectionData(apiServices: ApiServices, fieldDefinition: InputParameter | Property, setFetchedOptions: (newData: FieldValues) => void, instanceId?: string, fetchedOptions?: Record<string, unknown>, initialMiddleObjectInstances?: ObjectInstance[]): Promise<void>;
46
45
  export declare const getErrorCountForSection: (section: Section | Column, errors?: FieldErrors) => number;
47
46
  export declare const propertyToParameter: (property: Property) => InputParameter;
48
47
  export declare const propertyValidationToParameterValidation: (property: Property) => InputParameter['validation'];
@@ -50,13 +49,16 @@ export declare const convertPropertiesToParams: (object: Obj) => InputParameter[
50
49
  export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
51
50
  export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
52
51
  export declare const docProperties: Property[];
53
- export declare const uploadFiles: (files: (File | DocumentReference)[], apiServices: ApiServices, actionId?: string, linkTo?: InstanceLink, shortCircuit?: boolean) => Promise<FileUploadBatchResult>;
52
+ /**
53
+ * Upload files using the POST /files endpoint for sys__file objects
54
+ */
55
+ export declare const uploadFiles: (files: (File | SavedDocumentReference)[], apiServices: ApiServices, actionId?: string, metadata?: Record<string, string>, linkTo?: InstanceLink) => Promise<SavedDocumentReference[]>;
54
56
  /**
55
57
  * Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
56
58
  * This is used after instance creation when the instance ID becomes available
57
59
  */
58
- export declare const createFileLinks: (fileReferences: DocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
59
- export declare const uploadDocuments: (files: (File | DocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<DocumentReference[]>;
60
+ export declare const createFileLinks: (files: SavedDocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
61
+ export declare const uploadDocuments: (files: (File | SavedDocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<SavedDocumentReference[]>;
60
62
  export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
61
63
  showAlert: boolean;
62
64
  message?: string;
@@ -82,10 +84,9 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
82
84
  }>>, associatedObject?: {
83
85
  instanceId: string;
84
86
  propertyId: string;
85
- objectId?: string;
86
87
  }, parameters?: InputParameter[]): Promise<FieldValues>;
87
88
  export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
88
- export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
89
+ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[];
89
90
  /**
90
91
  * Converts a plain text string to RTF format suitable for a RichTextEditor.
91
92
  *
@@ -99,6 +100,5 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | Pa
99
100
  * This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
100
101
  */
101
102
  export declare function plainTextToRtf(plainText: string): string;
102
- export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
103
+ export declare function getFieldDefinition(entry: FormEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
103
104
  export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
104
- export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
@@ -1,5 +1,4 @@
1
1
  import { LocalDateTime } from '@js-joda/core';
2
- import { useQuery } from '@tanstack/react-query';
3
2
  import { jsonLogic } from 'json-logic-js-graphql';
4
3
  import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
5
4
  import { DateTime } from 'luxon';
@@ -116,27 +115,9 @@ export const getEntryId = (entry) => {
116
115
  ? entry.input.id
117
116
  : undefined;
118
117
  };
119
- const getEntryType = (entry, parameters) => {
120
- if (entry?.type === 'inputField') {
121
- return entry?.input?.type;
122
- }
123
- else if (entry?.type === 'input') {
124
- // For 'input' type entries, look up the parameter by parameterId
125
- const parameter = parameters?.find((param) => param.id === entry.parameterId);
126
- return parameter?.type;
127
- }
128
- };
129
118
  export function getPrefixedUrl(url) {
130
119
  const wcsMatchers = ['/apps', '/pages', '/widgets'];
131
- const dataMatchers = [
132
- '/objects',
133
- '/correspondenceTemplates',
134
- '/documents',
135
- '/payments',
136
- '/forms',
137
- '/locations',
138
- '/files',
139
- ];
120
+ const dataMatchers = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/forms', '/locations'];
140
121
  const signalrMatchers = ['/hubs'];
141
122
  const accessManagementMatchers = ['/users'];
142
123
  const workflowMatchers = ['/workflows'];
@@ -351,19 +332,31 @@ export function updateCriteriaInputs(criteria, data, user, instance) {
351
332
  },
352
333
  }));
353
334
  }
354
- export const fetchMiddleObject = async (fieldDefinition, apiServices) => {
355
- return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
356
- params: {
357
- filter: { fields: ['properties', 'actions', 'rootObjectId'] },
358
- },
359
- });
360
- };
361
- export const fetchInitialMiddleObjectInstances = async (apiServices, fieldDefinition, instanceId) => {
362
- const filter = getMiddleObjectFilter(fieldDefinition, instanceId);
363
- return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
364
- params: { filter: JSON.stringify(filter) },
365
- });
366
- };
335
+ export async function fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instanceId, fetchedOptions, initialMiddleObjectInstances) {
336
+ try {
337
+ if ((fetchedOptions && !fetchedOptions[`${fieldDefinition.id}MiddleObject`]) || !fetchedOptions) {
338
+ const fetchedMiddleObject = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
339
+ params: {
340
+ filter: { fields: ['properties', 'actions', 'rootObjectId'] },
341
+ },
342
+ });
343
+ setFetchedOptions({ [`${fieldDefinition.id}MiddleObject`]: fetchedMiddleObject });
344
+ }
345
+ // Fetch the initial middle object instances
346
+ const filter = instanceId ? getMiddleObjectFilter(fieldDefinition, instanceId) : {};
347
+ if (!isArray(initialMiddleObjectInstances)) {
348
+ const fetchedInitialMiddleObjectInstances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
349
+ params: { filter: JSON.stringify(filter) },
350
+ });
351
+ setFetchedOptions({
352
+ [`${fieldDefinition.id}InitialMiddleObjectInstances`]: fetchedInitialMiddleObjectInstances,
353
+ });
354
+ }
355
+ }
356
+ catch (error) {
357
+ console.error('Error fetching collection data:', error);
358
+ }
359
+ }
367
360
  export const getErrorCountForSection = (section, errors) => {
368
361
  const entries = section.entries || [];
369
362
  return isArray(section.entries)
@@ -476,90 +469,53 @@ export const docProperties = [
476
469
  type: 'string',
477
470
  },
478
471
  ];
479
- export const uploadFiles = async (files, apiServices, actionId = '_create', linkTo, shortCircuit = true) => {
472
+ /**
473
+ * Upload files using the POST /files endpoint for sys__file objects
474
+ */
475
+ export const uploadFiles = async (files, apiServices, actionId = '_create', metadata, linkTo) => {
480
476
  // Separate already uploaded files from files that need uploading
481
477
  const alreadyUploaded = files.filter((file) => !('size' in file));
482
478
  const filesToUpload = files.filter((file) => 'size' in file);
483
- let failedUpload = false;
484
- // Upload all files in parallel, handling each result individually
485
- const uploadPromises = [];
486
- for (const file of filesToUpload) {
479
+ // Upload all files in parallel
480
+ const uploadPromises = filesToUpload.map(async (file) => {
487
481
  const formData = new FormData();
488
482
  formData.append('file', file);
489
483
  formData.append('actionId', actionId);
490
484
  formData.append('objectId', 'sys__file');
491
- formData.append('input', JSON.stringify({ name: file.name, contentType: file.type }));
485
+ if (metadata) {
486
+ for (const [key, value] of Object.entries(metadata)) {
487
+ formData.append(key, value);
488
+ }
489
+ }
492
490
  if (linkTo) {
493
491
  formData.append('linkTo', JSON.stringify(linkTo));
494
492
  }
495
- const uploadPromise = (async () => {
496
- try {
497
- const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
498
- return {
499
- id: fileInstance.id,
500
- name: fileInstance.name,
501
- unsaved: true,
502
- };
503
- }
504
- catch (error) {
505
- console.error(`Failed to upload file ${file.name}:`, error);
506
- failedUpload = true;
507
- }
508
- })();
509
- uploadPromises.push(uploadPromise);
510
- }
511
- if (!shortCircuit) {
512
- // Wait for all upload attempts to complete (successes and failures)
513
- const uploadResults = await Promise.allSettled(uploadPromises);
514
- const uploadedFiles = uploadResults
515
- .filter((result) => result.status === 'fulfilled' && !!result.value)
516
- .map((result) => result.value);
517
- const failedCount = filesToUpload.length - uploadedFiles.length;
493
+ const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
518
494
  return {
519
- successfulUploads: [...alreadyUploaded, ...uploadedFiles],
520
- errorMessage: failedCount > 0 ? `Failed to upload ${failedCount} file(s)` : undefined,
495
+ id: fileInstance.id,
496
+ name: fileInstance.name,
521
497
  };
522
- }
498
+ });
523
499
  const uploadedFiles = await Promise.all(uploadPromises);
524
- if (failedUpload) {
525
- return {
526
- successfulUploads: [],
527
- errorMessage: 'An error occurred when uploading files',
528
- };
529
- }
530
- return {
531
- successfulUploads: [...alreadyUploaded, ...uploadedFiles],
532
- };
500
+ return [...alreadyUploaded, ...uploadedFiles];
533
501
  };
534
502
  /**
535
503
  * Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
536
504
  * This is used after instance creation when the instance ID becomes available
537
505
  */
538
- export const createFileLinks = async (fileReferences, linkedInstance, apiServices) => {
539
- // Link files in parallel, handling each result individually
540
- const linkPromises = [];
541
- for (const file of fileReferences) {
542
- const linkPromise = (async () => {
543
- try {
544
- await apiServices.post(getPrefixedUrl(`/objects/sys__fileLink/instances`), {
545
- name: 'File Link',
546
- file: { id: file.id, name: file.name },
547
- linkedInstance,
548
- }, {
549
- params: {
550
- actionId: '_create',
551
- },
552
- });
553
- }
554
- catch (error) {
555
- console.error(`Failed to create file link for ${file.name}:`, error);
556
- // The file remains unlinked and can be retried later
557
- }
558
- })();
559
- linkPromises.push(linkPromise);
560
- }
561
- // Wait for all linking attempts to complete (successes and failures)
562
- await Promise.allSettled(linkPromises);
506
+ export const createFileLinks = async (files, linkedInstance, apiServices) => {
507
+ const linkPromises = files.map(async (file) => {
508
+ await apiServices.post(getPrefixedUrl(`/objects/sys__fileLink/instances`), {
509
+ name: 'File Link',
510
+ file: { id: file.id, name: file.name },
511
+ linkedInstance,
512
+ }, {
513
+ params: {
514
+ actionId: '_create',
515
+ },
516
+ });
517
+ });
518
+ await Promise.all(linkPromises);
563
519
  };
564
520
  export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
565
521
  const allDocuments = [];
@@ -587,29 +543,20 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
587
543
  export const deleteDocuments = async (submittedFields, requestSuccess, apiServices, object, instance, action, setSnackbarError) => {
588
544
  const documentProperties = action?.parameters
589
545
  ? action.parameters.filter((param) => ['document', 'file'].includes(param.type))
590
- : object.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
546
+ : object?.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
591
547
  for (const docProperty of documentProperties ?? []) {
592
548
  const savedValue = submittedFields[docProperty.id];
593
- const originalValue = instance[docProperty.id];
549
+ const originalValue = instance?.[docProperty.id];
594
550
  const documentsToRemove = requestSuccess
595
551
  ? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
596
552
  : (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
597
553
  for (const doc of documentsToRemove) {
598
554
  try {
599
- // Build context for state model
600
- const fieldType = docProperty.type === 'file' ? 'file' : 'document';
601
- if (fieldType === 'file') {
602
- // For file properties, unlink the file. Don't delete the actual file
603
- // since other instances may be using it.
604
- await apiServices.post(getPrefixedUrl(`/files/${doc.id}/unlinkInstance`), {
605
- linkedInstanceId: instance.id,
606
- linkedObjectId: object.id,
607
- });
608
- }
609
- else {
610
- // For document properties, delete the document
611
- await apiServices.delete(getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`));
612
- }
555
+ // Use different endpoints based on property type
556
+ const deleteEndpoint = docProperty.type === 'file'
557
+ ? getPrefixedUrl(`/files/${doc.id}`)
558
+ : getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`);
559
+ await apiServices?.delete(deleteEndpoint);
613
560
  }
614
561
  catch (error) {
615
562
  if (error) {
@@ -624,28 +571,16 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
624
571
  }
625
572
  }
626
573
  };
627
- async function handleUploads(files, propertyType, entry, apiServices, objectId, instanceId) {
628
- if (propertyType === 'file') {
629
- // Get the createActionId from display options, default to '_create'
630
- const createActionId = entry?.display?.createActionId ?? '_create';
631
- return await uploadFiles(files, apiServices, createActionId, instanceId ? { id: instanceId, objectId } : undefined);
632
- }
633
- else if (propertyType === 'document' && instanceId) {
634
- try {
635
- const docs = await uploadDocuments(files, {
636
- type: '',
637
- view_permission: '',
638
- ...entry?.documentMetadata,
639
- }, apiServices, instanceId, objectId);
640
- return { successfulUploads: docs };
641
- }
642
- catch (error) {
643
- console.error('Error uploading documents:', error);
644
- return { successfulUploads: [], errorMessage: 'Error uploading documents' };
645
- }
646
- }
647
- return { successfulUploads: [] };
648
- }
574
+ const getEntryType = (entry, parameters) => {
575
+ if (entry?.type === 'inputField') {
576
+ return entry.input.type;
577
+ }
578
+ else if (entry?.type === 'input') {
579
+ // For 'input' type entries, look up the parameter by parameterId
580
+ const parameter = parameters?.find((param) => param.id === entry.parameterId);
581
+ return parameter?.type;
582
+ }
583
+ };
649
584
  /**
650
585
  * Transforms a form submission into a format safe for API submission.
651
586
  *
@@ -663,45 +598,45 @@ export async function formatSubmission(submission, apiServices, objectId, instan
663
598
  if (associatedObject) {
664
599
  delete submission[associatedObject.propertyId];
665
600
  }
666
- const allEntries = getUnnestedEntries(form?.entries ?? []);
601
+ const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
667
602
  for (const [key, value] of Object.entries(submission)) {
668
603
  const entry = allEntries?.find((entry) => getEntryId(entry) === key);
669
604
  if (isArray(value)) {
670
- const propertyType = getEntryType(entry, parameters);
671
- // The only array types we need to handle specially are 'file' and 'document'.
672
- if (propertyType !== 'file' && propertyType !== 'document') {
673
- continue;
674
- }
675
- // Only upload if array contains File instances (not SavedDocumentReference).
605
+ // Only upload if array contains File instances (not SavedDocumentReference)
676
606
  const fileInArray = value.some((item) => item instanceof File);
677
607
  if (fileInArray && apiServices && objectId) {
678
- const result = await handleUploads(value, propertyType ?? '', entry, apiServices, objectId, instanceId);
679
- if (result.errorMessage) {
680
- setSnackbarError?.({
681
- showAlert: true,
682
- // Provide generic message since we're ignoring a partial upload.
683
- message: `An error occurred while uploading associated ${propertyType === 'file' ? 'files' : 'documents'}`,
684
- isError: true,
685
- });
686
- return submission;
608
+ // Determine property type from the entry
609
+ const parameterType = getEntryType(entry, parameters);
610
+ try {
611
+ let uploadedDocuments = [];
612
+ if (parameterType === 'file') {
613
+ uploadedDocuments = await uploadFiles(value, apiServices, '_create', {
614
+ ...entry?.documentMetadata,
615
+ },
616
+ // Only pass linkTo if instanceId exists (update action)
617
+ instanceId ? { id: instanceId, objectId } : undefined);
618
+ }
619
+ else if (parameterType === 'document' && instanceId) {
620
+ uploadedDocuments = await uploadDocuments(value, {
621
+ type: '',
622
+ view_permission: '',
623
+ ...entry?.documentMetadata,
624
+ }, apiServices, instanceId, objectId);
625
+ }
626
+ submission[key] = uploadedDocuments;
687
627
  }
688
- else {
689
- // Filter out 'unsaved' flag before submission since the api doesn't know about it.
690
- submission[key] = result.successfulUploads.map((file) => ({ id: file.id, name: file.name }));
628
+ catch (err) {
629
+ if (err) {
630
+ setSnackbarError &&
631
+ setSnackbarError({
632
+ showAlert: true,
633
+ message: `An error occurred while uploading associated ${parameterType === 'file' ? 'files' : 'documents'}`,
634
+ isError: true,
635
+ });
636
+ }
637
+ return submission;
691
638
  }
692
639
  }
693
- else {
694
- // Filter out 'unsaved' flag before submission since the api doesn't know about it.
695
- submission[key] = value
696
- // This should never happen but it's possible that the else branch
697
- // is reached because either 'apiServices' or 'objectId' is undefined.
698
- // If that's the case the submission will fail if we submit File blobs.
699
- .filter((file) => !(file instanceof File))
700
- .map((file) => ({
701
- id: file.id,
702
- name: file.name,
703
- }));
704
- }
705
640
  // if there are address fields with no value address needs to be set to undefined to be able to submit
706
641
  }
707
642
  else if (typeof value === 'object' && value !== null) {
@@ -714,8 +649,7 @@ export async function formatSubmission(submission, apiServices, objectId, instan
714
649
  submission[key] =
715
650
  entry &&
716
651
  ['input', 'inputField'].includes(entry.type) &&
717
- (entry.display?.relatedObjectId ||
718
- associatedObject?.objectId)
652
+ entry.display?.relatedObjectId
719
653
  ? pick(value, 'id', 'name', 'objectId')
720
654
  : pick(value, 'id', 'name');
721
655
  }
@@ -947,14 +881,3 @@ function applyMaskToObfuscatedValue(value, mask) {
947
881
  }
948
882
  return maskedValue;
949
883
  }
950
- export function useFormById(formId, apiServices, errorMessage) {
951
- return useQuery({
952
- queryKey: ['form', formId],
953
- enabled: formId !== '_auto_' && !!formId,
954
- staleTime: Infinity,
955
- queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}`)),
956
- meta: {
957
- errorMessage,
958
- },
959
- });
960
- }